Implement report webhook support

This commit is contained in:
Lukas Schulte Pelkum 2021-07-21 09:39:16 +02:00
parent 71477f71f6
commit ebc7b20617
No known key found for this signature in database
GPG Key ID: 408DA7CA81DB885C
5 changed files with 125 additions and 0 deletions

View File

@ -90,6 +90,16 @@ Pasty provides an intuitive system to automatically delete pastes after a specif
| `PASTY_AUTODELETE_LIFETIME` | `720h` | `string` | Defines the duration a paste should live until it gets deleted |
| `PASTY_AUTODELETE_TASK_INTERVAL` | `5m` | `string` | Defines the interval in which the AutoDelete task should clean up the database |
## Reports
Pasty aims at being lightweight by default. This is why no fully-featured admin interface with an overview over all pastes and reports is included.
However, pasty does include a way of abstract reports to allow frontends work with this information.
If enabled, pasty makes a standardized request to the configured webhook URL if a paste is reported.
| Environment Variable | Default Value | Type | Description |
|------------------------------|---------------|----------|-----------------------------------------------------------------------------------------------------|
| `PASTY_REPORTS` | `false` | `bool` | Defines whether or not the report system should be enabled |
| `PASTY_REPORT_WEBHOOK` | `<empty>` | `string` | Defines the webhook URL that is called whenever a paste is reported |
| `PASTY_REPORT_WEBHOOK_TOKEN` | `<empty>` | `string` | Defines the token that is sent in the `Authorization` header on every request to the report webhook |
## Storage types
Pasty supports multiple storage types, defined using the `PASTY_STORAGE_TYPE` environment variable (use the value behind the corresponding title in this README).
Every single one of them has its own configuration variables:

View File

@ -20,6 +20,7 @@ type Config struct {
RateLimit string
LengthCap int
AutoDelete *AutoDeleteConfig
Reports *ReportConfig
File *FileConfig
Postgres *PostgresConfig
MongoDB *MongoDBConfig
@ -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
@ -83,6 +91,11 @@ func Load() {
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
View 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
}
// 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
}

View File

@ -7,6 +7,7 @@ import (
"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"
@ -21,6 +22,10 @@ func InitializePastesController(group *router.Group, rateLimiterMiddleware *limi
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
@ -230,3 +235,42 @@ func endpointDeletePaste(ctx *fasthttp.RequestCtx) {
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

@ -75,6 +75,7 @@ func Serve() error {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"modificationTokens": config.Current.ModificationTokens,
"reports": config.Current.Reports,
})
ctx.SetBody(jsonData)
})