475da0e95003a695f89c028240e692c325310914
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
475da0e950 |
Implement signed API communication to improve security
build / build-linux-amd64 (push) Successful in 1m50s
|
||
|
|
9d53999eea |
Implement password handling for unattended access
build / build-linux-amd64 (push) Successful in 2m0s
|
||
|
|
5b288d671c |
feat: M5 admin dashboard (HTMX + Tailwind CDN, embedded HTML)
A web admin UI for the rustdesk-server, mounted at /admin/* on the
existing HTTP API listener. Single-binary deploy preserved — the two
HTML files live in admin_ui/ and are pulled into the binary via
include_str! at build time, so there's nothing extra to ship.
================================================================================
Architecture
================================================================================
- Stack: HTMX 1.9 + Tailwind play CDN. No SPA, no Node toolchain. Pages
are server-rendered HTML fragments returned by Rust handlers via
Html<String>; the index.html shell uses hx-get to drop a fragment into
the main pane and hx-push-url for back-button history.
- Auth: same Bearer-token table the API uses. The dashboard log-in form
POSTs username + password (+ optional TOTP) to /admin/login; on success
the server mints a token and pins it in an HttpOnly + SameSite=Strict
cookie (`rd_admin_session`). The AuthedUser extractor was extended to
accept either the Authorization: Bearer header (curl, desktop client)
OR the session cookie (browser).
- Embedding: src/api/admin/mod.rs has `include_str!("../../../admin_ui/index.html")`
+ login.html. No tower_http::ServeDir wildcard — we ran into axum 0.5
routing conflicts between literal /admin/login routes and an /admin/*
catch-all, so each HTML file is its own explicit route.
================================================================================
M5a — foundation
================================================================================
Files:
admin_ui/index.html page shell + sidebar + HTMX + 401-bounces-to-login
admin_ui/login.html credentials + TOTP form, posts to /admin/login
src/api/admin/mod.rs router + include_str! + Cache-Control: no-cache
src/api/admin/auth.rs /admin/login POST (form-encoded), /admin/logout POST
src/api/admin/me.rs sidebar fragment ("Signed in as <name>")
src/api/middleware.rs `AuthedUser` now reads either Bearer OR cookie
src/api/state.rs `admin_ui_dir` (informational; UI is embedded)
src/main.rs --admin-ui-dir flag (empty disables the dashboard)
The login flow asks for TOTP transparently in the same form when the
target user has a secret enrolled, so the dashboard inherits the TOTP
gate from the API auth surface for free.
================================================================================
M5b — full CRUD pages
================================================================================
- Users (src/api/admin/pages/users.rs) — list, create, password reset,
toggle admin / status, TOTP enroll / unenroll, delete. TOTP enroll
surfaces the secret + otpauth URL once, on a dismissible banner above
the table.
- Devices (devices.rs) — list with hostname/OS/last-heartbeat/conn count,
force-disconnect (queues `heartbeat_commands` row consumed at the next
/api/heartbeat tick), force-sysinfo refresh.
- Device groups (groups.rs) — list / create / delete / add member /
remove member. Per-group section, with an add-member dropdown of users
not yet in the group.
- Strategies (strategies.rs) — list / create / edit config_options /
delete. config_options is validated as a JSON object on the server side
before persist; bad JSON is reflected to the page with a friendly
error notice.
- Address books (address_books.rs) — read-only overview of all books
with owner, kind (personal / shared badge), peer count, GUID.
- OIDC providers (oidc.rs) — read-only list of what's configured. Editing
remains operator-side via --oidc-config TOML or direct SQL.
================================================================================
M5c — audit + recordings browsers
================================================================================
- Audit log (audit.rs) — three tabs (Connections / File transfers /
Alarms), each capped at the latest 200 rows. Tab pills are HTMX links
with hx-get + hx-target="#main" so the tab switch is a single fetch.
- Recordings (recordings.rs) — read-only list with peer / size / state /
start / finish. Streaming download is a follow-up; for now operators
pull files from --recording-dir directly.
================================================================================
DB methods added
================================================================================
- Users: users_list_all, user_set_status, user_set_admin,
user_set_password, user_delete, user_has_totp,
raw_update_user_email
- Devices: devices_list_all, device_sysinfo_get_conns,
heartbeat_command_queue (also used elsewhere; surfaced)
- Groups: device_groups_list_all, device_group_members,
device_group_create, device_group_delete,
device_group_add_member, device_group_remove_member
- Strategy: strategies_list_all, strategy_create,
strategy_update_config, strategy_delete
- Audit: audit_conn_list, audit_file_list, audit_alarm_list
- Misc: ab_list_all_with_owner, recordings_list
All use the runtime sqlx::query("...") form (matching the project-wide
convention) so the SQLite compile-time-check macros don't require these
new tables to pre-exist in the dev DB.
================================================================================
Conventions enforced
================================================================================
- Every page handler gates on require_admin(&AuthedUser) — non-admin
users get an HTTP 403 + JSON envelope, which the SPA shell catches and
bounces back to the login form.
- HTML fragments are produced via `format!`-with-named-args; html_escape
is centralized in src/api/admin/pages/shared.rs and applied to every
user-supplied string before it lands in the DOM.
- All mutations return either the updated table fragment OR
notice_html(kind, msg) + the table — same pattern across pages, so
HTMX swap targets stay simple (always #region innerHTML).
- Cookie carries no path restriction so it also authorizes /api/* calls
the dashboard might want to make from the browser; HttpOnly +
SameSite=Strict mitigates XSS / CSRF; Max-Age tracks ApiConfig's
session_ttl_secs (30 days).
================================================================================
Verification
================================================================================
1. cargo build --release — clean.
2. End-to-end smoke test:
- /admin/ serves index.html (4406 bytes), /admin/login.html serves
login.html (2598 bytes).
- POST /admin/login with valid creds returns 200 + Set-Cookie
`rd_admin_session=…; HttpOnly; Path=/; SameSite=Strict; Max-Age=…`.
- All eight /admin/pages/* fragments return 200 with cookie.
- Users CRUD round-trip: create alice → toggle admin → disable →
reset password → enroll TOTP (32-char secret displayed once) →
unenroll → delete; self-action guard rejects suicide deletes.
- Groups CRUD: create engineering → add alice as member → SQL
confirms the row.
- Strategies: valid JSON accepted, invalid JSON rejected with a
friendly notice.
- Audit tabs: all three render 200; empty-state messages appear when
no rows.
- /admin/logout clears the cookie; subsequent /admin/me returns 401.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8ecf05b106 |
feat: HTTP-over-rendezvous fallback (HttpProxyRequest)
Closes the M4 plan. When `OPTION_USE_RAW_TCP_FOR_API=Y` (typical in locked-down networks where direct HTTPS to port 21114 is blocked), the client wraps every /api/* request in an HttpProxyRequest protobuf and ships it over the already-encrypted rendezvous TCP channel. We now decode those messages on hbbs and dispatch them through the *same* axum Router the HTTPS listener uses — so every existing handler (login, AB, audit, TOTP, OIDC, devices/cli, plugin-sign, …) is reachable through this path with zero per-route plumbing. Components ========== - libs/hbb_common (submodule, pro-features-httpproxy branch): backports HeaderEntry / HttpProxyRequest / HttpProxyResponse + union tags 27/28 from upstream @87b11a7 onto our pinned @83419b6. Proto-only — the rest of hbb_common is unchanged so we keep the tokio 1.x / axum 0.5 / pinned reqwest fork intact (a full submodule bump risked breaking those). - src/api/http_proxy.rs: the dispatch shim. Holds a `Mutex<Option<Router>>` populated by `api::serve` before the HTTPS listener starts, builds an `http::Request<Body>` from the proto fields (sanitizing hop-by-hop headers, defaulting Content-Type: application/json), runs it through `router.oneshot(req)`, and serializes the response into HttpProxyResponse. Tower added as a direct dep with the `util` feature for ServiceExt. - src/api/mod.rs: pub mod http_proxy; install_router(app.clone()) before axum::Server::bind to share the router. - src/rendezvous_server.rs::handle_tcp: new match arm right before the catch-all that decodes HttpProxyRequest and replies with an HttpProxyResponse via the existing Sink::TcpStream(..., Encrypt) path. The reply is automatically secretbox-sealed by `send_to_sink`, so the end-to-end channel is encrypted symmetrically with secure_tcp. - examples/http_proxy_test.rs: end-to-end smoke test that opens a TCP connection, walks the secure_tcp handshake by hand (read server's signed box pubkey, derive symmetric key, send sealed reply), then ships an HttpProxyRequest GET /api/login-options and verifies the response is 200 + ["account"]. Used as the validation gate. New crate deps ============== - tower = "0.4" (features = ["util"]) — for ServiceExt::oneshot - http-body = "0.4" — for the Body trait import in dispatch Verification ============ 1. cargo build --release — clean. 2. examples/http_proxy_test against a fresh hbbs: [ok] secure_tcp handshake complete [ok] sent HttpProxyRequest GET /api/login-options [ok] response status = 200 [ok] response body = ["account"] [pass] full HTTP-over-rendezvous round trip verified 3. hbbs log confirms the secure_tcp handshake completed and the dispatch went through the standard axum router. Notes on cherry-pick vs submodule bump ====================================== The plan flagged the bump as the riskiest M4 item because newer hbb_common pulls newer tokio that breaks axum 0.5. The proto-only cherry pick keeps everything stable; the upstream-divergence cost is one extra commit in the hbb_common submodule that we own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3e89d61566 |
feat: add Pro-equivalent management API on top of OSS hbbs
Brings the rustdesk-server up to feature parity with RustDesk Server Pro for
the API surface the desktop client expects (CONSOLE_API.md). Implemented as
an in-process axum router mounted by hbbs alongside its existing
rendezvous + relay TCP/UDP/WS listeners; everything persists in the existing
SQLx + SQLite database via additional CREATE TABLE IF NOT EXISTS migrations.
================================================================================
M1 — Auth foundation + heartbeat + sysinfo
================================================================================
- New tables: users, tokens, device_sysinfo.
- Endpoints: HEAD+GET /api/login-options, POST /api/login, POST /api/logout,
POST /api/currentUser, POST /api/heartbeat, POST /api/sysinfo_ver,
POST /api/sysinfo.
- Bearer-token auth: tokens are 32 random bytes (base64url); only the
sha256 of the token is stored. `tokens.last_used_at`/`expires_at` slide
forward on every authenticated request (30-day TTL by default).
- Bcrypt-cost-10 password hashing, always wrapped in
tokio::task::spawn_blocking to keep the runtime responsive.
- New CLI flags --http-port, --bootstrap-admin-username,
--bootstrap-admin-password.
- Heartbeat returns the `sysinfo: true` flag on first contact and after
cfg.sysinfo_ver bumps; sysinfo upload returns the bare-string body
("SYSINFO_UPDATED" / "ID_NOT_FOUND") the client expects.
================================================================================
M2 — Address book, device groups, accessible peers
================================================================================
- New tables: address_books, address_book_shares, address_book_peers,
address_book_tags, address_book_peer_tags, device_groups,
device_group_members. Soft-ALTER adds device_sysinfo.user_id (the
binding from a device to its enrolled user, set by /api/login).
- Endpoints: POST /api/ab/settings, POST /api/ab/personal,
POST /api/ab/shared/profiles, POST /api/ab/peers, POST /api/ab/tags/{guid},
POST /api/ab/peer/add/{guid}, PUT /api/ab/peer/update/{guid},
DELETE /api/ab/peer/{guid}, POST /api/ab/tag/add/{guid},
PUT /api/ab/tag/rename/{guid}, PUT /api/ab/tag/update/{guid},
DELETE /api/ab/tag/{guid}, GET+POST /api/ab (legacy single-blob fallback),
GET /api/device-group/accessible, GET /api/users, GET /api/peers.
- Share-rule enforcement (1=read, 2=read/write, 3=full) at the top of every
AB mutation. Owners are full; other rules come from
address_book_shares (direct or via device_group). Rejection is HTTP 200 +
{"error":"read-only"} so the client doesn't yank the session.
- New CLI flags --ab-legacy-mode, --ab-max-peers-per-book.
- Action endpoints (peer add/update/delete, tag CRUD) return HTTP 200 with
EMPTY body on success — matches the Flutter _jsonDecodeActionResp at
ab_model.dart:2002 which treats {} as the literal error string "null".
================================================================================
M3 — Audit, recording, strategy push
================================================================================
- New tables: audit_conn (PK guid echoed back to client),
audit_file, audit_alarm, recordings, strategies, strategy_assignments,
heartbeat_commands.
- Endpoints: POST /api/audit/conn (returns {"guid":"..."}),
POST /api/audit/file, POST /api/audit/alarm, PUT /api/audit (note update),
POST /api/record?type={new|part|tail|remove}.
- Recording uploader: filesystem state machine under --recording-dir;
filenames sanitized to a single Normal path component to block traversal;
`tail` writes the first ≤1024 bytes at offset 0 after all `part` chunks.
- Heartbeat extended to:
* resolve a per-peer strategy (peer > device-group > user, highest
priority wins) and emit `strategy.config_options` + `extra` +
`modified_at`.
* read-and-delete heartbeat_commands rows so an admin can queue
`disconnect: [conn_id]` or force `sysinfo: true` via SQL and have it
delivered on the next 15-second tick.
- New CLI flags --recording-dir (default ./recordings),
--recording-max-size-mb, --audit-retention-days.
================================================================================
secure_tcp on the rendezvous TCP listener (M3 polish)
================================================================================
A logged-in client conditionally calls secure_tcp() on its TCP rendezvous
connection (src/client.rs:427-431, gated on `key && token` both non-empty).
OSS hbbs had no KeyExchange handler at all on TCP rendezvous, so the
client's secure_tcp_impl read timed out with "Failed to secure tcp:
deadline has elapsed". Added:
- A try_secure_tcp_handshake helper that, on every accepted TCP connection,
generates an ephemeral box keypair, signs the box public key with the
server's Ed25519 sk (already loaded for relay-response signing), sends
KeyExchange, then waits 5s for the client's reply.
- Reply is KeyExchange[client_box_pk, sealed_sym_key] -> decrypt the
sealed key, install Encrypt on both halves of the stream.
- Reply is any other RendezvousMessage -> buffer it and replay through
the normal handle_tcp dispatcher (plain-mode clients filter unsolicited
KeyExchange via get_next_nonkeyexchange_msg, so our preceding KX is
harmless).
- Reply never comes (timeout) -> fall through to plain mode.
- Sink::TcpStream now carries an Option<Encrypt>; outgoing writes are
sealed when keyed. Symmetric Encrypt is cloned for inbound (`dec`) and
outbound (`enc`) so the two directions track independent counters.
================================================================================
M4 — Advanced auth (TOTP, email-code, OIDC), CLI assign, plugin signing
================================================================================
- New tables: user_totp_secrets, pending_tfa_challenges,
pending_email_codes, oidc_providers, oidc_sessions. Soft-ALTER adds
users.oidc_subject.
- /api/login extended:
* type:"account" (existing) — issues an `tfa_check` challenge (5-min
nonce in `secret`) when the user has TOTP enrolled.
* type:"tfa_code" — verifies the nonce + the 6-digit TOTP code against
user_totp_secrets.secret_b32.
* type:"email_code" — passwordless. First leg mints a 6-digit code and
sends it via SMTP (or logs to stdout when --smtp-host is empty);
second leg verifies. Brute-force capped at 5 attempts per code, then
the row is purged.
- /api/oidc/auth + GET /oidc/callback + GET /api/oidc/auth-query implement
the standard OAuth2 authorization-code flow with userinfo. Discovery via
<issuer>/.well-known/openid-configuration with an in-memory cache.
--oidc-config TOML upserts providers at startup; --public-base-url builds
the redirect_uri.
- New endpoints: POST /api/2fa/enroll (admin-only, returns secret_b32 +
otpauth_url), POST /api/2fa/unenroll, POST /api/devices/cli (used by
`rustdesk --assign`; binds device to user, ensures device-group, adds
AB entry, attaches peer-scoped strategy), POST /lic/web/api/plugin-sign
(Ed25519 over the request body using the same id_ed25519 secret).
- /api/login-options is now dynamic: returns ["account"], plus "email_code"
when SMTP or ALLOW_DEV_EMAIL_CODE is set, plus an "oidc/<name>" entry
per enabled provider in oidc_providers.
- New CLI flags --smtp-host, --smtp-port, --smtp-user, --smtp-pass,
--smtp-from, --smtp-tls, --public-base-url, --oidc-config.
- New crate deps: tokio (fs/io-util features), totp-rs, lettre (rustls +
builder + smtp-transport, no defaults), toml.
================================================================================
Code organization
================================================================================
- src/api/ axum router + shared state + error envelope
├── ab/ address book endpoints (settings/profiles/peers/
│ tags/legacy/rules)
├── audit/ conn/file/alarm/note
├── oidc/ providers/discovery/auth/callback/poll
├── record/ storage state machine + handler
├── strategy/ resolver wrapper around DB
├── auth.rs login/logout/currentUser
├── devices_cli.rs /api/devices/cli
├── email.rs SMTP transport (lettre) + dev-mode stdout fallback
├── error.rs ApiError enum -> HTTP 200/401/403/404 + JSON envelope
├── groups.rs /api/device-group/accessible
├── heartbeat.rs /api/heartbeat
├── middleware.rs AuthedUser extractor (Bearer -> sha256 -> token row)
├── pagination.rs Page<T> + PageQuery
├── peers.rs /api/peers
├── plugin_sign.rs /lic/web/api/plugin-sign
├── state.rs AppState + ApiConfig (incl. EmailConfig)
├── sysinfo.rs /api/sysinfo, /api/sysinfo_ver
├── twofa.rs /api/2fa/enroll, /unenroll
└── users.rs UserPayload + /api/users + bcrypt helpers
================================================================================
Conventions enforced throughout
================================================================================
- All new SQL uses the runtime sqlx::query("...") form (NOT the query!
macro) so first-time builds don't require DATABASE_URL to point at a DB
containing the new tables.
- Soft-ALTER helper (try_alter) swallows "duplicate column name" errors so
schema migrations are idempotent across re-runs and existing-DB upgrades.
- Bcrypt compares always via spawn_blocking.
- Tokens (Bearer access_token, TFA challenge nonce, OIDC poll handle) are
always 24-32 random bytes from sodiumoxide::randombytes; the Bearer is
stored only as its sha256.
- Constant-time hash comparison for email codes.
- Action endpoints return HTTP 200 with empty body on success; HTTP 200 +
{"error": "..."} for business errors so the client doesn't get logged
out; 401 only from the auth middleware.
Tested end-to-end via curl + a stock RustDesk client (M1-M2 verified
against two laptops; M3 verified against the strategy-push and
force-disconnect paths; M4 verified via direct flow tests + a mock IdP for
OIDC). Stock client connect now works whether the user is signed in or
not (the secure_tcp regression that blocked logged-in connect is fixed).
The remaining piece on the M4 plan — HttpProxyRequest, the TCP-over-
rendezvous fallback for clients with OPTION_USE_RAW_TCP_FOR_API=Y — is
gated on bumping the OSS server's vendored hbb_common to a commit that
includes proto tags 27 and 28. That work lives on a separate branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|