├── .github ├── FUNDING.yml └── workflows │ ├── general.yml │ └── docker.yml ├── internal ├── storage │ ├── postgres │ │ ├── migrations │ │ │ ├── 000001_initialize_schema.down.sql │ │ │ ├── 000003_add_metadata.down.sql │ │ │ ├── 000003_add_metadata.up.sql │ │ │ ├── 000004_remove_paste_specific_autodelete.up.sql │ │ │ ├── 000004_remove_paste_specific_autodelete.down.sql │ │ │ ├── 000002_rename_deletion_token.down.sql │ │ │ ├── 000002_rename_deletion_token.up.sql │ │ │ └── 000001_initialize_schema.up.sql │ │ └── postgres_driver.go │ ├── id_generation.go │ ├── driver.go │ ├── file │ │ └── file_driver.go │ ├── s3 │ │ └── s3_driver.go │ └── mongodb │ │ └── mongodb_driver.go ├── web │ ├── logger.go │ ├── controllers │ │ ├── v1 │ │ │ ├── paste_adapter.go │ │ │ ├── hastebin_support.go │ │ │ └── pastes.go │ │ └── v2 │ │ │ └── pastes.go │ └── web.go ├── shared │ ├── storage_type.go │ └── paste.go ├── static │ └── static.go ├── utils │ └── random_string.go ├── env │ └── env.go ├── report │ └── report.go └── config │ └── config.go ├── web ├── assets │ ├── fonts │ │ └── source-code-pro │ │ │ ├── SourceCodePro-Regular.otf.subset.woff2 │ │ │ └── font.css │ ├── js │ │ ├── app.js │ │ └── modules │ │ │ ├── animation.js │ │ │ ├── spinner.js │ │ │ ├── notifications.js │ │ │ ├── duration.js │ │ │ ├── api.js │ │ │ ├── encryption.js │ │ │ └── state.js │ ├── libs │ │ ├── highlightjs │ │ │ └── solarized-dark.min.css │ │ └── aesjs │ │ │ └── aes.min.js │ └── css │ │ ├── style.css.map │ │ ├── style.scss │ │ └── style.css └── index.html ├── docker-compose.dev.yml ├── go.mod ├── Dockerfile ├── LICENSE ├── CREDITS.md ├── cmd ├── pasty │ └── main.go └── transfer │ └── main.go ├── .gitignore ├── API.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 'lus' -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000001_initialize_schema.down.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | drop table if exists "pastes"; 4 | 5 | commit; -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000003_add_metadata.down.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | alter table if exists "pastes" drop column "metadata"; 4 | 5 | commit; -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000003_add_metadata.up.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | alter table if exists "pastes" add column "metadata" jsonb; 4 | 5 | commit; -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000004_remove_paste_specific_autodelete.up.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | alter table if exists "pastes" drop column "autoDelete"; 4 | 5 | commit; -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000004_remove_paste_specific_autodelete.down.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | alter table if exists "pastes" add column "autoDelete" boolean; 4 | 5 | commit; -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000002_rename_deletion_token.down.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | alter table if exists "pastes" rename column "modificationToken" to "deletionToken"; 4 | 5 | commit; -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000002_rename_deletion_token.up.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | alter table if exists "pastes" rename column "deletionToken" to "modificationToken"; 4 | 5 | commit; -------------------------------------------------------------------------------- /web/assets/fonts/source-code-pro/SourceCodePro-Regular.otf.subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leafee98/pasty/master/web/assets/fonts/source-code-pro/SourceCodePro-Regular.otf.subset.woff2 -------------------------------------------------------------------------------- /web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | import * as Spinner from "./modules/spinner.js"; 2 | import * as State from "./modules/state.js"; 3 | 4 | // Initialize the application state 5 | Spinner.surround(State.initialize); 6 | -------------------------------------------------------------------------------- /internal/web/logger.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | // nilLogger represents a logger that does not print anything 4 | type nilLogger struct { 5 | } 6 | 7 | // Printf prints nothing 8 | func (logger *nilLogger) Printf(string, ...interface{}) { 9 | } 10 | -------------------------------------------------------------------------------- /internal/storage/postgres/migrations/000001_initialize_schema.up.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | create table if not exists "pastes" ( 4 | "id" text not null, 5 | "content" text not null, 6 | "deletionToken" text not null, 7 | "created" bigint not null, 8 | "autoDelete" boolean not null, 9 | primary key ("id") 10 | ); 11 | 12 | commit; -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | volumes: 4 | postgres: 5 | 6 | services: 7 | postgres: 8 | image: "postgres:12-alpine" 9 | ports: 10 | - "5432:5432" 11 | volumes: 12 | - "postgres:/var/lib/postgresql/data" 13 | environment: 14 | POSTGRES_PASSWORD: "dev" 15 | POSTGRES_USER: "dev" 16 | POSTGRES_DB: "pasty" 17 | -------------------------------------------------------------------------------- /internal/shared/storage_type.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | // StorageType represents a type of storage a paste can be stored with 4 | type StorageType string 5 | 6 | const ( 7 | StorageTypeFile = StorageType("file") 8 | StorageTypePostgres = StorageType("postgres") 9 | StorageTypeMongoDB = StorageType("mongodb") 10 | StorageTypeS3 = StorageType("s3") 11 | ) 12 | -------------------------------------------------------------------------------- /internal/static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | // These variables represent the values that may be changed using ldflags 4 | var ( 5 | Version = "dev" 6 | EnvironmentVariablePrefix = "PASTY_" 7 | 8 | // TempFrontendPath defines the path where pasty loads the web frontend from; it will be removed any time soon 9 | // TODO: Remove this when issue #37 is fixed 10 | TempFrontendPath = "./web" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/utils/random_string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // RandomString returns a random string with the given length 9 | func RandomString(characters string, length int) string { 10 | rand.Seed(time.Now().UnixNano()) 11 | bytes := make([]byte, length) 12 | for i := range bytes { 13 | bytes[i] = characters[rand.Int63()%int64(len(characters))] 14 | } 15 | return string(bytes) 16 | } 17 | -------------------------------------------------------------------------------- /internal/storage/id_generation.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/lus/pasty/internal/config" 5 | "github.com/lus/pasty/internal/utils" 6 | ) 7 | 8 | // AcquireID generates a new unique ID 9 | func AcquireID() (string, error) { 10 | for { 11 | id := utils.RandomString(config.Current.IDCharacters, config.Current.IDLength) 12 | paste, err := Current.Get(id) 13 | if err != nil { 14 | return "", err 15 | } 16 | if paste == nil { 17 | return id, nil 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/assets/fonts/source-code-pro/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Source Code Pro'; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url(SourceCodePro-Regular.otf.subset.woff2) format('woff2'); 7 | unicode-range: U+0000-02AF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 8 | } 9 | -------------------------------------------------------------------------------- /web/assets/js/modules/animation.js: -------------------------------------------------------------------------------- 1 | // Properly animates an element 2 | export function animate(element, animation, duration, after) { 3 | element.style.setProperty("--animate-duration", duration); 4 | element.classList.add("animate__animated", animation); 5 | element.addEventListener("animationend", () => { 6 | element.style.removeProperty("--animate-duration"); 7 | element.classList.remove("animate__animated", animation); 8 | if (after) { 9 | after(); 10 | } 11 | }, {once: true}); 12 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lus/pasty 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b 7 | github.com/fasthttp/router v1.2.4 8 | github.com/golang-migrate/migrate/v4 v4.14.2-0.20201125065321-a53e6fc42574 9 | github.com/jackc/pgx/v4 v4.11.0 10 | github.com/johejo/golang-migrate-extra v0.0.0-20210217013041-51a992e50d16 11 | github.com/joho/godotenv v1.3.0 12 | github.com/minio/minio-go/v7 v7.0.5 13 | github.com/ulule/limiter/v3 v3.5.0 14 | github.com/valyala/fasthttp v1.16.0 15 | go.mongodb.org/mongo-driver v1.8.0 16 | ) 17 | -------------------------------------------------------------------------------- /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: general 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout code 14 | uses: actions/checkout@v2 15 | - name: setup go environment 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ^1.16 19 | - name: download dependencies 20 | run: | 21 | go version 22 | go mod download 23 | - name: build 24 | run: | 25 | go build ./cmd/pasty/ -------------------------------------------------------------------------------- /web/assets/js/modules/spinner.js: -------------------------------------------------------------------------------- 1 | import * as Animation from "./animation.js"; 2 | 3 | const ELEMENT = document.getElementById("spinner-container"); 4 | 5 | // SHows the spinner 6 | export function show() { 7 | ELEMENT.classList.remove("hidden"); 8 | Animation.animate(ELEMENT, "animate__zoomIn", "0.2s"); 9 | } 10 | 11 | // Hides the spinner 12 | export function hide() { 13 | Animation.animate(ELEMENT, "animate__zoomOut", "0.2s", () => ELEMENT.classList.add("hidden")); 14 | } 15 | 16 | // Surrounds an async action with a spinner 17 | export async function surround(innerFunction) { 18 | show(); 19 | await innerFunction(); 20 | hide(); 21 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Choose the golang image as the build base image 2 | FROM golang:1.16-alpine AS build 3 | 4 | # Define the directory we should work in 5 | WORKDIR /app 6 | 7 | # Download the necessary go modules 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | 11 | # Build the application 12 | ARG PASTY_VERSION=unset-debug 13 | COPY . . 14 | RUN go build \ 15 | -o pasty \ 16 | -ldflags "\ 17 | -X github.com/lus/pasty/internal/static.Version=$PASTY_VERSION" \ 18 | ./cmd/pasty/main.go 19 | 20 | # Run the application in an empty alpine environment 21 | FROM alpine:latest 22 | WORKDIR /root 23 | COPY --from=build /app/pasty . 24 | COPY web ./web/ 25 | EXPOSE 8080 26 | CMD ["./pasty"] -------------------------------------------------------------------------------- /internal/web/controllers/v1/paste_adapter.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "github.com/lus/pasty/internal/shared" 4 | 5 | type legacyPaste struct { 6 | ID string `json:"id"` 7 | Content string `json:"content"` 8 | DeletionToken string `json:"deletionToken,omitempty"` 9 | Created int64 `json:"created"` 10 | } 11 | 12 | func legacyFromModern(paste *shared.Paste) *legacyPaste { 13 | deletionToken := paste.ModificationToken 14 | if deletionToken == "" { 15 | deletionToken = paste.DeletionToken 16 | } 17 | 18 | return &legacyPaste{ 19 | ID: paste.ID, 20 | Content: paste.Content, 21 | DeletionToken: deletionToken, 22 | Created: paste.Created, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/assets/libs/highlightjs/solarized-dark.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496}.hljs-comment,.hljs-quote{color:#586e75}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#859900}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#2aa198}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#268bd2}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#b58900}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#cb4b16}.hljs-built_in,.hljs-deletion{color:#dc322f}.hljs-formula{background:#073642}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} -------------------------------------------------------------------------------- /web/assets/js/modules/notifications.js: -------------------------------------------------------------------------------- 1 | import * as Animation from "./animation.js"; 2 | 3 | const ELEMENT = document.getElementById("notifications"); 4 | 5 | // Shows a success notification 6 | export function success(message) { 7 | create("success", message, 3000); 8 | } 9 | 10 | // Shows an error notification 11 | export function error(message) { 12 | create("error", message, 3000); 13 | } 14 | 15 | // Creates a new custom notification 16 | function create(type, message, duration) { 17 | const node = document.createElement("div"); 18 | node.classList.add(type); 19 | Animation.animate(node, "animate__fadeInUp", "0.2s"); 20 | node.innerHTML = message; 21 | 22 | ELEMENT.childNodes.forEach(child => Animation.animate(child, "animate__slideInUp", "0.2s")); 23 | ELEMENT.appendChild(node); 24 | setTimeout(() => Animation.animate(node, "animate__fadeOutUp", "0.2s", () => ELEMENT.removeChild(node)), duration); 25 | } -------------------------------------------------------------------------------- /web/assets/js/modules/duration.js: -------------------------------------------------------------------------------- 1 | export function format(milliseconds) { 2 | if (milliseconds < 0) { 3 | return "forever"; 4 | } 5 | 6 | let parts = new Array(); 7 | 8 | let days = Math.floor(milliseconds / 86400000); 9 | if (days > 0) { 10 | parts.push(`${days} ${days > 1 ? "days" : "day"}`); 11 | milliseconds -= days * 86400000; 12 | } 13 | 14 | let hours = Math.floor(milliseconds / 3600000); 15 | if (hours > 0) { 16 | parts.push(`${hours} ${hours > 1 ? "hours" : "hour"}`); 17 | milliseconds -= hours * 3600000; 18 | } 19 | 20 | let minutes = Math.floor(milliseconds / 60000); 21 | if (minutes > 0) { 22 | parts.push(`${minutes} ${minutes > 1 ? "minutes" : "minute"}`); 23 | milliseconds -= minutes * 60000; 24 | } 25 | 26 | let seconds = Math.ceil(milliseconds / 1000); 27 | if (seconds > 0) { 28 | parts.push(`${seconds} ${seconds > 1 ? "seconds" : "second"}`); 29 | } 30 | 31 | return parts.join(", "); 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lukas SP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/lus/pasty/internal/static" 10 | ) 11 | 12 | // Load loads an optional .env file 13 | func Load() { 14 | godotenv.Load() 15 | } 16 | 17 | // MustString returns the content of the environment variable with the given key or the given fallback 18 | func MustString(key, fallback string) string { 19 | value, found := os.LookupEnv(static.EnvironmentVariablePrefix + key) 20 | if !found { 21 | return fallback 22 | } 23 | return value 24 | } 25 | 26 | // MustBool uses MustString and parses it into a boolean 27 | func MustBool(key string, fallback bool) bool { 28 | parsed, _ := strconv.ParseBool(MustString(key, strconv.FormatBool(fallback))) 29 | return parsed 30 | } 31 | 32 | // MustInt uses MustString and parses it into an integer 33 | func MustInt(key string, fallback int) int { 34 | parsed, _ := strconv.Atoi(MustString(key, strconv.Itoa(fallback))) 35 | return parsed 36 | } 37 | 38 | // MustDuration uses MustString and parses it into a duration 39 | func MustDuration(key string, fallback time.Duration) time.Duration { 40 | parsed, _ := time.ParseDuration(MustString(key, fallback.String())) 41 | return parsed 42 | } 43 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # credits 2 | 3 | pasty uses several awesome open source libraries to provide the functionality it has. Here is a complete list of them. 4 | 5 | ## Backend (Golang) 6 | 7 | * [alexedwards/argon2id](https://github.com/alexedwards/argon2id) (deletion token hashing) [**MIT**] 8 | * [fasthttp/router](https://github.com/fasthttp/router) (endpoint routing) [**BSD-3**] 9 | * [golang-migrate/migrate](https://github.com/golang-migrate/migrate) (SQL migrations) [**MIT**] 10 | * [jackc/pgx](https://github.com/jackc/pgx) (PostgreSQL driver) [**MIT**] 11 | * [johejo/golang-migrate-extra](github.com/johejo/golang-migrate-extra) (io/fs driver for golang-migrate) [**MIT**] 12 | * [joho/godotenv](https://github.com/joho/godotenv) (.env file loading) [**MIT**] 13 | * [minio/minio-go](https://github.com/minio/minio-go) (MinIO client) [**Apache-2.0**] 14 | * [ulule/limiter](https://github.com/ulule/limiter) (rate limiting) [**MIT**] 15 | * [valyala/fasthttp](https://github.com/valyala/fasthttp) (HTTP server) [**MIT**] 16 | * [mongodb/mongo-go-driver](https://github.com/mongodb/mongo-go-driver) (MongoDB driver) [**Apache-2.0**] 17 | 18 | ## Frontend 19 | 20 | * [highlightjs/highlight.js](https://github.com/highlightjs/highlight.js) (code highlighting) [**BSD-3**] 21 | * [tabler/tabler-icons](https://github.com/tabler/tabler-icons) (icons) [**MIT**] -------------------------------------------------------------------------------- /cmd/pasty/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/lus/pasty/internal/config" 8 | "github.com/lus/pasty/internal/storage" 9 | "github.com/lus/pasty/internal/web" 10 | ) 11 | 12 | func main() { 13 | // Load the configuration 14 | log.Println("Loading the application configuration...") 15 | config.Load() 16 | 17 | // Load the configured storage driver 18 | log.Println("Loading the configured storage driver...") 19 | err := storage.Load() 20 | if err != nil { 21 | panic(err) 22 | } 23 | defer func() { 24 | log.Println("Terminating the storage driver...") 25 | err := storage.Current.Terminate() 26 | if err != nil { 27 | log.Fatalln(err) 28 | } 29 | }() 30 | 31 | // Schedule the AutoDelete task 32 | if config.Current.AutoDelete.Enabled { 33 | log.Println("Scheduling the AutoDelete task...") 34 | go func() { 35 | for { 36 | // Run the cleanup sequence 37 | deleted, err := storage.Current.Cleanup() 38 | if err != nil { 39 | log.Fatalln(err) 40 | } 41 | log.Printf("AutoDelete: Deleted %d expired pastes", deleted) 42 | 43 | // Wait until the process should repeat 44 | time.Sleep(config.Current.AutoDelete.TaskInterval) 45 | } 46 | }() 47 | } 48 | 49 | // Serve the web resources 50 | log.Println("Serving the web resources...") 51 | panic(web.Serve()) 52 | } 53 | -------------------------------------------------------------------------------- /internal/storage/driver.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lus/pasty/internal/config" 7 | "github.com/lus/pasty/internal/shared" 8 | "github.com/lus/pasty/internal/storage/file" 9 | "github.com/lus/pasty/internal/storage/mongodb" 10 | "github.com/lus/pasty/internal/storage/postgres" 11 | "github.com/lus/pasty/internal/storage/s3" 12 | ) 13 | 14 | // Current holds the current storage driver 15 | var Current Driver 16 | 17 | // Driver represents a storage driver 18 | type Driver interface { 19 | Initialize() error 20 | Terminate() error 21 | ListIDs() ([]string, error) 22 | Get(id string) (*shared.Paste, error) 23 | Save(paste *shared.Paste) error 24 | Delete(id string) error 25 | Cleanup() (int, error) 26 | } 27 | 28 | // Load loads the current storage driver 29 | func Load() error { 30 | // Define the driver to use 31 | driver, err := GetDriver(config.Current.StorageType) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Initialize the driver 37 | err = driver.Initialize() 38 | if err != nil { 39 | return err 40 | } 41 | Current = driver 42 | return nil 43 | } 44 | 45 | // GetDriver returns the driver with the given type if it exists 46 | func GetDriver(storageType shared.StorageType) (Driver, error) { 47 | switch storageType { 48 | case shared.StorageTypeFile: 49 | return new(file.FileDriver), nil 50 | case shared.StorageTypePostgres: 51 | return new(postgres.PostgresDriver), nil 52 | case shared.StorageTypeMongoDB: 53 | return new(mongodb.MongoDBDriver), nil 54 | case shared.StorageTypeS3: 55 | return new(s3.S3Driver), nil 56 | default: 57 | return nil, fmt.Errorf("invalid storage type '%s'", storageType) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/assets/js/modules/api.js: -------------------------------------------------------------------------------- 1 | const API_BASE_URL = location.protocol + "//" + location.host + "/api/v2"; 2 | 3 | export async function getAPIInformation() { 4 | return fetch(API_BASE_URL + "/info"); 5 | } 6 | 7 | export async function getPaste(pasteID) { 8 | return fetch(API_BASE_URL + "/pastes/" + pasteID); 9 | } 10 | 11 | export async function createPaste(content, metadata) { 12 | return fetch(API_BASE_URL + "/pastes", { 13 | method: "POST", 14 | headers: { 15 | "Content-Type": "application/json" 16 | }, 17 | body: JSON.stringify({ 18 | content, 19 | metadata 20 | }) 21 | }); 22 | } 23 | 24 | export async function editPaste(pasteID, modificationToken, content, metadata) { 25 | return fetch(API_BASE_URL + "/pastes/" + pasteID, { 26 | method: "PATCH", 27 | headers: { 28 | "Content-Type": "application/json", 29 | "Authorization": "Bearer " + modificationToken, 30 | }, 31 | body: JSON.stringify({ 32 | content, 33 | metadata 34 | }) 35 | }); 36 | } 37 | 38 | export async function deletePaste(pasteID, modificationToken) { 39 | return fetch(API_BASE_URL + "/pastes/" + pasteID, { 40 | method: "DELETE", 41 | headers: { 42 | "Authorization": "Bearer " + modificationToken, 43 | } 44 | }); 45 | } 46 | 47 | export async function reportPaste(pasteID, reason) { 48 | return fetch(API_BASE_URL + "/pastes/" + pasteID + "/report", { 49 | method: "POST", 50 | headers: { 51 | "Content-Type": "application/json", 52 | }, 53 | body: JSON.stringify({ 54 | reason 55 | }) 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /internal/report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/lus/pasty/internal/config" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | // ReportRequest represents a report request sent to the report webhook 12 | type ReportRequest struct { 13 | Paste string `json:"paste"` 14 | Reason string `json:"reason"` 15 | } 16 | 17 | // ReportResponse represents a report response received from the report webhook 18 | type ReportResponse struct { 19 | Success bool `json:"success"` 20 | Message string `json:"message"` 21 | } 22 | 23 | // SendReport sends a report request to the report webhook 24 | func SendReport(reportRequest *ReportRequest) (*ReportResponse, error) { 25 | request := fasthttp.AcquireRequest() 26 | defer fasthttp.ReleaseRequest(request) 27 | 28 | response := fasthttp.AcquireResponse() 29 | defer fasthttp.ReleaseResponse(response) 30 | 31 | request.Header.SetMethod(fasthttp.MethodPost) 32 | request.SetRequestURI(config.Current.Reports.ReportWebhook) 33 | if config.Current.Reports.ReportWebhookToken != "" { 34 | request.Header.Set("Authorization", "Bearer "+config.Current.Reports.ReportWebhookToken) 35 | } 36 | 37 | data, err := json.Marshal(reportRequest) 38 | if err != nil { 39 | return nil, err 40 | } 41 | request.SetBody(data) 42 | 43 | if err := fasthttp.Do(request, response); err != nil { 44 | return nil, err 45 | } 46 | 47 | status := response.StatusCode() 48 | if status < 200 || status > 299 { 49 | return nil, fmt.Errorf("the report webhook responded with an unexpected error: %d (%s)", status, string(response.Body())) 50 | } 51 | 52 | reportResponse := new(ReportResponse) 53 | if err := json.Unmarshal(response.Body(), reportResponse); err != nil { 54 | return nil, err 55 | } 56 | return reportResponse, nil 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | paths-ignore: 9 | - '**.md' 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: checkout code 20 | uses: actions/checkout@v2 21 | - name: define branch name 22 | run: echo "BRANCH=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV 23 | - name: define commit hash 24 | run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 25 | - name: define staging tag 26 | if: env.BRANCH == 'develop' 27 | run: echo "TAG=staging" >> $GITHUB_ENV 28 | - name: define latest tag 29 | if: env.BRANCH == 'master' 30 | run: echo "TAG=latest" >> $GITHUB_ENV 31 | - name: set up qemu 32 | uses: docker/setup-qemu-action@v1 33 | - name: set up buildx 34 | id: docker_buildx 35 | uses: docker/setup-buildx-action@v1 36 | - name: log in to ghcr 37 | uses: docker/login-action@v1 38 | with: 39 | registry: ${{ env.REGISTRY }} 40 | username: ${{ secrets.SERVICE_USER }} # Defined in secrets for auth to registry 41 | password: ${{ secrets.GITHUB_TOKEN }} 42 | - name: build and push 43 | uses: docker/build-push-action@v2 44 | with: 45 | builder: ${{ steps.docker_buildx.outputs.name }} 46 | platforms: linux/amd64, linux/arm64 47 | push: true 48 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}, ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.COMMIT_HASH }} 49 | build-args: 50 | PASTY_VERSION=${{ env.BRANCH }}-${{ env.COMMIT_HASH }} 51 | -------------------------------------------------------------------------------- /internal/shared/paste.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/alexedwards/argon2id" 7 | ) 8 | 9 | // Paste represents a saved paste 10 | type Paste struct { 11 | ID string `json:"id" bson:"_id"` 12 | Content string `json:"content" bson:"content"` 13 | DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"` // Required for legacy paste storage support 14 | ModificationToken string `json:"modificationToken,omitempty" bson:"modificationToken"` 15 | Created int64 `json:"created" bson:"created"` 16 | Metadata map[string]interface{} `json:"metadata" bson:"metadata"` 17 | } 18 | 19 | // HashModificationToken hashes the current modification token of a paste 20 | func (paste *Paste) HashModificationToken() error { 21 | hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams) 22 | if err != nil { 23 | return err 24 | } 25 | paste.ModificationToken = hash 26 | return nil 27 | } 28 | 29 | // CheckModificationToken checks whether or not the given modification token is correct 30 | func (paste *Paste) CheckModificationToken(modificationToken string) bool { 31 | // The modification token may be stored in the deletion token field in old pastes 32 | usedToken := paste.ModificationToken 33 | if usedToken == "" { 34 | usedToken = paste.DeletionToken 35 | if usedToken != "" { 36 | 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.") 37 | } 38 | } 39 | 40 | match, err := argon2id.ComparePasswordAndHash(modificationToken, usedToken) 41 | return err == nil && match 42 | } 43 | -------------------------------------------------------------------------------- /cmd/transfer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/lus/pasty/internal/config" 8 | "github.com/lus/pasty/internal/shared" 9 | "github.com/lus/pasty/internal/storage" 10 | ) 11 | 12 | func main() { 13 | // Validate the command line arguments 14 | if len(os.Args) != 3 { 15 | panic("Invalid command line arguments") 16 | } 17 | 18 | // Load the configuration 19 | log.Println("Loading the application configuration...") 20 | config.Load() 21 | 22 | // Create and initialize the first (from) driver 23 | from, err := storage.GetDriver(shared.StorageType(os.Args[1])) 24 | if err != nil { 25 | panic(err) 26 | } 27 | err = from.Initialize() 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // Create and initialize the second (to) driver 33 | to, err := storage.GetDriver(shared.StorageType(os.Args[2])) 34 | if err != nil { 35 | panic(err) 36 | } 37 | err = to.Initialize() 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // Retrieve a list of IDs from the first (from) driver 43 | ids, err := from.ListIDs() 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // Transfer every paste to the second (to) driver 49 | for _, id := range ids { 50 | log.Println("Transferring ID " + id + "...") 51 | 52 | // Retrieve the paste 53 | paste, err := from.Get(id) 54 | if err != nil { 55 | log.Println("[ERR]", err.Error()) 56 | continue 57 | } 58 | 59 | // Move the content of the deletion token field to the modification field 60 | if paste.DeletionToken != "" { 61 | if paste.ModificationToken == "" { 62 | paste.ModificationToken = paste.DeletionToken 63 | } 64 | paste.DeletionToken = "" 65 | log.Println("[INFO] Paste " + id + " was a legacy one.") 66 | } 67 | 68 | // Initialize a new metadata map if the old one is null 69 | if paste.Metadata == nil { 70 | paste.Metadata = make(map[string]interface{}) 71 | } 72 | 73 | // Save the paste 74 | err = to.Save(paste) 75 | if err != nil { 76 | log.Println("[ERR]", err.Error()) 77 | continue 78 | } 79 | 80 | log.Println("Transferred ID " + id + ".") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/web/controllers/v1/hastebin_support.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/lus/pasty/internal/config" 8 | "github.com/lus/pasty/internal/shared" 9 | "github.com/lus/pasty/internal/storage" 10 | "github.com/lus/pasty/internal/utils" 11 | "github.com/valyala/fasthttp" 12 | ) 13 | 14 | // HastebinSupportHandler handles the legacy hastebin requests 15 | func HastebinSupportHandler(ctx *fasthttp.RequestCtx) { 16 | // Check content length before reading body into memory 17 | if config.Current.LengthCap > 0 && 18 | ctx.Request.Header.ContentLength() > config.Current.LengthCap { 19 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 20 | ctx.SetBodyString("request body length overflow") 21 | return 22 | } 23 | 24 | // Define the paste content 25 | var content string 26 | if string(ctx.Request.Header.ContentType()) == "multipart/form-data" { 27 | content = string(ctx.FormValue("data")) 28 | } else { 29 | content = string(ctx.PostBody()) 30 | } 31 | 32 | // Acquire the paste ID 33 | id, err := storage.AcquireID() 34 | if err != nil { 35 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 36 | ctx.SetBodyString(err.Error()) 37 | return 38 | } 39 | 40 | // Create the paste object 41 | paste := &shared.Paste{ 42 | ID: id, 43 | Content: content, 44 | Created: time.Now().Unix(), 45 | } 46 | 47 | // Set a modification token 48 | if config.Current.ModificationTokens { 49 | paste.ModificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, config.Current.ModificationTokenLength) 50 | 51 | err = paste.HashModificationToken() 52 | if err != nil { 53 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 54 | ctx.SetBodyString(err.Error()) 55 | return 56 | } 57 | } 58 | 59 | // Save the paste 60 | err = storage.Current.Save(paste) 61 | if err != nil { 62 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 63 | ctx.SetBodyString(err.Error()) 64 | return 65 | } 66 | 67 | // Respond with the paste key 68 | jsonData, _ := json.Marshal(map[string]string{ 69 | "key": paste.ID, 70 | }) 71 | ctx.SetBody(jsonData) 72 | } 73 | -------------------------------------------------------------------------------- /web/assets/js/modules/encryption.js: -------------------------------------------------------------------------------- 1 | // Encrypts a piece of text using AES-CBC and returns the HEX-encoded key, initialization vector and encrypted text 2 | export async function encrypt(encryptionData, text) { 3 | const key = encryptionData.key; 4 | const iv = encryptionData.iv; 5 | 6 | const textBytes = aesjs.padding.pkcs7.pad(aesjs.utils.utf8.toBytes(text)); 7 | 8 | const aes = new aesjs.ModeOfOperation.cbc(key, iv); 9 | const encrypted = aes.encrypt(textBytes); 10 | 11 | return { 12 | key: aesjs.utils.hex.fromBytes(key), 13 | iv: aesjs.utils.hex.fromBytes(iv), 14 | result: aesjs.utils.hex.fromBytes(encrypted) 15 | }; 16 | } 17 | 18 | // Decrypts an encrypted piece of AES-CBC encrypted text 19 | export async function decrypt(keyHex, ivHex, inputHex) { 20 | const key = aesjs.utils.hex.toBytes(keyHex); 21 | const iv = aesjs.utils.hex.toBytes(ivHex); 22 | const input = aesjs.utils.hex.toBytes(inputHex); 23 | 24 | const aes = new aesjs.ModeOfOperation.cbc(key, iv); 25 | const decrypted = aesjs.padding.pkcs7.strip(aes.decrypt(input)); 26 | 27 | return aesjs.utils.utf8.fromBytes(decrypted); 28 | } 29 | 30 | // Creates encryption data from hex key and IV 31 | export async function encryptionDataFromHex(keyHex, ivHex) { 32 | return { 33 | key: aesjs.utils.hex.toBytes(keyHex), 34 | iv: aesjs.utils.hex.toBytes(ivHex) 35 | }; 36 | } 37 | 38 | // Generates encryption data to pass into the encrypt function 39 | export async function generateEncryptionData() { 40 | return { 41 | key: await generateKey(), 42 | iv: generateIV() 43 | }; 44 | } 45 | 46 | // Generates a new 256-bit AES-CBC key 47 | async function generateKey() { 48 | const key = await crypto.subtle.generateKey({ 49 | name: "AES-CBC", 50 | length: 256 51 | }, true, ["encrypt", "decrypt"]); 52 | 53 | const extracted = await crypto.subtle.exportKey("raw", key); 54 | return new Uint8Array(extracted); 55 | } 56 | 57 | // Generates a new cryptographically secure 16-byte array which is used as the initialization vector (IV) for AES-CBC 58 | function generateIV() { 59 | return crypto.getRandomValues(new Uint8Array(16)); 60 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/jetbrains+all,go 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains+all,go 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | ### Go Patch ### 23 | /vendor/ 24 | /Godeps/ 25 | 26 | ### JetBrains+all ### 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 29 | 30 | # User-specific stuff 31 | .idea/**/workspace.xml 32 | .idea/**/tasks.xml 33 | .idea/**/usage.statistics.xml 34 | .idea/**/dictionaries 35 | .idea/**/shelf 36 | 37 | # Generated files 38 | .idea/**/contentModel.xml 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | # .idea/artifacts 58 | # .idea/compiler.xml 59 | # .idea/jarRepositories.xml 60 | # .idea/modules.xml 61 | # .idea/*.iml 62 | # .idea/modules 63 | # *.iml 64 | # *.ipr 65 | 66 | # CMake 67 | cmake-build-*/ 68 | 69 | # Mongo Explorer plugin 70 | .idea/**/mongoSettings.xml 71 | 72 | # File-based project format 73 | *.iws 74 | 75 | # IntelliJ 76 | out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Cursive Clojure plugin 85 | .idea/replstate.xml 86 | 87 | # Crashlytics plugin (for Android Studio and IntelliJ) 88 | com_crashlytics_export_strings.xml 89 | crashlytics.properties 90 | crashlytics-build.properties 91 | fabric.properties 92 | 93 | # Editor-based Rest Client 94 | .idea/httpRequests 95 | 96 | # Android studio 3.1+ serialized cache file 97 | .idea/caches/build_file_checksums.ser 98 | 99 | ### JetBrains+all Patch ### 100 | # Ignores the whole .idea folder and all .iml files 101 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 102 | 103 | .idea/ 104 | 105 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 106 | 107 | *.iml 108 | modules.xml 109 | .idea/misc.xml 110 | *.ipr 111 | 112 | # Sonarlint plugin 113 | .idea/sonarlint 114 | 115 | # End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go 116 | 117 | web/*.gz 118 | data/ 119 | .env -------------------------------------------------------------------------------- /internal/storage/postgres/postgres_driver.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "errors" 7 | "time" 8 | 9 | "github.com/golang-migrate/migrate/v4" 10 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 11 | "github.com/jackc/pgx/v4" 12 | "github.com/jackc/pgx/v4/pgxpool" 13 | "github.com/johejo/golang-migrate-extra/source/iofs" 14 | "github.com/lus/pasty/internal/config" 15 | "github.com/lus/pasty/internal/shared" 16 | ) 17 | 18 | //go:embed migrations/*.sql 19 | var migrations embed.FS 20 | 21 | // PostgresDriver represents the Postgres storage driver 22 | type PostgresDriver struct { 23 | pool *pgxpool.Pool 24 | } 25 | 26 | // Initialize initializes the Postgres storage driver 27 | func (driver *PostgresDriver) Initialize() error { 28 | pool, err := pgxpool.Connect(context.Background(), config.Current.Postgres.DSN) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | source, err := iofs.New(migrations, "migrations") 34 | if err != nil { 35 | return err 36 | } 37 | 38 | migrator, err := migrate.NewWithSourceInstance("iofs", source, config.Current.Postgres.DSN) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if err := migrator.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { 44 | return err 45 | } 46 | 47 | driver.pool = pool 48 | return nil 49 | } 50 | 51 | // Terminate terminates the Postgres storage driver 52 | func (driver *PostgresDriver) Terminate() error { 53 | driver.pool.Close() 54 | return nil 55 | } 56 | 57 | // ListIDs returns a list of all existing paste IDs 58 | func (driver *PostgresDriver) ListIDs() ([]string, error) { 59 | query := "SELECT id FROM pastes" 60 | 61 | rows, err := driver.pool.Query(context.Background(), query) 62 | if err != nil { 63 | return []string{}, err 64 | } 65 | 66 | var ids []string 67 | for rows.Next() { 68 | var id string 69 | if err := rows.Scan(&id); err != nil { 70 | return []string{}, err 71 | } 72 | ids = append(ids, id) 73 | } 74 | 75 | return ids, nil 76 | } 77 | 78 | // Get loads a paste 79 | func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) { 80 | query := "SELECT * FROM pastes WHERE id = $1" 81 | 82 | row := driver.pool.QueryRow(context.Background(), query, id) 83 | 84 | paste := new(shared.Paste) 85 | if err := row.Scan(&paste.ID, &paste.Content, &paste.ModificationToken, &paste.Created, &paste.Metadata); err != nil { 86 | if errors.Is(err, pgx.ErrNoRows) { 87 | return nil, nil 88 | } 89 | return nil, err 90 | } 91 | return paste, nil 92 | } 93 | 94 | // Save saves a paste 95 | func (driver *PostgresDriver) Save(paste *shared.Paste) error { 96 | query := ` 97 | INSERT INTO pastes (id, content, "modificationToken", created, metadata) 98 | VALUES ($1, $2, $3, $4, $5) 99 | ON CONFLICT (id) DO UPDATE 100 | SET content = excluded.content, 101 | "modificationToken" = excluded."modificationToken", 102 | created = excluded.created, 103 | metadata = excluded.metadata 104 | ` 105 | 106 | _, err := driver.pool.Exec(context.Background(), query, paste.ID, paste.Content, paste.ModificationToken, paste.Created, paste.Metadata) 107 | return err 108 | } 109 | 110 | // Delete deletes a paste 111 | func (driver *PostgresDriver) Delete(id string) error { 112 | query := "DELETE FROM pastes WHERE id = $1" 113 | 114 | _, err := driver.pool.Exec(context.Background(), query, id) 115 | return err 116 | } 117 | 118 | // Cleanup cleans up the expired pastes 119 | func (driver *PostgresDriver) Cleanup() (int, error) { 120 | query := "DELETE FROM pastes WHERE created < $1" 121 | 122 | tag, err := driver.pool.Exec(context.Background(), query, time.Now().Add(-config.Current.AutoDelete.Lifetime).Unix()) 123 | if err != nil { 124 | return 0, err 125 | } 126 | return int(tag.RowsAffected()), nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/storage/file/file_driver.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/lus/pasty/internal/config" 13 | "github.com/lus/pasty/internal/shared" 14 | ) 15 | 16 | // FileDriver represents the file storage driver 17 | type FileDriver struct { 18 | filePath string 19 | } 20 | 21 | // Initialize initializes the file storage driver 22 | func (driver *FileDriver) Initialize() error { 23 | driver.filePath = config.Current.File.Path 24 | return os.MkdirAll(driver.filePath, os.ModePerm) 25 | } 26 | 27 | // Terminate terminates the file storage driver (does nothing, because the file storage driver does not need any termination) 28 | func (driver *FileDriver) Terminate() error { 29 | return nil 30 | } 31 | 32 | // ListIDs returns a list of all existing paste IDs 33 | func (driver *FileDriver) ListIDs() ([]string, error) { 34 | // Define the IDs slice 35 | var ids []string 36 | 37 | // Fill the IDs slice 38 | err := filepath.Walk(driver.filePath, func(path string, info os.FileInfo, err error) error { 39 | // Check if a walking error occurred 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // Only count JSON files 45 | if !strings.HasSuffix(info.Name(), ".json") { 46 | return nil 47 | } 48 | 49 | // Decode the file name 50 | decoded, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(info.Name(), ".json")) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // Append the ID to the IDs slice 56 | ids = append(ids, string(decoded)) 57 | return nil 58 | }) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // Return the IDs slice 64 | return ids, nil 65 | } 66 | 67 | // Get loads a paste 68 | func (driver *FileDriver) Get(id string) (*shared.Paste, error) { 69 | // Read the file 70 | id = base64.StdEncoding.EncodeToString([]byte(id)) 71 | data, err := ioutil.ReadFile(filepath.Join(driver.filePath, id+".json")) 72 | if err != nil { 73 | if os.IsNotExist(err) { 74 | return nil, nil 75 | } 76 | return nil, err 77 | } 78 | 79 | // Unmarshal the file into a paste 80 | paste := new(shared.Paste) 81 | err = json.Unmarshal(data, &paste) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return paste, nil 86 | } 87 | 88 | // Save saves a paste 89 | func (driver *FileDriver) Save(paste *shared.Paste) error { 90 | // Marshal the paste 91 | jsonBytes, err := json.Marshal(paste) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // Create the file to save the paste to 97 | id := base64.StdEncoding.EncodeToString([]byte(paste.ID)) 98 | file, err := os.Create(filepath.Join(driver.filePath, id+".json")) 99 | if err != nil { 100 | return err 101 | } 102 | defer file.Close() 103 | 104 | // Write the JSON data into the file 105 | _, err = file.Write(jsonBytes) 106 | return err 107 | } 108 | 109 | // Delete deletes a paste 110 | func (driver *FileDriver) Delete(id string) error { 111 | id = base64.StdEncoding.EncodeToString([]byte(id)) 112 | return os.Remove(filepath.Join(driver.filePath, id+".json")) 113 | } 114 | 115 | // Cleanup cleans up the expired pastes 116 | func (driver *FileDriver) Cleanup() (int, error) { 117 | // Retrieve all paste IDs 118 | ids, err := driver.ListIDs() 119 | if err != nil { 120 | return 0, err 121 | } 122 | 123 | // Define the amount of deleted items 124 | deleted := 0 125 | 126 | // Loop through all pastes 127 | for _, id := range ids { 128 | // Retrieve the paste object 129 | paste, err := driver.Get(id) 130 | if err != nil { 131 | return deleted, err 132 | } 133 | 134 | // Delete the paste if it is expired 135 | lifetime := config.Current.AutoDelete.Lifetime 136 | if paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { 137 | err = driver.Delete(id) 138 | if err != nil { 139 | return deleted, err 140 | } 141 | deleted++ 142 | } 143 | } 144 | return deleted, nil 145 | } 146 | -------------------------------------------------------------------------------- /internal/storage/s3/s3_driver.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io/ioutil" 8 | "strings" 9 | "time" 10 | 11 | "github.com/lus/pasty/internal/config" 12 | "github.com/lus/pasty/internal/shared" 13 | "github.com/minio/minio-go/v7" 14 | "github.com/minio/minio-go/v7/pkg/credentials" 15 | ) 16 | 17 | // S3Driver represents the AWS S3 storage driver 18 | type S3Driver struct { 19 | client *minio.Client 20 | bucket string 21 | } 22 | 23 | // Initialize initializes the AWS S3 storage driver 24 | func (driver *S3Driver) Initialize() error { 25 | client, err := minio.New(config.Current.S3.Endpoint, &minio.Options{ 26 | Creds: credentials.NewStaticV4(config.Current.S3.AccessKeyID, config.Current.S3.SecretAccessKey, config.Current.S3.SecretToken), 27 | Secure: config.Current.S3.Secure, 28 | Region: config.Current.S3.Region, 29 | }) 30 | if err != nil { 31 | return err 32 | } 33 | driver.client = client 34 | driver.bucket = config.Current.S3.Bucket 35 | return nil 36 | } 37 | 38 | // Terminate terminates the AWS S3 storage driver (does nothing, because the AWS S3 storage driver does not need any termination) 39 | func (driver *S3Driver) Terminate() error { 40 | return nil 41 | } 42 | 43 | // ListIDs returns a list of all existing paste IDs 44 | func (driver *S3Driver) ListIDs() ([]string, error) { 45 | // Define the IDs slice 46 | var ids []string 47 | 48 | // Fill the IDs slice 49 | channel := driver.client.ListObjects(context.Background(), driver.bucket, minio.ListObjectsOptions{}) 50 | for object := range channel { 51 | if object.Err != nil { 52 | return nil, object.Err 53 | } 54 | ids = append(ids, strings.TrimSuffix(object.Key, ".json")) 55 | } 56 | 57 | // Return the IDs slice 58 | return ids, nil 59 | } 60 | 61 | // Get loads a paste 62 | func (driver *S3Driver) Get(id string) (*shared.Paste, error) { 63 | // Read the object 64 | object, err := driver.client.GetObject(context.Background(), driver.bucket, id+".json", minio.GetObjectOptions{}) 65 | if err != nil { 66 | return nil, err 67 | } 68 | data, err := ioutil.ReadAll(object) 69 | if err != nil { 70 | if minio.ToErrorResponse(err).Code == "NoSuchKey" { 71 | return nil, nil 72 | } 73 | return nil, err 74 | } 75 | 76 | // Unmarshal the object into a paste 77 | paste := new(shared.Paste) 78 | err = json.Unmarshal(data, &paste) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return paste, nil 83 | } 84 | 85 | // Save saves a paste 86 | func (driver *S3Driver) Save(paste *shared.Paste) error { 87 | // Marshal the paste 88 | jsonBytes, err := json.Marshal(paste) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // Put the object 94 | reader := bytes.NewReader(jsonBytes) 95 | _, err = driver.client.PutObject(context.Background(), driver.bucket, paste.ID+".json", reader, reader.Size(), minio.PutObjectOptions{ 96 | ContentType: "application/json", 97 | }) 98 | return err 99 | } 100 | 101 | // Delete deletes a paste 102 | func (driver *S3Driver) Delete(id string) error { 103 | return driver.client.RemoveObject(context.Background(), driver.bucket, id+".json", minio.RemoveObjectOptions{}) 104 | } 105 | 106 | // Cleanup cleans up the expired pastes 107 | func (driver *S3Driver) Cleanup() (int, error) { 108 | // Retrieve all paste IDs 109 | ids, err := driver.ListIDs() 110 | if err != nil { 111 | return 0, err 112 | } 113 | 114 | // Define the amount of deleted items 115 | deleted := 0 116 | 117 | // Loop through all pastes 118 | for _, id := range ids { 119 | // Retrieve the paste object 120 | paste, err := driver.Get(id) 121 | if err != nil { 122 | return 0, err 123 | } 124 | 125 | // Delete the paste if it is expired 126 | lifetime := config.Current.AutoDelete.Lifetime 127 | if paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { 128 | err = driver.Delete(id) 129 | if err != nil { 130 | return 0, err 131 | } 132 | deleted++ 133 | } 134 | } 135 | return deleted, nil 136 | } 137 | -------------------------------------------------------------------------------- /internal/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "path/filepath" 6 | "strings" 7 | 8 | routing "github.com/fasthttp/router" 9 | "github.com/lus/pasty/internal/config" 10 | "github.com/lus/pasty/internal/static" 11 | "github.com/lus/pasty/internal/storage" 12 | v1 "github.com/lus/pasty/internal/web/controllers/v1" 13 | v2 "github.com/lus/pasty/internal/web/controllers/v2" 14 | "github.com/ulule/limiter/v3" 15 | limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp" 16 | "github.com/ulule/limiter/v3/drivers/store/memory" 17 | "github.com/valyala/fasthttp" 18 | ) 19 | 20 | // Serve serves the web resources 21 | func Serve() error { 22 | // Create the router 23 | router := routing.New() 24 | 25 | // Define the 404 handler 26 | router.NotFound = func(ctx *fasthttp.RequestCtx) { 27 | ctx.SetStatusCode(fasthttp.StatusNotFound) 28 | ctx.SetBodyString("not found") 29 | } 30 | 31 | // Route the frontend requests 32 | frontend := frontendHandler() 33 | raw := rawHandler() 34 | router.GET("/{path:*}", func(ctx *fasthttp.RequestCtx) { 35 | path := string(ctx.Path()) 36 | if !strings.HasPrefix(path, "/api") && (strings.Count(path, "/") == 1 || strings.HasPrefix(path, "/assets")) { 37 | if strings.HasPrefix(path, "/assets/js/") { 38 | ctx.SetContentType("text/javascript") 39 | } 40 | frontend(ctx) 41 | return 42 | } else if strings.HasSuffix(strings.TrimSuffix(path, "/"), "/raw") { 43 | raw(ctx) 44 | return 45 | } 46 | router.NotFound(ctx) 47 | }) 48 | 49 | // Set up the rate limiter 50 | rate, err := limiter.NewRateFromFormatted(config.Current.RateLimit) 51 | if err != nil { 52 | return err 53 | } 54 | rateLimiter := limiter.New(memory.NewStore(), rate) 55 | rateLimiterMiddleware := limitFasthttp.NewMiddleware(rateLimiter) 56 | 57 | // Route the API endpoints 58 | apiRoute := router.Group("/api") 59 | { 60 | v1Route := apiRoute.Group("/v1") 61 | { 62 | v1Route.GET("/info", func(ctx *fasthttp.RequestCtx) { 63 | jsonData, _ := json.Marshal(map[string]interface{}{ 64 | "version": static.Version, 65 | "deletionTokens": config.Current.ModificationTokens, 66 | }) 67 | ctx.SetBody(jsonData) 68 | }) 69 | v1.InitializePastesController(v1Route.Group("/pastes"), rateLimiterMiddleware) 70 | } 71 | 72 | v2Route := apiRoute.Group("/v2") 73 | { 74 | pasteLifetime := int64(-1) 75 | if config.Current.AutoDelete.Enabled { 76 | pasteLifetime = config.Current.AutoDelete.Lifetime.Milliseconds() 77 | } 78 | v2Route.GET("/info", func(ctx *fasthttp.RequestCtx) { 79 | jsonData, _ := json.Marshal(map[string]interface{}{ 80 | "version": static.Version, 81 | "modificationTokens": config.Current.ModificationTokens, 82 | "reports": config.Current.Reports.Reports, 83 | "pasteLifetime": pasteLifetime, 84 | }) 85 | ctx.SetBody(jsonData) 86 | }) 87 | v2.InitializePastesController(v2Route.Group("/pastes"), rateLimiterMiddleware) 88 | } 89 | } 90 | 91 | // Route the hastebin documents route if hastebin support is enabled 92 | if config.Current.HastebinSupport { 93 | router.POST("/documents", rateLimiterMiddleware.Handle(v1.HastebinSupportHandler)) 94 | } 95 | 96 | // Serve the web resources 97 | return (&fasthttp.Server{ 98 | Handler: func(ctx *fasthttp.RequestCtx) { 99 | // Add the CORS headers 100 | ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET,POST,DELETE,OPTIONS") 101 | ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") 102 | 103 | // Call the router handler 104 | router.Handler(ctx) 105 | }, 106 | Logger: new(nilLogger), 107 | }).ListenAndServe(config.Current.WebAddress) 108 | } 109 | 110 | // frontendHandler handles the frontend routing 111 | func frontendHandler() fasthttp.RequestHandler { 112 | // Create the file server 113 | fs := &fasthttp.FS{ 114 | Root: static.TempFrontendPath, 115 | IndexNames: []string{"index.html"}, 116 | CacheDuration: 0, 117 | } 118 | fs.PathNotFound = func(ctx *fasthttp.RequestCtx) { 119 | if strings.HasPrefix(string(ctx.Path()), "/assets") { 120 | ctx.SetStatusCode(fasthttp.StatusNotFound) 121 | ctx.SetBodyString("not found") 122 | return 123 | } 124 | ctx.SendFile(filepath.Join(fs.Root, "index.html")) 125 | } 126 | return fs.NewRequestHandler() 127 | } 128 | 129 | func rawHandler() fasthttp.RequestHandler { 130 | return func(ctx *fasthttp.RequestCtx) { 131 | path := string(ctx.Path()) 132 | pathSanitized := strings.TrimPrefix(strings.TrimSuffix(path, "/"), "/") 133 | pasteID := strings.TrimSuffix(pathSanitized, "/raw") 134 | 135 | paste, err := storage.Current.Get(pasteID) 136 | if err != nil { 137 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 138 | ctx.SetBodyString(err.Error()) 139 | return 140 | } 141 | 142 | if paste == nil { 143 | ctx.SetStatusCode(fasthttp.StatusNotFound) 144 | ctx.SetBodyString("paste not found") 145 | return 146 | } 147 | 148 | ctx.SetBodyString(paste.Content) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/lus/pasty/internal/env" 8 | "github.com/lus/pasty/internal/shared" 9 | ) 10 | 11 | // Config represents the general application configuration structure 12 | type Config struct { 13 | WebAddress string 14 | StorageType shared.StorageType 15 | HastebinSupport bool 16 | IDLength int 17 | IDCharacters string 18 | ModificationTokens bool 19 | ModificationTokenMaster string 20 | ModificationTokenLength int 21 | ModificationTokenCharacters string 22 | RateLimit string 23 | LengthCap int 24 | AutoDelete *AutoDeleteConfig 25 | Reports *ReportConfig 26 | File *FileConfig 27 | Postgres *PostgresConfig 28 | MongoDB *MongoDBConfig 29 | S3 *S3Config 30 | } 31 | 32 | // AutoDeleteConfig represents the configuration specific for the AutoDelete behaviour 33 | type AutoDeleteConfig struct { 34 | Enabled bool 35 | Lifetime time.Duration 36 | TaskInterval time.Duration 37 | } 38 | 39 | // FileConfig represents the configuration specific for the file storage driver 40 | type FileConfig struct { 41 | Path string 42 | } 43 | 44 | // PostgresConfig represents the configuration specific for the Postgres storage driver 45 | type PostgresConfig struct { 46 | DSN string 47 | } 48 | 49 | // MongoDBConfig represents the configuration specific for the MongoDB storage driver 50 | type MongoDBConfig struct { 51 | DSN string 52 | Database string 53 | Collection string 54 | } 55 | 56 | // S3Config represents the configuration specific for the S3 storage driver 57 | type S3Config struct { 58 | Endpoint string 59 | AccessKeyID string 60 | SecretAccessKey string 61 | SecretToken string 62 | Secure bool 63 | Region string 64 | Bucket string 65 | } 66 | 67 | // ReportConfig represents the configuration specific for the report system 68 | type ReportConfig struct { 69 | Reports bool 70 | ReportWebhook string 71 | ReportWebhookToken string 72 | } 73 | 74 | // Current holds the currently loaded config 75 | var Current *Config 76 | 77 | // Load loads the current config from environment variables and an optional .env file 78 | func Load() { 79 | env.Load() 80 | 81 | Current = &Config{ 82 | WebAddress: env.MustString("WEB_ADDRESS", ":8080"), 83 | StorageType: shared.StorageType(strings.ToLower(env.MustString("STORAGE_TYPE", "file"))), 84 | HastebinSupport: env.MustBool("HASTEBIN_SUPPORT", false), 85 | IDLength: env.MustInt("ID_LENGTH", 6), 86 | IDCharacters: env.MustString("ID_CHARACTERS", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 87 | ModificationTokens: env.MustBool("MODIFICATION_TOKENS", env.MustBool("DELETION_TOKENS", true)), // --- 88 | ModificationTokenMaster: env.MustString("MODIFICATION_TOKEN_MASTER", env.MustString("DELETION_TOKEN_MASTER", "")), // - We don't want to destroy peoples old configuration 89 | ModificationTokenLength: env.MustInt("MODIFICATION_TOKEN_LENGTH", env.MustInt("DELETION_TOKEN_LENGTH", 12)), // --- 90 | ModificationTokenCharacters: env.MustString("MODIFICATION_TOKEN_CHARACTERS", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 91 | RateLimit: env.MustString("RATE_LIMIT", "30-M"), 92 | LengthCap: env.MustInt("LENGTH_CAP", 50_000), 93 | AutoDelete: &AutoDeleteConfig{ 94 | Enabled: env.MustBool("AUTODELETE", false), 95 | Lifetime: env.MustDuration("AUTODELETE_LIFETIME", 720*time.Hour), 96 | TaskInterval: env.MustDuration("AUTODELETE_TASK_INTERVAL", 5*time.Minute), 97 | }, 98 | Reports: &ReportConfig{ 99 | Reports: env.MustBool("REPORTS", false), 100 | ReportWebhook: env.MustString("REPORT_WEBHOOK", ""), 101 | ReportWebhookToken: env.MustString("REPORT_WEBHOOK_TOKEN", ""), 102 | }, 103 | File: &FileConfig{ 104 | Path: env.MustString("STORAGE_FILE_PATH", "./data"), 105 | }, 106 | Postgres: &PostgresConfig{ 107 | DSN: env.MustString("STORAGE_POSTGRES_DSN", "postgres://pasty:pasty@localhost/pasty"), 108 | }, 109 | MongoDB: &MongoDBConfig{ 110 | DSN: env.MustString("STORAGE_MONGODB_CONNECTION_STRING", "mongodb://pasty:pasty@localhost/pasty"), 111 | Database: env.MustString("STORAGE_MONGODB_DATABASE", "pasty"), 112 | Collection: env.MustString("STORAGE_MONGODB_COLLECTION", "pastes"), 113 | }, 114 | S3: &S3Config{ 115 | Endpoint: env.MustString("STORAGE_S3_ENDPOINT", ""), 116 | AccessKeyID: env.MustString("STORAGE_S3_ACCESS_KEY_ID", ""), 117 | SecretAccessKey: env.MustString("STORAGE_S3_SECRET_ACCESS_KEY", ""), 118 | SecretToken: env.MustString("STORAGE_S3_SECRET_TOKEN", ""), 119 | Secure: env.MustBool("STORAGE_S3_SECURE", true), 120 | Region: env.MustString("STORAGE_S3_REGION", ""), 121 | Bucket: env.MustString("STORAGE_S3_BUCKET", "pasty"), 122 | }, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/storage/mongodb/mongodb_driver.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/lus/pasty/internal/config" 8 | "github.com/lus/pasty/internal/shared" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | "go.mongodb.org/mongo-driver/mongo/readpref" 13 | ) 14 | 15 | // MongoDBDriver represents the MongoDB storage driver 16 | type MongoDBDriver struct { 17 | client *mongo.Client 18 | database string 19 | collection string 20 | } 21 | 22 | // Initialize initializes the MongoDB storage driver 23 | func (driver *MongoDBDriver) Initialize() error { 24 | // Define the context for the following database operation 25 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 26 | defer cancel() 27 | 28 | // Connect to the MongoDB host 29 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(config.Current.MongoDB.DSN)) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Ping the MongoDB host 35 | err = client.Ping(ctx, readpref.Primary()) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Set the driver attributes 41 | driver.client = client 42 | driver.database = config.Current.MongoDB.Database 43 | driver.collection = config.Current.MongoDB.Collection 44 | return nil 45 | } 46 | 47 | // Terminate terminates the MongoDB storage driver 48 | func (driver *MongoDBDriver) Terminate() error { 49 | return driver.client.Disconnect(context.TODO()) 50 | } 51 | 52 | // ListIDs returns a list of all existing paste IDs 53 | func (driver *MongoDBDriver) ListIDs() ([]string, error) { 54 | // Define the collection to use for this database operation 55 | collection := driver.client.Database(driver.database).Collection(driver.collection) 56 | 57 | // Define the context for the following database operation 58 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 59 | defer cancel() 60 | 61 | // Retrieve all paste documents 62 | result, err := collection.Find(ctx, bson.M{}) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // Decode all paste documents 68 | var pasteSlice []shared.Paste 69 | err = result.All(ctx, &pasteSlice) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | // Read and return the IDs of all paste objects 75 | var ids []string 76 | for _, paste := range pasteSlice { 77 | ids = append(ids, paste.ID) 78 | } 79 | return ids, nil 80 | } 81 | 82 | // Get loads a paste 83 | func (driver *MongoDBDriver) Get(id string) (*shared.Paste, error) { 84 | // Define the collection to use for this database operation 85 | collection := driver.client.Database(driver.database).Collection(driver.collection) 86 | 87 | // Define the context for the following database operation 88 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 89 | defer cancel() 90 | 91 | // Try to retrieve the corresponding paste document 92 | filter := bson.M{"_id": id} 93 | result := collection.FindOne(ctx, filter) 94 | err := result.Err() 95 | if err != nil { 96 | if err == mongo.ErrNoDocuments { 97 | return nil, nil 98 | } 99 | return nil, err 100 | } 101 | 102 | // Return the retrieved paste object 103 | paste := new(shared.Paste) 104 | err = result.Decode(paste) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return paste, nil 109 | } 110 | 111 | // Save saves a paste 112 | func (driver *MongoDBDriver) Save(paste *shared.Paste) error { 113 | // Define the collection to use for this database operation 114 | collection := driver.client.Database(driver.database).Collection(driver.collection) 115 | 116 | // Define the context for the following database operation 117 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 118 | defer cancel() 119 | 120 | // Upsert the paste object 121 | filter := bson.M{"_id": paste.ID} 122 | _, err := collection.UpdateOne(ctx, filter, bson.M{"$set": paste}, options.Update().SetUpsert(true)) 123 | return err 124 | } 125 | 126 | // Delete deletes a paste 127 | func (driver *MongoDBDriver) Delete(id string) error { 128 | // Define the collection to use for this database operation 129 | collection := driver.client.Database(driver.database).Collection(driver.collection) 130 | 131 | // Define the context for the following database operation 132 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 133 | defer cancel() 134 | 135 | // Delete the document 136 | filter := bson.M{"_id": id} 137 | _, err := collection.DeleteOne(ctx, filter) 138 | return err 139 | } 140 | 141 | // Cleanup cleans up the expired pastes 142 | func (driver *MongoDBDriver) Cleanup() (int, error) { 143 | // Retrieve all paste IDs 144 | ids, err := driver.ListIDs() 145 | if err != nil { 146 | return 0, err 147 | } 148 | 149 | // Define the amount of deleted items 150 | deleted := 0 151 | 152 | // Loop through all pastes 153 | for _, id := range ids { 154 | // Retrieve the paste object 155 | paste, err := driver.Get(id) 156 | if err != nil { 157 | return 0, err 158 | } 159 | 160 | // Delete the paste if it is expired 161 | lifetime := config.Current.AutoDelete.Lifetime 162 | if paste.Created+int64(lifetime.Seconds()) < time.Now().Unix() { 163 | err = driver.Delete(id) 164 | if err != nil { 165 | return 0, err 166 | } 167 | deleted++ 168 | } 169 | } 170 | return deleted, nil 171 | } 172 | -------------------------------------------------------------------------------- /internal/web/controllers/v1/pastes.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/fasthttp/router" 8 | "github.com/lus/pasty/internal/config" 9 | "github.com/lus/pasty/internal/shared" 10 | "github.com/lus/pasty/internal/storage" 11 | "github.com/lus/pasty/internal/utils" 12 | limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp" 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | // InitializePastesController initializes the '/v1/pastes/*' controller 17 | func InitializePastesController(group *router.Group, rateLimiterMiddleware *limitFasthttp.Middleware) { 18 | group.GET("/{id}", rateLimiterMiddleware.Handle(v1GetPaste)) 19 | group.POST("", rateLimiterMiddleware.Handle(v1PostPaste)) 20 | group.DELETE("/{id}", rateLimiterMiddleware.Handle(v1DeletePaste)) 21 | } 22 | 23 | // v1GetPaste handles the 'GET /v1/pastes/{id}' endpoint 24 | func v1GetPaste(ctx *fasthttp.RequestCtx) { 25 | // Read the ID 26 | id := ctx.UserValue("id").(string) 27 | 28 | // Retrieve the paste 29 | paste, err := storage.Current.Get(id) 30 | if err != nil { 31 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 32 | ctx.SetBodyString(err.Error()) 33 | return 34 | } 35 | if paste == nil { 36 | ctx.SetStatusCode(fasthttp.StatusNotFound) 37 | ctx.SetBodyString("paste not found") 38 | return 39 | } 40 | legacyPaste := legacyFromModern(paste) 41 | legacyPaste.DeletionToken = "" 42 | 43 | // Respond with the paste 44 | jsonData, err := json.Marshal(legacyPaste) 45 | if err != nil { 46 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 47 | ctx.SetBodyString(err.Error()) 48 | return 49 | } 50 | ctx.SetBody(jsonData) 51 | } 52 | 53 | // v1PostPaste handles the 'POST /v1/pastes' endpoint 54 | func v1PostPaste(ctx *fasthttp.RequestCtx) { 55 | // Check content length before reading body into memory 56 | if config.Current.LengthCap > 0 && 57 | ctx.Request.Header.ContentLength() > config.Current.LengthCap { 58 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 59 | ctx.SetBodyString("request body length overflow") 60 | return 61 | } 62 | 63 | // Unmarshal the body 64 | values := make(map[string]string) 65 | err := json.Unmarshal(ctx.PostBody(), &values) 66 | if err != nil { 67 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 68 | ctx.SetBodyString("invalid request body") 69 | return 70 | } 71 | 72 | // Validate the content of the paste 73 | if values["content"] == "" { 74 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 75 | ctx.SetBodyString("missing 'content' field") 76 | return 77 | } 78 | 79 | // Acquire the paste ID 80 | id, err := storage.AcquireID() 81 | if err != nil { 82 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 83 | ctx.SetBodyString(err.Error()) 84 | return 85 | } 86 | 87 | // Create the paste object 88 | paste := &shared.Paste{ 89 | ID: id, 90 | Content: values["content"], 91 | Created: time.Now().Unix(), 92 | } 93 | 94 | // Set a modification token 95 | modificationToken := "" 96 | if config.Current.ModificationTokens { 97 | modificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, config.Current.ModificationTokenLength) 98 | paste.ModificationToken = modificationToken 99 | 100 | err = paste.HashModificationToken() 101 | if err != nil { 102 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 103 | ctx.SetBodyString(err.Error()) 104 | return 105 | } 106 | } 107 | 108 | // Save the paste 109 | err = storage.Current.Save(paste) 110 | if err != nil { 111 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 112 | ctx.SetBodyString(err.Error()) 113 | return 114 | } 115 | 116 | // Respond with the paste 117 | pasteCopy := legacyFromModern(paste) 118 | pasteCopy.DeletionToken = modificationToken 119 | jsonData, err := json.Marshal(pasteCopy) 120 | if err != nil { 121 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 122 | ctx.SetBodyString(err.Error()) 123 | return 124 | } 125 | ctx.SetBody(jsonData) 126 | } 127 | 128 | // v1DeletePaste handles the 'DELETE /v1/pastes/{id}' 129 | func v1DeletePaste(ctx *fasthttp.RequestCtx) { 130 | // Read the ID 131 | id := ctx.UserValue("id").(string) 132 | 133 | // Unmarshal the body 134 | values := make(map[string]string) 135 | err := json.Unmarshal(ctx.PostBody(), &values) 136 | if err != nil { 137 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 138 | ctx.SetBodyString("invalid request body") 139 | return 140 | } 141 | 142 | // Validate the modification token of the paste 143 | modificationToken := values["deletionToken"] 144 | if modificationToken == "" { 145 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 146 | ctx.SetBodyString("missing 'deletionToken' field") 147 | return 148 | } 149 | 150 | // Retrieve the paste 151 | paste, err := storage.Current.Get(id) 152 | if err != nil { 153 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 154 | ctx.SetBodyString(err.Error()) 155 | return 156 | } 157 | if paste == nil { 158 | ctx.SetStatusCode(fasthttp.StatusNotFound) 159 | ctx.SetBodyString("paste not found") 160 | return 161 | } 162 | 163 | // Check if the modification token is correct 164 | if (config.Current.ModificationTokenMaster == "" || modificationToken != config.Current.ModificationTokenMaster) && !paste.CheckModificationToken(modificationToken) { 165 | ctx.SetStatusCode(fasthttp.StatusForbidden) 166 | ctx.SetBodyString("invalid deletion token") 167 | return 168 | } 169 | 170 | // Delete the paste 171 | err = storage.Current.Delete(paste.ID) 172 | if err != nil { 173 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 174 | ctx.SetBodyString(err.Error()) 175 | return 176 | } 177 | 178 | // Respond with 'ok' 179 | ctx.SetBodyString("ok") 180 | } 181 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | > **IMPORTANT:** Even though the API is defined pretty well, it may encounter breaking changes while on the `develop` branch! 4 | 5 | The REST API provided by pasty is the most important entrypoint when it comes to interacting with it. Basically everything, including the pasty frontend, is built on top of it. 6 | To make things easier for other developers who decide to develop something in connection to pasty, everything important about it is documented here. 7 | 8 | ## Authentication/Authorization 9 | 10 | Not everyone should be able to view, edit or delete all pastes. However, admins should be. 11 | In order to achieve that, an effective auth flow is required. 12 | 13 | There are two ways of authenticating: 14 | 15 | ### 1.) Paste-pecific 16 | 17 | The `Authorization` header is set to `Bearer `, where `` is replaced with the corresponding paste-specific **modification token**. 18 | This authentication is only valid for the requested paste. 19 | 20 | ### 2.) Admin tokens 21 | 22 | The `Authorization` header is set to `Bearer `, where `` is replaced with the configured **administration token**. 23 | This authentication is valid for all endpoints, regardless of the requested paste. 24 | 25 | ### Notation 26 | 27 | In the folllowing, all endpoints that require an **admin token** are annotated with `[ADMIN]`. 28 | All endpoints that are accessible through the **admin and modification token** are annotated with `[PASTE_SPECIFIC]`. 29 | All endpoints that are accessible to everyone are annotated with `[UNSECURED]`. 30 | 31 | ## The paste entity 32 | 33 | The central paste entity has the following fields: 34 | 35 | * `id` (string) 36 | * `content` (string) 37 | * `modificationToken` (string) 38 | * The token used to authenticate with paste-specific secured endpoints; stored hashed and only returned on initial paste creation 39 | * `created` (int64; UNIX timestamp) 40 | * `metadata` (key-value store) 41 | * Different frontends may store simple key-value metadata pairs on pastes to enable specific functionality (for example clientside encryption) 42 | 43 | ### Encryption 44 | 45 | The frontend pasty ships with implements an encryption option. This en- and decrypts pastes clientside and appends the HEX-encoded en-/decryption key to the paste URL (after a `#` because the so called **hash** is not sent to the server). 46 | If a paste is encrypted using this feature, its `metadata` field contains a field like this: 47 | 48 | ```jsonc 49 | { 50 | // --- omitted other entity field 51 | "metadata": { 52 | "pf_encryption": { 53 | "alg": "AES-CBC", // The algorithm used to encrypt the paste (currently, only AES-CBC is used) 54 | "iv": "54baa80cd8d8328dc4630f9316130f49" // The HEX-encoded initialization vector of the AES-CBC encryption 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## Endpoints 61 | 62 | ### [UNSECURED] Retrieve application information 63 | 64 | ```http 65 | GET /api/v2/info 66 | ``` 67 | 68 | **Request:** 69 | none 70 | 71 | **Response:** 72 | ```jsonc 73 | { 74 | "modificationTokens": true, 75 | "reports": true, 76 | "pasteLifetime": -1, // The configured AutoDelete paste lifetime; -1 if AutoDelete is disabled 77 | "version": "dev" 78 | } 79 | ``` 80 | 81 | --- 82 | 83 | ### [UNSECURED] Retrieve a paste 84 | 85 | ```http 86 | GET /api/v2/pastes/{paste_id} 87 | ``` 88 | 89 | **Request:** 90 | none 91 | 92 | **Response:** 93 | ```json 94 | { 95 | "id": "paste_id", 96 | "content": "paste_content", 97 | "created": 0000000000, 98 | "metadata": {} 99 | } 100 | ``` 101 | 102 | --- 103 | 104 | ### [UNSECURED] Create a paste 105 | 106 | ```http 107 | POST /api/v2/pastes 108 | ``` 109 | 110 | **Request:** 111 | ```jsonc 112 | { 113 | "content": "paste_content", // Required 114 | "metadata": {} // Optional 115 | } 116 | ``` 117 | 118 | **Response:** 119 | ```json 120 | { 121 | "id": "paste_id", 122 | "content": "paste_content", 123 | "modificationToken": "raw_modification_token", 124 | "created": 0000000000, 125 | "metadata": {} 126 | } 127 | ``` 128 | 129 | --- 130 | 131 | ### [PASTE_SPECIFIC] Update a paste 132 | 133 | ```http 134 | PATCH /api/v2/pastes/{paste_id} 135 | ``` 136 | 137 | **Request:** 138 | ```jsonc 139 | { 140 | "content": "new_paste_content", // Optional 141 | "metadata": {} // Optional 142 | } 143 | ``` 144 | 145 | **Response:** 146 | ```json 147 | { 148 | "id": "paste_id", 149 | "content": "new_paste_content", 150 | "created": 0000000000, 151 | "metadata": {} 152 | } 153 | ``` 154 | 155 | **Notes:** 156 | * Changes in the `metadata` field only affect the corresponding field and don't override the whole key-value store (`{"metadata": {"foo": "bar"}}` will effectively add or replace the `foo` key but won't affect other keys). 157 | * To remove a key from the key-value store simply set it to `null`. 158 | 159 | --- 160 | 161 | ### [PASTE_SPECIFIC] Delete a paste 162 | 163 | ```http 164 | DELETE /api/v2/pastes/{paste_id} 165 | ``` 166 | 167 | **Request:** 168 | none 169 | 170 | **Response:** 171 | none 172 | 173 | --- 174 | 175 | ### [UNSECURED] Report a paste 176 | 177 | ```http 178 | POST /api/v2/pastes/{paste_id}/report 179 | ``` 180 | 181 | **Request:** 182 | ```json 183 | { 184 | "reason": "reason" 185 | } 186 | ``` 187 | 188 | **Response:** 189 | ```jsonc 190 | { 191 | "success": true, // Whether or not the report was received successfully (this is returned by the report webhook to allow custom errors) 192 | "message": "message" // An optional message to display to the reporting user 193 | } 194 | ``` 195 | 196 | **Notes:** 197 | * The endpoint is only available if the report system is enabled. Otherwise it will return a `404 Not Found` error. 198 | * The request that will reach the report webhook looks like this: 199 | ```json 200 | { 201 | "paste": "paste_id", 202 | "reason": "reason" 203 | } 204 | ``` 205 | * The response from this endpoint is the exact same that pasty expects from the webhook. -------------------------------------------------------------------------------- /web/assets/css/style.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,OAAO,CAAC,4EAAI;AAEZ,AAAA,IAAI,EAAE,IAAI,CAAC;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,4BAA4B;CAgB5C;;AArBD,AAOQ,IAPJ,AAMC,SAAS,CACN,WAAW,EAPb,IAAI,AAML,SAAS,CACN,WAAW,CAAC;EACR,OAAO,EAAE,IAAI;CAChB;;AATT,AAUQ,IAVJ,AAMC,SAAS,CAIN,UAAU,EAVZ,IAAI,AAML,SAAS,CAIN,UAAU,CAAC;EACP,MAAM,EAAE,CAAC;CACZ;;AAZT,AAaQ,IAbJ,AAMC,SAAS,CAON,QAAQ,EAbhB,IAAI,AAMC,SAAS,CAOI,QAAQ,EAbpB,IAAI,AAML,SAAS,CAON,QAAQ,EAbV,IAAI,AAML,SAAS,CAOI,QAAQ,CAAC;EACf,WAAW,EAAE,IAAI;EACjB,UAAU,EAAE,kBAAkB;CACjC;;AAhBT,AAiBQ,IAjBJ,AAMC,SAAS,CAWN,OAAO,EAjBT,IAAI,AAML,SAAS,CAWN,OAAO,CAAC;EACJ,SAAS,EAAE,KAAK;CACnB;;AAIT,AAAA,mBAAmB,CAAC;EAChB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;CACf;;AAED,AAAA,yBAAyB,CAAC;EACtB,UAAU,EAAE,OAAO;CACtB;;AAED,AAAA,yBAAyB,CAAC;EACtB,UAAU,EAAE,OAAO;EACnB,aAAa,EAAE,GAAG;CACrB;;AAED,AAAA,yBAAyB,AAAA,MAAM,CAAC;EAC5B,UAAU,EAAE,OAAO;CACtB;;AAED,AAAA,yBAAyB,AAAA,OAAO,CAAC;EAC7B,UAAU,EAAE,OAAO;CACtB;;AAED,AAAA,OAAO,CAAC;EACJ,OAAO,EAAE,IAAI;CAChB;;AAED,kBAAkB,CAAlB,OAAkB;EACd,EAAE;IACE,iBAAiB,EAAE,0BAA0B,CAAC,YAAY;IAClD,SAAS,EAAE,0BAA0B,CAAC,YAAY;;EAE9D,IAAI;IACA,iBAAiB,EAAE,0BAA0B,CAAC,cAAc;IACpD,SAAS,EAAE,0BAA0B,CAAC,cAAc;;;;AAGpE,UAAU,CAAV,OAAU;EACN,EAAE;IACE,iBAAiB,EAAE,0BAA0B,CAAC,YAAY;IAClD,SAAS,EAAE,0BAA0B,CAAC,YAAY;;EAE9D,IAAI;IACA,iBAAiB,EAAE,0BAA0B,CAAC,cAAc;IACpD,SAAS,EAAE,0BAA0B,CAAC,cAAc;;;;AAGpE,AAAA,kBAAkB,CAAC;EACf,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,KAAK;EACV,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;CAed;;AApBD,AAMI,kBANc,CAMZ,QAAQ,CAAC;EACP,iBAAiB,EAAE,4BAA4B;EAC3C,SAAS,EAAE,4BAA4B;EAC3C,4BAA4B,EAAE,OAAO;EAC7B,oBAAoB,EAAE,OAAO;EACrC,MAAM,EAAE,iBAAiB;EACzB,mBAAmB,EAAE,WAAW;EAChC,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,iBAAiB,EAAE,0BAA0B;EACrC,SAAS,EAAE,0BAA0B;EAC7C,WAAW,EAAE,SAAS;CACzB;;AAGL,AAAA,WAAW,CAAC;EACR,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;CAUd;;AAbD,AAII,WAJO,CAIL,GAAG,CAAC;EACF,UAAU,EAAE,SAAS;CACxB;;AANL,AAOI,WAPO,AAON,MAAM,CAAC;EACJ,MAAM,EAAE,OAAO;CAIlB;;AAZL,AASQ,WATG,AAON,MAAM,CAED,GAAG,CAAC;EACF,MAAM,EAAE,OAAO;CAClB;;AAIT,AAAA,WAAW,CAAC;EACR,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,kBAAkB;EACzB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,WAAW,EAAE,MAAM;EACnB,eAAe,EAAE,aAAa;EAC9B,OAAO,EAAE,MAAM;EACf,gBAAgB,EAAE,OAAO;CA4B5B;;AArCD,AAUI,WAVO,CAUL,OAAO,CAAC;EACN,OAAO,EAAE,SAAS;EAClB,gBAAgB,EAAE,WAAW;EAC7B,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,IAAI;CAsBhB;;AApCL,AAeQ,WAfG,CAUL,OAAO,CAKH,GAAG,CAAC;EACF,UAAU,EAAE,SAAS;CACxB;;AAjBT,AAkBQ,WAlBG,CAUL,OAAO,AAQJ,OAAO,CAAC,GAAG,CAAC;EACT,MAAM,EAAE,OAAO;CAClB;;AApBT,AAqBQ,WArBG,CAUL,OAAO,AAWJ,MAAM,CAAC;EACJ,MAAM,EAAE,OAAO;CAIlB;;AA1BT,AAuBY,WAvBD,CAUL,OAAO,AAWJ,MAAM,CAED,GAAG,CAAC;EACF,MAAM,EAAE,OAAO;CAClB;;AAzBb,AA4BY,WA5BD,CAUL,OAAO,AAiBJ,SAAS,CACJ,GAAG,CAAC;EACF,MAAM,EAAE,OAAO;CAClB;;AA9Bb,AA+BY,WA/BD,CAUL,OAAO,AAiBJ,SAAS,AAIL,MAAM,CAAC;EACJ,MAAM,EAAE,OAAO;EACf,KAAK,EAAE,OAAO;CACjB;;AAKb,AAAA,UAAU,CAAC;EACP,UAAU,EAAE,IAAI;EAChB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;CAwFtB;;AA3FD,AAII,UAJM,CAIJ,QAAQ,CAAC;EACP,OAAO,EAAE,MAAM;EACf,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,mBAAmB;EAC/B,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;CAUjB;;AAnBL,AAUQ,UAVE,CAIJ,QAAQ,CAMJ,IAAI,CAAC;EACH,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,MAAM;CAIrB;;AAlBT,AAeY,UAfF,CAIJ,QAAQ,CAMJ,IAAI,AAKD,WAAW,CAAC;EACT,aAAa,EAAE,IAAI;CACtB;;AAjBb,AAoBI,UApBM,CAoBJ,QAAQ,CAAC;EACP,UAAU,EAAE,UAAU;EACtB,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,kBAAkB;CAkB5B;;AAzCL,AAwBQ,UAxBE,CAoBJ,QAAQ,CAIJ,KAAK,CAAC;EACJ,WAAW,EAAE,GAAG;EAChB,WAAW,EAAE,IAAI;EACjB,UAAU,EAAE,IAAI;CACnB;;AA5BT,AA6BQ,UA7BE,CAoBJ,QAAQ,CASJ,MAAM,CAAC;EACL,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,CAAC;EACV,gBAAgB,EAAE,WAAW;EAC7B,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,OAAO;EACd,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,OAAO;EACb,WAAW,EAAE,IAAI;CACpB;;AAxCT,AA0CI,UA1CM,CA0CJ,cAAc,CAAC;EACb,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,CAAC;EACR,OAAO,EAAE,IAAI;EACb,OAAO,EAAE,CAAC;CAgBb;;AA/DL,AAgDQ,UAhDE,CA0CJ,cAAc,CAMV,GAAG,CAAC;EACF,aAAa,EAAE,IAAI;EACnB,KAAK,EAAE,KAAK;EACZ,UAAU,EAAE,IAAI;EAChB,OAAO,EAAE,SAAS;CAUrB;;AA9DT,AAqDY,UArDF,CA0CJ,cAAc,CAMV,GAAG,AAKA,MAAM,CAAC;EACJ,gBAAgB,EAAE,OAAO;CAC5B;;AAvDb,AAwDY,UAxDF,CA0CJ,cAAc,CAMV,GAAG,AAQA,QAAQ,CAAC;EACN,gBAAgB,EAAE,OAAO;CAC5B;;AA1Db,AA2DY,UA3DF,CA0CJ,cAAc,CAMV,GAAG,AAWA,YAAY,CAAC;EACV,UAAU,EAAE,CAAC;CAChB;;AA7Db,AAgEI,UAhEM,CAgEJ,mBAAmB,CAAC;EAClB,QAAQ,EAAE,KAAK;EACf,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,IAAI;EACT,OAAO,EAAE,SAAS;EAClB,gBAAgB,EAAE,OAAO;EACzB,aAAa,EAAE,IAAI;CAOtB;;AA7EL,AAuEQ,UAvEE,CAgEJ,mBAAmB,CAOf,SAAS,CAAC;EACR,gBAAgB,EAAE,OAAO;EACzB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,QAAQ;EACjB,aAAa,EAAE,IAAI;CACtB;;AA5ET,AA8EI,UA9EM,CA8EJ,yBAAyB,CAAC;EACxB,QAAQ,EAAE,KAAK;EACf,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,SAAS;EAClB,gBAAgB,EAAE,OAAO;EACzB,aAAa,EAAE,IAAI;CAMtB;;AA1FL,AAqFQ,UArFE,CA8EJ,yBAAyB,CAOrB,IAAI,CAAC;EACH,gBAAgB,EAAE,OAAO;EACzB,OAAO,EAAE,QAAQ;EACjB,aAAa,EAAE,IAAI;CACtB;;AAIT,AAAA,OAAO,CAAC;EACJ,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;EACX,gBAAgB,EAAE,OAAO;CA6B5B;;AAlCD,AAMI,OANG,CAMD,KAAK,CAAC;EACJ,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,WAAW,EAAE,MAAM;EACnB,eAAe,EAAE,aAAa;EAC9B,MAAM,EAAE,aAAa;CACxB;;AAZL,AAaI,OAbG,CAaD,GAAG,CAAC;EACF,OAAO,EAAE,YAAY;CACxB;;AAfL,AAgBI,OAhBG,CAgBD,CAAC,CAAC;EACA,OAAO,EAAE,YAAY;EACrB,eAAe,EAAE,IAAI;EACrB,KAAK,EAAE,OAAO;EACd,OAAO,EAAE,QAAQ;EACjB,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,SAAS;CAKxB;;AA3BL,AAuBQ,OAvBD,CAgBD,CAAC,AAOE,MAAM,CAAC;EACJ,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;CACjB;;AA1BT,AA4BI,OA5BG,CA4BD,QAAQ,CAAC;EACP,OAAO,EAAE,YAAY;EACrB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,QAAQ;EACjB,gBAAgB,EAAE,OAAO;CAC5B;;AAGL,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACpC,AAAA,WAAW,CAAC;IACR,OAAO,EAAE,MAAM;IACf,KAAK,EAAE,kBAAkB;GAW5B;EAbD,AAGI,WAHO,CAGL,OAAO,CAAC;IACN,OAAO,EAAE,SAAS;GAKrB;EATL,AAKQ,WALG,CAGL,OAAO,CAEH,GAAG,CAAC;IACF,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;GACf;EART,AAUI,WAVO,CAUL,KAAK,CAAC,QAAQ,CAAC;IACb,OAAO,EAAE,IAAI;GAChB;EAGL,AAAA,UAAU,CAAC,cAAc,CAAC;IACtB,OAAO,EAAE,CAAC;GAOb;EARD,AAEI,UAFM,CAAC,cAAc,CAEnB,GAAG,CAAC;IACF,MAAM,EAAE,CAAC;IACT,aAAa,EAAE,CAAC;IAChB,KAAK,EAAE,KAAK;IACZ,UAAU,EAAE,UAAU;GACzB;EAIL,AACI,OADG,CACD,KAAK,CAAC;IACJ,MAAM,EAAE,UAAU;GACrB;EAHL,AAII,OAJG,CAID,kBAAkB,CAAC,IAAI,CAAC;IACtB,OAAO,EAAE,IAAI;GAChB;EANL,AAOI,OAPG,CAOD,CAAC,CAAC;IACA,OAAO,EAAE,QAAQ;GACpB;;;AAIT,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACpC,AAAA,SAAS,EAAE,mBAAmB,EAAE,yBAAyB,CAAC;IACtD,OAAO,EAAE,IAAI;GAChB;;;AAGL,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACpC,AACI,OADG,CACD,KAAK,CAAC;IACJ,MAAM,EAAE,CAAC;IACT,eAAe,EAAE,YAAY;GAChC;EAJL,AAKI,OALG,CAKD,kBAAkB,CAAC;IACjB,OAAO,EAAE,IAAI;GAChB", 4 | "sources": [ 5 | "style.scss" 6 | ], 7 | "names": [], 8 | "file": "style.css" 9 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pasty 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 111 |
112 |
113 | 116 |
117 | 0 characters, 0 lines 118 |
119 |
>
120 |
121 |
122 | 123 |
124 |
125 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /web/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | @import url('/assets/fonts/source-code-pro/font.css'); 2 | 3 | html, body { 4 | margin: 0; 5 | padding: 0; 6 | background-color: #000000; 7 | color: #ffffff; 8 | font-family: 'Source Code Pro', monospace; 9 | &.embedded { 10 | .navigation { 11 | display: none; 12 | } 13 | .container { 14 | margin: 0; 15 | } 16 | #content, #linenos { 17 | padding-top: 10px; 18 | min-height: calc(100vh - 50px); 19 | } 20 | #footer { 21 | font-size: 0.8em; 22 | } 23 | } 24 | } 25 | 26 | ::-webkit-scrollbar { 27 | width: 10px; 28 | height: 10px; 29 | } 30 | 31 | ::-webkit-scrollbar-track { 32 | background: #000000; 33 | } 34 | 35 | ::-webkit-scrollbar-thumb { 36 | background: #444444; 37 | border-radius: 5px; 38 | } 39 | 40 | ::-webkit-scrollbar-thumb:hover { 41 | background: #333333; 42 | } 43 | 44 | ::-webkit-scrollbar-thumb:active { 45 | background: #222222; 46 | } 47 | 48 | .hidden { 49 | display: none; 50 | } 51 | 52 | @-webkit-keyframes spinner { 53 | 0% { 54 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg); 55 | transform: translate3d(-50%, -50%, 0) rotate(0deg); 56 | } 57 | 100% { 58 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg); 59 | transform: translate3d(-50%, -50%, 0) rotate(360deg); 60 | } 61 | } 62 | @keyframes spinner { 63 | 0% { 64 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg); 65 | transform: translate3d(-50%, -50%, 0) rotate(0deg); 66 | } 67 | 100% { 68 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg); 69 | transform: translate3d(-50%, -50%, 0) rotate(360deg); 70 | } 71 | } 72 | #spinner-container { 73 | position: fixed; 74 | top: 130px; 75 | right: 20px; 76 | height: 50px; 77 | width: 50px; 78 | & .spinner { 79 | -webkit-animation: .75s linear infinite spinner; 80 | animation: .75s linear infinite spinner; 81 | -webkit-animation-play-state: inherit; 82 | animation-play-state: inherit; 83 | border: solid 5px #ffffff; 84 | border-bottom-color: transparent; 85 | border-radius: 50%; 86 | height: 100%; 87 | width: 100%; 88 | -webkit-transform: translate3d(-50%, -50%, 0); 89 | transform: translate3d(-50%, -50%, 0); 90 | will-change: transform; 91 | } 92 | } 93 | 94 | #btn_report { 95 | position: fixed; 96 | bottom: 60px; 97 | right: 30px; 98 | & svg { 99 | transition: all 250ms; 100 | } 101 | &:hover { 102 | cursor: pointer; 103 | & svg { 104 | stroke: #2daa57; 105 | } 106 | } 107 | } 108 | 109 | .navigation { 110 | position: fixed; 111 | top: 0; 112 | width: calc(100vw - 80px); 113 | display: flex; 114 | flex-direction: row; 115 | align-items: center; 116 | justify-content: space-between; 117 | padding: 0 40px; 118 | background-color: #222222; 119 | & .button { 120 | padding: 10px 20px; 121 | background-color: transparent; 122 | border: none; 123 | outline: none; 124 | & svg { 125 | transition: all 250ms; 126 | } 127 | &.active svg { 128 | stroke: #2daa57; 129 | } 130 | &:hover { 131 | cursor: pointer; 132 | & svg { 133 | stroke: #2daa57; 134 | } 135 | } 136 | &:disabled { 137 | & svg { 138 | stroke: #5a5a5a; 139 | } 140 | &:hover { 141 | cursor: initial; 142 | color: initial; 143 | } 144 | } 145 | } 146 | } 147 | 148 | .container { 149 | margin-top: 60px; 150 | display: flex; 151 | flex-direction: row; 152 | & #linenos { 153 | padding: 20px 0; 154 | width: 50px; 155 | min-height: calc(100vh - 100px); 156 | background-color: #111111; 157 | color: #bebebe; 158 | & span { 159 | display: block; 160 | width: 100%; 161 | height: 20px; 162 | text-align: center; 163 | &:last-child { 164 | margin-bottom: 25px; 165 | } 166 | } 167 | } 168 | & #content { 169 | box-sizing: border-box; 170 | padding: 20px; 171 | width: calc(100vw - 50px); 172 | & #code { 173 | white-space: pre; 174 | line-height: 20px; 175 | overflow-x: auto; 176 | } 177 | & #input { 178 | height: 100%; 179 | width: 100%; 180 | padding: 0; 181 | background-color: transparent; 182 | border: none; 183 | outline: none; 184 | color: inherit; 185 | resize: none; 186 | font: inherit; 187 | line-height: 20px; 188 | } 189 | } 190 | & #notifications { 191 | position: fixed; 192 | bottom: 30px; 193 | right: 0; 194 | padding: 20px; 195 | z-index: 1; 196 | & div { 197 | border-radius: 10px; 198 | width: 500px; 199 | margin-top: 20px; 200 | padding: 20px 30px; 201 | &.error { 202 | background-color: #ff4d4d; 203 | } 204 | &.success { 205 | background-color: #389b38; 206 | } 207 | &:first-child { 208 | margin-top: 0; 209 | } 210 | } 211 | } 212 | & #lifetime_container { 213 | position: fixed; 214 | right: 30px; 215 | top: 90px; 216 | padding: 10px 15px; 217 | background-color: #222222; 218 | border-radius: 10px; 219 | & #lifetime { 220 | background-color: #111111; 221 | margin-left: 10px; 222 | padding: 5px 10px; 223 | border-radius: 10px; 224 | } 225 | } 226 | & #content_length_container { 227 | position: fixed; 228 | right: 30px; 229 | bottom: 60px; 230 | padding: 10px 15px; 231 | background-color: #222222; 232 | border-radius: 10px; 233 | & span { 234 | background-color: #111111; 235 | padding: 5px 10px; 236 | border-radius: 10px; 237 | } 238 | } 239 | } 240 | 241 | #footer { 242 | position: fixed; 243 | bottom: 0; 244 | left: 0; 245 | width: 100%; 246 | background-color: #222222; 247 | & #flex { 248 | display: flex; 249 | flex-direction: row; 250 | align-items: center; 251 | justify-content: space-between; 252 | margin: 0 80px 0 60px; 253 | } 254 | & div { 255 | display: inline-block; 256 | } 257 | & a { 258 | display: inline-block; 259 | text-decoration: none; 260 | color: #ffffff; 261 | padding: 5px 20px; 262 | height: 100%; 263 | transition: all 200ms; 264 | &:hover { 265 | background-color: #333333; 266 | color: #2daa57; 267 | } 268 | } 269 | & #version { 270 | display: inline-block; 271 | margin-left: 10px; 272 | padding: 5px 30px; 273 | background-color: #111111; 274 | } 275 | } 276 | 277 | @media only screen and (max-width: 650px) { 278 | .navigation { 279 | padding: 0 20px; 280 | width: calc(100vw - 40px); 281 | & .button { 282 | padding: 15px 10px; 283 | & svg { 284 | width: 30px; 285 | height: 30px; 286 | } 287 | } 288 | & .meta #version { 289 | display: none; 290 | } 291 | } 292 | 293 | .container #notifications { 294 | padding: 0; 295 | & div { 296 | margin: 0; 297 | border-radius: 0; 298 | width: 100vw; 299 | box-sizing: border-box; 300 | } 301 | } 302 | 303 | 304 | #footer { 305 | & #flex { 306 | margin: 0 0 0 25px; 307 | } 308 | & .version-container span { 309 | display: none; 310 | } 311 | & a { 312 | padding: 5px 15px; 313 | } 314 | } 315 | } 316 | 317 | @media only screen and (max-width: 400px) { 318 | #btn_copy, #lifetime_container, #content_length_container { 319 | display: none; 320 | } 321 | } 322 | 323 | @media only screen and (max-width: 500px) { 324 | #footer { 325 | & #flex { 326 | margin: 0; 327 | justify-content: space-around; 328 | } 329 | & .version-container { 330 | display: none; 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /web/assets/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('/assets/fonts/source-code-pro/font.css'); 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | background-color: #000000; 6 | color: #ffffff; 7 | font-family: 'Source Code Pro', monospace; 8 | } 9 | 10 | html.embedded .navigation, body.embedded .navigation { 11 | display: none; 12 | } 13 | 14 | html.embedded .container, body.embedded .container { 15 | margin: 0; 16 | } 17 | 18 | html.embedded #content, html.embedded #linenos, body.embedded #content, body.embedded #linenos { 19 | padding-top: 10px; 20 | min-height: calc(100vh - 50px); 21 | } 22 | 23 | html.embedded #footer, body.embedded #footer { 24 | font-size: 0.8em; 25 | } 26 | 27 | ::-webkit-scrollbar { 28 | width: 10px; 29 | height: 10px; 30 | } 31 | 32 | ::-webkit-scrollbar-track { 33 | background: #000000; 34 | } 35 | 36 | ::-webkit-scrollbar-thumb { 37 | background: #444444; 38 | border-radius: 5px; 39 | } 40 | 41 | ::-webkit-scrollbar-thumb:hover { 42 | background: #333333; 43 | } 44 | 45 | ::-webkit-scrollbar-thumb:active { 46 | background: #222222; 47 | } 48 | 49 | .hidden { 50 | display: none; 51 | } 52 | 53 | @-webkit-keyframes spinner { 54 | 0% { 55 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg); 56 | transform: translate3d(-50%, -50%, 0) rotate(0deg); 57 | } 58 | 100% { 59 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg); 60 | transform: translate3d(-50%, -50%, 0) rotate(360deg); 61 | } 62 | } 63 | 64 | @keyframes spinner { 65 | 0% { 66 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg); 67 | transform: translate3d(-50%, -50%, 0) rotate(0deg); 68 | } 69 | 100% { 70 | -webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg); 71 | transform: translate3d(-50%, -50%, 0) rotate(360deg); 72 | } 73 | } 74 | 75 | #spinner-container { 76 | position: fixed; 77 | top: 130px; 78 | right: 20px; 79 | height: 50px; 80 | width: 50px; 81 | } 82 | 83 | #spinner-container .spinner { 84 | -webkit-animation: .75s linear infinite spinner; 85 | animation: .75s linear infinite spinner; 86 | -webkit-animation-play-state: inherit; 87 | animation-play-state: inherit; 88 | border: solid 5px #ffffff; 89 | border-bottom-color: transparent; 90 | border-radius: 50%; 91 | height: 100%; 92 | width: 100%; 93 | -webkit-transform: translate3d(-50%, -50%, 0); 94 | transform: translate3d(-50%, -50%, 0); 95 | will-change: transform; 96 | } 97 | 98 | #btn_report { 99 | position: fixed; 100 | bottom: 60px; 101 | right: 30px; 102 | } 103 | 104 | #btn_report svg { 105 | -webkit-transition: all 250ms; 106 | transition: all 250ms; 107 | } 108 | 109 | #btn_report:hover { 110 | cursor: pointer; 111 | } 112 | 113 | #btn_report:hover svg { 114 | stroke: #2daa57; 115 | } 116 | 117 | .navigation { 118 | position: fixed; 119 | top: 0; 120 | width: calc(100vw - 80px); 121 | display: -webkit-box; 122 | display: -ms-flexbox; 123 | display: flex; 124 | -webkit-box-orient: horizontal; 125 | -webkit-box-direction: normal; 126 | -ms-flex-direction: row; 127 | flex-direction: row; 128 | -webkit-box-align: center; 129 | -ms-flex-align: center; 130 | align-items: center; 131 | -webkit-box-pack: justify; 132 | -ms-flex-pack: justify; 133 | justify-content: space-between; 134 | padding: 0 40px; 135 | background-color: #222222; 136 | } 137 | 138 | .navigation .button { 139 | padding: 10px 20px; 140 | background-color: transparent; 141 | border: none; 142 | outline: none; 143 | } 144 | 145 | .navigation .button svg { 146 | -webkit-transition: all 250ms; 147 | transition: all 250ms; 148 | } 149 | 150 | .navigation .button.active svg { 151 | stroke: #2daa57; 152 | } 153 | 154 | .navigation .button:hover { 155 | cursor: pointer; 156 | } 157 | 158 | .navigation .button:hover svg { 159 | stroke: #2daa57; 160 | } 161 | 162 | .navigation .button:disabled svg { 163 | stroke: #5a5a5a; 164 | } 165 | 166 | .navigation .button:disabled:hover { 167 | cursor: initial; 168 | color: initial; 169 | } 170 | 171 | .container { 172 | margin-top: 60px; 173 | display: -webkit-box; 174 | display: -ms-flexbox; 175 | display: flex; 176 | -webkit-box-orient: horizontal; 177 | -webkit-box-direction: normal; 178 | -ms-flex-direction: row; 179 | flex-direction: row; 180 | } 181 | 182 | .container #linenos { 183 | padding: 20px 0; 184 | width: 50px; 185 | min-height: calc(100vh - 100px); 186 | background-color: #111111; 187 | color: #bebebe; 188 | } 189 | 190 | .container #linenos span { 191 | display: block; 192 | width: 100%; 193 | height: 20px; 194 | text-align: center; 195 | } 196 | 197 | .container #linenos span:last-child { 198 | margin-bottom: 25px; 199 | } 200 | 201 | .container #content { 202 | -webkit-box-sizing: border-box; 203 | box-sizing: border-box; 204 | padding: 20px; 205 | width: calc(100vw - 50px); 206 | } 207 | 208 | .container #content #code { 209 | white-space: pre; 210 | line-height: 20px; 211 | overflow-x: auto; 212 | } 213 | 214 | .container #content #input { 215 | height: 100%; 216 | width: 100%; 217 | padding: 0; 218 | background-color: transparent; 219 | border: none; 220 | outline: none; 221 | color: inherit; 222 | resize: none; 223 | font: inherit; 224 | line-height: 20px; 225 | } 226 | 227 | .container #notifications { 228 | position: fixed; 229 | bottom: 30px; 230 | right: 0; 231 | padding: 20px; 232 | z-index: 1; 233 | } 234 | 235 | .container #notifications div { 236 | border-radius: 10px; 237 | width: 500px; 238 | margin-top: 20px; 239 | padding: 20px 30px; 240 | } 241 | 242 | .container #notifications div.error { 243 | background-color: #ff4d4d; 244 | } 245 | 246 | .container #notifications div.success { 247 | background-color: #389b38; 248 | } 249 | 250 | .container #notifications div:first-child { 251 | margin-top: 0; 252 | } 253 | 254 | .container #lifetime_container { 255 | position: fixed; 256 | right: 30px; 257 | top: 90px; 258 | padding: 10px 15px; 259 | background-color: #222222; 260 | border-radius: 10px; 261 | } 262 | 263 | .container #lifetime_container #lifetime { 264 | background-color: #111111; 265 | margin-left: 10px; 266 | padding: 5px 10px; 267 | border-radius: 10px; 268 | } 269 | 270 | .container #content_length_container { 271 | position: fixed; 272 | right: 30px; 273 | bottom: 60px; 274 | padding: 10px 15px; 275 | background-color: #222222; 276 | border-radius: 10px; 277 | } 278 | 279 | .container #content_length_container span { 280 | background-color: #111111; 281 | padding: 5px 10px; 282 | border-radius: 10px; 283 | } 284 | 285 | #footer { 286 | position: fixed; 287 | bottom: 0; 288 | left: 0; 289 | width: 100%; 290 | background-color: #222222; 291 | } 292 | 293 | #footer #flex { 294 | display: -webkit-box; 295 | display: -ms-flexbox; 296 | display: flex; 297 | -webkit-box-orient: horizontal; 298 | -webkit-box-direction: normal; 299 | -ms-flex-direction: row; 300 | flex-direction: row; 301 | -webkit-box-align: center; 302 | -ms-flex-align: center; 303 | align-items: center; 304 | -webkit-box-pack: justify; 305 | -ms-flex-pack: justify; 306 | justify-content: space-between; 307 | margin: 0 80px 0 60px; 308 | } 309 | 310 | #footer div { 311 | display: inline-block; 312 | } 313 | 314 | #footer a { 315 | display: inline-block; 316 | text-decoration: none; 317 | color: #ffffff; 318 | padding: 5px 20px; 319 | height: 100%; 320 | -webkit-transition: all 200ms; 321 | transition: all 200ms; 322 | } 323 | 324 | #footer a:hover { 325 | background-color: #333333; 326 | color: #2daa57; 327 | } 328 | 329 | #footer #version { 330 | display: inline-block; 331 | margin-left: 10px; 332 | padding: 5px 30px; 333 | background-color: #111111; 334 | } 335 | 336 | @media only screen and (max-width: 650px) { 337 | .navigation { 338 | padding: 0 20px; 339 | width: calc(100vw - 40px); 340 | } 341 | .navigation .button { 342 | padding: 15px 10px; 343 | } 344 | .navigation .button svg { 345 | width: 30px; 346 | height: 30px; 347 | } 348 | .navigation .meta #version { 349 | display: none; 350 | } 351 | .container #notifications { 352 | padding: 0; 353 | } 354 | .container #notifications div { 355 | margin: 0; 356 | border-radius: 0; 357 | width: 100vw; 358 | -webkit-box-sizing: border-box; 359 | box-sizing: border-box; 360 | } 361 | #footer #flex { 362 | margin: 0 0 0 25px; 363 | } 364 | #footer .version-container span { 365 | display: none; 366 | } 367 | #footer a { 368 | padding: 5px 15px; 369 | } 370 | } 371 | 372 | @media only screen and (max-width: 400px) { 373 | #btn_copy, #lifetime_container, #content_length_container { 374 | display: none; 375 | } 376 | } 377 | 378 | @media only screen and (max-width: 500px) { 379 | #footer #flex { 380 | margin: 0; 381 | -ms-flex-pack: distribute; 382 | justify-content: space-around; 383 | } 384 | #footer .version-container { 385 | display: none; 386 | } 387 | } 388 | /*# sourceMappingURL=style.css.map */ 389 | -------------------------------------------------------------------------------- /internal/web/controllers/v2/pastes.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | 8 | "github.com/fasthttp/router" 9 | "github.com/lus/pasty/internal/config" 10 | "github.com/lus/pasty/internal/report" 11 | "github.com/lus/pasty/internal/shared" 12 | "github.com/lus/pasty/internal/storage" 13 | "github.com/lus/pasty/internal/utils" 14 | limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp" 15 | "github.com/valyala/fasthttp" 16 | ) 17 | 18 | // InitializePastesController initializes the '/v2/pastes/*' controller 19 | func InitializePastesController(group *router.Group, rateLimiterMiddleware *limitFasthttp.Middleware) { 20 | // moms spaghetti 21 | group.GET("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(endpointGetPaste))) 22 | group.POST("", rateLimiterMiddleware.Handle(endpointCreatePaste)) 23 | group.PATCH("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(middlewareValidateModificationToken(endpointModifyPaste)))) 24 | group.DELETE("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(middlewareValidateModificationToken(endpointDeletePaste)))) 25 | 26 | if config.Current.Reports.Reports { 27 | group.POST("/{id}/report", rateLimiterMiddleware.Handle(middlewareInjectPaste(endpointReportPaste))) 28 | } 29 | } 30 | 31 | // middlewareInjectPaste retrieves and injects the paste with the specified ID 32 | func middlewareInjectPaste(next fasthttp.RequestHandler) fasthttp.RequestHandler { 33 | return func(ctx *fasthttp.RequestCtx) { 34 | pasteID := ctx.UserValue("id").(string) 35 | 36 | paste, err := storage.Current.Get(pasteID) 37 | if err != nil { 38 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 39 | ctx.SetBodyString(err.Error()) 40 | return 41 | } 42 | if paste == nil { 43 | ctx.SetStatusCode(fasthttp.StatusNotFound) 44 | ctx.SetBodyString("paste not found") 45 | return 46 | } 47 | 48 | if paste.Metadata == nil { 49 | paste.Metadata = map[string]interface{}{} 50 | } 51 | 52 | ctx.SetUserValue("_paste", paste) 53 | 54 | next(ctx) 55 | } 56 | } 57 | 58 | // middlewareValidateModificationToken extracts and validates a given modification token for an injected paste 59 | func middlewareValidateModificationToken(next fasthttp.RequestHandler) fasthttp.RequestHandler { 60 | return func(ctx *fasthttp.RequestCtx) { 61 | paste := ctx.UserValue("_paste").(*shared.Paste) 62 | 63 | authHeaderSplit := strings.SplitN(string(ctx.Request.Header.Peek("Authorization")), " ", 2) 64 | if len(authHeaderSplit) < 2 || authHeaderSplit[0] != "Bearer" { 65 | ctx.SetStatusCode(fasthttp.StatusUnauthorized) 66 | ctx.SetBodyString("unauthorized") 67 | return 68 | } 69 | 70 | modificationToken := authHeaderSplit[1] 71 | if config.Current.ModificationTokenMaster != "" && modificationToken == config.Current.ModificationTokenMaster { 72 | next(ctx) 73 | return 74 | } 75 | valid := paste.CheckModificationToken(modificationToken) 76 | if !valid { 77 | ctx.SetStatusCode(fasthttp.StatusUnauthorized) 78 | ctx.SetBodyString("unauthorized") 79 | return 80 | } 81 | 82 | next(ctx) 83 | } 84 | } 85 | 86 | // endpointGetPaste handles the 'GET /v2/pastes/{id}' endpoint 87 | func endpointGetPaste(ctx *fasthttp.RequestCtx) { 88 | paste := ctx.UserValue("_paste").(*shared.Paste) 89 | paste.DeletionToken = "" 90 | paste.ModificationToken = "" 91 | 92 | jsonData, err := json.Marshal(paste) 93 | if err != nil { 94 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 95 | ctx.SetBodyString(err.Error()) 96 | return 97 | } 98 | ctx.SetBody(jsonData) 99 | } 100 | 101 | type endpointCreatePastePayload struct { 102 | Content string `json:"content"` 103 | Metadata map[string]interface{} `json:"metadata"` 104 | } 105 | 106 | // endpointCreatePaste handles the 'POST /v2/pastes' endpoint 107 | func endpointCreatePaste(ctx *fasthttp.RequestCtx) { 108 | // Read, parse and validate the request payload 109 | payload := new(endpointCreatePastePayload) 110 | if err := json.Unmarshal(ctx.PostBody(), payload); err != nil { 111 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 112 | ctx.SetBodyString(err.Error()) 113 | return 114 | } 115 | if payload.Content == "" { 116 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 117 | ctx.SetBodyString("missing paste content") 118 | return 119 | } 120 | if config.Current.LengthCap > 0 && len(payload.Content) > config.Current.LengthCap { 121 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 122 | ctx.SetBodyString("too large paste content") 123 | return 124 | } 125 | 126 | // Acquire a new paste ID 127 | id, err := storage.AcquireID() 128 | if err != nil { 129 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 130 | ctx.SetBodyString(err.Error()) 131 | return 132 | } 133 | 134 | // Prepare the paste object 135 | if payload.Metadata == nil { 136 | payload.Metadata = map[string]interface{}{} 137 | } 138 | paste := &shared.Paste{ 139 | ID: id, 140 | Content: payload.Content, 141 | Created: time.Now().Unix(), 142 | Metadata: payload.Metadata, 143 | } 144 | 145 | // Create a new modification token if enabled 146 | modificationToken := "" 147 | if config.Current.ModificationTokens { 148 | modificationToken = utils.RandomString(config.Current.ModificationTokenCharacters, config.Current.ModificationTokenLength) 149 | paste.ModificationToken = modificationToken 150 | 151 | err = paste.HashModificationToken() 152 | if err != nil { 153 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 154 | ctx.SetBodyString(err.Error()) 155 | return 156 | } 157 | } 158 | 159 | // Save the paste 160 | err = storage.Current.Save(paste) 161 | if err != nil { 162 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 163 | ctx.SetBodyString(err.Error()) 164 | return 165 | } 166 | 167 | // Respond with the paste 168 | pasteCopy := *paste 169 | pasteCopy.ModificationToken = modificationToken 170 | jsonData, err := json.Marshal(pasteCopy) 171 | if err != nil { 172 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 173 | ctx.SetBodyString(err.Error()) 174 | return 175 | } 176 | ctx.SetStatusCode(fasthttp.StatusCreated) 177 | ctx.SetBody(jsonData) 178 | } 179 | 180 | type endpointModifyPastePayload struct { 181 | Content *string `json:"content"` 182 | Metadata map[string]interface{} `json:"metadata"` 183 | } 184 | 185 | // endpointModifyPaste handles the 'PATCH /v2/pastes/{id}' endpoint 186 | func endpointModifyPaste(ctx *fasthttp.RequestCtx) { 187 | // Read, parse and validate the request payload 188 | payload := new(endpointModifyPastePayload) 189 | if err := json.Unmarshal(ctx.PostBody(), payload); err != nil { 190 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 191 | ctx.SetBodyString(err.Error()) 192 | return 193 | } 194 | if payload.Content != nil && *payload.Content == "" { 195 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 196 | ctx.SetBodyString("missing paste content") 197 | return 198 | } 199 | if payload.Content != nil && config.Current.LengthCap > 0 && len(*payload.Content) > config.Current.LengthCap { 200 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 201 | ctx.SetBodyString("too large paste content") 202 | return 203 | } 204 | 205 | // Modify the paste itself 206 | paste := ctx.UserValue("_paste").(*shared.Paste) 207 | if payload.Content != nil { 208 | paste.Content = *payload.Content 209 | } 210 | if payload.Metadata != nil { 211 | for key, value := range payload.Metadata { 212 | if value == nil { 213 | delete(paste.Metadata, key) 214 | continue 215 | } 216 | paste.Metadata[key] = value 217 | } 218 | } 219 | 220 | // Save the modified paste 221 | if err := storage.Current.Save(paste); err != nil { 222 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 223 | ctx.SetBodyString(err.Error()) 224 | return 225 | } 226 | } 227 | 228 | // endpointDeletePaste handles the 'DELETE /v2/pastes/{id}' endpoint 229 | func endpointDeletePaste(ctx *fasthttp.RequestCtx) { 230 | paste := ctx.UserValue("_paste").(*shared.Paste) 231 | if err := storage.Current.Delete(paste.ID); err != nil { 232 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 233 | ctx.SetBodyString(err.Error()) 234 | return 235 | } 236 | } 237 | 238 | type endpointReportPastePayload struct { 239 | Reason string `json:"reason"` 240 | } 241 | 242 | func endpointReportPaste(ctx *fasthttp.RequestCtx) { 243 | // Read, parse and validate the request payload 244 | payload := new(endpointReportPastePayload) 245 | if err := json.Unmarshal(ctx.PostBody(), payload); err != nil { 246 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 247 | ctx.SetBodyString(err.Error()) 248 | return 249 | } 250 | if payload.Reason == "" { 251 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 252 | ctx.SetBodyString("missing report reason") 253 | return 254 | } 255 | 256 | request := &report.ReportRequest{ 257 | Paste: ctx.UserValue("_paste").(*shared.Paste).ID, 258 | Reason: payload.Reason, 259 | } 260 | response, err := report.SendReport(request) 261 | if err != nil { 262 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 263 | ctx.SetBodyString(err.Error()) 264 | return 265 | } 266 | 267 | jsonData, err := json.Marshal(response) 268 | if err != nil { 269 | ctx.SetStatusCode(fasthttp.StatusInternalServerError) 270 | ctx.SetBodyString(err.Error()) 271 | return 272 | } 273 | ctx.SetBody(jsonData) 274 | } 275 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pasty 2 | Pasty is a fast and lightweight code pasting server 3 | 4 | ## !!! Important deprecation notices !!! 5 | 6 | > This version of pasty uses a new field name for the so far called `deletionToken`: `modificationToken`. 7 | > Instances using **PostgreSQL** are **not affected** as a corresponding SQL migration will run before the first startup. 8 | > 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. 9 | > 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). 10 | > 11 | > The old `deletionToken` field will be processed corresponding to these changes but I strongly recommend updating old pastes if possible. 12 | 13 | > Additionally, I changed the three `DELETION_TOKEN*`environment variables to their corresponding `MODIFICATION_TOKEN*` ones: 14 | > - `DELETION_TOKENS` -> `MODIFICATION_TOKENS` 15 | > - `DELETION_TOKEN_MASTER` -> `MODIFICATION_TOKEN_MASTER` 16 | > - `DELETION_TOKEN_LENGTH` -> `MODIFICATION_TOKEN_LENGTH` 17 | > 18 | > 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 ^^. 19 | 20 | 21 | ## Support 22 | 23 | As pasty is an open source project on GitHub you can open an [issue](https://github.com/lus/pasty/issues) whenever you encounter a problem or feature request. 24 | However, it may be annoying to open an issue just to ask a simple question about pastys functionalities, get help with the installation process or mention something about the hosted version. 25 | This is why I created a simple [Discord server](https://go.lus.pm/discord) you may want to join to get an answer to stuff like that pretty quickly. 26 | 27 | ## Disclaimer 28 | 29 | The pasty web frontend comes with some service-related links in it (Discord server). Of course, you are allowed to remove these references. 30 | However, a small reference to pasty would be nice ^^. 31 | 32 | ## Installation 33 | 34 | You may set up pasty in multiple different ways. However, I won't cover all of them as I want to keep this documentation as clean as possible. 35 | 36 | ### 1.) Building from source 37 | To build pasty from source make sure you have [Go](https://go.dev) installed. 38 | 39 | 1. Clone the repository: 40 | ```sh 41 | git clone https://github.com/lus/pasty 42 | ``` 43 | 44 | 2. Switch directory: 45 | ```sh 46 | cd pasty/ 47 | ``` 48 | 49 | 3. Run `go build`: 50 | ```sh 51 | go build -o pasty ./cmd/pasty/main.go 52 | ``` 53 | 54 | To configure pasty, simply create a `.env` file in the same directory as the binary is placed in. 55 | 56 | To run pasty, simply execute the binary. 57 | 58 | ### 2.) Docker (recommended) 59 | To run pasty with Docker, you should have basic understanding of it. 60 | 61 | An example `docker run` command may look like this: 62 | ```sh 63 | docker run -d \ 64 | -p 8080:8080 \ 65 | --name pasty \ 66 | -e PASTY_AUTODELETE="true" \ 67 | ghcr.io/lus/pasty:latest 68 | ``` 69 | 70 | Pasty will be available at http://localhost:8080. 71 | 72 | --- 73 | 74 | ## General environment variables 75 | | Environment Variable | Default Value | Type | Description | 76 | |---------------------------------------|------------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------| 77 | | `PASTY_WEB_ADDRESS` | `:8080` | `string` | Defines the address the web server listens to | 78 | | `PASTY_STORAGE_TYPE` | `file` | `string` | Defines the storage type the pastes are saved to | 79 | | `PASTY_HASTEBIN_SUPPORT` | `false` | `bool` | Defines whether or not the `POST /documents` endpoint should be enabled, as known from the hastebin servers | 80 | | `PASTY_ID_LENGTH` | `6` | `number` | Defines the length of the ID of a paste | 81 | | `PASTY_ID_CHARACTERS` | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789` | `string` | Defines the characters to use when generating paste IDs | 82 | | `PASTY_MODIFICATION_TOKENS` | `true` | `bool` | Defines whether or not modification tokens should be generated | 83 | | `PASTY_MODIFICATION_TOKEN_MASTER` | `` | `string` | Defines the master modification token which is authorized to modify every paste (even if modification tokens are disabled) | 84 | | `PASTY_MODIFICATION_TOKEN_LENGTH` | `12` | `number` | Defines the length of the modification token of a paste | 85 | | `PASTY_MODIFICATION_TOKEN_CHARACTERS` | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789` | `string` | Defines the characters to use when generating modification tokens | 86 | | `PASTY_RATE_LIMIT` | `30-M` | `string` | Defines the rate limit of the API (see https://github.com/ulule/limiter#usage) | 87 | | `PASTY_LENGTH_CAP` | `50000` | `number` | Defines the maximum amount of characters a paste is allowed to contain (a value `<= 0` means no limit) | 88 | 89 | ## AutoDelete 90 | Pasty provides an intuitive system to automatically delete pastes after a specific amount of time. You can configure it with the following variables: 91 | | Environment Variable | Default Value | Type | Description | 92 | |----------------------------------|---------------|----------|--------------------------------------------------------------------------------| 93 | | `PASTY_AUTODELETE` | `false` | `bool` | Defines whether or not the AutoDelete system should be enabled | 94 | | `PASTY_AUTODELETE_LIFETIME` | `720h` | `string` | Defines the duration a paste should live until it gets deleted | 95 | | `PASTY_AUTODELETE_TASK_INTERVAL` | `5m` | `string` | Defines the interval in which the AutoDelete task should clean up the database | 96 | 97 | ## Reports 98 | 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. 99 | However, pasty does include a way of abstract reports to allow frontends work with this information. 100 | If enabled, pasty makes a standardized request to the configured webhook URL if a paste is reported. 101 | | Environment Variable | Default Value | Type | Description | 102 | |------------------------------|---------------|----------|-----------------------------------------------------------------------------------------------------| 103 | | `PASTY_REPORTS` | `false` | `bool` | Defines whether or not the report system should be enabled | 104 | | `PASTY_REPORT_WEBHOOK` | `` | `string` | Defines the webhook URL that is called whenever a paste is reported | 105 | | `PASTY_REPORT_WEBHOOK_TOKEN` | `` | `string` | Defines the token that is sent in the `Authorization` header on every request to the report webhook | 106 | 107 | ## Storage types 108 | Pasty supports multiple storage types, defined using the `PASTY_STORAGE_TYPE` environment variable (use the value behind the corresponding title in this README). 109 | Every single one of them has its own configuration variables: 110 | 111 | ### File (`file`) 112 | | Environment Variable | Default Value | Type | Description | 113 | |---------------------------|---------------|----------|-----------------------------------------------------------| 114 | | `PASTY_STORAGE_FILE_PATH` | `./data` | `string` | Defines the file path the paste files are being saved to | 115 | 116 | --- 117 | 118 | ### PostgreSQL (`postgres`) 119 | | Environment Variable | Default Value | Type | Description | 120 | |------------------------------|------------------------------------------|----------|-------------------------------------------------------------------------------------| 121 | | `PASTY_STORAGE_POSTGRES_DSN` | `postgres://pasty:pasty@localhost/pasty` | `string` | Defines the PostgreSQL connection string (you might have to add `?sslmode=disable`) | 122 | 123 | --- 124 | 125 | ### MongoDB (`mongodb`) 126 | | Environment Variable | Default Value | Type | Description | 127 | |-------------------------------------------|-----------------------------------------|----------|-----------------------------------------------------------------| 128 | | `PASTY_STORAGE_MONGODB_CONNECTION_STRING` | `mongodb://pasty:pasty@localhost/pasty` | `string` | Defines the connection string to use for the MongoDB connection | 129 | | `PASTY_STORAGE_MONGODB_DATABASE` | `pasty` | `string` | Defines the name of the database to use | 130 | | `PASTY_STORAGE_MONGODB_COLLECTION` | `pastes` | `string` | Defines the name of the collection to use | 131 | 132 | --- 133 | 134 | ### S3 (`s3`) 135 | | Environment Variable | Default Value | Type | Description | 136 | |--------------------------------------|---------------|----------|-------------------------------------------------------------------------------------------| 137 | | `PASTY_STORAGE_S3_ENDPOINT` | `` | `string` | Defines the S3 endpoint to connect to | 138 | | `PASTY_STORAGE_S3_ACCESS_KEY_ID` | `` | `string` | Defines the access key ID to use for the S3 storage | 139 | | `PASTY_STORAGE_S3_SECRET_ACCESS_KEY` | `` | `string` | Defines the secret acces key to use for the S3 storage | 140 | | `PASTY_STORAGE_S3_SECRET_TOKEN` | `` | `string` | Defines the session token to use for the S3 storage (may be left empty in the most cases) | 141 | | `PASTY_STORAGE_S3_SECURE` | `true` | `bool` | Defines whether or not SSL should be used for the S3 connection | 142 | | `PASTY_STORAGE_S3_REGION` | `` | `string` | Defines the region of the S3 storage | 143 | | `PASTY_STORAGE_S3_BUCKET` | `pasty` | `string` | Defines the name of the S3 bucket (has to be created before setup) | 144 | -------------------------------------------------------------------------------- /web/assets/js/modules/state.js: -------------------------------------------------------------------------------- 1 | import * as API from "./api.js"; 2 | import * as Notifications from "./notifications.js"; 3 | import * as Spinner from "./spinner.js"; 4 | import * as Animation from "./animation.js"; 5 | import * as Encryption from "./encryption.js"; 6 | import * as Duration from "./duration.js"; 7 | 8 | const CODE_ELEMENT = document.getElementById("code"); 9 | const LINE_NUMBERS_ELEMENT = document.getElementById("linenos"); 10 | const INPUT_ELEMENT = document.getElementById("input"); 11 | 12 | const LIFETIME_CONTAINER_ELEMENT = document.getElementById("lifetime_container"); 13 | 14 | const CHARACTER_AMOUNT_ELEMENT = document.getElementById("characters"); 15 | const LINES_AMOUNT_ELEMENT = document.getElementById("lines"); 16 | 17 | const BUTTONS_DEFAULT_ELEMENT = document.getElementById("buttons_default"); 18 | const BUTTON_NEW_ELEMENT = document.getElementById("btn_new"); 19 | const BUTTON_SAVE_ELEMENT = document.getElementById("btn_save"); 20 | const BUTTON_EDIT_ELEMENT = document.getElementById("btn_edit"); 21 | const BUTTON_DELETE_ELEMENT = document.getElementById("btn_delete"); 22 | const BUTTON_COPY_ELEMENT = document.getElementById("btn_copy"); 23 | 24 | const BUTTON_REPORT_ELEMENT = document.getElementById("btn_report"); 25 | 26 | const BUTTONS_EDIT_ELEMENT = document.getElementById("buttons_edit"); 27 | const BUTTON_EDIT_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel"); 28 | const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply"); 29 | 30 | const BUTTON_TOGGLE_ENCRYPTION_ELEMENT = document.getElementById("btn_toggle_encryption"); 31 | 32 | let PASTE_ID; 33 | let LANGUAGE; 34 | let CODE; 35 | 36 | let ENCRYPTION_KEY; 37 | let ENCRYPTION_IV; 38 | 39 | let EDIT_MODE = false; 40 | 41 | let API_INFORMATION = { 42 | version: "error", 43 | pasteLifetime: -1, 44 | modificationTokens: false, 45 | reports: false 46 | }; 47 | 48 | // Initializes the state system 49 | export async function initialize() { 50 | loadAPIInformation(); 51 | 52 | setupButtonFunctionality(); 53 | setupKeybinds(); 54 | 55 | // When embedded inside an iframe, add "embedded" 56 | // class to body element. 57 | if (window != window.parent) { 58 | document.body.classList += " embedded"; 59 | } 60 | 61 | // Enable encryption if enabled from last session 62 | if (localStorage.getItem("encryption") === "true") { 63 | BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.add("active"); 64 | } 65 | 66 | if (location.pathname !== "/") { 67 | // Extract the paste data (ID and language) 68 | const split = location.pathname.replace("/", "").split("."); 69 | const pasteID = split[0]; 70 | const language = split[1]; 71 | 72 | // Try to retrieve the paste data from the API 73 | const response = await API.getPaste(pasteID); 74 | if (!response.ok) { 75 | Notifications.error("Could not load paste: " + await response.text() + ""); 76 | setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000); 77 | return; 78 | } 79 | 80 | // Set the persistent paste data 81 | PASTE_ID = pasteID; 82 | LANGUAGE = language; 83 | 84 | // Decode the response and decrypt the content if needed 85 | const json = await response.json(); 86 | CODE = json.content; 87 | if (json.metadata.pf_encryption) { 88 | ENCRYPTION_KEY = location.hash.replace("#", ""); 89 | while (ENCRYPTION_KEY.length == 0) { 90 | ENCRYPTION_KEY = prompt("Your decryption key:"); 91 | } 92 | 93 | try { 94 | CODE = await Encryption.decrypt(ENCRYPTION_KEY, json.metadata.pf_encryption.iv, CODE); 95 | ENCRYPTION_IV = json.metadata.pf_encryption.iv; 96 | } catch (error) { 97 | console.log(error); 98 | Notifications.error("Could not decrypt paste; make sure the decryption key is correct."); 99 | setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000); 100 | return; 101 | } 102 | } 103 | 104 | // Fill the code block with the just received data 105 | updateCode(); 106 | } else { 107 | // Give the user the opportunity to paste his code 108 | INPUT_ELEMENT.classList.remove("hidden"); 109 | INPUT_ELEMENT.focus(); 110 | LIFETIME_CONTAINER_ELEMENT.classList.remove("hidden"); 111 | } 112 | 113 | // Update the state of the buttons to match the current state 114 | updateButtonState(); 115 | 116 | INPUT_ELEMENT.addEventListener("input", () => { 117 | updateLineNumbers(INPUT_ELEMENT.value); 118 | 119 | if (BUTTON_SAVE_ELEMENT.hasAttribute("disabled") && INPUT_ELEMENT.value.length > 0) { 120 | BUTTON_SAVE_ELEMENT.removeAttribute("disabled"); 121 | } 122 | if (!BUTTON_SAVE_ELEMENT.hasAttribute("disabled") && INPUT_ELEMENT.value.length == 0) { 123 | BUTTON_SAVE_ELEMENT.setAttribute("disabled", true); 124 | } 125 | }); 126 | } 127 | 128 | // Loads the API information 129 | async function loadAPIInformation() { 130 | // try to retrieve the API information 131 | const response = await API.getAPIInformation(); 132 | if (response.ok) { 133 | API_INFORMATION = await response.json(); 134 | } else { 135 | Notifications.error("Failed loading API information: " + await response.text() + ""); 136 | } 137 | 138 | // Display the API version 139 | document.getElementById("version").innerText = API_INFORMATION.version; 140 | 141 | // Display the paste lifetime 142 | document.getElementById("lifetime").innerText = Duration.format(API_INFORMATION.pasteLifetime); 143 | } 144 | 145 | // Sets the current persistent code to the code block, highlights it and updates the line numbers 146 | function updateCode() { 147 | CODE_ELEMENT.innerHTML = LANGUAGE 148 | ? hljs.highlight(LANGUAGE, CODE).value 149 | : hljs.highlightAuto(CODE).value; 150 | updateLineNumbers(CODE); 151 | } 152 | 153 | function updateLineNumbers(content) { 154 | CHARACTER_AMOUNT_ELEMENT.innerText = content.length; 155 | LINES_AMOUNT_ELEMENT.innerText = content.split(/\n/).length; 156 | 157 | if (content == "") { 158 | LINE_NUMBERS_ELEMENT.innerHTML = ">"; 159 | return; 160 | } 161 | LINE_NUMBERS_ELEMENT.innerHTML = content.split(/\n/).map((_, index) => `${index + 1}`).join(""); 162 | } 163 | 164 | // Updates the button state according to the current state 165 | function updateButtonState() { 166 | if (PASTE_ID) { 167 | BUTTON_SAVE_ELEMENT.setAttribute("disabled", true); 168 | BUTTON_EDIT_ELEMENT.removeAttribute("disabled"); 169 | BUTTON_DELETE_ELEMENT.removeAttribute("disabled"); 170 | BUTTON_COPY_ELEMENT.removeAttribute("disabled"); 171 | 172 | if (API_INFORMATION.reports) { 173 | BUTTON_REPORT_ELEMENT.classList.remove("hidden"); 174 | } 175 | } else { 176 | BUTTON_EDIT_ELEMENT.setAttribute("disabled", true); 177 | BUTTON_DELETE_ELEMENT.setAttribute("disabled", true); 178 | BUTTON_COPY_ELEMENT.setAttribute("disabled", true); 179 | 180 | if (API_INFORMATION.reports) { 181 | BUTTON_REPORT_ELEMENT.classList.add("hidden"); 182 | } 183 | } 184 | } 185 | 186 | // Toggles the edit mode 187 | function toggleEditMode() { 188 | if (EDIT_MODE) { 189 | EDIT_MODE = false; 190 | INPUT_ELEMENT.classList.add("hidden"); 191 | LIFETIME_CONTAINER_ELEMENT.classList.add("hidden"); 192 | CODE_ELEMENT.classList.remove("hidden"); 193 | updateLineNumbers(CODE); 194 | Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeOutDown", "0.3s", () => { 195 | BUTTONS_EDIT_ELEMENT.classList.add("hidden"); 196 | BUTTONS_DEFAULT_ELEMENT.classList.remove("hidden"); 197 | Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeInDown", "0.3s"); 198 | }); 199 | } else { 200 | EDIT_MODE = true; 201 | CODE_ELEMENT.classList.add("hidden"); 202 | LIFETIME_CONTAINER_ELEMENT.classList.remove("hidden"); 203 | INPUT_ELEMENT.classList.remove("hidden"); 204 | INPUT_ELEMENT.value = CODE; 205 | INPUT_ELEMENT.focus(); 206 | Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeOutUp", "0.3s", () => { 207 | BUTTONS_DEFAULT_ELEMENT.classList.add("hidden"); 208 | BUTTONS_EDIT_ELEMENT.classList.remove("hidden"); 209 | Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeInUp", "0.3s"); 210 | }); 211 | } 212 | } 213 | 214 | // Sets up the keybinds for the buttons 215 | function setupKeybinds() { 216 | window.addEventListener("keydown", (event) => { 217 | // All keybinds in the default button set include the CTRL key 218 | if ((EDIT_MODE && !event.ctrlKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey)) { 219 | return; 220 | } 221 | 222 | // Find the DOM element of the button to trigger 223 | let element; 224 | if (EDIT_MODE) { 225 | switch (event.code) { 226 | case "Escape": { 227 | element = BUTTON_EDIT_CANCEL_ELEMENT; 228 | break 229 | } 230 | case "KeyS": { 231 | element = BUTTON_EDIT_APPLY_ELEMENT; 232 | break; 233 | } 234 | } 235 | } else { 236 | switch (event.code) { 237 | case "KeyQ": { 238 | element = BUTTON_NEW_ELEMENT; 239 | break; 240 | } 241 | case "KeyS": { 242 | element = BUTTON_SAVE_ELEMENT; 243 | break; 244 | } 245 | case "KeyO": { 246 | element = BUTTON_EDIT_ELEMENT; 247 | break; 248 | } 249 | case "KeyX": { 250 | element = BUTTON_DELETE_ELEMENT; 251 | break; 252 | } 253 | case "KeyB": { 254 | element = BUTTON_COPY_ELEMENT; 255 | break; 256 | } 257 | } 258 | } 259 | 260 | // Trigger the found button 261 | if (element) { 262 | event.preventDefault(); 263 | if (element.hasAttribute("disabled")) { 264 | return; 265 | } 266 | element.click(); 267 | } 268 | }); 269 | 270 | // Additionally fix the behaviour of the Tab key 271 | window.addEventListener("keydown", (event) => { 272 | if (event.code != "Tab") { 273 | return; 274 | } 275 | event.preventDefault(); 276 | 277 | insertTextAtCursor(inputElement, " "); 278 | }); 279 | } 280 | 281 | // Sets up the different button functionalities 282 | function setupButtonFunctionality() { 283 | BUTTON_NEW_ELEMENT.addEventListener("click", () => location.replace(location.protocol + "//" + location.host)); 284 | 285 | BUTTON_SAVE_ELEMENT.addEventListener("click", () => { 286 | Spinner.surround(async () => { 287 | // Only proceed if the input is not empty 288 | if (!INPUT_ELEMENT.value) { 289 | return; 290 | } 291 | 292 | // Encrypt the paste if needed 293 | let value = INPUT_ELEMENT.value; 294 | let metadata; 295 | let key; 296 | if (BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.contains("active")) { 297 | const encrypted = await Encryption.encrypt(await Encryption.generateEncryptionData(), value); 298 | value = encrypted.result; 299 | metadata = { 300 | pf_encryption: { 301 | alg: "AES-CBC", 302 | iv: encrypted.iv 303 | } 304 | }; 305 | key = encrypted.key; 306 | } 307 | 308 | // Try to create the paste 309 | const response = await API.createPaste(value, metadata); 310 | if (!response.ok) { 311 | Notifications.error("Error while creating paste: " + await response.text() + ""); 312 | return; 313 | } 314 | const data = await response.json(); 315 | 316 | // Display the modification token if provided 317 | if (data.modificationToken) { 318 | prompt("The modification token for your paste is:", data.modificationToken); 319 | } 320 | 321 | // Redirect the user to his newly created paste 322 | location.replace(location.protocol + "//" + location.host + "/" + data.id + (key ? "#" + key : "")); 323 | }); 324 | }); 325 | 326 | BUTTON_EDIT_ELEMENT.addEventListener("click", toggleEditMode); 327 | 328 | BUTTON_DELETE_ELEMENT.addEventListener("click", () => { 329 | Spinner.surround(async () => { 330 | // Ask for the modification token 331 | const modificationToken = prompt("Modification token:"); 332 | if (!modificationToken) { 333 | return; 334 | } 335 | 336 | // Try to delete the paste 337 | const response = await API.deletePaste(PASTE_ID, modificationToken); 338 | if (!response.ok) { 339 | Notifications.error("Error while deleting paste: " + await response.text() + ""); 340 | return; 341 | } 342 | 343 | // Redirect the user to the start page 344 | location.replace(location.protocol + "//" + location.host); 345 | }); 346 | }); 347 | 348 | BUTTON_COPY_ELEMENT.addEventListener("click", async () => { 349 | if (!navigator.clipboard) { 350 | Notifications.error("Clipboard API not supported by your browser."); 351 | return; 352 | } 353 | 354 | await navigator.clipboard.writeText(CODE); 355 | Notifications.success("Successfully copied the code."); 356 | }); 357 | 358 | BUTTON_EDIT_CANCEL_ELEMENT.addEventListener("click", toggleEditMode); 359 | 360 | BUTTON_EDIT_APPLY_ELEMENT.addEventListener("click", async () => { 361 | // Only proceed if the input is not empty 362 | if (!INPUT_ELEMENT.value) { 363 | return; 364 | } 365 | 366 | // Ask for the modification token 367 | const modificationToken = prompt("Modification token:"); 368 | if (!modificationToken) { 369 | return; 370 | } 371 | 372 | // Re-encrypt the paste data if needed 373 | let value = INPUT_ELEMENT.value; 374 | if (ENCRYPTION_KEY && ENCRYPTION_IV) { 375 | const encrypted = await Encryption.encrypt(await Encryption.encryptionDataFromHex(ENCRYPTION_KEY, ENCRYPTION_IV), value); 376 | value = encrypted.result; 377 | } 378 | 379 | // Try to edit the paste 380 | const response = await API.editPaste(PASTE_ID, modificationToken, value); 381 | if (!response.ok) { 382 | Notifications.error("Error while editing paste: " + await response.text() + ""); 383 | return; 384 | } 385 | 386 | // Update the code and leave the edit mode 387 | CODE = INPUT_ELEMENT.value; 388 | updateCode(); 389 | toggleEditMode(); 390 | Notifications.success("Successfully edited paste."); 391 | }); 392 | 393 | BUTTON_TOGGLE_ENCRYPTION_ELEMENT.addEventListener("click", () => { 394 | const active = BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.toggle("active"); 395 | localStorage.setItem("encryption", active); 396 | Notifications.success((active ? "Enabled" : "Disabled") + " automatic paste encryption."); 397 | }); 398 | 399 | BUTTON_REPORT_ELEMENT.addEventListener("click", async () => { 400 | // Ask the user for a reason 401 | const reason = prompt("Reason:"); 402 | if (!reason) { 403 | return; 404 | } 405 | 406 | // Try to report the paste 407 | const response = await API.reportPaste(PASTE_ID, reason); 408 | if (!response.ok) { 409 | Notifications.error("Error while reporting paste: " + await response.text() + ""); 410 | return; 411 | } 412 | 413 | // Show the response message 414 | const data = await response.json(); 415 | if (!data.success) { 416 | Notifications.error("Error while reporting paste: " + data.message + ""); 417 | return; 418 | } 419 | Notifications.success(data.message); 420 | }); 421 | } 422 | 423 | // 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is 424 | function insertTextAtCursor(element, text) { 425 | let value = element.value, endIndex, range, doc = element.ownerDocument; 426 | if (typeof element.selectionStart == "number" 427 | && typeof element.selectionEnd == "number") { 428 | endIndex = element.selectionEnd; 429 | element.value = value.slice(0, endIndex) + text + value.slice(endIndex); 430 | element.selectionStart = element.selectionEnd = endIndex + text.length; 431 | } else if (doc.selection != "undefined" && doc.selection.createRange) { 432 | element.focus(); 433 | range = doc.selection.createRange(); 434 | range.collapse(false); 435 | range.text = text; 436 | range.select(); 437 | } 438 | } -------------------------------------------------------------------------------- /web/assets/libs/aesjs/aes.min.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(t){function e(t){return parseInt(t)===t}function r(t){if(!e(t.length))return!1;for(var r=0;r255)return!1;return!0}function i(t,i){if(t.buffer&&ArrayBuffer.isView(t)&&"Uint8Array"===t.name)return i&&(t=t.slice?t.slice():Array.prototype.slice.call(t)),t;if(Array.isArray(t)){if(!r(t))throw new Error("Array contains invalid value: "+t);return new Uint8Array(t)}if(e(t.length)&&r(t))return new Uint8Array(t);throw new Error("unsupported array-like object")}function n(t){return new Uint8Array(t)}function s(t,e,r,i,n){null==i&&null==n||(t=t.slice?t.slice(i,n):Array.prototype.slice.call(t,i,n)),e.set(t,r)}var o,h={toBytes:function(t){var e=[],r=0;for(t=encodeURI(t);r191&&i<224?(e.push(String.fromCharCode((31&i)<<6|63&t[r+1])),r+=2):(e.push(String.fromCharCode((15&i)<<12|(63&t[r+1])<<6|63&t[r+2])),r+=3)}return e.join("")}},a=(o="0123456789abcdef",{toBytes:function(t){for(var e=[],r=0;r>4]+o[15&i])}return e.join("")}}),f={16:10,24:12,32:14},c=[1,2,4,8,16,32,64,128,27,54,108,216,171,77,154,47,94,188,99,198,151,53,106,212,179,125,250,239,197,145],u=[99,124,119,123,242,107,111,197,48,1,103,43,254,215,171,118,202,130,201,125,250,89,71,240,173,212,162,175,156,164,114,192,183,253,147,38,54,63,247,204,52,165,229,241,113,216,49,21,4,199,35,195,24,150,5,154,7,18,128,226,235,39,178,117,9,131,44,26,27,110,90,160,82,59,214,179,41,227,47,132,83,209,0,237,32,252,177,91,106,203,190,57,74,76,88,207,208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,168,81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,210,205,12,19,236,95,151,68,23,196,167,126,61,100,93,25,115,96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,219,224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,121,231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,8,186,120,37,46,28,166,180,198,232,221,116,31,75,189,139,138,112,62,181,102,72,3,246,14,97,53,87,185,134,193,29,158,225,248,152,17,105,217,142,148,155,30,135,233,206,85,40,223,140,161,137,13,191,230,66,104,65,153,45,15,176,84,187,22],l=[82,9,106,213,48,54,165,56,191,64,163,158,129,243,215,251,124,227,57,130,155,47,255,135,52,142,67,68,196,222,233,203,84,123,148,50,166,194,35,61,238,76,149,11,66,250,195,78,8,46,161,102,40,217,36,178,118,91,162,73,109,139,209,37,114,248,246,100,134,104,152,22,212,164,92,204,93,101,182,146,108,112,72,80,253,237,185,218,94,21,70,87,167,141,157,132,144,216,171,0,140,188,211,10,247,228,88,5,184,179,69,6,208,44,30,143,202,63,15,2,193,175,189,3,1,19,138,107,58,145,17,65,79,103,220,234,151,242,207,206,240,180,230,115,150,172,116,34,231,173,53,133,226,249,55,232,28,117,223,110,71,241,26,113,29,41,197,137,111,183,98,14,170,24,190,27,252,86,62,75,198,210,121,32,154,219,192,254,120,205,90,244,31,221,168,51,136,7,199,49,177,18,16,89,39,128,236,95,96,81,127,169,25,181,74,13,45,229,122,159,147,201,156,239,160,224,59,77,174,42,245,176,200,235,187,60,131,83,153,97,23,43,4,126,186,119,214,38,225,105,20,99,85,33,12,125],p=[3328402341,4168907908,4000806809,4135287693,4294111757,3597364157,3731845041,2445657428,1613770832,33620227,3462883241,1445669757,3892248089,3050821474,1303096294,3967186586,2412431941,528646813,2311702848,4202528135,4026202645,2992200171,2387036105,4226871307,1101901292,3017069671,1604494077,1169141738,597466303,1403299063,3832705686,2613100635,1974974402,3791519004,1033081774,1277568618,1815492186,2118074177,4126668546,2211236943,1748251740,1369810420,3521504564,4193382664,3799085459,2883115123,1647391059,706024767,134480908,2512897874,1176707941,2646852446,806885416,932615841,168101135,798661301,235341577,605164086,461406363,3756188221,3454790438,1311188841,2142417613,3933566367,302582043,495158174,1479289972,874125870,907746093,3698224818,3025820398,1537253627,2756858614,1983593293,3084310113,2108928974,1378429307,3722699582,1580150641,327451799,2790478837,3117535592,0,3253595436,1075847264,3825007647,2041688520,3059440621,3563743934,2378943302,1740553945,1916352843,2487896798,2555137236,2958579944,2244988746,3151024235,3320835882,1336584933,3992714006,2252555205,2588757463,1714631509,293963156,2319795663,3925473552,67240454,4269768577,2689618160,2017213508,631218106,1269344483,2723238387,1571005438,2151694528,93294474,1066570413,563977660,1882732616,4059428100,1673313503,2008463041,2950355573,1109467491,537923632,3858759450,4260623118,3218264685,2177748300,403442708,638784309,3287084079,3193921505,899127202,2286175436,773265209,2479146071,1437050866,4236148354,2050833735,3362022572,3126681063,840505643,3866325909,3227541664,427917720,2655997905,2749160575,1143087718,1412049534,999329963,193497219,2353415882,3354324521,1807268051,672404540,2816401017,3160301282,369822493,2916866934,3688947771,1681011286,1949973070,336202270,2454276571,201721354,1210328172,3093060836,2680341085,3184776046,1135389935,3294782118,965841320,831886756,3554993207,4068047243,3588745010,2345191491,1849112409,3664604599,26054028,2983581028,2622377682,1235855840,3630984372,2891339514,4092916743,3488279077,3395642799,4101667470,1202630377,268961816,1874508501,4034427016,1243948399,1546530418,941366308,1470539505,1941222599,2546386513,3421038627,2715671932,3899946140,1042226977,2521517021,1639824860,227249030,260737669,3765465232,2084453954,1907733956,3429263018,2420656344,100860677,4160157185,470683154,3261161891,1781871967,2924959737,1773779408,394692241,2579611992,974986535,664706745,3655459128,3958962195,731420851,571543859,3530123707,2849626480,126783113,865375399,765172662,1008606754,361203602,3387549984,2278477385,2857719295,1344809080,2782912378,59542671,1503764984,160008576,437062935,1707065306,3622233649,2218934982,3496503480,2185314755,697932208,1512910199,504303377,2075177163,2824099068,1841019862,739644986],y=[2781242211,2230877308,2582542199,2381740923,234877682,3184946027,2984144751,1418839493,1348481072,50462977,2848876391,2102799147,434634494,1656084439,3863849899,2599188086,1167051466,2636087938,1082771913,2281340285,368048890,3954334041,3381544775,201060592,3963727277,1739838676,4250903202,3930435503,3206782108,4149453988,2531553906,1536934080,3262494647,484572669,2923271059,1783375398,1517041206,1098792767,49674231,1334037708,1550332980,4098991525,886171109,150598129,2481090929,1940642008,1398944049,1059722517,201851908,1385547719,1699095331,1587397571,674240536,2704774806,252314885,3039795866,151914247,908333586,2602270848,1038082786,651029483,1766729511,3447698098,2682942837,454166793,2652734339,1951935532,775166490,758520603,3000790638,4004797018,4217086112,4137964114,1299594043,1639438038,3464344499,2068982057,1054729187,1901997871,2534638724,4121318227,1757008337,0,750906861,1614815264,535035132,3363418545,3988151131,3201591914,1183697867,3647454910,1265776953,3734260298,3566750796,3903871064,1250283471,1807470800,717615087,3847203498,384695291,3313910595,3617213773,1432761139,2484176261,3481945413,283769337,100925954,2180939647,4037038160,1148730428,3123027871,3813386408,4087501137,4267549603,3229630528,2315620239,2906624658,3156319645,1215313976,82966005,3747855548,3245848246,1974459098,1665278241,807407632,451280895,251524083,1841287890,1283575245,337120268,891687699,801369324,3787349855,2721421207,3431482436,959321879,1469301956,4065699751,2197585534,1199193405,2898814052,3887750493,724703513,2514908019,2696962144,2551808385,3516813135,2141445340,1715741218,2119445034,2872807568,2198571144,3398190662,700968686,3547052216,1009259540,2041044702,3803995742,487983883,1991105499,1004265696,1449407026,1316239930,504629770,3683797321,168560134,1816667172,3837287516,1570751170,1857934291,4014189740,2797888098,2822345105,2754712981,936633572,2347923833,852879335,1133234376,1500395319,3084545389,2348912013,1689376213,3533459022,3762923945,3034082412,4205598294,133428468,634383082,2949277029,2398386810,3913789102,403703816,3580869306,2297460856,1867130149,1918643758,607656988,4049053350,3346248884,1368901318,600565992,2090982877,2632479860,557719327,3717614411,3697393085,2249034635,2232388234,2430627952,1115438654,3295786421,2865522278,3633334344,84280067,33027830,303828494,2747425121,1600795957,4188952407,3496589753,2434238086,1486471617,658119965,3106381470,953803233,334231800,3005978776,857870609,3151128937,1890179545,2298973838,2805175444,3056442267,574365214,2450884487,550103529,1233637070,4289353045,2018519080,2057691103,2399374476,4166623649,2148108681,387583245,3664101311,836232934,3330556482,3100665960,3280093505,2955516313,2002398509,287182607,3413881008,4238890068,3597515707,975967766],g=[1671808611,2089089148,2006576759,2072901243,4061003762,1807603307,1873927791,3310653893,810573872,16974337,1739181671,729634347,4263110654,3613570519,2883997099,1989864566,3393556426,2191335298,3376449993,2106063485,4195741690,1508618841,1204391495,4027317232,2917941677,3563566036,2734514082,2951366063,2629772188,2767672228,1922491506,3227229120,3082974647,4246528509,2477669779,644500518,911895606,1061256767,4144166391,3427763148,878471220,2784252325,3845444069,4043897329,1905517169,3631459288,827548209,356461077,67897348,3344078279,593839651,3277757891,405286936,2527147926,84871685,2595565466,118033927,305538066,2157648768,3795705826,3945188843,661212711,2999812018,1973414517,152769033,2208177539,745822252,439235610,455947803,1857215598,1525593178,2700827552,1391895634,994932283,3596728278,3016654259,695947817,3812548067,795958831,2224493444,1408607827,3513301457,0,3979133421,543178784,4229948412,2982705585,1542305371,1790891114,3410398667,3201918910,961245753,1256100938,1289001036,1491644504,3477767631,3496721360,4012557807,2867154858,4212583931,1137018435,1305975373,861234739,2241073541,1171229253,4178635257,33948674,2139225727,1357946960,1011120188,2679776671,2833468328,1374921297,2751356323,1086357568,2408187279,2460827538,2646352285,944271416,4110742005,3168756668,3066132406,3665145818,560153121,271589392,4279952895,4077846003,3530407890,3444343245,202643468,322250259,3962553324,1608629855,2543990167,1154254916,389623319,3294073796,2817676711,2122513534,1028094525,1689045092,1575467613,422261273,1939203699,1621147744,2174228865,1339137615,3699352540,577127458,712922154,2427141008,2290289544,1187679302,3995715566,3100863416,339486740,3732514782,1591917662,186455563,3681988059,3762019296,844522546,978220090,169743370,1239126601,101321734,611076132,1558493276,3260915650,3547250131,2901361580,1655096418,2443721105,2510565781,3828863972,2039214713,3878868455,3359869896,928607799,1840765549,2374762893,3580146133,1322425422,2850048425,1823791212,1459268694,4094161908,3928346602,1706019429,2056189050,2934523822,135794696,3134549946,2022240376,628050469,779246638,472135708,2800834470,3032970164,3327236038,3894660072,3715932637,1956440180,522272287,1272813131,3185336765,2340818315,2323976074,1888542832,1044544574,3049550261,1722469478,1222152264,50660867,4127324150,236067854,1638122081,895445557,1475980887,3117443513,2257655686,3243809217,489110045,2662934430,3778599393,4162055160,2561878936,288563729,1773916777,3648039385,2391345038,2493985684,2612407707,505560094,2274497927,3911240169,3460925390,1442818645,678973480,3749357023,2358182796,2717407649,2306869641,219617805,3218761151,3862026214,1120306242,1756942440,1103331905,2578459033,762796589,252780047,2966125488,1425844308,3151392187,372911126],d=[1667474886,2088535288,2004326894,2071694838,4075949567,1802223062,1869591006,3318043793,808472672,16843522,1734846926,724270422,4278065639,3621216949,2880169549,1987484396,3402253711,2189597983,3385409673,2105378810,4210693615,1499065266,1195886990,4042263547,2913856577,3570689971,2728590687,2947541573,2627518243,2762274643,1920112356,3233831835,3082273397,4261223649,2475929149,640051788,909531756,1061110142,4160160501,3435941763,875846760,2779116625,3857003729,4059105529,1903268834,3638064043,825316194,353713962,67374088,3351728789,589522246,3284360861,404236336,2526454071,84217610,2593830191,117901582,303183396,2155911963,3806477791,3958056653,656894286,2998062463,1970642922,151591698,2206440989,741110872,437923380,454765878,1852748508,1515908788,2694904667,1381168804,993742198,3604373943,3014905469,690584402,3823320797,791638366,2223281939,1398011302,3520161977,0,3991743681,538992704,4244381667,2981218425,1532751286,1785380564,3419096717,3200178535,960056178,1246420628,1280103576,1482221744,3486468741,3503319995,4025428677,2863326543,4227536621,1128514950,1296947098,859002214,2240123921,1162203018,4193849577,33687044,2139062782,1347481760,1010582648,2678045221,2829640523,1364325282,2745433693,1077985408,2408548869,2459086143,2644360225,943212656,4126475505,3166494563,3065430391,3671750063,555836226,269496352,4294908645,4092792573,3537006015,3452783745,202118168,320025894,3974901699,1600119230,2543297077,1145359496,387397934,3301201811,2812801621,2122220284,1027426170,1684319432,1566435258,421079858,1936954854,1616945344,2172753945,1330631070,3705438115,572679748,707427924,2425400123,2290647819,1179044492,4008585671,3099120491,336870440,3739122087,1583276732,185277718,3688593069,3772791771,842159716,976899700,168435220,1229577106,101059084,606366792,1549591736,3267517855,3553849021,2897014595,1650632388,2442242105,2509612081,3840161747,2038008818,3890688725,3368567691,926374254,1835907034,2374863873,3587531953,1313788572,2846482505,1819063512,1448540844,4109633523,3941213647,1701162954,2054852340,2930698567,134748176,3132806511,2021165296,623210314,774795868,471606328,2795958615,3031746419,3334885783,3907527627,3722280097,1953799400,522133822,1263263126,3183336545,2341176845,2324333839,1886425312,1044267644,3048588401,1718004428,1212733584,50529542,4143317495,235803164,1633788866,892690282,1465383342,3115962473,2256965911,3250673817,488449850,2661202215,3789633753,4177007595,2560144171,286339874,1768537042,3654906025,2391705863,2492770099,2610673197,505291324,2273808917,3924369609,3469625735,1431699370,673740880,3755965093,2358021891,2711746649,2307489801,218961690,3217021541,3873845719,1111672452,1751693520,1094828930,2576986153,757954394,252645662,2964376443,1414855848,3149649517,370555436],_=[1374988112,2118214995,437757123,975658646,1001089995,530400753,2902087851,1273168787,540080725,2910219766,2295101073,4110568485,1340463100,3307916247,641025152,3043140495,3736164937,632953703,1172967064,1576976609,3274667266,2169303058,2370213795,1809054150,59727847,361929877,3211623147,2505202138,3569255213,1484005843,1239443753,2395588676,1975683434,4102977912,2572697195,666464733,3202437046,4035489047,3374361702,2110667444,1675577880,3843699074,2538681184,1649639237,2976151520,3144396420,4269907996,4178062228,1883793496,2403728665,2497604743,1383856311,2876494627,1917518562,3810496343,1716890410,3001755655,800440835,2261089178,3543599269,807962610,599762354,33778362,3977675356,2328828971,2809771154,4077384432,1315562145,1708848333,101039829,3509871135,3299278474,875451293,2733856160,92987698,2767645557,193195065,1080094634,1584504582,3178106961,1042385657,2531067453,3711829422,1306967366,2438237621,1908694277,67556463,1615861247,429456164,3602770327,2302690252,1742315127,2968011453,126454664,3877198648,2043211483,2709260871,2084704233,4169408201,0,159417987,841739592,504459436,1817866830,4245618683,260388950,1034867998,908933415,168810852,1750902305,2606453969,607530554,202008497,2472011535,3035535058,463180190,2160117071,1641816226,1517767529,470948374,3801332234,3231722213,1008918595,303765277,235474187,4069246893,766945465,337553864,1475418501,2943682380,4003061179,2743034109,4144047775,1551037884,1147550661,1543208500,2336434550,3408119516,3069049960,3102011747,3610369226,1113818384,328671808,2227573024,2236228733,3535486456,2935566865,3341394285,496906059,3702665459,226906860,2009195472,733156972,2842737049,294930682,1206477858,2835123396,2700099354,1451044056,573804783,2269728455,3644379585,2362090238,2564033334,2801107407,2776292904,3669462566,1068351396,742039012,1350078989,1784663195,1417561698,4136440770,2430122216,775550814,2193862645,2673705150,1775276924,1876241833,3475313331,3366754619,270040487,3902563182,3678124923,3441850377,1851332852,3969562369,2203032232,3868552805,2868897406,566021896,4011190502,3135740889,1248802510,3936291284,699432150,832877231,708780849,3332740144,899835584,1951317047,4236429990,3767586992,866637845,4043610186,1106041591,2144161806,395441711,1984812685,1139781709,3433712980,3835036895,2664543715,1282050075,3240894392,1181045119,2640243204,25965917,4203181171,4211818798,3009879386,2463879762,3910161971,1842759443,2597806476,933301370,1509430414,3943906441,3467192302,3076639029,3776767469,2051518780,2631065433,1441952575,404016761,1942435775,1408749034,1610459739,3745345300,2017778566,3400528769,3110650942,941896748,3265478751,371049330,3168937228,675039627,4279080257,967311729,135050206,3635733660,1683407248,2076935265,3576870512,1215061108,3501741890],v=[1347548327,1400783205,3273267108,2520393566,3409685355,4045380933,2880240216,2471224067,1428173050,4138563181,2441661558,636813900,4233094615,3620022987,2149987652,2411029155,1239331162,1730525723,2554718734,3781033664,46346101,310463728,2743944855,3328955385,3875770207,2501218972,3955191162,3667219033,768917123,3545789473,692707433,1150208456,1786102409,2029293177,1805211710,3710368113,3065962831,401639597,1724457132,3028143674,409198410,2196052529,1620529459,1164071807,3769721975,2226875310,486441376,2499348523,1483753576,428819965,2274680428,3075636216,598438867,3799141122,1474502543,711349675,129166120,53458370,2592523643,2782082824,4063242375,2988687269,3120694122,1559041666,730517276,2460449204,4042459122,2706270690,3446004468,3573941694,533804130,2328143614,2637442643,2695033685,839224033,1973745387,957055980,2856345839,106852767,1371368976,4181598602,1033297158,2933734917,1179510461,3046200461,91341917,1862534868,4284502037,605657339,2547432937,3431546947,2003294622,3182487618,2282195339,954669403,3682191598,1201765386,3917234703,3388507166,0,2198438022,1211247597,2887651696,1315723890,4227665663,1443857720,507358933,657861945,1678381017,560487590,3516619604,975451694,2970356327,261314535,3535072918,2652609425,1333838021,2724322336,1767536459,370938394,182621114,3854606378,1128014560,487725847,185469197,2918353863,3106780840,3356761769,2237133081,1286567175,3152976349,4255350624,2683765030,3160175349,3309594171,878443390,1988838185,3704300486,1756818940,1673061617,3403100636,272786309,1075025698,545572369,2105887268,4174560061,296679730,1841768865,1260232239,4091327024,3960309330,3497509347,1814803222,2578018489,4195456072,575138148,3299409036,446754879,3629546796,4011996048,3347532110,3252238545,4270639778,915985419,3483825537,681933534,651868046,2755636671,3828103837,223377554,2607439820,1649704518,3270937875,3901806776,1580087799,4118987695,3198115200,2087309459,2842678573,3016697106,1003007129,2802849917,1860738147,2077965243,164439672,4100872472,32283319,2827177882,1709610350,2125135846,136428751,3874428392,3652904859,3460984630,3572145929,3593056380,2939266226,824852259,818324884,3224740454,930369212,2801566410,2967507152,355706840,1257309336,4148292826,243256656,790073846,2373340630,1296297904,1422699085,3756299780,3818836405,457992840,3099667487,2135319889,77422314,1560382517,1945798516,788204353,1521706781,1385356242,870912086,325965383,2358957921,2050466060,2388260884,2313884476,4006521127,901210569,3990953189,1014646705,1503449823,1062597235,2031621326,3212035895,3931371469,1533017514,350174575,2256028891,2177544179,1052338372,741876788,1606591296,1914052035,213705253,2334669897,1107234197,1899603969,3725069491,2631447780,2422494913,1635502980,1893020342,1950903388,1120974935],w=[2807058932,1699970625,2764249623,1586903591,1808481195,1173430173,1487645946,59984867,4199882800,1844882806,1989249228,1277555970,3623636965,3419915562,1149249077,2744104290,1514790577,459744698,244860394,3235995134,1963115311,4027744588,2544078150,4190530515,1608975247,2627016082,2062270317,1507497298,2200818878,567498868,1764313568,3359936201,2305455554,2037970062,1047239e3,1910319033,1337376481,2904027272,2892417312,984907214,1243112415,830661914,861968209,2135253587,2011214180,2927934315,2686254721,731183368,1750626376,4246310725,1820824798,4172763771,3542330227,48394827,2404901663,2871682645,671593195,3254988725,2073724613,145085239,2280796200,2779915199,1790575107,2187128086,472615631,3029510009,4075877127,3802222185,4107101658,3201631749,1646252340,4270507174,1402811438,1436590835,3778151818,3950355702,3963161475,4020912224,2667994737,273792366,2331590177,104699613,95345982,3175501286,2377486676,1560637892,3564045318,369057872,4213447064,3919042237,1137477952,2658625497,1119727848,2340947849,1530455833,4007360968,172466556,266959938,516552836,0,2256734592,3980931627,1890328081,1917742170,4294704398,945164165,3575528878,958871085,3647212047,2787207260,1423022939,775562294,1739656202,3876557655,2530391278,2443058075,3310321856,547512796,1265195639,437656594,3121275539,719700128,3762502690,387781147,218828297,3350065803,2830708150,2848461854,428169201,122466165,3720081049,1627235199,648017665,4122762354,1002783846,2117360635,695634755,3336358691,4234721005,4049844452,3704280881,2232435299,574624663,287343814,612205898,1039717051,840019705,2708326185,793451934,821288114,1391201670,3822090177,376187827,3113855344,1224348052,1679968233,2361698556,1058709744,752375421,2431590963,1321699145,3519142200,2734591178,188127444,2177869557,3727205754,2384911031,3215212461,2648976442,2450346104,3432737375,1180849278,331544205,3102249176,4150144569,2952102595,2159976285,2474404304,766078933,313773861,2570832044,2108100632,1668212892,3145456443,2013908262,418672217,3070356634,2594734927,1852171925,3867060991,3473416636,3907448597,2614737639,919489135,164948639,2094410160,2997825956,590424639,2486224549,1723872674,3157750862,3399941250,3501252752,3625268135,2555048196,3673637356,1343127501,4130281361,3599595085,2957853679,1297403050,81781910,3051593425,2283490410,532201772,1367295589,3926170974,895287692,1953757831,1093597963,492483431,3528626907,1446242576,1192455638,1636604631,209336225,344873464,1015671571,669961897,3375740769,3857572124,2973530695,3747192018,1933530610,3464042516,935293895,3454686199,2858115069,1863638845,3683022916,4085369519,3292445032,875313188,1080017571,3279033885,621591778,1233856572,2504130317,24197544,3017672716,3835484340,3247465558,2220981195,3060847922,1551124588,1463996600],m=[4104605777,1097159550,396673818,660510266,2875968315,2638606623,4200115116,3808662347,821712160,1986918061,3430322568,38544885,3856137295,718002117,893681702,1654886325,2975484382,3122358053,3926825029,4274053469,796197571,1290801793,1184342925,3556361835,2405426947,2459735317,1836772287,1381620373,3196267988,1948373848,3764988233,3385345166,3263785589,2390325492,1480485785,3111247143,3780097726,2293045232,548169417,3459953789,3746175075,439452389,1362321559,1400849762,1685577905,1806599355,2174754046,137073913,1214797936,1174215055,3731654548,2079897426,1943217067,1258480242,529487843,1437280870,3945269170,3049390895,3313212038,923313619,679998e3,3215307299,57326082,377642221,3474729866,2041877159,133361907,1776460110,3673476453,96392454,878845905,2801699524,777231668,4082475170,2330014213,4142626212,2213296395,1626319424,1906247262,1846563261,562755902,3708173718,1040559837,3871163981,1418573201,3294430577,114585348,1343618912,2566595609,3186202582,1078185097,3651041127,3896688048,2307622919,425408743,3371096953,2081048481,1108339068,2216610296,0,2156299017,736970802,292596766,1517440620,251657213,2235061775,2933202493,758720310,265905162,1554391400,1532285339,908999204,174567692,1474760595,4002861748,2610011675,3234156416,3693126241,2001430874,303699484,2478443234,2687165888,585122620,454499602,151849742,2345119218,3064510765,514443284,4044981591,1963412655,2581445614,2137062819,19308535,1928707164,1715193156,4219352155,1126790795,600235211,3992742070,3841024952,836553431,1669664834,2535604243,3323011204,1243905413,3141400786,4180808110,698445255,2653899549,2989552604,2253581325,3252932727,3004591147,1891211689,2487810577,3915653703,4237083816,4030667424,2100090966,865136418,1229899655,953270745,3399679628,3557504664,4118925222,2061379749,3079546586,2915017791,983426092,2022837584,1607244650,2118541908,2366882550,3635996816,972512814,3283088770,1568718495,3499326569,3576539503,621982671,2895723464,410887952,2623762152,1002142683,645401037,1494807662,2595684844,1335535747,2507040230,4293295786,3167684641,367585007,3885750714,1865862730,2668221674,2960971305,2763173681,1059270954,2777952454,2724642869,1320957812,2194319100,2429595872,2815956275,77089521,3973773121,3444575871,2448830231,1305906550,4021308739,2857194700,2516901860,3518358430,1787304780,740276417,1699839814,1592394909,2352307457,2272556026,188821243,1729977011,3687994002,274084841,3594982253,3613494426,2701949495,4162096729,322734571,2837966542,1640576439,484830689,1202797690,3537852828,4067639125,349075736,3342319475,4157467219,4255800159,1030690015,1155237496,2951971274,1757691577,607398968,2738905026,499347990,3794078908,1011452712,227885567,2818666809,213114376,3034881240,1455525988,3414450555,850817237,1817998408,3092726480],b=[0,235474187,470948374,303765277,941896748,908933415,607530554,708780849,1883793496,2118214995,1817866830,1649639237,1215061108,1181045119,1417561698,1517767529,3767586992,4003061179,4236429990,4069246893,3635733660,3602770327,3299278474,3400528769,2430122216,2664543715,2362090238,2193862645,2835123396,2801107407,3035535058,3135740889,3678124923,3576870512,3341394285,3374361702,3810496343,3977675356,4279080257,4043610186,2876494627,2776292904,3076639029,3110650942,2472011535,2640243204,2403728665,2169303058,1001089995,899835584,666464733,699432150,59727847,226906860,530400753,294930682,1273168787,1172967064,1475418501,1509430414,1942435775,2110667444,1876241833,1641816226,2910219766,2743034109,2976151520,3211623147,2505202138,2606453969,2302690252,2269728455,3711829422,3543599269,3240894392,3475313331,3843699074,3943906441,4178062228,4144047775,1306967366,1139781709,1374988112,1610459739,1975683434,2076935265,1775276924,1742315127,1034867998,866637845,566021896,800440835,92987698,193195065,429456164,395441711,1984812685,2017778566,1784663195,1683407248,1315562145,1080094634,1383856311,1551037884,101039829,135050206,437757123,337553864,1042385657,807962610,573804783,742039012,2531067453,2564033334,2328828971,2227573024,2935566865,2700099354,3001755655,3168937228,3868552805,3902563182,4203181171,4102977912,3736164937,3501741890,3265478751,3433712980,1106041591,1340463100,1576976609,1408749034,2043211483,2009195472,1708848333,1809054150,832877231,1068351396,766945465,599762354,159417987,126454664,361929877,463180190,2709260871,2943682380,3178106961,3009879386,2572697195,2538681184,2236228733,2336434550,3509871135,3745345300,3441850377,3274667266,3910161971,3877198648,4110568485,4211818798,2597806476,2497604743,2261089178,2295101073,2733856160,2902087851,3202437046,2968011453,3936291284,3835036895,4136440770,4169408201,3535486456,3702665459,3467192302,3231722213,2051518780,1951317047,1716890410,1750902305,1113818384,1282050075,1584504582,1350078989,168810852,67556463,371049330,404016761,841739592,1008918595,775550814,540080725,3969562369,3801332234,4035489047,4269907996,3569255213,3669462566,3366754619,3332740144,2631065433,2463879762,2160117071,2395588676,2767645557,2868897406,3102011747,3069049960,202008497,33778362,270040487,504459436,875451293,975658646,675039627,641025152,2084704233,1917518562,1615861247,1851332852,1147550661,1248802510,1484005843,1451044056,933301370,967311729,733156972,632953703,260388950,25965917,328671808,496906059,1206477858,1239443753,1543208500,1441952575,2144161806,1908694277,1675577880,1842759443,3610369226,3644379585,3408119516,3307916247,4011190502,3776767469,4077384432,4245618683,2809771154,2842737049,3144396420,3043140495,2673705150,2438237621,2203032232,2370213795],E=[0,185469197,370938394,487725847,741876788,657861945,975451694,824852259,1483753576,1400783205,1315723890,1164071807,1950903388,2135319889,1649704518,1767536459,2967507152,3152976349,2801566410,2918353863,2631447780,2547432937,2328143614,2177544179,3901806776,3818836405,4270639778,4118987695,3299409036,3483825537,3535072918,3652904859,2077965243,1893020342,1841768865,1724457132,1474502543,1559041666,1107234197,1257309336,598438867,681933534,901210569,1052338372,261314535,77422314,428819965,310463728,3409685355,3224740454,3710368113,3593056380,3875770207,3960309330,4045380933,4195456072,2471224067,2554718734,2237133081,2388260884,3212035895,3028143674,2842678573,2724322336,4138563181,4255350624,3769721975,3955191162,3667219033,3516619604,3431546947,3347532110,2933734917,2782082824,3099667487,3016697106,2196052529,2313884476,2499348523,2683765030,1179510461,1296297904,1347548327,1533017514,1786102409,1635502980,2087309459,2003294622,507358933,355706840,136428751,53458370,839224033,957055980,605657339,790073846,2373340630,2256028891,2607439820,2422494913,2706270690,2856345839,3075636216,3160175349,3573941694,3725069491,3273267108,3356761769,4181598602,4063242375,4011996048,3828103837,1033297158,915985419,730517276,545572369,296679730,446754879,129166120,213705253,1709610350,1860738147,1945798516,2029293177,1239331162,1120974935,1606591296,1422699085,4148292826,4233094615,3781033664,3931371469,3682191598,3497509347,3446004468,3328955385,2939266226,2755636671,3106780840,2988687269,2198438022,2282195339,2501218972,2652609425,1201765386,1286567175,1371368976,1521706781,1805211710,1620529459,2105887268,1988838185,533804130,350174575,164439672,46346101,870912086,954669403,636813900,788204353,2358957921,2274680428,2592523643,2441661558,2695033685,2880240216,3065962831,3182487618,3572145929,3756299780,3270937875,3388507166,4174560061,4091327024,4006521127,3854606378,1014646705,930369212,711349675,560487590,272786309,457992840,106852767,223377554,1678381017,1862534868,1914052035,2031621326,1211247597,1128014560,1580087799,1428173050,32283319,182621114,401639597,486441376,768917123,651868046,1003007129,818324884,1503449823,1385356242,1333838021,1150208456,1973745387,2125135846,1673061617,1756818940,2970356327,3120694122,2802849917,2887651696,2637442643,2520393566,2334669897,2149987652,3917234703,3799141122,4284502037,4100872472,3309594171,3460984630,3545789473,3629546796,2050466060,1899603969,1814803222,1730525723,1443857720,1560382517,1075025698,1260232239,575138148,692707433,878443390,1062597235,243256656,91341917,409198410,325965383,3403100636,3252238545,3704300486,3620022987,3874428392,3990953189,4042459122,4227665663,2460449204,2578018489,2226875310,2411029155,3198115200,3046200461,2827177882,2743944855],C=[0,218828297,437656594,387781147,875313188,958871085,775562294,590424639,1750626376,1699970625,1917742170,2135253587,1551124588,1367295589,1180849278,1265195639,3501252752,3720081049,3399941250,3350065803,3835484340,3919042237,4270507174,4085369519,3102249176,3051593425,2734591178,2952102595,2361698556,2177869557,2530391278,2614737639,3145456443,3060847922,2708326185,2892417312,2404901663,2187128086,2504130317,2555048196,3542330227,3727205754,3375740769,3292445032,3876557655,3926170974,4246310725,4027744588,1808481195,1723872674,1910319033,2094410160,1608975247,1391201670,1173430173,1224348052,59984867,244860394,428169201,344873464,935293895,984907214,766078933,547512796,1844882806,1627235199,2011214180,2062270317,1507497298,1423022939,1137477952,1321699145,95345982,145085239,532201772,313773861,830661914,1015671571,731183368,648017665,3175501286,2957853679,2807058932,2858115069,2305455554,2220981195,2474404304,2658625497,3575528878,3625268135,3473416636,3254988725,3778151818,3963161475,4213447064,4130281361,3599595085,3683022916,3432737375,3247465558,3802222185,4020912224,4172763771,4122762354,3201631749,3017672716,2764249623,2848461854,2331590177,2280796200,2431590963,2648976442,104699613,188127444,472615631,287343814,840019705,1058709744,671593195,621591778,1852171925,1668212892,1953757831,2037970062,1514790577,1463996600,1080017571,1297403050,3673637356,3623636965,3235995134,3454686199,4007360968,3822090177,4107101658,4190530515,2997825956,3215212461,2830708150,2779915199,2256734592,2340947849,2627016082,2443058075,172466556,122466165,273792366,492483431,1047239e3,861968209,612205898,695634755,1646252340,1863638845,2013908262,1963115311,1446242576,1530455833,1277555970,1093597963,1636604631,1820824798,2073724613,1989249228,1436590835,1487645946,1337376481,1119727848,164948639,81781910,331544205,516552836,1039717051,821288114,669961897,719700128,2973530695,3157750862,2871682645,2787207260,2232435299,2283490410,2667994737,2450346104,3647212047,3564045318,3279033885,3464042516,3980931627,3762502690,4150144569,4199882800,3070356634,3121275539,2904027272,2686254721,2200818878,2384911031,2570832044,2486224549,3747192018,3528626907,3310321856,3359936201,3950355702,3867060991,4049844452,4234721005,1739656202,1790575107,2108100632,1890328081,1402811438,1586903591,1233856572,1149249077,266959938,48394827,369057872,418672217,1002783846,919489135,567498868,752375421,209336225,24197544,376187827,459744698,945164165,895287692,574624663,793451934,1679968233,1764313568,2117360635,1933530610,1343127501,1560637892,1243112415,1192455638,3704280881,3519142200,3336358691,3419915562,3907448597,3857572124,4075877127,4294704398,3029510009,3113855344,2927934315,2744104290,2159976285,2377486676,2594734927,2544078150],z=[0,151849742,303699484,454499602,607398968,758720310,908999204,1059270954,1214797936,1097159550,1517440620,1400849762,1817998408,1699839814,2118541908,2001430874,2429595872,2581445614,2194319100,2345119218,3034881240,3186202582,2801699524,2951971274,3635996816,3518358430,3399679628,3283088770,4237083816,4118925222,4002861748,3885750714,1002142683,850817237,698445255,548169417,529487843,377642221,227885567,77089521,1943217067,2061379749,1640576439,1757691577,1474760595,1592394909,1174215055,1290801793,2875968315,2724642869,3111247143,2960971305,2405426947,2253581325,2638606623,2487810577,3808662347,3926825029,4044981591,4162096729,3342319475,3459953789,3576539503,3693126241,1986918061,2137062819,1685577905,1836772287,1381620373,1532285339,1078185097,1229899655,1040559837,923313619,740276417,621982671,439452389,322734571,137073913,19308535,3871163981,4021308739,4104605777,4255800159,3263785589,3414450555,3499326569,3651041127,2933202493,2815956275,3167684641,3049390895,2330014213,2213296395,2566595609,2448830231,1305906550,1155237496,1607244650,1455525988,1776460110,1626319424,2079897426,1928707164,96392454,213114376,396673818,514443284,562755902,679998e3,865136418,983426092,3708173718,3557504664,3474729866,3323011204,4180808110,4030667424,3945269170,3794078908,2507040230,2623762152,2272556026,2390325492,2975484382,3092726480,2738905026,2857194700,3973773121,3856137295,4274053469,4157467219,3371096953,3252932727,3673476453,3556361835,2763173681,2915017791,3064510765,3215307299,2156299017,2307622919,2459735317,2610011675,2081048481,1963412655,1846563261,1729977011,1480485785,1362321559,1243905413,1126790795,878845905,1030690015,645401037,796197571,274084841,425408743,38544885,188821243,3613494426,3731654548,3313212038,3430322568,4082475170,4200115116,3780097726,3896688048,2668221674,2516901860,2366882550,2216610296,3141400786,2989552604,2837966542,2687165888,1202797690,1320957812,1437280870,1554391400,1669664834,1787304780,1906247262,2022837584,265905162,114585348,499347990,349075736,736970802,585122620,972512814,821712160,2595684844,2478443234,2293045232,2174754046,3196267988,3079546586,2895723464,2777952454,3537852828,3687994002,3234156416,3385345166,4142626212,4293295786,3841024952,3992742070,174567692,57326082,410887952,292596766,777231668,660510266,1011452712,893681702,1108339068,1258480242,1343618912,1494807662,1715193156,1865862730,1948373848,2100090966,2701949495,2818666809,3004591147,3122358053,2235061775,2352307457,2535604243,2653899549,3915653703,3764988233,4219352155,4067639125,3444575871,3294430577,3746175075,3594982253,836553431,953270745,600235211,718002117,367585007,484830689,133361907,251657213,2041877159,1891211689,1806599355,1654886325,1568718495,1418573201,1335535747,1184342925];function S(t){for(var e=[],r=0;r>2,this._Ke[r][e%4]=s[e],this._Kd[t-r][e%4]=s[e];for(var o,h=0,a=n;a>16&255]<<24^u[o>>8&255]<<16^u[255&o]<<8^u[o>>24&255]^c[h]<<24,h+=1,8!=n)for(e=1;e>8&255]<<8^u[o>>16&255]<<16^u[o>>24&255]<<24;for(e=n/2+1;e>2,p=a%4,this._Ke[l][p]=s[e],this._Kd[t-l][p]=s[e++],a++}for(var l=1;l>24&255]^E[o>>16&255]^C[o>>8&255]^z[255&o]},A.prototype.encrypt=function(t){if(16!=t.length)throw new Error("invalid plaintext size (must be 16 bytes)");for(var e=this._Ke.length-1,r=[0,0,0,0],i=S(t),s=0;s<4;s++)i[s]^=this._Ke[0][s];for(var o=1;o>24&255]^y[i[(s+1)%4]>>16&255]^g[i[(s+2)%4]>>8&255]^d[255&i[(s+3)%4]]^this._Ke[o][s];i=r.slice()}var h,a=n(16);for(s=0;s<4;s++)h=this._Ke[e][s],a[4*s]=255&(u[i[s]>>24&255]^h>>24),a[4*s+1]=255&(u[i[(s+1)%4]>>16&255]^h>>16),a[4*s+2]=255&(u[i[(s+2)%4]>>8&255]^h>>8),a[4*s+3]=255&(u[255&i[(s+3)%4]]^h);return a},A.prototype.decrypt=function(t){if(16!=t.length)throw new Error("invalid ciphertext size (must be 16 bytes)");for(var e=this._Kd.length-1,r=[0,0,0,0],i=S(t),s=0;s<4;s++)i[s]^=this._Kd[0][s];for(var o=1;o>24&255]^v[i[(s+3)%4]>>16&255]^w[i[(s+2)%4]>>8&255]^m[255&i[(s+1)%4]]^this._Kd[o][s];i=r.slice()}var h,a=n(16);for(s=0;s<4;s++)h=this._Kd[e][s],a[4*s]=255&(l[i[s]>>24&255]^h>>24),a[4*s+1]=255&(l[i[(s+3)%4]>>16&255]^h>>16),a[4*s+2]=255&(l[i[(s+2)%4]>>8&255]^h>>8),a[4*s+3]=255&(l[255&i[(s+1)%4]]^h);return a};var K=function(t){if(!(this instanceof K))throw Error("AES must be instanitated with `new`");this.description="Electronic Code Block",this.name="ecb",this._aes=new A(t)};K.prototype.encrypt=function(t){if((t=i(t)).length%16!=0)throw new Error("invalid plaintext size (must be multiple of 16 bytes)");for(var e=n(t.length),r=n(16),o=0;o=0;--e)this._counter[e]=t%256,t>>=8},P.prototype.setBytes=function(t){if(16!=(t=i(t,!0)).length)throw new Error("invalid counter bytes size (must be 16 bytes)");this._counter=t},P.prototype.increment=function(){for(var t=15;t>=0;t--){if(255!==this._counter[t]){this._counter[t]++;break}this._counter[t]=0}};var R=function(t,e){if(!(this instanceof R))throw Error("AES must be instanitated with `new`");this.description="Counter",this.name="ctr",e instanceof P||(e=new P(e)),this._counter=e,this._remainingCounter=null,this._remainingCounterIndex=16,this._aes=new A(t)};R.prototype.encrypt=function(t){for(var e=i(t,!0),r=0;r16)throw new Error("PKCS#7 padding byte out of range");for(var r=t.length-e,o=0;o