aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rwxr-xr-xbuild.sh3
-rw-r--r--client/main.go203
-rw-r--r--go.mod13
-rw-r--r--go.sum24
-rw-r--r--main.go773
-rw-r--r--readme.md19
7 files changed, 311 insertions, 726 deletions
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