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
6.2 KiB
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 | Noneannotations evaluated at runtime) opensslon 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
git clone <repo-url> ~/projects/share
cd ~/projects/share
./install.sh
That will:
- check python/openssl versions,
- generate a random token, hash it, and write
share.config(mode 600), - install
~/.config/systemd/user/share.servicepointing at wherever you cloned, enable --nowthe 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
./install.sh --token 'my-own-secret'
Or after the fact, without re-running the installer:
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
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.1plus 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--sanor 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 thefirewalld/iptablesequivalent). - 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
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
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
tokendisables 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.configholds 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 |