diff --git a/.gitignore b/.gitignore index 4e96fc2..2c71750 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ pastebin -config.json +*.db diff --git a/Makefile b/Makefile index f46238a..7825d2c 100644 --- a/Makefile +++ b/Makefile @@ -2,18 +2,26 @@ .PHONY: all test clean build install GOFLAGS ?= $(GOFLAGS:) +dbtype=$(shell grep dbtype config.json | cut -d \" -f 4) +dbname=$(shell grep dbname config.json | cut -d \" -f 4) +dbtable=$(shell grep dbtable config.json | cut -d \" -f 4) all: clean install build build: + gofmt -w pastebin.go go build $(GOFLAGS) ./... +ifeq ($(dbtype),sqlite3) + cat database.sql | sed 's/pastebin/$(dbtable)/' | sqlite3 $(dbname) +endif install: go get github.com/dchest/uniuri go get github.com/ewhal/pygments - go get github.com/go-sql-driver/mysql + go get github.com/mattn/go-sqlite3 go get github.com/gorilla/mux - go get github.com/ChannelMeter/iso8601duration + go get github.com/go-sql-driver/mysql + go get github.com/lib/pq test: install go install $(GOFLAGS) ./... @@ -23,6 +31,5 @@ bench: install clean: go clean $(GOFLAGS) -i ./... - rm -rf ./build - - + rm -rf ./build + rm -rf pastebin.db diff --git a/assets/clone.html b/assets/clone.html deleted file mode 100644 index 3a21e21..0000000 --- a/assets/clone.html +++ /dev/null @@ -1,505 +0,0 @@ - - - - - - - - {{.Title}} - - - - - - - - - - - - - - - - -
- -
-
-
-
- - Paste Title -
- -
- - Paste your text here -
-
-
-
-
- - - - - - - Home - Download - Raw - Clone - -
-
-
-
-
-
- $ <command> | curl -X POST -F 'p=<-' https://p.pantsu.cat/api
- POST https://p.pantsu.cat/api
- GET https://p.pantsu.cat/api/{PASTE}
- GET: https://p.pantsu.cat/p/(PASTE)/(lang)
- $ curl -X DEL -F 'delkey=' https://p.pantsu.cat/api
-

Source: Github

-

Tools: Paste.sh

-
- -
- - - - - - - - - - diff --git a/assets/index.html b/assets/index.html index aaa134b..3d8e81e 100644 --- a/assets/index.html +++ b/assets/index.html @@ -1,498 +1,184 @@ - - - - - - Pantsu Paste + + + - - - - + {{ .Title }} - + + + + + + - + + + + + - - - - - -
- -
-
-
-
- - Paste Title -
-
- - Paste your text here -
-
-
-
-
- - + +
+ - - -
-
-
-
-
-
- $ <command> | curl -X POST -F 'p=<-' https://p.pantsu.cat/api
- POST https://p.pantsu.cat/api
- GET https://p.pantsu.cat/api/{PASTE}
- GET: https://p.pantsu.cat/p/(PASTE)/(lang)
- $ curl -X DEL -F 'delkey=' https://p.pantsu.cat/api
-

Source: Github

-

Tools: Paste.sh

-
-
- - - - - - +
+
+ + Paste Title +
- + + - +
  • + + {{ range $key, $value := .LangsFirst }} + + {{ end }} + +
  • + + {{ range $key, $value := .LangsLast }} + + {{ end }} + +
    + + +
    + +
    + Forever + +
    +
    + +
    + +
    + Help +
    +
    + +
    + +
    + +
    +
    + + + + + + + + + + + + + + + diff --git a/assets/paste.html b/assets/paste.html deleted file mode 100644 index 6cbf536..0000000 --- a/assets/paste.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - {{.Title}} - - - - - - - - - - - - - - - - -
    - - -
    - -
    - -
    -
    - Home - Download - Raw - Clone -
    - -
    -
    - - - - - - - - - - diff --git a/assets/pastebin.css b/assets/pastebin.css new file mode 100644 index 0000000..02b53f7 --- /dev/null +++ b/assets/pastebin.css @@ -0,0 +1,218 @@ + + +/* * * +/* The labels around the paste-box */ + +#title{ + margin-bottom: -3px; +} + +.urlshortener, .expiry_label{ + font-size : 11px; +} + +.expiry_label{ + float : right; +} + +.expiry_date{ + font-weight : bold +} + + + + +/* * * +/* The actual paste-box */ + +pre { + background : none; + border : none; + border-radius : 0px; + font-family : 'Courier New', Courier, monospace; + font-size : 12px; + font-weight : normal; + letter-spacing : 0.015em; + line-height : 14px; + margin : 5px 5px 5px 5px; + min-height : 250px; +} + +.code-row { + display : block; + font-size : 14px; + font-weight : lighter; +} + +.codenum-row { + display : block; + font-size : 14px; + font-weight : lighter; +} + +.code{ + width : 10000px; +} + +.highlight{ + border : solid 1px #ccc; + border-radius : 6px; +} + +.highlighttable{ + margin-bottom : 8px; +} + +.form-no-margin{ + margin-top: 0px; +} + +.linenodiv { + background : none; + border : solid 1px #ccc; + border-radius : 6px; + display : none; + padding-right : 0px!important; +} + +.toggles{ + width : 100px; +} + +label.togglerows{ + text-align : left!important; + cursor : default!important; +} + +.text{ + background : #f0f3f3; + font-size : 10px; + line-height : 150%; + margin-bottom : 10px; + padding-left : 14px; + padding-top : 7px; +} + +.well{ + font-size : 11px; + padding-top : 25px!important; + /*padding-top: 10px; resize: both;"*/ +} + +div#paste.well{ + border-radius : 6px; +} + + + + +/* * * +/* Button row under paste box */ + +label { + font-weight: bold!important; +} + +.btn { + font-size : 12px; +} + +.paste-actions{ + margin-bottom : 20px; + margin-right : 0px; +} + +.paste-actions .control-label{ + display : inline-block; + padding-right : 10px; + vertical-align : -webkit-baseline-middle; +} + +.paste-actions .btn{ + width : 230px; +} + +.paste-actions .pull-right .btn{ + width : 120px; +} + +#button-language{ + text-align : left; +} + +#dropdown-language, #dropdown-style, #dropdown-expiry{ + bottom : 100% !important; + top : auto !important; +} + +#dropdown-language{ + width : 230px; +} + +#button-style, #button-expiry{ + width : 160px; + text-align: left; +} + +#button-help{ + width : 140px; + text-align: left; +} + +#dropdown-style{ + width : 110px; +} + +#toggle-numbers, #toggle-hover-rows{ + margin-top : 25px; +} + +div.row { + margin-right : 0px; +} + +.dropdown-scrollbar { + height : auto; + max-height : 450px; + overflow-x : hidden; + overflow-y : auto; +} + +.dropdown-scrollbar::-webkit-scrollbar { width : 10px; background-color : #f7f7f7;} +.dropdown-scrollbar::-webkit-scrollbar-thumb { background-color : #ccc; border-radius : 4px;} +.dropdown-scrollbar::-webkit-scrollbar-thumb:hover { background-color : #aaa;} + +.dropdown-item:hover{ + background-color : #f7f7f7; +} + +.dropdown-item{ + cursor : pointer; +} + +.dropdown-label{ + font-size : 12px; + font-weight : bold; + padding-left : 10px; +} + +.swal-wide{ + width:850px !important; + margin-left: -400px; +} + +.swal-code { + display: block; + font-size: 14px; + margin-left: 8px; + text-align: left; +} + +.swal-bold { + font-weight: bold; + margin-bottom: 5px; + margin-top: 10px; + text-align: left; + display: block; + margin-left: 8px; +} diff --git a/assets/prio-lexers b/assets/prio-lexers new file mode 100644 index 0000000..7d7e38f --- /dev/null +++ b/assets/prio-lexers @@ -0,0 +1,25 @@ +Bash +C +C# +C++ +CMake +CSS +Clojure +CoffeeScript +Diff +ERB +EmacsLisp +Go +HTML +HTTP +Java +JavaScript +Makefile +MySQL +PHP +Perl +Python +Python 3 +Ruby +XML +YAML diff --git a/assets/syntax.html b/assets/syntax.html index 5da999b..c7bf8f9 100644 --- a/assets/syntax.html +++ b/assets/syntax.html @@ -2,59 +2,254 @@ - - - %s + + {{.Title}} - - - - - - + + +
    -
    -
    - %s -
    +

    {{.Title}}

    +
    +   +
    -
    -
    - Home - Download - Raw - Clone -
    - -
    -
    + This paste expires : + {{.Expiry}} + +
    + +
    {{ .Body }} + {{.WrapperErr}} +
    + +
    +
    + +
    + {{.Lang}} + +
    +
    + +
    + +
    + {{.Style}} + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    + +
    + Home + Download + Raw + Clone +
    +
    + - + + - + diff --git a/config.example.json b/config.example.json deleted file mode 100644 index 0a8bff4..0000000 --- a/config.example.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Port": ":8080", - "Length": 6, - "Username": "", - "Password": "", - "Name": "", - "Address": "https://p.pantsu.cat" - -} - diff --git a/config.json b/config.json new file mode 100644 index 0000000..309eabd --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "address": "http://localhost:9999", + "dbhost": "", + "dbname": "pastebin.db", + "dbtable": "pastebin", + "dbtype": "sqlite3", + "dbport": "", + "dbuser":"", + "dbpassword":"", + "displayname": "MyCompany", + "listenaddress": "localhost", + "listenport": "9999", + "shorturllength": "5", + "highlighter":"./highlighter-wrapper.py", + "googleAPIKey":"insert-if-you-want-goo.gl/addr" +} diff --git a/database.sql b/database.sql index 3adb90b..4574d6e 100644 --- a/database.sql +++ b/database.sql @@ -1,9 +1,9 @@ CREATE TABLE `pastebin` ( `id` varchar(30) NOT NULL, - `title` char(20) default NULL, + `title` varchar(50) default NULL, `hash` char(40) default NULL, `data` longtext, `delkey` char(40) default NULL, - `expiry` DATETIME, + `expiry` int, PRIMARY KEY (`id`) ); diff --git a/highlighter-wrapper.py b/highlighter-wrapper.py new file mode 100755 index 0000000..ac9f55e --- /dev/null +++ b/highlighter-wrapper.py @@ -0,0 +1,106 @@ +#!/usr/bin/python + +import pygments +from pygments import highlight +from pygments.lexers import get_lexer_by_name, guess_lexer +from pygments.formatters import HtmlFormatter +import sys + +def render(code, lang, theme): + + guess = "" + lang_org = lang + + try: + lexer = get_lexer_by_name(lang) + except: + try: + guess = 1 + lexer = guess_lexer(code) + lang = lexer.aliases[0] + except: + if lang == "autodetect": + out = "Could not autodetect language (returning plain text).\n" + else: + out = "Given language was not found :: '"+lang+"' (returning plain text).\n" + + lexer = get_lexer_by_name("text") + html_format = HtmlFormatter(style=theme, noclasses="true", linenos="true", encoding="utf-8") + return highlight(code, lexer, html_format),out + + if guess: + out = "Lexer guessed :: "+lang + if lang != lang_org and lang_org != "autodetect": + out += " (although given language was "+lang_org+") " + else: + out = "Successfully used lexer for given language :: "+lang + + try: + html_format = HtmlFormatter(style=theme, noclasses="true", linenos="true", encoding="utf-8") + except: + html_format = HtmlFormatter(noclasses="true", linenos="true", encoding="utf-8") + + return highlight(code, lexer, html_format),out + + + +def usage(err=0): + print("\n Description, \n") + print(" - This is a small wrapper for the pygments html-formatter.") + print(" It will read data on stdin and simply print it on stdout") + + print("\n Usage, \n") + print(" - %s [lang] [style] < FILE" % sys.argv[0]) + print(" - %s getlexers" % sys.argv[0]) + print(" - %s getstyles" % sys.argv[0]) + + print("\n Where, \n") + print(" - lang is the language of your code") + print(" - style is the 'theme' for the formatter") + print(" - getlexers will print available lexers (displayname;lexer-name)") + print(" - getstyles will print available styles \n") + + sys.exit(err) + +def get_styles(): + item = pygments.styles.get_all_styles() + for items in item: + print items + sys.exit(0) + +def get_lexers(): + item = pygments.lexers.get_all_lexers() + for items in item: + print items[0]+";"+items[1][0] + sys.exit(0) + + +# " Main " + +code = "" + +if len(sys.argv) >= 2: + for arg in sys.argv: + if arg == '--help' or arg == '-h': + usage() + if arg == 'getlexers': + get_lexers() + if arg == 'getstyles': + get_styles() + +if len(sys.argv) == 3: + lang = sys.argv[1] + theme = sys.argv[2] +else: + usage(1); + +if not sys.stdin.isatty(): + for line in sys.stdin: + code += line + + out, stderr = render(code, lang, theme) + print(out) + sys.stderr.write(stderr) +else: + print("err : No data on stdin.") + sys.exit(1) diff --git a/pastebin.go b/pastebin.go index 423c9cf..58dad70 100644 --- a/pastebin.go +++ b/pastebin.go @@ -2,198 +2,478 @@ package main import ( + "bufio" + "bytes" "crypto/sha1" - "database/sql" "encoding/base64" "encoding/json" "fmt" "html" "html/template" "io" - "io/ioutil" "log" "net/http" "os" + "os/exec" + "strconv" + "strings" "time" - duration "github.com/ChannelMeter/iso8601duration" - // uniuri is used for easy random string generation + // Random string generation, "github.com/dchest/uniuri" - // pygments is used for syntax highlighting - "github.com/ewhal/pygments" - // mysql driver + + // Database drivers, + "database/sql" _ "github.com/go-sql-driver/mysql" - // mux is used for url routing + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + + // For url routing "github.com/gorilla/mux" ) +// Configuration struct, type Configuration struct { - // ADDRESS that pastebin will return links for - Address string - // LENGTH of paste id - Length int - // PORT that pastebin will listen on - Port string - // USERNAME for database - Username string - // PASS database password - Password string - // NAME database name - Name string + Address string `json:"address"` // Url to to the pastebin + DBHost string `json:"dbhost"` // Name of your database host + DBName string `json:"dbname"` // Name of your database + DBPassword string `json:"dbpassword"` // The password for the database user + DBPlaceHolder [6]string // ? / $[i] Depending on db driver. + DBPort string `json:"dbport"` // Port of the database + DBTable string `json:"dbtable"` // Name of the table in the database + DBType string `json:"dbtype"` // Type of database + DBUser string `json:"dbuser"` // The database user + DisplayName string `json:"displayname"` // Name of your pastebin + GoogleAPIKey string `json:"googleapikey"` // Your google api key + Highlighter string `json:"highlighter"` // The name of the highlighter. + ListenAddress string `json:"listenaddress"` // Address that pastebin will bind on + ListenPort string `json:"listenport"` // Port that pastebin will listen on + ShortUrlLength int `json:"shorturllength,string"` // Length of the generated short urls } -var configuration Configuration - -// DATABASE connection String -var DATABASE string - -// Template pages -var templates = template.Must(template.ParseFiles("assets/paste.html", "assets/index.html", "assets/clone.html")) -var syntax, _ = ioutil.ReadFile("assets/syntax.html") - -// Response API struct +// This struct is used for responses. +// A request to the pastebin will always this json struct. type Response struct { - SUCCESS bool `json:"success"` - STATUS string `json:"status"` - ID string `json:"id"` - TITLE string `json:"title"` - SHA1 string `json:"sha1"` - URL string `json:"url"` - SIZE int `json:"size"` - DELKEY string `json:"delkey"` + DelKey string `json:"delkey"` // The id to use when delete a paste + Expiry string `json:"expiry"` // The date when post expires + Extra string `json:"extra"` // Extra output from the highlight-wrapper + Id string `json:"id"` // The id of the paste + Lang string `json:"lang"` // Specified language + Paste string `json:"paste"` // The eactual paste data + Sha1 string `json:"sha1"` // The sha1 of the paste + Size int `json:"size"` // The length of the paste + Status string `json:"status"` // A custom status message + Style string `json:"style"` // Specified style + Title string `json:"title"` // The title of the paste + Url string `json:"url"` // The url of the paste } -// Page generation struct +// This struct is used for indata when a request is being made to the pastebin. +type Request struct { + DelKey string `json:"delkey"` // The delkey that is used to delete paste + Expiry int64 `json:"expiry,string"` // An expiry date + Id string `json:"id"` // The id of the paste + Lang string `json:"lang"` // The language of the paste + Paste string `json:"paste"` // The actual pase + Style string `json:"style"` // The style of the paste + Title string `json:"title"` // The title of the paste + WebReq bool `json:"webreq"` // If its a webrequest or not +} + +// This struct is used for generating pages. type Page struct { - Title string - Body []byte - Raw string - Home string - Download string - Clone string + Body template.HTML + Expiry string + GoogleAPIKey string + Lang string + LangsFirst map[string]string + LangsLast map[string]string + PasteTitle string + Style string + SupportedStyles map[string]string + Title string + UrlAddress string + UrlClone string + UrlDownload string + UrlHome string + UrlRaw string + WrapperErr string } -// check error handling function -func Check(err error) { - if err != nil { - log.Println(err) +// Template pages, +var templates = template.Must(template.ParseFiles("assets/index.html", + "assets/syntax.html")) + +// Global variables, *shrug* +var configuration Configuration +var dbHandle *sql.DB +var debug bool +var debugLogger *log.Logger +var listOfLangsFirst map[string]string +var listOfLangsLast map[string]string +var listOfStyles map[string]string + +// +// Functions below, +// + +// loggy prints a message if the debug flag is turned. +func loggy(str string) { + if debug { + debugLogger.Println(" " + str) } } -// GenerateName uses uniuri to generate a random string that isn't in the -// database -func GenerateName() string { - // use uniuri to generate random string - // hardcode this for now until I figure out why json isn't parsing correctly - id := uniuri.NewLen(6) +// checkErr simply checks if passed error is anything but nil. +// If an error exists it will be printed and the program terminates. +func checkErr(err error) { + if err != nil { + debugLogger.Println(" " + err.Error()) + os.Exit(1) + } +} - db, err := sql.Open("mysql", DATABASE) - Check(err) - defer db.Close() - // query database if id exists and if it does call generateName again - query, err := db.Query("select id from pastebin where id=?", id) - if err != sql.ErrNoRows { - for query.Next() { - GenerateName() +// getSupportedStyless reads supported styles from the highlighter-wrapper +// (which in turn gets available styles from pygments). It then puts them into +// an array which is used by the html-template. The function doesn't return +// anything since the array is defined globally (shrug). +func getSupportedStyles() { + + listOfStyles = make(map[string]string) + + arg := "getstyles" + out, err := exec.Command(configuration.Highlighter, arg).Output() + if err != nil { + log.Fatal(err) + } + + // Loop lexers and add them to respectively map, + for _, line := range strings.Split(string(out), "\n") { + if line == "" { + continue } + + loggy(fmt.Sprintf("Populating supported styles map with %s", line)) + listOfStyles[line] = strings.Title(line) + } +} + +// getSupportedLangs reads supported lexers from the highlighter-wrapper (which +// in turn gets available lexers from pygments). It then puts them into two +// maps, depending on if it's a "prioritized" lexers. If it's prioritized or not +// is determined by if its listed in the assets/prio-lexers. The description is +// the key and the actual lexer is the value. The maps are used by the +// html-template. The function doesn't return anything since the maps are +// defined globally (shrug). +func getSupportedLangs() { + + var prioLexers map[string]string + + // Initialize maps, + prioLexers = make(map[string]string) + listOfLangsFirst = make(map[string]string) + listOfLangsLast = make(map[string]string) + + // Get prioritized lexers and put them in a separate map, + file, err := os.Open("assets/prio-lexers") + checkErr(err) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + prioLexers[scanner.Text()] = "1" + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + file.Close() + + arg := "getlexers" + out, err := exec.Command(configuration.Highlighter, arg).Output() + if err != nil { + log.Fatal(err) + } + + // Loop lexers and add them to respectively map, + for _, line := range strings.Split(string(out), "\n") { + if line == "" { + continue + } + + s := strings.Split(line, ";") + if len(s) != 2 { + loggy(fmt.Sprintf("Could not split '%v' from %s (fields should be seperated by ;)", + s, configuration.Highlighter)) + os.Exit(1) + } + s[0] = strings.Title(s[0]) + if prioLexers[s[0]] == "1" { + loggy(fmt.Sprintf("Populating first languages map with %s - %s", + s[0], s[1])) + listOfLangsFirst[s[0]] = s[1] + } else { + loggy(fmt.Sprintf("Populating second languages map with %s - %s", + s[0], s[1])) + listOfLangsLast[s[0]] = s[1] + } + } +} + +// printHelp prints a description of the program. +// Exit code will depend on how the function is called. +func printHelp(err int) { + + fmt.Printf("\n Description, \n") + fmt.Printf(" - This is a small (< 600 line of go) pastebing with") + fmt.Printf(" support for syntax highlightnig (trough python-pygments).\n") + fmt.Printf(" No more no less.\n\n") + + fmt.Printf(" Usage, \n") + fmt.Printf(" - %s [--help] [--debug]\n\n", os.Args[0]) + + fmt.Printf(" Where, \n") + fmt.Printf(" - help shows this incredibly useful help.\n") + fmt.Printf(" - debug shows quite detailed information about whats") + fmt.Printf(" going on.\n\n") + + os.Exit(err) +} + +// checkArgs parses the command line in a very simple manner. +func checkArgs() { + + if len(os.Args[1:]) >= 1 { + for _, arg := range os.Args[1:] { + switch arg { + case "-h", "--help": + printHelp(0) + case "-d", "--debug": + debug = true + default: + printHelp(1) + } + } + } +} + +// getDbHandle opens a connection to database. +// Returns the dbhandle if the open was successful +func getDBHandle() *sql.DB { + + var dbinfo string + for i := 0; i < 6; i++ { + configuration.DBPlaceHolder[i] = "?" + } + + switch configuration.DBType { + + case "sqlite3": + dbinfo = configuration.DBName + loggy("Specified databasetype : " + configuration.DBType) + loggy(fmt.Sprintf("Trying to open %s (%s)", + configuration.DBName, configuration.DBType)) + + case "postgres": + dbinfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + configuration.DBHost, + configuration.DBPort, + configuration.DBUser, + configuration.DBPassword, + configuration.DBName) + for i := 0; i < 6; i++ { + configuration.DBPlaceHolder[i] = "$" + strconv.Itoa(i+1) + } + + case "mysql": + dbinfo = configuration.DBUser + ":" + configuration.DBPassword + "@tcp(" + configuration.DBHost + ":" + configuration.DBPort + ")/" + configuration.DBName + + case "": + debugLogger.Println(" Database error : dbtype not specified in configuration.") + os.Exit(1) + + default: + debugLogger.Println(" Database error : Specified dbtype (" + + configuration.DBType + ") not supported.") + os.Exit(1) + } + + db, err := sql.Open(configuration.DBType, dbinfo) + checkErr(err) + + // Just create a dummy query to really verify that the database is working as + // expected, + var dummy string + err = db.QueryRow("select id from " + configuration.DBTable + " where id='dummyid'").Scan(&dummy) + + switch { + case err == sql.ErrNoRows: + loggy("Successfully connected and found table " + configuration.DBTable) + case err != nil: + debugLogger.Println(" Database error : " + err.Error()) + os.Exit(1) + } + + return db +} + +// generateName generates a short url with the length defined in main config +// The function calls itself recursively until an id that doesn't exist is found +// Returns the id +func generateName() string { + + // Use uniuri to generate random string + id := uniuri.NewLen(configuration.ShortUrlLength) + loggy(fmt.Sprintf("Generated id is '%s', checking if it's already taken in the database", + id)) + + // Query database if id exists and if it does call generateName again + var id_taken string + err := dbHandle.QueryRow("select id from "+configuration.DBTable+ + " where id="+configuration.DBPlaceHolder[0], id). + Scan(&id_taken) + + switch { + case err == sql.ErrNoRows: + loggy(fmt.Sprintf("Id '%s' is not taken, will use it.", id)) + case err != nil: + debugLogger.Println(" Database error : " + err.Error()) + os.Exit(1) + default: + loggy(fmt.Sprintf("Id '%s' is taken, generating new id.", id_taken)) + generateName() } return id - } -// Sha1 hashes paste into a sha1 hash -func Sha1(paste string) string { - hasher := sha1.New() +// shaPaste hashes the paste data into a sha1 hash which will be used to +// determine if the pasted data already exists in the database +// Returns the hash +func shaPaste(paste string) string { + hasher := sha1.New() hasher.Write([]byte(paste)) sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + loggy(fmt.Sprintf("Generated sha for paste is '%s'", sha)) return sha } -// DurationFromExpiry takes the expiry in string format and returns the duration -// that the paste will exist for -func DurationFromExpiry(expiry string) time.Duration { - if expiry == "" { - expiry = "P20Y" - } - dura, err := duration.FromString(expiry) // dura is time.Duration type - Check(err) +// savePaste handles the saving for each paste. +// Takes the arguments, +// title, title of the paste as string, +// paste, the actual paste data as a string, +// expiry, the epxpiry date in epoch time as an int64 +// Returns the Response struct +func savePaste(title string, paste string, expiry int64) Response { - duration := dura.ToDuration() + var id, hash, delkey, url string - return duration -} + // Escape user input, + paste = html.EscapeString(paste) + title = html.EscapeString(title) -// Save function handles the saving of each paste. -// raw string is the raw paste input -// lang string is the user specified language for syntax highlighting -// title string user customized title -// expiry string duration that the paste will exist for -// Returns Response struct -func Save(raw string, lang string, title string, expiry string) Response { + // Hash paste data and query database to see if paste exists + sha := shaPaste(paste) + loggy("Checking if pasted data is already in the database.") - db, err := sql.Open("mysql", DATABASE) - Check(err) - defer db.Close() + err := dbHandle.QueryRow("select id, title, hash, data, delkey from "+ + configuration.DBTable+" where hash="+ + configuration.DBPlaceHolder[0], sha).Scan(&id, + &title, &hash, &paste, &delkey) + switch { + case err == sql.ErrNoRows: + loggy("Pasted data is not in the database, will insert it.") + case err != nil: + debugLogger.Println(" Database error : " + err.Error()) + os.Exit(1) + default: + loggy(fmt.Sprintf("Pasted data already exists at id '%s' with title '%s'.", + id, html.UnescapeString(title))) - // hash paste data and query database to see if paste exists - sha := Sha1(raw) - query, err := db.Query("select id, title, hash, data, delkey from pastebin where hash=?", sha) - - if err != sql.ErrNoRows { - for query.Next() { - var id, title, hash, paste, delkey string - err := query.Scan(&id, &title, &hash, &paste, &delkey) - Check(err) - url := configuration.Address + "/p/" + id - return Response{true, "saved", id, title, hash, url, len(paste), delkey} - } - } - id := GenerateName() - url := configuration.Address + "/p/" + id - if lang != "" { - url += "/" + lang + url = configuration.Address + "/p/" + id + return Response{ + Status: "Paste data already exists ...", + Id: id, + Title: title, + Sha1: hash, + Url: url, + Size: len(paste)} } - const timeFormat = "2006-01-02 15:04:05" - expiryTime := time.Now().Add(DurationFromExpiry(expiry)).Format(timeFormat) + // Generate id, + id = generateName() + url = configuration.Address + "/p/" + id - delKey := uniuri.NewLen(40) - dataEscaped := html.EscapeString(raw) + // Set expiry if it's specified, + if expiry != 0 { + expiry += time.Now().Unix() + } - stmt, err := db.Prepare("INSERT INTO pastebin(id, title, hash, data, delkey, expiry) values(?,?,?,?,?,?)") - Check(err) + // Set the generated id as title if not given, if title == "" { title = id } - _, err = stmt.Exec(id, html.EscapeString(title), sha, dataEscaped, delKey, expiryTime) - Check(err) - return Response{true, "saved", id, title, sha, url, len(dataEscaped), delKey} + delKey := uniuri.NewLen(40) + + // This is needed since mysql/postgres uses different placeholders, + var dbQuery string + for i := 0; i < 6; i++ { + dbQuery += configuration.DBPlaceHolder[i] + "," + } + dbQuery = dbQuery[:len(dbQuery)-1] + + stmt, err := dbHandle.Prepare("INSERT INTO " + configuration.DBTable + " (id,title,hash,data,delkey,expiry)values(" + dbQuery + ")") + checkErr(err) + + _, err = stmt.Exec(id, title, sha, paste, delKey, expiry) + checkErr(err) + + loggy(fmt.Sprintf("Sucessfully inserted data at id '%s', title '%s', expiry '%v' and data \n \n* * * *\n\n%s\n\n* * * *\n", + id, + html.UnescapeString(title), + expiry, + html.UnescapeString(paste))) + stmt.Close() + checkErr(err) + + return Response{ + Status: "Successfully saved paste.", + Id: id, + Title: title, + Sha1: hash, + Url: url, + Size: len(paste), + DelKey: delKey} } -// DelHandler checks to see if delkey and pasteid exist in the database. -// if both exist and are correct the paste will be removed. +// DelHandler handles the deletion of pastes. +// If pasteId and DelKey consist the paste will be removed. func DelHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - id := vars["pasteId"] - delkey := r.FormValue("delkey") + var inData Request - db, err := sql.Open("mysql", DATABASE) - Check(err) - defer db.Close() + inData.Id = vars["pasteId"] - stmt, err := db.Prepare("delete from pastebin where delkey=? and id=?") - Check(err) + // Escape user input, + inData.DelKey = html.EscapeString(inData.DelKey) + inData.Id = html.EscapeString(inData.Id) - res, err := stmt.Exec(html.EscapeString(delkey), html.EscapeString(id)) - Check(err) + fmt.Printf("Trying to delete paste with id '%s' and delkey '%s'\n", + inData.Id, inData.DelKey) + stmt, err := dbHandle.Prepare("delete from pastebin where delkey=" + + configuration.DBPlaceHolder[0] + " and id=" + + configuration.DBPlaceHolder[1]) + checkErr(err) + res, err := stmt.Exec(inData.DelKey, inData.Id) + checkErr(err) _, err = res.RowsAffected() + if err != sql.ErrNoRows { w.Header().Set("Content-Type", "application/json") - b := Response{STATUS: "DELETED " + id} + b := Response{Status: "Deleted paste " + inData.Id} err := json.NewEncoder(w).Encode(b) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -202,127 +482,309 @@ func DelHandler(w http.ResponseWriter, r *http.Request) { } } -// SaveHandler Handles saving pastes and outputing responses +// SaveHandler will handle the actual save of each paste. +// Returns with a Response struct. func SaveHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - output := vars["output"] - switch r.Method { - case "POST": - paste := r.FormValue("p") - lang := r.FormValue("lang") - title := r.FormValue("title") - expiry := r.FormValue("expiry") - if paste == "" { - http.Error(w, "Empty paste", 500) - return - } - b := Save(paste, lang, title, expiry) - switch output { - case "redirect": - http.Redirect(w, r, b.URL, 301) + var inData Request - default: - w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(b) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + loggy(fmt.Sprintf("Recieving request to save new paste, trying to parse indata.")) + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&inData) - } - } - -} - -// Highlight uses user specified input to call pygments library to highlight the -// paste -func Highlight(s string, lang string) (string, error) { - - highlight, err := pygments.Highlight(html.UnescapeString(s), html.EscapeString(lang), "html", "style=autumn, linenos=inline, noclasses=True,", "utf-8") - if err != nil { - return "", err - } - return highlight, nil - -} - -// GetPaste takes pasteid and language -// queries the database and returns paste data -func GetPaste(paste string, lang string) (string, string) { - param1 := html.EscapeString(paste) - db, err := sql.Open("mysql", DATABASE) - Check(err) - defer db.Close() - var title, s string - var expiry string - err = db.QueryRow("select title, data, expiry from pastebin where id=?", param1).Scan(&title, &s, &expiry) - Check(err) - if time.Now().Format("2006-01-02 15:04:05") >= expiry { - stmt, err := db.Prepare("delete from pastebin where id=?") - Check(err) - _, err = stmt.Exec(param1) - Check(err) - return "Error invalid paste", "" - } - - if err == sql.ErrNoRows { - return "Error invalid paste", "" - } - if lang != "" { - high, err := Highlight(s, lang) - Check(err) - return high, html.UnescapeString(title) - } - return html.UnescapeString(s), html.UnescapeString(title) -} - -// APIHandler handles get requests of pastes -func APIHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - paste := vars["pasteId"] - - b, _ := GetPaste(paste, "") - w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(b) + // Return error if we can't decode the json-data, if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + d, _ := json.MarshalIndent(inData, "DEBUG : ", " ") + loggy(fmt.Sprintf("Successfully parsed json indata into struct \nDEBUG : %s", d)) + + // Return error if we don't have any data at all + if inData.Paste == "" { + loggy("Empty paste received, returning 500.") + http.Error(w, "Empty paste.", 500) + return + } + + // Return error if title is to long + // TODO add check of paste size. + if len(inData.Title) > 50 { + loggy(fmt.Sprintf("Paste title to long (%v).", len(inData.Title))) + http.Error(w, "Title to long.", 500) + return + } + + p := savePaste(inData.Title, inData.Paste, inData.Expiry) + + d, _ = json.MarshalIndent(p, "DEBUG : ", " ") + loggy(fmt.Sprintf("Returning json data to requester \nDEBUG : %s", d)) + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(p) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } -// PasteHandler handles the generation of paste pages with the links -func PasteHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - paste := vars["pasteId"] - lang := vars["lang"] +// high calls the highlighter-wrapper and runs the paste through it. +// Takes the arguments, +// paste, the actual paste data as a string, +// lang, the pygments lexer to use as a string, +// style, the pygments style to use as a string +// Returns two strings, first is the output from the pygments html-formatter, +// the second is a custom message +func high(paste string, lang string, style string) (string, string, string, string) { - s, title := GetPaste(paste, lang) + // Lets loop through the supported languages to catch if the user is doing + // something fishy. We do this to be extra safe since we are making an + // an external call with user input. + var supported_lang, supported_styles bool + supported_lang = false + supported_styles = false - // button links - link := configuration.Address + "/raw/" + paste - download := configuration.Address + "/download/" + paste - clone := configuration.Address + "/clone/" + paste - // Page struct - p := &Page{ - Title: title, - Body: []byte(s), - Raw: link, - Home: configuration.Address, - Download: download, - Clone: clone, + for _, v1 := range listOfLangsFirst { + if lang == v1 { + supported_lang = true + } } - if lang == "" { - err := templates.ExecuteTemplate(w, "paste.html", p) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + for _, v2 := range listOfLangsLast { + if lang == v2 { + supported_lang = true + } + } + + if lang == "" { + lang = "autodetect" + } + + if !supported_lang && lang != "autodetect" { + lang = "text" + loggy(fmt.Sprintf("Given language ('%s') not supported, using 'text'", lang)) + } + + for _, s := range listOfStyles { + if style == strings.ToLower(s) { + supported_styles = true + } + } + + // Same with the styles, + if !supported_styles { + style = "manni" + loggy(fmt.Sprintf("Given style ('%s') not supported, using ", style)) + } + + if _, err := os.Stat(configuration.Highlighter); os.IsNotExist(err) { + log.Fatal(err) + } + + loggy(fmt.Sprintf("Executing command : %s %s %s", configuration.Highlighter, + lang, style)) + cmd := exec.Command(configuration.Highlighter, lang, style) + cmd.Stdin = strings.NewReader(paste) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + loggy(fmt.Sprintf("The highlightning feature failed, returning text. Error : %s", stderr.String())) + return paste, "Internal Error, returning plain text.", lang, style + } + + loggy(fmt.Sprintf("The wrapper returned the requested language (%s)", lang)) + return stdout.String(), stderr.String(), lang, style +} + +// checkPasteExpiry checks if a paste is overdue. +// It takes the pasteId as sting and the expiry date as an int64 as arguments. +// If the paste is overdue it gets deleted and false is returned. +func checkPasteExpiry(pasteId string, expiry int64) bool { + + loggy("Checking if paste is overdue.") + if expiry == 0 { + loggy("Paste doesn't have a duedate.") + } else { + // Current time, + now := time.Now().Unix() + + // Human friendly strings for logging, + nowStr := time.Unix(now, 0).Format("2006-01-02 15:04:05") + expiryStr := time.Unix(expiry, 0).Format("2006-01-02 15:04:05") + loggy(fmt.Sprintf("Checking if paste is overdue (is %s later than %s).", + nowStr, expiryStr)) + + // If expiry is greater than current time, delete paste, + if now >= expiry { + loggy("User requested a paste that is overdue, deleting it.") + delPaste(pasteId) + return false + } + } + + return true +} + +// delPaste deletes the actual paste. +// It takes the pasteId as sting as argument. +func delPaste(pasteId string) { + + // Prepare statement, + stmt, err := dbHandle.Prepare("delete from pastebin where id=" + + configuration.DBPlaceHolder[0]) + checkErr(err) + + // Execute it, + _, err = stmt.Exec(pasteId) + checkErr(err) + + stmt.Close() + loggy("Successfully deleted paste.") +} + +// getPaste gets the paste from the database. +// Takes the pasteid as a string argument. +// Returns the Response struct. +func getPaste(pasteId string) Response { + + var title, paste string + var expiry int64 + + err := dbHandle.QueryRow("select title, data, expiry from "+ + configuration.DBTable+" where id="+configuration.DBPlaceHolder[0], + pasteId).Scan(&title, &paste, &expiry) + + switch { + case err == sql.ErrNoRows: + loggy("Requested paste doesn't exist.") + return Response{Status: "Requested paste doesn't exist."} + case err != nil: + debugLogger.Println(" Database error : " + err.Error()) + os.Exit(1) + } + + // Check if paste is overdue, + if !checkPasteExpiry(pasteId, expiry) { + return Response{Status: "Requested paste doesn't exist."} + } + + // Unescape the saved data, + paste = html.UnescapeString(paste) + title = html.UnescapeString(title) + + expiryS := "Never" + if expiry != 0 { + expiryS = time.Unix(expiry, 0).Format("2006-01-02 15:04:05") + } + + r := Response{ + Status: "Success", + Id: pasteId, + Title: title, + Paste: paste, + Size: len(paste), + Expiry: expiryS} + + d, _ := json.MarshalIndent(r, "DEBUG : ", " ") + loggy(fmt.Sprintf("Returning data from getPaste \nDEBUG : %s", d)) + + return r +} + +// APIHandler handles all +func APIHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + pasteId := vars["pasteId"] + + var inData Request + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&inData) + + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + + loggy(fmt.Sprintf("Getting paste with id '%s' and lang '%s' and style '%s'.", + pasteId, inData.Lang, inData.Style)) + + // Get the actual paste data, + p := getPaste(pasteId) + + if inData.WebReq { + // If no style is given, use default style, + if inData.Style == "" { + inData.Style = "manni" + p.Url += "/" + inData.Style } - } else { - fmt.Fprintf(w, string(syntax), p.Title, p.Title, s, p.Home, p.Download, p.Raw, p.Clone) + // If no lang is given, use autodetect + if inData.Lang == "" { + inData.Lang = "autodetect" + p.Url += "/" + inData.Lang + } + // Run it through the highgligther., + p.Paste, p.Extra, p.Lang, p.Style = high(p.Paste, inData.Lang, inData.Style) + } + + d, _ := json.MarshalIndent(p, "DEBUG : ", " ") + loggy(fmt.Sprintf("Returning json data to requester \nDEBUG : %s", d)) + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(p) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// pasteHandler generates the html paste pages +func pasteHandler(w http.ResponseWriter, r *http.Request) { + + vars := mux.Vars(r) + pasteId := vars["pasteId"] + lang := vars["lang"] + style := vars["style"] + + loggy(fmt.Sprintf("Getting paste with id '%s' and lang '%s' and style '%s'.", pasteId, lang, style)) + + // Get the actual paste data, + p := getPaste(pasteId) + + // Run it through the highgligther., + p.Paste, p.Extra, p.Lang, p.Style = high(p.Paste, lang, style) + + // Construct page struct + page := &Page{ + Body: template.HTML(p.Paste), + Expiry: p.Expiry, + Lang: p.Lang, + LangsFirst: listOfLangsFirst, + LangsLast: listOfLangsLast, + Style: p.Style, + SupportedStyles: listOfStyles, + Title: p.Title, + GoogleAPIKey: configuration.GoogleAPIKey, + UrlClone: configuration.Address + "/clone/" + pasteId, + UrlDownload: configuration.Address + "/download/" + pasteId, + UrlHome: configuration.Address, + UrlRaw: configuration.Address + "/raw/" + pasteId, + WrapperErr: p.Extra, + } + + err := templates.ExecuteTemplate(w, "syntax.html", page) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } } @@ -331,92 +793,131 @@ func CloneHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) paste := vars["pasteId"] - s, title := GetPaste(paste, "") + p := getPaste(paste) - // Page links - link := configuration.Address + "/raw/" + paste - download := configuration.Address + "/download/" + paste - clone := configuration.Address + "/clone/" + paste + loggy(p.Paste) // Clone page struct - p := &Page{ - Title: title, - Body: []byte(s), - Raw: link, - Home: configuration.Address, - Download: download, - Clone: clone, + page := &Page{ + Body: template.HTML(p.Paste), + PasteTitle: "Copy of " + p.Title, + Title: "Copy of " + p.Title, } - err := templates.ExecuteTemplate(w, "clone.html", p) + + err := templates.ExecuteTemplate(w, "index.html", page) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } - } // DownloadHandler forces downloads of selected pastes func DownloadHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - paste := vars["pasteId"] - s, _ := GetPaste(paste, "") + pasteId := vars["pasteId"] + + p := getPaste(pasteId) // Set header to an attachment so browser will automatically download it - w.Header().Set("Content-Disposition", "attachment; filename="+paste) + w.Header().Set("Content-Disposition", "attachment; filename="+p.Paste) w.Header().Set("Content-Type", r.Header.Get("Content-Type")) - io.WriteString(w, s) - + io.WriteString(w, p.Paste) } // RawHandler displays the pastes in text/plain format func RawHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - paste := vars["pasteId"] - s, _ := GetPaste(paste, "") + pasteId := vars["pasteId"] + p := getPaste(pasteId) w.Header().Set("Content-Type", "text/plain; charset=UTF-8; imeanit=yes") - // simply write string to browser - io.WriteString(w, s) + // Simply write string to browser + io.WriteString(w, p.Paste) } // RootHandler handles generating the root page func RootHandler(w http.ResponseWriter, r *http.Request) { - err := templates.ExecuteTemplate(w, "index.html", &Page{}) + + p := &Page{ + LangsFirst: listOfLangsFirst, + LangsLast: listOfLangsLast, + Title: configuration.DisplayName, + UrlAddress: configuration.Address, + } + + err := templates.ExecuteTemplate(w, "index.html", p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } +func serveCss(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "assets/pastebin.css") +} + func main() { + + // Set up new logger, + debugLogger = log.New(os.Stderr, "DEBUG : ", log.Ldate|log.Ltime) + + // Check args, + checkArgs() + + // Load config, file, err := os.Open("config.json") if err != nil { - panic(err) + loggy(fmt.Sprintf("Error opening config.json (%s)", err)) + os.Exit(1) } + loggy(fmt.Sprintf("Successfully opened %s", "config.json")) + + // Try to parse json, decoder := json.NewDecoder(file) err = decoder.Decode(&configuration) if err != nil { - panic(err) + loggy(fmt.Sprintf("Error parsing json data from %s : %s", "config.json", err)) + os.Exit(1) } - DATABASE = configuration.Username + ":" + configuration.Password + "@/" + configuration.Name + "?charset=utf8" - // create new mux router + d, _ := json.MarshalIndent(configuration, "DEBUG : ", " ") + loggy(fmt.Sprintf("Successfully parsed json data into struct \nDEBUG : %s", d)) + + // Get languages and styles, + getSupportedLangs() + getSupportedStyles() + + // Get the database handle + dbHandle = getDBHandle() + + // Router object, router := mux.NewRouter() - // serverside rending stuff - router.HandleFunc("/p/{pasteId}", PasteHandler).Methods("GET") - router.HandleFunc("/raw/{pasteId}", RawHandler).Methods("GET") - router.HandleFunc("/p/{pasteId}/{lang}", PasteHandler).Methods("GET") - router.HandleFunc("/clone/{pasteId}", CloneHandler).Methods("GET") - router.HandleFunc("/download/{pasteId}", DownloadHandler).Methods("GET") - // api - router.HandleFunc("/api", SaveHandler).Methods("POST") - router.HandleFunc("/api/{output}", SaveHandler).Methods("POST") - router.HandleFunc("/api/{pasteid}", APIHandler).Methods("GET") - router.HandleFunc("/api/{pasteId}", DelHandler).Methods("DELETE") + // Routes, router.HandleFunc("/", RootHandler) - err = http.ListenAndServe(configuration.Port, router) - if err != nil { - log.Fatal(err) + router.HandleFunc("/p/{pasteId}", pasteHandler).Methods("GET") + router.HandleFunc("/p/{pasteId}/{lang}", pasteHandler).Methods("GET") + router.HandleFunc("/p/{pasteId}/{lang}/{style}", pasteHandler).Methods("GET") + + // Api + router.HandleFunc("/api", SaveHandler).Methods("POST") + router.HandleFunc("/api/{pasteId}", APIHandler).Methods("POST") + router.HandleFunc("/api/{pasteId}", APIHandler).Methods("GET") + router.HandleFunc("/api/{pasteId}", DelHandler).Methods("DELETE") + + router.HandleFunc("/raw/{pasteId}", RawHandler).Methods("GET") + router.HandleFunc("/clone/{pasteId}", CloneHandler).Methods("GET") + + router.HandleFunc("/download/{pasteId}", DownloadHandler).Methods("GET") + router.HandleFunc("/assets/pastebin.css", serveCss).Methods("GET") + + // Set up server, + srv := &http.Server{ + Handler: router, + Addr: configuration.ListenAddress + ":" + configuration.ListenPort, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, } + err = srv.ListenAndServe() + checkErr(err) }