This commit is contained in:
Lukas Schulte Pelkum 2023-06-07 18:24:17 +00:00 committed by GitHub
commit fcb3384143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 785 additions and 2769 deletions

View File

@ -1,22 +1,6 @@
# pasty
Pasty is a fast and lightweight code pasting server
## !!! Important deprecation notices !!!
> This version of pasty uses a new field name for the so far called `deletionToken`: `modificationToken`.
> Instances using **PostgreSQL** are **not affected** as a corresponding SQL migration will run before the first startup.
> If you however use **another storage driver** you may have to **update the entries** by hand or using a simple query, depending on your driver as I don't plan to ship migrations for every single storage driver.
> It may be important to know that the **data migrator has been upgraded** too. This may serve as a **convenient workaround** (export data (field will be renamed) and import data with changed field names again).
>
> The old `deletionToken` field will be processed corresponding to these changes but I strongly recommend updating old pastes if possible.
> Additionally, I changed the three `DELETION_TOKEN*`environment variables to their corresponding `MODIFICATION_TOKEN*` ones:
> - `DELETION_TOKENS` -> `MODIFICATION_TOKENS`
> - `DELETION_TOKEN_MASTER` -> `MODIFICATION_TOKEN_MASTER`
> - `DELETION_TOKEN_LENGTH` -> `MODIFICATION_TOKEN_LENGTH`
>
> Again, **the old ones will still work** because I do not want to jumble your configurations. However, **please consider updating** them to stay future-proof ^^.
pasty is a fast and lightweight code pasting server.
## Support

View File

@ -1,52 +1,73 @@
package main
import (
"log"
"time"
"context"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/meta"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/web"
"github.com/lus/pasty/internal/storage/postgres"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"os"
"os/signal"
"strings"
)
func main() {
// Load the configuration
log.Println("Loading the application configuration...")
config.Load()
// Set up the logger
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
if !meta.IsProdEnvironment() {
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
})
log.Warn().Msg("This distribution was compiled for development mode and is thus not meant to be run in production!")
}
// Load the configured storage driver
log.Println("Loading the configured storage driver...")
err := storage.Load()
// Load the configuration
cfg, err := config.Load()
if err != nil {
panic(err)
log.Fatal().Err(err).Msg("Could not load the configuration.")
}
// Adjust the log level
if !meta.IsProdEnvironment() {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else {
level, err := zerolog.ParseLevel(cfg.LogLevel)
if err != nil {
log.Warn().Msg("An invalid log level was configured. Falling back to 'info'.")
level = zerolog.InfoLevel
}
zerolog.SetGlobalLevel(level)
}
// Determine the correct storage driver to use
var driver storage.Driver
switch strings.TrimSpace(strings.ToLower(cfg.StorageDriver)) {
case "postgres":
driver = postgres.New(cfg.Postgres.DSN)
break
default:
log.Fatal().Str("driver_name", cfg.StorageDriver).Msg("An invalid storage driver name was given.")
return
}
// Initialize the configured storage driver
log.Info().Str("driver_name", cfg.StorageDriver).Msg("Initializing the storage driver...")
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
}
defer func() {
log.Println("Terminating the storage driver...")
err := storage.Current.Terminate()
if err != nil {
log.Fatalln(err)
log.Info().Msg("Shutting down the storage driver...")
if err := driver.Close(); err != nil {
log.Err(err).Str("driver_name", cfg.StorageDriver).Msg("Could not shut down the storage driver.")
}
}()
// Schedule the AutoDelete task
if config.Current.AutoDelete.Enabled {
log.Println("Scheduling the AutoDelete task...")
go func() {
for {
// Run the cleanup sequence
deleted, err := storage.Current.Cleanup()
if err != nil {
log.Fatalln(err)
}
log.Printf("AutoDelete: Deleted %d expired pastes", deleted)
// Wait until the process should repeat
time.Sleep(config.Current.AutoDelete.TaskInterval)
}
}()
}
// Serve the web resources
log.Println("Serving the web resources...")
panic(web.Serve())
// Wait for an interrupt signal
log.Info().Msg("The application has been started. Use Ctrl+C to shut it down.")
shutdownChan := make(chan os.Signal, 1)
signal.Notify(shutdownChan, os.Interrupt)
<-shutdownChan
}

View File

@ -1,82 +0,0 @@
package main
import (
"log"
"os"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
)
func main() {
// Validate the command line arguments
if len(os.Args) != 3 {
panic("Invalid command line arguments")
}
// Load the configuration
log.Println("Loading the application configuration...")
config.Load()
// Create and initialize the first (from) driver
from, err := storage.GetDriver(shared.StorageType(os.Args[1]))
if err != nil {
panic(err)
}
err = from.Initialize()
if err != nil {
panic(err)
}
// Create and initialize the second (to) driver
to, err := storage.GetDriver(shared.StorageType(os.Args[2]))
if err != nil {
panic(err)
}
err = to.Initialize()
if err != nil {
panic(err)
}
// Retrieve a list of IDs from the first (from) driver
ids, err := from.ListIDs()
if err != nil {
panic(err)
}
// Transfer every paste to the second (to) driver
for _, id := range ids {
log.Println("Transferring ID " + id + "...")
// Retrieve the paste
paste, err := from.Get(id)
if err != nil {
log.Println("[ERR]", err.Error())
continue
}
// Move the content of the deletion token field to the modification field
if paste.DeletionToken != "" {
if paste.ModificationToken == "" {
paste.ModificationToken = paste.DeletionToken
}
paste.DeletionToken = ""
log.Println("[INFO] Paste " + id + " was a legacy one.")
}
// Initialize a new metadata map if the old one is null
if paste.Metadata == nil {
paste.Metadata = make(map[string]interface{})
}
// Save the paste
err = to.Save(paste)
if err != nil {
log.Println("[ERR]", err.Error())
continue
}
log.Println("Transferred ID " + id + ".")
}
}

