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