Add token rotation
This commit is contained in:
@@ -3,56 +3,30 @@
|
||||
A small self-hosted file drop. Uploads land in a flat data directory, expire on
|
||||
their own, and are served back as inert attachments.
|
||||
|
||||
Built for sharing large binaries with friends — a 400 MB Godot export is a
|
||||
normal day here — without a `tmp/` folder that grows forever.
|
||||
Built for sharing large binaries with friends so that I don't have to manually manage the `tmp` folder on my generic web server.
|
||||
|
||||
- One static Go binary, standard library only. No database, no CSS or
|
||||
JavaScript build step, nothing to install.
|
||||
- Anyone who can reach the page may upload, within a size cap and a lifetime.
|
||||
- Named tokens raise those limits, unlock custom URLs, and can grant admin.
|
||||
- Everything expires unless a token says otherwise.
|
||||
* One static Go binary, standard library only.
|
||||
* Anyone who can reach the page may upload, within a size cap and a lifetime.
|
||||
* Named tokens raise those limits, unlock custom URLs, and can grant admin.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```
|
||||
go build -o send .
|
||||
./send token add me --token - --vanity --admin # type a passphrase, or omit
|
||||
./send --port 8080 # --data defaults to ./data
|
||||
go build
|
||||
./send token add me --vanity --admin
|
||||
./send
|
||||
```
|
||||
|
||||
Open <http://localhost:8080>, click **Log in**, paste the token. That's it.
|
||||
|
||||
`--token -` reads a passphrase you choose from standard input; leave the flag
|
||||
off and a random one is generated and printed once. Either way `--data` must
|
||||
name the *same directory* the server runs with, or the server will never see
|
||||
the token.
|
||||
|
||||
Uploading from a script:
|
||||
|
||||
```
|
||||
curl --data-binary @MyGame.zip \
|
||||
-H 'Content-Disposition: attachment; filename="MyGame.zip"' \
|
||||
-H 'Authorization: Bearer <token>' \
|
||||
-H 'Vanity: my-game' -H 'Expiry: 7d' \
|
||||
http://localhost:8080/api/upload
|
||||
```
|
||||
|
||||
The reply carries the download URL and a delete token. Use a generated token
|
||||
for scripts — a chosen passphrase is verified with a deliberately slow
|
||||
derivation, which is wasted on every request a script makes.
|
||||
|
||||
In production, put it behind a reverse proxy that terminates TLS, set
|
||||
`--public-url`, and make sure the proxy neither buffers request bodies nor
|
||||
imposes its own upload limit.
|
||||
When you put it behind a reverse proxy that terminates TLS, set `--public-url`,
|
||||
and make sure the proxy neither buffers request bodies nor imposes its own
|
||||
upload limit.
|
||||
|
||||
## Options
|
||||
|
||||
**Read `./send --help` rather than this file.** It lists every option with its
|
||||
default and its environment variable, and unlike a README it cannot drift out
|
||||
of date. `./send token --help` does the same for credentials.
|
||||
**Read `./send --help` rather than this file.**
|
||||
|
||||
Options take **one hyphen with a single letter** and **two with a full word**:
|
||||
`-s 4GiB` and `--max-size=4GiB` are the same option; `-max-size` is an error.
|
||||
Every option also reads from `SEND_`-prefixed environment variables.
|
||||
|
||||
## Development
|
||||
@@ -61,38 +35,3 @@ Every option also reads from `SEND_`-prefixed environment variables.
|
||||
go test ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
Layout: `main.go` and `token.go` are the CLI; `internal/config` parses options;
|
||||
`internal/store` is the object store; `internal/auth` is credentials;
|
||||
`internal/server` is the HTTP surface; `web/` holds the templates and assets,
|
||||
embedded at build time.
|
||||
|
||||
A few invariants worth knowing before changing anything:
|
||||
|
||||
- **Upload size is never taken from `Content-Length`.** It is enforced on bytes
|
||||
actually written, and the transfer is cut off the moment it is exceeded.
|
||||
- **Nothing served from `/d/` may execute.** Always `application/octet-stream`,
|
||||
always an attachment, always `nosniff` and `default-src 'none'; sandbox`.
|
||||
- **Filenames are metadata, never paths.** Every path is built from a validated
|
||||
ID. `store.CleanID` is the only function allowed to turn input into a path
|
||||
element, and object files are opened through an `os.Root`.
|
||||
- **Expiry is checked on every read**, not only by the sweeper, and a missing
|
||||
file and an expired one answer identically.
|
||||
- **Uploads are published atomically**: the blob is fsynced and renamed into
|
||||
place before the metadata that advertises it, also by rename. A crash leaves
|
||||
something invisible rather than something broken.
|
||||
- **Generated tokens are hashed with SHA-256; chosen ones get PBKDF2** with
|
||||
their own salt. A 256-bit random value has nothing to crack, while a
|
||||
passphrase is guessable and probably reused elsewhere. The derivation is
|
||||
memoised per process and rate limited, so it cannot be used as an amplifier.
|
||||
- **The session cookie is `HttpOnly` and `SameSite=Strict`**, and every `POST`
|
||||
must be same-origin — `SameSite` does not cover logging in, which needs no
|
||||
cookie to submit.
|
||||
- **The app pages and the download responses have different CSP policies.**
|
||||
The app one must permit `connect-src` or the upload script is blocked; the
|
||||
download one must grant nothing at all. Both are pinned by tests.
|
||||
|
||||
The tests cover the places where a mistake is expensive: size limits against a
|
||||
body with no declared length, vanity collisions, expiry on read, traversal and
|
||||
reserved names, `Content-Disposition` for hostile filenames, delete
|
||||
authorisation, symlink escapes, crash debris, CSRF, and credential handling.
|
||||
|
||||
Reference in New Issue
Block a user