42
go.mod
View File

@ -1,23 +1,29 @@
module github.com/lus/pasty
go 1.16
go 1.20
require (
github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b
github.com/fasthttp/router v1.2.4
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang-migrate/migrate/v4 v4.14.2-0.20201125065321-a53e6fc42574
github.com/golang/snappy v0.0.4 // indirect
github.com/jackc/pgx/v4 v4.11.0
github.com/johejo/golang-migrate-extra v0.0.0-20210217013041-51a992e50d16
github.com/joho/godotenv v1.3.0
github.com/klauspost/compress v1.15.1 // indirect
github.com/minio/minio-go/v7 v7.0.5
github.com/ulule/limiter/v3 v3.5.0
github.com/valyala/fasthttp v1.16.0
github.com/xdg-go/scram v1.1.1 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.mongodb.org/mongo-driver v1.8.4
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
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
github.com/kelseyhightower/envconfig v1.4.0
github.com/rs/zerolog v1.29.1
)
require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
)

1034
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,124 +1,50 @@
package config
import (
"strings"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
"time"
"github.com/lus/pasty/internal/env"
"github.com/lus/pasty/internal/shared"
)
// Config represents the general application configuration structure
type Config struct {
WebAddress string
StorageType shared.StorageType
HastebinSupport bool
IDLength int
IDCharacters string
ModificationTokens bool
ModificationTokenMaster string
ModificationTokenLength int
ModificationTokenCharacters string
RateLimit string
LengthCap int
AutoDelete *AutoDeleteConfig
LogLevel string `default:"info" split_words:"true"`
WebAddress string `default:":8080" split_words:"true"`
StorageDriver string `default:"sqlite" split_words:"true"`
HastebinSupport bool `default:"false" split_words:"true"`
IDLength int `default:"6" split_words:"true"`
IDCharacters string `default:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" split_words:"true"`
ModificationTokens bool `default:"true" split_words:"true"`
ModificationTokenMaster string `split_words:"true"`
ModificationTokenLength int `default:"12" split_words:"true"`
ModificationTokenCharacters string `default:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" split_words:"true"`
RateLimit string `default:"30-M" split_words:"true"`
LengthCap int `default:"50000" split_words:"true"`
AutoDelete *AutoDeleteConfig `split_words:"true"`
Reports *ReportConfig
File *FileConfig
Postgres *PostgresConfig
MongoDB *MongoDBConfig
S3 *S3Config
}
// AutoDeleteConfig represents the configuration specific for the AutoDelete behaviour
type AutoDeleteConfig struct {
Enabled bool
Lifetime time.Duration
TaskInterval time.Duration
Enabled bool `default:"false"`
Lifetime time.Duration `default:"720h"`
TaskInterval time.Duration `default:"5m" split_words:"true"`
}
// FileConfig represents the configuration specific for the file storage driver
type FileConfig struct {
Path string
}
// PostgresConfig represents the configuration specific for the Postgres storage driver
type PostgresConfig struct {
DSN string
}
// MongoDBConfig represents the configuration specific for the MongoDB storage driver
type MongoDBConfig struct {
DSN string
Database string
Collection string
}
// S3Config represents the configuration specific for the S3 storage driver
type S3Config struct {
Endpoint string
AccessKeyID string
SecretAccessKey string
SecretToken string
Secure bool
Region string
Bucket string
}
// ReportConfig represents the configuration specific for the report system
type ReportConfig struct {
Reports bool
ReportWebhook string
ReportWebhookToken string
Enabled bool `default:"false" split_words:"true"`
WebhookURL string `split_words:"true"`
WebhookToken string `split_words:"true"`
}
// Current holds the currently loaded config
var Current *Config
type PostgresConfig struct {
DSN string `default:"postgres://pasty:pasty@localhost/pasty"`
}
// Load loads the current config from environment variables and an optional .env file
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),
IDCharacters: env.MustString("ID_CHARACTERS", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"),
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)), // ---
ModificationTokenCharacters: env.MustString("MODIFICATION_TOKEN_CHARACTERS", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"),
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"),
},
Postgres: &PostgresConfig{
DSN: env.MustString("STORAGE_POSTGRES_DSN", "postgres://pasty:pasty@localhost/pasty"),
},
MongoDB: &MongoDBConfig{
DSN: env.MustString("STORAGE_MONGODB_CONNECTION_STRING", "mongodb://pasty:pasty@localhost/pasty"),
Database: env.MustString("STORAGE_MONGODB_DATABASE", "pasty"),
Collection: env.MustString("STORAGE_MONGODB_COLLECTION", "pastes"),
},
S3: &S3Config{
Endpoint: env.MustString("STORAGE_S3_ENDPOINT", ""),
AccessKeyID: env.MustString("STORAGE_S3_ACCESS_KEY_ID", ""),
SecretAccessKey: env.MustString("STORAGE_S3_SECRET_ACCESS_KEY", ""),
SecretToken: env.MustString("STORAGE_S3_SECRET_TOKEN", ""),
Secure: env.MustBool("STORAGE_S3_SECURE", true),
Region: env.MustString("STORAGE_S3_REGION", ""),
Bucket: env.MustString("STORAGE_S3_BUCKET", "pasty"),
},
func Load() (*Config, error) {
_ = godotenv.Overload()
cfg := new(Config)
if err := envconfig.Process("pasty", cfg); err != nil {
return nil, err
}
return cfg, nil
}

42
internal/env/env.go vendored
View File

