1
0
mirror of https://github.com/lus/pasty.git synced 2023-08-10 21:13:09 +03:00

Implement API v2 (#13)

* Add documentation notice

* Rename the deletion token to modification token

* Implement API basics

* Implement paste modification endpoint (#10)

* Implement paste metadata

* Implement report webhook support

* Document API

* Document paste entity

* Update syntax highlighting types (js -> jsonc)

* Update migrator
This commit is contained in:
Lukas Schulte Pelkum
2021-07-22 22:26:21 +02:00
committed by GitHub
parent 4c392b4b52
commit 99504e0bba
18 changed files with 723 additions and 71 deletions

View File

@@ -50,11 +50,11 @@ func HastebinSupportHandler(ctx *fasthttp.RequestCtx) {
AutoDelete: config.Current.AutoDelete.Enabled,
}
// Set a deletion token
if config.Current.DeletionTokens {
paste.DeletionToken = utils.RandomString(config.Current.DeletionTokenLength)
// Set a modification token
if config.Current.ModificationTokens {
paste.ModificationToken = utils.RandomString(config.Current.ModificationTokenLength)
err = paste.HashDeletionToken()
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())

View File

@@ -0,0 +1,26 @@
package v1
import "github.com/lus/pasty/internal/shared"
type legacyPaste struct {
ID string `json:"id"`
Content string `json:"content"`
DeletionToken string `json:"deletionToken,omitempty"`
Created int64 `json:"created"`
AutoDelete bool `json:"autoDelete"`
}
func legacyFromModern(paste *shared.Paste) *legacyPaste {
deletionToken := paste.ModificationToken
if deletionToken == "" {
deletionToken = paste.DeletionToken
}
return &legacyPaste{
ID: paste.ID,
Content: paste.Content,
DeletionToken: deletionToken,
Created: paste.Created,
AutoDelete: paste.AutoDelete,
}
}

View File

@@ -37,10 +37,11 @@ func v1GetPaste(ctx *fasthttp.RequestCtx) {
ctx.SetBodyString("paste not found")
return
}
paste.DeletionToken = ""
legacyPaste := legacyFromModern(paste)
legacyPaste.DeletionToken = ""
// Respond with the paste
jsonData, err := json.Marshal(paste)
jsonData, err := json.Marshal(legacyPaste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
@@ -91,13 +92,13 @@ func v1PostPaste(ctx *fasthttp.RequestCtx) {
AutoDelete: config.Current.AutoDelete.Enabled,
}
// Set a deletion token
deletionToken := ""
if config.Current.DeletionTokens {
deletionToken = utils.RandomString(config.Current.DeletionTokenLength)
paste.DeletionToken = deletionToken
// Set a modification token
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenLength)
paste.ModificationToken = modificationToken
err = paste.HashDeletionToken()
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
@@ -114,8 +115,8 @@ func v1PostPaste(ctx *fasthttp.RequestCtx) {
}
// Respond with the paste
pasteCopy := *paste
pasteCopy.DeletionToken = deletionToken
pasteCopy := legacyFromModern(paste)
pasteCopy.DeletionToken = modificationToken
jsonData, err := json.Marshal(pasteCopy)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
@@ -139,9 +140,9 @@ func v1DeletePaste(ctx *fasthttp.RequestCtx) {
return
}
// Validate the deletion token of the paste
deletionToken := values["deletionToken"]
if deletionToken == "" {
// Validate the modification token of the paste
modificationToken := values["deletionToken"]
if modificationToken == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing 'deletionToken' field")
return
@@ -160,8 +161,8 @@ func v1DeletePaste(ctx *fasthttp.RequestCtx) {
return
}
// Check if the deletion token is correct
if (config.Current.DeletionTokenMaster == "" || deletionToken != config.Current.DeletionTokenMaster) && !paste.CheckDeletionToken(deletionToken) {
// Check if the modification token is correct
if (config.Current.ModificationTokenMaster == "" || modificationToken != config.Current.ModificationTokenMaster) && !paste.CheckModificationToken(modificationToken) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetBodyString("invalid deletion token")
return

View File

@@ -0,0 +1,276 @@
package v2
import (
"encoding/json"
"strings"
"time"
"github.com/fasthttp/router"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/report"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/utils"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/valyala/fasthttp"
)
// InitializePastesController initializes the '/v2/pastes/*' controller
func InitializePastesController(group *router.Group, rateLimiterMiddleware *limitFasthttp.Middleware) {
// moms spaghetti
group.GET("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(endpointGetPaste)))
group.POST("/", rateLimiterMiddleware.Handle(endpointCreatePaste))
group.PATCH("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(middlewareValidateModificationToken(endpointModifyPaste))))
group.DELETE("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(middlewareValidateModificationToken(endpointDeletePaste))))
if config.Current.Reports.Reports {
group.POST("/{id}/report", rateLimiterMiddleware.Handle(middlewareInjectPaste(endpointReportPaste)))
}
}
// middlewareInjectPaste retrieves and injects the paste with the specified ID
func middlewareInjectPaste(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
pasteID := ctx.UserValue("id").(string)
paste, err := storage.Current.Get(pasteID)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if paste == nil {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("paste not found")
return
}
if paste.Metadata == nil {
paste.Metadata = map[string]interface{}{}
}
ctx.SetUserValue("_paste", paste)
next(ctx)
}
}
// middlewareValidateModificationToken extracts and validates a given modification token for an injected paste
func middlewareValidateModificationToken(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
authHeaderSplit := strings.SplitN(string(ctx.Request.Header.Peek("Authorization")), " ", 2)
if len(authHeaderSplit) < 2 || authHeaderSplit[0] != "Bearer" {
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
ctx.SetBodyString("unauthorized")
return
}
modificationToken := authHeaderSplit[1]
if config.Current.ModificationTokenMaster != "" && modificationToken == config.Current.ModificationTokenMaster {
next(ctx)
return
}
valid := paste.CheckModificationToken(modificationToken)
if !valid {
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
ctx.SetBodyString("unauthorized")
return
}
next(ctx)
}
}
// endpointGetPaste handles the 'GET /v2/pastes/{id}' endpoint
func endpointGetPaste(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
paste.DeletionToken = ""
paste.ModificationToken = ""
jsonData, err := json.Marshal(paste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}
type endpointCreatePastePayload struct {
Content string `json:"content"`
Metadata map[string]interface{} `json:"metadata"`
}
// endpointCreatePaste handles the 'POST /v2/pastes' endpoint
func endpointCreatePaste(ctx *fasthttp.RequestCtx) {
// Read, parse and validate the request payload
payload := new(endpointCreatePastePayload)
if err := json.Unmarshal(ctx.PostBody(), payload); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if payload.Content == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing paste content")
return
}
if config.Current.LengthCap > 0 && len(payload.Content) > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("too large paste content")
return
}
// Acquire a new paste ID
id, err := storage.AcquireID()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Prepare the paste object
if payload.Metadata == nil {
payload.Metadata = map[string]interface{}{}
}
paste := &shared.Paste{
ID: id,
Content: payload.Content,
Created: time.Now().Unix(),
AutoDelete: config.Current.AutoDelete.Enabled,
Metadata: payload.Metadata,
}
// Create a new modification token if enabled
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenLength)
paste.ModificationToken = modificationToken
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
// Save the paste
err = storage.Current.Save(paste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Respond with the paste
pasteCopy := *paste
pasteCopy.ModificationToken = modificationToken
jsonData, err := json.Marshal(pasteCopy)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetStatusCode(fasthttp.StatusCreated)
ctx.SetBody(jsonData)
}
type endpointModifyPastePayload struct {
Content *string `json:"content"`
Metadata map[string]interface{} `json:"metadata"`
}
// endpointModifyPaste handles the 'PATCH /v2/pastes/{id}' endpoint
func endpointModifyPaste(ctx *fasthttp.RequestCtx) {
// Read, parse and validate the request payload
payload := new(endpointModifyPastePayload)
if err := json.Unmarshal(ctx.PostBody(), payload); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if payload.Content != nil && *payload.Content == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing paste content")
return
}
if payload.Content != nil && config.Current.LengthCap > 0 && len(*payload.Content) > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("too large paste content")
return
}
// Modify the paste itself
paste := ctx.UserValue("_paste").(*shared.Paste)
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 := storage.Current.Save(paste); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
// endpointDeletePaste handles the 'DELETE /v2/pastes/{id}' endpoint
func endpointDeletePaste(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
if err := storage.Current.Delete(paste.ID); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
type endpointReportPastePayload struct {
Reason string `json:"reason"`
}
func endpointReportPaste(ctx *fasthttp.RequestCtx) {
// Read, parse and validate the request payload
payload := new(endpointReportPastePayload)
if err := json.Unmarshal(ctx.PostBody(), payload); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if payload.Reason == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing report reason")
return
}
request := &report.ReportRequest{
Paste: ctx.UserValue("_paste").(*shared.Paste).ID,
Reason: payload.Reason,
Timestamp: time.Now().Unix(),
}
response, err := report.SendReport(request)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
jsonData, err := json.Marshal(response)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/lus/pasty/internal/static"
"github.com/lus/pasty/internal/storage"
v1 "github.com/lus/pasty/internal/web/controllers/v1"
v2 "github.com/lus/pasty/internal/web/controllers/v2"
"github.com/ulule/limiter/v3"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/ulule/limiter/v3/drivers/store/memory"
@@ -61,12 +62,25 @@ func Serve() error {
v1Route.GET("/info", func(ctx *fasthttp.RequestCtx) {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"deletionTokens": config.Current.DeletionTokens,
"deletionTokens": config.Current.ModificationTokens,
})
ctx.SetBody(jsonData)
})
v1.InitializePastesController(v1Route.Group("/pastes"), rateLimiterMiddleware)
}
v2Route := apiRoute.Group("/v2")
{
v2Route.GET("/info", func(ctx *fasthttp.RequestCtx) {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"modificationTokens": config.Current.ModificationTokens,
"reports": config.Current.Reports.Reports,
})
ctx.SetBody(jsonData)
})
v2.InitializePastesController(v2Route.Group("/pastes"), rateLimiterMiddleware)
}
}
// Route the hastebin documents route if hastebin support is enabled