From 0704674ba408db54855c33bcb8ca71a7ae1e74b7 Mon Sep 17 00:00:00 2001 From: Mitchell Riedstra Date: Sun, 25 Dec 2022 19:13:40 -0500 Subject: Strip brack to a basic application based off of bpaste again, prepare for a React UI --- .gitignore | 2 + build.sh | 3 +- client/main.go | 203 --------------- go.mod | 13 +- go.sum | 24 +- main.go | 773 ++++++++++++++++++++++----------------------------------- readme.md | 19 +- 7 files changed, 311 insertions(+), 726 deletions(-) delete mode 100644 client/main.go diff --git a/.gitignore b/.gitignore index cc10a06..333aca1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ bpaste +paste +.idea/* diff --git a/build.sh b/build.sh index eea4a15..8fad3bd 100755 --- a/build.sh +++ b/build.sh @@ -17,5 +17,4 @@ fi export CGO_ENABLED=0 -go build -ldflags="-X 'main.Version=$version'" -o paste . -go build -ldflags="-X 'main.Version=$version'" -o paste-client ./client +go build -ldflags="-X 'main.Version=$version'" . diff --git a/client/main.go b/client/main.go deleted file mode 100644 index d322e84..0000000 --- a/client/main.go +++ /dev/null @@ -1,203 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -var Version = "Development" - -var ConfigFn = filepath.Join(os.Getenv("HOME"), ".paste") -var TokenFN = filepath.Join(os.Getenv("HOME"), ".paste-token") - -var logger = log.New(os.Stderr, "", 0) -var httpC = &http.Client{} - -type Paste struct { - Id string - Title string - Tags map[string]struct{} - Content string -} - -type Config struct { - Username string - Password string - Hostname string -} - -func loadConfig(fn string) (*Config, error) { - fh, err := os.Open(fn) - if err != nil { - return nil, err - } - dec := yaml.NewDecoder(fh) - dec.KnownFields(true) - - c := &Config{} - err = dec.Decode(c) - - return c, err -} - -func (c *Config) NoTokenReq(method, pth string, data interface{}) (*http.Response, error) { - b, err := json.Marshal(data) - if err != nil { - return nil, err - } - - buf := bytes.NewBuffer(b) - - req, err := http.NewRequest(method, c.Hostname+pth, buf) - if err != nil { - logger.Fatal(err) - } - req.Header.Add("Content-type", "application/json") - - resp, err := httpC.Do(req) - if err != nil { - logger.Fatal(err) - } - - return resp, err -} - -func (c *Config) Req(method, pth string, data interface{}) (*http.Response, error) { - b, err := json.Marshal(data) - if err != nil { - return nil, err - } - - buf := bytes.NewBuffer(b) - - req, err := http.NewRequest(method, c.Hostname+pth, buf) - if err != nil { - logger.Fatal(err) - } - req.Header.Add("Authorization", "Bearer "+c.Token()) - req.Header.Add("Content-type", "application/json") - - resp, err := httpC.Do(req) - if err != nil { - logger.Fatal(err) - } - - return resp, err -} - -func (c *Config) Token() string { - s, err := c.GetToken() - if err != nil { - logger.Fatal(err) - } - return s -} - -func (c *Config) GetToken() (string, error) { - resp, err := c.NoTokenReq("POST", "/login", map[string]string{ - "Username": c.Username, - "Password": c.Password, - }) - if err != nil { - return "", err - } - - respData := map[string]string{} - - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&respData) - if err != nil { - return "", err - } - - token, ok := respData["token"] - if !ok { - logger.Fatal("Cannot have empty token") - } - - // logger.Println(token) - - return token, nil -} - -func (c *Config) NewPaste(title string) { - buf := &bytes.Buffer{} - - _, err := io.Copy(buf, os.Stdin) - if err != nil { - logger.Fatal(err) - } - - p := &Paste{ - Title: title, - Content: buf.String(), - } - - resp, err := c.Req("POST", "/new", p) - if err != nil { - logger.Fatal(err) - } - - if resp.StatusCode != 200 { - io.Copy(os.Stderr, resp.Body) - os.Exit(1) - } - - out := map[string]string{} - - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&out) - if err != nil { - logger.Println(err) - } - - fmt.Printf("%s/view/%s\n", c.Hostname, out["id"]) -} - -func main() { - fl := flag.NewFlagSet("simple pastebin client", flag.ExitOnError) - - fl.StringVar(&ConfigFn, "c", ConfigFn, "Configuration file") - title := fl.String("t", "", "Optional title for the message") - debug := fl.Bool("d", false, "debugging add information to the logging output DEBUG=true|false controls this as well") - version := fl.Bool("v", false, "Print version and exit") - - _ = fl.Parse(os.Args[1:]) - - if *version { - logger.Fatal(Version) - } - - if d := os.Getenv("DEBUG"); d == "true" || *debug { - logger.SetFlags(log.LstdFlags | log.Llongfile) - } - - conf, err := loadConfig(ConfigFn) - if err != nil { - logger.Println(err) - - fmt.Println("") - fmt.Println("//") - fmt.Println("// Example configuration file") - enc := yaml.NewEncoder(os.Stdout) - enc.SetIndent(2) - enc.Encode(&Config{ - Username: "changeme", - Password: "changeme", - Hostname: "https://example.com/paste", - }) - - os.Exit(1) - } - - conf.NewPaste(*title) -} diff --git a/go.mod b/go.mod index 3d92265..9f3323b 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ -module riedstra.dev/mitch/bpaste +module riedstra.dev/mitch/paste -go 1.16 +go 1.19 require ( - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-jwt/jwt/v4 v4.0.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + golang.org/x/crypto v0.4.0 + golang.org/x/term v0.3.0 ) + +require golang.org/x/sys v0.3.0 // indirect diff --git a/go.sum b/go.sum index fbde530..4742e51 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,6 @@ -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= diff --git a/main.go b/main.go index df0711a..c3f5613 100644 --- a/main.go +++ b/main.go @@ -1,649 +1,460 @@ package main import ( - "crypto/ed25519" + "bytes" "crypto/rand" "embed" + _ "embed" "encoding/base64" "encoding/json" - "encoding/pem" "errors" "flag" "fmt" - "html/template" "io" + "io/fs" "log" "net/http" "os" "path/filepath" + "strconv" "strings" "time" - "github.com/golang-jwt/jwt/v4" - "github.com/gorilla/mux" "golang.org/x/crypto/bcrypt" - "gopkg.in/yaml.v3" + "golang.org/x/term" ) -var Version = "Development" +// Default to the system umask +const OPEN_MODE = 0777 -var logger = log.New(os.Stderr, "", 0) +var ( + Version = "git or development" + logger = log.New(os.Stderr, "", 0) + ID_BYTES = 8 -var ID_BYTES = 8 - -//go:embed templates/* -var templateFS embed.FS - -var tpls = map[string]*template.Template{} + //go:embed static/* + staticEmbedded embed.FS +) -func init() { - // List of templates that will be pre-rendered and toss into - // the tpls map above - tplList := []string{"index", "view", "error"} - for _, tpl := range tplList { - tpls[tpl] = template.Must(template.ParseFS(templateFS, - "templates/"+tpl+".tpl", - "templates/base.tpl", - )) - } +type App struct { + static fs.FS + users map[string]string + storage string // path to where the paste files are actually stored } -//go:embed static/* -var staticFS embed.FS - -type Paste struct { - Id string - Title string - Tags map[string]struct{} - Content string +// 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)) } -func (p Paste) GetContent() string { - return string(p.Content) +type errResp struct { + w http.ResponseWriter + Code int + Msg string } -func (p *Paste) Load() error { - fh, err := os.Open(filepath.Join("p", p.Id)) - if err != nil { - return err - } +func main() { + var ( + listen = ":6130" + idBytes = "8" + debug = "false" + genhash = false + storage = "" + fsdir = "" + static fs.FS + err error + ) - dec := json.NewDecoder(fh) - err = dec.Decode(&p) + static, err = fs.Sub(staticEmbedded, "static") if err != nil { - return err - } + 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") - return fh.Close() -} + _ = fl.Parse(os.Args[1:]) -func (p Paste) Save() error { - fh, err := os.OpenFile(filepath.Join("p", p.Id), os.O_CREATE|os.O_RDWR, 0666) - if err != nil { - return err + if *version { + log.Println(Version) + os.Exit(0) } - enc := json.NewEncoder(fh) - enc.SetIndent("", " ") - err = enc.Encode(&p) - if err != nil { - return err + if genhash { + interactiveHashGen() + os.Exit(0) } - return fh.Close() -} + if d, err := strconv.ParseBool(debug); err != nil && d { + logger.SetFlags(log.LstdFlags | log.Llongfile) + } -func GenId() string { - r := make([]byte, ID_BYTES) - _, err := rand.Read(r) - if err != nil { - logger.Fatal(err) + if fsdir != "" { + static = os.DirFS(fsdir) } - return base64.RawURLEncoding.EncodeToString(r) -} + if b, err := strconv.Atoi(idBytes); err != nil && b > 4 { + ID_BYTES = b + } -type User struct { - Username string `yaml:"Username"` - Password string `yaml:"Password"` - HashedPassword string `yaml:"HashedPassword"` -} + if storage == "" { + logger.Fatal("Cannot continue without storage directory, set " + + "`-s` flag or STORAGE_DIR environment variable") + } -// CheckPass returns true if passwords match -func (u *User) CheckPass(pass string) bool { - err := bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(pass)) - if err != nil { - return false + app := &App{ + static: static, + storage: storage, + users: getUsersFromEnviron(), } - return true -} + logger.Println("listening on: ", listen) -type Conf struct { - // Populated after read used for lookups - Users map[string]*User `yaml:"Users"` + srv := &http.Server{ + Handler: logRequests(app.Handler()), + Addr: listen, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + logger.Fatal(srv.ListenAndServe()) } -func hashPasswd(pass string) (string, error) { - b, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) - return string(b), err +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 writeSampleConf(fn string) (*os.File, error) { - c := &Conf{ - Users: map[string]*User{ - "admin": &User{ - Password: "admin", - }, - }, +func interactiveHashGen() { + fmt.Print("Enter password: ") + passwd, err := term.ReadPassword(0) + if err != nil { + logger.Fatal("\nFailed: ", err) } - fh, err := os.OpenFile(fn, os.O_CREATE|os.O_RDWR, 0600) + fmt.Printf("\nAgain: ") + passwd2, err := term.ReadPassword(0) if err != nil { - return nil, err + logger.Fatal("\nFailed: ", err) } - enc := yaml.NewEncoder(fh) - enc.SetIndent(2) - - err = enc.Encode(c) - if err != nil { - return nil, err + fmt.Println("") + if !bytes.Equal(passwd, passwd2) { + logger.Fatal("Passwords do not match") } - err = fh.Close() + passwd, err = bcrypt.GenerateFromPassword(passwd, bcrypt.DefaultCost) if err != nil { - return nil, err + logger.Fatal("Failed: ", err) } - return os.Open(fn) + fmt.Printf("hash: %s\n", string(passwd)) } -func readConf(fn string) (*Conf, error) { - fh, err := os.Open(fn) - if errors.Is(err, os.ErrNotExist) { - fh, err = writeSampleConf(fn) - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } +func (a *App) Handler() http.Handler { + mux := http.NewServeMux() - dec := yaml.NewDecoder(fh) - dec.KnownFields(true) + secHandlers := map[string]http.Handler{ + "/new": a.HandleNew(), + "/list": a.HandleList(), - c := &Conf{} - err = dec.Decode(c) - if err != nil { - return nil, err + "/api/v1/new": a.HandleNewJSON(), + "/api/v1/list": a.HandleListJSON(), } - fh.Close() - - changed := false - // Convert any plain passwords to HashedPasswords - for n, u := range c.Users { - u.Username = n - if u.Password != "" { - u.HashedPassword, err = hashPasswd(u.Password) - if err != nil { - return nil, err - } - u.Password = "" - changed = true - } - } - - if changed { - fh, err := os.OpenFile(fn, os.O_TRUNC|os.O_RDWR, 0600) - if err != nil { - return nil, err - } - enc := yaml.NewEncoder(fh) - enc.SetIndent(2) - err = enc.Encode(c) - if err != nil { - return nil, err - } - fh.Close() - } + handlers := map[string]http.Handler{ + "/api/v1/view": http.StripPrefix( + "/api/v1/view/", + a.HandleViewJSON()), - return c, nil -} + "/view": http.StripPrefix( + "/view/", + a.HandleViewPlain()), -func loadOrGenKeys() (ed25519.PublicKey, ed25519.PrivateKey, error) { - var ( - key ed25519.PrivateKey - pub ed25519.PublicKey - err error - ) - if _, err = os.Stat("key"); err != nil { - pub, key, err = ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, nil, err - } + "/": http.FileServer(http.FS(a.static)), + } - fh, err := os.OpenFile("key", os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return nil, nil, err + if len(a.users) > 0 { + for user := range a.users { + logger.Println("Found user:", user) } - err = pem.Encode(fh, &pem.Block{ - Type: "ED25519 PRIVATE KEY", - Bytes: key, - }) - if err != nil { - return nil, nil, err + for pth, handler := range secHandlers { + mux.Handle(pth, authHandler( + handler, + a.users, + )) } - - fh.Close() } else { - fh, err := os.Open("key") - if err != nil { - return nil, nil, err - } + _, _ = fmt.Fprintf(os.Stderr, + "\033[1;31mWARNING: RUNNING WITH NO AUTHENTICATION\033[0m\n") - b, err := io.ReadAll(fh) - if err != nil { - return nil, nil, err - } - - blk, _ := pem.Decode(b) - if blk == nil || blk.Type != "ED25519 PRIVATE KEY" { - return nil, nil, errors.New("Failed to decode PEM file on disk") + for pth, handler := range secHandlers { + mux.Handle(pth, handler) } + } - key = ed25519.PrivateKey(blk.Bytes) - pub = key.Public().(ed25519.PublicKey) - fh.Close() + for pth, handler := range handlers { + mux.Handle(pth, handler) } - return pub, key, err + return mux } -func main() { - fl := flag.NewFlagSet("simple pastebin", flag.ExitOnError) - listen := fl.String("listen", ":6130", "Address to bind to, LISTEN_ADDR environment variable overrides") - debug := fl.Bool("d", false, "debugging add information to the logging output DEBUG=true|false controls this as well") - storage := fl.String("s", "", "Directory to serve, must be supplied via flag or STORAGE_DIR environment variable") - fl.IntVar(&ID_BYTES, "b", ID_BYTES, "How many random bytes for the id?") - version := fl.Bool("v", false, "Print version and exit") - - _ = fl.Parse(os.Args[1:]) - - if *version { - logger.Fatal(Version) - } - - if addr := os.Getenv("LISTEN_ADDR"); addr != "" { - *listen = addr - } - if d := os.Getenv("DEBUG"); d == "true" || *debug { - logger.SetFlags(log.LstdFlags | log.Llongfile) - } - if d := os.Getenv("STORAGE_DIR"); d != "" { - *storage = d - } +func getUsersFromEnviron() map[string]string { + users := map[string]string{} + for _, entry := range os.Environ() { + if !strings.HasPrefix(entry, "USER_") { + continue + } - if *storage == "" { - logger.Fatal("Cannot continue without storage directory, set `-s` flag or STORAGE_DIR environment variable") - } + e := strings.SplitN(entry, "=", 2) + key := e[0] + val := e[1] - err := os.MkdirAll(filepath.Join(*storage, "p"), 0755) - if err != nil { - logger.Fatal(err) - } + username := strings.TrimPrefix(key, "USER_") - err = os.Chdir(*storage) - if err != nil { - logger.Fatal(err) + users[username] = val } + return users +} - c, err := readConf("config.yml") - if err != nil { - logger.Fatal(err) - } - logger.Println("Config:") - b, _ := json.MarshalIndent(c, "", " ") - logger.Println(string(b)) +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) +} - pubKey, key, err := loadOrGenKeys() +func GenId() string { + r := make([]byte, ID_BYTES) + _, err := rand.Read(r) if err != nil { logger.Fatal(err) } - r := mux.NewRouter() - - r.Handle("/new", handlerRequireJWT(pubKey, c.Users, handlerNewPasteJSON())) - r.HandleFunc("/view/{id}", handlerFuncLoadPaste) - r.HandleFunc("/view/json/{id}", handlerFuncLoadPasteJson) - r.Handle("/list", handlerRequireJWT(pubKey, c.Users, handlerList())) - r.PathPrefix("/static").Handler(http.FileServer(http.FS(staticFS))) - r.Handle("/login", handlerLogin(key, c.Users)) - r.Handle("/", handlerIndex()) - - logger.Println("listening on: ", *listen) - - srv := &http.Server{ - Handler: r, - Addr: *listen, - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } - logger.Fatal(srv.ListenAndServe()) -} - -func jsonResp(w http.ResponseWriter, code int, data interface{}) { - w.Header().Add("Content-type", "application/json") - w.WriteHeader(code) - - enc := json.NewEncoder(w) - err := enc.Encode(data) - if err != nil { - logger.Println("While jsonResp: ", err) - } + return base64.RawURLEncoding.EncodeToString(r) } -func jsonErr(logMsg string, msg string, - w http.ResponseWriter, statusCode int) { +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")) + } - logger.Println(logMsg) + username, passwd, ok := r.BasicAuth() + if _, haveUser := users[username]; !ok || !haveUser { + respUnAuth() + return + } - w.Header().Add("Content-type", "application/json") - w.WriteHeader(statusCode) + err := bcrypt.CompareHashAndPassword( + []byte(users[username]), + []byte(passwd), + ) - enc := json.NewEncoder(w) - err := enc.Encode(map[string]string{"error": msg}) - if err != nil { - logger.Println("While logMsg: ", err) - } -} - -func handlerLogin(key ed25519.PrivateKey, users map[string]*User) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - reqU := &User{} - dec := json.NewDecoder(r.Body) + if errors.Is(err, bcrypt.ErrHashTooShort) { + logger.Println("Hash too short for username: ", + username) + } - err := dec.Decode(reqU) if err != nil { - jsonErr( - fmt.Sprintf("Encountered error decoding user: %s", err), - "invalid json", - w, http.StatusBadRequest) + respUnAuth() return } - u, ok := users[reqU.Username] + next.ServeHTTP(w, r) + }) +} - if !ok || reqU.Username == "" || reqU.Password == "" { - jsonErr( - "Invalid username or password", - "invalid username or password", - w, http.StatusBadRequest) - return - } +func (a *App) HandleNew() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := GenId() - if !u.CheckPass(reqU.Password) { - jsonErr( - fmt.Sprintf("Bad password for: %s", u.Username), - "bad username or password", - w, http.StatusBadRequest) + 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 } - t := jwt.NewWithClaims(jwt.SigningMethodEdDSA, &jwt.StandardClaims{ - ExpiresAt: time.Now().Unix() + (12 * 60 * 60), - Subject: u.Username, - }) - - s, err := t.SignedString(key) + _, err = io.Copy(fh, r.Body) if err != nil { - jsonErr( - fmt.Sprintf("Failed to sign: %s", err), - "internal server error", - w, http.StatusInternalServerError) + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } - w.Header().Add("Content-type", "application/json") - w.Header().Add("Authorization", "Bearer "+s) + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-type", "text/plain") enc := json.NewEncoder(w) - _ = enc.Encode(map[string]string{ - "token": s, - }) + _ = enc.Encode(struct { + Id string + }{id}) }) } -func handlerRequireJWT(key ed25519.PublicKey, users map[string]*User, - next http.Handler) http.Handler { +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() - tokenS := r.Header.Get("Authorization") - if tokenS == "" { - jsonErr("Empty token received", "Unauthorized: empty token", - w, http.StatusUnauthorized) - return - } - - tokenS = strings.TrimPrefix(tokenS, "Bearer ") + p := &NewPost{} - claims := &jwt.StandardClaims{} - - token, err := jwt.ParseWithClaims(tokenS, claims, - func(token *jwt.Token) (interface{}, error) { - return key, nil - }) + dec := json.NewDecoder(r.Body) - if err != nil { - jsonErr(fmt.Sprintf("Error parsing token: %s", err), - "Unauthorized: invalid token", w, http.StatusUnauthorized) + err := dec.Decode(p) + if err != nil || p.Content == "" { + sendErr(errResp{w, http.StatusBadRequest, "Malformed JSON"}) return } - if !token.Valid { - jsonErr( - fmt.Sprintf("Token for %s expires at: %v", claims.Subject, - claims.ExpiresAt), - "Unauthorized: invalid token", w, http.StatusUnauthorized) + 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 } - u, ok := users[claims.Subject] - - if !ok { - jsonErr( - fmt.Sprintf("User %s not valid", claims.Subject), - "invalid user", w, http.StatusUnauthorized) + _, err = io.Copy(fh, bytes.NewBufferString(p.Content)) + if err != nil { + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } - logger.Printf("%s -> authed user: %s", r.URL.Path, u.Username) - - next.ServeHTTP(w, r) + sendErr(errResp{w, http.StatusOK, "OK"}) }) } -func handlerNewPasteJSON() http.Handler { +func (a *App) HandleViewJSON() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - dec := json.NewDecoder(r.Body) - dec.DisallowUnknownFields() - - paste := &Paste{} - - err := dec.Decode(paste) + pth := filepath.Join(a.storage, strings.ReplaceAll(filepath.Clean(r.URL.Path), "/", "")) + fh, err := os.Open(pth) if err != nil { - jsonErr(fmt.Sprintf("Encountered error decoding: %s", err), - "unable to decode input", w, http.StatusBadRequest) + sendErr(errResp{w, http.StatusNotFound, "ID not found"}) return } - if paste.Content == "" { - jsonErr("Not saving paste with empty content", - "empty content", w, http.StatusBadRequest) + b, err := io.ReadAll(fh) + if err != nil { + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } - paste.Id = GenId() - - err = paste.Save() + 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 { - jsonErr(fmt.Sprintf("Encountered error saving paste: %s", err), - "internal server error", w, http.StatusInternalServerError) + sendErr(errResp{w, http.StatusNotFound, "ID not found"}) return } - jsonResp(w, http.StatusOK, map[string]string{ - "status": "ok", - "id": paste.Id, - }) + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-type", "text/plain") + _, _ = io.Copy(w, fh) }) } -func handlerFuncNewPaste(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - logger.Println(err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - title := r.FormValue("title") - content := r.FormValue("content") - - if title == "" || content == "" { - logger.Println("Empty title or content") - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - p := &Paste{ - Id: GenId(), - Title: title, - Content: content, - } - - err = p.Save() - if err != nil { - logger.Println(err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - http.Redirect(w, r, "/view/"+p.Id, http.StatusFound) -} - -func handlerFuncLoadPasteJson(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - id, ok := vars["id"] - if !ok { - jsonErr("No ID supplied", - "ID must be supplied", w, http.StatusBadRequest) - return - } - - p := &Paste{Id: id} +func (a *App) HandleList() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} - err := p.Load() - if err != nil { - logger.Println(err) - if errors.Is(err, os.ErrNotExist) { - jsonErr( - fmt.Sprintf("Snip with id: %s not found", id), - "ID not found", w, http.StatusNotFound) - } else { - jsonErr( - fmt.Sprintf("Snip with id faild loading: %s", err), - "Failed to load snippet", w, http.StatusInternalServerError) + de, err := os.ReadDir(a.storage) + if err != nil { + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) + return } - return - } - jsonResp(w, http.StatusOK, p) -} + for _, e := range de { + if e.IsDir() { + continue + } -func handlerFuncLoadPaste(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) + info, err := e.Info() + if err != nil { + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) + return + } - id, ok := vars["id"] - if !ok { - w.WriteHeader(http.StatusBadRequest) - err := tpls["error"].Execute(w, map[string]string{ - "Short": "ID Not found", - "Long": "There was no ID supplied", - }) - if err != nil { - logger.Println(err) + _, _ = buf.Write([]byte(fmt.Sprintf("%d\t%s\t%s\n", + info.Size(), + info.ModTime().Format("2006-01-02 15:04 MST"), + e.Name()))) } - return - } - - p := &Paste{Id: id} - err := p.Load() - if err != nil { - logger.Println(err) - if errors.Is(err, os.ErrNotExist) { - w.WriteHeader(http.StatusNotFound) - err = tpls["error"].Execute(w, map[string]string{ - "Short": "ID Not found", - "Long": "ID: " + p.Id + "Was not found", - }) - } else { - w.WriteHeader(http.StatusInternalServerError) - err = tpls["error"].Execute(w, map[string]string{ - "Short": "ID Not found", - "Long": "There was an issue reading ID: " + p.Id, - }) - } - if err != nil { - logger.Println(err) - } - return - } + _, _ = io.Copy(w, buf) + }) +} - err = tpls["view"].Execute(w, p) - if err != nil { - logger.Println(err) - } +type PasteListing struct { + Id string `json:"id"` + Created time.Time `json:"created"` + Size int64 `json:"size"` } -func handlerList() http.Handler { +func (a *App) HandleListJSON() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - lst, err := os.ReadDir("p") + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + out := []*PasteListing{} + + de, err := os.ReadDir(a.storage) if err != nil { - jsonErr( - fmt.Sprintf("Failed reading directory: %s", err), - "Internal server error", - w, http.StatusInternalServerError) + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) return } - out := struct { - Pastes []string - }{ - Pastes: []string{}, - } - - for _, e := range lst { + for _, e := range de { if e.IsDir() { continue } - out.Pastes = append(out.Pastes, e.Name()) - } - jsonResp(w, http.StatusOK, out) - }) -} + info, err := e.Info() + if err != nil { + sendErr(errResp{w, http.StatusInternalServerError, "Internal server error"}) + return + } -func handlerIndex() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := tpls["index"].Execute(w, nil) - if err != nil { - logger.Println(err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + out = append(out, &PasteListing{ + e.Name(), + info.ModTime(), + info.Size(), + }) } + + _ = enc.Encode(out) }) } diff --git a/readme.md b/readme.md index 230e004..7c8d00b 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,13 @@ # Simple pastebin -Very much a work in progress. - -There's a simple server and client. - -The server does require authorization and has some minimal documentation -when you fire it up. - -The client require a configuration file in your home folder and outputs -the URL to the paste if successful. +The bigger brother of `bpaste` In a similar vein it's designed to be +a minimal pastebin program, however this is designed to be more featureful +while still maintaining a simple architecture. ## Ideas for future versions - * Index for all the pastes - * Including tags on each of the pastes * Automatic syntax highlighting * React UI * Swagger Documentation -- embedded into the application as well ( swaggo? ) * Deleting pastes - * Updating pastes - * read templates from disk or embed - * write out sample/default configuration if a new directory is being used - * option to be backed by S3 + * Updating/Editing pastes -- cgit v1.2.3