@ -1,42 +0,0 @@
package env
import (
"os"
"strconv"
"time"
"github.com/joho/godotenv"
"github.com/lus/pasty/internal/static"
)
// Load loads an optional .env file
func Load() {
godotenv.Load()
}
// MustString returns the content of the environment variable with the given key or the given fallback
func MustString(key, fallback string) string {
value, found := os.LookupEnv(static.EnvironmentVariablePrefix + key)
if !found {
return fallback
}
return value
}
// MustBool uses MustString and parses it into a boolean
func MustBool(key string, fallback bool) bool {
parsed, _ := strconv.ParseBool(MustString(key, strconv.FormatBool(fallback)))
return parsed
}
// MustInt uses MustString and parses it into an integer
func MustInt(key string, fallback int) int {
parsed, _ := strconv.Atoi(MustString(key, strconv.Itoa(fallback)))
return parsed
}
// MustDuration uses MustString and parses it into a duration
func MustDuration(key string, fallback time.Duration) time.Duration {
parsed, _ := time.ParseDuration(MustString(key, fallback.String()))
return parsed
}

13
internal/meta/metadata.go Normal file
View File

@ -0,0 +1,13 @@
package meta
import "strings"
const devEnvironmentName = "dev"
var (
Environment = devEnvironmentName
)
func IsProdEnvironment() bool {
return strings.ToLower(Environment) != devEnvironmentName
}

View File

@ -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
}
}
}

31
internal/pastes/paste.go Normal file
View File

@ -0,0 +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]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
}

View File

@ -0,0 +1,14 @@
package pastes
import (
"context"
"time"
)
type Repository interface {
ListIDs(ctx context.Context) ([]string, error)
FindByID(ctx context.Context, id string) (*Paste, error)
Upsert(ctx context.Context, paste *Paste) error
DeleteByID(ctx context.Context, id string) error
DeleteOlderThan(ctx context.Context, age time.Duration) (int, error)
}

14
internal/randx/string.go Normal file
View File

@ -0,0 +1,14 @@
package randx
import (
"math/rand"
)
// String generates a random string with the given length.
func String(characters string, length int) string {
bytes := make([]byte, length)
for i := range bytes {
bytes[i] = characters[rand.Int63()%int64(len(characters))]
}
return string(bytes)
}

View File

@ -1,57 +0,0 @@
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"`
}
// ReportResponse represents a report response received from the report webhook
type ReportResponse struct {
Success bool `json:"success"`
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
}

View File

@ -1,42 +0,0 @@
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"` // Required for legacy paste storage support
ModificationToken string `json:"modificationToken,omitempty" bson:"modificationToken"`
Created int64 `json:"created" bson:"created"`
Metadata map[string]interface{} `json:"metadata" bson:"metadata"`
}
// 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.ModificationToken = hash
return nil
}
// 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
}

View File

@ -1,11 +0,0 @@
package shared
// StorageType represents a type of storage a paste can be stored with
type StorageType string
const (
StorageTypeFile = StorageType("file")
StorageTypePostgres = StorageType("postgres")
StorageTypeMongoDB = StorageType("mongodb")
StorageTypeS3 = StorageType("s3")
)

View File

@ -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
}

View File

@ -1,11 +0,0 @@
package static
// These variables represent the values that may be changed using ldflags
var (
Version = "dev"
EnvironmentVariablePrefix = "PASTY_"
// TempFrontendPath defines the path where pasty loads the web frontend from; it will be removed any time soon
// TODO: Remove this when issue #37 is fixed
TempFrontendPath = "./web"
)

View File

@ -1,59 +1,12 @@
package storage
import (
"fmt"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage/file"
"github.com/lus/pasty/internal/storage/mongodb"
"github.com/lus/pasty/internal/storage/postgres"
"github.com/lus/pasty/internal/storage/s3"
"context"
"github.com/lus/pasty/internal/pastes"
)
// Current holds the current storage driver
var Current Driver
// Driver represents a storage driver
type Driver interface {
Initialize() error
Terminate() error
ListIDs() ([]string, error)
Get(id string) (*shared.Paste, error)
Save(paste *shared.Paste) error
Delete(id string) error
Cleanup() (int, error)
}
// Load loads the current storage driver
func Load() error {
// Define the driver to use
driver, err := GetDriver(config.Current.StorageType)
if err != nil {
return err
}
// Initialize the driver
err = driver.Initialize()
if err != nil {
return err
}
Current = driver
return nil
}
// GetDriver returns the driver with the given type if it exists
func GetDriver(storageType shared.StorageType) (Driver, error) {
switch storageType {
case shared.StorageTypeFile:
return new(file.FileDriver), nil
case shared.StorageTypePostgres:
return new(postgres.PostgresDriver), nil
case shared.StorageTypeMongoDB:
return new(mongodb.MongoDBDriver), nil
case shared.StorageTypeS3:
return new(s3.S3Driver), nil
default:
return nil, fmt.Errorf("invalid storage type '%s'", storageType)
}
Initialize(ctx context.Context) error
Close() error
Pastes() pastes.Repository
}

View File

