package main import ( "bytes" "crypto/rand" "embed" _ "embed" "encoding/base64" "encoding/json" "errors" "flag" "fmt" "io" "io/fs" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" "golang.org/x/term" ) // Default to the system umask const OPEN_MODE = 0777 var ( Version = "git or development" logger = log.New(os.Stderr, "", 0) ID_BYTES = 8 //go:embed static/* staticEmbedded embed.FS ) type App struct { static fs.FS users map[string]string storage string // path to where the paste files are actually stored } // EnvFlagString is a convent way to set value from environment variable, // and allow override when a command line flag is set. It's assumed `p` is // not nil. func EnvFlagString(fl *flag.FlagSet, p *string, name, envvar, usage string) { if v := os.Getenv(envvar); v != "" { *p = v } fl.StringVar(p, name, *p, fmt.Sprintf("%s (Environ: '%s')", usage, envvar)) } type errResp struct { w http.ResponseWriter Code int Msg string } func main() { var ( listen = ":6130" idBytes = "8" debug = "false" genhash = false storage = "" fsdir = "" static fs.FS err error ) static, err = fs.Sub(staticEmbedded, "static") if err != nil { logger.Fatal("Embedding failed no static directory") } fl := flag.NewFlagSet("Simple pastebin", flag.ExitOnError) EnvFlagString(fl, &listen, "listen", "LISTEN_ADDR", "Address to bind to") EnvFlagString(fl, &idBytes, "b", "ID_BYTES", "How many bytes long should the IDs be") EnvFlagString(fl, &debug, "d", "DEBUG", "Additional logger flags for debugging") EnvFlagString(fl, &storage, "s", "STORAGE_DIR", "Path of directory to serve") EnvFlagString(fl, &fsdir, "fs", "FS_DIR", "Path which static assets are located, empty to use embedded") fl.BoolVar(&genhash, "genhash", genhash, "Interactively prompt for a password and spit out a hash\n") version := fl.Bool("v", false, "Print version then exit") _ = fl.Parse(os.Args[1:]) if *version { log.Println(Version) os.Exit(0) } if genhash { interactiveHashGen() os.Exit(0) } if d, err := strconv.ParseBool(debug); err != nil && d { logger.SetFlags(log.LstdFlags | log.Llongfile) } if fsdir != "" { static = os.DirFS(fsdir) } if b, err := strconv.Atoi(idBytes); err != nil && b > 4 { ID_BYTES = b } if storage == "" { logger.Fatal("Cannot continue without storage directory, set " + "`-s` flag or STORAGE_DIR environment variable") } app := &App{ static: static, storage: storage, users: getUsersFromEnviron(), } logger.Println("listening on: ", listen) srv := &http.Server{ Handler: logRequests(app.Handler()), Addr: listen, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } logger.Fatal(srv.ListenAndServe()) } func logRequests(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger.Printf("%s %s %s \"%s\" \"%s\"\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent(), r.Referer()) next.ServeHTTP(w, r) }) } func interactiveHashGen() { fmt.Print("Enter password: ") passwd, err := term.ReadPassword(0) if err != nil { logger.Fatal("\nFailed: ", err) } fmt.Printf("\nAgain: ") passwd2, err := term.ReadPassword(0) if err != nil { logger.Fatal("\nFailed: ", err) } fmt.Println("") if !bytes.Equal(passwd, passwd2) { logger.Fatal("Passwords do not match") } passwd, err = bcrypt.GenerateFromPassword(passwd, bcrypt.DefaultCost) if err != nil { logger.Fatal("Failed: ", err) } fmt.Printf("hash: %s\n", string(passwd)) } func (a *App) Handler() http.Handler { mux := http.NewServeMux() secHandlers := map[string]http.Handler{ "/new": a.HandleNew(), "/list": a.HandleList(), "/api/v1/new": a.HandleNewJSON(), "/api/v1/list": a.HandleListJSON(), } handlers := map[string]http.Handler{ "/api/v1/view": http.StripPrefix( "/api/v1/view/", a.HandleViewJSON()), "/view": http.StripPrefix( "/view/", a.HandleViewPlain()), "/": http.FileServer(http.FS(a.static)), } if len(a.users) > 0 { for user := range a.users { logger.Println("Found user:", user) } for pth, handler := range secHandlers { mux.Handle(pth, authHandler( handler, a.users, )) } } else { _, _ = fmt.Fprintf(os.Stderr, "\033[1;31mWARNING: RUNNING WITH NO AUTHENTICATION\033[0m\n") for pth, handler := range secHandlers { mux.Handle(pth, handler) } } for pth, handler := range handlers { mux.Handle(pth, handler) } return mux } func getUsersFromEnviron() map[string]string { users := map[string]string{} for _, entry := range os.Environ() { if !strings.HasPrefix(entry, "USER_") { continue } e := strings.SplitN(entry, "=", 2) key := e[0] val := e[1] username := strings.TrimPrefix(key, "USER_") users[username] = val } return users } func sendErr(er errResp) { er.w.WriteHeader(er.Code) er.w.Header().Add("Content-type", "application/json") enc := json.NewEncoder(er.w) _ = enc.Encode(er) } func GenId() string { r := make([]byte, ID_BYTES) _, err := rand.Read(r) if err != nil { logger.Fatal(err) } return base64.RawURLEncoding.EncodeToString(r) } func authHandler(next http.Handler, users map[string]string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { respUnAuth := func() { logger.Println("Unauthed") w.Header().Add("Content-type", "text/plain") w.Header().Add("WWW-Authenticate", "Basic realm=\"Login\", charset=\"UTF-8\"") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte("Unauthorized\n")) } username, passwd, ok := r.BasicAuth() if _, haveUser := users[username]; !ok || !haveUser { respUnAuth() return } err := bcrypt.CompareHashAndPassword( []byte(users[username]), []byte(passwd), ) if errors.Is(err, bcrypt.ErrHashTooShort) { logger.Println("Hash too short for username: ", username) } if err != nil { respUnAuth() return } next.ServeHTTP(w, r) }) } func (a *App) HandleNew() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := GenId() fh, err := os.OpenFile(id, os.O_CREATE|os.O_EXCL|os.O_RDWR, OPEN_MODE) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } _, err = io.Copy(fh, r.Body) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } w.WriteHeader(http.StatusOK) w.Header().Add("Content-type", "text/plain") enc := json.NewEncoder(w) _ = enc.Encode(struct { Id string }{id}) }) } type NewPost struct { Content string `json:"content"` } func (a *App) HandleNewJSON() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := GenId() p := &NewPost{} dec := json.NewDecoder(r.Body) err := dec.Decode(p) if err != nil || p.Content == "" { sendErr(errResp{w, http.StatusBadRequest, "Malformed JSON"}) return } fh, err := os.OpenFile(id, os.O_CREATE|os.O_EXCL|os.O_RDWR, OPEN_MODE) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } _, err = io.Copy(fh, bytes.NewBufferString(p.Content)) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } sendErr(errResp{w, http.StatusOK, "OK"}) }) } func (a *App) HandleViewJSON() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pth := filepath.Join(a.storage, strings.ReplaceAll(filepath.Clean(r.URL.Path), "/", "")) fh, err := os.Open(pth) if err != nil { sendErr(errResp{w, http.StatusNotFound, "ID not found"}) return } b, err := io.ReadAll(fh) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } w.WriteHeader(http.StatusOK) w.Header().Add("Content-type", "application/json") enc := json.NewEncoder(w) _ = enc.Encode(struct { Content string }{string(b)}) }) } func (a *App) HandleViewPlain() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pth := filepath.Join(a.storage, strings.ReplaceAll(filepath.Clean(r.URL.Path), "/", "")) fh, err := os.Open(pth) if err != nil { sendErr(errResp{w, http.StatusNotFound, "ID not found"}) return } w.WriteHeader(http.StatusOK) w.Header().Add("Content-type", "text/plain") _, _ = io.Copy(w, fh) }) } func (a *App) HandleList() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := &bytes.Buffer{} de, err := os.ReadDir(a.storage) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } for _, e := range de { if e.IsDir() { continue } info, err := e.Info() if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } _, _ = buf.Write([]byte(fmt.Sprintf("%d\t%s\t%s\n", info.Size(), info.ModTime().Format("2006-01-02 15:04 MST"), e.Name()))) } _, _ = io.Copy(w, buf) }) } type PasteListing struct { Id string `json:"id"` Created time.Time `json:"created"` Size int64 `json:"size"` } func (a *App) HandleListJSON() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { enc := json.NewEncoder(w) enc.SetIndent("", " ") out := []*PasteListing{} de, err := os.ReadDir(a.storage) if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } for _, e := range de { if e.IsDir() { continue } info, err := e.Info() if err != nil { sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } out = append(out, &PasteListing{ e.Name(), info.ModTime(), info.Size(), }) } _ = enc.Encode(out) }) }