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 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
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)
}