@ -1,145 +0,0 @@
package file
import (
"encoding/base64"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
)
// FileDriver represents the file storage driver
type FileDriver struct {
filePath string
}
// Initialize initializes the file storage driver
func (driver *FileDriver) Initialize() error {
driver.filePath = config.Current.File.Path
return os.MkdirAll(driver.filePath, os.ModePerm)
}
// Terminate terminates the file storage driver (does nothing, because the file storage driver does not need any termination)
func (driver *FileDriver) Terminate() error {
return nil
}
// ListIDs returns a list of all existing paste IDs
func (driver *FileDriver) ListIDs() ([]string, error) {
// Define the IDs slice
var ids []string
// Fill the IDs slice
err := filepath.Walk(driver.filePath, func(path string, info os.FileInfo, err error) error {
// Check if a walking error occurred
if err != nil {
return err
}
// Only count JSON files
if !strings.HasSuffix(info.Name(), ".json") {
return nil
}
// Decode the file name
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(info.Name(), ".json"))
if err != nil {
return err
}
// Append the ID to the IDs slice
ids = append(ids, string(decoded))
return nil
})
if err != nil {
return nil, err
}
// Return the IDs slice
return ids, nil
}
// Get loads a paste
func (driver *FileDriver) Get(id string) (*shared.Paste, error) {
// Read the file
id = base64.StdEncoding.EncodeToString([]byte(id))
data, err := ioutil.ReadFile(filepath.Join(driver.filePath, id+".json"))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
// Unmarshal the file into a paste
paste := new(shared.Paste)
err = json.Unmarshal(data, &paste)
if err != nil {
return nil, err
}
return paste, nil
}
// Save saves a paste
func (driver *FileDriver) Save(paste *shared.Paste) error {
// Marshal the paste
jsonBytes, err := json.Marshal(paste)
if err != nil {
return err
}
// Create the file to save the paste to
id := base64.StdEncoding.EncodeToString([]byte(paste.ID))
file, err := os.Create(filepath.Join(driver.filePath, id+".json"))
if err != nil {
return err
}
defer file.Close()
// Write the JSON data into the file
_, err = file.Write(jsonBytes)
return err
}
// Delete deletes a paste
func (driver *FileDriver) Delete(id string) error {
id = base64.StdEncoding.EncodeToString([]byte(id))
return os.Remove(filepath.Join(driver.filePath, id+".json"))
}
// Cleanup cleans up the expired pastes
func (driver *FileDriver) Cleanup() (int, error) {
// Retrieve all paste IDs
ids, err := driver.ListIDs()
if err != nil {
return 0, err
}
// Define the amount of deleted items
deleted := 0
// Loop through all pastes
for _, id := range ids {
// Retrieve the paste object
paste, err := driver.Get(id)
if err != nil {
return deleted, err
}
// Delete the paste if it is expired
lifetime := config.Current.AutoDelete.Lifetime
if paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() {
err = driver.Delete(id)
if err != nil {
return deleted, err
}
deleted++
}
}
return deleted, nil
}

View File

@ -1,20 +0,0 @@
package storage
import (
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/utils"
)
// AcquireID generates a new unique ID
func AcquireID() (string, error) {
for {
id := utils.RandomString(config.Current.IDCharacters, config.Current.IDLength)
paste, err := Current.Get(id)
if err != nil {
return "", err
}
if paste == nil {
return id, nil
}
}
}

View File

@ -1,171 +0,0 @@
package mongodb
import (
"context"
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
// MongoDBDriver represents the MongoDB storage driver
type MongoDBDriver struct {
client *mongo.Client
database string
collection string
}
// Initialize initializes the MongoDB storage driver
func (driver *MongoDBDriver) Initialize() error {
// Define the context for the following database operation
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Connect to the MongoDB host
client, err := mongo.Connect(ctx, options.Client().ApplyURI(config.Current.MongoDB.DSN))
if err != nil {
return err
}
// Ping the MongoDB host
err = client.Ping(ctx, readpref.Primary())
if err != nil {
return err
}
// Set the driver attributes
driver.client = client
driver.database = config.Current.MongoDB.Database
driver.collection = config.Current.MongoDB.Collection
return nil
}
// Terminate terminates the MongoDB storage driver
func (driver *MongoDBDriver) Terminate() error {
return driver.client.Disconnect(context.TODO())
}
// ListIDs returns a list of all existing paste IDs
func (driver *MongoDBDriver) ListIDs() ([]string, error) {
// Define the collection to use for this database operation
collection := driver.client.Database(driver.database).Collection(driver.collection)
// Define the context for the following database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Retrieve all paste documents
result, err := collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}
// Decode all paste documents
var pasteSlice []shared.Paste
err = result.All(ctx, &pasteSlice)
if err != nil {
return nil, err
}
// Read and return the IDs of all paste objects
var ids []string
for _, paste := range pasteSlice {
ids = append(ids, paste.ID)
}
return ids, nil
}
// Get loads a paste
func (driver *MongoDBDriver) Get(id string) (*shared.Paste, error) {
// Define the collection to use for this database operation
collection := driver.client.Database(driver.database).Collection(driver.collection)
// Define the context for the following database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Try to retrieve the corresponding paste document
filter := bson.M{"_id": id}
result := collection.FindOne(ctx, filter)
err := result.Err()
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, err
}
// Return the retrieved paste object
paste := new(shared.Paste)
err = result.Decode(paste)
if err != nil {
return nil, err
}
return paste, nil
}
// Save saves a paste
func (driver *MongoDBDriver) Save(paste *shared.Paste) error {
// Define the collection to use for this database operation
collection := driver.client.Database(driver.database).Collection(driver.collection)
// Define the context for the following database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Upsert the paste object
filter := bson.M{"_id": paste.ID}
_, err := collection.UpdateOne(ctx, filter, bson.M{"$set": paste}, options.Update().SetUpsert(true))
return err
}
// Delete deletes a paste
func (driver *MongoDBDriver) Delete(id string) error {
// Define the collection to use for this database operation
collection := driver.client.Database(driver.database).Collection(driver.collection)
// Define the context for the following database operation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Delete the document
filter := bson.M{"_id": id}
_, err := collection.DeleteOne(ctx, filter)
return err
}
// Cleanup cleans up the expired pastes
func (driver *MongoDBDriver) Cleanup() (int, error) {
// Retrieve all paste IDs
ids, err := driver.ListIDs()
if err != nil {
return 0, err
}
// Define the amount of deleted items
deleted := 0
// Loop through all pastes
for _, id := range ids {
// Retrieve the paste object
paste, err := driver.Get(id)
if err != nil {
return 0, err
}
// Delete the paste if it is expired
lifetime := config.Current.AutoDelete.Lifetime
if paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() {
err = driver.Delete(id)
if err != nil {
return 0, err
}
deleted++
}
}
return deleted, nil
}

