nat/pastebin.go

1186 lines
33 KiB
Go

// Package pastebin is a simple modern and powerful pastebin service
package main
import (
"bufio"
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"html"
"html/template"
"io"
"log"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"time"
// Random string generation,
"github.com/dchest/uniuri"
// Database drivers,
"database/sql"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
// For url routing
"github.com/gorilla/mux"
// securecookie for cookie handling
"github.com/gorilla/securecookie"
// bcrypt for password hashing
"golang.org/x/crypto/bcrypt"
)
// Configuration struct,
type Configuration struct {
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 [7]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
DBAccountsTable string `json:"dbaccountstable"` // 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
}
// This struct is used for responses.
// A request to the pastebin will always this json struct.
type Response struct {
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
}
// 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
UserKey string `json:"key"` // 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 {
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
UserKey string
}
type Pastes struct {
Response []Response
}
// Template pages,
var templates = template.Must(template.ParseFiles("assets/index.html",
"assets/syntax.html",
"assets/register.html",
"assets/pastes.html",
"assets/login.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
// generate new random cookie keys
var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
)
//
// Functions below,
//
// loggy prints a message if the debug flag is turned.
func loggy(str string) {
if debug {
debugLogger.Println(" " + str)
}
}
// 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)
}
}
// 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, string(out))
}
// 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 < 7; 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 < 7; 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
}
// 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
}
// 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, user_key string) Response {
var id, hash, delkey, url string
// Escape user input,
paste = html.EscapeString(paste)
title = html.EscapeString(title)
user_key = html.EscapeString(user_key)
// Hash paste data and query database to see if paste exists
sha := shaPaste(paste)
loggy("Checking if pasted data is already in the database.")
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)))
url = configuration.Address + "/p/" + id
return Response{
Status: "Paste data already exists ...",
Id: id,
Title: title,
Sha1: hash,
Url: url,
Size: len(paste)}
}
// Generate id,
id = generateName()
url = configuration.Address + "/p/" + id
// Set expiry if it's specified,
if expiry != 0 {
expiry += time.Now().Unix()
}
// Set the generated id as title if not given,
if title == "" {
title = id
}
delKey := uniuri.NewLen(40)
// This is needed since mysql/postgres uses different placeholders,
var dbQuery string
for i := 0; i < 7; i++ {
dbQuery += configuration.DBPlaceHolder[i] + ","
}
dbQuery = dbQuery[:len(dbQuery)-1]
stmt, err := dbHandle.Prepare("INSERT INTO " + configuration.DBTable + " (id,title,hash,data,delkey,expiry,userid)values(" + dbQuery + ")")
checkErr(err)
_, err = stmt.Exec(id, title, sha, paste, delKey, expiry, user_key)
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 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)
var inData Request
loggy(fmt.Sprintf("Recieving request to delete a paste, trying to parse indata."))
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&inData)
inData.Id = vars["pasteId"]
// Escape user input,
inData.DelKey = html.EscapeString(inData.DelKey)
inData.Id = html.EscapeString(inData.Id)
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 paste " + inData.Id}
err := json.NewEncoder(w).Encode(b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// SaveHandler will handle the actual save of each paste.
// Returns with a Response struct.
func SaveHandler(w http.ResponseWriter, r *http.Request) {
var inData Request
loggy(fmt.Sprintf("Recieving request to save new paste, trying to parse indata."))
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&inData)
// 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, inData.UserKey)
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
}
}
// 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) {
// 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
for _, v1 := range listOfLangsFirst {
if lang == v1 {
supported_lang = true
}
}
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
}
// 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)
}
}
// CloneHandler handles generating the clone pages
func CloneHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
paste := vars["pasteId"]
p := getPaste(paste)
loggy(p.Paste)
// Clone page struct
page := &Page{
Body: template.HTML(p.Paste),
PasteTitle: "Copy of " + p.Title,
Title: "Copy of " + p.Title,
UserKey: getUserKey(r),
}
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)
pasteId := vars["pasteId"]
p := getPaste(pasteId)
// Set header to an attachment so browser will automatically download it
w.Header().Set("Content-Disposition", "attachment; filename="+p.Paste)
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
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)
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, p.Paste)
}
// loginHandler
func loginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
err := templates.ExecuteTemplate(w, "login.html", "")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "POST":
email := r.FormValue("email")
password := r.FormValue("password")
email_escaped := html.EscapeString(email)
// Query database if id exists and if it does call generateName again
var hashedPassword []byte
err := dbHandle.QueryRow("select password from "+configuration.DBAccountsTable+
" where email="+configuration.DBPlaceHolder[0], email_escaped).
Scan(&hashedPassword)
switch {
case err == sql.ErrNoRows:
loggy(fmt.Sprintf("Email '%s' is not taken.", email))
http.Redirect(w, r, "/register", 302)
case err != nil:
debugLogger.Println(" Database error : " + err.Error())
os.Exit(1)
default:
loggy(fmt.Sprintf("Account '%s' exists.", email))
}
// compare bcrypt hash to userinput password
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err == nil {
// prepare cookie
value := map[string]string{
"email": email,
}
// encode variables into cookie
if encoded, err := cookieHandler.Encode("session", value); err == nil {
cookie := &http.Cookie{
Name: "session",
Value: encoded,
Path: "/",
}
// set user cookie
http.SetCookie(w, cookie)
}
loggy(fmt.Sprintf("Successfully logged account '%s' in.", email))
// Redirect to home page
http.Redirect(w, r, "/", 302)
}
// Redirect to login page
http.Redirect(w, r, "/login", 302)
}
}
func pastesHandler(w http.ResponseWriter, r *http.Request) {
key := getUserKey(r)
b := Pastes{Response: []Response{}}
rows, err := dbHandle.Query("select id, title, delkey, data from "+
configuration.DBTable+" where userid="+
configuration.DBPlaceHolder[0], key)
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:
for rows.Next() {
var id, title, url, delKey, data string
rows.Scan(&id, &title, &delKey, &data)
url = configuration.Address + "/p/" + id
res := Response{
Id: id,
Title: title,
Url: url,
Size: len(data),
DelKey: delKey}
b.Response = append(b.Response, res)
}
rows.Close()
}
err = templates.ExecuteTemplate(w, "pastes.html", &b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// loggedIn returns true if cookie exists
func getUserKey(r *http.Request) string {
cookie, err := r.Cookie("session")
cookieValue := make(map[string]string)
if err != nil {
return ""
}
err = cookieHandler.Decode("session", cookie.Value, &cookieValue)
if err != nil {
return ""
}
email := cookieValue["email"]
// Query database if id exists and if it does call generateName again
var user_key string
err = dbHandle.QueryRow("select key from "+configuration.DBAccountsTable+
" where email="+configuration.DBPlaceHolder[0], email).
Scan(&user_key)
switch {
case err == sql.ErrNoRows:
loggy(fmt.Sprintf("Key does not exist for user '%s'", email))
case err != nil:
debugLogger.Println(" Database error : " + err.Error())
os.Exit(1)
default:
loggy(fmt.Sprintf("User key found for user '%s'", email))
}
return user_key
}
// generateKey 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 generateKey() string {
// Use uniuri to generate random string
key := uniuri.NewLen(20)
loggy(fmt.Sprintf("Generated id is '%s', checking if it's already taken in the database",
key))
// Query database if id exists and if it does call generateName again
var key_taken string
err := dbHandle.QueryRow("select key from "+configuration.DBAccountsTable+
" where key="+configuration.DBPlaceHolder[0], key).
Scan(&key_taken)
switch {
case err == sql.ErrNoRows:
loggy(fmt.Sprintf("Key '%s' is not taken, will use it.", key))
case err != nil:
debugLogger.Println(" Database error : " + err.Error())
os.Exit(1)
default:
loggy(fmt.Sprintf("Key '%s' is taken, generating new key.", key_taken))
generateKey()
}
return key
}
// registerHandler
func registerHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
err := templates.ExecuteTemplate(w, "register.html", "")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "POST":
email := r.FormValue("email")
pass := r.FormValue("password")
email_escaped := html.EscapeString(email)
loggy(fmt.Sprintf("Attempting to create account '%s', checking if it's already taken in the database",
email))
// Query database if id exists and if it does call generateName again
var email_taken string
err := dbHandle.QueryRow("select email from "+configuration.DBAccountsTable+
" where email="+configuration.DBPlaceHolder[0], email_escaped).
Scan(&email_taken)
switch {
case err == sql.ErrNoRows:
loggy(fmt.Sprintf("Email '%s' is not taken, will use it.", email))
case err != nil:
debugLogger.Println(" Database error : " + err.Error())
os.Exit(1)
default:
loggy(fmt.Sprintf("Email '%s' is taken.", email_taken))
http.Redirect(w, r, "/register", 302)
}
// This is needed since mysql/postgres uses different placeholders,
var dbQuery string
for i := 0; i < 3; i++ {
dbQuery += configuration.DBPlaceHolder[i] + ","
}
dbQuery = dbQuery[:len(dbQuery)-1]
stmt, err := dbHandle.Prepare("INSERT into " + configuration.DBAccountsTable + "(email, password, key) values(" + dbQuery + ")")
checkErr(err)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
checkErr(err)
key := generateKey()
_, err = stmt.Exec(email_escaped, hashedPassword, key)
checkErr(err)
loggy(fmt.Sprintf("Successfully created account '%s' with hashed password '%s'",
email,
hashedPassword))
stmt.Close()
checkErr(err)
http.Redirect(w, r, "/login", 302)
}
}
// logoutHandler destroys cookie data and redirects to root
func logoutHandler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/", 301)
}
// RootHandler handles generating the root page
func RootHandler(w http.ResponseWriter, r *http.Request) {
p := &Page{
LangsFirst: listOfLangsFirst,
LangsLast: listOfLangsLast,
Title: configuration.DisplayName,
UrlAddress: configuration.Address,
UserKey: getUserKey(r),
}
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 {
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 {
loggy(fmt.Sprintf("Error parsing json data from %s : %s", "config.json", err))
os.Exit(1)
}
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()
// Routes,
router.HandleFunc("/", RootHandler)
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("/login", loginHandler)
router.HandleFunc("/logout", logoutHandler)
router.HandleFunc("/register", registerHandler)
router.HandleFunc("/pastes", pastesHandler).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)
}