From 825fffe5020578c8b21fd316034beaffa7236fdd Mon Sep 17 00:00:00 2001 From: Patrik Martinsson Date: Sun, 18 Dec 2016 15:12:24 +0100 Subject: [PATCH 1/3] Initial commit of pastebin fork It's quite big rewrite and indivdual commits are not available since its so big. Introducing features like, - Support for multiple database backends and configurations like dbname/tablename/ports/adresses/etc. - Support for dynamically adding languages and lexers from pygments, autodetection of language is also supported. - Support for changing styles (and languages) on the fly, also row-highlightning and rownumbers. - Support for showing information about when paste expires. - Support for goo.gl-shortener. - Extreme debugging =) - Probably added a few bugs in there as well =) --- .gitignore | 2 +- Makefile | 17 +- assets/clone.html | 505 ------------------- assets/index.html | 656 +++++++------------------ assets/paste.html | 69 --- assets/pastebin.css | 218 +++++++++ assets/prio-lexers | 25 + assets/syntax.html | 259 ++++++++-- config.example.json | 10 - config.json | 16 + database.sql | 4 +- highlighter-wrapper.py | 106 ++++ pastebin.go | 1043 +++++++++++++++++++++++++++++----------- 13 files changed, 1550 insertions(+), 1380 deletions(-) delete mode 100644 assets/clone.html delete mode 100644 assets/paste.html create mode 100644 assets/pastebin.css create mode 100644 assets/prio-lexers delete mode 100644 config.example.json create mode 100644 config.json create mode 100755 highlighter-wrapper.py 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 }} + +
    + + +
    + + +
    + +
    + +
    + 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..7b94a4e --- /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 : 10px; + margin : 5px 5px 5px 5px; + min-height : 250px; +} + +.code-row { + display : block; + font-size : 10px; + font-weight : lighter; +} + +.codenum-row { + display : block; + font-size : 10px; + 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) } From a7ea8be05344554c930eb7be2f50a104cc5095dd Mon Sep 17 00:00:00 2001 From: Eliot Whalan Date: Mon, 19 Dec 2016 15:09:07 +1000 Subject: [PATCH 2/3] Fix small font size --- assets/pastebin.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/pastebin.css b/assets/pastebin.css index 7b94a4e..0d5fdb3 100644 --- a/assets/pastebin.css +++ b/assets/pastebin.css @@ -33,20 +33,20 @@ pre { font-size : 12px; font-weight : normal; letter-spacing : 0.015em; - line-height : 10px; + line-height : 14px; margin : 5px 5px 5px 5px; min-height : 250px; } .code-row { display : block; - font-size : 10px; + font-size : 14px; font-weight : lighter; } .codenum-row { display : block; - font-size : 10px; + font-size : 14; font-weight : lighter; } From 5a0b1f5d4de4d5491c7dd053a935dc189ad35eca Mon Sep 17 00:00:00 2001 From: patchon Date: Sun, 18 Dec 2016 15:12:24 +0100 Subject: [PATCH 3/3] Initial commit of Pastebin fork This commit is quite big rewrite and individual commits for each feature is unfortunately not available. This fork introduces quite a lot of new features, and possibly while doing so, some bugs as well. Nonetheless the features introduced are listed below. - Support for multiple database backends (sqlite3, postgresql and mysql are now supported). Configurations options like dbname/tablename/ports/adresses/etc. are supported as well. - Support for dynamically adding lexers (languages) and styles (themes) from pygments. This means no manual configuration depending on where the installation is done. There is however a way of adding "prioritized" lexers. This feature can be nice to use sine pygments now days support hundreds of languages, some more common than others. Simply add the "display name" for each lexer you want to prioritize in the file 'assets/prio-lexers' and they will show up first in the list when the user selects languages. - Support for changing styles and lexers directly from the webgui on the fly (no reload of the page, just content update). - Support for row-highlightning (on/off) and rownumbers (show/hide). - Support for showing information about when paste expires. - Support for goo.gl-shortener. - Extreme debugging. --- .gitignore | 2 +- Makefile | 17 +- assets/clone.html | 505 ------------------- assets/index.html | 656 +++++++------------------ assets/paste.html | 69 --- assets/pastebin.css | 218 +++++++++ assets/prio-lexers | 25 + assets/syntax.html | 259 ++++++++-- config.example.json | 10 - config.json | 16 + database.sql | 4 +- highlighter-wrapper.py | 106 ++++ pastebin.go | 1043 +++++++++++++++++++++++++++++----------- 13 files changed, 1550 insertions(+), 1380 deletions(-) delete mode 100644 assets/clone.html delete mode 100644 assets/paste.html create mode 100644 assets/pastebin.css create mode 100644 assets/prio-lexers delete mode 100644 config.example.json create mode 100644 config.json create mode 100755 highlighter-wrapper.py 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 }} + +
    +
    + +
    + + +
    + +
    + +
    + 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..7b94a4e --- /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 : 10px; + margin : 5px 5px 5px 5px; + min-height : 250px; +} + +.code-row { + display : block; + font-size : 10px; + font-weight : lighter; +} + +.codenum-row { + display : block; + font-size : 10px; + 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) }