diff --git a/cmd/pasty/main.go b/cmd/pasty/main.go index e763e29..4e8eff9 100644 --- a/cmd/pasty/main.go +++ b/cmd/pasty/main.go @@ -45,7 +45,7 @@ func main() { var driver storage.Driver switch strings.TrimSpace(strings.ToLower(cfg.StorageDriver)) { case "postgres": - driver = new(postgres.Driver) + driver = postgres.New(cfg.Postgres.DSN) break default: log.Fatal().Str("driver_name", cfg.StorageDriver).Msg("An invalid storage driver name was given.") @@ -54,7 +54,7 @@ func main() { // Initialize the configured storage driver log.Info().Str("driver_name", cfg.StorageDriver).Msg("Initializing the storage driver...") - if err := driver.Initialize(context.Background(), cfg); err != nil { + if err := driver.Initialize(context.Background()); err != nil { log.Fatal().Err(err).Str("driver_name", cfg.StorageDriver).Msg("The storage driver could not be initialized.") return } diff --git a/go.mod b/go.mod index 34ce6e6..6f24254 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/lus/pasty go 1.20 require ( + github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 + github.com/go-chi/chi/v5 v5.0.8 github.com/golang-migrate/migrate/v4 v4.16.1 github.com/jackc/pgx/v5 v5.3.1 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index 98db06e..c589896 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 h1:qZaEtLxnqY5mJ0fVKbk31NVhlgi0yrKm51Pq/I5wcz4= +github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736/go.mod h1:mTeFRcTdnpzOlRjMoFYC/80HwVUreupyAiqPkCZQOXc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -8,6 +10,8 @@ github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m3 github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang-migrate/migrate/v4 v4.16.1 h1:O+0C55RbMN66pWm5MjO6mw0px6usGpY0+bkSGW9zCo0= @@ -54,23 +58,57 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/pastes/id_generation.go b/internal/pastes/id_generation.go new file mode 100644 index 0000000..6046e8b --- /dev/null +++ b/internal/pastes/id_generation.go @@ -0,0 +1,19 @@ +package pastes + +import ( + "context" + "github.com/lus/pasty/internal/randx" +) + +func GenerateID(ctx context.Context, repo Repository, charset string, length int) (string, error) { + for { + id := randx.String(charset, length) + existing, err := repo.FindByID(ctx, id) + if err != nil { + return "", err + } + if existing == nil { + return id, nil + } + } +} diff --git a/internal/pastes/paste.go b/internal/pastes/paste.go index 59d0a7c..5371a36 100644 --- a/internal/pastes/paste.go +++ b/internal/pastes/paste.go @@ -1,9 +1,31 @@ package pastes +import "github.com/alexedwards/argon2id" + type Paste struct { - ID string `json:"id"` - Content string `json:"content"` - ModificationToken string `json:"modificationToken,omitempty"` - Created int64 `json:"created"` - Metadata map[string]interface{} `json:"metadata"` + ID string `json:"id"` + Content string `json:"content"` + ModificationToken string `json:"modificationToken,omitempty"` + Created int64 `json:"created"` + Metadata map[string]any `json:"metadata"` +} + +func (paste *Paste) HashModificationToken() error { + if paste.ModificationToken == "" { + return nil + } + hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams) + if err != nil { + return err + } + paste.ModificationToken = hash + return nil +} + +func (paste *Paste) CheckModificationToken(modificationToken string) bool { + if paste.ModificationToken == "" { + return false + } + match, err := argon2id.ComparePasswordAndHash(modificationToken, paste.ModificationToken) + return err == nil && match } diff --git a/internal/slices/slice_utils.go b/internal/slices/slice_utils.go new file mode 100644 index 0000000..f162737 --- /dev/null +++ b/internal/slices/slice_utils.go @@ -0,0 +1,10 @@ +package slices + +func Contains[T comparable](src []T, val T) bool { + for _, elem := range src { + if elem == val { + return true + } + } + return false +} diff --git a/internal/storage/driver.go b/internal/storage/driver.go index 3b4042e..1283905 100644 --- a/internal/storage/driver.go +++ b/internal/storage/driver.go @@ -2,12 +2,11 @@ package storage import ( "context" - "github.com/lus/pasty/internal/config" "github.com/lus/pasty/internal/pastes" ) type Driver interface { - Initialize(ctx context.Context, cfg *config.Config) error + Initialize(ctx context.Context) error Close() error Pastes() pastes.Repository } diff --git a/internal/storage/postgres/driver.go b/internal/storage/postgres/driver.go index 39f649f..e401044 100644 --- a/internal/storage/postgres/driver.go +++ b/internal/storage/postgres/driver.go @@ -8,7 +8,6 @@ import ( _ "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jackc/pgx/v5/pgxpool" - "github.com/lus/pasty/internal/config" "github.com/lus/pasty/internal/pastes" "github.com/lus/pasty/internal/storage" "github.com/rs/zerolog/log" @@ -18,14 +17,21 @@ import ( var migrations embed.FS type Driver struct { + dsn string connPool *pgxpool.Pool pastes *pasteRepository } var _ storage.Driver = (*Driver)(nil) -func (driver *Driver) Initialize(ctx context.Context, cfg *config.Config) error { - pool, err := pgxpool.New(ctx, cfg.Postgres.DSN) +func New(dsn string) *Driver { + return &Driver{ + dsn: dsn, + } +} + +func (driver *Driver) Initialize(ctx context.Context) error { + pool, err := pgxpool.New(ctx, driver.dsn) if err != nil { return err } @@ -36,7 +42,7 @@ func (driver *Driver) Initialize(ctx context.Context, cfg *config.Config) error pool.Close() return err } - migrator, err := migrate.NewWithSourceInstance("iofs", source, cfg.Postgres.DSN) + migrator, err := migrate.NewWithSourceInstance("iofs", source, driver.dsn) if err != nil { pool.Close() return err diff --git a/internal/web/response_writer.go b/internal/web/response_writer.go new file mode 100644 index 0000000..9f3a947 --- /dev/null +++ b/internal/web/response_writer.go @@ -0,0 +1,33 @@ +package web + +import ( + "encoding/json" + "net/http" +) + +func writeErr(writer http.ResponseWriter, err error) { + writeString(writer, http.StatusInternalServerError, err.Error()) +} + +func writeString(writer http.ResponseWriter, status int, value string) { + writer.WriteHeader(status) + writer.Write([]byte(value)) +} + +func writeJSON(writer http.ResponseWriter, status int, value any) error { + jsonData, err := json.Marshal(value) + if err != nil { + return err + } + + writer.WriteHeader(status) + writer.Write(jsonData) + + return nil +} + +func writeJSONOrErr(writer http.ResponseWriter, status int, value any) { + if err := writeJSON(writer, status, value); err != nil { + writeErr(writer, err) + } +} diff --git a/internal/web/server.go b/internal/web/server.go new file mode 100644 index 0000000..9eb484b --- /dev/null +++ b/internal/web/server.go @@ -0,0 +1,49 @@ +package web + +import ( + "github.com/go-chi/chi/v5" + "github.com/lus/pasty/internal/storage" + "net/http" +) + +type Server struct { + // The address the web server should listen to. + Address string + + // The storage driver to use. + Storage storage.Driver + + // Whether the Hastebin support should be enabled. + // If this is set to 'false', the Hastebin specific endpoints will not be registered. + HastebinSupport bool + + // The length of newly generated paste IDs. + PasteIDLength int + // The charset to use when generating new paste IDs. + PasteIDCharset string + + // The maximum length of newly generated pastes. + PasteLengthCap int + + // Whether modification tokens are enabled. + ModificationTokensEnabled bool + // The length of newly generated modification tokens. + ModificationTokenLength int + // The charset to use when generating new modification tokens. + ModificationTokenCharset string + + // The administration tokens. + AdminTokens []string +} + +func (server *Server) Start() error { + router := chi.NewRouter() + + // Register the paste API endpoints + router.With(server.v2MiddlewareInjectPaste).Get("/api/v2/pastes/{paste_id}", server.v2EndpointGetPaste) + router.Post("/api/v2/pastes", server.v2EndpointCreatePaste) + router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Patch("/api/v2/pastes/{paste_id}", server.v2EndpointModifyPaste) + router.Delete("/api/v2/pastes/{paste_id}", server.v2EndpointDeletePaste) + + return http.ListenAndServe(server.Address, router) +} diff --git a/internal/web/v2_end_create_paste.go b/internal/web/v2_end_create_paste.go new file mode 100644 index 0000000..4ac0cb4 --- /dev/null +++ b/internal/web/v2_end_create_paste.go @@ -0,0 +1,70 @@ +package web + +import ( + "encoding/json" + "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/randx" + "io" + "net/http" + "time" +) + +type v2EndpointCreatePastePayload struct { + Content string `json:"content"` + Metadata map[string]any `json:"metadata"` +} + +func (server *Server) v2EndpointCreatePaste(writer http.ResponseWriter, request *http.Request) { + // Read, parse and validate the request payload + body, err := io.ReadAll(request.Body) + if err != nil { + writeErr(writer, err) + return + } + payload := new(v2EndpointCreatePastePayload) + if err := json.Unmarshal(body, payload); err != nil { + writeErr(writer, err) + return + } + if payload.Content == "" { + writeString(writer, http.StatusBadRequest, "missing paste content") + return + } + if server.PasteLengthCap > 0 && len(payload.Content) > server.PasteLengthCap { + writeString(writer, http.StatusBadRequest, "too large paste content") + return + } + + id, err := pastes.GenerateID(request.Context(), server.Storage.Pastes(), server.PasteIDCharset, server.PasteIDLength) + if err != nil { + writeErr(writer, err) + return + } + + paste := &pastes.Paste{ + ID: id, + Content: payload.Content, + Created: time.Now().Unix(), + Metadata: payload.Metadata, + } + + modificationToken := "" + if server.ModificationTokensEnabled { + modificationToken = randx.String(server.ModificationTokenCharset, server.ModificationTokenLength) + paste.ModificationToken = modificationToken + + if err := paste.HashModificationToken(); err != nil { + writeErr(writer, err) + return + } + } + + if err := server.Storage.Pastes().Upsert(request.Context(), paste); err != nil { + writeErr(writer, err) + return + } + + cpy := *paste + cpy.ModificationToken = modificationToken + writeJSONOrErr(writer, http.StatusCreated, cpy) +} diff --git a/internal/web/v2_end_delete_paste.go b/internal/web/v2_end_delete_paste.go new file mode 100644 index 0000000..f5e6c1b --- /dev/null +++ b/internal/web/v2_end_delete_paste.go @@ -0,0 +1,18 @@ +package web + +import ( + "github.com/lus/pasty/internal/pastes" + "net/http" +) + +func (server *Server) v2EndpointDeletePaste(writer http.ResponseWriter, request *http.Request) { + paste, ok := request.Context().Value("paste").(*pastes.Paste) + if !ok { + writeString(writer, http.StatusInternalServerError, "missing paste object") + return + } + + if err := server.Storage.Pastes().DeleteByID(request.Context(), paste.ID); err != nil { + writeErr(writer, err) + } +} diff --git a/internal/web/v2_end_get_paste.go b/internal/web/v2_end_get_paste.go new file mode 100644 index 0000000..5be338e --- /dev/null +++ b/internal/web/v2_end_get_paste.go @@ -0,0 +1,18 @@ +package web + +import ( + "github.com/lus/pasty/internal/pastes" + "net/http" +) + +func (server *Server) v2EndpointGetPaste(writer http.ResponseWriter, request *http.Request) { + paste, ok := request.Context().Value("paste").(*pastes.Paste) + if !ok { + writeString(writer, http.StatusInternalServerError, "missing paste object") + return + } + + cpy := *paste + cpy.ModificationToken = "" + writeJSONOrErr(writer, http.StatusOK, cpy) +} diff --git a/internal/web/v2_end_modify_paste.go b/internal/web/v2_end_modify_paste.go new file mode 100644 index 0000000..ad648ea --- /dev/null +++ b/internal/web/v2_end_modify_paste.go @@ -0,0 +1,60 @@ +package web + +import ( + "encoding/json" + "github.com/lus/pasty/internal/pastes" + "io" + "net/http" +) + +type v2EndpointModifyPastePayload struct { + Content *string `json:"content"` + Metadata map[string]any `json:"metadata"` +} + +func (server *Server) v2EndpointModifyPaste(writer http.ResponseWriter, request *http.Request) { + paste, ok := request.Context().Value("paste").(*pastes.Paste) + if !ok { + writeString(writer, http.StatusInternalServerError, "missing paste object") + return + } + + // Read, parse and validate the request payload + body, err := io.ReadAll(request.Body) + if err != nil { + writeErr(writer, err) + return + } + payload := new(v2EndpointModifyPastePayload) + if err := json.Unmarshal(body, payload); err != nil { + writeErr(writer, err) + return + } + if payload.Content != nil && *payload.Content == "" { + writeString(writer, http.StatusBadRequest, "missing paste content") + return + } + if payload.Content != nil && server.PasteLengthCap > 0 && len(*payload.Content) > server.PasteLengthCap { + writeString(writer, http.StatusBadRequest, "too large paste content") + return + } + + // Modify the paste itself + if payload.Content != nil { + paste.Content = *payload.Content + } + if payload.Metadata != nil { + for key, value := range payload.Metadata { + if value == nil { + delete(paste.Metadata, key) + continue + } + paste.Metadata[key] = value + } + } + + // Save the modified paste + if err := server.Storage.Pastes().Upsert(request.Context(), paste); err != nil { + writeErr(writer, err) + } +} diff --git a/internal/web/v2_mid_authorize.go b/internal/web/v2_mid_authorize.go new file mode 100644 index 0000000..5bd66c1 --- /dev/null +++ b/internal/web/v2_mid_authorize.go @@ -0,0 +1,37 @@ +package web + +import ( + "github.com/lus/pasty/internal/pastes" + "github.com/lus/pasty/internal/slices" + "net/http" + "strings" +) + +func (server *Server) v2MiddlewareAuthorize(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + paste, ok := request.Context().Value("paste").(*pastes.Paste) + if !ok { + writeString(writer, http.StatusInternalServerError, "missing paste object") + return + } + + authHeader := strings.SplitN(request.Header.Get("Authorization"), " ", 2) + if len(authHeader) != 2 || authHeader[0] != "Bearer" { + writeString(writer, http.StatusUnauthorized, "unauthorized") + return + } + + isAdmin := slices.Contains(server.AdminTokens, authHeader[1]) + if isAdmin { + next.ServeHTTP(writer, request) + return + } + + if !server.ModificationTokensEnabled || !paste.CheckModificationToken(authHeader[1]) { + writeString(writer, http.StatusUnauthorized, "unauthorized") + return + } + + next.ServeHTTP(writer, request) + }) +} diff --git a/internal/web/v2_mid_inject_paste.go b/internal/web/v2_mid_inject_paste.go new file mode 100644 index 0000000..399a1f2 --- /dev/null +++ b/internal/web/v2_mid_inject_paste.go @@ -0,0 +1,38 @@ +package web + +import ( + "context" + "github.com/go-chi/chi/v5" + "net/http" + "strings" +) + +func (server *Server) v2MiddlewareInjectPaste(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + pasteID := strings.TrimSpace(chi.URLParam(request, "paste_id")) + if pasteID == "" { + writeString(writer, http.StatusNotFound, "paste not found") + return + } + + paste, err := server.Storage.Pastes().FindByID(request.Context(), pasteID) + if err != nil { + if pasteID == "" { + writeErr(writer, err) + return + } + } + if paste == nil { + writeString(writer, http.StatusNotFound, "paste not found") + return + } + + if paste.Metadata == nil { + paste.Metadata = make(map[string]any) + } + + request = request.WithContext(context.WithValue(request.Context(), "paste", paste)) + + next.ServeHTTP(writer, request) + }) +} diff --git a/web/assets/js/modules/api.js b/web/assets/js/modules/api.js index 6140672..d4c84aa 100644 --- a/web/assets/js/modules/api.js +++ b/web/assets/js/modules/api.js @@ -1,4 +1,4 @@ -const API_BASE_URL = location.protocol + "//" + location.host + "/api/v2"; +const API_BASE_URL = location.protocol + "//" + location.host + "/web/v2"; export async function getAPIInformation() { return fetch(API_BASE_URL + "/info");