View File

@ -0,0 +1,74 @@
package postgres
import (
"context"
"embed"
"errors"
"github.com/golang-migrate/migrate/v4"
_ "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/pastes"
"github.com/lus/pasty/internal/storage"
"github.com/rs/zerolog/log"
)
//go:embed migrations/*.sql
var migrations embed.FS
type Driver struct {
dsn string
connPool *pgxpool.Pool
pastes *pasteRepository
}
var _ storage.Driver = (*Driver)(nil)
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
}
log.Info().Msg("Performing PostgreSQL database migrations...")
source, err := iofs.New(migrations, "migrations")
if err != nil {
pool.Close()
return err
}
migrator, err := migrate.NewWithSourceInstance("iofs", source, driver.dsn)
if err != nil {
pool.Close()
return err
}
defer func() {
_, _ = migrator.Close()
}()
if err := migrator.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
pool.Close()
return err
}
log.Info().Msg("Successfully performed PostgreSQL database migrations.")
driver.connPool = pool
driver.pastes = &pasteRepository{
connPool: pool,
}
return nil
}
func (driver *Driver) Close() error {
driver.pastes = nil
driver.connPool.Close()
return nil
}
func (driver *Driver) Pastes() pastes.Repository {
return driver.pastes
}

View File

@ -0,0 +1,63 @@
package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/lus/pasty/internal/pastes"
"time"
)
type pasteRepository struct {
connPool *pgxpool.Pool
}
var _ pastes.Repository = (*pasteRepository)(nil)
func (repo *pasteRepository) ListIDs(ctx context.Context) ([]string, error) {
rows, _ := repo.connPool.Query(ctx, "SELECT id FROM pastes")
result, err := pgx.CollectRows(rows, pgx.RowTo[string])
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, err
}
return result, nil
}
func (repo *pasteRepository) FindByID(ctx context.Context, id string) (*pastes.Paste, error) {
rows, _ := repo.connPool.Query(ctx, "SELECT * FROM pastes WHERE id = $1", id)
result, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByPos[pastes.Paste])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return result, nil
}
func (repo *pasteRepository) Upsert(ctx context.Context, paste *pastes.Paste) error {
const query = `
INSERT INTO pastes
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE
SET content = excluded.content,
"modificationToken" = excluded."modificationToken",
metadata = excluded.metadata
`
_, err := repo.connPool.Exec(ctx, query, paste.ID, paste.Content, paste.ModificationToken, paste.Created, paste.Metadata)
return err
}
func (repo *pasteRepository) DeleteByID(ctx context.Context, id string) error {
_, err := repo.connPool.Exec(ctx, "DELETE FROM pastes WHERE id = $1", id)
return err
}
func (repo *pasteRepository) DeleteOlderThan(ctx context.Context, age time.Duration) (int, error) {
tag, err := repo.connPool.Exec(ctx, "DELETE FROM pastes WHERE created < $1", time.Now().Add(-age).Unix())
if err != nil {
return 0, err
}
return int(tag.RowsAffected()), nil
}

View File

@ -1,127 +0,0 @@
package postgres
import (
"context"
"embed"
"errors"
"time"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/johejo/golang-migrate-extra/source/iofs"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
)
//go:embed migrations/*.sql
var migrations embed.FS
// PostgresDriver represents the Postgres storage driver
type PostgresDriver struct {
pool *pgxpool.Pool
}
// Initialize initializes the Postgres storage driver
func (driver *PostgresDriver) Initialize() error {
pool, err := pgxpool.Connect(context.Background(), config.Current.Postgres.DSN)
if err != nil {
return err
}
source, err := iofs.New(migrations, "migrations")
if err != nil {
return err
}
migrator, err := migrate.NewWithSourceInstance("iofs", source, config.Current.Postgres.DSN)
if err != nil {
return err
}
if err := migrator.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
driver.pool = pool
return nil
}
// Terminate terminates the Postgres storage driver
func (driver *PostgresDriver) Terminate() error {
driver.pool.Close()
return nil
}
// ListIDs returns a list of all existing paste IDs
func (driver *PostgresDriver) ListIDs() ([]string, error) {
query := "SELECT id FROM pastes"
rows, err := driver.pool.Query(context.Background(), query)
if err != nil {
return []string{}, err
}
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return []string{}, err
}
ids = append(ids, id)
}
return ids, nil
}
// Get loads a paste
func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
query := "SELECT * FROM pastes WHERE id = $1"
row := driver.pool.QueryRow(context.Background(), query, id)
paste := new(shared.Paste)
if err := row.Scan(&paste.ID, &paste.Content, &paste.ModificationToken, &paste.Created, &paste.Metadata); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return paste, nil
}
// Save saves a paste
func (driver *PostgresDriver) Save(paste *shared.Paste) error {
query := `
INSERT INTO pastes (id, content, "modificationToken", created, metadata)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE
SET content = excluded.content,
"modificationToken" = excluded."modificationToken",
created = excluded.created,
metadata = excluded.metadata
`
_, err := driver.pool.Exec(context.Background(), query, paste.ID, paste.Content, paste.ModificationToken, paste.Created, paste.Metadata)
return err
}
// Delete deletes a paste
func (driver *PostgresDriver) Delete(id string) error {
query := "DELETE FROM pastes WHERE id = $1"
_, err := driver.pool.Exec(context.Background(), query, id)
return err
}
// Cleanup cleans up the expired pastes
func (driver *PostgresDriver) Cleanup() (int, error) {
query := "DELETE FROM pastes WHERE created < $1"
tag, err := driver.pool.Exec(context.Background(), query, time.Now().Add(-config.Current.AutoDelete.Lifetime).Unix())
if err != nil {
return 0, err
}
return int(tag.RowsAffected()), nil
}

