From 34e74e94367835861bf188034650620a250a5c3e Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Tue, 14 Jul 2026 19:29:15 +0800 Subject: [PATCH 1/2] docs(relay-server): clarify self-host account setup for open-source users Expand the relay README with an account-enabled deploy checklist, and make deploy.sh remind operators with the exact add-user command when no accounts exist. --- src/apps/relay-server/README.md | 346 ++++++++++++++++++++++---------- src/apps/relay-server/deploy.sh | 42 +++- 2 files changed, 275 insertions(+), 113 deletions(-) diff --git a/src/apps/relay-server/README.md b/src/apps/relay-server/README.md index f5a60e79c8..764d6ecc79 100644 --- a/src/apps/relay-server/README.md +++ b/src/apps/relay-server/README.md @@ -1,35 +1,181 @@ # BitFun Relay Server -WebSocket relay server for BitFun Remote Connect. It bridges desktop (WebSocket) and mobile (HTTP) clients while forwarding end-to-end encrypted payloads. +WebSocket / HTTP relay for BitFun **Remote Connect** and **account login**. + +Open-source BitFun does **not** ship a public hosted login service. If you want +Desktop / CLI **account login**, cross-device session & settings sync, or +**Peer Device Mode** (control another online device on the same account), you +must: + +1. Deploy this relay yourself +2. Enable the account database (`RELAY_DB_PATH`) +3. Create user accounts out-of-band with `relay-admin` (no public sign-up) +4. Point BitFun Desktop or CLI at your relay URL and log in + +The relay stays **zero-knowledge**: clients encrypt with a master key derived +locally; the server stores Argon2id password hashes and AES-GCM-wrapped keys, +never plaintext passwords or decryptable sync payloads. + +## Two operating modes + +| Mode | When | What you get | +|------|------|----------------| +| **Pure relay** | `RELAY_DB_PATH` unset | Room pairing + mobile HTTP ↔ Desktop WebSocket bridge only. **No** account login, sync, or Peer Device Mode. | +| **Account-enabled** | `RELAY_DB_PATH` set to a persistent SQLite path | Everything above **plus** login, device presence, device RPC (Peer HostInvoke), encrypted session/settings sync. | + +Docker Compose in this directory **already enables account mode** +(`RELAY_DB_PATH=/app/data/bitfun_relay.db`). Manual / cargo runs must set the +variable yourself or accounts stay disabled. ## Features -- Desktop connects via WebSocket, mobile via HTTP +- Desktop and CLI connect via WebSocket; mobile uses HTTP - End-to-end encrypted passthrough (the server does not decrypt payloads) - Correlation-based HTTP-to-WebSocket request-response matching - Per-room mobile-web static file upload and serving - Heartbeat-based connection management with configurable room TTL -- Optional zero-knowledge account storage (E2E encrypted — the server never sees passwords or master keys) +- Optional zero-knowledge account storage + device routing + sync - Docker deployment support with optional Caddy reverse proxy -## Quick Start +## Open-source: enable account login (recommended path) -### Recommended: Run on the target server +Use this checklist on a machine you control (VPS, LAN server, or localhost). + +### 1. Deploy the relay ```bash -# Clone on the target server git clone https://github.com/GCWing/BitFun cd BitFun/src/apps/relay-server - -# Deploy to the current machine bash deploy.sh ``` -`deploy.sh` must run on the target server itself. It deploys to the current machine only and does not SSH to another host. +`deploy.sh` must run **on the target server** (it does not SSH elsewhere). +Requires Docker and Docker Compose. + +After a successful start, the script runs `relay-admin list-users`. If the +database has **no accounts**, it prints the exact `add-user` command to run +next (account login will not work until you create at least one user). + +Verify: + +```bash +curl -fsS http://127.0.0.1:9700/health +docker compose ps +``` + +### 2. Confirm account database is on + +Compose sets: + +```yaml +RELAY_DB_PATH=/app/data/bitfun_relay.db +``` + +Data lives in the `relay-db` Docker volume. If you run the binary without +Compose, export a persistent path first: + +```bash +export RELAY_DB_PATH=/var/lib/bitfun/bitfun_relay.db +mkdir -p "$(dirname "$RELAY_DB_PATH")" +RELAY_PORT=9700 ./target/release/bitfun-relay-server +``` + +If the process logs `RELAY_DB_PATH not set — account features disabled`, login +will fail with “account features disabled” until you fix the env and restart. + +### 3. Create accounts (`relay-admin`) + +There is **no** public registration API. Operators create users with +`relay-admin` (bundled in the Docker image). `--db` must be the **same path** +as `RELAY_DB_PATH`. + +```bash +# Interactive password prompt (recommended) +docker exec -it bitfun-relay \ + /app/relay-admin --db /app/data/bitfun_relay.db add-user --username alice + +# Non-interactive (scripts / CI) +docker exec bitfun-relay \ + /app/relay-admin --db /app/data/bitfun_relay.db add-user \ + --username alice --password 'choose-a-strong-password' + +# List accounts +docker exec bitfun-relay \ + /app/relay-admin --db /app/data/bitfun_relay.db list-users +``` + +Other commands: + +```bash +# Reset password (also rotates the master key — old synced blobs become unreadable) +docker exec -it bitfun-relay \ + /app/relay-admin --db /app/data/bitfun_relay.db reset-password --username alice + +# Rename (credentials / user_id unchanged) +docker exec bitfun-relay \ + /app/relay-admin --db /app/data/bitfun_relay.db rename-user \ + --username alice --new-username alice2 + +# Delete account and all of its relay-side data +docker exec bitfun-relay \ + /app/relay-admin --db /app/data/bitfun_relay.db delete-user --username alice +``` + +Without Docker, build and run the same tool from this crate: + +```bash +cargo build --release -p bitfun-relay-server +./target/release/relay-admin --db "$RELAY_DB_PATH" add-user --username alice +``` + +### 4. Point BitFun clients at your relay + +Relay URL examples: + +- Direct: `http://:9700` +- Localhost: `http://127.0.0.1:9700` +- Behind a reverse proxy: `https://relay.example.com` (only add a path prefix + such as `/relay` if your proxy is configured that way) + +**Desktop** + +1. Open account / login UI (or Remote Connect self-hosted settings, depending + on your build). +2. Set **Auth Server / Relay URL** to the URL above. +3. Sign in with the username and password you created with `relay-admin`. + +**CLI** + +1. Run `bitfun-cli`, open `/login`. +2. Fill **Auth Server**, **Username**, **Password**, then Login. +3. After login, the CLI can act as a **Peer Host** for same-account Desktops. + +Clients remember a non-secret hint (`~/.bitfun/account_hint.json`: username + +relay URL) and an encrypted session file for restart without retyping the +password. + +### 5. What works after login + +- Encrypted **settings / session sync** across devices on the same account +- **Device list** and online presence for that account +- **Peer Device Mode**: one Desktop controls another online Desktop **or** CLI + host over device RPC (`HostInvoke` / `DeviceEvent`) +- Same machine Desktop + CLI share one `device_id`; the **last successful** + `AuthConnect` wins as the live Peer Host for that id + +## Quick Start (service ops) + +### Recommended: Run on the target server + +```bash +git clone https://github.com/GCWing/BitFun +cd BitFun/src/apps/relay-server +bash deploy.sh +``` ### Service Operations -Run these commands on the target server inside this directory: +Run these on the target server inside this directory: ```bash bash start.sh @@ -46,45 +192,39 @@ Notes: - `restart.sh` restarts the service when running, or starts it when stopped. - The container uses `restart: unless-stopped`. -### What URL should I fill in BitFun Desktop? - -In **Remote Connect → Self-Hosted → Server URL**, use one of: - -- `http://:9700` - -`/relay` is only needed when your reverse proxy is configured with that path prefix. - ### Network Binding -By default, the relay process listens on `0.0.0.0:9700` (all interfaces) and Docker Compose publishes the container port on the host's `0.0.0.0:9700`. +By default the relay listens on `0.0.0.0:9700` and Compose publishes that port +on the host. -If you need to restrict the service to localhost only, set the environment variable before running `start.sh`/`restart.sh`/`deploy.sh`: +Restrict to localhost: ```bash export RELAY_HOST_BIND_IP=127.0.0.1 bash deploy.sh ``` -### Manual Run +### Manual Run (without Docker) ```bash -# From project root +# From repository root cargo build --release -p bitfun-relay-server -# Run +# Account-enabled (persistent DB path required for login) +export RELAY_DB_PATH="$HOME/.bitfun-relay/bitfun_relay.db" +mkdir -p "$(dirname "$RELAY_DB_PATH")" RELAY_PORT=9700 ./target/release/bitfun-relay-server ``` ## Deployment Checklist -1. Open required ports: - - `9700` for direct relay access - - `80/443` when using Caddy or another reverse proxy -2. Verify the health endpoint: - - `http://:9700/health` -3. Decide the final URL strategy: - - direct port or reverse proxy domain -4. Fill the same URL into BitFun Desktop custom server settings +1. Open ports: `9700` (direct), and `80/443` if using Caddy / another proxy. +2. Hit `http://:9700/health`. +3. Confirm `RELAY_DB_PATH` if you need accounts (Compose does this for you). +4. Create at least one user with `relay-admin`. +5. Fill the same relay URL into Desktop / CLI and log in. +6. If you terminate TLS on a reverse proxy, raise body size and read timeouts + (see sync + device RPC notes below). ## Environment Variables @@ -94,7 +234,7 @@ RELAY_PORT=9700 ./target/release/bitfun-relay-server | `RELAY_STATIC_DIR` | _(none)_ | Path to mobile web static files fallback SPA. When unset, no fallback static files are served. Docker Compose sets this to `/app/static`. | | `RELAY_ROOM_WEB_DIR` | `/tmp/bitfun-room-web` | Directory for per-room uploaded mobile-web files. Docker Compose uses a named volume mounted at `/app/room-web`. | | `RELAY_ROOM_TTL` | `3600` | Room TTL in seconds (0 = no expiry) | -| `RELAY_DB_PATH` | _(none)_ | SQLite database path for account storage. When unset, the relay runs in pure-relay mode with no account features. Set to a persistent path (e.g. `/app/data/bitfun_relay.db`) to enable account login, device routing, and cross-device session/settings sync. The server stays zero-knowledge: it only stores Argon2id password hashes and AES-GCM-wrapped master keys — never plaintext passwords or master keys. Accounts are provisioned via the `relay-admin` CLI (see below). | +| `RELAY_DB_PATH` | _(none)_ | SQLite path for account storage. **Unset = pure relay (no login).** Set a persistent path (Compose: `/app/data/bitfun_relay.db`) to enable login, device routing, and sync. Accounts are provisioned only via `relay-admin`. | ## API Endpoints @@ -102,43 +242,41 @@ RELAY_PORT=9700 ./target/release/bitfun-relay-server | Endpoint | Method | Description | |----------|--------|-------------| -| `/health` | GET | Health check (returns status, version, uptime, room and connection counts) | +| `/health` | GET | Health check (status, version, uptime, room and connection counts) | | `/api/info` | GET | Server info (name, version, protocol version) | -### Account (optional — requires `RELAY_DB_PATH`) +### Account (requires `RELAY_DB_PATH`) -Zero-knowledge authentication. Clients derive an Argon2id KEK locally and send only password hashes; the server never sees plaintext passwords or master keys. Brute-force protection: per-account exponential-backoff lockout + per-IP sliding-window rate limit. Accounts are **provisioned out-of-band** (no public registration endpoint) so the relay never handles a password. +Zero-knowledge authentication. Clients derive an Argon2id KEK locally and send +only password hashes. Brute-force protection: per-account lockout + per-IP rate +limit. **No public registration endpoint.** | Endpoint | Method | Description | |----------|--------|-------------| | `/api/auth/login/challenge` | POST | Fetch KDF params + wrapped master key for local derivation | | `/api/auth/login` | POST | Verify password hash and issue a token; returns `{ token, user_id }` | +| `/api/auth/logout` | POST | Revoke the caller's token | +| `/api/auth/delegate` | POST | Issue a delegated token for a paired client (authenticated caller) | -#### Account Provisioning (`relay-admin` CLI) +### Devices (requires `RELAY_DB_PATH` + Bearer token) -Accounts are created out-of-band via the `relay-admin` binary (shipped inside the Docker image). No public registration endpoint is exposed. +Used by Desktop / CLI / mobile-web for presence and Peer Device Mode RPC. -```bash -# All commands require --db pointing to the same SQLite file as RELAY_DB_PATH - -# Add an account (interactive password prompt, recommended) -docker exec -it /app/relay-admin --db /app/data/bitfun_relay.db add-user --username alice - -# Add with password on the command line (for scripts) -docker exec /app/relay-admin --db /app/data/bitfun_relay.db add-user --username alice --password "Secret123!" - -# List all accounts -docker exec /app/relay-admin --db /app/data/bitfun_relay.db list-users +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/devices` | GET | List devices for the account (online + offline) | +| `/api/devices/:target_device_id/rpc` | POST | Route an opaque encrypted RPC to an **online** device (waits up to **120s**) | +| `/api/devices/:target_device_id` | DELETE | Remove a device registration (and drop any live WS session) | -# Reset password (generates new salts + new master key; -# previously synced encrypted data becomes unreadable) -docker exec -it /app/relay-admin --db /app/data/bitfun_relay.db reset-password --username alice +#### Device RPC timeouts (Peer HostInvoke) -# Delete an account and all its data -docker exec /app/relay-admin --db /app/data/bitfun_relay.db delete-user --username alice -``` +`POST /api/devices/:target_device_id/rpc` waits up to **120 seconds** for the +target device (`RPC_TIMEOUT` in `src/routes/devices.rs`). Peer Device Mode uses +this for product `invoke` calls. -The tool performs the same client-side Argon2id key derivation and AES-256-GCM master-key wrapping as the login client, then writes only non-secret artifacts (salts, hashes, wrapped key) to the database. Passwords are never stored. When `--password` is omitted the tool prompts with hidden input and asks for confirmation. +Reverse proxies in front of the relay must use a read / response timeout +**≥ 120s** (recommend 130s), or clients see **HTTP 504** before Axum finishes. +See `Caddyfile` for `transport http` timeout settings. ### Room Operations (Mobile HTTP → Desktop WS bridge) @@ -160,11 +298,12 @@ The tool performs the same client-side Argon2id key derivation and AES-256-GCM m | Endpoint | Method | Description | |----------|--------|-------------| -| `/ws` | WebSocket | Desktop client connection endpoint | +| `/ws` | WebSocket | Desktop **and CLI** account / room clients | -### Cross-Device Sync (optional — requires `RELAY_DB_PATH` + Bearer token) +### Cross-Device Sync (requires `RELAY_DB_PATH` + Bearer token) -Encrypted session and settings blobs, stored opaquely on the relay. All payloads are AES-256-GCM encrypted client-side with the account master key; the relay cannot read them. +Encrypted session and settings blobs. All payloads are AES-256-GCM encrypted +client-side with the account master key; the relay cannot read them. | Endpoint | Method | Description | |----------|--------|-------------| @@ -177,14 +316,15 @@ Encrypted session and settings blobs, stored opaquely on the relay. All payloads #### Request body size limits (Axum vs reverse proxy) -Session sync posts a **full** encrypted session bundle (metadata + all dialog turns). Long conversations with large tool outputs routinely exceed Axum’s default ~2 MiB request body limit and fail with **HTTP 413 Payload Too Large**. +Session sync posts a **full** encrypted session bundle. Large conversations can +exceed Axum’s default ~2 MiB limit and fail with **HTTP 413**. -This server raises the limit on `POST /api/sync/sessions` and `POST /api/sync/settings` to **64 MiB** (`SYNC_BODY_LIMIT` in `src/routes/sync.rs`). - -If you put nginx / Caddy / another reverse proxy in front of the relay (common for TLS on port 443 → container `9700`), you must also raise the proxy body limit, or the proxy will reject large uploads **before** Axum sees them: +This server raises the limit on sync POSTs to **64 MiB** (`SYNC_BODY_LIMIT` in +`src/routes/sync.rs`). Proxies must raise their body limit too, or they reject +uploads before Axum sees them: ```nginx -# nginx example — must be >= Axum SYNC_BODY_LIMIT (64M) +# nginx — must be >= Axum SYNC_BODY_LIMIT (64M) client_max_body_size 100M; ``` @@ -192,22 +332,18 @@ client_max_body_size 100M; # Caddy: request_body { max_size 100MB } ``` -When diagnosing 413s, check **both** gates: reverse-proxy `client_max_body_size` (or equivalent) **and** Axum `DefaultBodyLimit`. The effective limit is the stricter of the two. Direct host-port access (e.g. `host:9701` without a proxy) only hits the Axum limit. - -#### Device RPC timeouts (Peer HostInvoke) - -`POST /api/devices/:target_device_id/rpc` waits up to **120 seconds** for the target device to answer over WebSocket (`RPC_TIMEOUT` in `src/routes/devices.rs`). Peer Device Mode uses this path for every product `invoke`. - -If a reverse proxy sits in front of the relay, its read / response timeouts must be **≥ 120s** (recommend 130s), or the proxy returns **HTTP 504 Gateway Timeout** before Axum can. See `Caddyfile` for the `transport http` timeout settings. +When diagnosing 413s, check **both** the proxy and Axum. Direct host-port access +only hits the Axum limit. -## WebSocket Protocol (Desktop Only) +## WebSocket Protocol -Only desktop clients connect via WebSocket. Mobile clients use the HTTP endpoints above. +Desktop and CLI use WebSocket for rooms and/or account device routing. Mobile +clients use the HTTP endpoints above. -### Desktop → Server (Inbound) +### Client → Server (Inbound) ```json -// Create a room +// Create a room (Remote Connect room bridge) { "type": "create_room", "room_id": "optional-id", "device_id": "...", "device_type": "desktop", "public_key": "base64..." } // Respond to a bridged HTTP request (pair or command) @@ -216,53 +352,43 @@ Only desktop clients connect via WebSocket. Mobile clients use the HTTP endpoint // Heartbeat { "type": "heartbeat" } -// Account-authenticated device routing (parallel to room pairing) +// Account-authenticated device routing (requires RELAY_DB_PATH) { "type": "auth_connect", "token": "...", "device_name": "..." } { "type": "device_message", "target_device_id": "...", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." } ``` -### Server → Desktop (Outbound) +A second `auth_connect` with the same `(user_id, device_id)` **replaces** the +previous live connection (last connect wins). + +### Server → Client (Outbound) ```json -// Room created confirmation { "type": "room_created", "room_id": "..." } - -// Pair request forwarded from mobile HTTP { "type": "pair_request", "correlation_id": "...", "public_key": "base64...", "device_id": "...", "device_name": "..." } - -// Encrypted command forwarded from mobile HTTP { "type": "command", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." } - -// Heartbeat acknowledgment { "type": "heartbeat_ack" } - -// Device routing responses { "type": "auth_ok", "user_id": "...", "device_id": "..." } { "type": "auth_error", "message": "..." } { "type": "incoming_device_message", "source_device_id": "...", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." } { "type": "device_presence", "devices": [{ "device_id": "...", "device_name": "..." }] } - -// Error { "type": "error", "message": "..." } ``` ## Architecture ``` -Mobile Phone ──HTTP POST──► Relay Server ◄──WebSocket── Desktop Client - │ - E2E Encrypted - (server cannot - read messages) +Mobile ──HTTP──► Relay ◄──WebSocket── Desktop / CLI + │ + opaque E2E payloads + (optional SQLite for + accounts / sync / devices) ``` -The relay server bridges HTTP and WebSocket: - -- **Desktop** connects via WebSocket, creates a room, and stays connected. -- **Mobile** sends HTTP POST requests such as `/pair` and `/command`. -- The relay forwards requests to the desktop over WebSocket with correlation IDs, waits for the response, and returns it over HTTP. -- The relay only manages rooms and forwards opaque encrypted payloads. -- Per-room mobile-web static files can be uploaded and served at `/r/:room_id/`. +- **Room bridge**: Desktop creates a room; mobile posts `/pair` and `/command`; + the relay correlates HTTP ↔ WebSocket without reading ciphertext. +- **Account plane** (when `RELAY_DB_PATH` is set): clients log in over HTTP, + then `auth_connect` on WebSocket; device RPC and sync store opaque blobs. +- Per-room mobile-web files can be served at `/r/:room_id/`. ## Directory structure @@ -277,20 +403,20 @@ relay-server/ │ ├── bin/ │ │ └── relay_admin.rs # relay-admin CLI binary │ ├── relay/ # Room manager + device routing manager -│ └── routes/ # HTTP/WS route handlers (auth, sync, api, websocket) +│ └── routes/ # HTTP/WS route handlers (auth, devices, sync, api, websocket) ├── static/ # Mobile-web static files -├── Cargo.toml # Crate manifest -├── Dockerfile # Docker build -├── docker-compose.yml # Docker Compose config -├── Caddyfile # Optional reverse proxy config -├── deploy.sh # Deploy on the target server itself -├── start.sh # Start service if not already running -├── stop.sh # Stop running service -├── restart.sh # Restart service, or start if stopped +├── Cargo.toml +├── Dockerfile +├── docker-compose.yml # Sets RELAY_DB_PATH for account mode +├── Caddyfile # Optional reverse proxy (body + RPC timeouts) +├── deploy.sh +├── start.sh / stop.sh / restart.sh └── README.md ``` ## About `src/apps/server` vs `src/apps/relay-server` -- Remote Connect self-hosted deployment uses the relay server in this directory. -- `src/apps/server` is a different application and is not the relay service used by mobile and desktop Remote Connect. +- Self-hosted Remote Connect **and** open-source account login use **this** + `relay-server` directory. +- `src/apps/server` is a different application and is not the relay used by + Desktop / CLI / mobile Remote Connect. diff --git a/src/apps/relay-server/deploy.sh b/src/apps/relay-server/deploy.sh index 905c4a511f..638e3f3332 100755 --- a/src/apps/relay-server/deploy.sh +++ b/src/apps/relay-server/deploy.sh @@ -118,10 +118,46 @@ fi echo "" echo "=== Deploy complete ===" echo "Relay server running on port 9700" -echo "Caddy proxy on ports 80/443" echo "" -echo "Custom Server URL examples for BitFun Desktop:" -echo " - Direct relay: http://:9700" + +CONTAINER_NAME="bitfun-relay" +RELAY_ADMIN_DB="/app/data/bitfun_relay.db" +ADD_USER_CMD="docker exec -it ${CONTAINER_NAME} /app/relay-admin --db ${RELAY_ADMIN_DB} add-user --username " + +print_client_url_hint() { + echo "Point BitFun Desktop / CLI Auth Server URL to:" + echo " http://:9700" + echo "See README.md for sync, Peer Device Mode, and proxy timeouts." +} + +if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1 \ + && [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" = "true" ]; then + # Empty DB prints "No accounts found."; otherwise a USERNAME header + rows. + USER_LIST="$( + docker exec "$CONTAINER_NAME" /app/relay-admin --db "$RELAY_ADMIN_DB" list-users 2>/dev/null || true + )" + if echo "$USER_LIST" | grep -q '^No accounts found\.'; \ + || ! echo "$USER_LIST" | grep -q '^USERNAME'; then + echo "No relay accounts yet. Account login will not work until you create one." + echo "Run:" + echo " ${ADD_USER_CMD}" + echo "(omit --password to enter the password interactively)" + echo "" + print_client_url_hint + else + USER_COUNT="$( + echo "$USER_LIST" | awk 'NR>2 && NF { count++ } END { print count+0 }' + )" + echo "Relay accounts found: ${USER_COUNT}" + echo "" + print_client_url_hint + fi +else + echo "Warning: container '${CONTAINER_NAME}' is not running; skipped account check." + echo "After it is up, create an account with:" + echo " ${ADD_USER_CMD}" +fi + echo "" echo "Check status: docker compose ps" echo "Start: bash start.sh" From 007e00a3a5b993c9fbe58a5f26ca228e3b0d3d75 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Tue, 14 Jul 2026 19:42:23 +0800 Subject: [PATCH 2/2] feat: harden relay deploy for amd64/arm64 and add CLI install.sh Make relay one-click scripts portable across Compose V1/V2 and native linux/amd64+arm64 hosts, and add a CLI install script that builds release, installs to ~/.local/bin, and wires bashrc/zshrc PATH. --- AGENTS-CN.md | 1 + AGENTS.md | 1 + package.json | 1 + src/apps/cli/AGENTS.md | 6 + src/apps/cli/README.md | 58 +++++ src/apps/cli/install.sh | 261 +++++++++++++++++++++++ src/apps/relay-server/Dockerfile | 38 +++- src/apps/relay-server/README.md | 23 +- src/apps/relay-server/common.sh | 183 ++++++++++++++++ src/apps/relay-server/deploy.sh | 146 +++++-------- src/apps/relay-server/docker-compose.yml | 12 ++ src/apps/relay-server/restart.sh | 36 +--- src/apps/relay-server/start.sh | 38 +--- src/apps/relay-server/stop.sh | 32 +-- 14 files changed, 648 insertions(+), 188 deletions(-) create mode 100644 src/apps/cli/README.md create mode 100755 src/apps/cli/install.sh create mode 100755 src/apps/relay-server/common.sh diff --git a/AGENTS-CN.md b/AGENTS-CN.md index e0a4a62c48..a5ced96d5c 100644 --- a/AGENTS-CN.md +++ b/AGENTS-CN.md @@ -55,6 +55,7 @@ pnpm run desktop:dev # 完整热更新:Vite HMR + Rust 自动重 pnpm run desktop:preview:debug # 复用预构建二进制 + Vite HMR;无 Rust 自动重编译 pnpm run dev:web # 纯浏览器前端 pnpm run cli:dev # CLI 运行时 +pnpm run cli:install # release 编译并安装 bitfun-cli 到 ~/.local/bin(写入 bashrc/zshrc PATH) # 检查 pnpm run fmt:rs # 只格式化已改动 / 已暂存的 Rust 文件 diff --git a/AGENTS.md b/AGENTS.md index c2790b0b41..0a204dac65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ pnpm run desktop:dev # full hot-reload: Vite HMR + Rust auto-rebui pnpm run desktop:preview:debug # reuse pre-built binary + Vite HMR; no Rust auto-rebuild pnpm run dev:web # browser-only frontend pnpm run cli:dev # CLI runtime +pnpm run cli:install # build release + install bitfun-cli to ~/.local/bin (bashrc/zshrc PATH) # Check pnpm run fmt:rs # format only changed / staged Rust files diff --git a/package.json b/package.json index 9b686714a7..723bba02e1 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "installer:dev": "pnpm --dir BitFun-Installer run installer:dev", "cli:dev": "cd src/apps/cli && cargo run --", "cli:build": "cd src/apps/cli && cargo build --release", + "cli:install": "bash src/apps/cli/install.sh", "cli:run": "cd src/apps/cli && cargo run --release --", "cli:exec": "cd src/apps/cli && cargo run -- exec", "cli:check": "cd src/apps/cli && cargo check", diff --git a/src/apps/cli/AGENTS.md b/src/apps/cli/AGENTS.md index 2cafe5a984..fe84e5c5ec 100644 --- a/src/apps/cli/AGENTS.md +++ b/src/apps/cli/AGENTS.md @@ -86,3 +86,9 @@ input, session control, config import, plugin management, or product assembly behavior changes. Theme/color changes require `pnpm run theme:color-audit:all`. Packaging or branding changes require the CLI package smoke path and a clean-tree two-product build assertion. + +## Install for end users + +Prefer [`install.sh`](install.sh) / [`README.md`](README.md): release build, copy to +`~/.local/bin`, and idempotent `~/.bashrc` / `~/.zshrc` PATH wiring so users can +run `bitfun-cli` after install. diff --git a/src/apps/cli/README.md b/src/apps/cli/README.md new file mode 100644 index 0000000000..da7ba719b9 --- /dev/null +++ b/src/apps/cli/README.md @@ -0,0 +1,58 @@ +# BitFun CLI + +Terminal UI for BitFun (chat, tools, `/login` account + Peer Host). + +## One-click install (Linux / macOS, amd64 + arm64) + +From the repository root: + +```bash +bash src/apps/cli/install.sh +``` + +Or from this directory: + +```bash +bash install.sh +``` + +The script will: + +1. `cargo build -p bitfun-cli --release` (native host CPU) +2. Install `bitfun-cli` to `~/.local/bin` (override with `BITFUN_CLI_BIN_DIR`) +3. Idempotently add a PATH block to `~/.bashrc` and `~/.zshrc` +4. `source` the matching rc when the current shell is interactive bash/zsh + +Then run: + +```bash +bitfun-cli +``` + +### Options / environment + +| Variable | Meaning | +|----------|---------| +| `BITFUN_CLI_BIN_DIR` | Install directory (default `~/.local/bin`) | +| `BITFUN_CLI_SKIP_SHELLRC` | Set `1` to skip bashrc/zshrc edits | +| `CARGO_TARGET_DIR` | Cargo target dir (e.g. `$HOME/bitfun-build/target` on shared mounts) | +| `CARGO_BUILD_JOBS` | Limit rustc parallelism on small VPS | + +Example on a small arm64 VPS: + +```bash +CARGO_BUILD_JOBS=1 bash src/apps/cli/install.sh +``` + +### Prerequisites + +- Rust toolchain (`rustup` / `cargo`) +- Repository checked out with workspace `Cargo.toml` at the root + +## Dev commands (from repo root) + +```bash +pnpm run cli:dev # cargo run +pnpm run cli:build # cargo build --release +pnpm run cli:install # same as bash src/apps/cli/install.sh +``` diff --git a/src/apps/cli/install.sh b/src/apps/cli/install.sh new file mode 100755 index 0000000000..2c75a579a2 --- /dev/null +++ b/src/apps/cli/install.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# BitFun CLI — one-click build + install into PATH. +# +# Usage (from anywhere inside the repo, or this directory): +# bash src/apps/cli/install.sh +# bash install.sh # when cwd is src/apps/cli +# +# What it does: +# 1. cargo build -p bitfun-cli --release (native host arch) +# 2. Install binary to ~/.local/bin/bitfun-cli (override with BITFUN_CLI_BIN_DIR) +# 3. Idempotently append a PATH block to ~/.bashrc and ~/.zshrc +# 4. Source the matching rc for the current shell when interactive +# +# Supported hosts: Linux/macOS on amd64 (x86_64) and arm64 (aarch64). +# +# Environment: +# BITFUN_CLI_BIN_DIR Install directory (default: ~/.local/bin) +# BITFUN_CLI_SKIP_SHELLRC Set to 1 to skip bashrc/zshrc edits +# CARGO_TARGET_DIR Optional cargo target dir (e.g. $HOME/bitfun-build/target) +# CARGO_BUILD_JOBS Optional rustc parallelism limit for small VPS + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── Resolve repository root (directory that owns the workspace Cargo.toml) ── +resolve_repo_root() { + local dir candid + dir="$SCRIPT_DIR" + while [ "$dir" != "/" ]; do + if [ -f "$dir/Cargo.toml" ] && [ -d "$dir/src/apps/cli" ]; then + # Prefer the workspace root that lists members / workspace deps. + if grep -q '^\[workspace\]' "$dir/Cargo.toml" 2>/dev/null; then + echo "$dir" + return 0 + fi + candid="$dir" + fi + dir="$(dirname "$dir")" + done + if [ -n "${candid:-}" ]; then + echo "$candid" + return 0 + fi + echo "Error: could not locate BitFun repository root from $SCRIPT_DIR" >&2 + exit 1 +} + +host_arch_label() { + local m + m="$(uname -m 2>/dev/null || echo unknown)" + case "$m" in + x86_64 | amd64) echo "amd64" ;; + aarch64 | arm64) echo "arm64" ;; + *) echo "$m" ;; + esac +} + +assert_supported_host() { + local os arch + os="$(uname -s 2>/dev/null || echo unknown)" + arch="$(host_arch_label)" + case "$os" in + Linux | Darwin) ;; + *) + echo "Error: unsupported OS '$os'. install.sh supports Linux and macOS." + exit 1 + ;; + esac + case "$arch" in + amd64 | arm64) ;; + *) + echo "Error: unsupported CPU '$arch' ($(uname -m))." + echo "install.sh supports amd64 (x86_64) and arm64 (aarch64)." + exit 1 + ;; + esac +} + +require_cargo() { + if ! command -v cargo >/dev/null 2>&1; then + echo "Error: cargo not found. Install Rust from https://rustup.rs and re-run." + exit 1 + fi + if ! command -v rustc >/dev/null 2>&1; then + echo "Error: rustc not found. Install Rust from https://rustup.rs and re-run." + exit 1 + fi +} + +# Marker keeps shellrc edits idempotent across re-installs. +SHELLRC_MARKER_BEGIN="# >>> BitFun CLI PATH (managed by src/apps/cli/install.sh) >>>" +SHELLRC_MARKER_END="# <<< BitFun CLI PATH (managed by src/apps/cli/install.sh) <<<" + +ensure_bin_dir_on_path_block() { + local bin_dir="$1" + cat </dev/null; then + tmp="$(mktemp)" + # Replace existing managed block. + awk -v begin="$SHELLRC_MARKER_BEGIN" -v end="$SHELLRC_MARKER_END" ' + $0 == begin { in_block=1; next } + $0 == end { in_block=0; next } + !in_block { print } + ' "$rc_file" >"$tmp" + printf '\n%s\n' "$block" >>"$tmp" + mv "$tmp" "$rc_file" + echo "Updated PATH block in $rc_file" + else + printf '\n%s\n' "$block" >>"$rc_file" + echo "Appended PATH block to $rc_file" + fi +} + +maybe_source_shellrc() { + local bin_dir="$1" + # Always export for the remainder of this install.sh process. + case ":${PATH}:" in + *":${bin_dir}:"*) ;; + *) export PATH="${bin_dir}:$PATH" ;; + esac + + if [ ! -t 0 ] || [ ! -t 1 ]; then + echo "Non-interactive shell: open a new terminal, or run:" + echo " export PATH=\"${bin_dir}:\$PATH\"" + return 0 + fi + + local shell_name + shell_name="$(basename "${SHELL:-}")" + case "$shell_name" in + zsh) + # shellcheck disable=SC1090 + source "${HOME}/.zshrc" 2>/dev/null || true + echo "Sourced ~/.zshrc for this session." + ;; + bash) + # shellcheck disable=SC1090 + source "${HOME}/.bashrc" 2>/dev/null || true + echo "Sourced ~/.bashrc for this session." + ;; + *) + echo "Current SHELL=${SHELL:-unknown}: PATH updated for this install process." + echo "For new terminals, ensure ${bin_dir} is on PATH (bashrc/zshrc were updated)." + ;; + esac +} + +usage() { + cat <<'EOF' +BitFun CLI install script + +Usage: + bash install.sh [--help] + +Builds a release bitfun-cli and installs it for interactive use. + +Options: + -h, --help Show this help + +Environment: + BITFUN_CLI_BIN_DIR Install directory (default: ~/.local/bin) + BITFUN_CLI_SKIP_SHELLRC Set to 1 to skip ~/.bashrc and ~/.zshrc edits + CARGO_TARGET_DIR Cargo target directory override + CARGO_BUILD_JOBS Limit rustc parallelism (useful on small VPS) +EOF +} + +for arg in "$@"; do + case "$arg" in + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $arg" + usage + exit 1 + ;; + esac +done + +REPO_ROOT="$(resolve_repo_root)" +BIN_DIR="${BITFUN_CLI_BIN_DIR:-${HOME}/.local/bin}" +HOST_ARCH="$(host_arch_label)" +HOST_OS="$(uname -s)" + +echo "=== BitFun CLI Install ===" +echo "Repo: $REPO_ROOT" +echo "Host: ${HOST_OS} / ${HOST_ARCH} ($(uname -m))" +echo "Install dir: $BIN_DIR" + +assert_supported_host +require_cargo + +mkdir -p "$BIN_DIR" + +echo "" +echo "[1/3] Building bitfun-cli (release)..." +cd "$REPO_ROOT" +# Build from workspace root so path deps resolve. +cargo build -p bitfun-cli --release + +TARGET_DIR="${CARGO_TARGET_DIR:-${REPO_ROOT}/target}" +BUILT_BIN="${TARGET_DIR}/release/bitfun-cli" +if [ ! -x "$BUILT_BIN" ]; then + echo "Error: built binary not found at $BUILT_BIN" + exit 1 +fi + +echo "" +echo "[2/3] Installing binary..." +install -m 755 "$BUILT_BIN" "${BIN_DIR}/bitfun-cli" +echo "Installed: ${BIN_DIR}/bitfun-cli" +"${BIN_DIR}/bitfun-cli" --version 2>/dev/null || "${BIN_DIR}/bitfun-cli" -V 2>/dev/null || true + +echo "" +echo "[3/3] Configuring shell PATH..." +if [ "${BITFUN_CLI_SKIP_SHELLRC:-0}" = "1" ]; then + echo "Skipped shell rc edits (BITFUN_CLI_SKIP_SHELLRC=1)." + case ":${PATH}:" in + *":${BIN_DIR}:"*) ;; + *) export PATH="${BIN_DIR}:$PATH" ;; + esac +else + upsert_shellrc_path "${HOME}/.bashrc" "$BIN_DIR" + upsert_shellrc_path "${HOME}/.zshrc" "$BIN_DIR" + maybe_source_shellrc "$BIN_DIR" +fi + +echo "" +echo "=== Install complete ===" +echo "Run: bitfun-cli" +echo "Login (Peer Host): open /login inside the TUI after start." +if ! command -v bitfun-cli >/dev/null 2>&1; then + echo "" + echo "Note: \`bitfun-cli\` is not yet visible in this shell's command lookup." + echo "Use one of:" + echo " hash -r && bitfun-cli" + echo " export PATH=\"${BIN_DIR}:\$PATH\" && bitfun-cli" + echo " ${BIN_DIR}/bitfun-cli" +fi diff --git a/src/apps/relay-server/Dockerfile b/src/apps/relay-server/Dockerfile index 96de0e1a5c..235744ea6b 100644 --- a/src/apps/relay-server/Dockerfile +++ b/src/apps/relay-server/Dockerfile @@ -1,32 +1,60 @@ # BitFun Relay Server — standalone Docker build. # Build context is relay-server root (Cargo.toml + src/), no workspace needed. +# +# Multi-arch: build natively on the deploy host (linux/amd64 or linux/arm64). +# Do not force --platform unless you intentionally cross-build with qemu. + FROM rust:1-slim AS builder WORKDIR /build -RUN apt-get update && apt-get install -y pkg-config libssl-dev build-essential && rm -rf /var/lib/apt/lists/* +# Optional: limit rustc parallelism on small / low-memory VPS (esp. arm64). +# docker compose build --build-arg CARGO_BUILD_JOBS=1 +ARG CARGO_BUILD_JOBS= +ENV CARGO_BUILD_JOBS=${CARGO_BUILD_JOBS} +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + pkg-config \ + libssl-dev \ + build-essential \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* COPY Cargo.toml ./ -RUN mkdir -p src/bin && echo 'fn main() { println!("placeholder"); }' > src/main.rs && echo 'fn main() { println!("placeholder"); }' > src/bin/relay_admin.rs +RUN mkdir -p src/bin \ + && echo 'fn main() { println!("placeholder"); }' > src/main.rs \ + && echo 'fn main() { println!("placeholder"); }' > src/bin/relay_admin.rs +# Dependency cache layer (placeholder sources). Ignore failure so a toolchain +# mismatch does not abort before real sources are copied. RUN cargo build --release 2>/dev/null || true RUN rm -rf src target/release/bitfun-relay-server target/release/relay-admin target/release/deps/bitfun* COPY src/ src/ -RUN cargo build --release +RUN cargo build --release \ + && strip target/release/bitfun-relay-server target/release/relay-admin || true FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=builder /build/target/release/bitfun-relay-server /app/bitfun-relay-server COPY --from=builder /build/target/release/relay-admin /app/relay-admin -RUN mkdir -p /app/static +RUN mkdir -p /app/static /app/data /app/room-web ENV RELAY_PORT=9700 ENV RELAY_STATIC_DIR=/app/static EXPOSE 9700 +HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=5 \ + CMD curl -fsS "http://127.0.0.1:${RELAY_PORT}/health" || exit 1 + CMD ["/app/bitfun-relay-server"] diff --git a/src/apps/relay-server/README.md b/src/apps/relay-server/README.md index 764d6ecc79..b7f0e1b145 100644 --- a/src/apps/relay-server/README.md +++ b/src/apps/relay-server/README.md @@ -16,6 +16,26 @@ The relay stays **zero-knowledge**: clients encrypt with a master key derived locally; the server stores Argon2id password hashes and AES-GCM-wrapped keys, never plaintext passwords or decryptable sync payloads. +## Supported deploy hosts + +One-click Docker deploy (`bash deploy.sh`) targets: + +| OS | CPU | +|----|-----| +| Linux | **amd64** (`x86_64`) | +| Linux | **arm64** (`aarch64`) | + +Requirements: Docker Engine + Compose V2 (`docker compose`) **or** legacy +`docker-compose`, plus permission to talk to the Docker daemon. + +Build natively on the server (do **not** set `DOCKER_DEFAULT_PLATFORM` to a +foreign arch unless you intentionally cross-build with qemu). On small +memory VPS (common on arm64), use: + +```bash +RELAY_CARGO_BUILD_JOBS=1 bash deploy.sh +``` + ## Two operating modes | Mode | When | What you get | @@ -50,7 +70,7 @@ bash deploy.sh ``` `deploy.sh` must run **on the target server** (it does not SSH elsewhere). -Requires Docker and Docker Compose. +Requires Docker and Docker Compose on **linux/amd64** or **linux/arm64**. After a successful start, the script runs `relay-admin list-users`. If the database has **no accounts**, it prints the exact `add-user` command to run @@ -411,6 +431,7 @@ relay-server/ ├── Caddyfile # Optional reverse proxy (body + RPC timeouts) ├── deploy.sh ├── start.sh / stop.sh / restart.sh +├── common.sh # Shared helpers for the scripts above └── README.md ``` diff --git a/src/apps/relay-server/common.sh b/src/apps/relay-server/common.sh new file mode 100755 index 0000000000..99479e4f9e --- /dev/null +++ b/src/apps/relay-server/common.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# Shared helpers for BitFun relay-server deploy/start/stop/restart scripts. +# Sourced by the other *.sh files in this directory (not executed directly). + +CONTAINER_NAME="${CONTAINER_NAME:-bitfun-relay}" +RELAY_ADMIN_DB="${RELAY_ADMIN_DB:-/app/data/bitfun_relay.db}" +RELAY_HEALTH_URL="${RELAY_HEALTH_URL:-http://127.0.0.1:9700/health}" +COMPOSE=() + +check_command() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: '$cmd' is required but not installed." + exit 1 + fi +} + +# Prefer Compose V2 plugin (`docker compose`); fall back to legacy binary. +resolve_compose() { + if docker compose version >/dev/null 2>&1; then + COMPOSE=(docker compose) + return 0 + fi + if command -v docker-compose >/dev/null 2>&1; then + COMPOSE=(docker-compose) + return 0 + fi + echo "Error: Docker Compose is required." + echo "Install either:" + echo " - Docker Compose V2 plugin (docker compose), or" + echo " - legacy docker-compose binary" + exit 1 +} + +compose() { + if [ "${#COMPOSE[@]}" -eq 0 ]; then + resolve_compose + fi + "${COMPOSE[@]}" "$@" +} + +require_docker_daemon() { + check_command docker + if ! docker info >/dev/null 2>&1; then + echo "Error: Docker daemon is not running or this user cannot access it." + echo "Try: sudo systemctl start docker" + echo "Or add your user to the 'docker' group and re-login." + exit 1 + fi +} + +# Normalize uname -m to a short label used in logs / docs. +host_arch_label() { + local m + m="$(uname -m 2>/dev/null || echo unknown)" + case "$m" in + x86_64 | amd64) echo "amd64" ;; + aarch64 | arm64) echo "arm64" ;; + armv7l | armhf) echo "armv7" ;; + *) echo "$m" ;; + esac +} + +# Refuse obscure arches early; amd64 + arm64 are the supported deploy targets. +assert_supported_arch() { + local arch + arch="$(host_arch_label)" + case "$arch" in + amd64 | arm64) ;; + *) + echo "Error: unsupported host architecture '$arch' ($(uname -m))." + echo "One-click Docker deploy is supported on linux/amd64 and linux/arm64." + exit 1 + ;; + esac +} + +# Warn if the environment forces a foreign Docker platform (common on mixed hosts). +warn_if_forced_foreign_platform() { + local host_arch docker_platform normalized_host + host_arch="$(host_arch_label)" + docker_platform="${DOCKER_DEFAULT_PLATFORM:-}" + [ -z "$docker_platform" ] && return 0 + + normalized_host="linux/${host_arch}" + case "$docker_platform" in + *"${host_arch}"* | *"$(uname -m)"*) return 0 ;; + esac + + echo "Warning: DOCKER_DEFAULT_PLATFORM=${docker_platform} differs from host ${normalized_host}." + echo " Native deploy builds for the host CPU. Unset DOCKER_DEFAULT_PLATFORM" + echo " unless you intentionally cross-build (needs qemu/binfmt)." +} + +container_exists() { + docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1 +} + +container_running() { + [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" = "true" ] +} + +# Health probe that works without host curl/wget when possible. +probe_relay_health() { + # 1) Inside the container (most reliable; works even if host bind IP != 127.0.0.1) + if container_running; then + if docker exec "$CONTAINER_NAME" curl -fsS --max-time 5 "$RELAY_HEALTH_URL" >/dev/null 2>&1; then + return 0 + fi + # BusyBox-style wget (if present) + if docker exec "$CONTAINER_NAME" wget -q -O /dev/null --timeout=5 "$RELAY_HEALTH_URL" >/dev/null 2>&1; then + return 0 + fi + fi + + # 2) Host curl / wget against published port + if command -v curl >/dev/null 2>&1; then + curl -fsS --max-time 5 "$RELAY_HEALTH_URL" >/dev/null 2>&1 && return 0 + fi + if command -v wget >/dev/null 2>&1; then + wget -q -O /dev/null --timeout=5 "$RELAY_HEALTH_URL" >/dev/null 2>&1 && return 0 + fi + + return 1 +} + +wait_for_relay_health() { + local max_retries="${1:-12}" + local retry=0 + echo "Checking relay health (${RELAY_HEALTH_URL})..." + while [ "$retry" -lt "$max_retries" ]; do + if probe_relay_health; then + echo "Health check passed." + return 0 + fi + retry=$((retry + 1)) + if [ "$retry" -lt "$max_retries" ]; then + echo " Retry $retry/$max_retries in 3s..." + sleep 3 + fi + done + echo "Error: health check failed after $max_retries attempts." + compose logs --tail=40 relay-server || true + return 1 +} + +print_add_user_command() { + echo " docker exec -it ${CONTAINER_NAME} /app/relay-admin --db ${RELAY_ADMIN_DB} add-user --username " +} + +check_relay_accounts_or_remind() { + if ! container_running; then + echo "Warning: container '${CONTAINER_NAME}' is not running; skipped account check." + echo "After it is up, create an account with:" + print_add_user_command + return 0 + fi + + local user_list + user_list="$( + docker exec "$CONTAINER_NAME" /app/relay-admin --db "$RELAY_ADMIN_DB" list-users 2>/dev/null || true + )" + + local empty=0 + if echo "$user_list" | grep -q '^No accounts found\.'; then + empty=1 + elif ! echo "$user_list" | grep -q '^USERNAME'; then + empty=1 + fi + + if [ "$empty" -eq 1 ]; then + echo "No relay accounts yet. Account login will not work until you create one." + echo "Run:" + print_add_user_command + echo "(omit --password to enter the password interactively)" + else + local user_count + user_count="$( + echo "$user_list" | awk 'NR>2 && NF { count++ } END { print count+0 }' + )" + echo "Relay accounts found: ${user_count}" + fi +} diff --git a/src/apps/relay-server/deploy.sh b/src/apps/relay-server/deploy.sh index 638e3f3332..6ce674e3ba 100755 --- a/src/apps/relay-server/deploy.sh +++ b/src/apps/relay-server/deploy.sh @@ -5,11 +5,18 @@ # Run this script on the target server itself after SSH login. # It deploys to the current machine only; it does not SSH to a remote host. # -# Prerequisites: Docker, Docker Compose +# Supported hosts: Linux amd64 (x86_64) and arm64 (aarch64) with Docker. +# +# Prerequisites: Docker + Compose V2 (`docker compose`) or legacy docker-compose +# +# Low-memory VPS tip (especially arm64): +# RELAY_CARGO_BUILD_JOBS=1 bash deploy.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" SKIP_BUILD=false SKIP_HEALTH_CHECK=false @@ -25,27 +32,19 @@ Run location: Execute this script on the target server itself after SSH login. This script only deploys to the current machine. +Supported architectures: + linux/amd64 (x86_64), linux/arm64 (aarch64) + Options: - --skip-build Skip docker compose build, only restart services + --skip-build Skip docker compose build, only recreate/start services --skip-health-check Skip post-deploy health check -h, --help Show this help message -EOF -} - -check_command() { - local cmd="$1" - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "Error: '$cmd' is required but not installed." - exit 1 - fi -} -check_docker_compose() { - if docker compose version >/dev/null 2>&1; then - return 0 - fi - echo "Error: Docker Compose (docker compose) is required." - exit 1 +Environment: + RELAY_HOST_BIND_IP Host bind address for published port (default 0.0.0.0) + RELAY_CARGO_BUILD_JOBS Limit rustc parallelism inside Docker (e.g. 1 on small VPS) + DOCKER_DEFAULT_PLATFORM Leave unset for native host builds (recommended) +EOF } for arg in "$@"; do @@ -64,103 +63,56 @@ for arg in "$@"; do esac done +HOST_ARCH="$(host_arch_label)" + echo "=== BitFun Relay Server Deploy ===" -echo "Target: current machine" +echo "Target: current machine ($(uname -s) / ${HOST_ARCH}, uname=$(uname -m))" echo "Note: run this script on the target server after SSH login." -check_command docker -check_docker_compose -cd "$SCRIPT_DIR" +assert_supported_arch +require_docker_daemon +resolve_compose +warn_if_forced_foreign_platform -# Stop old containers if running -echo "[1/3] Stopping old containers (if running)..." -docker compose down 2>/dev/null || true -echo " Done." +echo "Compose: ${COMPOSE[*]}" +cd "$SCRIPT_DIR" -# Build +# Build first so a compile failure does not take down a running relay. if [ "$SKIP_BUILD" = true ]; then - echo "[2/3] Skipping Docker build (--skip-build)" + echo "[1/2] Skipping Docker build (--skip-build)" else - echo "[2/3] Building Docker images..." - docker compose build + echo "[1/2] Building Docker image for host architecture (${HOST_ARCH})..." + BUILD_ARGS=() + if [ -n "${RELAY_CARGO_BUILD_JOBS:-}" ]; then + BUILD_ARGS+=(--build-arg "CARGO_BUILD_JOBS=${RELAY_CARGO_BUILD_JOBS}") + echo " Using CARGO_BUILD_JOBS=${RELAY_CARGO_BUILD_JOBS}" + fi + # Do not pass --platform unless the user explicitly set DOCKER_DEFAULT_PLATFORM; + # native builds on amd64/arm64 servers are the supported path. + compose build "${BUILD_ARGS[@]}" fi -# Start -echo "[3/3] Starting services..." -docker compose up -d +echo "[2/2] Starting / recreating services..." +compose up -d --force-recreate --remove-orphans if [ "$SKIP_HEALTH_CHECK" = false ]; then echo "Waiting for services to start..." - sleep 5 - echo "Checking relay health endpoint..." - if command -v curl >/dev/null 2>&1; then - MAX_RETRIES=6 - RETRY=0 - while [ $RETRY -lt $MAX_RETRIES ]; do - if curl -fsS --max-time 5 "http://127.0.0.1:9700/health" >/dev/null 2>&1; then - echo "Health check passed: http://127.0.0.1:9700/health" - break - fi - RETRY=$((RETRY + 1)) - if [ $RETRY -lt $MAX_RETRIES ]; then - echo " Retry $RETRY/$MAX_RETRIES in 3s..." - sleep 3 - else - echo "Warning: health check failed after $MAX_RETRIES attempts. Check logs:" - docker compose logs --tail=30 relay-server - fi - done - else - echo "Warning: 'curl' not found, skipped health check." - fi + sleep 2 + wait_for_relay_health 12 fi echo "" echo "=== Deploy complete ===" -echo "Relay server running on port 9700" +echo "Relay server running on port 9700 (host arch: ${HOST_ARCH})" echo "" - -CONTAINER_NAME="bitfun-relay" -RELAY_ADMIN_DB="/app/data/bitfun_relay.db" -ADD_USER_CMD="docker exec -it ${CONTAINER_NAME} /app/relay-admin --db ${RELAY_ADMIN_DB} add-user --username " - -print_client_url_hint() { - echo "Point BitFun Desktop / CLI Auth Server URL to:" - echo " http://:9700" - echo "See README.md for sync, Peer Device Mode, and proxy timeouts." -} - -if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1 \ - && [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" = "true" ]; then - # Empty DB prints "No accounts found."; otherwise a USERNAME header + rows. - USER_LIST="$( - docker exec "$CONTAINER_NAME" /app/relay-admin --db "$RELAY_ADMIN_DB" list-users 2>/dev/null || true - )" - if echo "$USER_LIST" | grep -q '^No accounts found\.'; \ - || ! echo "$USER_LIST" | grep -q '^USERNAME'; then - echo "No relay accounts yet. Account login will not work until you create one." - echo "Run:" - echo " ${ADD_USER_CMD}" - echo "(omit --password to enter the password interactively)" - echo "" - print_client_url_hint - else - USER_COUNT="$( - echo "$USER_LIST" | awk 'NR>2 && NF { count++ } END { print count+0 }' - )" - echo "Relay accounts found: ${USER_COUNT}" - echo "" - print_client_url_hint - fi -else - echo "Warning: container '${CONTAINER_NAME}' is not running; skipped account check." - echo "After it is up, create an account with:" - echo " ${ADD_USER_CMD}" -fi - +check_relay_accounts_or_remind +echo "" +echo "Point BitFun Desktop / CLI Auth Server URL to:" +echo " http://:9700" +echo "See README.md for sync, Peer Device Mode, and proxy timeouts." echo "" -echo "Check status: docker compose ps" +echo "Check status: bash -c 'cd \"${SCRIPT_DIR}\" && ${COMPOSE[*]} ps'" echo "Start: bash start.sh" echo "Restart: bash restart.sh" echo "Stop: bash stop.sh" -echo "View logs: docker compose logs -f relay-server" +echo "View logs: ${COMPOSE[*]} logs -f relay-server" diff --git a/src/apps/relay-server/docker-compose.yml b/src/apps/relay-server/docker-compose.yml index 552a5c5dad..e7ad4222ac 100644 --- a/src/apps/relay-server/docker-compose.yml +++ b/src/apps/relay-server/docker-compose.yml @@ -3,6 +3,12 @@ services: build: context: . dockerfile: Dockerfile + args: + # Pass through optional parallelism limit for low-memory hosts: + # RELAY_CARGO_BUILD_JOBS=1 docker compose build + CARGO_BUILD_JOBS: ${RELAY_CARGO_BUILD_JOBS:-} + # Intentionally no `platform:` pin — build/run for the host CPU + # (linux/amd64 on x86_64, linux/arm64 on aarch64). container_name: bitfun-relay restart: unless-stopped ports: @@ -17,6 +23,12 @@ services: - ./static:/app/static:ro - room-web:/app/room-web - relay-db:/app/data + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9700/health"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s # Optional: Caddy reverse proxy for automatic HTTPS. # Uncomment if you need HTTPS / domain-based access. diff --git a/src/apps/relay-server/restart.sh b/src/apps/relay-server/restart.sh index 35e2ef3193..b7b6c67afa 100755 --- a/src/apps/relay-server/restart.sh +++ b/src/apps/relay-server/restart.sh @@ -5,7 +5,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONTAINER_NAME="bitfun-relay" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" usage() { cat <<'EOF' @@ -23,26 +24,6 @@ Behavior: EOF } -check_command() { - local cmd="$1" - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "Error: '$cmd' is required but not installed." - exit 1 - fi -} - -check_docker_compose() { - if docker compose version >/dev/null 2>&1; then - return 0 - fi - echo "Error: Docker Compose (docker compose) is required." - exit 1 -} - -container_running() { - [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" = "true" ] -} - for arg in "$@"; do case "$arg" in -h|--help) @@ -58,21 +39,20 @@ for arg in "$@"; do done echo "=== BitFun Relay Server Restart ===" -check_command docker -check_docker_compose - +require_docker_daemon +resolve_compose cd "$SCRIPT_DIR" if container_running; then echo "Relay service is running. Restarting it..." - docker compose up -d --force-recreate + compose up -d --force-recreate else echo "Relay service is not running. Starting it instead..." - docker compose up -d + compose up -d fi echo "" echo "Relay service is ready." echo "Relay endpoint: http://:9700" -echo "Check status: docker compose ps" -echo "View logs: docker compose logs -f relay-server" +echo "Check status: ${COMPOSE[*]} ps" +echo "View logs: ${COMPOSE[*]} logs -f relay-server" diff --git a/src/apps/relay-server/start.sh b/src/apps/relay-server/start.sh index 7585838a0d..54bdc4279f 100755 --- a/src/apps/relay-server/start.sh +++ b/src/apps/relay-server/start.sh @@ -5,7 +5,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONTAINER_NAME="bitfun-relay" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" usage() { cat <<'EOF' @@ -22,30 +23,6 @@ Behavior: EOF } -check_command() { - local cmd="$1" - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "Error: '$cmd' is required but not installed." - exit 1 - fi -} - -check_docker_compose() { - if docker compose version >/dev/null 2>&1; then - return 0 - fi - echo "Error: Docker Compose (docker compose) is required." - exit 1 -} - -container_exists() { - docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1 -} - -container_running() { - [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" = "true" ] -} - for arg in "$@"; do case "$arg" in -h|--help) @@ -61,9 +38,8 @@ for arg in "$@"; do done echo "=== BitFun Relay Server Start ===" -check_command docker -check_docker_compose - +require_docker_daemon +resolve_compose cd "$SCRIPT_DIR" if container_running; then @@ -77,10 +53,10 @@ else echo "Relay service is not created yet. Creating and starting it..." fi -docker compose up -d +compose up -d echo "" echo "Relay service started." echo "Relay endpoint: http://:9700" -echo "Check status: docker compose ps" -echo "View logs: docker compose logs -f relay-server" +echo "Check status: ${COMPOSE[*]} ps" +echo "View logs: ${COMPOSE[*]} logs -f relay-server" diff --git a/src/apps/relay-server/stop.sh b/src/apps/relay-server/stop.sh index 2af179ee3a..0d6cb4ea28 100755 --- a/src/apps/relay-server/stop.sh +++ b/src/apps/relay-server/stop.sh @@ -5,7 +5,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONTAINER_NAME="bitfun-relay" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" usage() { cat <<'EOF' @@ -19,26 +20,6 @@ Run location: EOF } -check_command() { - local cmd="$1" - if ! command -v "$cmd" >/dev/null 2>&1; then - echo "Error: '$cmd' is required but not installed." - exit 1 - fi -} - -check_docker_compose() { - if docker compose version >/dev/null 2>&1; then - return 0 - fi - echo "Error: Docker Compose (docker compose) is required." - exit 1 -} - -container_running() { - [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" = "true" ] -} - for arg in "$@"; do case "$arg" in -h|--help) @@ -54,9 +35,8 @@ for arg in "$@"; do done echo "=== BitFun Relay Server Stop ===" -check_command docker -check_docker_compose - +require_docker_daemon +resolve_compose cd "$SCRIPT_DIR" if ! container_running; then @@ -64,9 +44,9 @@ if ! container_running; then exit 0 fi -docker compose stop +compose stop echo "" echo "Relay service stopped." -echo "Check status: docker compose ps" +echo "Check status: ${COMPOSE[*]} ps" echo "Start again: bash start.sh"