diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index a751266..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2016 Eliot Whalan - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/Pastebin b/Pastebin deleted file mode 100755 index 8e58781..0000000 Binary files a/Pastebin and /dev/null differ diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..4f288ae --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,596 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html" + "html/template" + "io" + "log" + "net/http" + "os" + "time" + + "git.tuxpa.in/a/nat/lib/store" + "git.tuxpa.in/a/nat/lib/store/sqlike" + "git.tuxpa.in/a/nat/lib/styler" + + "database/sql" + + "github.com/go-chi/chi/v5" + + "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 + + 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 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 +} + +// Template pages, +var templates = template.Must(template.ParseFiles("static/index.html", + "static/syntax.html", + "static/register.html", + "static/pastes.html", + "static/login.html"), +) + +// Global variables, *shrug* +var configuration Configuration +var dbHandle *sql.DB +var debug bool +var debugLogger *log.Logger + +type Server struct { + store store.Store + styler *styler.Styler +} + +// generate new random cookie keys +var cookieHandler = securecookie.New( + securecookie.GenerateRandomKey(64), + securecookie.GenerateRandomKey(32), +) + +// +// Functions below, +// + +// 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 +} + +// 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). + +// 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(" - pastebin") + fmt.Printf(" support for syntax highlightnig (trough python-pygments).\n") + + fmt.Printf(" Usage, \n") + fmt.Printf(" - %s [--help] \n\n", os.Args[0]) + + fmt.Printf(" Where, \n") + fmt.Printf(" - help shows this *incredibly* useful help.\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) + } + } + } +} + +// DelHandler handles the deletion of pastes. +// If pasteId and DelKey consist the paste will be removed. +func (s *Server) DelHandler(w http.ResponseWriter, r *http.Request) { + var inData Request + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&inData) + + inData.Id = chi.URLParam(r, "pasteId") + + // Escape user input, + inData.DelKey = html.EscapeString(inData.DelKey) + inData.Id = html.EscapeString(inData.Id) + + err = s.store.DelPaste(r.Context(), inData.Id, inData.DelKey) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + b := store.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 store.Response struct. +func (s *Server) SaveHandler(w http.ResponseWriter, r *http.Request) { + + var inData Request + + 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 + } + + if len(inData.Paste) > 1500000 { + http.Error(w, "Paste too long.", 500) + return + } + // Return error if we don't have any data at all + if inData.Paste == "" { + http.Error(w, "Empty paste.", 500) + return + } + + if len(inData.Title) > 50 { + http.Error(w, "Title to long.", 500) + return + } + + p, err := s.store.SavePaste(r.Context(), inData.Title, inData.Paste, time.Second*time.Duration(inData.Expiry), inData.UserKey) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + 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 + +// 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 (s *Server) checkPasteExpiry(pasteId string, expiretime time.Time) bool { + if expiretime.IsZero() { + } else { + // Current time, + now := time.Now() + // Human friendly strings for logging, + // If expiry is greater than current time, delete paste, + if now.After(expiretime) { + err := s.store.ForceDelPaste(context.TODO(), pasteId) + if err != nil { + log.Printf("failed to delete paste: %s, %w\n", pasteId) + } + return false + } + } + + return true +} + +func (s *Server) APIHandler(w http.ResponseWriter, r *http.Request) { + pasteId := chi.URLParam(r, "pasteId") + + var inData Request + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&inData) + + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + + // Get the actual paste data, + p, err := s.store.GetPaste(r.Context(), pasteId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if inData.WebReq { + // Run it through the highgligther., + p.Paste, p.Extra, p.Lang, p.Style, err = s.styler.Highlight(p.Paste, inData.Lang, inData.Style) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + 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 (s *Server) pasteHandler(w http.ResponseWriter, r *http.Request) { + + pasteId := chi.URLParam(r, "pasteId") + lang := chi.URLParam(r, "lang") + style := chi.URLParam(r, "style") + + // Get the actual paste data, + p, err := s.store.GetPaste(r.Context(), pasteId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + // Run it through the highgligther., + p.Paste, p.Extra, p.Lang, p.Style, err = s.styler.Highlight(p.Paste, lang, style) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + l := s.styler.Legacy() + + // Construct page struct + page := &Page{ + Body: template.HTML(p.Paste), + Expiry: p.Expiry, + Lang: p.Lang, + LangsFirst: l[0], + LangsLast: l[1], + Style: p.Style, + SupportedStyles: l[2], + Title: p.Title, + 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 (s *Server) CloneHandler(w http.ResponseWriter, r *http.Request) { + paste := chi.URLParam(r, "pasteId") + + p, err := s.store.GetPaste(r.Context(), paste) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + + } + user, _ := s.store.GetUserKey(r.Context(), paste) + page := &Page{ + Body: template.HTML(p.Paste), + PasteTitle: "Copy of " + p.Title, + Title: "Copy of " + p.Title, + UserKey: user, + } + + err = templates.ExecuteTemplate(w, "index.html", page) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// DownloadHandler forces downloads of selected pastes +func (s *Server) DownloadHandler(w http.ResponseWriter, r *http.Request) { + pasteId := chi.URLParam(r, "pasteId") + + p, err := s.store.GetPaste(r.Context(), pasteId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // 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 (s *Server) RawHandler(w http.ResponseWriter, r *http.Request) { + pasteId := chi.URLParam(r, "pasteId") + + p, err := s.store.GetPaste(r.Context(), pasteId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/plain; charset=UTF-8; imeanit=yes") + + // Simply write string to browser + io.WriteString(w, p.Paste) +} + +// loginHandler +func (s *Server) loginHandlerGet(w http.ResponseWriter, r *http.Request) { + err := templates.ExecuteTemplate(w, "login.html", "") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} +func (s *Server) loginHandlerPost(w http.ResponseWriter, r *http.Request) { + email := r.FormValue("email") + password := r.FormValue("password") + email_escaped := html.EscapeString(email) + + hashedPassword, err := s.store.HasAccount(r.Context(), email_escaped) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(hashedPassword) == 0 { + http.Redirect(w, r, "/register", 302) + return + } + // 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) + } + // Redirect to home page + http.Redirect(w, r, "/", 302) + } + // Redirect to login page + http.Redirect(w, r, "/login", 302) + +} + +func (s *Server) pastesHandler(w http.ResponseWriter, r *http.Request) { + + key, err := s.getUserKey(r) + b, err := s.store.GetUserPastes(r.Context(), key) + + err = templates.ExecuteTemplate(w, "pastes.html", &b) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// loggedIn returns true if cookie exists +func (s *Server) getUserKey(r *http.Request) (string, error) { + cookie, err := r.Cookie("session") + cookieValue := make(map[string]string) + if err != nil { + return "", err + } + err = cookieHandler.Decode("session", cookie.Value, &cookieValue) + if err != nil { + return "", err + } + email := cookieValue["email"] + // Query database if id exists and if it does call generateName again + user_key, err := s.store.GetUserKey(r.Context(), email) + switch { + case err == sql.ErrNoRows: + return "", nil + case err != nil: + return "", err + default: + } + return user_key, nil + +} + +// registerHandler +func (s *Server) registerHandlerGet(w http.ResponseWriter, r *http.Request) { + err := templates.ExecuteTemplate(w, "register.html", "") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +func (s *Server) registerHandlerPost(w http.ResponseWriter, r *http.Request) { + email := r.FormValue("email") + pass := r.FormValue("password") + email_escaped := html.EscapeString(email) + bts, err := s.store.HasAccount(r.Context(), email_escaped) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + email_taken := true + if len(bts) == 0 { + email_taken = false + } + if email_taken { + http.Redirect(w, r, "/register", 302) + return + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = s.store.RegisterUser(r.Context(), email_escaped, hashedPassword) + if err != nil { + log.Printf("failed register user %v\n", err) + } + + http.Redirect(w, r, "/login", 302) + +} + +// logoutHandler destroys cookie data and redirects to root +func (s *Server) 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 (s *Server) RootHandler(w http.ResponseWriter, r *http.Request) { + userkey, err := s.getUserKey(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + l := s.styler.Legacy() + p := &Page{ + LangsFirst: l[0], + LangsLast: l[1], + Title: "nat", + UrlAddress: configuration.Address, + UserKey: userkey, + } + 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, "static/pastebin.css") +} + +func main() { + // Check args, + checkArgs() + + // Load config, + file, err := os.Open("config.json") + if err != nil { + os.Exit(1) + } + + // Try to parse json, + err = json.NewDecoder(file).Decode(&configuration) + if err != nil { + panic(err) + } + + srv := &Server{ + store: sqlike.MustNew(), + styler: styler.New(), + } + + router := chi.NewRouter() + router.Get("/", srv.RootHandler) + router.Get("/p/{pasteId}", srv.pasteHandler) + router.Get("/p/{pasteId}/{lang}", srv.pasteHandler) + router.Get("/p/{pasteId}/{lang}/{style}", srv.pasteHandler) + + // Api + router.Post("/api", srv.SaveHandler) + router.Post("/api/{pasteId}", srv.APIHandler) + router.Get("/api/{pasteId}", srv.APIHandler) + router.Delete("/api/{pasteId}", srv.DelHandler) + + router.Get("/raw/{pasteId}", srv.RawHandler) + router.Get("/clone/{pasteId}", srv.CloneHandler) + router.Get("/login", srv.loginHandlerGet) + router.Post("/login", srv.loginHandlerPost) + router.HandleFunc("/logout", srv.logoutHandler) + router.Get("/register", srv.registerHandlerGet) + router.Post("/register", srv.registerHandlerPost) + router.Get("/pastes", srv.pastesHandler) + + router.Get("/download/{pasteId}", srv.DownloadHandler) + router.Get("/assets/pastebin.css", serveCss) + + http_srv := &http.Server{ + Handler: router, + Addr: configuration.ListenAddress + ":" + configuration.ListenPort, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Println("starting http server on", configuration.ListenAddress, configuration.ListenPort) + err = http_srv.ListenAndServe() + if err != nil { + panic(err) + } +} diff --git a/config.json b/config.json index c3f8cec..b93db29 100644 --- a/config.json +++ b/config.json @@ -1,17 +1,15 @@ { "address": "http://localhost:9999", + "dbtype": "sql", "dbhost": "", "dbname": "pastebin.db", "dbtable": "pastebin", "dbaccountstable": "accounts", - "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" + "shorturllength": "5" } diff --git a/database.sql b/database.sql deleted file mode 100644 index 2a6adb2..0000000 --- a/database.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE `pastebin` ( - `id` varchar(30) NOT NULL, - `title` varchar(50) default NULL, - `hash` char(40) default NULL, - `data` longtext, - `delkey` char(40) default NULL, - `expiry` int, - `userid` varchar(255), - PRIMARY KEY (`id`) -); - -CREATE TABLE `accounts` ( - `email` varchar(255) NOT NULL, - `password` varchar(255) NOT NULL, - `key` varchar(255) NOT NULL, - PRIMARY KEY (`key`) -); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f19f8a3 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module git.tuxpa.in/a/nat + +go 1.18 + +require ( + github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 + github.com/go-chi/chi/v5 v5.0.7 + github.com/go-sql-driver/mysql v1.6.0 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/securecookie v1.1.1 + github.com/lib/pq v1.10.6 + github.com/mattn/go-sqlite3 v1.14.14 + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0c2ba1f --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/highlighter-wrapper.py b/highlighter-wrapper.py deleted file mode 100755 index 0656cb9..0000000 --- a/highlighter-wrapper.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python - -try: - import pygments -except ImportError: - print(" Please install python pygments module") - -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/lib/idgen/idgen.go b/lib/idgen/idgen.go new file mode 100644 index 0000000..6200713 --- /dev/null +++ b/lib/idgen/idgen.go @@ -0,0 +1,286 @@ +package idgen + +import ( + randc "crypto/rand" + "errors" + "fmt" + "math" + randm "math/rand" + "sync" + "time" +) + +const DefaultABC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" + +// Abc represents a shuffled alphabet used to generate the Ids and provides methods to +// encode data. +type Abc struct { + alphabet []rune +} + +// Shortid type represents a short Id generator working with a given alphabet. +type Shortid struct { + abc Abc + worker uint + epoch time.Time // ids can be generated for 34 years since this date + ms uint // ms since epoch for the last id + count uint // request count within the same ms + mx sync.Mutex // locks access to ms and count +} + +var shortid *Shortid + +func init() { + shortid = MustNew(0, DefaultABC, 1) +} + +func GetDefault() *Shortid { + return shortid +} + +// SetDefault overwrites the default generator. +// should not be used concurrently with generation +func SetDefault(sid *Shortid) { + shortid = sid +} + +// Generate generates an Id using the default generator. +func Generate() (string, error) { + return shortid.Generate() +} + +// MustGenerate acts just like Generate, but panics instead of returning errors. +func MustGenerate() string { + id, err := Generate() + if err == nil { + return id + } + panic(err) +} + +// New constructs an instance of the short Id generator for the given worker number [0,31], alphabet +// (64 unique symbols) and seed value (to shuffle the alphabet). The worker number should be +// different for multiple or distributed processes generating Ids into the same data space. The +// seed, on contrary, should be identical. +func New(worker uint8, alphabet string, seed uint64) (*Shortid, error) { + if worker > 31 { + return nil, errors.New("expected worker in the range [0,31]") + } + abc, err := NewAbc(alphabet, seed) + if err == nil { + sid := &Shortid{ + abc: abc, + worker: uint(worker), + epoch: time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), + ms: 0, + count: 0, + } + return sid, nil + } + return nil, err +} + +// MustNew acts just like New, but panics instead of returning errors. +func MustNew(worker uint8, alphabet string, seed uint64) *Shortid { + sid, err := New(worker, alphabet, seed) + if err == nil { + return sid + } + panic(err) +} + +// Generate generates a new short Id. +func (sid *Shortid) Generate() (string, error) { + return sid.generateInternal(nil, sid.epoch) +} + +// MustGenerate acts just like Generate, but panics instead of returning errors. +func (sid *Shortid) MustGenerate() string { + id, err := sid.Generate() + if err == nil { + return id + } + panic(err) +} + +func (sid *Shortid) generateInternal(tm *time.Time, epoch time.Time) (string, error) { + ms, count := sid.getMsAndCounter(tm, epoch) + idrunes := make([]rune, 9) + if tmp, err := sid.abc.Encode(ms, 8, 5); err == nil { + copy(idrunes, tmp) // first 8 symbols + } else { + return "", err + } + if tmp, err := sid.abc.Encode(sid.worker, 1, 5); err == nil { + idrunes[8] = tmp[0] + } else { + return "", err + } + if count > 0 { + if countrunes, err := sid.abc.Encode(count, 0, 6); err == nil { + // only extend if really need it + idrunes = append(idrunes, countrunes...) + } else { + return "", err + } + } + return string(idrunes), nil +} + +func (sid *Shortid) getMsAndCounter(tm *time.Time, epoch time.Time) (uint, uint) { + sid.mx.Lock() + defer sid.mx.Unlock() + var ms uint + if tm != nil { + ms = uint(tm.Sub(epoch).Nanoseconds() / 1000000) + } else { + ms = uint(time.Now().Sub(epoch).Nanoseconds() / 1000000) + } + if ms == sid.ms { + sid.count++ + } else { + sid.count = 0 + sid.ms = ms + } + return sid.ms, sid.count +} + +// String returns a string representation of the short Id generator. +func (sid *Shortid) String() string { + return fmt.Sprintf("Shortid(worker=%v, epoch=%v, abc=%v)", sid.worker, sid.epoch, sid.abc) +} + +// Abc returns the instance of alphabet used for representing the Ids. +func (sid *Shortid) Abc() Abc { + return sid.abc +} + +// Epoch returns the value of epoch used as the beginning of millisecond counting (normally +// 2016-01-01 00:00:00 local time) +func (sid *Shortid) Epoch() time.Time { + return sid.epoch +} + +// Worker returns the value of worker for this short Id generator. +func (sid *Shortid) Worker() uint { + return sid.worker +} + +// NewAbc constructs a new instance of shuffled alphabet to be used for Id representation. +func NewAbc(alphabet string, seed uint64) (Abc, error) { + runes := []rune(alphabet) + if len(runes) != len(DefaultABC) { + return Abc{}, fmt.Errorf("alphabet must contain %v unique characters", len(DefaultABC)) + } + if nonUnique(runes) { + return Abc{}, errors.New("alphabet must contain unique characters only") + } + abc := Abc{alphabet: nil} + abc.shuffle(alphabet, seed) + return abc, nil +} + +// MustNewAbc acts just like NewAbc, but panics instead of returning errors. +func MustNewAbc(alphabet string, seed uint64) Abc { + res, err := NewAbc(alphabet, seed) + if err == nil { + return res + } + panic(err) +} + +func nonUnique(runes []rune) bool { + found := make(map[rune]struct{}) + for _, r := range runes { + if _, seen := found[r]; !seen { + found[r] = struct{}{} + } + } + return len(found) < len(runes) +} + +func (abc *Abc) shuffle(alphabet string, seed uint64) { + source := []rune(alphabet) + for len(source) > 1 { + seed = (seed*9301 + 49297) % 233280 + i := int(seed * uint64(len(source)) / 233280) + + abc.alphabet = append(abc.alphabet, source[i]) + source = append(source[:i], source[i+1:]...) + } + abc.alphabet = append(abc.alphabet, source[0]) +} + +// Encode encodes a given value into a slice of runes of length nsymbols. In case nsymbols==0, the +// length of the result is automatically computed from data. Even if fewer symbols is required to +// encode the data than nsymbols, all positions are used encoding 0 where required to guarantee +// uniqueness in case further data is added to the sequence. The value of digits [4,6] represents +// represents n in 2^n, which defines how much randomness flows into the algorithm: 4 -- every value +// can be represented by 4 symbols in the alphabet (permitting at most 16 values), 5 -- every value +// can be represented by 2 symbols in the alphabet (permitting at most 32 values), 6 -- every value +// is represented by exactly 1 symbol with no randomness (permitting 64 values). +func (abc *Abc) Encode(val, nsymbols, digits uint) ([]rune, error) { + if digits < 4 || 6 < digits { + return nil, fmt.Errorf("allowed digits range [4,6], found %v", digits) + } + + var computedSize uint = 1 + if val >= 1 { + computedSize = uint(math.Log2(float64(val)))/digits + 1 + } + if nsymbols == 0 { + nsymbols = computedSize + } else if nsymbols < computedSize { + return nil, fmt.Errorf("cannot accommodate data, need %v digits, got %v", computedSize, nsymbols) + } + + mask := 1<>shift) & mask) | random[i] + res[i] = abc.alphabet[index] + } + return res, nil +} + +// MustEncode acts just like Encode, but panics instead of returning errors. +func (abc *Abc) MustEncode(val, size, digits uint) []rune { + res, err := abc.Encode(val, size, digits) + if err == nil { + return res + } + panic(err) +} + +func maskedRandomInts(size, mask int) []int { + ints := make([]int, size) + bytes := make([]byte, size) + if _, err := randc.Read(bytes); err == nil { + for i, b := range bytes { + ints[i] = int(b) & mask + } + } else { + for i := range ints { + ints[i] = randm.Intn(0xff) & mask + } + } + return ints +} + +// String returns a string representation of the Abc instance. +func (abc Abc) String() string { + return fmt.Sprintf("Abc{alphabet='%v')", abc.Alphabet()) +} + +// Alphabet returns the alphabet used as an immutable string. +func (abc Abc) Alphabet() string { + return string(abc.alphabet) +} diff --git a/lib/store/sqlike/sqlike.go b/lib/store/sqlike/sqlike.go new file mode 100644 index 0000000..6f0bff3 --- /dev/null +++ b/lib/store/sqlike/sqlike.go @@ -0,0 +1,328 @@ +package sqlike + +import ( + "context" + "crypto/sha1" + "database/sql" + "encoding/base64" + "errors" + "fmt" + "html" + "strconv" + "time" + + "git.tuxpa.in/a/nat/lib/idgen" + "git.tuxpa.in/a/nat/lib/store" + "github.com/dchest/uniuri" + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +type SqlikeConfig struct { + 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 +} + +var _ store.Store = (*Sqlike)(nil) + +type Sqlike struct { + config SqlikeConfig + handle *sql.DB +} + +func MustNew(config ...SqlikeConfig) *Sqlike { + o, err := New(config...) + if err != nil { + panic(err) + } + return o +} + +func New(config ...SqlikeConfig) (*Sqlike, error) { + s := &Sqlike{} + // default settings + s.config.DBType = "sqlite3" + s.config.DBHost = "db.sqlite" + s.config.DBName = "pastebin" + s.config.DBAccountsTable = "accounts" + if len(config) > 0 { + s.config = config[0] + } + if err := s.connect(); err != nil { + return nil, err + } + return s, nil +} + +func (s *Sqlike) connect() error { + var dbinfo string + for i := 0; i < 7; i++ { + s.config.DBPlaceHolder[i] = "?" + } + + switch s.config.DBType { + case "sqlite3": + dbinfo = s.config.DBName + case "postgres": + dbinfo = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + s.config.DBHost, + s.config.DBPort, + s.config.DBUser, + s.config.DBPassword, + s.config.DBName) + for i := 0; i < 7; i++ { + s.config.DBPlaceHolder[i] = "$" + strconv.Itoa(i+1) + } + case "mysql": + dbinfo = s.config.DBUser + ":" + s.config.DBPassword + "@tcp(" + s.config.DBHost + ":" + s.config.DBPort + ")/" + s.config.DBName + case "": + return errors.New(" Database error : dbtype not specified in sqlike config") + + default: + return errors.New(" Database error : Specified dbtype (" + + s.config.DBType + ") not supported.") + } + + db, err := sql.Open(s.config.DBType, dbinfo) + if err != nil { + return err + } + + var dummy string + err = db.QueryRow("select id from " + s.config.DBTable + " where id='dummyid'").Scan(&dummy) + + switch { + case err == sql.ErrNoRows: + case err != nil: + return err + } + s.handle = db + return nil +} + +func shaPaste(paste string) string { + hasher := sha1.New() + hasher.Write([]byte(paste)) + sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + return sha +} + +func (s *Sqlike) SavePaste(ctx context.Context, title string, data string, expiry time.Duration, userKey string) (*store.Response, error) { + var id, hash, delkey string + + // Escape user input, + data = html.EscapeString(data) + title = html.EscapeString(title) + userKey = html.EscapeString(userKey) + + // Hash paste data and query database to see if paste exists + sha := shaPaste(data) + + err := s.handle.QueryRow("select id, title, hash, data, delkey from "+ + s.config.DBTable+" where hash="+ + s.config.DBPlaceHolder[0], sha).Scan(&id, + &title, &hash, &data, &delkey) + switch { + case err == sql.ErrNoRows: + case err != nil: + return nil, err + default: + return &store.Response{ + Status: "Paste data already exists ...", + Id: id, + Title: title, + Sha1: hash, + Size: len(data)}, nil + } + + // Generate id, + id = idgen.MustGenerate() + + expiretime := time.Now().Add(expiry) + // 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 += s.config.DBPlaceHolder[i] + "," + } + dbQuery = dbQuery[:len(dbQuery)-1] + + stmt, err := s.handle.Prepare("INSERT INTO " + s.config.DBTable + " (id,title,hash,data,delkey,expiry,userid)values(" + dbQuery + ")") + if err != nil { + return nil, err + } + + _, err = stmt.Exec(id, title, sha, data, delKey, expiretime, userKey) + if err != nil { + return nil, err + } + stmt.Close() + if err != nil { + return nil, err + } + return &store.Response{ + Status: "Successfully saved paste.", + Id: id, + Title: title, + Sha1: hash, + Size: len(data), + DelKey: delKey}, nil +} + +func (s *Sqlike) GetUserPastes(ctx context.Context, userKey string) (*store.Pastes, error) { + pst := &store.Pastes{} + rows, err := s.handle.Query("select id, title, delkey, data from "+ + s.config.DBTable+" where userid="+ + s.config.DBPlaceHolder[0], userKey) + switch { + case err == sql.ErrNoRows: + case err != nil: + return nil, err + default: + defer rows.Close() + for rows.Next() { + var id, title, delKey, data string + rows.Scan(&id, &title, &delKey, &data) + res := store.Response{ + Id: id, + Title: title, + Size: len(data), + DelKey: delKey} + pst.Response = append(pst.Response, res) + } + } + return pst, nil +} +func (s *Sqlike) GetUserKey(ctx context.Context, email string) (string, error) { + var user_key string + err := s.handle.QueryRowContext(ctx, "select key from "+s.config.DBAccountsTable+ + " where email="+s.config.DBPlaceHolder[0], email). + Scan(&user_key) + if err != nil { + return "", err + } + return user_key, nil +} + +func (s *Sqlike) GetPaste(ctx context.Context, pasteId string) (*store.Response, error) { + var title, paste string + var expiry int64 + err := s.handle.QueryRowContext(ctx, "select title, data, expiry from "+ + s.config.DBTable+" where id="+s.config.DBPlaceHolder[0], + pasteId).Scan(&title, &paste, &expiry) + + switch { + case err == sql.ErrNoRows: + return &store.Response{Status: "Requested paste doesn't exist."}, nil + case err != nil: + return nil, err + } + expiretime := time.Unix(expiry, 0) + if expiry == 0 { + expiretime = time.Time{} + } + // Check if paste is overdue, + ok := time.Now().After(expiretime) + if err != nil || !ok { + return &store.Response{Status: "Requested paste doesn't exist."}, nil + } + + // 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 := &store.Response{ + Status: "Success", + Id: pasteId, + Title: title, + Paste: paste, + Size: len(paste), + Expiry: expiryS} + + return r, nil +} + +func (s *Sqlike) ForceDelPaste(ctx context.Context, pasteId string) error { + stmt, err := s.handle.PrepareContext(ctx, "delete from pastebin where id="+ + s.config.DBPlaceHolder[0]) + if err != nil { + return err + } + defer stmt.Close() + // Execute it, + _, err = stmt.ExecContext(ctx, pasteId) + if err != nil { + return err + } + return nil +} +func (s *Sqlike) RegisterUser(ctx context.Context, email string, hashpass []byte) error { + var dbQuery string + for i := 0; i < 3; i++ { + dbQuery += s.config.DBPlaceHolder[i] + "," + } + dbQuery = dbQuery[:len(dbQuery)-1] + stmt, err := s.handle.PrepareContext(ctx, "INSERT into "+s.config.DBAccountsTable+"(email, password, key) values("+dbQuery+")") + defer stmt.Close() + if err != nil { + return err + } + key := idgen.MustGenerate() + _, err = stmt.ExecContext(ctx, email, hashpass, key) + if err != nil { + return err + } + return nil +} +func (s *Sqlike) DelPaste(ctx context.Context, pasteId, delKey string) error { + stmt, err := s.handle.PrepareContext(ctx, "delete from pastebin where delkey="+ + s.config.DBPlaceHolder[0]+" and id="+ + s.config.DBPlaceHolder[1]) + if err != nil { + return err + } + defer stmt.Close() + res, err := stmt.ExecContext(ctx, delKey, pasteId) + _, err = res.RowsAffected() + if err == sql.ErrNoRows { + return nil + } + if err != nil { + return err + } + return nil +} + +func (s *Sqlike) HasAccount(ctx context.Context, email string) ([]byte, error) { + var hashedPassword []byte + err := s.handle.QueryRowContext(ctx, "select password from "+s.config.DBAccountsTable+ + " where email="+s.config.DBPlaceHolder[0], email). + Scan(&hashedPassword) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return hashedPassword, nil +} diff --git a/lib/store/store.go b/lib/store/store.go new file mode 100644 index 0000000..09e877b --- /dev/null +++ b/lib/store/store.go @@ -0,0 +1,39 @@ +package store + +import ( + "context" + "time" +) + +// A request to the store will be 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 +} + +type Pastes struct { + Response []Response +} + +type Store interface { + GetUserKey(ctx context.Context, email string) (string, error) + GetUserPastes(ctx context.Context, userKey string) (*Pastes, error) + GetPaste(ctx context.Context, pasteId string) (*Response, error) + + SavePaste(ctx context.Context, title string, data string, expiry time.Duration, userKey string) (*Response, error) + + ForceDelPaste(ctx context.Context, pasteId string) error + DelPaste(ctx context.Context, pasteId, delKey string) error + + HasAccount(ctx context.Context, email string) ([]byte, error) + RegisterUser(ctx context.Context, email string, hashpass []byte) error +} diff --git a/lib/styler/styler.go b/lib/styler/styler.go new file mode 100644 index 0000000..33b82c4 --- /dev/null +++ b/lib/styler/styler.go @@ -0,0 +1,218 @@ +package styler + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os" + "os/exec" + "strings" + "unicode/utf8" +) + +type Highlighter string + +const ( + HIGHLIGHTER_NONE Highlighter = "none" + HIGHLIGHTER_PYTHON Highlighter = "python" + HIGHLIGHTER_GO Highlighter = "go" +) + +func (h Highlighter) Cmd() string { + switch h { + case HIGHLIGHTER_PYTHON: + return ".highlighers/highlighter-wrapper.py" + case HIGHLIGHTER_GO: + return ".highlighers/highligher.gobin" + case HIGHLIGHTER_NONE: + fallthrough + default: + return "" + } +} + +type Styler struct { + listOfLangsFirst map[string]string + listOfLangsLast map[string]string + listOfStyles map[string]string + + config StylerConfig +} + +func (s *Styler) Legacy() [3]map[string]string { + return [3]map[string]string{ + s.listOfLangsFirst, + s.listOfLangsLast, + s.listOfStyles, + } +} + +type StylerConfig struct { + Highlighter Highlighter `json:"highlighter"` // The name of the highlighter. +} + +func New(config ...StylerConfig) *Styler { + s := &Styler{ + listOfLangsFirst: make(map[string]string), + listOfLangsLast: make(map[string]string), + listOfStyles: make(map[string]string), + } + // default settings + s.config.Highlighter = "none" + if len(config) > 0 { + s.config = config[0] + } + return s +} +func isBad(s string) bool { + for i := 0; i < len(s); i++ { + b := s[i] + if ('a' <= b && b <= 'z') || + ('A' <= b && b <= 'Z') || + ('0' <= b && b <= '9') || + b == ' ' { + continue + } else { + return false + } + } + return true +} + +func sanitize(s string) string { + if !utf8.ValidString(s) { + return s + } + if len(s) < 30 { + return s + } + if !isBad(s) { + return s + } + return "" +} + +func (s *Styler) Highlight(paste string, lang string, style string) (string, string, string, string, error) { + var supported_lang, supported_styles bool + lang = sanitize(lang) + style = sanitize(style) + lang, supported_lang = s.listOfLangsFirst[lang] + style, supported_styles = s.listOfStyles[style] + lang = sanitize(lang) + style = sanitize(style) + + if lang == "" { + lang = "autodetect" + } + + if !supported_lang && lang != "autodetect" { + lang = "text" + } + + // Same with the styles, + if !supported_styles { + style = "autodetect" + } + switch s.config.Highlighter { + case HIGHLIGHTER_PYTHON: + if _, err := os.Stat(s.config.Highlighter.Cmd()); os.IsNotExist(err) { + return "", "", "", "", err + } + cmd := exec.Command(s.config.Highlighter.Cmd(), 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 { + return "", "", "", "", err + } + return stdout.String(), stderr.String(), lang, style, nil + case "none": + fallthrough + default: + return paste, "", lang, style, nil + } +} + +func (s *Styler) loadSupportedStyles() { + switch s.config.Highlighter { + case HIGHLIGHTER_PYTHON: + arg := "getstyles" + out, err := exec.Command(s.config.Highlighter.Cmd(), 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.listOfStyles[line] = strings.Title(line) + } + case "none": + fallthrough + default: + return + } +} + +func (st *Styler) loadSupportedLangs() error { + switch st.config.Highlighter { + case HIGHLIGHTER_PYTHON: + var prioLexers map[string]string + + // Initialize maps, + prioLexers = make(map[string]string) + + // Get prioritized lexers and put them in a separate map, + file, err := os.Open("static/prio-lexers") + if err != nil { + return 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(st.config.Highlighter.Cmd(), arg).Output() + if err != nil { + return fmt.Errorf("%w: %s", 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 { + os.Exit(1) + } + s[0] = strings.Title(s[0]) + if prioLexers[s[0]] == "1" { + st.listOfLangsFirst[s[0]] = s[1] + } else { + st.listOfLangsLast[s[0]] = s[1] + } + } + return nil + case "none": + fallthrough + default: + return nil + } +} diff --git a/pastebin.go b/pastebin.go deleted file mode 100644 index d9baf13..0000000 --- a/pastebin.go +++ /dev/null @@ -1,1185 +0,0 @@ -// 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) -} diff --git a/assets/index.html b/static/index.html similarity index 100% rename from assets/index.html rename to static/index.html diff --git a/assets/login.html b/static/login.html similarity index 100% rename from assets/login.html rename to static/login.html diff --git a/assets/pastebin.css b/static/pastebin.css similarity index 100% rename from assets/pastebin.css rename to static/pastebin.css diff --git a/assets/pastes.html b/static/pastes.html similarity index 100% rename from assets/pastes.html rename to static/pastes.html diff --git a/assets/prio-lexers b/static/prio-lexers similarity index 100% rename from assets/prio-lexers rename to static/prio-lexers diff --git a/assets/register.html b/static/register.html similarity index 100% rename from assets/register.html rename to static/register.html diff --git a/assets/syntax.html b/static/syntax.html similarity index 100% rename from assets/syntax.html rename to static/syntax.html