View File

@ -1,136 +0,0 @@
package s3
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"strings"
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// S3Driver represents the AWS S3 storage driver
type S3Driver struct {
client *minio.Client
bucket string
}
// Initialize initializes the AWS S3 storage driver
func (driver *S3Driver) Initialize() error {
client, err := minio.New(config.Current.S3.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.Current.S3.AccessKeyID, config.Current.S3.SecretAccessKey, config.Current.S3.SecretToken),
Secure: config.Current.S3.Secure,
Region: config.Current.S3.Region,
})
if err != nil {
return err
}
driver.client = client
driver.bucket = config.Current.S3.Bucket
return nil
}
// Terminate terminates the AWS S3 storage driver (does nothing, because the AWS S3 storage driver does not need any termination)
func (driver *S3Driver) Terminate() error {
return nil
}
// ListIDs returns a list of all existing paste IDs
func (driver *S3Driver) ListIDs() ([]string, error) {
// Define the IDs slice
var ids []string
// Fill the IDs slice
channel := driver.client.ListObjects(context.Background(), driver.bucket, minio.ListObjectsOptions{})
for object := range channel {
if object.Err != nil {
return nil, object.Err
}
ids = append(ids, strings.TrimSuffix(object.Key, ".json"))
}
// Return the IDs slice
return ids, nil
}
// Get loads a paste
func (driver *S3Driver) Get(id string) (*shared.Paste, error) {
// Read the object
object, err := driver.client.GetObject(context.Background(), driver.bucket, id+".json", minio.GetObjectOptions{})
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(object)
if err != nil {
if minio.ToErrorResponse(err).Code == "NoSuchKey" {
return nil, nil
}
return nil, err
}
// Unmarshal the object into a paste
paste := new(shared.Paste)
err = json.Unmarshal(data, &paste)
if err != nil {
return nil, err
}
return paste, nil
}
// Save saves a paste
func (driver *S3Driver) Save(paste *shared.Paste) error {
// Marshal the paste
jsonBytes, err := json.Marshal(paste)
if err != nil {
return err
}
// Put the object
reader := bytes.NewReader(jsonBytes)
_, err = driver.client.PutObject(context.Background(), driver.bucket, paste.ID+".json", reader, reader.Size(), minio.PutObjectOptions{
ContentType: "application/json",
})
return err
}
// Delete deletes a paste
func (driver *S3Driver) Delete(id string) error {
return driver.client.RemoveObject(context.Background(), driver.bucket, id+".json", minio.RemoveObjectOptions{})
}
// Cleanup cleans up the expired pastes
func (driver *S3Driver) Cleanup() (int, error) {
// Retrieve all paste IDs
ids, err := driver.ListIDs()
if err != nil {
return 0, err
}
// Define the amount of deleted items
deleted := 0
// Loop through all pastes
for _, id := range ids {
// Retrieve the paste object
paste, err := driver.Get(id)
if err != nil {
return 0, err
}
// Delete the paste if it is expired
lifetime := config.Current.AutoDelete.Lifetime
if paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() {
err = driver.Delete(id)
if err != nil {
return 0, err
}
deleted++
}
}
return deleted, nil
}

View File

@ -1,16 +0,0 @@
package utils
import (
"math/rand"
"time"
)
// RandomString returns a random string with the given length
func RandomString(characters string, length int) string {
rand.Seed(time.Now().UnixNano())
bytes := make([]byte, length)
for i := range bytes {
bytes[i] = characters[rand.Int63()%int64(len(characters))]
}
return string(bytes)
}

View File

@ -1,72 +0,0 @@
package v1
import (
"encoding/json"
"time"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/utils"
"github.com/valyala/fasthttp"
)
// HastebinSupportHandler handles the legacy hastebin requests
func HastebinSupportHandler(ctx *fasthttp.RequestCtx) {
// Check content length before reading body into memory
if config.Current.LengthCap > 0 &&
ctx.Request.Header.ContentLength() > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("request body length overflow")
return
}
// Define the paste content
var content string
if string(ctx.Request.Header.ContentType()) == "multipart/form-data" {
content = string(ctx.FormValue("data"))
} else {
content = string(ctx.PostBody())
}
// Acquire the paste ID
id, err := storage.AcquireID()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Create the paste object
paste := &shared.Paste{
ID: id,
Content: content,
Created: time.Now().Unix(),
}
// Set a modification token
if config.Current.ModificationTokens {
paste.ModificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, config.Current.ModificationTokenLength)
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 key
jsonData, _ := json.Marshal(map[string]string{
"key": paste.ID,
})
ctx.SetBody(jsonData)
}

View File

@ -1,24 +0,0 @@
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"`
}
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,
}
}

View File

