Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:ph03nix:tools
pasta
pasta-0.7.2.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File pasta-0.7.2.obscpio of Package pasta
07070100000000000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001400000000pasta-0.7.2/.github07070100000001000081A400000000000000000000000164BFCD7C0000007A000000000000000000000000000000000000002300000000pasta-0.7.2/.github/dependabot.yml--- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" 07070100000002000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001E00000000pasta-0.7.2/.github/workflows07070100000003000081A400000000000000000000000164BFCD7C000002F9000000000000000000000000000000000000002900000000pasta-0.7.2/.github/workflows/docker.yml--- name: docker image 'on': release: types: [published] jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: grisu48/pasta:latest 07070100000004000081A400000000000000000000000164BFCD7C0000047F000000000000000000000000000000000000002700000000pasta-0.7.2/.github/workflows/ghcr.yml--- # See https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages name: Create and publish container 'on': release: types: [published] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: github-image: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v3 - name: Log in to the Container registry uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image uses: docker/build-push-action@v4 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }}07070100000005000081A400000000000000000000000164BFCD7C000001BF000000000000000000000000000000000000002900000000pasta-0.7.2/.github/workflows/pastad.yml--- name: pastad 'on': push jobs: pastad: name: pasta server runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup go uses: actions/setup-go@v2 with: go-version: '1.16' - name: Install requirements run: make requirements - name: Compile binaries run: make pastad pasta - name: Run tests run: make test 07070100000006000081A400000000000000000000000164BFCD7C000001A9000000000000000000000000000000000000001700000000pasta-0.7.2/.gitignore# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib /pastad /pasta cmd/pastad/pastad cmd/pasta/pasta # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # vscode .vscode __debug_bin # data files, directories and databases *.db bins *.toml pasta_test /pastas 07070100000007000081A400000000000000000000000164BFCD7C00000133000000000000000000000000000000000000001A00000000pasta-0.7.2/ContainerfileFROM registry.suse.com/bci/golang AS build-env WORKDIR /app COPY . /app RUN cd /app && make requirements && make pastad-static FROM scratch WORKDIR /data COPY --from=build-env /app/pastad /app/mime.types /app/ ENTRYPOINT ["/app/pastad", "-m", "/app/mime.types", "-c", "/data/pastad.toml"] VOLUME ["/data"] 070701000000080000A1FF00000000000000000000000164BFCE090000000D000000000000000000000000000000000000001700000000pasta-0.7.2/DockerfileContainerfile07070100000009000081A400000000000000000000000164BFCD7C00000433000000000000000000000000000000000000001400000000pasta-0.7.2/LICENSEMIT License Copyright (c) 2020 Felix Niederwanger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 0707010000000A000081A400000000000000000000000164BFCD7C000002CB000000000000000000000000000000000000001500000000pasta-0.7.2/Makefiledefault: all all: pasta pastad static: pasta-static pastad-static .PHONY: all test clean requirements: go get github.com/BurntSushi/toml go get github.com/akamensky/argparse pasta: cmd/pasta/*.go go build -o pasta $^ pastad: cmd/pastad/*.go go build -o pastad $^ pasta-static: cmd/pasta/*.go CGO_ENABLED=0 go build -ldflags="-w -s" -o pasta $^ pastad-static: cmd/pastad/*.go CGO_ENABLED=0 go build -ldflags="-w -s" -o pastad $^ test: pastad pasta go test ./... # TODO: This syntax is horrible :-) bash -c 'cd test && ./test.sh' container-docker: Containerfile pasta pastad docker build . -t feldspaten.org/pasta container-podman: Containerfile pasta pastad podman build . -t feldspaten.org/pasta 0707010000000B000081A400000000000000000000000164BFCD7C0000103E000000000000000000000000000000000000001600000000pasta-0.7.2/README.md![Build status badge](https://github.com/grisu48/pasta/workflows/pastad/badge.svg) # pasta Stupid simple pastebin service written in go. The aim of this project is to create a simple pastebin service for self-hosting. pasta is self-contained, this means it does not need any additional services, e.g. a database to function. All it needs is a data directory and a config `toml` file and it will work. This README contains the most important information. See the [docs](docs/index.md) folder for more documentation, e.g. the [getting-started](docs/getting-started.md) guide. ## Run as container (podman/docker) The easiest way of self-hosting a `pasta` server is via the provided container from `ghcr.io/grisu48/pasta:latest`. Setup your own `pasta` instance is as easy as: * Create your `data` directory (holds config + data) * Create a [pastad.toml](pastad.toml.example) file therein * Start the container, mount the `data` directory as `/data` and publish port `8199` * Configure your reverse proxy (e.g. `nginx`) to forward requests to the `pasta` container Assuming you want your data directory be e.g. `/srv/pasta`, prepare your server: mkdir /srv/pasta cp pastad.toml.example /srv/pastsa/pastad.toml $EDITOR /srv/pastsa/pastad.toml # Modify the configuration to your needs And then create and run your container with your preferred container engine: docker container run -d --name pasta -v /srv/pasta:/data -p 127.0.0.1:8199:8199 ghcr.io/grisu48/pasta podman container run -d --name pasta -v /srv/pasta:/data -p 127.0.0.1:8199:8199 ghcr.io/grisu48/pasta `pasta` listens here on port 8199 and all you need to do is to configure your reverse proxy (e.g. `nginx`) accordingly: ```nginx server { listen 80; listen [::]:80; server_name my-awesome-pasta.server; client_max_body_size 32M; location / { proxy_pass http://127.0.0.1:8199/; } } ``` Note that the good old [dockerhub image](https://hub.docker.com/r/grisu48/pasta/) is deprecated. It still gets updates but will be removed one fine day. The container runs fine as rootless container (podman). ### environment variables In addition to the config file, `pastad` can also be configured via environmental variables. This might be useful for running pasta as a container without a dedicated config file. Supported environmental variables are: | Key | Description | |-----|-------------| | `PASTA_BASEURL` | Base URL for the pasta instance | | `PASTA_PASTADIR` | Data directory for pastas | | `PASTA_BINDADDR` | Address to bind the server to | | `PASTA_MAXSIZE` | Maximum size (in Bytes) for new pastas | | `PASTA_CHARACTERS` | Number of characters for new pastas | | `PASTA_MIMEFILE` | MIME file | | `PASTA_EXPIRE` | Default expiration time (in seconds) | | `PASTA_CLEANUP` | Seconds between cleanup cycles | | `PASTA_REQUESTDELAY` | Delay between requests from the same host in milliseconds | | `PASTA_PUBLICPASTAS` | Number of public pastas to be displayed | ### macros The `BASEURL` setting, defined either via configuration file or via the `PASTA_BASEURL` environment variable, supports custom macros, that should help you in various scenarios. Macros are pre-defined strings, which will be replaced. The following macros are currently supported | Macro | Replaced with | Example | | `$hostname` | Local hostname | `localhost` | A usage example would be to e.g. define the following in your local `pastad.conf` ```toml BaseURL = "http://$hostname:8199" # base URL as used within pasta ``` # Usage Assuing the server runs on http://localhost:8199, you can use the `pasta` CLI tool (See below) or `curl`: curl -X POST 'http://localhost:8199' --data-binary @README.md ## pasta CLI `pasta` is the CLI utility for making the creation of a pastas (i.e. files submitted to a pasta server) as easy as possible. For instance, if you want to push the `README.md` file and create a pasta out of it: pasta README.md pasta -r http://localhost:8199 REAME.md # Define a custom remote server `pasta` reads the config from `~/.pasta.toml` (see the [example file](pasta.toml.example)) 0707010000000C000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001000000000pasta-0.7.2/cmd0707010000000D000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001600000000pasta-0.7.2/cmd/pasta0707010000000E000081A400000000000000000000000164BFCD7C000030B8000000000000000000000000000000000000001F00000000pasta-0.7.2/cmd/pasta/pasta.go/* * pasta client */ package main import ( "bufio" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" "github.com/BurntSushi/toml" ) const VERSION = "0.7.1" type Config struct { RemoteHost string `toml:"RemoteHost"` RemoteHosts []RemoteHost `toml:"Remote"` } type RemoteHost struct { URL string `toml:"url"` // URL of the remote host Alias string `toml:"alias"` // Alias for the remote host Aliases []string `toml:"aliases"` // List of additional aliases for the remote host } var cf Config // Search for the given remote alias. Returns true and the remote if found, otherwise false and an empty instance func (cf *Config) FindRemoteAlias(remote string) (bool, RemoteHost) { for _, remote := range cf.RemoteHosts { if cf.RemoteHost == remote.Alias { return true, remote } for _, alias := range remote.Aliases { if cf.RemoteHost == alias { return true, remote } } } var ret RemoteHost return false, ret } /* http error instance */ type HttpError struct { err string StatusCode int } func (e *HttpError) Error() string { return e.err } func FileExists(filename string) bool { _, err := os.Stat(filename) if err != nil { return false } return !os.IsNotExist(err) } func usage() { fmt.Printf("Usage: %s [OPTIONS] [FILE,[FILE2,...]]\n\n", os.Args[0]) fmt.Println("OPTIONS") fmt.Println(" -h, --help Print this help message") fmt.Println(" -r, --remote HOST Define remote host or alias (Default: http://localhost:8199)") fmt.Println(" -c, --config FILE Define config file (Default: ~/.pasta.toml)") fmt.Println(" -f, --file FILE Send FILE to server") fmt.Println("") fmt.Println(" --ls, --list List known pasta pushes") fmt.Println(" --gc Garbage collector (clean expired pastas)") fmt.Println(" --version Show client version") fmt.Println("") fmt.Println("One or more files can be pushed to the server.") fmt.Println("If no file is given, the input from stdin will be pushed.") } func push(filename string, mime string, src io.Reader) (Pasta, error) { pasta := Pasta{} client := &http.Client{} // For compatability reasons, set the return format in URL and header for some time req, err := http.NewRequest("POST", cf.RemoteHost+"?ret=json", src) if err != nil { return pasta, err } req.Header.Set("Return-Format", "json") if mime != "" { req.Header.Set("Content-Type", mime) } if filename != "" { req.Header.Set("Filename", filename) } resp, err := client.Do(req) if err != nil { return pasta, err } defer resp.Body.Close() if resp.StatusCode != 200 { return pasta, fmt.Errorf("http status code: %d", resp.StatusCode) } pasta.Date = time.Now().Unix() err = json.NewDecoder(resp.Body).Decode(&pasta) if err != nil { return pasta, err } return pasta, nil } func httpRequest(url string, method string) error { client := &http.Client{} req, err := http.NewRequest(method, url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } if resp.StatusCode == 200 { return nil } else { // Try to fetch a small error message buf := make([]byte, 200) n, err := resp.Body.Read(buf) if err != nil || n == 0 || n >= 200 { return &HttpError{err: fmt.Sprintf("http code %d", resp.StatusCode), StatusCode: resp.StatusCode} } return &HttpError{err: fmt.Sprintf("http code %d: %s", resp.StatusCode, string(buf)), StatusCode: resp.StatusCode} } } func rm(pasta Pasta) error { url := fmt.Sprintf("%s?token=%s", pasta.Url, pasta.Token) if err := httpRequest(url, "DELETE"); err != nil { // Ignore 404 errors, because that means that the pasta is remove on the server (e.g. expired) if strings.HasPrefix(err.Error(), "http code 404") { return nil } return err } return nil } func getFilename(filename string) string { i := strings.LastIndex(filename, "/") if i < 0 { return filename } else { return filename[i+1:] } } /* Try to parse an integer range (1..2 or 5-9) - returns the range and a boolean indicating, if such a range could have been parsed */ func tryParseRange(txt string) (int, int, bool) { if txt == "" { return 0, 0, false } if i := strings.Index(txt, "-"); i > 0 { if i == 0 || i >= len(txt)-1 { // Incomplete range return 0, 0, false } l, r := txt[:i], txt[i+1:] // Try to parse i, err := strconv.Atoi(l) if err != nil { return 0, 0, false } j, err := strconv.Atoi(r) if err != nil { return 0, 0, false } return i, j, true } if i := strings.Index(txt, ".."); i > 1 { if i == 0 || i >= len(txt)-2 { // Incomplete range return 0, 0, false } l, r := txt[:i], txt[i+2:] // Try to parse i, err := strconv.Atoi(l) if err != nil { return 0, 0, false } j, err := strconv.Atoi(r) if err != nil { return 0, 0, false } return i, j, true } return 0, 0, false } func main() { cf.RemoteHost = "http://localhost:8199" action := "" // Load configuration file if possible homeDir, _ := os.UserHomeDir() configFile := homeDir + "/.pasta.toml" if FileExists(configFile) { if _, err := toml.DecodeFile(configFile, &cf); err != nil { fmt.Fprintf(os.Stderr, "config-toml file parse error: %s %s\n", configFile, err) } } // Files to be pushed files := make([]string, 0) explicit := false // marking files as explicitly given. This disabled the shortcut commands (ls, rm, gc) // Parse program arguments args := os.Args[1:] for i := 0; i < len(args); i++ { arg := args[i] if arg == "" { continue } if arg[0] == '-' { if arg == "-h" || arg == "--help" { usage() os.Exit(0) } else if arg == "-r" || arg == "--remote" { i++ cf.RemoteHost = args[i] } else if arg == "-c" || arg == "--config" { i++ if _, err := toml.DecodeFile(args[i], &cf); err != nil { fmt.Fprintf(os.Stderr, "config-toml file parse error: %s %s\n", configFile, err) } } else if arg == "-f" || arg == "--file" { i++ explicit = true files = append(files, args[i]) } else if arg == "--ls" || arg == "--list" { action = "list" } else if arg == "--rm" || arg == "--remote" || arg == "--delete" { action = "rm" } else if arg == "--gc" { action = "gc" } else if arg == "--version" { fmt.Printf("pasta version %s\n", VERSION) os.Exit(1) } else if arg == "--" { // The rest are filenames if i+1 < len(args) { files = append(files, args[i+1:]...) } i = len(args) continue } else { fmt.Fprintf(os.Stderr, "Invalid argument: %s\n", arg) os.Exit(1) } } else { files = append(files, arg) } } if found, remote := cf.FindRemoteAlias(cf.RemoteHost); found { fmt.Fprintf(os.Stderr, "Alias found: %s for %s\n", cf.RemoteHost, remote.URL) cf.RemoteHost = remote.URL } // Sanity checks if !strings.Contains(cf.RemoteHost, "://") { fmt.Fprintf(os.Stderr, "Invalid remote: %s\n", cf.RemoteHost) os.Exit(1) } // Load stored pastas stor, err := OpenStorage(homeDir + "/.pastas.dat") if err != nil { fmt.Fprintf(os.Stderr, "Cannot open pasta storage: %s\n", err) } if !explicit { // Special action: "pasta ls" list pasta if action == "" && len(files) == 1 && files[0] == "ls" { if FileExists(files[0]) { fmt.Fprintf(os.Stderr, "Ambiguous command %s (file '%s' exists) - please use '-f %s' to upload or --ls to list pastas\n", files[0], files[0], files[0]) os.Exit(1) } action = "list" files = make([]string, 0) } // Special action: "pasta rm" is the same as "pasta --rm" if len(files) > 1 && files[0] == "rm" { if FileExists(files[0]) { fmt.Fprintf(os.Stderr, "Ambiguous command %s (file '%s' exists) - please use '-f %s' to upload or --rm to remove pastas\n", files[0], files[0], files[0]) os.Exit(1) } action = "rm" files = files[1:] } // Special action: "pasta gc" is the same as "pasta --gc" if len(files) == 1 && (files[0] == "gc" || files[0] == "clean" || files[0] == "expire") { if FileExists(files[0]) { fmt.Fprintf(os.Stderr, "Ambiguous command %s (file '%s' exists) - please use '-f %s' to upload or --gc to cleanup expired pastas\n", files[0], files[0], files[0]) os.Exit(1) } action = "gc" files = files[1:] } } if action == "push" || action == "" { if len(files) > 0 { for _, filename := range files { file, err := os.OpenFile(filename, os.O_RDONLY, 0400) if err != nil { fmt.Fprintf(os.Stderr, "%s: %s\n", filename, err) os.Exit(1) } defer file.Close() if stat, err := file.Stat(); err != nil { fmt.Fprintf(os.Stderr, "%s: %s\n", filename, err) os.Exit(1) } else if stat.Size() == 0 { fmt.Fprintf(os.Stderr, "Skipping empty file %s\n", filename) continue } // Push file f_name := getFilename(filename) pasta, err := push(f_name, "", file) pasta.Filename = f_name if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } if err = stor.Append(pasta); err != nil { fmt.Fprintf(os.Stderr, "Cannot writing pasta to local store: %s\n", err) } // For a single file just print the link if len(files) == 1 { fmt.Printf("%s\n", pasta.Url) } else { fmt.Printf("%s - %s\n", pasta.Filename, pasta.Url) } } } else { fmt.Fprintln(os.Stderr, "Reading from stdin") reader := bufio.NewReader(os.Stdin) pasta, err := push("", "text/plain", reader) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } if err = stor.Append(pasta); err != nil { fmt.Fprintf(os.Stderr, "Cannot writing pasta to local store: %s\n", err) } fmt.Println(pasta.Url) } } else if action == "list" { // list known pastas if len(stor.Pastas) > 0 { fmt.Printf("Id %-30s %-19s %s\n", "Filename", "Date", "URL") for i, pasta := range stor.Pastas { t := time.Unix(pasta.Date, 0) filename := pasta.Filename if filename == "" { filename = "<none>" } fmt.Printf("%-3d %-30s %-19s %s\n", i, filename, t.Format("2006-01-02 15:04:05"), pasta.Url) } } } else if action == "rm" { // remove pastas // List of pastas to be deleted spoiled := make([]Pasta, 0) // Match given pastas and get tokens for _, file := range files { // If it is and integer, take the n-th item if id, err := strconv.Atoi(file); err == nil { if id < 0 || id >= len(stor.Pastas) { fmt.Fprintf(os.Stderr, "Cannot find pasta '%d'\n", id) os.Exit(1) } if id < 0 || id >= len(stor.Pastas) { fmt.Fprintf(os.Stderr, "Pasta index %d out of range\n", id) os.Exit(1) } spoiled = append(spoiled, stor.Pastas[id]) // If it is a range (e.g. 3-4 or 3..4) use the i..j items } else if l, r, found := tryParseRange(file); found { // First ensure that the given string is not a file. Files have precedence if pasta, ok := stor.Get(file); ok { spoiled = append(spoiled, pasta) } else { // Assume it's a range if l < 0 { fmt.Fprintf(os.Stderr, "Pasta index %d out of range\n", l) os.Exit(1) } if r >= len(stor.Pastas) { fmt.Fprintf(os.Stderr, "Pasta index %d out of range\n", r) os.Exit(1) } for i := l; i <= r; i++ { spoiled = append(spoiled, stor.Pastas[i]) } } } else { if pasta, ok := stor.Get(file); ok { spoiled = append(spoiled, pasta) } else { // Stop execution fmt.Fprintf(os.Stderr, "Cannot find pasta '%s'\n", file) os.Exit(1) } } } // Delete found pastas for _, pasta := range spoiled { if err := rm(pasta); err != nil { fmt.Fprintf(os.Stderr, "Error deleting '%s': %s\n", pasta.Url, err) } else { fmt.Printf("Deleted: %s\n", pasta.Url) stor.Remove(pasta.Url, pasta.Token) // Mark as removed for when rewriting storage } } // And re-write storage if err = stor.Write(); err != nil { fmt.Fprintf(os.Stderr, "Error writing to local storage: %s\n", err) } } else if action == "gc" || action == "clean" { // Cleanup happens when loading pastas expired := stor.ExpiredPastas() if expired == 0 { fmt.Println("all good") } else if expired == 1 { fmt.Println("one expired pasta cleared") } else { fmt.Printf("%d expired pastas cleared\n", expired) } } else { fmt.Fprintf(os.Stderr, "Unkown action: %s\n", action) os.Exit(1) } } 0707010000000F000081A400000000000000000000000164BFCD7C00000ECF000000000000000000000000000000000000002100000000pasta-0.7.2/cmd/pasta/storage.gopackage main import ( "bufio" "fmt" "os" "strconv" "strings" "time" ) type Pasta struct { Url string `json:"url"` Token string `json:"token"` Date int64 `json:"date"` Expire int64 `json:"expire"` Filename string `json:"filename"` } type Storage struct { Pastas []Pasta file *os.File filename string expired int // number of expired pastas when loading } /* Format for writing to storage*/ func (pasta *Pasta) format() string { return fmt.Sprintf("%s:%d:%d:%s:%s", pasta.Token, pasta.Date, pasta.Expire, strings.Replace(pasta.Filename, ":", "", -1), pasta.Url) } func OpenStorage(filename string) (Storage, error) { stor := Storage{filename: filename} return stor, stor.Open(filename) } func (stor *Storage) Open(filename string) error { var err error stor.filename = filename stor.file, err = os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0640) if err != nil { return err } stor.Pastas = make([]Pasta, 0) dirty := false // dirty flag used to rewrite the file if some pastas are expired stor.expired = 0 now := time.Now().Unix() // Read file scanner := bufio.NewScanner(stor.file) for scanner.Scan() { if err := scanner.Err(); err != nil { stor.file.Close() stor.file = nil return err } split := strings.Split(scanner.Text(), ":") if len(split) < 5 { continue } pasta := Pasta{Token: split[0], Filename: split[3], Url: strings.Join(split[4:], ":")} pasta.Date, _ = strconv.ParseInt(split[1], 10, 64) pasta.Expire, _ = strconv.ParseInt(split[2], 10, 64) // Don't add expired pastas and mark storage as dirty for re-write in the end if pasta.Expire != 0 && now > pasta.Expire { dirty = true stor.expired++ } else { stor.Pastas = append(stor.Pastas, pasta) } } // Rewrite storage if expired pastas have been removed if dirty { return stor.Write() } return nil } func (stor *Storage) Close() error { if stor.file == nil { return nil } return stor.file.Close() } func (stor *Storage) Append(pasta Pasta) error { if _, err := stor.file.Write([]byte(pasta.format() + "\n")); err != nil { return err } return stor.file.Sync() } /* Rewrite the whole storage file */ func (stor *Storage) Write() error { var err error stor.file.Close() stor.file, err = os.OpenFile(stor.filename, os.O_RDWR|os.O_TRUNC, 0640) if err != nil { return err } for _, pasta := range stor.Pastas { if pasta.Url == "" { continue } _, err = stor.file.Write([]byte(pasta.format() + "\n")) if err != nil { return err } } return stor.file.Sync() } func (stor *Storage) ExpiredPastas() int { return stor.expired } func getPastaId(url string) string { i := strings.LastIndex(url, "/") if i < 0 { return url } return url[i+1:] } func (stor *Storage) Get(id string) (Pasta, bool) { // If the id is a url, check for url match first if strings.Contains(id, "://") { for _, pasta := range stor.Pastas { if pasta.Url == id { return pasta, true } } } // Check for pasta ID only. This needs to happen as second step als url matching has precedence for _, pasta := range stor.Pastas { if pasta.Url == id { return pasta, true } } // Nothing found, return empty pasta return Pasta{}, false } func (stor *Storage) find(url string, token string) int { for i, pasta := range stor.Pastas { if pasta.Url == url && pasta.Token == token { return i } } return -1 } /** Marks the given pasta (given by url and token) as removed from storage. Returns true if the pasta is found, false if not found*/ func (stor *Storage) Remove(url string, token string) bool { i := stor.find(url, token) if i < 0 { return false } after := stor.Pastas[i+1:] stor.Pastas = stor.Pastas[:i] stor.Pastas = append(stor.Pastas, after...) return true } 07070100000010000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001700000000pasta-0.7.2/cmd/pastad07070100000011000081A400000000000000000000000164BFCD7C00000FBE000000000000000000000000000000000000002100000000pasta-0.7.2/cmd/pastad/config.gopackage main import ( "fmt" "os" ) type Config struct { BaseUrl string `toml:"BaseURL"` // Instance base URL PastaDir string `toml:"PastaDir"` // dir where pasta are stored BindAddr string `toml:"BindAddress"` MaxPastaSize int64 `toml:"MaxPastaSize"` // Max bin size in bytes PastaCharacters int `toml:"PastaCharacters"` MimeTypesFile string `toml:"MimeTypes"` // Load mime types from this file DefaultExpire int64 `toml:"Expire"` // Default expire time for a new pasta in seconds CleanupInterval int `toml:"Cleanup"` // Seconds between cleanup cycles RequestDelay int64 `toml:"RequestDelay"` // Required delay between requests in milliseconds PublicPastas int `toml:"PublicPastas"` // Number of pastas to display on public page or 0 to disable } type ParserConfig struct { ConfigFile *string BaseURL *string PastaDir *string BindAddr *string MaxPastaSize *int // parser doesn't support int64 PastaCharacters *int MimeTypesFile *string DefaultExpire *int // parser doesn't support int64 CleanupInterval *int PublicPastas *int } func CreateDefaultConfigfile(filename string) error { hostname, _ := os.Hostname() if hostname == "" { hostname = "localhost" } content := []byte(fmt.Sprintf("BaseURL = 'http://%s:8199'\nBindAddress = ':8199'\nPastaDir = 'pastas'\nMaxPastaSize = 5242880 # 5 MiB\nPastaCharacters = 8\nExpire = 2592000 # 1 month\nCleanup = 3600 # cleanup interval in seconds\nRequestDelay = 2000\nPublicPastas = 0\n", hostname)) file, err := os.Create(filename) if err != nil { return err } defer file.Close() if _, err = file.Write(content); err != nil { return err } if err := file.Chmod(0640); err != nil { return err } return file.Close() } // SetDefaults sets the default values to a config instance func (cf *Config) SetDefaults() { cf.BaseUrl = "http://localhost:8199" cf.PastaDir = "pastas/" cf.BindAddr = "127.0.0.1:8199" cf.MaxPastaSize = 1024 * 1024 * 25 // Default max size: 25 MB cf.PastaCharacters = 8 // Note: Never use less than 8 characters! cf.MimeTypesFile = "mime.types" cf.DefaultExpire = 0 cf.CleanupInterval = 60 * 60 // Default cleanup is once per hour cf.RequestDelay = 0 // By default not spam protection (Assume we are in safe environment) cf.PublicPastas = 0 } // ReadEnv reads the environmental variables and sets the config accordingly func (cf *Config) ReadEnv() { cf.BaseUrl = getenv("PASTA_BASEURL", cf.BaseUrl) cf.PastaDir = getenv("PASTA_PASTADIR", cf.PastaDir) cf.BindAddr = getenv("PASTA_BINDADDR", cf.BindAddr) cf.MaxPastaSize = getenv_i64("PASTA_MAXSIZE", cf.MaxPastaSize) cf.PastaCharacters = getenv_i("PASTA_CHARACTERS", cf.PastaCharacters) cf.MimeTypesFile = getenv("PASTA_MIMEFILE", cf.MimeTypesFile) cf.DefaultExpire = getenv_i64("PASTA_EXPIRE", cf.DefaultExpire) cf.CleanupInterval = getenv_i("PASTA_CLEANUP", cf.CleanupInterval) cf.RequestDelay = getenv_i64("PASTA_REQUESTDELAY", cf.RequestDelay) cf.PublicPastas = getenv_i("PASTA_PUBLICPASTAS", cf.PublicPastas) } func (pc *ParserConfig) ApplyTo(cf *Config) { if pc.BaseURL != nil && *pc.BaseURL != "" { cf.BaseUrl = *pc.BaseURL } if pc.PastaDir != nil && *pc.PastaDir != "" { cf.PastaDir = *pc.PastaDir } if pc.BindAddr != nil && *pc.BindAddr != "" { cf.BindAddr = *pc.BindAddr } if pc.MaxPastaSize != nil && *pc.MaxPastaSize > 0 { cf.MaxPastaSize = int64(*pc.MaxPastaSize) } if pc.PastaCharacters != nil && *pc.PastaCharacters > 0 { cf.PastaCharacters = *pc.PastaCharacters } if pc.MimeTypesFile != nil && *pc.MimeTypesFile != "" { cf.MimeTypesFile = *pc.MimeTypesFile } if pc.DefaultExpire != nil && *pc.DefaultExpire > 0 { cf.DefaultExpire = int64(*pc.DefaultExpire) } if pc.CleanupInterval != nil && *pc.CleanupInterval > 0 { cf.CleanupInterval = *pc.CleanupInterval } if pc.PublicPastas != nil && *pc.PublicPastas > 0 { cf.PublicPastas = *pc.PublicPastas } } 07070100000012000081A400000000000000000000000164BFCD7C00005BA2000000000000000000000000000000000000002100000000pasta-0.7.2/cmd/pastad/pastad.go/* * pasted - stupid simple paste server */ package main import ( "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "strconv" "strings" "sync" "time" "github.com/BurntSushi/toml" "github.com/akamensky/argparse" ) const VERSION = "0.7" var cf Config var bowl PastaBowl var publicPastas []Pasta var mimeExtensions map[string]string var delays map[string]int64 var delayMutex sync.Mutex func SendPasta(pasta Pasta, w http.ResponseWriter) error { file, err := bowl.GetPastaReader(pasta.Id) if err != nil { return err } defer file.Close() w.Header().Set("Content-Disposition", "inline") w.Header().Set("Content-Length", strconv.FormatInt(pasta.Size, 10)) if pasta.Mime != "" { w.Header().Set("Content-Type", pasta.Mime) } if pasta.ContentFilename != "" { w.Header().Set("Filename", pasta.ContentFilename) } _, err = io.Copy(w, file) return err } func removePublicPasta(id string) { copy := make([]Pasta, 0) for _, pasta := range publicPastas { if pasta.Id != id { copy = append(copy, pasta) } } publicPastas = copy } func deletePasta(id string, token string, w http.ResponseWriter) { var pasta Pasta var err error if id == "" || token == "" { goto Invalid } pasta, err = bowl.GetPasta(id) if err != nil { log.Fatalf("Error getting pasta %s: %s", pasta.Id, err) goto ServerError } if pasta.Id == "" { goto NotFound } if pasta.Token == token { err = bowl.DeletePasta(pasta.Id) if err != nil { log.Fatalf("Error deleting pasta %s: %s", pasta.Id, err) goto ServerError } // Also remove from public pastas, if present removePublicPasta(pasta.Id) w.WriteHeader(200) fmt.Fprintf(w, "<html><head><meta http-equiv=\"refresh\" content=\"2; url='%s'\" /></head>\n", cf.BaseUrl) fmt.Fprintf(w, "<body>\n") fmt.Fprintf(w, "<p>OK - Redirecting to <a href=\"/\">main page</a> ... </p>") fmt.Fprintf(w, "\n</body>\n</html>") } else { goto Invalid } return NotFound: w.WriteHeader(404) fmt.Fprintf(w, "pasta not found") return Invalid: w.WriteHeader(403) fmt.Fprintf(w, "Invalid request") return ServerError: w.WriteHeader(500) fmt.Fprintf(w, "server error") } func receive(reader io.Reader, pasta *Pasta) error { buf := make([]byte, 4096) file, err := os.OpenFile(pasta.DiskFilename, os.O_RDWR|os.O_APPEND, 0640) if err != nil { file.Close() return err } defer file.Close() pasta.Size = 0 for pasta.Size < cf.MaxPastaSize { n, err := reader.Read(buf) if (err == nil || err == io.EOF) && n > 0 { if _, err = file.Write(buf[:n]); err != nil { log.Fatalf("Write error while receiving bin: %s", err) return err } pasta.Size += int64(n) } if err != nil { if err == io.EOF { return nil } log.Fatalf("Receive error while receiving bin: %s", err) return err } } return nil } func receiveMultibody(r *http.Request, pasta *Pasta) (io.ReadCloser, bool, error) { public := false filename := "" // Read http headers first value := r.Header.Get("public") if value != "" { public = strBool(value, public) } // If the content length is given, reject immediately if the size is too big size := r.Header.Get("Content-Length") if size != "" { size, err := strconv.ParseInt(size, 10, 64) if err == nil && size > 0 && size > cf.MaxPastaSize { log.Println("Max size exceeded (Content-Length)") return nil, public, errors.New("content size exceeded") } } // Receive multipart form err := r.ParseMultipartForm(cf.MaxPastaSize) if err != nil { return nil, public, err } file, header, err := r.FormFile("file") if err != nil { return nil, public, err } // Read file headers filename = header.Filename if filename != "" { pasta.ContentFilename = filename } // Read form values after headers, as the form values have precedence form := r.MultipartForm values := form.Value if value, ok := values["public"]; ok { if len(value) > 0 { public = strBool(value[0], public) } } // Determine MIME type based on file extension, if present if filename != "" { pasta.Mime = mimeByFilename(filename) } else { pasta.Mime = "application/octet-stream" } return file, public, err } /* Parse expire header value. Returns expire value or 0 on error or invalid settings */ func parseExpire(headerValue []string) int64 { var ret int64 for _, value := range headerValue { if expire, err := strconv.ParseInt(value, 10, 64); err == nil { // No negative values allowed if expire < 0 { return 0 } ret = time.Now().Unix() + int64(expire) } } return ret } /* isMultipart returns true if the given request is multipart form */ func isMultipart(r *http.Request) bool { contentType := r.Header.Get("Content-Type") return contentType == "multipart/form-data" || strings.HasPrefix(contentType, "multipart/form-data;") } func ReceivePasta(r *http.Request) (Pasta, bool, error) { var err error var reader io.ReadCloser pasta := Pasta{Id: ""} public := false // Parse expire if given if cf.DefaultExpire > 0 { pasta.ExpireDate = time.Now().Unix() + cf.DefaultExpire } if expire := parseExpire(r.Header["Expire"]); expire > 0 { pasta.ExpireDate = expire // TODO: Add maximum expiration parameter } pasta.Id = removeNonAlphaNumeric(bowl.GenerateRandomBinId(cf.PastaCharacters)) formRead := true // Read values from the form if isMultipart(r) { // InsertPasta to obtain a filename if err = bowl.InsertPasta(&pasta); err != nil { return pasta, public, err } reader, public, err = receiveMultibody(r, &pasta) if err != nil { bowl.DeletePasta(pasta.Id) pasta.Id = "" return pasta, public, err } } else { // Check if the input is coming from the POST form inputs := r.URL.Query()["input"] if len(inputs) > 0 && inputs[0] == "form" { // Copy reader, as r.FromValue consumes it's contents defer r.Body.Close() if content := r.FormValue("content"); content != "" { reader = io.NopCloser(strings.NewReader(content)) } else { pasta.Id = "" // Empty pasta return pasta, public, nil } } else { reader = r.Body formRead = false } } defer reader.Close() header := r.Header // If the content length is given, reject immediately if the size is too big size := header.Get("Content-Length") if size != "" { size, err := strconv.ParseInt(size, 10, 64) if err == nil && size > 0 && size > cf.MaxPastaSize { log.Println("Max size exceeded (Content-Length)") return pasta, public, errors.New("content size exceeded") } } // Get property. URL parameter has precedence over header prop_get := func(name string) string { var val string if formRead { val = r.FormValue(name) if val != "" { return val } } val = header.Get(name) if val != "" { return val } return "" } // Check if public value := prop_get("public") if value != "" { public = strBool(value, public) } // Apply filename, if present // Due to inconsitent naming between URL and http parameters, we have to check for Filename and filename. URL parameters have precedence filename := prop_get("filename") if filename != "" { pasta.ContentFilename = filename } else { filename := prop_get("Filename") if filename != "" { pasta.ContentFilename = filename } } // InsertPasta sets filename if err = bowl.InsertPasta(&pasta); err != nil { return pasta, public, err } if err := receive(reader, &pasta); err != nil { return pasta, public, err } if pasta.Size >= cf.MaxPastaSize { log.Println("Max size exceeded while receiving bin") return pasta, public, errors.New("content size exceeded") } pasta.Mime = "text/plain" if pasta.Size == 0 { bowl.DeletePasta(pasta.Id) pasta.Id = "" pasta.DiskFilename = "" pasta.Token = "" pasta.ExpireDate = 0 return pasta, public, nil } return pasta, public, nil } /* Delay a request for the given remote if required by spam protection */ func delayIfRequired(remote string) { if cf.RequestDelay == 0 { return } address := extractRemoteIP(remote) now := time.Now().UnixNano() / 1000000 // Timestamp now in milliseconds. This should be fine until 2262 delayMutex.Lock() delay, ok := delays[address] delayMutex.Unlock() if ok { delta := cf.RequestDelay - (now - delay) if delta > 0 { time.Sleep(time.Duration(delta) * time.Millisecond) } } delays[address] = time.Now().UnixNano() / 1000000 // Fresh timestamp } func handlerHead(w http.ResponseWriter, r *http.Request) { var pasta Pasta id, err := ExtractPastaId(r.URL.Path) if err != nil { goto BadRequest } if pasta, err := bowl.GetPasta(id); err != nil { log.Fatalf("Error getting pasta %s: %s", pasta.Id, err) goto ServerError } if pasta.Id == "" { goto NotFound } w.Header().Set("Content-Length", strconv.FormatInt(pasta.Size, 10)) if pasta.Mime != "" { w.Header().Set("Content-Type", pasta.Mime) } if pasta.ExpireDate > 0 { w.Header().Set("Expires", time.Unix(pasta.ExpireDate, 0).Format("2006-01-02-15:04:05")) } w.WriteHeader(200) fmt.Fprintf(w, "OK") return ServerError: w.WriteHeader(500) fmt.Fprintf(w, "server error") return NotFound: w.WriteHeader(404) fmt.Fprintf(w, "pasta not found") return BadRequest: w.WriteHeader(400) if err == nil { fmt.Fprintf(w, "bad request") } else { fmt.Fprintf(w, "%s", err) } return } func handlerPost(w http.ResponseWriter, r *http.Request) { delayIfRequired(r.RemoteAddr) pasta, public, err := ReceivePasta(r) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "server error") log.Printf("Receive error: %s", err) return } else { if pasta.Id == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("empty pasta")) } else { // Save into public pastas, if this is public if public { // Store at the beginning pastas := make([]Pasta, 1) pastas[0] = pasta pastas = append(pastas, publicPastas...) publicPastas = pastas // Crop to maximum allowed number if len(publicPastas) > cf.PublicPastas { publicPastas = publicPastas[len(publicPastas)-cf.PublicPastas:] } if err := bowl.WritePublicPastas(publicPastas); err != nil { log.Printf("Error writing public pastas: %s", err) } } log.Printf("Received pasta %s (%d bytes) from %s", pasta.Id, pasta.Size, r.RemoteAddr) w.WriteHeader(http.StatusOK) url := fmt.Sprintf("%s/%s", cf.BaseUrl, pasta.Id) // Return format. URL has precedence over http heder retFormat := r.Header.Get("Return-Format") retFormats := r.URL.Query()["ret"] if len(retFormats) > 0 { retFormat = retFormats[0] } if retFormat == "html" { // Website as return format fmt.Fprintf(w, "<!doctype html><html><head><title>pasta</title></head>\n") fmt.Fprintf(w, "<body>\n") fmt.Fprintf(w, "<h1>pasta</h1>\n") deleteLink := fmt.Sprintf("%s/delete?id=%s&token=%s", cf.BaseUrl, pasta.Id, pasta.Token) fmt.Fprintf(w, "<p>Pasta: <a href=\"%s\">%s</a> | <a href=\"%s\">🗑️ Delete</a><br/>", url, url, deleteLink) fmt.Fprintf(w, "<pre>") if pasta.ContentFilename != "" { fmt.Fprintf(w, "Filename: %s\n", pasta.ContentFilename) } if pasta.Mime != "" { fmt.Fprintf(w, "Mime-Type: %s\n", pasta.Mime) } if pasta.Size > 0 { fmt.Fprintf(w, "Size: %d B\n", pasta.Size) } if pasta.ExpireDate > 0 { fmt.Fprintf(w, "Expiration: %s\n", time.Unix(pasta.ExpireDate, 0).Format("2006-01-02-15:04:05")) } if public { fmt.Fprintf(w, "Public: yes\n") } fmt.Fprintf(w, "Modification token: %s\n</pre>\n", pasta.Token) fmt.Fprintf(w, "<p>That was fun! Fancy <a href=\"%s\">another one?</a>.</p>\n", cf.BaseUrl) fmt.Fprintf(w, "</body></html>") } else if retFormat == "json" { // Dont use json package, the reply is simple enough to build it on-the-fly reply := fmt.Sprintf("{\"url\":\"%s\",\"token\":\"%s\", \"expire\":%d}", url, pasta.Token, pasta.ExpireDate) w.Write([]byte(reply)) } else { fmt.Fprintf(w, "url: %s\ntoken: %s\n", url, pasta.Token) } } } } func handler(w http.ResponseWriter, r *http.Request) { var err error if r.Method == http.MethodGet { // Check if bin ID is given id, err := ExtractPastaId(r.URL.Path) if err != nil { goto BadRequest } if id == "" { handlerIndex(w, r) } else { pasta, err := bowl.GetPasta(id) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Storage error") log.Fatalf("Storage error: %s", err) return } if pasta.Id == "" { goto NoSuchPasta } else { // Delete expired pasta if present if pasta.Expired() { if err = bowl.DeletePasta(pasta.Id); err != nil { log.Fatalf("Cannot deleted expired pasta %s: %s", pasta.Id, err) } goto NoSuchPasta } if err = SendPasta(pasta, w); err != nil { log.Printf("Error sending pasta %s: %s", pasta.Id, err) } } } } else if r.Method == http.MethodPost || r.Method == http.MethodPut { handlerPost(w, r) } else if r.Method == http.MethodDelete { delayIfRequired(r.RemoteAddr) id, err := ExtractPastaId(r.URL.Path) if err != nil { goto BadRequest } token := takeFirst(r.URL.Query()["token"]) deletePasta(id, token, w) } else if r.Method == http.MethodHead { handlerHead(w, r) } else { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Unsupported method")) } return NoSuchPasta: w.WriteHeader(404) fmt.Fprintf(w, "No pasta\n\nSorry, there is no pasta for this link") return BadRequest: w.WriteHeader(400) if err == nil { fmt.Fprintf(w, "bad request") } else { fmt.Fprintf(w, "%s", err) } } func handlerHealth(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "OK") } func handlerHealthJson(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "{\"status\":\"ok\"}") } func handlerPublic(w http.ResponseWriter, r *http.Request) { if cf.PublicPastas == 0 { w.WriteHeader(400) fmt.Fprintf(w, "public pasta listing is disabled") return } w.WriteHeader(200) w.Write([]byte("<html>\n<head>\n<title>public pastas</title>\n</head>\n<body>")) w.Write([]byte("<h2>public pastas</h2>\n")) w.Write([]byte("<table>\n")) w.Write([]byte("<tr><td>Filename</td><td>Size</td></tr>\n")) for _, pasta := range publicPastas { filename := pasta.ContentFilename if filename == "" { filename = pasta.Id } w.Write([]byte(fmt.Sprintf("<tr><td><a href=\"%s\">%s</a></td><td>%d B</td></tr>\n", pasta.Id, filename, pasta.Size))) } w.Write([]byte("</table>\n")) fmt.Fprintf(w, "<p>The server presents at most %d public pastas.<p>\n", cf.PublicPastas) w.Write([]byte("</body>\n")) } func handlerPublicJson(w http.ResponseWriter, r *http.Request) { if cf.PublicPastas == 0 { w.WriteHeader(400) fmt.Fprintf(w, "public pasta listing is disabled") return } type PublicPasta struct { Filename string `json:"filename"` Size int64 `json:"size"` URL string `json:"url"` } pastas := make([]PublicPasta, 0) for _, pasta := range publicPastas { filename := pasta.ContentFilename if filename == "" { filename = pasta.Id } pastas = append(pastas, PublicPasta{Filename: filename, URL: fmt.Sprintf("%s/%s", cf.BaseUrl, pasta.Id), Size: pasta.Size}) } buf, err := json.Marshal(pastas) if err != nil { log.Printf("json error (public pastas): %s\n", err) goto ServerError } w.WriteHeader(200) w.Write(buf) return ServerError: w.WriteHeader(500) w.Write([]byte("Server error")) } func handlerRobots(w http.ResponseWriter, r *http.Request) { // no robots allowed here fmt.Fprintf(w, "User-agent: *\nDisallow: /\n") } // Delete pasta func handlerDelete(w http.ResponseWriter, r *http.Request) { delayIfRequired(r.RemoteAddr) id := takeFirst(r.URL.Query()["id"]) token := takeFirst(r.URL.Query()["token"]) deletePasta(id, token, w) } func handlerIndex(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "<!doctype html><html><head><title>pasta</title></head>\n") fmt.Fprintf(w, "<body>\n") fmt.Fprintf(w, "<h1>pasta</h1>\n") fmt.Fprintf(w, "<p>pasta is a stupid simple pastebin service for easy usage and deployment.</p>\n") // List public pastas, if enabled and available if cf.PublicPastas > 0 && len(publicPastas) > 0 { fmt.Fprintf(w, "<h2>Public pastas</h2>\n") fmt.Fprintf(w, "<table>\n") fmt.Fprintf(w, "<tr><td>Filename</td><td>Size</td></tr>\n") for _, pasta := range publicPastas { filename := pasta.ContentFilename if filename == "" { filename = pasta.Id } fmt.Fprintf(w, "<tr><td><a href=\"%s\">%s</a></td><td>%d B</td></tr>\n", pasta.Id, filename, pasta.Size) } fmt.Fprintf(w, "</table>\n") if len(publicPastas) == cf.PublicPastas { fmt.Fprintf(w, "<p>The server presents at most %d public pastas.<p>\n", cf.PublicPastas) } } fmt.Fprintf(w, "<h2>Post a new pasta</h2>\n") fmt.Fprintf(w, "<p><code>curl -X POST '%s' --data-binary @FILE</code></p>\n", cf.BaseUrl) if cf.DefaultExpire > 0 { fmt.Fprintf(w, "<p>pastas expire by default after %s - Enjoy them while they are fresh!</p>\n", timeHumanReadable(cf.DefaultExpire)) } fmt.Fprintf(w, "<h3>File upload</h3>") fmt.Fprintf(w, "<p>Upload your file and make a fresh pasta out of it:</p>") fmt.Fprintf(w, "<form enctype=\"multipart/form-data\" method=\"post\" action=\"/?ret=html\">\n") fmt.Fprintf(w, "<input type=\"file\" name=\"file\">\n") if cf.PublicPastas > 0 { fmt.Fprintf(w, "<input type=\"checkbox\" id=\"public\" name=\"public\" value=\"true\"> Public\n") } fmt.Fprintf(w, "<input type=\"submit\" value=\"Upload\">\n") fmt.Fprintf(w, "</form>\n") fmt.Fprintf(w, "<h3>Text paste</h3>") fmt.Fprintf(w, "<p>Just paste your contents in the textfield and hit the <tt>pasta</tt> button below</p>\n") fmt.Fprintf(w, "<form method=\"post\" action=\"/?input=form&ret=html\">\n") fmt.Fprintf(w, "Filename (optional): <input type=\"text\" name=\"filename\" value=\"\" max=\"255\"><br/>\n") if cf.MaxPastaSize > 0 { fmt.Fprintf(w, "<textarea name=\"content\" rows=\"10\" cols=\"80\" maxlength=\"%d\"></textarea><br/>\n", cf.MaxPastaSize) } else { fmt.Fprintf(w, "<textarea name=\"content\" rows=\"10\" cols=\"80\"></textarea><br/>\n") } if cf.PublicPastas > 0 { fmt.Fprintf(w, "<input type=\"checkbox\" id=\"public\" name=\"public\" value=\"true\"> Public pasta\n") } fmt.Fprintf(w, "<input type=\"submit\" value=\"Pasta!\">\n") fmt.Fprintf(w, "</form>\n") fmt.Fprintf(w, "\n<hr/>\n") fmt.Fprintf(w, "<p>project page: <a href=\"https://codeberg.org/grisu48/pasta\" target=\"_BLANK\">codeberg.org/grisu48/pasta</a></p>\n") fmt.Fprintf(w, "</body></html>") } func cleanupThread() { // Double check this, because I know that I will screw this up at some point in the main routine :-) if cf.CleanupInterval == 0 { return } for { duration := time.Now().Unix() if err := bowl.RemoveExpired(); err != nil { log.Fatalf("Error while removing expired pastas: %s", err) } if cf.RequestDelay > 0 { // Cleanup of the spam protection addresses only if enabled delayMutex.Lock() delays = make(map[string]int64) delayMutex.Unlock() } duration = time.Now().Unix() - duration + int64(cf.CleanupInterval) if duration > 0 { time.Sleep(time.Duration(cf.CleanupInterval) * time.Second) } else { // Don't spam the system, give it at least some time time.Sleep(time.Second) } } } func main() { cf.SetDefaults() cf.ReadEnv() delays = make(map[string]int64) publicPastas = make([]Pasta, 0) // Parse program arguments for config parseCf := ParserConfig{} parser := argparse.NewParser("pastad", "pasta server") parseCf.ConfigFile = parser.String("c", "config", &argparse.Options{Default: "", Help: "Set config file"}) parseCf.BaseURL = parser.String("B", "baseurl", &argparse.Options{Help: "Set base URL for instance"}) parseCf.PastaDir = parser.String("d", "dir", &argparse.Options{Help: "Set pasta data directory"}) parseCf.BindAddr = parser.String("b", "bind", &argparse.Options{Help: "Address to bind server to"}) parseCf.MaxPastaSize = parser.Int("s", "size", &argparse.Options{Help: "Maximum allowed size for a pasta"}) parseCf.PastaCharacters = parser.Int("n", "chars", &argparse.Options{Help: "Random characters for new pastas"}) parseCf.MimeTypesFile = parser.String("m", "mime", &argparse.Options{Help: "Define mime types file"}) parseCf.DefaultExpire = parser.Int("e", "expire", &argparse.Options{Help: "Pasta expire in seconds"}) parseCf.CleanupInterval = parser.Int("C", "cleanup", &argparse.Options{Help: "Cleanup interval in seconds"}) parseCf.PublicPastas = parser.Int("p", "public", &argparse.Options{Help: "Number of public pastas to display, if any"}) if err := parser.Parse(os.Args); err != nil { fmt.Fprintf(os.Stderr, "%s\n", parser.Usage(err)) os.Exit(1) } log.Printf("Starting pasta server v%s ... \n", VERSION) configFile := *parseCf.ConfigFile if configFile != "" { if FileExists(configFile) { if _, err := toml.DecodeFile(configFile, &cf); err != nil { fmt.Printf("Error loading configuration file: %s\n", err) os.Exit(1) } } else { if err := CreateDefaultConfigfile(configFile); err == nil { fmt.Fprintf(os.Stderr, "Created default config file '%s'\n", configFile) } else { fmt.Fprintf(os.Stderr, "Warning: Cannot create default config file '%s': %s\n", configFile, err) } } } // Program arguments overwrite config file parseCf.ApplyTo(&cf) // Sanity check if cf.PastaCharacters <= 0 { log.Println("Setting pasta characters to default 8 because it was <= 0") cf.PastaCharacters = 8 } if cf.PastaCharacters < 8 { log.Println("Warning: Using less than 8 pasta characters might not be side-effects free") } if cf.PastaDir == "" { cf.PastaDir = "." } // Preparation steps baseURL, err := ApplyMacros(cf.BaseUrl) if err != nil { fmt.Fprintf(os.Stderr, "error applying macros: %s", err) os.Exit(1) } cf.BaseUrl = baseURL bowl.Directory = cf.PastaDir os.Mkdir(bowl.Directory, os.ModePerm) // Load MIME types file if cf.MimeTypesFile == "" { mimeExtensions = make(map[string]string, 0) } else { var err error mimeExtensions, err = loadMimeTypes(cf.MimeTypesFile) if err != nil { log.Printf("Warning: Cannot load mime types file '%s': %s", cf.MimeTypesFile, err) } else { log.Printf("Loaded %d mime types", len(mimeExtensions)) } } // Load public pastas if cf.PublicPastas > 0 { pastas, err := bowl.GetPublicPastas() if err != nil { log.Printf("Error loading public pastas: %s", err) } else { // Crop if necessary if len(pastas) > cf.PublicPastas { pastas = pastas[len(pastas)-cf.PublicPastas:] bowl.WritePublicPastaIDs(pastas) } for _, id := range pastas { if id == "" { continue } pasta, err := bowl.GetPasta(id) if err == nil && pasta.Id != "" { publicPastas = append(publicPastas, pasta) } } log.Printf("Loaded %d public pastas", len(publicPastas)) } } // Start cleanup thread if cf.CleanupInterval > 0 { go cleanupThread() } // Setup webserver http.HandleFunc("/", handler) http.HandleFunc("/health", handlerHealth) http.HandleFunc("/health.json", handlerHealthJson) http.HandleFunc("/public", handlerPublic) http.HandleFunc("/public.json", handlerPublicJson) http.HandleFunc("/delete", handlerDelete) http.HandleFunc("/robots.txt", handlerRobots) log.Printf("Serving http://%s", cf.BindAddr) log.Fatal(http.ListenAndServe(cf.BindAddr, nil)) } 07070100000013000081A400000000000000000000000164BFCD7C00001C92000000000000000000000000000000000000002200000000pasta-0.7.2/cmd/pastad/storage.gopackage main import ( "bufio" "crypto/rand" "errors" "fmt" "io" "io/ioutil" "os" "strconv" "strings" "time" ) type Pasta struct { Id string // id of the pasta Token string // modification token DiskFilename string // filename for the pasta on the disk ContentFilename string // Filename of the content ExpireDate int64 // Unix() date when it will expire Size int64 // file size Mime string // mime type } func (pasta *Pasta) Expired() bool { if pasta.ExpireDate == 0 { return false } else { return time.Now().Unix() > pasta.ExpireDate } } func randBytes(n int) []byte { buf := make([]byte, n) i, err := rand.Read(buf) if err != nil { panic(err) } if i < n { panic(fmt.Errorf("random generator empty")) } return buf } func randInt() int { buf := randBytes(4) ret := 0 for i := 0; i < 4; i++ { ret += int(buf[i]) << (i * 8) } return ret } func RandomString(n int) string { var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") b := make([]rune, n) for i := range b { b[i] = letterRunes[randInt()%len(letterRunes)] } return string(b) } func FileExists(filename string) bool { _, err := os.Stat(filename) if err != nil { return false } return !os.IsNotExist(err) } func strBool(val string, def bool) bool { val = strings.TrimSpace(val) val = strings.ToLower(val) if val == "true" { return true } else if val == "yes" { return true } else if val == "on" { return true } else if val == "1" { return true } else if val == "false" { return false } else if val == "no" { return false } else if val == "off" { return false } else if val == "0" { return false } return def } /* PastaBowl is the main storage instance */ type PastaBowl struct { Directory string // Directory where the pastas are } func (bowl *PastaBowl) filename(id string) string { return fmt.Sprintf("%s/%s", bowl.Directory, id) } func (bowl *PastaBowl) Exists(id string) bool { return FileExists(bowl.filename(id)) } /** Check for expired pastas and delete them */ func (bowl *PastaBowl) RemoveExpired() error { files, err := ioutil.ReadDir(bowl.Directory) if err != nil { return err } for _, file := range files { if file.Size() == 0 { continue } pasta, err := bowl.GetPasta(file.Name()) if err != nil { return err } if pasta.Expired() { if err := bowl.DeletePasta(pasta.Id); err != nil { return err } } } return nil } // get pasta metadata func (bowl *PastaBowl) GetPasta(id string) (Pasta, error) { pasta := Pasta{Id: "", DiskFilename: bowl.filename(id)} stat, err := os.Stat(bowl.filename(id)) if err != nil { // Does not exists results in empty pasta result if !os.IsExist(err) { return pasta, nil } return pasta, err } pasta.Size = stat.Size() file, err := os.OpenFile(pasta.DiskFilename, os.O_RDONLY, 0400) if err != nil { return pasta, err } defer file.Close() // Read metadata (until "---") scanner := bufio.NewScanner(file) for scanner.Scan() { if err = scanner.Err(); err != nil { return pasta, err } line := scanner.Text() pasta.Size -= int64(len(line) + 1) if line == "---" { break } // Parse metadata (name: value) i := strings.Index(line, ":") if i <= 0 { continue } name, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]) if name == "token" { pasta.Token = value } else if name == "expire" { pasta.ExpireDate, _ = strconv.ParseInt(value, 10, 64) } else if name == "mime" { pasta.Mime = value } else if name == "filename" { pasta.ContentFilename = value } } // All good pasta.Id = id return pasta, nil } func (bowl *PastaBowl) getPastaFile(id string, flag int) (*os.File, error) { filename := bowl.filename(id) file, err := os.OpenFile(filename, flag, 0640) if err != nil { return nil, err } buf := make([]byte, 1) c := 0 // Counter for { n, err := file.Read(buf) if err != nil { if err == io.EOF { file.Close() return nil, errors.New("unexpected end of block") } file.Close() return nil, errors.New("unexpected end of block") } if n == 0 { continue } if buf[0] == '-' { c++ } else if buf[0] == '\n' { if c == 3 { return file, nil } c = 0 } else { c = 0 } } } // Get the file instance to the pasta content (read-only) func (bowl *PastaBowl) GetPastaReader(id string) (*os.File, error) { return bowl.getPastaFile(id, os.O_RDONLY) } // Get the file instance to the pasta content (read-only) func (bowl *PastaBowl) GetPastaWriter(id string) (*os.File, error) { return bowl.getPastaFile(id, os.O_RDWR) } // Prepare a pasta file to be written. Id and Token will be set, if not already done func (bowl *PastaBowl) InsertPasta(pasta *Pasta) error { if pasta.Id == "" { // TODO: Use crypto rand pasta.Id = bowl.GenerateRandomBinId(8) // Use default length here } if pasta.Token == "" { // TODO: Use crypto rand pasta.Token = RandomString(16) } pasta.DiskFilename = bowl.filename(pasta.Id) file, err := os.OpenFile(pasta.DiskFilename, os.O_RDWR|os.O_CREATE, 0640) if err != nil { return err } defer file.Close() if _, err := file.Write([]byte(fmt.Sprintf("token:%s\n", pasta.Token))); err != nil { return err } if pasta.ExpireDate > 0 { if _, err := file.Write([]byte(fmt.Sprintf("expire:%d\n", pasta.ExpireDate))); err != nil { return err } } if pasta.Mime != "" { if _, err := file.Write([]byte(fmt.Sprintf("mime:%s\n", pasta.Mime))); err != nil { return err } } if pasta.ContentFilename != "" { if _, err := file.Write([]byte(fmt.Sprintf("filename:%s\n", pasta.ContentFilename))); err != nil { return err } } if _, err := file.Write([]byte("---\n")); err != nil { return err } return file.Sync() } func (bowl *PastaBowl) DeletePasta(id string) error { if !bowl.Exists(id) { return nil } return os.Remove(bowl.filename(id)) } func (bowl *PastaBowl) GenerateRandomBinId(n int) string { for { id := RandomString(n) if !bowl.Exists(id) { return id } } } // GetPublicPastas returns a list of Public pasta IDs, stored in the bowl func (bowl *PastaBowl) GetPublicPastas() ([]string, error) { ret := make([]string, 0) filename := fmt.Sprintf("%s/_public", bowl.Directory) if !FileExists(filename) { return ret, nil } file, err := os.OpenFile(filename, os.O_RDONLY, 0400) if err != nil { return ret, err } defer file.Close() // Read public pastas, one by one scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } ret = append(ret, line) } return ret, scanner.Err() } // WritePublicPastas writes a list of public pastas to the public file func (bowl *PastaBowl) WritePublicPastaIDs(ids []string) error { filename := fmt.Sprintf("%s/_public", bowl.Directory) file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0640) if err != nil { return err } defer file.Close() for _, id := range ids { file.Write([]byte(fmt.Sprintf("%s\n", id))) } return file.Sync() } func (bowl *PastaBowl) WritePublicPastas(pastas []Pasta) error { ids := make([]string, 0) for _, pasta := range pastas { ids = append(ids, pasta.Id) } return bowl.WritePublicPastaIDs(ids) } 07070100000014000081A400000000000000000000000164BFCD7C000016A1000000000000000000000000000000000000002800000000pasta-0.7.2/cmd/pastad/storage__test.gopackage main import ( "io/ioutil" "math/rand" "os" "testing" "time" ) var testBowl PastaBowl func TestMain(m *testing.M) { // Initialisation rand.Seed(time.Now().UnixNano()) testBowl.Directory = "pasta_test" os.Mkdir(testBowl.Directory, os.ModePerm) defer os.RemoveAll(testBowl.Directory) // Run tests ret := m.Run() os.Exit(ret) } func TestMetadata(t *testing.T) { var err error var pasta, p1, p2, p3 Pasta p1.Mime = "text/plain" p1.ExpireDate = 0 if err = testBowl.InsertPasta(&p1); err != nil { t.Fatalf("Error inserting pasta 1: %s", err) return } if p1.Id == "" { t.Fatal("Pasta 1 id not set") return } if p1.Token == "" { t.Fatal("Pasta 1 id not set") return } p2.Mime = "application/json" p2.ExpireDate = time.Now().Unix() + 10000 if err = testBowl.InsertPasta(&p2); err != nil { t.Fatalf("Error inserting pasta 2: %s", err) return } // Insert pasta with given ID and Token p3Id := testBowl.GenerateRandomBinId(12) p3Token := RandomString(20) p3.Id = p3Id p3.Token = p3Token p3.Mime = "text/rtf" p3.ExpireDate = time.Now().Unix() + 20000 if err = testBowl.InsertPasta(&p3); err != nil { t.Fatalf("Error inserting pasta 3: %s", err) return } if p3.Id != p3Id { t.Fatal("Pasta 3 id mismatch") return } if p3.Token != p3Token { t.Fatal("Pasta 3 id mismatch") return } pasta, err = testBowl.GetPasta(p1.Id) if err != nil { t.Fatalf("Error getting pasta 1: %s", err) return } if pasta != p1 { t.Fatal("Pasta 1 mismatch") return } pasta, err = testBowl.GetPasta(p2.Id) if err != nil { t.Fatalf("Error getting pasta 2: %s", err) return } if pasta != p2 { t.Fatal("Pasta 2 mismatch") return } pasta, err = testBowl.GetPasta(p3.Id) if err != nil { t.Fatalf("Error getting pasta 3: %s", err) return } if pasta != p3 { t.Fatal("Pasta 3 mismatch") return } if err = testBowl.DeletePasta(p1.Id); err != nil { t.Fatalf("Error deleting pasta 1: %s", err) } pasta, err = testBowl.GetPasta(p1.Id) if err != nil { t.Fatalf("Error getting pasta 1 (after delete): %s", err) return } if pasta.Id != "" { t.Fatal("Pasta 1 exists after delete") return } // Ensure pasta 2 and 3 are not affected if we delete pasta 1 pasta, err = testBowl.GetPasta(p2.Id) if err != nil { t.Fatalf("Error getting pasta 2 after deleting pasta 1: %s", err) return } if pasta != p2 { t.Fatal("Pasta 2 mismatch after deleting pasta 1") return } pasta, err = testBowl.GetPasta(p3.Id) if err != nil { t.Fatalf("Error getting pasta 3 after deleting pasta 1: %s", err) return } if pasta != p3 { t.Fatal("Pasta 3 mismatch after deleteing pasta 1") return } // Delete also pasta 2 if err = testBowl.DeletePasta(p2.Id); err != nil { t.Fatalf("Error deleting pasta 2: %s", err) } pasta, err = testBowl.GetPasta(p2.Id) if err != nil { t.Fatalf("Error getting pasta 2 (after delete): %s", err) return } if pasta.Id != "" { t.Fatal("Pasta 2 exists after delete") return } pasta, err = testBowl.GetPasta(p3.Id) if err != nil { t.Fatalf("Error getting pasta 3 after deleting pasta 2: %s", err) return } if pasta != p3 { t.Fatal("Pasta 3 mismatch after deleting pasta 2") return } } func TestBlobs(t *testing.T) { var err error var p1, p2 Pasta // Contents testString1 := RandomString(4096 * 8) testString2 := RandomString(4096 * 8) if err = testBowl.InsertPasta(&p1); err != nil { t.Fatalf("Error inserting pasta 1: %s", err) return } file, err := testBowl.GetPastaWriter(p1.Id) if err != nil { t.Fatalf("Error getting pasta file 1: %s", err) return } defer file.Close() if _, err = file.Write([]byte(testString1)); err != nil { t.Fatalf("Error writing to pasta file 1: %s", err) return } if err = file.Close(); err != nil { t.Fatalf("Error closing pasta file 1: %s", err) return } if err = testBowl.InsertPasta(&p2); err != nil { t.Fatalf("Error inserting pasta 2: %s", err) return } file, err = testBowl.GetPastaWriter(p2.Id) if err != nil { t.Fatalf("Error getting pasta file 2: %s", err) return } defer file.Close() if _, err = file.Write([]byte(testString2)); err != nil { t.Fatalf("Error writing to pasta file 2: %s", err) return } if err = file.Close(); err != nil { t.Fatalf("Error closing pasta file 2: %s", err) return } // Fetch contents now file, err = testBowl.GetPastaReader(p1.Id) if err != nil { t.Fatalf("Error getting pasta reader 1: %s", err) return } buf, err := ioutil.ReadAll(file) file.Close() if err != nil { t.Fatalf("Error reading pasta 1: %s", err) return } if testString1 != string(buf) { t.Fatal("Mismatch: pasta 1 contents") t.Logf("Bytes: Read %d, Expected %d", len(buf), len(([]byte(testString1)))) return } // Same for pasta 2 file, err = testBowl.GetPastaReader(p2.Id) if err != nil { t.Fatalf("Error getting pasta reader 2: %s", err) return } buf, err = ioutil.ReadAll(file) file.Close() if err != nil { t.Fatalf("Error reading pasta 2: %s", err) return } if testString2 != string(buf) { t.Fatal("Mismatch: pasta 2 contents") t.Logf("Bytes: Read %d, Expected %d", len(buf), len(([]byte(testString2)))) return } // Check if pasta 1 can be deleted and the contents of pasta 2 are still OK afterwards if err = testBowl.DeletePasta(p1.Id); err != nil { t.Fatalf("Error deleting pasta 1: %s", err) } file, err = testBowl.GetPastaReader(p2.Id) if err != nil { t.Fatalf("Error getting pasta reader 2: %s", err) return } buf, err = ioutil.ReadAll(file) file.Close() if err != nil { t.Fatalf("Error reading pasta 2: %s", err) return } if testString2 != string(buf) { t.Fatal("Mismatch: pasta 2 contents") t.Logf("Bytes: Read %d, Expected %d", len(buf), len(([]byte(testString2)))) return } } 07070100000015000081A400000000000000000000000164BFCD7C000010FE000000000000000000000000000000000000002000000000pasta-0.7.2/cmd/pastad/utils.gopackage main import ( "bufio" "fmt" "os" "strconv" "strings" ) // getenv reads a given environmental variable and returns it's value if present or defval if not present or empty func getenv(key string, defval string) string { val := os.Getenv(key) if val == "" { return defval } return val } // getenv reads a given environmental variable as integer and returns it's value if present or defval if not present or empty func getenv_i(key string, defval int) int { val := os.Getenv(key) if val == "" { return defval } if i32, err := strconv.Atoi(val); err != nil { return defval } else { return i32 } } // getenv reads a given environmental variable as integer and returns it's value if present or defval if not present or empty func getenv_i64(key string, defval int64) int64 { val := os.Getenv(key) if val == "" { return defval } if i64, err := strconv.ParseInt(val, 10, 64); err != nil { return defval } else { return i64 } } func isAlphaNumeric(c rune) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') } func containsOnlyAlphaNumeric(input string) bool { for _, c := range input { if !isAlphaNumeric(c) { return false } } return true } func removeNonAlphaNumeric(input string) string { ret := "" for _, c := range input { if isAlphaNumeric(c) { ret += string(c) } } return ret } func ExtractPastaId(path string) (string, error) { var id string i := strings.LastIndex(path, "/") if i < 0 { id = path } else { id = path[i+1:] } if !containsOnlyAlphaNumeric(id) { return "", fmt.Errorf("invalid id") } return id, nil } /* Load MIME types file. MIME types file is a simple text file that describes mime types based on file extenstions. * The format of the file is * EXTENSION = MIMETYPE */ func loadMimeTypes(filename string) (map[string]string, error) { ret := make(map[string]string, 0) file, err := os.OpenFile(filename, os.O_RDONLY, 0400) if err != nil { return ret, err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || line[0] == '#' { continue } i := strings.Index(line, "=") if i < 0 { continue } name, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]) if name != "" && value != "" { ret[name] = value } } return ret, scanner.Err() } func takeFirst(arr []string) string { if len(arr) == 0 { return "" } return arr[0] } /* try to determine the mime type by file extension. Returns empty string on failure */ func mimeByFilename(filename string) string { i := strings.LastIndex(filename, ".") if i < 0 { return "" } extension := filename[i+1:] if mime, ok := mimeExtensions[extension]; ok { return mime } return "" } /* Extract the remote IP address of the given remote * The remote is expected to come from http.Request and contain the IP address plus the port */ func extractRemoteIP(remote string) string { // Check if IPv6 i := strings.Index(remote, "[") if i >= 0 { j := strings.Index(remote, "]") if j <= i { return remote } return remote[i+1 : j] } i = strings.Index(remote, ":") if i > 0 { return remote[:i] } return remote } func timeHumanReadable(timestamp int64) string { if timestamp < 60 { return fmt.Sprintf("%d s", timestamp) } minutes := timestamp / 60 seconds := timestamp - (minutes * 60) if minutes < 60 { return fmt.Sprintf("%d:%d min", minutes, seconds) } hours := minutes / 60 minutes -= hours * 60 if hours < 24 { return fmt.Sprintf("%d s", hours) } days := hours / 24 hours -= days * 24 if days > 365 { years := float32(days) / 365.0 return fmt.Sprintf("%.2f years", years) } else if days > 28 { weeks := days / 7 if weeks > 4 { months := days / 30 return fmt.Sprintf("%d months", months) } return fmt.Sprintf("%d weeks", weeks) } else { return fmt.Sprintf("%d days", days) } } /* Apply custom macros in the given string and return the result. The following macros are supports: * `$hostname` - Replace with the system hostname */ func ApplyMacros(txt string) (string, error) { if strings.Contains(txt, "$hostname") { hostname, err := os.Hostname() if err != nil { return "", err } txt = strings.ReplaceAll(txt, "$hostname", hostname) } return txt, nil } 07070100000016000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001100000000pasta-0.7.2/docs07070100000017000081A400000000000000000000000164BFCD7C00000371000000000000000000000000000000000000001A00000000pasta-0.7.2/docs/build.md# Building ## Build and run from source Use the provided `Makefile` commands: make # all make pastad # Server make pasta # Client make static # build static binaries (for release) ## Build docker image make docker Or manually: docker build . -t feldspaten.org/pasta # Build docker container Create or run the container with docker container create --name pasta -p 8199:8199 -v ABSOLUTE_PATH_TO_DATA_DIR:/data feldspaten.org/pasta docker container run --name pasta -p 8199:8199 -v ABSOLUTE_PATH_TO_DATA_DIR:/data feldspaten.org/pasta The container needs a `data` directory with a valid `pastad.toml` (See the [example file](pastad.toml.example), otherwise default values will be used).07070100000018000081A400000000000000000000000164BFCD7C0000050D000000000000000000000000000000000000002900000000pasta-0.7.2/docs/cloud-init.yaml.examplessh_authorized_keys: - ssh-rsa ... mounts: - ["/dev/sdb1", "/data", "ext4", ""] write_files: - path: /mnt/pastad.toml permissions: "0755" owner: root content: | BaseURL = "https://pasta.domain.com" # replace with your hostname PastaDir = "pastas" # absolute or relative path to the pastas BindAddress = ":8199" # server bind address MaxPastaSize = 26214400 # max allowed pasta size - 5 MB PastaCharacters = 8 # Number of characters for pasta id Expire = 2592000 # Default expire in seconds (1 Month) Cleanup = 3600 # Cleanup interval in seconds (1 hour) runcmd: - sudo wget https://raw.githubusercontent.com/grisu48/pasta/main/mime.types -O /data/mime.types - sudo cp /mnt/pastad.toml /data/pastad.toml rancher: network: dns: nameservers: - 8.8.8.8 - 1.1.1.1 interfaces: eth0: addresses: - 192.0.2.2/24 - 2001:db8::2/64 gateway: 192.0.2.1 gateway_ipv6: 2001:db8::1 mtu: 1500 dhcp: false services: past: image: grisu48/pasta volumes: - /data:/data ports: - "80:8199" restart: always 07070100000019000081A400000000000000000000000164BFCD7C00000486000000000000000000000000000000000000002400000000pasta-0.7.2/docs/getting-started.md# Installation The easiest way is to run `pasta` as a container service or get the pre-build binaries from the releases within this repository. If you prefer the native applications, checkout the sections below. ## Install on openSUSE openSUSE packages are provided at [build.opensuse.org](https://build.opensuse.org/package/show/home%3Aph03nix%3Atools/pasta). To install follow the instructions from [software.opensuse.org](https://software.opensuse.org/download/package?package=pasta&project=home%3Aph03nix%3Atools) or the following snippet: # Tumbleweed zypper addrepo zypper addrepo https://download.opensuse.org/repositories/home:ph03nix:tools/openSUSE_Tumbleweed/home:ph03nix:tools.repo zypper refresh && zypper install pasta ## RancherOS Let's assume we have `/dev/sda` for the system and `/dev/sdb` for data. * Prepare persistent storage for data * Install the system with given [`cloud-init.yaml`](cloud-init.yaml.example) to system storage * Configure your proxy and enojoy! ```bash $ sudo parted /dev/sdb # mktable - gpt - mkpart - 1 - 0% - 100% $ sudo mkfs.ext4 /dev/sdb1 $ sudo ros install -d /dev/sda -c cloud-init.yaml ``` 0707010000001A000081A400000000000000000000000164BFCD7C000000C8000000000000000000000000000000000000001A00000000pasta-0.7.2/docs/index.md# pasta documentation This folder contains auxilliary documentation for pasta. Checkout one of the following sections. * [Getting started guides](getting-started.md) * [Build instructions](build.md)0707010000001B000081A400000000000000000000000164BFCD7C0000007F000000000000000000000000000000000000001300000000pasta-0.7.2/go.modmodule github.com/grisu48/pasta go 1.13 require ( github.com/BurntSushi/toml v1.3.2 github.com/akamensky/argparse v1.4.0 ) 0707010000001C000081A400000000000000000000000164BFCD7C0000015C000000000000000000000000000000000000001300000000pasta-0.7.2/go.sumgithub.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= 0707010000001D000081A400000000000000000000000164BFCD7C000005CA000000000000000000000000000000000000001700000000pasta-0.7.2/mime.types# Known mime types based on file extension bmp = image/bmp bz = application/x-bzip bz2 = application/x-bzip2 csh = application/x-csh css = text/csvv csv = text/csv doc = application/msword docx = application/vnd.openxmlformats-officedocument.wordprocessingml.document epub = application/epub+zip gz = application/gzip gif = image/gif htm = text/html html = text/html ics = text/calendar jar = application/java-archive jpg = image/jpeg jpeg = image/jpeg js = text/javascript json = application/json mp3 = audio/mpeg mpg = audio/mpeg mpeg = audio/mpeg ods = application/vnd.oasis.opendocument.presentation otd = application/vnd.oasis.opendocument.spreadsheet otd = application/vnd.oasis.opendocument.text oga = audio/ogg ogv = audio/ogg opus = audio/opus otf = font/otf png = image/png pdf = application/pdf php = application/x-httpd-php ppt = application/vnd.ms-powerpoint pptx = application/vnd.openxmlformats-officedocument.presentationml.presentation rat = application/vnd.rar rtf = application/rtf sh = application/x-sh svg = image/svg+xml tar = application/x-tar tif = image/tiff tiff = image/tiff ts = video/mp2t ttf = font/ttf txt = text/plain wav = audio/wav weba = audio/webm webm = video/webm webp = image/webp woff = font/woff woff2 = font/woff2 xhtml = application/xhtml+xml xls = application/vnd.ms-excel xlsx = application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xml = text/xml xz = application/x-xz zip = application/zip 7z = application/x-7z-compressed 0707010000001E000081A400000000000000000000000164BFCD7C000001FC000000000000000000000000000000000000001F00000000pasta-0.7.2/pasta.toml.example# Keep in mind to escape string. This is TOML not an ini file! # Place this file in ~/.pasta.toml RemoteHost = "http://localhost:8199" # Example for a remote with one alias # aliases can be given to `pasta` as a remote argument and will be # completed to the given url [[Remote]] url = "http://localhost:8199" alias = "localhost" # Example of a remote with multiple aliases [[Remote]] url = ""http://localhost:8200" alias = "localhost2" # one alias aliases = ["local2", "loc2"] # more aliases 0707010000001F000081A400000000000000000000000164BFCD7C000002CA000000000000000000000000000000000000002000000000pasta-0.7.2/pastad.toml.exampleBaseURL = "http://localhost:8199" # base URL as used within pasta BindAddress = ":8199" # bind address PastaDir = "pastas" # absolute or relative path to the pastas data directory MaxPastaSize = 5242880 # max allowed pasta size (5 MiB) PastaCharacters = 8 # Number of characters for pasta id Expire = 2592000 # Default expire in seconds (1 Month) Cleanup = 3600 # Cleanup interval in seconds (1 hour) RequestDelay = 2000 # Milliseconds between POST/DELETE requests per host PublicPastas = 0 # Number of public pastas to display or 0 to disable public display (default) 07070100000020000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001100000000pasta-0.7.2/test07070100000021000081ED00000000000000000000000164BFCD7C00000EC1000000000000000000000000000000000000001900000000pasta-0.7.2/test/test.sh#!/bin/bash -e # Summary: Function test for pasta & pastad PASTAS=~/.pastas.dat # pasta client dat file PASTAS_TEMP="" # temp file, if present function cleanup() { set +e # restore old pasta client file if [[ $PASTAS_TEMP != "" ]]; then mv "$PASTAS_TEMP" "$PASTAS" fi rm -f testfile rm -f testfile2 rm -f rm kill %1 rm -rf pasta_test rm -f pasta.json rm -f test_config.toml } trap cleanup EXIT ## Preparation: Safe old pastas.dat, if existing if [[ -s $PASTAS ]]; then PASTAS_TEMP=`mktemp` mv "$PASTAS" "$PASTAS_TEMP" fi ## Setup pasta server ../pastad -c pastad.toml -m ../mime.types -B http://127.0.0.1:8200 -b 127.0.0.1:8200 & sleep 2 ## Push a testfile echo "Testfile 123" > testfile link=`../pasta -r http://127.0.0.1:8200 < testfile` curl --fail -o testfile2 $link diff testfile testfile2 echo "Testfile matches" echo "Testfile 123456789" > testfile link=`../pasta -r http://127.0.0.1:8200 < testfile` curl --fail -o testfile2 $link diff testfile testfile2 echo "Testfile 2 matches" # Test also sending via curl url=`curl --fail -X POST http://127.0.0.1:8200/ --data-binary @testfile | grep -Eo 'http://.*'` echo "curl stored as $url" curl --fail -o testfile3 "$url" diff testfile testfile3 echo "Testfile 3 matches" # Test the POST form echo -n "testpasta" > testfile4 url=`curl --fail -X POST "http://127.0.0.1:8200?input=form&content=testpasta" | grep -Eo 'http://.*'` curl --fail -o testfile5 "$url" diff testfile4 testfile5 # Test different format in link curl --fail -X POST http://127.0.0.1:8200?ret=json --data-binary @testfile ## Second pasta server with environment variables echo "Testing environment variables ... " PASTA_BASEURL=pastas PASTA_BINDADDR=127.0.0.1:8201 PASTA_CHARACTERS=12 ../pastad -m ../mime.types & SECONDPID=$! sleep 2 # TODO: Don't do sleep here you lazy ... :-) link2=`../pasta -r http://127.0.0.1:8201 < testfile` curl --fail -o testfile_second $link diff testfile testfile_second kill $SECONDPID ## Test spam protection echo "Testing spam protection ... " ../pasta -r http://127.0.0.1:8200 testfile >/dev/null ! timeout 1 ../pasta -r http://127.0.0.1:8200 testfile >/dev/null sleep 2 ../pasta -r http://127.0.0.1:8200 testfile >/dev/null ## TODO: Test expire pasta cleanup ## Test special commands function test_special_command() { command="$1" echo "test" > $command # Ambiguous, if the shortcut command and a similar file exists. This must fail ! ../pasta -r http://127.0.0.1:8200 "$command" # However it must pass, if the file is explicitly stated ../pasta -r http://127.0.0.1:8200 -f "$command" # And it must succeed, if there is no such file and thus is it clear what should happen if [[ "$command" != "rm" ]]; then rm "$command"; fi ../pasta -r http://127.0.0.1:8200 "$command" } test_special_command "ls" test_special_command "rm" test_special_command "gc" ## Test creation of default config rm -f test_config.toml ../pastad -c test_config.toml -B http://127.0.0.1:8201 -b 127.0.0.1:8201 & sleep 2 # TODO: Don't sleep here either but create a valid monitor kill %2 stat test_config.toml # Ensure the test config contains the expected entries grep 'BaseURL[[:space:]]=' test_config.toml grep 'BindAddress[[:space:]]*=' test_config.toml grep 'PastaDir[[:space:]]*=' test_config.toml grep 'MaxPastaSize[[:space:]]*=' test_config.toml grep 'PastaCharacters[[:space:]]*=' test_config.toml grep 'Expire[[:space:]]*=' test_config.toml grep 'Cleanup[[:space:]]*=' test_config.toml grep 'RequestDelay[[:space:]]*=' test_config.toml echo "test_config.toml has been successfully created" ## Check the date handling of the pasta client ## Ensure there are no 1970 entries in ls ! ../pasta ls | grep '1970' ../pasta ls | grep `date +"%Y-%m-%d"` echo "All good :-)" 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!168 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor