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:
committed by
GitHub
parent
4c392b4b52
commit
99504e0bba
@@ -10,20 +10,21 @@ import (
|
||||
|
||||
// Config represents the general application configuration structure
|
||||
type Config struct {
|
||||
WebAddress string
|
||||
StorageType shared.StorageType
|
||||
HastebinSupport bool
|
||||
IDLength int
|
||||
DeletionTokens bool
|
||||
DeletionTokenMaster string
|
||||
DeletionTokenLength int
|
||||
RateLimit string
|
||||
LengthCap int
|
||||
AutoDelete *AutoDeleteConfig
|
||||
File *FileConfig
|
||||
Postgres *PostgresConfig
|
||||
MongoDB *MongoDBConfig
|
||||
S3 *S3Config
|
||||
WebAddress string
|
||||
StorageType shared.StorageType
|
||||
HastebinSupport bool
|
||||
IDLength int
|
||||
ModificationTokens bool
|
||||
ModificationTokenMaster string
|
||||
ModificationTokenLength int
|
||||
RateLimit string
|
||||
LengthCap int
|
||||
AutoDelete *AutoDeleteConfig
|
||||
Reports *ReportConfig
|
||||
File *FileConfig
|
||||
Postgres *PostgresConfig
|
||||
MongoDB *MongoDBConfig
|
||||
S3 *S3Config
|
||||
}
|
||||
|
||||
// AutoDeleteConfig represents the configuration specific for the AutoDelete behaviour
|
||||
@@ -61,6 +62,13 @@ type S3Config struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
// ReportConfig represents the configuration specific for the report system
|
||||
type ReportConfig struct {
|
||||
Reports bool
|
||||
ReportWebhook string
|
||||
ReportWebhookToken string
|
||||
}
|
||||
|
||||
// Current holds the currently loaded config
|
||||
var Current *Config
|
||||
|
||||
@@ -69,20 +77,25 @@ func Load() {
|
||||
env.Load()
|
||||
|
||||
Current = &Config{
|
||||
WebAddress: env.MustString("WEB_ADDRESS", ":8080"),
|
||||
StorageType: shared.StorageType(strings.ToLower(env.MustString("STORAGE_TYPE", "file"))),
|
||||
HastebinSupport: env.MustBool("HASTEBIN_SUPPORT", false),
|
||||
IDLength: env.MustInt("ID_LENGTH", 6),
|
||||
DeletionTokens: env.MustBool("DELETION_TOKENS", true),
|
||||
DeletionTokenMaster: env.MustString("DELETION_TOKEN_MASTER", ""),
|
||||
DeletionTokenLength: env.MustInt("DELETION_TOKEN_LENGTH", 12),
|
||||
RateLimit: env.MustString("RATE_LIMIT", "30-M"),
|
||||
LengthCap: env.MustInt("LENGTH_CAP", 50_000),
|
||||
WebAddress: env.MustString("WEB_ADDRESS", ":8080"),
|
||||
StorageType: shared.StorageType(strings.ToLower(env.MustString("STORAGE_TYPE", "file"))),
|
||||
HastebinSupport: env.MustBool("HASTEBIN_SUPPORT", false),
|
||||
IDLength: env.MustInt("ID_LENGTH", 6),
|
||||
ModificationTokens: env.MustBool("MODIFICATION_TOKENS", env.MustBool("DELETION_TOKENS", true)), // ---
|
||||
ModificationTokenMaster: env.MustString("MODIFICATION_TOKEN_MASTER", env.MustString("DELETION_TOKEN_MASTER", "")), // - We don't want to destroy peoples old configuration
|
||||
ModificationTokenLength: env.MustInt("MODIFICATION_TOKEN_LENGTH", env.MustInt("DELETION_TOKEN_LENGTH", 12)), // ---
|
||||
RateLimit: env.MustString("RATE_LIMIT", "30-M"),
|
||||
LengthCap: env.MustInt("LENGTH_CAP", 50_000),
|
||||
AutoDelete: &AutoDeleteConfig{
|
||||
Enabled: env.MustBool("AUTODELETE", false),
|
||||
Lifetime: env.MustDuration("AUTODELETE_LIFETIME", 720*time.Hour),
|
||||
TaskInterval: env.MustDuration("AUTODELETE_TASK_INTERVAL", 5*time.Minute),
|
||||
},
|
||||
Reports: &ReportConfig{
|
||||
Reports: env.MustBool("REPORTS", false),
|
||||
ReportWebhook: env.MustString("REPORT_WEBHOOK", ""),
|
||||
ReportWebhookToken: env.MustString("REPORT_WEBHOOK_TOKEN", ""),
|
||||
},
|
||||
File: &FileConfig{
|
||||
Path: env.MustString("STORAGE_FILE_PATH", "./data"),
|
||||
},
|
||||
|
||||
57
internal/report/report.go
Normal file
57
internal/report/report.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/lus/pasty/internal/config"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// ReportRequest represents a report request sent to the report webhook
|
||||
type ReportRequest struct {
|
||||
Paste string `json:"paste"`
|
||||
Reason string `json:"reason"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ReportResponse represents a report response received from the report webhook
|
||||
type ReportResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SendReport sends a report request to the report webhook
|
||||
func SendReport(reportRequest *ReportRequest) (*ReportResponse, error) {
|
||||
request := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(request)
|
||||
|
||||
response := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(response)
|
||||
|
||||
request.Header.SetMethod(fasthttp.MethodPost)
|
||||
request.SetRequestURI(config.Current.Reports.ReportWebhook)
|
||||
if config.Current.Reports.ReportWebhookToken != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+config.Current.Reports.ReportWebhookToken)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(reportRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.SetBody(data)
|
||||
|
||||
if err := fasthttp.Do(request, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status := response.StatusCode()
|
||||
if status < 200 || status > 299 {
|
||||
return nil, fmt.Errorf("the report webhook responded with an unexpected error: %d (%s)", status, string(response.Body()))
|
||||
}
|
||||
|
||||
reportResponse := new(ReportResponse)
|
||||
if err := json.Unmarshal(response.Body(), reportResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reportResponse, nil
|
||||
}
|
||||
@@ -1,30 +1,43 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
)
|
||||
|
||||
// Paste represents a saved paste
|
||||
type Paste struct {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
Content string `json:"content" bson:"content"`
|
||||
DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"`
|
||||
Created int64 `json:"created" bson:"created"`
|
||||
AutoDelete bool `json:"autoDelete" bson:"autoDelete"`
|
||||
ID string `json:"id" bson:"_id"`
|
||||
Content string `json:"content" bson:"content"`
|
||||
DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"` // Required for legacy paste storage support
|
||||
ModificationToken string `json:"modificationToken,omitempty" bson:"modificationToken"`
|
||||
Created int64 `json:"created" bson:"created"`
|
||||
AutoDelete bool `json:"autoDelete" bson:"autoDelete"`
|
||||
Metadata map[string]interface{} `json:"metadata" bson:"metadata"`
|
||||
}
|
||||
|
||||
// HashDeletionToken hashes the current deletion token of a paste
|
||||
func (paste *Paste) HashDeletionToken() error {
|
||||
hash, err := argon2id.CreateHash(paste.DeletionToken, argon2id.DefaultParams)
|
||||
// HashModificationToken hashes the current modification token of a paste
|
||||
func (paste *Paste) HashModificationToken() error {
|
||||
hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paste.DeletionToken = hash
|
||||
paste.ModificationToken = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckDeletionToken checks whether or not the given deletion token is correct
|
||||
func (paste *Paste) CheckDeletionToken(deletionToken string) bool {
|
||||
match, err := argon2id.ComparePasswordAndHash(deletionToken, paste.DeletionToken)
|
||||
// CheckModificationToken checks whether or not the given modification token is correct
|
||||
func (paste *Paste) CheckModificationToken(modificationToken string) bool {
|
||||
// The modification token may be stored in the deletion token field in old pastes
|
||||
usedToken := paste.ModificationToken
|
||||
if usedToken == "" {
|
||||
usedToken = paste.DeletionToken
|
||||
if usedToken != "" {
|
||||
log.Println("WARNING: You seem to have pastes with the old 'deletionToken' field stored in your storage driver. Though this does not cause any issues right now, it may in the future. Consider some kind of migration.")
|
||||
}
|
||||
}
|
||||
|
||||
match, err := argon2id.ComparePasswordAndHash(modificationToken, usedToken)
|
||||
return err == nil && match
|
||||
}
|
||||
|
||||
@@ -117,8 +117,9 @@ func (driver *MongoDBDriver) Save(paste *shared.Paste) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Insert the paste object
|
||||
_, err := collection.InsertOne(ctx, paste)
|
||||
// Upsert the paste object
|
||||
filter := bson.M{"_id": paste.ID}
|
||||
_, err := collection.UpdateOne(ctx, filter, paste, options.Update().SetUpsert(true))
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
begin;
|
||||
|
||||
alter table if exists "pastes" rename column "modificationToken" to "deletionToken";
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,5 @@
|
||||
begin;
|
||||
|
||||
alter table if exists "pastes" rename column "deletionToken" to "modificationToken";
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,5 @@
|
||||
begin;
|
||||
|
||||
alter table if exists "pastes" drop column "metadata";
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,5 @@
|
||||
begin;
|
||||
|
||||
alter table if exists "pastes" add column "metadata" jsonb not null;
|
||||
|
||||
commit;
|
||||
@@ -82,7 +82,7 @@ func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
|
||||
row := driver.pool.QueryRow(context.Background(), query, id)
|
||||
|
||||
paste := new(shared.Paste)
|
||||
if err := row.Scan(&paste.ID, &paste.Content, &paste.DeletionToken, &paste.Created, &paste.AutoDelete); err != nil {
|
||||
if err := row.Scan(&paste.ID, &paste.Content, &paste.ModificationToken, &paste.Created, &paste.AutoDelete, &paste.Metadata); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -93,9 +93,18 @@ func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
|
||||
|
||||
// Save saves a paste
|
||||
func (driver *PostgresDriver) Save(paste *shared.Paste) error {
|
||||
query := "INSERT INTO pastes VALUES ($1, $2, $3, $4, $5)"
|
||||
query := `
|
||||
INSERT INTO pastes (id, content, modificationToken, created, autoDelete)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET content = excluded.token,
|
||||
modificationToken = excluded.modificationToken,
|
||||
created = excluded.created,
|
||||
autoDelete = excluded.autoDelete,
|
||||
metadata = excluded.metadata
|
||||
`
|
||||
|
||||
_, err := driver.pool.Exec(context.Background(), query, paste.ID, paste.Content, paste.DeletionToken, paste.Created, paste.AutoDelete)
|
||||
_, err := driver.pool.Exec(context.Background(), query, paste.ID, paste.Content, paste.ModificationToken, paste.Created, paste.AutoDelete, paste.Metadata)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
26
internal/web/controllers/v1/paste_adapter.go
Normal file
26
internal/web/controllers/v1/paste_adapter.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
276
internal/web/controllers/v2/pastes.go
Normal file
276
internal/web/controllers/v2/pastes.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user