@ -1,180 +0,0 @@
package v1
import (
"encoding/json"
"time"
"github.com/fasthttp/router"
"github.com/lus/pasty/internal/config"
"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 '/v1/pastes/*' controller
func InitializePastesController(group *router.Group, rateLimiterMiddleware *limitFasthttp.Middleware) {
group.GET("/{id}", rateLimiterMiddleware.Handle(v1GetPaste))
group.POST("", rateLimiterMiddleware.Handle(v1PostPaste))
group.DELETE("/{id}", rateLimiterMiddleware.Handle(v1DeletePaste))
}
// v1GetPaste handles the 'GET /v1/pastes/{id}' endpoint
func v1GetPaste(ctx *fasthttp.RequestCtx) {
// Read the ID
id := ctx.UserValue("id").(string)
// Retrieve the paste
paste, err := storage.Current.Get(id)
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
}
legacyPaste := legacyFromModern(paste)
legacyPaste.DeletionToken = ""
// Respond with the paste
jsonData, err := json.Marshal(legacyPaste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}
// v1PostPaste handles the 'POST /v1/pastes' endpoint
func v1PostPaste(ctx *fasthttp.RequestCtx) {
// Check content length before reading body into memory
if config.Current.LengthCap > 0 &&
ctx.Request.Header.ContentLength() > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("request body length overflow")
return
}
// Unmarshal the body
values := make(map[string]string)
err := json.Unmarshal(ctx.PostBody(), &values)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("invalid request body")
return
}
// Validate the content of the paste
if values["content"] == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing 'content' field")
return
}
// Acquire the paste ID
id, err := storage.AcquireID()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Create the paste object
paste := &shared.Paste{
ID: id,
Content: values["content"],
Created: time.Now().Unix(),
}
// Set a modification token
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, 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 := legacyFromModern(paste)
pasteCopy.DeletionToken = modificationToken
jsonData, err := json.Marshal(pasteCopy)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}
// v1DeletePaste handles the 'DELETE /v1/pastes/{id}'
func v1DeletePaste(ctx *fasthttp.RequestCtx) {
// Read the ID
id := ctx.UserValue("id").(string)
// Unmarshal the body
values := make(map[string]string)
err := json.Unmarshal(ctx.PostBody(), &values)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("invalid request body")
return
}
// Validate the modification token of the paste
modificationToken := values["deletionToken"]
if modificationToken == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing 'deletionToken' field")
return
}
// Retrieve the paste
paste, err := storage.Current.Get(id)
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
}
// 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
}
// Delete the paste
err = storage.Current.Delete(paste.ID)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Respond with 'ok'
ctx.SetBodyString("ok")
}

View File

@ -1,274 +0,0 @@
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(),
Metadata: payload.Metadata,
}
// Create a new modification token if enabled
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, 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,
}
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

@ -1,9 +0,0 @@
package web
// nilLogger represents a logger that does not print anything
type nilLogger struct {
}
// Printf prints nothing
func (logger *nilLogger) Printf(string, ...interface{}) {
}

View File

@ -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)
}
}

49
internal/web/server.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -1,150 +0,0 @@
package web
import (
"encoding/json"
"path/filepath"
"strings"
routing "github.com/fasthttp/router"
"github.com/lus/pasty/internal/config"
"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"
"github.com/valyala/fasthttp"
)
// Serve serves the web resources
func Serve() error {
// Create the router
router := routing.New()
// Define the 404 handler
router.NotFound = func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("not found")
}
// Route the frontend requests
frontend := frontendHandler()
raw := rawHandler()
router.GET("/{path:*}", func(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
if !strings.HasPrefix(path, "/api") && (strings.Count(path, "/") == 1 || strings.HasPrefix(path, "/assets")) {
if strings.HasPrefix(path, "/assets/js/") {
ctx.SetContentType("text/javascript")
}
frontend(ctx)
return
} else if strings.HasSuffix(strings.TrimSuffix(path, "/"), "/raw") {
raw(ctx)
return
}
router.NotFound(ctx)
})
// Set up the rate limiter
rate, err := limiter.NewRateFromFormatted(config.Current.RateLimit)
if err != nil {
return err
}
rateLimiter := limiter.New(memory.NewStore(), rate)
rateLimiterMiddleware := limitFasthttp.NewMiddleware(rateLimiter)
// Route the API endpoints
apiRoute := router.Group("/api")
{
v1Route := apiRoute.Group("/v1")
{
v1Route.GET("/info", func(ctx *fasthttp.RequestCtx) {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"deletionTokens": config.Current.ModificationTokens,
})
ctx.SetBody(jsonData)
})
v1.InitializePastesController(v1Route.Group("/pastes"), rateLimiterMiddleware)
}
v2Route := apiRoute.Group("/v2")
{
pasteLifetime := int64(-1)
if config.Current.AutoDelete.Enabled {
pasteLifetime = config.Current.AutoDelete.Lifetime.Milliseconds()
}
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,
"pasteLifetime": pasteLifetime,
})
ctx.SetBody(jsonData)
})
v2.InitializePastesController(v2Route.Group("/pastes"), rateLimiterMiddleware)
}
}
// Route the hastebin documents route if hastebin support is enabled
if config.Current.HastebinSupport {
router.POST("/documents", rateLimiterMiddleware.Handle(v1.HastebinSupportHandler))
}
// Serve the web resources
return (&fasthttp.Server{
Handler: func(ctx *fasthttp.RequestCtx) {
// Add the CORS headers
ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS")
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
// Call the router handler
router.Handler(ctx)
},
Logger: new(nilLogger),
}).ListenAndServe(config.Current.WebAddress)
}
// frontendHandler handles the frontend routing
func frontendHandler() fasthttp.RequestHandler {
// Create the file server
fs := &fasthttp.FS{
Root: static.TempFrontendPath,
IndexNames: []string{"index.html"},
CacheDuration: 0,
}
fs.PathNotFound = func(ctx *fasthttp.RequestCtx) {
if strings.HasPrefix(string(ctx.Path()), "/assets") {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("not found")
return
}
ctx.SendFile(filepath.Join(fs.Root, "index.html"))
}
return fs.NewRequestHandler()
}
func rawHandler() fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
pathSanitized := strings.TrimPrefix(strings.TrimSuffix(path, "/"), "/")
pasteID := strings.TrimSuffix(pathSanitized, "/raw")
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
}
ctx.SetBodyString(paste.Content)
}
}

