feat(deploy): add install script and untrack share.config
Package the service for setup on a new machine: - install.sh generates a token, writes share.config (mode 600, token stored as a SHA-256 hash) and installs the systemd user unit from share.service.template - share.config is now gitignored, with share.config.example as the tracked template — the config holds the secret and must not be in the repo - warn at startup when token is empty, since that means open access - SETUP.md documents install, verification and security posture
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# 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.
|
||||
|
||||
## 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 |
|
||||
Reference in New Issue
Block a user