nat/cmd/server/main.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)
}
}