597 lines
16 KiB
Go
597 lines
16 KiB
Go
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)
|
|
}
|
|
}
|