A token passed as `?token=` was accepted on every route, so it ended up in the request log (and journald), browser history and Referer. - drop `?token=` from _check_auth; API and download routes now take only the cookie or an Authorization: Bearer header - keep pre-authenticated links working: on the index route a valid `?token=` is swapped for the cookie and redirected to a clean URL, so the secret does not linger in the address bar - redact `token=` from log output - send Referrer-Policy: no-referrer, and mark the cookie Secure
159 lines
6.2 KiB
Markdown
159 lines
6.2 KiB
Markdown
# Deploying Share on a new machine
|
|
|
|
Instructions for a fresh Claude Code session (or a human) bringing this service up
|
|
from a clean clone. Read this top to bottom before running anything.
|
|
|
|
## What this is
|
|
|
|
A LAN file-sharing web app: drag & drop upload, clipboard paste, download, delete,
|
|
auto-expiry of old files. Python 3 stdlib only — no pip install, no virtualenv.
|
|
|
|
It serves **HTTPS with a self-signed certificate**. That is not optional decoration:
|
|
the browser Clipboard API (`navigator.clipboard.write`) only works on a secure
|
|
origin, and the paste/copy features depend on it.
|
|
|
|
## Requirements
|
|
|
|
- Python **3.10+** (the code uses `X | None` annotations evaluated at runtime)
|
|
- `openssl` on PATH (used once, to generate the self-signed cert)
|
|
- Linux with systemd user units for the service install; without systemd it still
|
|
runs fine in the foreground
|
|
|
|
## Install
|
|
|
|
```bash
|
|
git clone <repo-url> ~/projects/share
|
|
cd ~/projects/share
|
|
./install.sh
|
|
```
|
|
|
|
That will:
|
|
|
|
1. check python/openssl versions,
|
|
2. generate a random token, hash it, and write `share.config` (mode 600),
|
|
3. install `~/.config/systemd/user/share.service` pointing at wherever you cloned,
|
|
4. `enable --now` the service and confirm it actually came up.
|
|
|
|
**The generated token is printed once, at the end. Save it.** Only its SHA-256 hash
|
|
is stored, so it cannot be recovered afterwards — losing it means re-running with
|
|
`--force` to set a new one.
|
|
|
|
### Setting your own secret
|
|
|
|
```bash
|
|
./install.sh --token 'my-own-secret'
|
|
```
|
|
|
|
Or after the fact, without re-running the installer:
|
|
|
|
```bash
|
|
python3 share.py hash 'my-own-secret' # prints the SHA-256 hash
|
|
# paste that hash as the `token = ` value in share.config
|
|
systemctl --user restart share
|
|
```
|
|
|
|
Both forms are equivalent — `share.config` never holds the plaintext. A 64-char hex
|
|
value is treated as an already-hashed token; anything else is hashed at startup
|
|
(convenient, but then the plaintext sits on disk — prefer the hash).
|
|
|
|
### Other install options
|
|
|
|
| Flag | Effect |
|
|
|------|--------|
|
|
| `--token S` | use secret `S` instead of generating one |
|
|
| `--port N` | listen port (default `3001`) |
|
|
| `--dir P` | upload directory (default `~/Downloads/shared`) |
|
|
| `--ttl N` | file lifetime in days, `0` = keep forever |
|
|
| `--san IP` | extra IP in the cert SAN; repeatable |
|
|
| `--lang ru\|en` | UI language |
|
|
| `--no-service` | write config only, skip systemd |
|
|
| `--force` | overwrite an existing `share.config` |
|
|
|
|
Re-running `install.sh` without `--force` keeps the existing config, so it is safe
|
|
to run again just to reinstall the unit file.
|
|
|
|
## Verify it works
|
|
|
|
```bash
|
|
systemctl --user status share
|
|
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost:3001/files
|
|
# → 401 (no token: correct, auth is on)
|
|
curl -sk -H "Authorization: Bearer YOUR_TOKEN" https://localhost:3001/files
|
|
# → [] (empty file list: correct)
|
|
```
|
|
|
|
A `401` from the first call is the success signal — it means the token gate is
|
|
active. A `200` there means `token` is empty in `share.config` and **anyone on the
|
|
network can read and write your files**.
|
|
|
|
Then open `https://<lan-ip>:3001` from a phone or laptop on the same network. The
|
|
browser will show a certificate warning; that is expected for a self-signed cert —
|
|
accept it once. Enter the token on the login page; it is stored in an `HttpOnly`
|
|
cookie for a year.
|
|
|
|
You can also hand out a pre-authenticated link, `https://<host>:3001/?token=YOUR_TOKEN`.
|
|
Opening it swaps the token for the cookie and immediately redirects to `/`, so the
|
|
token does not stay in the address bar, history or Referer. `?token=` works **only**
|
|
on that entry page — API and download routes require the cookie or an
|
|
`Authorization: Bearer` header, and the token is redacted from the server log.
|
|
|
|
## Networking notes
|
|
|
|
- The server binds `0.0.0.0`, so it is reachable from the whole LAN.
|
|
- The cert's SAN list gets `127.0.0.1` plus the auto-detected LAN IP. If you reach
|
|
the box by another address (a router doing NAT, a second interface, a static IP
|
|
that differs from the detected one), pass it with `--san` or the cert will not
|
|
validate for that address.
|
|
- Changing SANs later: delete `.certs/` and restart — the cert regenerates.
|
|
- Open the port in the firewall if one is active (`ufw allow 3001/tcp`, or the
|
|
`firewalld`/`iptables` equivalent).
|
|
- The service starts on **login**, not boot. For a headless box that should serve
|
|
without anyone logging in: `sudo loginctl enable-linger $USER`.
|
|
|
|
## Managing the service
|
|
|
|
```bash
|
|
systemctl --user restart share # after editing share.config
|
|
systemctl --user stop share
|
|
systemctl --user disable --now share
|
|
journalctl --user -u share -f # live logs
|
|
```
|
|
|
|
`share.config` is read only at startup — every config change needs a restart.
|
|
|
|
## Running without systemd
|
|
|
|
```bash
|
|
python3 share.py # foreground, reads share.config
|
|
python3 share.py -p 4000 --ttl 0 # CLI flags override share.config
|
|
```
|
|
|
|
## Security posture — know what this is
|
|
|
|
This is a LAN convenience tool, not a hardened public service. Deploy accordingly:
|
|
|
|
- The token is a single shared bearer secret. Everyone who has it has full
|
|
read/write/delete access to the upload directory. There are no user accounts.
|
|
- An empty `token` disables auth completely. Never leave it empty.
|
|
- The cert is self-signed, so clients cannot distinguish it from a
|
|
man-in-the-middle cert on first use. Fine on a home LAN, not fine on a hostile
|
|
network.
|
|
- Do not port-forward this to the internet. It is designed for a trusted LAN.
|
|
- `share.config` holds the secret hash and is gitignored — keep it that way. Copy
|
|
it between machines out of band, never through the repo.
|
|
|
|
## Files
|
|
|
|
| Path | Role |
|
|
|------|------|
|
|
| `share.py` | entry point, startup wiring, `hash` subcommand |
|
|
| `server.py` | HTTP handler, routing, auth, upload/download |
|
|
| `config.py` | config file + CLI arg loading, constants |
|
|
| `cert.py` | self-signed cert generation, LAN IP detection |
|
|
| `cleanup.py` | background thread expiring files past `ttl` |
|
|
| `static/index.html` | the entire frontend, single file |
|
|
| `share.config.example` | tracked template — copy to `share.config` |
|
|
| `share.config` | **gitignored**, holds the token hash |
|
|
| `share.service.template` | tracked systemd unit template |
|
|
| `install.sh` | the setup script described above |
|