uploadservice
Publishes static build output to cdn.si/<github-user>/<project>/ over SSH.
The connecting SSH username is the GitHub username; github.com/<username>.keys
is the authorised key set, fetched on first login and cached. Registration on
the site is optional: it triggers the same fetch and creates the user page.
Reads are public. Every write needs the key: rsync over SSH directly, or a
short-lived token issued by the same SSH login for the web form and curl -T.
Documentation
Data
/data/_keys/<user>.pub raw github keys, lowercased user, fetched on login
/data/_ssh/host_ed25519 generated host key
/data/_ssh/token_secret persisted HMAC secret (unless CDN_TOKEN_SECRET is set)
/data/<user>/<project>/... published tree, 50 MiB per user
/data/_staging/... per-rsync clone, discarded on failure
/data/_tmp/... multipart spill
There is no database. Usernames follow GitHub rules and are lowercased.
All filesystem reads below a user root go through os.Root, so a symlink
cannot escape the tree. FIFOs, devices and sockets are never served.
Publishing
There is no registration step. The first SSH login as a GitHub username fetches that user’s published keys and verifies the presented key:
ssh -p 2222 alice@cdn.si whoami
ssh -p 2222 alice@cdn.si token
rsync -a --delete -e 'ssh -p 2222' ./ alice@cdn.si:/docs/
curl -T coverage.html -H "Authorization: Bearer $TOKEN" \
https://cdn.si/alice/docs/coverage.html
The key document is cached and trusted for CDN_KEYS_TTL (5m); a presented
key not in it triggers a refetch. Fetches for one user are at least
CDN_KEYS_FETCH_INTERVAL apart, so a wrong key cannot hit GitHub on every
attempt. If GitHub is unreachable the last cached document is used; if GitHub
answers 404 the login is refused.
Usernames are lowercased and restricted to the GitHub alphabet: alphanumeric and single hyphens, no leading or trailing hyphen, at most 39 characters. The validated name is the only thing used in the keys URL and the data path.
ssh:// URLs are not rsync remote syntax; rsync splits at the first colon.
Use user@host:path with -e 'ssh -p <port>', or drop -e on port 22.
HTTP
| Method | Route | Auth | Purpose |
|---|---|---|---|
| GET | / |
public | Landing and published trees |
| POST | /register |
public | Fetch GitHub keys and create the tree |
| GET | /{user}/{path...} |
public | Listing, rendered page, image, text or download |
| POST | /upload |
token | Multipart upload |
| PUT | /{user}/{path...} |
Bearer token | Single file, 201 |
The upload token is HMAC-SHA256 over v1|user|expiry|nonce, valid for
CDN_TOKEN_TTL (15m default). It can be replayed within its lifetime.
A read answers by file type: a .md file is rendered as an HTML page, an
image is drawn inline, text is served as text/plain, and anything else is
sent with Content-Disposition: attachment so it downloads.
Quota
HTTP writes are exact and atomic: projected usage is checked before any file is renamed into place; over-quota requests return 413 and leave no files.
An rsync session is transactional. The live tree is copied to _staging, the
client writes only to that copy, and the copy is swapped into place only if
the final size is within quota. Watching the staging size kills abusive
transfers at 2*quota+64MiB. A rejected push leaves the previous tree
untouched; the client sees a non-zero exit after the transfer completed.
Configuration
| Variable | Default | Purpose |
|---|---|---|
CDN_DATA |
/data |
Data root |
CDN_HTTP_ADDR |
:8080 |
HTTP listener |
CDN_SSH_ADDR |
:2222 |
SSH listener |
CDN_PUBLIC_URL |
https://cdn.si |
Links and curl examples |
CDN_SSH_HOST |
cdn.si |
Host shown in commands |
CDN_SSH_PORT |
listener port | Port shown in commands |
CDN_QUOTA_BYTES |
52428800 |
Per-user quota |
CDN_TOKEN_TTL |
15m |
Upload token lifetime |
CDN_TOKEN_SECRET |
generated | Shared HMAC secret; set in production |
CDN_GITHUB_BASE |
https://github.com |
Keys source, for tests |
CDN_KEYS_TTL |
5m |
How long a cached key document is trusted |
CDN_KEYS_FETCH_INTERVAL |
30s |
Minimum gap between fetches per user |
CDN_RRSYNC |
/usr/bin/rrsync |
Forced-command wrapper |
CDN_MONITOR_INTERVAL |
100ms |
Staging size sampling |
Local test
atkins test
go test ./...
docker compose up -d --build
bash tests/integration.sh
docker compose down -v
atkins test runs gotestsum and writes the coverage profile and the HTML
report into docs/coverage/, which .gitignore keeps out of the tree.
atkins deploy publishes README.md and docs/ to
titpetric@cdn.si:/cdn/.
compose.yml runs cdn plus a busybox server exposing
tests/fixtures/github/alice.keys. The matching private key is a test fixture;
never use it outside this repository. The container runs as uid 65534. The
compose file uses a named volume; a bind mount needs
chown -R 65534:65534 <dir> first.
One instance per data directory. Reads are public. Only markdown renders as a page; every other file is shown as text or downloaded, so published content cannot run on the site’s origin.