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"]
070701000000080000A1FF00000000000000000000000164BFCD7C0000000D000000000000000000000000000000000000001700000000pasta-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
# 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