Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c0c80f20b |
@@ -149,3 +149,149 @@ jobs:
|
||||
name: jray-server-x86_64-musl
|
||||
path: target/x86_64-unknown-linux-musl/release/jray-server
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Debian package ────────────────────────────────────────────────────────
|
||||
#
|
||||
# DR-015. The .deb is not a second build of the software: it packages the very
|
||||
# binary the job above already proved static, so the artifact an operator
|
||||
# installs is byte-identical to the one CI verified. Building it twice would
|
||||
# let the two diverge silently.
|
||||
deb:
|
||||
name: debian package
|
||||
runs-on: ubuntu-latest
|
||||
needs: musl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch the verified musl binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: jray-server-x86_64-musl
|
||||
path: prebuilt
|
||||
|
||||
- name: Build the package
|
||||
run: |
|
||||
chmod +x prebuilt/jray-server
|
||||
scripts/build-deb.sh --binary prebuilt/jray-server
|
||||
|
||||
# The install is where packaging actually fails, so it is exercised rather
|
||||
# than assumed: an unattended preseed proves the debconf path works without
|
||||
# a terminal, which is the case a release must not break. `dpkg -i` runs as
|
||||
# root on the runner, and the maintainer scripts skip the systemd wiring
|
||||
# when /run/systemd/system is absent.
|
||||
- name: Install it unattended and check what it configured
|
||||
run: |
|
||||
set -e
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq debconf-utils
|
||||
cat <<'SEED' | sudo debconf-set-selections
|
||||
jray-server jray-server/server-id string ci.example.org
|
||||
jray-server jray-server/bind string 127.0.0.1:8080
|
||||
jray-server jray-server/trusted-proxies string 127.0.0.1
|
||||
jray-server jray-server/tmdb-api-key password ci-key
|
||||
SEED
|
||||
sudo DEBIAN_FRONTEND=noninteractive dpkg -i dist/*.deb
|
||||
|
||||
sudo test -f /etc/jray-server/env
|
||||
[ "$(sudo stat -c '%a' /etc/jray-server/env)" = "600" ] \
|
||||
|| { echo "::error::env file is not mode 600"; exit 1; }
|
||||
sudo grep -q '^JRAY_SERVER_ID=ci.example.org$' /etc/jray-server/env \
|
||||
|| { echo "::error::debconf answer did not reach the env file"; exit 1; }
|
||||
# The key must land in the file and not linger in debconf's database.
|
||||
sudo grep -q '^JRAY_TMDB_API_KEY=ci-key$' /etc/jray-server/env \
|
||||
|| { echo "::error::API key missing from the env file"; exit 1; }
|
||||
if sudo debconf-show jray-server | grep -q 'ci-key'; then
|
||||
echo "::error::API key still present in the debconf database"; exit 1
|
||||
fi
|
||||
sudo /usr/bin/jray-server --version >/dev/null 2>&1 || true
|
||||
echo "installed cleanly"
|
||||
|
||||
# DR-016. Reconfigure is the operation that silently destroys a working
|
||||
# install: press Enter through the password prompt and a naive postinst
|
||||
# blanks the key, after which every upload stays pending forever and the
|
||||
# server looks fine. Asserted, not trusted.
|
||||
#
|
||||
# The second half guards the inverse mistake — the debconf `config` script
|
||||
# seeds unanswered questions from the env file, and an unguarded seed would
|
||||
# overwrite the preseed above, so an unattended install would reconfigure
|
||||
# itself back to whatever was on disk.
|
||||
- name: Reconfigure keeps the key, and updates what it was told to
|
||||
run: |
|
||||
set -e
|
||||
printf 'jray-server jray-server/tmdb-api-key password\n' | sudo debconf-set-selections
|
||||
printf 'jray-server jray-server/contact string changed@example.org\n' | sudo debconf-set-selections
|
||||
echo 'JRAY_JOB_BATCH=32' | sudo tee -a /etc/jray-server/env >/dev/null
|
||||
sudo dpkg-reconfigure -f noninteractive jray-server
|
||||
|
||||
sudo grep -q '^JRAY_TMDB_API_KEY=ci-key$' /etc/jray-server/env \\
|
||||
|| { echo "::error::a blank answer wiped the configured API key"; exit 1; }
|
||||
sudo grep -q '^JRAY_CONTACT=changed@example.org$' /etc/jray-server/env \\
|
||||
|| { echo "::error::preseeded value was overwritten by the env-file seed"; exit 1; }
|
||||
# A setting the package does not manage must survive untouched.
|
||||
sudo grep -q '^JRAY_JOB_BATCH=32$' /etc/jray-server/env \\
|
||||
|| { echo "::error::reconfigure discarded a hand-added setting"; exit 1; }
|
||||
|
||||
- name: Purge, and check the database is not collateral
|
||||
run: |
|
||||
set -e
|
||||
sudo mkdir -p /var/lib/jray-server && sudo touch /var/lib/jray-server/jray.db
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq jray-server
|
||||
sudo test ! -f /etc/jray-server/env \
|
||||
|| { echo "::error::configuration survived purge"; exit 1; }
|
||||
# Deliberate deviation from "purge removes everything": manifests are
|
||||
# real CV compute and federation is not a backup. See postrm.
|
||||
sudo test -f /var/lib/jray-server/jray.db \
|
||||
|| { echo "::error::purge destroyed the database"; exit 1; }
|
||||
|
||||
- name: Upload the package
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jray-server-deb
|
||||
path: dist/*.deb
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Publishing, on tags only ──────────────────────────────────────────
|
||||
#
|
||||
# Two channels, deliberately. The apt registry is the one that gives
|
||||
# operators upgrades; the release asset is for people who would rather not
|
||||
# add a third-party apt source to their machine.
|
||||
#
|
||||
# NOTE: this uses the automatic Actions token. If your Gitea build does not
|
||||
# grant it package:write, replace it with a PAT held in a repository secret
|
||||
# — the symptom is a 401 from the upload below, not a silent no-op.
|
||||
- name: Publish to the Gitea Debian registry
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
DEB=$(ls dist/*.deb)
|
||||
code=$(curl -sS -o /tmp/up.log -w '%{http_code}' \
|
||||
--user "${{ github.repository_owner }}:$TOKEN" \
|
||||
--upload-file "$DEB" \
|
||||
"${{ github.server_url }}/api/packages/${{ github.repository_owner }}/debian/pool/stable/main/upload")
|
||||
echo "upload HTTP $code"; cat /tmp/up.log
|
||||
case "$code" in 201|409) ;; *) echo "::error::registry upload failed"; exit 1 ;; esac
|
||||
|
||||
- name: Attach the package to the release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||
id=$(curl -sS -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" \
|
||||
| sed -n 's/.*"id":[ ]*\([0-9]*\).*/\1/p' | head -1)
|
||||
if [ -z "$id" ]; then
|
||||
id=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}" "$API/releases" \
|
||||
| sed -n 's/.*"id":[ ]*\([0-9]*\).*/\1/p' | head -1)
|
||||
fi
|
||||
[ -n "$id" ] || { echo "::error::could not resolve a release for $TAG"; exit 1; }
|
||||
DEB=$(ls dist/*.deb)
|
||||
curl -sS -X POST -H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@$DEB" \
|
||||
"$API/releases/$id/assets?name=$(basename "$DEB")" >/dev/null
|
||||
echo "attached $(basename "$DEB") to release $TAG"
|
||||
|
||||
@@ -134,6 +134,37 @@ curl -sX POST -H 'content-type: application/json' -d '{}' \
|
||||
|
||||
## Deployment
|
||||
|
||||
### Install from the apt repository
|
||||
|
||||
```sh
|
||||
sudo install -d -m0755 /etc/apt/keyrings
|
||||
curl -fsSL https://gitea.tourolle.paris/api/packages/dtourolle/debian/repository.key \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/gitea-dtourolle.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/gitea-dtourolle.gpg] \
|
||||
https://gitea.tourolle.paris/api/packages/dtourolle/debian stable main" \
|
||||
| sudo tee /etc/apt/sources.list.d/jray-server.list
|
||||
sudo apt update && sudo apt install jray-server
|
||||
```
|
||||
|
||||
The install **asks** for the settings that matter — public hostname, listen
|
||||
address, trusted proxies, TMDB key, contact — because two of them fail silently
|
||||
when unset rather than loudly (see §8, DR-016). Change them later with:
|
||||
|
||||
```sh
|
||||
sudo dpkg-reconfigure jray-server
|
||||
```
|
||||
|
||||
The package ships one static binary, the systemd unit, and an example nginx
|
||||
site at `/usr/share/doc/jray-server/examples/nginx-jray-server.conf`. The
|
||||
example is **not** installed into nginx: the proxy usually runs on a different
|
||||
host, so copy it to whichever machine that is.
|
||||
|
||||
Prefer not to add a third-party apt source? Every tagged release also carries
|
||||
the `.deb` as an asset — `sudo dpkg -i jray-server_*.deb` does the same thing,
|
||||
minus upgrades.
|
||||
|
||||
### Notes
|
||||
|
||||
§8's deployment notes are requirements, not suggestions:
|
||||
|
||||
- Enforce the body cap at **both** the proxy and the app. `client_max_body_size`
|
||||
|
||||
@@ -1510,6 +1510,13 @@ addresses.
|
||||
|
||||
- Ship a **single static binary** (musl target) plus the SQLite file. Optional
|
||||
container image, but neither Docker nor Compose should be required.
|
||||
- Ship a **Debian package** as well (DR-015). It is a third distribution form
|
||||
alongside the raw binary and the container, not a replacement for either: it
|
||||
packages the *same* musl binary CI has already proved static, so what an
|
||||
operator installs is byte-identical to the artifact that was verified.
|
||||
Published to the Gitea Debian registry, so `apt install jray-server` and
|
||||
ordinary upgrades work, and attached to the release for operators who would
|
||||
rather not add a third-party apt source.
|
||||
- Put all database access behind a **thin repository trait** rather than
|
||||
scattering queries through handlers. This is what keeps the Turso/Postgres
|
||||
options above cheap, and it localises the single-writer serialization
|
||||
@@ -1532,6 +1539,46 @@ addresses.
|
||||
plain file copy of a live WAL database). Manifests represent real CV
|
||||
compute; federation (§9a) gives partial resilience but is not a backup.
|
||||
|
||||
### First-run configuration — DR-016
|
||||
|
||||
The settings an operator must get right are not discoverable from the binary,
|
||||
and two of them fail *silently* when unset: without `JRAY_TMDB_API_KEY` every
|
||||
upload stays `pending` and is never listed, and without `JRAY_TRUSTED_PROXIES`
|
||||
the `X-Forwarded-For` header is ignored, so every client shares one rate-limit
|
||||
bucket and every abuse report points at the proxy. Both produce a working
|
||||
server that is quietly doing the wrong thing — the worst kind of default to
|
||||
leave to a README.
|
||||
|
||||
So the package **asks**, at install time, via debconf: public hostname, listen
|
||||
address, trusted proxies, TMDB key, contact, and whether to publish the peer
|
||||
directory. It is re-runnable with `dpkg-reconfigure jray-server`, and
|
||||
preseedable for unattended installs.
|
||||
|
||||
Three properties this has to hold, each of which is a way packaging usually
|
||||
goes wrong:
|
||||
|
||||
- **The generated config is not a dpkg conffile.** It is written from the
|
||||
debconf answers, so shipping it as a conffile would make dpkg prompt on every
|
||||
upgrade about changes the package itself had made.
|
||||
- **Hand edits survive.** Only the keys debconf manages are rewritten;
|
||||
comments, ordering and any other setting are left alone, so editing the file
|
||||
directly and running `dpkg-reconfigure` later do not fight.
|
||||
- **A blank API key on reconfigure keeps the existing one.** Otherwise pressing
|
||||
Enter through a reconfigure would silently unpublish every future upload.
|
||||
|
||||
The database is **not** removed on purge, which knowingly departs from the
|
||||
usual expectation. Manifests are the output of real CV compute on media the
|
||||
operator may no longer have, and federation is explicitly not a backup;
|
||||
destroying that during an `apt purge` is not a trade worth making for
|
||||
tidiness. `postrm` says where the file is and leaves the decision to the
|
||||
operator.
|
||||
|
||||
An example nginx site ships in `/usr/share/doc/jray-server/examples/` rather
|
||||
than being installed into any nginx configuration directory. The proxy commonly
|
||||
runs on a *different* host from the server, so a file dropped into this
|
||||
machine's nginx would be in the wrong place — and §8 leaves the edge to the
|
||||
operator deliberately.
|
||||
|
||||
---
|
||||
|
||||
## 9. JRay plugin integration
|
||||
|
||||
@@ -100,6 +100,8 @@ attempts are rejected.
|
||||
| DR-012 | Dependency audit: advisories, licence policy, source policy | PR-004 | Medium | Done |
|
||||
| DR-013 | API errors use the status codes the spec names, not the framework's defaults | SR-003 | Medium | Done |
|
||||
| DR-014 | Portable SQL — no SQLite-specific form where a standard one exists | PR-004 | Medium | Done |
|
||||
| DR-015 | Debian package installing the binary, the systemd unit and a default configuration | PR-004 | Medium | Done |
|
||||
| DR-016 | First-run configuration is prompted at install time and re-runnable, never hand-written from scratch | PR-004 | Medium | Done |
|
||||
|
||||
---
|
||||
|
||||
@@ -169,6 +171,8 @@ topology is the point, so this is a deliberate choice rather than an oversight.
|
||||
| DR-007 | **static** | The musl release target builds and links one binary needing only a database file | Build property, not a runtime one — a unit test asserting it would assert nothing |
|
||||
| DR-012 | **static** | `cargo deny check` passes advisories, licences and sources | A copyleft-incompatible transitive dependency must fail the build, not be discovered later |
|
||||
| DR-014 | **static** | `schema.sql` uses no SQLite-specific form where a standard one exists | `INSERT OR REPLACE` must not reappear in place of `INSERT ... ON CONFLICT` |
|
||||
| DR-015 | **static** | The package builds, installs unattended and purges without taking the database with it | Exercised in CI by a real `dpkg -i` and `apt purge`, not by inspecting the file list — packaging fails at install time or not at all |
|
||||
| DR-016 | **static** | A preseeded install reaches the env file, and a blank key on reconfigure keeps the existing one | The reconfigure case is the one that silently destroys a working install, so it is asserted rather than trusted |
|
||||
|
||||
Three are worth singling out, because each verifies a claim that would otherwise
|
||||
be an assertion:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
# debconf question script. Runs before unpacking, and again on
|
||||
# `dpkg-reconfigure jray-server`.
|
||||
#
|
||||
# Existing values are read back out of /etc/jray-server/env first, so a
|
||||
# reconfigure shows what is actually in force rather than the package defaults.
|
||||
# Without this, an operator who edited the env file by hand would be shown stale
|
||||
# answers and silently have their edits reverted by postinst.
|
||||
set -e
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
ENV_FILE=/etc/jray-server/env
|
||||
|
||||
# Read one KEY=value out of the env file, ignoring comments. Values are written
|
||||
# unquoted by postinst, so no unquoting is needed.
|
||||
env_value() {
|
||||
[ -f "$ENV_FILE" ] || return 0
|
||||
sed -n "s/^$1=//p" "$ENV_FILE" | tail -1
|
||||
}
|
||||
|
||||
# Seed only a question debconf has never had an answer to.
|
||||
#
|
||||
# The test is the `seen` flag, not whether the value is empty: two of these
|
||||
# templates carry a Default, so db_get returns "localhost" or "127.0.0.1:8080"
|
||||
# for a question nobody has answered, and an emptiness check would never seed
|
||||
# them. `seen` distinguishes "this is the template default" from "somebody chose
|
||||
# this", which is the actual question.
|
||||
#
|
||||
# And it has to be a guard rather than an unconditional db_set. debconf-set-
|
||||
# selections marks what it sets as seen, so an unconditional seed would overwrite
|
||||
# a value the operator had just preseeded — every unattended install would
|
||||
# quietly reconfigure itself back to whatever was already on disk. Preseeding is
|
||||
# the entire point of the unattended path, so debconf wins wherever it has an
|
||||
# answer and the env file only fills in what it does not.
|
||||
seed() { # seed <debconf-key> <env-key>
|
||||
db_fget "$1" seen || RET=""
|
||||
[ "$RET" = "true" ] && return 0
|
||||
v=$(env_value "$2")
|
||||
[ -n "$v" ] && db_set "$1" "$v"
|
||||
return 0
|
||||
}
|
||||
|
||||
seed jray-server/server-id JRAY_SERVER_ID
|
||||
seed jray-server/bind JRAY_BIND
|
||||
seed jray-server/trusted-proxies JRAY_TRUSTED_PROXIES
|
||||
seed jray-server/contact JRAY_CONTACT
|
||||
|
||||
# The API key is deliberately NOT seeded back into the prompt: it is a password
|
||||
# template, so debconf would render it as a filled-in field the operator cannot
|
||||
# read, and accepting it would just rewrite what is already there. Blank means
|
||||
# "keep the existing key" and postinst implements exactly that.
|
||||
|
||||
db_fget jray-server/publish-peer-directory seen || RET=""
|
||||
if [ "$RET" != "true" ] && [ "$(env_value JRAY_PUBLISH_PEER_DIRECTORY)" = "1" ]; then
|
||||
db_set jray-server/publish-peer-directory true
|
||||
fi
|
||||
|
||||
db_input high jray-server/server-id || true
|
||||
db_input high jray-server/bind || true
|
||||
db_input high jray-server/trusted-proxies || true
|
||||
db_input high jray-server/tmdb-api-key || true
|
||||
db_input medium jray-server/contact || true
|
||||
db_input medium jray-server/publish-peer-directory || true
|
||||
db_go || true
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/bin/sh
|
||||
# Configure jray-server from the debconf answers.
|
||||
#
|
||||
# /etc/jray-server/env is deliberately NOT a dpkg conffile. A conffile is for a
|
||||
# file the package ships and the operator may edit; this one is *generated* from
|
||||
# debconf, so shipping it would make dpkg prompt on every upgrade about changes
|
||||
# the package itself had made. Instead it is written here and updated key by
|
||||
# key, which leaves comments, ordering and any setting debconf does not manage
|
||||
# untouched.
|
||||
set -e
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
CONF_DIR=/etc/jray-server
|
||||
ENV_FILE="$CONF_DIR/env"
|
||||
|
||||
# Update one KEY=value in place, appending if absent. Everything else in the
|
||||
# file - comments, blank lines, settings this package does not ask about - is
|
||||
# preserved, which is what makes hand-editing and dpkg-reconfigure coexist.
|
||||
set_kv() {
|
||||
key="$1"; val="$2"
|
||||
if grep -q "^$key=" "$ENV_FILE" 2>/dev/null; then
|
||||
# `|` as the delimiter: values are hostnames, IP lists and URLs, none of
|
||||
# which contain it, whereas `/` appears in contacts and base URLs.
|
||||
sed -i "s|^$key=.*|$key=$val|" "$ENV_FILE"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
mkdir -p "$CONF_DIR"
|
||||
chmod 0755 "$CONF_DIR"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
cat > "$ENV_FILE" <<'EOF'
|
||||
# jray-server configuration.
|
||||
#
|
||||
# Written by the package from your debconf answers; re-run
|
||||
# dpkg-reconfigure jray-server
|
||||
# to change them. Hand edits to this file are preserved: the package updates
|
||||
# only the keys it manages and leaves everything else alone.
|
||||
#
|
||||
# The full set of variables is in SPEC.md section 8 and src/config.rs.
|
||||
|
||||
EOF
|
||||
fi
|
||||
# 0600 before anything is written into it: the TMDB key lands here.
|
||||
chmod 0600 "$ENV_FILE"
|
||||
|
||||
db_get jray-server/server-id && set_kv JRAY_SERVER_ID "$RET"
|
||||
db_get jray-server/bind && set_kv JRAY_BIND "$RET"
|
||||
db_get jray-server/trusted-proxies && set_kv JRAY_TRUSTED_PROXIES "$RET"
|
||||
db_get jray-server/contact && set_kv JRAY_CONTACT "$RET"
|
||||
|
||||
db_get jray-server/publish-peer-directory
|
||||
if [ "$RET" = "true" ]; then
|
||||
set_kv JRAY_PUBLISH_PEER_DIRECTORY 1
|
||||
else
|
||||
set_kv JRAY_PUBLISH_PEER_DIRECTORY 0
|
||||
fi
|
||||
|
||||
# Blank means "keep whatever is already configured" - see the note in the
|
||||
# debconf template. Only overwrite when the operator actually supplied one.
|
||||
db_get jray-server/tmdb-api-key
|
||||
if [ -n "$RET" ]; then
|
||||
set_kv JRAY_TMDB_API_KEY "$RET"
|
||||
elif ! grep -q '^JRAY_TMDB_API_KEY=' "$ENV_FILE" 2>/dev/null; then
|
||||
set_kv JRAY_TMDB_API_KEY ""
|
||||
fi
|
||||
# Drop the secret from debconf's database now that it is in the env file.
|
||||
# config.dat is root-only, so this is defence in depth rather than a fix for
|
||||
# a leak - but there is no reason for a second copy to outlive its use.
|
||||
db_set jray-server/tmdb-api-key "" || true
|
||||
|
||||
set_kv JRAY_DB "/var/lib/jray-server/jray.db"
|
||||
|
||||
# Warn about the two settings whose absence fails silently rather than
|
||||
# loudly. Both are recoverable with dpkg-reconfigure, and neither stops the
|
||||
# service starting, so the operator would otherwise find out from a log line
|
||||
# they had no reason to read.
|
||||
if ! grep -q '^JRAY_TMDB_API_KEY=.' "$ENV_FILE" 2>/dev/null; then
|
||||
echo "jray-server: no TMDB API key set - uploads will stay pending and never be listed." >&2
|
||||
echo " Set one with: dpkg-reconfigure jray-server" >&2
|
||||
fi
|
||||
if ! grep -q '^JRAY_TRUSTED_PROXIES=.' "$ENV_FILE" 2>/dev/null; then
|
||||
echo "jray-server: no trusted proxies set - X-Forwarded-For will be ignored, so every" >&2
|
||||
echo " client shares one rate-limit bucket. Set your proxy's address with:" >&2
|
||||
echo " dpkg-reconfigure jray-server" >&2
|
||||
fi
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure) ;;
|
||||
*) echo "postinst called with unknown argument \`$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# systemd wiring, in the form dh_installsystemd generates. StateDirectory= in the
|
||||
# unit creates and owns /var/lib/jray-server, so there is no directory or user to
|
||||
# set up here.
|
||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ]; then
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl --system daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
if deb-systemd-helper debian-installed jray-server.service 2>/dev/null; then
|
||||
deb-systemd-helper unmask jray-server.service >/dev/null || true
|
||||
if deb-systemd-helper --quiet was-enabled jray-server.service; then
|
||||
deb-systemd-helper enable jray-server.service >/dev/null || true
|
||||
else
|
||||
deb-systemd-helper update-state jray-server.service >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
if [ -d /run/systemd/system ]; then
|
||||
# Starting an unconfigured install is safe by construction: JRAY_BIND
|
||||
# defaults to loopback, so it is not reachable until the operator says
|
||||
# otherwise.
|
||||
deb-systemd-invoke restart jray-server.service >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
DB_DIR=/var/lib/jray-server
|
||||
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl --system daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
purge)
|
||||
# Configuration goes, including the TMDB key.
|
||||
rm -f /etc/jray-server/env
|
||||
rmdir --ignore-fail-on-non-empty /etc/jray-server 2>/dev/null || true
|
||||
|
||||
if [ -f /usr/share/debconf/confmodule ]; then
|
||||
. /usr/share/debconf/confmodule
|
||||
db_purge || true
|
||||
fi
|
||||
|
||||
if [ -x /usr/bin/deb-systemd-helper ]; then
|
||||
deb-systemd-helper purge jray-server.service >/dev/null || true
|
||||
deb-systemd-helper unmask jray-server.service >/dev/null || true
|
||||
fi
|
||||
|
||||
# The database is deliberately NOT deleted on purge, and that is a knowing
|
||||
# deviation from the usual expectation that purge removes everything.
|
||||
#
|
||||
# Manifests are the output of real CV compute on media the operator may no
|
||||
# longer have, and federation (SPEC section 9a) gives partial resilience but
|
||||
# is explicitly not a backup. Silently destroying that during an `apt purge`
|
||||
# - a command people run to clean up - is not a trade worth making for
|
||||
# tidiness. Say where it is instead, and let the operator decide.
|
||||
if [ -d "$DB_DIR" ]; then
|
||||
echo "jray-server: purged, but the database was kept at $DB_DIR" >&2
|
||||
echo " It holds contributed manifests, which are not recoverable" >&2
|
||||
echo " from this package. Remove it yourself if you mean to:" >&2
|
||||
echo " rm -rf $DB_DIR" >&2
|
||||
fi
|
||||
;;
|
||||
|
||||
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
if [ "$1" = remove ] && [ -x /usr/bin/deb-systemd-helper ]; then
|
||||
deb-systemd-helper mask jray-server.service >/dev/null || true
|
||||
fi
|
||||
;;
|
||||
|
||||
*) echo "postrm called with unknown argument \`$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ -d /run/systemd/system ] && [ "$1" = remove ]; then
|
||||
# SIGTERM, which main.rs handles: stop accepting, drain in-flight requests,
|
||||
# let the cast-check worker finish its tick. TimeoutStopSec in the unit gives
|
||||
# it 30 s before systemd escalates.
|
||||
deb-systemd-invoke stop jray-server.service >/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,64 @@
|
||||
Template: jray-server/server-id
|
||||
Type: string
|
||||
Default: localhost
|
||||
Description: Public hostname of this JRay server:
|
||||
Identifies this instance in federation (SPEC section 9a) and is recorded on
|
||||
every manifest it originates, so peers can tell whose judgement they are
|
||||
replicating.
|
||||
.
|
||||
Use the name operators will reach you on, for example jray.example.org.
|
||||
Leaving it as "localhost" is fine for a private trial and wrong for anything
|
||||
federated.
|
||||
|
||||
Template: jray-server/bind
|
||||
Type: string
|
||||
Default: 127.0.0.1:8080
|
||||
Description: Address and port to listen on:
|
||||
The server speaks plain HTTP and expects TLS to be terminated by your reverse
|
||||
proxy (SPEC section 8).
|
||||
.
|
||||
Keep the default if the proxy runs on this same host. If the proxy is
|
||||
elsewhere - a separate container or VM, which is the common case - this must
|
||||
be an address that host can reach, for example 0.0.0.0:8080. Firewall the
|
||||
port to the proxy if you do that: the default is loopback precisely so an
|
||||
unconfigured install is not reachable.
|
||||
|
||||
Template: jray-server/trusted-proxies
|
||||
Type: string
|
||||
Description: Trusted reverse proxy addresses (comma-separated):
|
||||
Rate limiting and abuse-report attribution both key on the client IP, so
|
||||
X-Forwarded-For is honoured only from addresses listed here. A header trusted
|
||||
unconditionally would let any client mint itself a fresh rate-limit budget and
|
||||
pin its reports on someone else.
|
||||
.
|
||||
If the proxy runs on this host, enter 127.0.0.1. If it runs elsewhere, enter
|
||||
the address it connects from - not the address you reach it on.
|
||||
.
|
||||
Leaving this empty is safe but coarse: X-Forwarded-For is then ignored
|
||||
entirely and every request is attributed to the proxy, so all clients share
|
||||
one rate-limit bucket.
|
||||
|
||||
Template: jray-server/tmdb-api-key
|
||||
Type: password
|
||||
Description: TMDB API key:
|
||||
Uploaded manifests are cross-checked against the TMDB cast list before being
|
||||
published (SPEC section 6, stage 3). Without a key the server still serves
|
||||
reads normally, but every upload stays in "pending" and is never listed -
|
||||
the correct failure mode, but a silent one.
|
||||
.
|
||||
Leave blank to configure later with: dpkg-reconfigure jray-server
|
||||
If a key is already configured, leaving this blank keeps it.
|
||||
|
||||
Template: jray-server/contact
|
||||
Type: string
|
||||
Description: Operator contact (optional):
|
||||
Published so other operators can arrange peering out of band. An email
|
||||
address or a URL. Leave blank to publish no contact.
|
||||
|
||||
Template: jray-server/publish-peer-directory
|
||||
Type: boolean
|
||||
Default: false
|
||||
Description: Publish this server's peer directory?
|
||||
Section 9a makes this deliberately optional: publishing lists the peers you
|
||||
replicate from, which discloses your federation topology. A server that would
|
||||
rather not disclose it simply does not, and federation still works.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Example nginx site for jray-server. NOT installed anywhere by the package —
|
||||
# the proxy usually runs on a different host from the server, so a file dropped
|
||||
# into this machine's nginx would be in the wrong place. Copy it to the proxy.
|
||||
#
|
||||
# /etc/nginx/sites-available/jray-server (then symlink into sites-enabled)
|
||||
#
|
||||
# Replace jray.example.org and the upstream address, and point ssl_certificate
|
||||
# at your own certificate.
|
||||
|
||||
upstream jray_server {
|
||||
# The address jray-server listens on. If the proxy runs on the SAME host,
|
||||
# this is 127.0.0.1:8080 and JRAY_BIND can stay at its loopback default. If
|
||||
# the proxy is elsewhere, put the server's address here, set JRAY_BIND to
|
||||
# something that host can reach (0.0.0.0:8080), and firewall the port to
|
||||
# this proxy.
|
||||
server 10.0.0.42:8080;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name jray.example.org;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/jray.example.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/jray.example.org/privkey.pem;
|
||||
|
||||
# SPEC section 6 stage 1 body caps, mirrored at the edge. Section 8 asks for
|
||||
# them in both places: the proxy rejects the bulk before it reaches the
|
||||
# application, and the application stays correct if it is ever run without a
|
||||
# proxy. These must not be tightened below the application's own limits or
|
||||
# legitimate uploads get a 413 from nginx that the server never sees.
|
||||
client_max_body_size 2m;
|
||||
|
||||
location = /api/v1/manifests/bundle {
|
||||
# Series bundles only (section 2). This is why the cap is per-location
|
||||
# rather than one global 25m: widening it everywhere would hand every
|
||||
# other endpoint a 25 MiB budget it has no use for.
|
||||
client_max_body_size 25m;
|
||||
proxy_pass http://jray_server;
|
||||
include snippets/jray-server-proxy.conf;
|
||||
}
|
||||
|
||||
# Liveness. Kept out of the access log because uptime checks poll it hard.
|
||||
location = /health {
|
||||
proxy_pass http://jray_server;
|
||||
include snippets/jray-server-proxy.conf;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://jray_server;
|
||||
include snippets/jray-server-proxy.conf;
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /etc/nginx/snippets/jray-server-proxy.conf
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Connection "";
|
||||
#
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
#
|
||||
# # $proxy_add_x_forwarded_for appends the real peer on the RIGHT of any header
|
||||
# # the client sent. That is the safe form for this server: client_ip() in
|
||||
# # src/auth.rs reads X-Forwarded-For from the right and walks left past further
|
||||
# # trusted hops, so a client that forges its own entries only pollutes the part
|
||||
# # that is ignored. Do not "harden" this to $remote_addr unless you have exactly
|
||||
# # one proxy layer — with two, overwriting loses the real client.
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
#
|
||||
# # Longer than JRAY_REQUEST_TIMEOUT_SEC (30 by default) so the application's own
|
||||
# # timeout fires first and returns a real status rather than nginx reporting 504
|
||||
# # for a request the server was still handling.
|
||||
# proxy_read_timeout 60s;
|
||||
@@ -0,0 +1,57 @@
|
||||
[Unit]
|
||||
Description=JRay public server
|
||||
Documentation=https://gitea.tourolle.paris/dtourolle/JRay-public-server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/bin/jray-server
|
||||
EnvironmentFile=/etc/jray-server/env
|
||||
|
||||
# No user to create at install time: systemd allocates one for the lifetime of
|
||||
# the unit and remaps StateDirectory ownership to it, so the package ships no
|
||||
# useradd and leaves nothing behind on purge.
|
||||
DynamicUser=yes
|
||||
StateDirectory=jray-server
|
||||
StateDirectoryMode=0700
|
||||
WorkingDirectory=/var/lib/jray-server
|
||||
|
||||
# main.rs installs a SIGTERM handler that stops accepting, drains in-flight
|
||||
# requests and lets the cast-check worker finish its tick before exit. SIGTERM is
|
||||
# already systemd's default; this only gives it room to finish rather than being
|
||||
# killed mid-drain.
|
||||
TimeoutStopSec=30
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# The binary is static (musl, bundled SQLite, bundled TLS roots). It opens one
|
||||
# database file under StateDirectory and makes outbound HTTPS calls to TMDB. It
|
||||
# needs nothing else, so everything else is denied.
|
||||
NoNewPrivileges=yes
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ProtectProc=invisible
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
# No AF_UNIX: musl resolves DNS itself from /etc/resolv.conf and the TLS roots
|
||||
# are compiled in (reqwest `rustls-tls` uses webpki-roots), so there is no NSS
|
||||
# socket and no CA bundle to read. A glibc build would need AF_UNIX added back.
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
# build-deb.sh — stage and build the jray-server Debian package.
|
||||
#
|
||||
# Built with dpkg-deb from an explicit staging tree rather than with cargo-deb.
|
||||
# The reason is debconf: its `config` script and `templates` live in the control
|
||||
# archive alongside the maintainer scripts, and controlling that archive directly
|
||||
# is simpler than discovering what a wrapper will and will not copy into it.
|
||||
# dpkg-dev is present on any Debian builder, so this adds no build dependency.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/build-deb.sh # build the binary, then package
|
||||
# scripts/build-deb.sh --binary path/to/bin # package an existing binary
|
||||
# scripts/build-deb.sh --version 1.2.3 # override the computed version
|
||||
# scripts/build-deb.sh --out dist # output directory
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
TARGET="x86_64-unknown-linux-musl"
|
||||
BINARY=""
|
||||
VERSION=""
|
||||
OUT="$REPO_ROOT/dist"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--binary) BINARY="${2:?--binary needs a path}"; shift ;;
|
||||
--version) VERSION="${2:?--version needs a value}"; shift ;;
|
||||
--out) OUT="${2:?--out needs a path}"; shift ;;
|
||||
-h|--help) sed -n '2,16p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
*) echo "error: unknown argument '$1'" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# ── Version ─────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# A tagged commit packages as that tag. Anything else packages as a PRE-release
|
||||
# of the version in Cargo.toml: `0.1.0~git20260905.9bcc765` sorts BELOW `0.1.0`
|
||||
# in dpkg's ordering, because `~` sorts before everything including the empty
|
||||
# string. That is what makes a master build upgradeable to the eventual release
|
||||
# rather than blocking it — the mistake would be `0.1.0+git...`, which sorts
|
||||
# above and would leave apt refusing the real 0.1.0.
|
||||
if [ -z "$VERSION" ]; then
|
||||
if tag=$(git describe --exact-match --tags HEAD 2>/dev/null); then
|
||||
VERSION="${tag#v}"
|
||||
else
|
||||
cargo_version=$(sed -n 's/^version *= *"\(.*\)"/\1/p' Cargo.toml | head -1)
|
||||
VERSION="${cargo_version}~git$(date -u +%Y%m%d).$(git rev-parse --short HEAD)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Binary ──────────────────────────────────────────────────────────────────
|
||||
if [ -z "$BINARY" ]; then
|
||||
echo "=== building $TARGET"
|
||||
cargo build --release --target "$TARGET"
|
||||
BINARY="target/$TARGET/release/jray-server"
|
||||
fi
|
||||
|
||||
[ -f "$BINARY" ] || { echo "error: no binary at $BINARY" >&2; exit 1; }
|
||||
|
||||
# The package claims no libc dependency, which is only honest if the binary
|
||||
# genuinely has none. Asserted with `file` rather than `ldd`: a musl static-PIE
|
||||
# makes ldd print the musl loader path, so an ldd check calls a static binary
|
||||
# dynamic. Same reasoning as the Dockerfile and the CI musl job.
|
||||
if command -v file >/dev/null 2>&1; then
|
||||
linkage=$(file -b "$BINARY")
|
||||
case "$linkage" in
|
||||
*"static-pie linked"*|*"statically linked"*) ;;
|
||||
*) echo "error: $BINARY is not static ($linkage)." >&2
|
||||
echo " The package declares no libc dependency, so a dynamic" >&2
|
||||
echo " binary here would install cleanly and then fail to run." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── Stage ───────────────────────────────────────────────────────────────────
|
||||
STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
|
||||
install -d -m 0755 "$STAGE/DEBIAN"
|
||||
install -d -m 0755 "$STAGE/usr/bin"
|
||||
install -d -m 0755 "$STAGE/lib/systemd/system"
|
||||
install -d -m 0755 "$STAGE/usr/share/doc/jray-server/examples"
|
||||
|
||||
install -m 0755 "$BINARY" "$STAGE/usr/bin/jray-server"
|
||||
install -m 0644 packaging/jray-server.service "$STAGE/lib/systemd/system/jray-server.service"
|
||||
install -m 0644 packaging/examples/nginx-jray-server.conf \
|
||||
"$STAGE/usr/share/doc/jray-server/examples/nginx-jray-server.conf"
|
||||
install -m 0644 SPEC.md "$STAGE/usr/share/doc/jray-server/SPEC.md"
|
||||
install -m 0644 README.md "$STAGE/usr/share/doc/jray-server/README.md"
|
||||
|
||||
# Licences. LICENSE-DATA is not the code licence: contributed manifests are CC0
|
||||
# while the server itself is GPLv3, and shipping only one of them would misstate
|
||||
# what the operator is redistributing.
|
||||
install -m 0644 LICENSE "$STAGE/usr/share/doc/jray-server/LICENSE"
|
||||
install -m 0644 LICENSE-DATA "$STAGE/usr/share/doc/jray-server/LICENSE-DATA"
|
||||
|
||||
install -m 0755 packaging/debian/config "$STAGE/DEBIAN/config"
|
||||
install -m 0755 packaging/debian/postinst "$STAGE/DEBIAN/postinst"
|
||||
install -m 0755 packaging/debian/prerm "$STAGE/DEBIAN/prerm"
|
||||
install -m 0755 packaging/debian/postrm "$STAGE/DEBIAN/postrm"
|
||||
install -m 0644 packaging/debian/templates "$STAGE/DEBIAN/templates"
|
||||
|
||||
SIZE=$(du -ks "$STAGE" | cut -f1)
|
||||
|
||||
# No libc, no libsqlite3, no CA bundle: SQLite is compiled in (`rusqlite`
|
||||
# bundled), the TLS roots are compiled in (`reqwest` rustls-tls uses
|
||||
# webpki-roots), and the target is musl static. The only dependencies are the
|
||||
# two the maintainer scripts themselves call.
|
||||
cat > "$STAGE/DEBIAN/control" <<EOF
|
||||
Package: jray-server
|
||||
Version: $VERSION
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Depends: debconf (>= 0.5) | debconf-2.0, init-system-helpers (>= 1.54)
|
||||
Installed-Size: $SIZE
|
||||
Maintainer: Duncan Tourolle <duncan@tourolle.paris>
|
||||
Homepage: https://gitea.tourolle.paris/dtourolle/JRay-public-server
|
||||
Description: JRay public server - community manifest exchange
|
||||
Serves and accepts JRay manifests: per-actor scene windows contributed by
|
||||
media-centre users and matched to a cut by runtime and audio signature.
|
||||
.
|
||||
One static binary and one SQLite file, behind a reverse proxy the operator
|
||||
provides. The server stores no binary content by design - no images, no
|
||||
embeddings, no opaque blobs - which is what makes it safe for a volunteer to
|
||||
run.
|
||||
.
|
||||
An example nginx site is installed under
|
||||
/usr/share/doc/jray-server/examples/, to be copied to whichever host runs
|
||||
your proxy.
|
||||
EOF
|
||||
|
||||
mkdir -p "$OUT"
|
||||
DEB="$OUT/jray-server_${VERSION}_amd64.deb"
|
||||
dpkg-deb --root-owner-group --build "$STAGE" "$DEB" >/dev/null
|
||||
|
||||
echo "=== built $DEB"
|
||||
dpkg-deb --info "$DEB" | sed 's/^/ /'
|
||||
echo "=== contents"
|
||||
dpkg-deb --contents "$DEB" | sed 's/^/ /'
|
||||
Reference in New Issue
Block a user