View File

@ -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");

View File

@ -64,20 +64,20 @@ export async function initialize() {
}
if (location.pathname !== "/") {
// Extract the paste data (ID and language)
// Extract the pastes data (ID and language)
const split = location.pathname.replace("/", "").split(".");
const pasteID = split[0];
const language = split[1];
// Try to retrieve the paste data from the API
// Try to retrieve the pastes data from the API
const response = await API.getPaste(pasteID);
if (!response.ok) {
Notifications.error("Could not load paste: <b>" + await response.text() + "</b>");
Notifications.error("Could not load pastes: <b>" + await response.text() + "</b>");
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
return;
}
// Set the persistent paste data
// Set the persistent pastes data
PASTE_ID = pasteID;
LANGUAGE = language;
@ -95,7 +95,7 @@ export async function initialize() {
ENCRYPTION_IV = json.metadata.pf_encryption.iv;
} catch (error) {
console.log(error);
Notifications.error("Could not decrypt paste; make sure the decryption key is correct.");
Notifications.error("Could not decrypt pastes; make sure the decryption key is correct.");
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
return;
}
@ -104,7 +104,7 @@ export async function initialize() {
// Fill the code block with the just received data
updateCode();
} else {
// Give the user the opportunity to paste his code
// Give the user the opportunity to pastes his code
INPUT_ELEMENT.classList.remove("hidden");
INPUT_ELEMENT.focus();
LIFETIME_CONTAINER_ELEMENT.classList.remove("hidden");
@ -138,7 +138,7 @@ async function loadAPIInformation() {
// Display the API version
document.getElementById("version").innerText = API_INFORMATION.version;
// Display the paste lifetime
// Display the pastes lifetime
document.getElementById("lifetime").innerText = Duration.format(API_INFORMATION.pasteLifetime);
}
@ -289,7 +289,7 @@ function setupButtonFunctionality() {
return;
}
// Encrypt the paste if needed
// Encrypt the pastes if needed
let value = INPUT_ELEMENT.value;
let metadata;
let key;
@ -305,20 +305,20 @@ function setupButtonFunctionality() {
key = encrypted.key;
}
// Try to create the paste
// Try to create the pastes
const response = await API.createPaste(value, metadata);
if (!response.ok) {
Notifications.error("Error while creating paste: <b>" + await response.text() + "</b>");
Notifications.error("Error while creating pastes: <b>" + await response.text() + "</b>");
return;
}
const data = await response.json();
// Display the modification token if provided
if (data.modificationToken) {
prompt("The modification token for your paste is:", data.modificationToken);
prompt("The modification token for your pastes is:", data.modificationToken);
}
// Redirect the user to his newly created paste
// Redirect the user to his newly created pastes
location.replace(location.protocol + "//" + location.host + "/" + data.id + (key ? "#" + key : ""));
});
});
@ -333,10 +333,10 @@ function setupButtonFunctionality() {
return;
}
// Try to delete the paste
// Try to delete the pastes
const response = await API.deletePaste(PASTE_ID, modificationToken);
if (!response.ok) {
Notifications.error("Error while deleting paste: <b>" + await response.text() + "</b>");
Notifications.error("Error while deleting pastes: <b>" + await response.text() + "</b>");
return;
}
@ -369,17 +369,17 @@ function setupButtonFunctionality() {
return;
}
// Re-encrypt the paste data if needed
// Re-encrypt the pastes data if needed
let value = INPUT_ELEMENT.value;
if (ENCRYPTION_KEY && ENCRYPTION_IV) {
const encrypted = await Encryption.encrypt(await Encryption.encryptionDataFromHex(ENCRYPTION_KEY, ENCRYPTION_IV), value);
value = encrypted.result;
}
// Try to edit the paste
// Try to edit the pastes
const response = await API.editPaste(PASTE_ID, modificationToken, value);
if (!response.ok) {
Notifications.error("Error while editing paste: <b>" + await response.text() + "</b>");
Notifications.error("Error while editing pastes: <b>" + await response.text() + "</b>");
return;
}
@ -387,13 +387,13 @@ function setupButtonFunctionality() {
CODE = INPUT_ELEMENT.value;
updateCode();
toggleEditMode();
Notifications.success("Successfully edited paste.");
Notifications.success("Successfully edited pastes.");
});
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.addEventListener("click", () => {
const active = BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.toggle("active");
localStorage.setItem("encryption", active);
Notifications.success((active ? "Enabled" : "Disabled") + " automatic paste encryption.");
Notifications.success((active ? "Enabled" : "Disabled") + " automatic pastes encryption.");
});
BUTTON_REPORT_ELEMENT.addEventListener("click", async () => {
@ -403,17 +403,17 @@ function setupButtonFunctionality() {
return;
}
// Try to report the paste
// Try to report the pastes
const response = await API.reportPaste(PASTE_ID, reason);
if (!response.ok) {
Notifications.error("Error while reporting paste: <b>" + await response.text() + "</b>");
Notifications.error("Error while reporting pastes: <b>" + await response.text() + "</b>");
return;
}
// Show the response message
const data = await response.json();
if (!data.success) {
Notifications.error("Error while reporting paste: <b>" + data.message + "</b>");
Notifications.error("Error while reporting pastes: <b>" + data.message + "</b>");
return;
}
Notifications.success(data.message);