cdn.si

rendered markdown titpetric

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.