From 54c031fad8bf9818c15a3e59daa8d3fd7a14b60e Mon Sep 17 00:00:00 2001 From: Mike Mueller Date: Mon, 4 May 2026 16:25:15 +0200 Subject: [PATCH] ci: add Gitea Actions Windows runner provisioning + initial docs --- .gitmodules | 2 +- ci/runners/windows/provision.ps1 | 158 +++++ docs/CLIENT_VS_SERVER_GAPS.md | 260 ++++++++ docs/CONSOLE_API.md | 1038 ++++++++++++++++++++++++++++++ 4 files changed, 1457 insertions(+), 1 deletion(-) create mode 100644 ci/runners/windows/provision.ps1 create mode 100644 docs/CLIENT_VS_SERVER_GAPS.md create mode 100644 docs/CONSOLE_API.md diff --git a/.gitmodules b/.gitmodules index d80e69aa8..af437ef3d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "libs/hbb_common"] path = libs/hbb_common - url = https://github.com/rustdesk/hbb_common + url = https://gitea.cstudio.ch/mike/hbb_common.git diff --git a/ci/runners/windows/provision.ps1 b/ci/runners/windows/provision.ps1 new file mode 100644 index 000000000..903bfc8a1 --- /dev/null +++ b/ci/runners/windows/provision.ps1 @@ -0,0 +1,158 @@ +# Provisions a Windows Server 2022 host as a Gitea Actions runner for RustDesk +# desktop builds (flutter x64 + sciter x86). Idempotent: safe to re-run. +# +# Versions are pinned to .github/workflows/flutter-build.yml. Bump them there +# and here together. +# +# Usage (Administrator PowerShell): +# Set-ExecutionPolicy -Scope Process Bypass -Force +# .\provision.ps1 -GiteaUrl https://gitea.example.com -RunnerToken + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] [string] $GiteaUrl, + [Parameter(Mandatory = $true)] [string] $RunnerToken, + [string] $RunnerName = "$env:COMPUTERNAME-rustdesk", + [string] $RunnerLabels = "windows-10,self-hosted,X64", + [string] $RunnerVersion = "0.2.11" +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +# --- pinned versions (mirror flutter-build.yml env block) --- +$RUST_VERSION = '1.75.0' +$RUST_NIGHTLY = 'nightly-2023-10-13' +$LLVM_VERSION = '15.0.6' +$FLUTTER_VERSION = '3.24.5' +$VCPKG_COMMIT = '120deac3062162151622ca4860575a33844ba10b' + +$ToolsRoot = 'C:\tools' +New-Item -ItemType Directory -Force -Path $ToolsRoot | Out-Null + +function Add-MachinePath([string]$Dir) { + $cur = [Environment]::GetEnvironmentVariable('Path', 'Machine') + if ($cur -notlike "*$Dir*") { + [Environment]::SetEnvironmentVariable('Path', "$cur;$Dir", 'Machine') + } + if ($env:Path -notlike "*$Dir*") { $env:Path = "$env:Path;$Dir" } +} + +# --- 1. Chocolatey (used for git, python, nuget, 7zip) --- +if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Host '==> Installing Chocolatey' + Set-ExecutionPolicy Bypass -Scope Process -Force + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Expression ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +} + +Write-Host '==> Installing base packages' +choco install -y --no-progress git python311 nuget.commandline 7zip cmake ninja +Add-MachinePath 'C:\Program Files\Git\cmd' +Add-MachinePath 'C:\Python311' +Add-MachinePath 'C:\Python311\Scripts' + +# --- 2. Visual Studio 2022 Build Tools (MSVC v143 + Win10 SDK) --- +$vsInstaller = "$env:ProgramFiles(x86)\Microsoft Visual Studio\Installer\vswhere.exe" +$vsPresent = (Test-Path $vsInstaller) -and ((& $vsInstaller -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath) -ne $null) +if (-not $vsPresent) { + Write-Host '==> Installing VS 2022 Build Tools (this takes a while)' + $vsBootstrapper = "$env:TEMP\vs_buildtools.exe" + Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_buildtools.exe' -OutFile $vsBootstrapper + $args = @( + '--quiet','--wait','--norestart','--nocache', + '--add','Microsoft.VisualStudio.Workload.VCTools', + '--add','Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '--add','Microsoft.VisualStudio.Component.VC.ATL', + '--add','Microsoft.VisualStudio.Component.Windows10SDK.20348', + '--add','Microsoft.VisualStudio.Component.VC.CMake.Project', + '--includeRecommended' + ) + $p = Start-Process -FilePath $vsBootstrapper -ArgumentList $args -Wait -PassThru + if ($p.ExitCode -notin 0,3010) { throw "VS Build Tools installer exit $($p.ExitCode)" } +} + +# --- 3. Rust (stable 1.75 + nightly-2023-10-13 with i686 target) --- +if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + Write-Host '==> Installing rustup' + Invoke-WebRequest -Uri 'https://win.rustup.rs/x86_64' -OutFile "$env:TEMP\rustup-init.exe" + & "$env:TEMP\rustup-init.exe" -y --default-toolchain none --profile minimal + Add-MachinePath "$env:USERPROFILE\.cargo\bin" +} +rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt +rustup target add --toolchain $RUST_VERSION x86_64-pc-windows-msvc +rustup toolchain install $RUST_NIGHTLY --profile minimal --component rustfmt +rustup target add --toolchain $RUST_NIGHTLY i686-pc-windows-msvc +rustup default $RUST_VERSION + +# --- 4. LLVM/Clang 15.0.6 (matches KyleMayes/install-llvm-action) --- +$llvmDir = "$ToolsRoot\llvm-$LLVM_VERSION" +if (-not (Test-Path "$llvmDir\bin\clang.exe")) { + Write-Host "==> Installing LLVM $LLVM_VERSION" + $llvmExe = "$env:TEMP\LLVM-$LLVM_VERSION-win64.exe" + Invoke-WebRequest -Uri "https://github.com/llvm/llvm-project/releases/download/llvmorg-$LLVM_VERSION/LLVM-$LLVM_VERSION-win64.exe" -OutFile $llvmExe + & $llvmExe /S "/D=$llvmDir" | Out-Null +} +[Environment]::SetEnvironmentVariable('LIBCLANG_PATH', "$llvmDir\bin", 'Machine') +Add-MachinePath "$llvmDir\bin" + +# --- 5. Flutter 3.24.5 (stable channel, with windows precache) --- +$flutterDir = "$ToolsRoot\flutter" +if (-not (Test-Path "$flutterDir\bin\flutter.bat")) { + Write-Host "==> Installing Flutter $FLUTTER_VERSION" + $flutterZip = "$env:TEMP\flutter.zip" + Invoke-WebRequest -Uri "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_$FLUTTER_VERSION-stable.zip" -OutFile $flutterZip + Expand-Archive -Force -Path $flutterZip -DestinationPath $ToolsRoot +} +Add-MachinePath "$flutterDir\bin" +& "$flutterDir\bin\flutter.bat" config --no-analytics | Out-Null +& "$flutterDir\bin\flutter.bat" precache --windows | Out-Null + +# --- 6. vcpkg pinned to commit --- +$vcpkgDir = 'C:\vcpkg' +if (-not (Test-Path "$vcpkgDir\.git")) { + Write-Host '==> Cloning vcpkg' + git clone https://github.com/microsoft/vcpkg.git $vcpkgDir +} +Push-Location $vcpkgDir +git fetch --tags origin +git -c advice.detachedHead=false checkout $VCPKG_COMMIT +if (-not (Test-Path "$vcpkgDir\vcpkg.exe")) { & "$vcpkgDir\bootstrap-vcpkg.bat" -disableMetrics } +Pop-Location +[Environment]::SetEnvironmentVariable('VCPKG_ROOT', $vcpkgDir, 'Machine') +Add-MachinePath $vcpkgDir + +# --- 7. Gitea act_runner --- +$runnerDir = 'C:\actions-runner' +New-Item -ItemType Directory -Force -Path $runnerDir | Out-Null +$runnerExe = "$runnerDir\act_runner.exe" +if (-not (Test-Path $runnerExe)) { + Write-Host "==> Downloading act_runner $RunnerVersion" + Invoke-WebRequest -Uri "https://gitea.com/gitea/act_runner/releases/download/v$RunnerVersion/act_runner-$RunnerVersion-windows-amd64.exe" -OutFile $runnerExe +} + +Push-Location $runnerDir +if (-not (Test-Path "$runnerDir\.runner")) { + Write-Host '==> Registering runner' + & $runnerExe register --no-interactive ` + --instance $GiteaUrl ` + --token $RunnerToken ` + --name $RunnerName ` + --labels $RunnerLabels +} +if (-not (Get-Service -Name 'gitea-act-runner' -ErrorAction SilentlyContinue)) { + Write-Host '==> Installing runner as Windows service' + # nssm is the cleanest way to host a console exe as a service on Windows. + choco install -y --no-progress nssm + nssm install gitea-act-runner $runnerExe daemon + nssm set gitea-act-runner AppDirectory $runnerDir + nssm set gitea-act-runner Start SERVICE_AUTO_START + nssm set gitea-act-runner AppStdout "$runnerDir\runner.log" + nssm set gitea-act-runner AppStderr "$runnerDir\runner.log" +} +Start-Service gitea-act-runner +Pop-Location + +Write-Host '' +Write-Host '==> Done. Reboot recommended so PATH/env changes take effect for the runner service.' +Write-Host ' After reboot, verify the runner shows up green in Gitea > Site Admin > Actions > Runners.' diff --git a/docs/CLIENT_VS_SERVER_GAPS.md b/docs/CLIENT_VS_SERVER_GAPS.md new file mode 100644 index 000000000..2f7872239 --- /dev/null +++ b/docs/CLIENT_VS_SERVER_GAPS.md @@ -0,0 +1,260 @@ +# RustDesk Client vs OSS Server — Feature Gap Analysis + +This document compares the RustDesk **client** ([/Users/sn0/Desktop/rustdesk](../)) against the OSS **server** ([/Users/sn0/Desktop/rustdesk-server](../../rustdesk-server/)) and lists every feature the client implements or expects, but that the OSS server (`hbbs` + `hbbr` + `rustdesk-utils`) does **not** provide. + +The OSS server is, by upstream's own description in its README, deliberately minimal: + +> Self-host your own RustDesk server, it is free and open source. +> If you want extra features, **RustDesk Server Pro** might suit you better. + +Almost every gap below is filled by RustDesk Server Pro (closed source). What follows is the concrete list. + +--- + +## TL;DR + +| Area | OSS server | Client expects | +|-----------------------------------------------------|-----------------|----------------------------------------------| +| Rendezvous protocol (UDP/TCP/WS) | ✅ implemented | ✅ | +| Relay protocol (`hbbr`) | ✅ implemented | ✅ | +| `RegisterPk` over **TCP** | ❌ `NOT_SUPPORT`| Uses UDP — non-issue if UDP reachable | +| `HttpProxyRequest` / `HttpProxyResponse` (HTTP-via-rendezvous tunnel) | ❌ no handler | Used when `USE_RAW_TCP_FOR_API=Y` | +| **Entire `/api/*` HTTP surface** (35 endpoints in [CONSOLE_API.md](CONSOLE_API.md)) | ❌ no HTTP server | All login, AB, group, audit, sysinfo, etc. | +| User / password / token authentication | ❌ | Bearer-token model | +| Address book (legacy or shared) | ❌ | Personal AB, shared AB, tags, peers | +| Device groups, users, accessible peers list | ❌ | "Group" tab in UI | +| Audit logging (conn, file, alarm, note) | ❌ | Background fire-and-forget | +| Session recording upload | ❌ | Chunked uploader | +| Sysinfo / heartbeat-based device tracking | ❌ | Every 15 s (3 s when active) | +| Strategy/policy push via heartbeat | ❌ | `config_options` / `disconnect` / `sysinfo` flag | +| 2FA, email-code, SMS-code, OIDC/SSO | ❌ | Login challenge variants | +| Plugin signing (`/lic/web/api/plugin-sign`) | ❌ | Optional, only if signed plugins shipped | +| CLI bulk device assignment (`rustdesk --assign`) | ❌ | `POST /api/devices/cli` | +| Per-tenant licensing / Pro status flag | ❌ | Inferred from sysinfo success | + +--- + +## 1. Rendezvous-protocol gaps + +The OSS server *does* speak the rendezvous protocol — it accepts and responds to all of the variants the client sends in the normal connection path. There are exactly **two** variant-level gaps: + +### 1.1 `RegisterPk` over TCP — explicitly rejected + +[src/rendezvous_server.rs:556-563](../../rustdesk-server/src/rendezvous_server.rs#L556) returns `RegisterPkResponse { result: NOT_SUPPORT }` for any TCP-arriving `RegisterPk`. UDP works fine. The client has UDP `RegisterPk` as the primary path ([src/rendezvous_mediator.rs:685-693](../src/rendezvous_mediator.rs#L685-L693)), so this is only an issue in fully UDP-blocked environments where the client falls back to TCP for everything. + +### 1.2 `HttpProxyRequest` / `HttpProxyResponse` — no handler + +When `USE_RAW_TCP_FOR_API` is enabled (and WebSocket is off), the client tunnels its HTTP API calls **as protobuf messages** over the rendezvous server's TCP socket — see [src/common.rs:1188-1250](../src/common.rs#L1188-L1250) (`tcp_proxy_request`). + +- **Wire format:** an encrypted `KeyExchange` handshake, then a `RendezvousMessage::set_http_proxy_request(HttpProxyRequest { method, path, headers, body })`, expecting `HttpProxyResponse { status_code, headers, body }` back. +- **OSS handling:** there is no match arm for `HttpProxyRequest` in [/Users/sn0/Desktop/rustdesk-server/src/rendezvous_server.rs](../../rustdesk-server/src/rendezvous_server.rs); it falls into the `_ => {}` catch-all and is silently dropped. +- **Impact:** any client that has been pushed `OPTION_USE_RAW_TCP_FOR_API=Y` (typical for restricted networks) cannot reach the HTTP API at all on an OSS-only deployment, even if the API itself were implemented. + +### 1.3 What the OSS server *does* handle (not gaps, for completeness) + +- UDP: `RegisterPeer`, `RegisterPk`, `PunchHoleRequest`, `PunchHoleSent`, `LocalAddr`, `ConfigureUpdate` (loopback only), `SoftwareUpdate`. +- TCP: `PunchHoleRequest`, `RequestRelay`, `RelayResponse`, `PunchHoleSent`, `LocalAddr`, `TestNatRequest`. +- TCP listener2 (`port - 1`): `TestNatRequest`, `OnlineRequest`, plus a loopback admin CLI. +- WebSocket on `port + 2` (default 21118): same handler set as TCP. +- The relay (`hbbr`) listens on `port` (default 21117) and `port + 2` (WebSocket, default 21119), and handles `RequestRelay` for tunnelling. + +So the **rendezvous + relay surface for plain peer-to-peer use is complete in the OSS server.** All the actual gaps are above the rendezvous layer. + +--- + +## 2. HTTP API — none of it exists in OSS + +[/Users/sn0/Desktop/rustdesk-server/Cargo.toml](../../rustdesk-server/Cargo.toml) pulls in `axum` and `tower-http`, but they are unused — no `Router::new`, no route definitions, no HTTP listener bound. **All 35 endpoints documented in [CONSOLE_API.md](CONSOLE_API.md) are gaps.** + +Grouped by feature area: + +### 2.1 Authentication & session (`/api/login`, `/api/login-options`, `/api/logout`, `/api/currentUser`) + +- No user table in the OSS schema (which is a single `peer` table — see §4 below). No password storage, hashing, salting. +- No bearer-token issuance or validation. +- No `email_check` / `tfa_check` challenge types (no email sender, no TOTP store). +- No login-options registry — client always gets an empty `oidc/...` list. +- No OIDC / SSO device flow (`/api/oidc/auth`, `/api/oidc/auth-query`). + +### 2.2 Address book — both modes missing + +The client supports two AB modes ([CONSOLE_API.md §4](CONSOLE_API.md)) and the OSS server implements neither: + +- **Legacy single-blob mode** (`GET/POST /api/ab`) — needs a per-user blob store + gzip handling. +- **Shared mode** (`/api/ab/settings`, `/api/ab/personal`, `/api/ab/shared/profiles`, `/api/ab/peers`, `/api/ab/tags/{guid}`, plus the per-peer and per-tag CRUD on `{guid}`) — needs a normalized AB / peer / tag / share-rule schema with `read | read/write | full control` ACLs. + +Without this, the client falls back to its local-only "Recents" / "Favorites" lists; nothing syncs across devices. + +### 2.3 Device groups, users, accessible peers (`/api/device-group/accessible`, `/api/users`, `/api/peers`) + +The "Group" tab in the desktop UI populates from these three endpoints. With OSS the tab is empty; the client logs `get accessible device groups: ` and silently swallows it. + +### 2.4 Heartbeat / sysinfo / strategy push (`/api/heartbeat`, `/api/sysinfo`, `/api/sysinfo_ver`) + +This is the agent-management heartbeat loop ([CONSOLE_API.md §6](CONSOLE_API.md)). + +OSS gaps: + +- No device-tracking table (last-seen, OS, hostname, version, IP, online state). +- No sysinfo cache or version string. +- No mechanism to **push back** in the heartbeat response: + - `sysinfo: ` to force an immediate sysinfo re-upload. + - `disconnect: [conn_id, ...]` to force a remote session to close. + - `modified_at` / `strategy.config_options` to push policy. +- Without strategy push, the operator cannot remotely set: + - `whitelist`, `relay-server`, `rendezvous-servers`, `direct-access-port`, + `stop-service`, `OPTION_DISABLE_UDP`, `OPTION_ENABLE_UDP_PUNCH`, + `OPTION_ENABLE_IPV6_PUNCH`, `OPTION_USE_RAW_TCP_FOR_API`, + `OPTION_DIRECT_SERVER`, etc. + - Or any of the `OPTION_PRESET_*` keys that pre-fill the address book / username / device group / strategy on a freshly enrolled client. +- `is_pro()` is set to `true` only when `/api/sysinfo` returns `SYSINFO_UPDATED` ([src/hbbs_http/sync.rs:219](../src/hbbs_http/sync.rs#L219)) — with OSS, `is_pro()` is permanently `false`, so any client behavior gated on it is disabled. + +### 2.5 Audit (`/api/audit/conn`, `/api/audit/file`, `/api/audit/alarm`, `PUT /api/audit`) + +The client emits these fire-and-forget on every: + +- new remote session ([src/server/connection.rs:1248-1252](../src/server/connection.rs#L1248-L1252)), +- file send/receive ([src/server/connection.rs:1297-1330](../src/server/connection.rs#L1297-L1330)), +- security alarm — IP whitelist hit, brute-force thresholds ([src/server/connection.rs:1332-1349](../src/server/connection.rs#L1332-L1349)), and +- the operator-typed end-of-session note (PUT, [flutter/lib/common/widgets/dialog.dart:1656-1687](../flutter/lib/common/widgets/dialog.dart#L1656-L1687)). + +Even though the client doesn't *block* on these, OSS silently swallows all of them, so: + +- No central session log. +- No file-transfer log (incl. the top-10-by-size summary the client computes). +- No alarm notifications for IP-whitelist hits or brute-force attempts. +- No audit-row GUID exists, so the "leave a note when the session ends" dialog has nothing to attach to. + +### 2.6 Session recording upload (`POST /api/record`) + +Chunked uploader ([src/hbbs_http/record_upload.rs](../src/hbbs_http/record_upload.rs)) with `?type=new|part|tail|remove`. OSS has no `/api/record` route and no on-disk recording store, so server-side recording is impossible. + +(Local-only recording on the controlling side still works — that's not a server feature.) + +### 2.7 OIDC / SSO device flow (`/api/oidc/auth`, `/api/oidc/auth-query`) + +The polled device-code flow ([src/hbbs_http/account.rs](../src/hbbs_http/account.rs)) requires an OIDC client implementation, browser-flow URL generation, and a poll-for-token side. None of this is in OSS. + +### 2.8 CLI bulk assign (`POST /api/devices/cli`) + +[src/core_main.rs:519-616](../src/core_main.rs#L519-L616). Used by `rustdesk --assign --token ...` for mass-deploy scripts to register a freshly-installed agent into a tenant, optionally setting `user_name`, `strategy_name`, `address_book_*`, `device_group_name`, `device_username`, `device_name`, `note`. Requires user/group/AB tables, none of which exist in OSS. + +### 2.9 Plugin signature service (`POST /lic/web/api/plugin-sign`) + +[src/plugin/callback_msg.rs:282-296](../src/plugin/callback_msg.rs#L282-L296). Required only if the deployment ships signed plugins. OSS has no plugin infrastructure of any kind. + +### 2.10 Generic file downloader + +`HEAD` then `GET` against an arbitrary URL with a required `Content-Length` ([src/hbbs_http/downloader.rs](../src/hbbs_http/downloader.rs)). Works against any static file server — OSS doesn't *need* to serve this, but a complete Pro-replacement backend usually exposes installer/plugin/recording downloads via this. + +--- + +## 3. Schema gaps + +The OSS database is a single SQLite table called `peer`: + +``` +guid (PK), id (UNIQUE), uuid, pk, created_at, user (unused), +status (unused), note (unused), info (JSON: { ip }) +``` + +[/Users/sn0/Desktop/rustdesk-server/src/database.rs:71-144](../../rustdesk-server/src/database.rs#L71-L144). + +To support the client's HTTP surface a backend needs at minimum: + +- `users` (id, name, display_name, avatar, email, note, password_hash, status, is_admin, totp_secret, oidc_subject, …) +- `tokens` (token_hash, user_id, expires_at) +- `oidc_sessions` (poll_code, state, created_at, access_token, …) +- `address_books` (guid, owner_user_id, name, note, kind=personal|shared) +- `address_book_shares` (ab_guid, user_or_group_id, rule={1,2,3}) +- `address_book_peers` (ab_guid, peer_id, alias, tags[], note, password|hash, username, hostname, platform) +- `address_book_tags` (ab_guid, name, color) +- `device_groups` (id, name) +- `device_group_members` (device_group_id, user_or_group_id) +- `peers_extended` (peer_id, user_id, device_group_id, last_seen, version, sysinfo_blob, sysinfo_hash, sysinfo_ver, online, …) +- `audit_conn` (guid, peer_id, conn_id, session_id, action, ip, started_at, ended_at, note) +- `audit_file` (peer_id, peer_remote, type, path, is_file, info_json) +- `audit_alarm` (peer_id, typ, info_json) +- `recordings` (filename, peer_id, size, header_blob, started_at, finished_at) +- `strategies` (id, name, modified_at, config_options_json, extra_json) +- `peer_strategy_assignment` / `device_group_strategy_assignment` + +The OSS schema covers exactly **one row** of one of those tables (`peers_extended.peer_id` plus `pk`/`uuid`). Everything else is a gap. + +--- + +## 4. Authentication / authorization gaps + +- **No user/password.** The OSS server identifies a peer entirely by `(id, uuid, pk)`. There is no concept of a logged-in *human user*, no password, no session, no role. +- **No bearer tokens.** The client adds `Authorization: Bearer ` to every authenticated HTTP call ([flutter/lib/common.dart:2691-2695](../flutter/lib/common.dart#L2691-L2695)). With no HTTP API and no user store, OSS has nothing to validate against. +- **No 2FA.** The client supports TOTP challenge (`type: tfa_check`, `tfa_type`, `secret`) and per-device 2FA-trust ([src/ui_session_interface.rs](../src/ui_session_interface.rs)). Not present in OSS. +- **No email/SMS verification.** The `email_check` challenge type and `verificationCode` field have no sender on the OSS side. +- **No SSO / OIDC.** No identity-provider integration. +- **No admin/role concept.** The `UserPayload.is_admin` flag (used to gate the user-management UI) has no source. +- **No per-AB ACL.** `AbProfile.rule` (read / read-write / full control) has no enforcement layer. +- **No IP allowlisting / per-IP rate-limiting on HTTP endpoints.** OSS rate-limits `RegisterPk` per source IP ([src/rendezvous_server.rs:891-919](../../rustdesk-server/src/rendezvous_server.rs#L891-L919)) but that's at the rendezvous layer only. + +What the OSS server *does* offer in this space: + +- Optional symmetric server key (`-k`) checked against `licence_key` in `PunchHoleRequest` and `RequestRelay`. This is shared-secret deployment lockdown, not user auth. +- Ed25519 signing of `RelayResponse` payloads using the server's private key. + +--- + +## 5. Operations / fleet management gaps + +These are conveniences a Pro server offers via the strategy/heartbeat channel; OSS has no equivalent because heartbeat itself is not implemented. + +- **Force-disconnect a remote session** from the admin console (heartbeat returns `disconnect: [conn_id]` — [src/hbbs_http/sync.rs:251-254](../src/hbbs_http/sync.rs#L251-L254)). +- **Force-refresh sysinfo** (`sysinfo` truthy in heartbeat). +- **Push global config** to all enrolled agents (the `strategy.config_options` map). Without this, every option must be set per-machine. +- **Pre-seed an agent** at install time with an address-book entry, alias, password, note, strategy, device group, custom hostname/username (`OPTION_PRESET_ADDRESS_BOOK_*`, `OPTION_PRESET_USERNAME`, `OPTION_PRESET_STRATEGY_NAME`, `OPTION_PRESET_DEVICE_GROUP_NAME`, …). Client emits these preset values on every sysinfo, but OSS discards them. +- **Operator end-of-session notes** (PUT `/api/audit`). +- **`rustdesk --assign --token …`** for mass deployment. +- **Brute-force / IP-whitelist alarms** to a central log. + +What OSS *does* offer for ops: + +- A loopback-only TCP admin CLI on `port - 1` (default 21115) for hbbs and on `port` for hbbr ([src/rendezvous_server.rs:1102-1116](../../rustdesk-server/src/rendezvous_server.rs#L1102-L1116), [src/relay_server.rs:152-323](../../rustdesk-server/src/relay_server.rs#L152-L323)) — `relay-servers`, `ip-blocker`, `ip-changes`, `punch-requests`, `always-use-relay`, `test-geo`, `blacklist-add`, `blocklist-add`, `total-bandwidth`, `usage`, etc. +- A `ConfigureUpdate` push *only from loopback* — the operator can update the rendezvous-server list pushed to clients, but only by `nc 127.0.0.1 21115` on the server box itself. + +--- + +## 6. Client-side features that work fine against OSS + +For balance — these features in the client need no Pro-server support at all: + +- All in-session protocol (after the relay/direct connection is established): screen sharing, file transfer, terminal, RDP / VNC tunnel, port forward, voice call, view-only mode, whiteboard, printer, clipboard, multi-monitor, mouse/keyboard injection. These are negotiated on the session stream itself and never touch the management server. +- LAN discovery (when both ends are reachable on the same LAN, no rendezvous server needed at all). +- The client's local 2FA on the *controlled* side ("ask the operator for a one-time code"). That's a peer-to-peer protocol negotiation, not a server feature. +- IP-whitelist enforcement on the controlled side ([src/server/connection.rs:1202-1228](../src/server/connection.rs#L1202-L1228)) — done locally against the `whitelist` config option. (But the *operator UX* of pushing that whitelist to a fleet is missing — see §5.) +- Self-update — the client checks a hardcoded URL on the public update server, not the configured rendezvous/API server. +- Custom-server bootstrap via filename (`rustdesk-host=…,key=…,api=…,relay=….exe`, [src/custom_server.rs](../src/custom_server.rs)) — works against OSS as long as the `api=` field is left empty / public. + +--- + +## 7. What you'd need to build to fully replace Pro + +Given the analysis above, a full Pro-replacement backend on top of OSS would need: + +1. **Add an HTTP server** (axum is already in the Cargo.toml of OSS, unused). Implement the 35 routes in [CONSOLE_API.md](CONSOLE_API.md). +2. **Add a `HttpProxyRequest` handler** in `rendezvous_server.rs` so that locked-down clients can reach the HTTP API through the rendezvous TCP port (decode the protobuf, replay the request internally, wrap the response). +3. **Extend the schema** along the lines of §3. +4. **Add user / token / OIDC / 2FA layers**, plus an email sender for `email_check`. +5. **Implement the strategy / push-config side of `/api/heartbeat`** and the sysinfo cache for `/api/sysinfo*`. +6. **Add audit + recording stores** with retention and access-control. +7. (Optional) **Plugin signing service** if you're shipping signed plugins. + +The rendezvous + relay protocol itself does not need to change — OSS is correct and complete there. + +--- + +## Source-of-truth references + +- Client HTTP API the server must serve: [docs/CONSOLE_API.md](CONSOLE_API.md). +- Client rendezvous receive loop: [src/rendezvous_mediator.rs](../src/rendezvous_mediator.rs). +- Client HTTP-via-TCP fallback: [src/common.rs:1188-1250](../src/common.rs#L1188-L1250). +- Client heartbeat loop: [src/hbbs_http/sync.rs](../src/hbbs_http/sync.rs). +- OSS rendezvous handler: [/Users/sn0/Desktop/rustdesk-server/src/rendezvous_server.rs](../../rustdesk-server/src/rendezvous_server.rs). +- OSS relay handler: [/Users/sn0/Desktop/rustdesk-server/src/relay_server.rs](../../rustdesk-server/src/relay_server.rs). +- OSS schema: [/Users/sn0/Desktop/rustdesk-server/src/database.rs](../../rustdesk-server/src/database.rs). diff --git a/docs/CONSOLE_API.md b/docs/CONSOLE_API.md new file mode 100644 index 000000000..55e6e1736 --- /dev/null +++ b/docs/CONSOLE_API.md @@ -0,0 +1,1038 @@ +# RustDesk Console API — Backend Specification + +This document specifies every HTTP/HTTPS endpoint that the RustDesk client in this +workspace calls against its server-side **Console / API server** (the API surface +exposed by RustDesk Server Pro / `hbbs`). The intent is that a backend implemented +to this spec will be able to serve a stock RustDesk client with no client-side +changes. + +The client base URL is whatever the user / installer has configured as +`api-server` (or, falling back, derived from `custom-rendezvous-server`). All +paths below are relative to that base URL. + +If the configured API host string `contains "rustdesk.com/"` or ends with +`"rustdesk.com"`, the client treats it as the public/managed server and +**suppresses** the heartbeat, sysinfo, and audit endpoints +([`is_public()` in src/common.rs:1088](../src/common.rs#L1088)). +A self-hosted backend MUST NOT use a hostname matching that pattern. + +--- + +## 1. Conventions + +### 1.1 Transport + +- HTTPS expected in production. The client probes TLS (`rustls-tls` first, then + `native-tls`, then with `danger_accept_invalid_certs`) on the first call and + caches the result. See [src/hbbs_http/http_client.rs](../src/hbbs_http/http_client.rs). +- A non-200 response body that is valid JSON with an `"error"` key is treated as + a structured error. See [`HbbHttpResponse::parse` in src/hbbs_http.rs:24-39](../src/hbbs_http.rs#L24-L39): + ```json + { "error": "human readable message" } + ``` +- Default request timeout: **12 seconds** + ([src/common.rs:1431](../src/common.rs#L1431)). +- Logout has a hard **2 second** timeout + ([flutter/lib/models/user_model.dart:168](../flutter/lib/models/user_model.dart#L168)). +- The plugin-sign call uses **10 seconds** ([src/plugin/callback_msg.rs:293](../src/plugin/callback_msg.rs#L293)). + +### 1.2 Headers + +For every authenticated request the client adds: + +``` +Authorization: Bearer +Content-Type: application/json +``` + +`Authorization` is built by [`getHttpHeaders()` in flutter/lib/common.dart:2691-2695](../flutter/lib/common.dart#L2691-L2695) +from the locally stored `access_token`. Unauthenticated calls (login, OIDC, +heartbeat, sysinfo, audit, record, plugin-sign) omit `Authorization`. + +### 1.3 Error envelope + +Successful action endpoints (peer add/update/delete, tag CRUD, etc.) typically +return either an empty body / `null` / `{}` on success, or +`{ "error": "" }` on failure with HTTP 200. The status code alone +is not a sufficient success indicator — the client always inspects the JSON +`error` field. + +### 1.4 Pagination + +List endpoints use 1-indexed pagination, query parameters: + +``` +?current=&pageSize= +``` + +The client always uses `pageSize=100` and iterates pages until +`current * pageSize >= total`. Response: + +```json +{ + "total": , + "data": [ ... ] +} +``` + +### 1.5 IDs and identifiers + +- **`id`** — RustDesk peer/device ID (the 9-digit number shown in the UI). +- **`uuid`** — A per-install machine identifier, base64-encoded by the client + before transmission (`crate::encode64(hbb_common::get_uuid())`). +- **`guid`** — Server-assigned GUID for an address book. +- **`conn_id`**, **`session_id`** — Internal ints assigned by the client per + remote session, echoed back in audit logs. + +### 1.6 `deviceInfo` object + +Sent inside login bodies. Shape (from +[src/hbbs_http/account.rs:31-44](../src/hbbs_http/account.rs#L31-L44)): + +```json +{ + "os": "Linux | Windows | macOS | Android | iOS", + "type": "client | browser", + "name": "" +} +``` + +--- + +## 2. Base URL & API discovery + +The client resolves its base URL via +[`get_api_server()` in src/common.rs](../src/common.rs); a custom-rendezvous +server with no explicit `api-server` is mapped to +`https://` (port 21114 if specified inline). + +The client makes a `HEAD` request against `/api/login-options` on +startup as a connectivity / TLS-detection probe +([src/hbbs_http/account.rs:155](../src/hbbs_http/account.rs#L155), +[src/hbbs_http/record_upload.rs:34](../src/hbbs_http/record_upload.rs#L34)). +A backend SHOULD respond 200 to a HEAD on `/api/login-options`. + +--- + +## 3. Authentication + +### 3.1 `POST /api/login` + +User credential / 2FA / SSO completion. + +- **Source:** [flutter/lib/models/user_model.dart:178-202](../flutter/lib/models/user_model.dart#L178-L202) +- **Auth:** none +- **Headers:** `Content-Type: application/json` (sent implicitly by `http.post`) + +**Request body** (from `LoginRequest.toJson()` at +[flutter/lib/common/hbbs/hbbs.dart:133-178](../flutter/lib/common/hbbs/hbbs.dart#L133-L178)): + +```json +{ + "username": "string", // optional + "password": "string", // optional + "id": "string", // RustDesk peer ID + "uuid": "string", // base64 + "autoLogin": true, // optional bool + "type": "account | mobile | sms_code | email_code | tfa_code | oidc/", + "verificationCode": "string", // optional, for email/SMS challenge + "tfaCode": "string", // optional + "secret": "string", // optional, echoed back from a tfa_check response + "deviceInfo": { "os": "...", "type": "...", "name": "..." } +} +``` + +`type` constants +([flutter/lib/common/hbbs/hbbs.dart:11-15](../flutter/lib/common/hbbs/hbbs.dart#L11-L15)): +`account`, `mobile`, `sms_code`, `email_code`, `tfa_code`. `oidc/` is +also accepted (the `oidc/` prefix triggers the device-flow described in §3.5). + +**Successful response** — `LoginResponse` shape +([flutter/lib/common/hbbs/hbbs.dart:180-197](../flutter/lib/common/hbbs/hbbs.dart#L180-L197)): + +```json +{ + "access_token": "opaque-bearer-token", + "type": "access_token", + "tfa_type": "totp | ...", // present when type == "tfa_check" + "secret": "string", // present when type == "tfa_check" + "user": { + "name": "string", + "display_name": "string", + "avatar": "string", + "email": "string", + "note": "string", + "status": 1, // -1=unverified, 0=disabled, 1=normal + "is_admin": false, + "verifier": "string" // optional, used by web build only + } +} +``` + +**Challenge responses.** When 2FA / verification is required, `type` is one of +([flutter/lib/common/hbbs/hbbs.dart:17-19](../flutter/lib/common/hbbs/hbbs.dart#L17-L19)): + +| `type` | Meaning | +|----------------|-----------------------------------------------------------------| +| `access_token` | Login complete, `access_token` populated. | +| `email_check` | Server emailed a code; client must POST again with `type:"email_code"` and `verificationCode`. | +| `tfa_check` | TOTP required; server returns `tfa_type` and `secret` to echo back with `type:"tfa_code"` + `tfaCode`. | + +**Status / error semantics:** +- Non-200 with body `{"error": "..."}` → client surfaces the error. +- 401 from this endpoint is treated as bad credentials (no auto-logout). +- 401 from any **other** authenticated endpoint clears the local token (see §3.6). + +### 3.2 `GET /api/login-options` + +Returns the list of login methods the server is configured to expose. + +- **Source:** [flutter/lib/models/user_model.dart:222-245](../flutter/lib/models/user_model.dart#L222-L245) +- **Auth:** none +- **Method:** `GET` (no body) + +**Response** — JSON array of strings: + +```json +[ + "account", + "oidc/google", + "oidc/github", + "common-oidc/[ {\"name\":\"google\",\"icon\":\"...\"} ]" +] +``` + +Two recognised conventions: + +- `oidc/` — exposed in the UI as an SSO button labelled ``. +- `common-oidc/` — the suffix is a JSON-encoded array of provider + descriptors `{ "name": "...", ... }`. If present anywhere in the list, it + takes precedence over individual `oidc/...` entries. + +### 3.3 `POST /api/currentUser` + +Refreshes the cached profile of the currently logged-in user. + +- **Source:** [flutter/lib/models/user_model.dart:60-99](../flutter/lib/models/user_model.dart#L60-L99) +- **Auth:** Bearer + +**Request body:** + +```json +{ "id": "", "uuid": "" } +``` + +**Response:** `UserPayload` (see §3.1). On HTTP 401 or 400 the client clears +the access token (401 also wipes the address-book and group caches). + +### 3.4 `POST /api/logout` + +- **Source:** [flutter/lib/models/user_model.dart:155-175](../flutter/lib/models/user_model.dart#L155-L175) +- **Auth:** Bearer +- **Timeout:** 2 s; failures are silently ignored client-side. + +**Request body:** + +```json +{ "id": "", "uuid": "" } +``` + +**Response:** body is ignored. + +### 3.5 OIDC / SSO device flow + +Two endpoints implement a polled device-code flow. + +#### `POST /api/oidc/auth` + +- **Source:** [src/hbbs_http/account.rs:160-176](../src/hbbs_http/account.rs#L160-L176) +- **Auth:** none + +**Request body:** + +```json +{ + "op": "", // matches an entry from /api/login-options + "id": "", + "uuid": "", + "deviceInfo": { "os": "...", "type": "...", "name": "..." } +} +``` + +**Response** — `OidcAuthUrl` ([src/hbbs_http/account.rs:24-28](../src/hbbs_http/account.rs#L24-L28)): + +```json +{ + "code": "opaque-poll-handle", + "url": "https://server/oidc/redirect?code=...&..." +} +``` + +The client opens `url` in the user's browser and polls (§ next). + +#### `GET /api/oidc/auth-query` + +- **Source:** [src/hbbs_http/account.rs:178-202](../src/hbbs_http/account.rs#L178-L202) +- **Auth:** none +- **Polling:** 1 s interval, **180 s** timeout (`QUERY_TIMEOUT_SECS = 60*3`). + +**Query parameters:** `code` (from auth response), `id`, `uuid`. + +**Response wrapper** — the response is an outer envelope whose `body` field is +itself JSON: + +```json +{ + "body": "" +} +``` + +The inner body, once parsed, is either: +- A normal `AuthBody` (same shape as §3.1 success), **or** +- `{ "error": "No authed oidc is found" }` while waiting (the client keeps + polling until success or timeout), **or** +- Any other `{ "error": "..." }` (terminates polling with an error). + +### 3.6 401 handling + +A 401 from any authenticated endpoint causes the Flutter client to: + +1. Clear `access_token` and cached `user_info` from local config. +2. Reset address-book and group state. + +A 400 from `/api/currentUser` clears the access token but does not reset other state +([flutter/lib/models/user_model.dart:81-83](../flutter/lib/models/user_model.dart#L81-L83)). + +--- + +## 4. Address book + +The client supports two modes: + +- **Legacy mode** — the whole address book is GET/PUT as a single JSON blob. + The server returns 404 on `/api/ab/personal` to signal legacy mode + ([flutter/lib/models/ab_model.dart:271-274](../flutter/lib/models/ab_model.dart#L271-L274)). +- **Shared mode** — the personal AB and zero or more shared address books are + represented as separate `guid`-keyed objects with paginated peer/tag lists. + +A backend SHOULD implement shared mode (legacy mode is supported only as +fall-back). + +### 4.1 `POST /api/ab/settings` + +Capability/limits probe. Called once per pull cycle. + +- **Source:** [flutter/lib/models/ab_model.dart:230-258](../flutter/lib/models/ab_model.dart#L230-L258) +- **Auth:** Bearer +- **Request body:** empty (Content-Type still `application/json`) +- **Special status:** `404` ⇒ "this server does not support shared AB; abort + init". Any other non-200 surfaces `pull_ab_failed_tip`. + +**Response:** + +```json +{ "max_peer_one_ab": 100 } +``` + +### 4.2 `POST /api/ab/personal` + +Returns the GUID of the caller's personal address book. + +- **Source:** [flutter/lib/models/ab_model.dart:262-293](../flutter/lib/models/ab_model.dart#L262-L293) +- **Auth:** Bearer +- **Request body:** empty +- **Special status:** `404` ⇒ legacy mode; client falls back to §4.7/§4.8. + +**Response:** + +```json +{ "guid": "personal-ab-guid" } +``` + +### 4.3 `POST /api/ab/shared/profiles` + +Paginated list of shared address books visible to the user. + +- **Source:** [flutter/lib/models/ab_model.dart:295-360](../flutter/lib/models/ab_model.dart#L295-L360) +- **Auth:** Bearer +- **Method:** `POST` (the resource is fetched with POST in the Flutter client, + query params are still used for pagination) +- **Query:** `current`, `pageSize` +- **Request body:** empty +- **Special status:** `404` ⇒ no shared-AB support. + +**Response — page of `AbProfile`:** + +```json +{ + "total": 12, + "data": [ + { + "guid": "ab-guid", + "name": "Engineering", + "owner": "alice", + "note": "optional string", + "rule": 2, // 1=read, 2=read/write, 3=full control + "info": { ... } // opaque, surfaced in the UI as-is + } + ] +} +``` + +`rule` enum from [flutter/lib/common/hbbs/hbbs.dart:210-256](../flutter/lib/common/hbbs/hbbs.dart#L210-L256). + +### 4.4 `POST /api/ab/peers?ab=` + +Paginated list of peers in a given address book. + +- **Source:** [flutter/lib/models/ab_model.dart:1432-1497](../flutter/lib/models/ab_model.dart#L1432-L1497) +- **Auth:** Bearer +- **Method:** `POST` +- **Query:** `current`, `pageSize`, `ab=` +- **Request body:** empty + +**Response page entry — `Peer`:** + +```json +{ + "id": "123456789", + "alias": "string", + "tags": ["tag1", "tag2"], + "note": "string", + "hash": "string", // present in personal AB only (password hash) + "password": "string", // present in shared AB only (encrypted password) + "username": "string", // OS username on the remote device + "hostname": "string", + "platform": "Windows | Linux | Mac OS | Android | iOS" +} +``` + +`hash`/`password` are mutually exclusive: the client strips `password` for personal +ABs and `hash` for shared ABs before pushing +([flutter/lib/models/ab_model.dart:1561-1565](../flutter/lib/models/ab_model.dart#L1561-L1565)). + +### 4.5 `POST /api/ab/tags/{guid}` + +List the tags of an address book. Returns a JSON array (no pagination). + +- **Source:** [flutter/lib/models/ab_model.dart:1499-1544](../flutter/lib/models/ab_model.dart#L1499-L1544) +- **Auth:** Bearer +- **Method:** `POST` +- **Request body:** empty + +**Response:** + +```json +[ + { "name": "Production", "color": -16776961 }, + { "name": "QA", "color": -65536 } +] +``` + +`color` is a Flutter `Color.value` (signed 32-bit ARGB packed integer). + +### 4.6 Peer mutations on a shared/personal AB + +All four take `Authorization` + `Content-Type: application/json`. Success is +either an empty body or `{}`; failure is HTTP 200 with `{"error":"..."}` (the +client reads `error` regardless of status). + +#### `POST /api/ab/peer/add/{guid}` — add a peer + +Source: [flutter/lib/models/ab_model.dart:1548-1578](../flutter/lib/models/ab_model.dart#L1548-L1578). + +```json +{ + "id": "123456789", + "alias": "string", + "tags": ["..."], + "note": "string", + "password": "string", // shared AB + "hash": "string", // personal AB + "username": "string", + "hostname": "string", + "platform": "string" +} +``` + +The client adds peers one-by-one (one HTTP call per peer). + +#### `PUT /api/ab/peer/update/{guid}` — partial peer update + +Source: [flutter/lib/models/ab_model.dart:1580-1729](../flutter/lib/models/ab_model.dart#L1580-L1729). + +The body always contains `id`, plus any subset of mutable fields. The client uses +this single endpoint for: alias change, note change, tag change, password +(`password`) / hash (`hash`) change, and `username`/`hostname`/`platform` sync +from recent connections. + +```json +{ + "id": "123456789", + "alias": "string", + "tags": ["..."], + "note": "string", + "password": "string", + "hash": "string", + "username": "string", + "hostname": "string", + "platform": "string" +} +``` + +#### `DELETE /api/ab/peer/{guid}` — bulk delete + +Source: [flutter/lib/models/ab_model.dart:1751-1771](../flutter/lib/models/ab_model.dart#L1751-L1771). + +Body is a JSON array of peer IDs: + +```json +["123456789", "987654321"] +``` + +### 4.7 Tag mutations on a shared/personal AB + +#### `POST /api/ab/tag/add/{guid}` + +Source: [flutter/lib/models/ab_model.dart:1775-1802](../flutter/lib/models/ab_model.dart#L1775-L1802). +The client iterates one POST per tag. + +```json +{ "name": "tag", "color": -16776961 } +``` + +#### `PUT /api/ab/tag/rename/{guid}` + +Source: [flutter/lib/models/ab_model.dart:1804-1831](../flutter/lib/models/ab_model.dart#L1804-L1831). + +```json +{ "old": "old-name", "new": "new-name" } +``` + +#### `PUT /api/ab/tag/update/{guid}` — set color + +Source: [flutter/lib/models/ab_model.dart:1833-1855](../flutter/lib/models/ab_model.dart#L1833-L1855). + +```json +{ "name": "tag", "color": -16776961 } +``` + +#### `DELETE /api/ab/tag/{guid}` — bulk delete + +Source: [flutter/lib/models/ab_model.dart:1857-1876](../flutter/lib/models/ab_model.dart#L1857-L1876). + +Body is a JSON array of tag names: `["tagA", "tagB"]`. + +### 4.8 Legacy address book (single-blob mode) + +Used only when `/api/ab/personal` returns 404. + +#### `GET /api/ab` + +Source: [flutter/lib/models/ab_model.dart:1007-1053](../flutter/lib/models/ab_model.dart#L1007-L1053). + +- **Auth:** Bearer +- **Headers:** `Accept-Encoding: gzip` +- **Response:** + +```json +{ + "data": "", + "licensed_devices": 100 +} +``` + +`data` decoded: + +```json +{ + "tags": ["tag1", ...], + "peers": [ { "id": "...", "alias": "...", ... }, ... ], + "tag_colors": "" +} +``` + +A response body of the literal string `null` (or empty) means "empty AB, no +error". + +#### `POST /api/ab` + +Source: [flutter/lib/models/ab_model.dart:1055-1096](../flutter/lib/models/ab_model.dart#L1055-L1096). + +- **Auth:** Bearer +- **Body:** the entire address book replaces what the server stores. + +```json +{ "data": "" } +``` + +Success: HTTP 200 with body empty / `null` / `{}`. Failure: `{"error":"..."}`. + +--- + +## 5. Device groups, users, peers (group view) + +These three endpoints together populate the "Group / Device" tab. All three are +GET, paginated, Bearer-authenticated. + +### 5.1 `GET /api/device-group/accessible` + +Source: [flutter/lib/models/group_model.dart:103-158](../flutter/lib/models/group_model.dart#L103-L158). + +- **Query:** `current`, `pageSize` +- **Behaviour:** the client treats *any* error from this endpoint as + "old hbbs without device-group support" and silently continues. + +**Response page entry — `DeviceGroupPayload`:** + +```json +{ "name": "Engineering" } +``` + +### 5.2 `GET /api/users` + +Source: [flutter/lib/models/group_model.dart:160-222](../flutter/lib/models/group_model.dart#L160-L222). + +- **Query:** `current`, `pageSize`, `accessible=` (empty string), `status=1` +- **Auth:** Bearer + +**Response page entry — `UserPayload`** (same shape as §3.1). + +The client recognises the legacy errors `"Admin required!"` and +`"ambiguous column name: status"` and translates them to a "please upgrade +RustDesk Server Pro" toast. Backends should not produce them. + +### 5.3 `GET /api/peers` + +Source: [flutter/lib/models/group_model.dart:224-282](../flutter/lib/models/group_model.dart#L224-L282). + +- **Query:** `current`, `pageSize`, `accessible=` (empty), `status=1` +- **Auth:** Bearer + +**Response page entry — `PeerPayload`** (from +[flutter/lib/common/hbbs/hbbs.dart:77-131](../flutter/lib/common/hbbs/hbbs.dart#L77-L131)): + +```json +{ + "id": "123456789", + "user": "alice", + "user_name": "Alice Doe", + "device_group_name": "Engineering", + "note": "string", + "status": 1, + "info": { + "username": "alice", + "device_name": "ALICE-PC", + "os": "Windows 10 / x64" // first " / "-separated token used + // to determine platform + } +} +``` + +--- + +## 6. Heartbeat & system info + +These three endpoints are the "agent loop". They are **never** sent if the +configured API server matches the public `rustdesk.com` pattern +([src/hbbs_http/sync.rs:276-285](../src/hbbs_http/sync.rs#L276-L285)). + +The loop wakes every 3 s +([`TIME_CONN` in src/hbbs_http/sync.rs:19](../src/hbbs_http/sync.rs#L19)) +but only sends a heartbeat every **15 s** unless connections changed +([`TIME_HEARTBEAT` in src/hbbs_http/sync.rs:17](../src/hbbs_http/sync.rs#L17)). +Sysinfo is re-uploaded at most every **120 s** +([`UPLOAD_SYSINFO_TIMEOUT` in src/hbbs_http/sync.rs:18](../src/hbbs_http/sync.rs#L18)) +and only if hash/version differ. + +### 6.1 `POST /api/heartbeat` + +Source: [src/hbbs_http/sync.rs:235-271](../src/hbbs_http/sync.rs#L235-L271). + +- **Auth:** none +- **Headers:** `Content-Type: application/json` + +**Request body:** + +```json +{ + "id": "123456789", + "uuid": "", + "ver": 123456, // numeric version (hbb_common::get_version_number) + "conns": [101, 102], // omitted if no active connections + "modified_at": 0 // last strategy timestamp the client knows +} +``` + +**Response** — any subset of: + +```json +{ + "sysinfo": "any-truthy-value", // presence forces sysinfo re-upload + "disconnect": [101], // conn IDs the client should drop + "modified_at": 1700000000, // newer timestamp ⇒ persist locally + "strategy": { + "config_options": { "": "", ... }, + "extra": { ... } + } +} +``` + +Any value the client does not recognise is ignored. `strategy.config_options` +is merged into the client's options; an empty value with no built-in default +removes the option, otherwise it overwrites. + +### 6.2 `POST /api/sysinfo` + +The client first probes versions with `/api/sysinfo_ver` (§6.3); if the version +matches what the server already has, this POST is skipped. + +Source: [src/hbbs_http/sync.rs:131-229](../src/hbbs_http/sync.rs#L131-L229). + +- **Auth:** none +- **Headers:** `Content-Type: application/json` + +**Request body** (top-level fields are merged from +[`get_sysinfo()`](../src/common.rs) plus preset options): + +```json +{ + "version": "1.4.x", + "id": "123456789", + "uuid": "", + "username": "alice", // OS username + "hostname": "ALICE-PC", + "os": "Windows 10 / x64", + "cpu": "...", "memory": "...", // and other fields from get_sysinfo + "preset_address_book_name": "...", // optional, only if configured + "preset_address_book_tag": "...", + "preset_address_book_alias": "...", + "preset_address_book_password": "...", + "preset_address_book_note": "...", + "preset_username": "...", + "preset_strategy_name": "...", + "preset_device_group_name": "..." +} +``` + +**Response** — body is treated as a **bare string** (not JSON): + +| Body | Meaning | +|---------------------|---------| +| `SYSINFO_UPDATED` | Success. Client caches a SHA-256 of (URL+body) and the version returned by `/api/sysinfo_ver`. | +| `ID_NOT_FOUND` | Re-upload at next heartbeat tick (no cache). | +| anything else / err | Treated as success-with-deferral (cache still skipped). | + +### 6.3 `POST /api/sysinfo_ver` + +Source: [src/hbbs_http/sync.rs:192-208](../src/hbbs_http/sync.rs#L192-L208). + +- **Auth:** none +- **Body:** empty +- **Response:** an opaque version string. The client compares against its + cached `sysinfo_ver`; if equal **and** the request hash is unchanged, the + full sysinfo upload is skipped this cycle. + +Backends without versioning may always return an empty string; the client will +then upload sysinfo each cycle. + +--- + +## 7. Audit logging + +Audit endpoints are **only** called when the API server is non-public +([`get_audit_server()` in src/common.rs:1119-1125](../src/common.rs#L1119-L1125)). + +All three are POST with `Content-Type: application/json`, **no Authorization +header**, and fire-and-forget (response is ignored beyond logging). They share a +common envelope: + +```json +{ + "id": "", + "uuid": "", + ...endpoint-specific fields +} +``` + +### 7.1 `POST /api/audit/conn` + +Source: [src/server/connection.rs:1248-1279](../src/server/connection.rs#L1248-L1279). + +```json +{ + "id": "...", + "uuid": "...", + "conn_id": 101, + "session_id": 7, + "ip": "192.0.2.10", + "action": "new" +} +``` + +Currently the only `action` emitted by the client is `"new"` (sent immediately +after the remote IP is verified against the IP whitelist). The server response +is ignored, but the dialog flow in §7.4 implies the server returns or stores +a `guid` per audit row. + +### 7.2 `POST /api/audit/file` + +Source: [src/server/connection.rs:1297-1330](../src/server/connection.rs#L1297-L1330). + +```json +{ + "id": "...", + "uuid": "...", + "peer_id": "", + "type": 0, // 0 = RemoteSend, 1 = RemoteReceive + "path": "C:\\path\\to\\dir", + "is_file": false, + "info": "" +} +``` + +`info`, decoded: + +```json +{ + "ip": "192.0.2.10", + "name": "alice (display name)", + "num": 42, // total files in the operation + "files": [ { "name": "big.iso", "size": 4400000000 }, ... ] // top-10 by size +} +``` + +`type` enum from [src/server/connection.rs:5063-5066](../src/server/connection.rs#L5063-L5066): +`0 = RemoteSend`, `1 = RemoteReceive`. `is_file` is `true` only when the +operation is a single file (`files.len() == 1 && files[0].name == ""`). + +### 7.3 `POST /api/audit/alarm` + +Source: [src/server/connection.rs:1332-1349](../src/server/connection.rs#L1332-L1349). + +```json +{ + "id": "...", + "uuid": "...", + "typ": 0, // see enum below + "info": "" +} +``` + +`typ` enum from [src/server/connection.rs:5053-5061](../src/server/connection.rs#L5053-L5061): + +| Value | Constant | Meaning | +|-------|----------------------------|----------------------------------------| +| 0 | `IpWhitelist` | Connection rejected by IP whitelist. | +| 1 | `ExceedThirtyAttempts` | >30 password attempts. | +| 2 | `SixAttemptsWithinOneMinute` | 6 password attempts in 60 s. | +| 6 | `ExceedIPv6PrefixAttempts` | Per-/64 IPv6 attempt ceiling exceeded. | + +(Values 3–5 are reserved / commented out.) + +### 7.4 `PUT /api/audit` + +Update an existing connection-audit row (used by the "leave a note at end of +session" dialog). + +- **Source:** [flutter/lib/common/widgets/dialog.dart:1656-1687](../flutter/lib/common/widgets/dialog.dart#L1656-L1687) +- **Auth:** Bearer +- **Headers:** `Content-Type: application/json` + +```json +{ + "guid": "", + "note": "free-form text" +} +``` + +A 200 status is treated as success; non-200 is logged and discarded. + +--- + +## 8. Session recording upload + +Used when session-recording-on-the-server is enabled. All requests share the +endpoint `POST /api/record` and disambiguate via the `type` query parameter. + +- **Source:** [src/hbbs_http/record_upload.rs](../src/hbbs_http/record_upload.rs) +- **Auth:** none +- **Content-Type:** the body is raw `application/octet-stream`-style bytes + (the client uses `reqwest`'s default for `Bytes`/`Vec` — no explicit + `Content-Type` header). Servers should accept any. +- **Send cadence:** at most every 1 s, or whenever ≥1 MiB of new data is + available, whichever comes first + ([`SHOULD_SEND_TIME` and `SHOULD_SEND_SIZE` at lines 16-17](../src/hbbs_http/record_upload.rs#L16-L17)). + +**Per-call query parameters:** + +| `type` | `file` | `offset` | `length` | Body | When sent | +|----------|--------|-----------------------|---------------------|--------------------------------------|--------------------------| +| `new` | yes | — | — | empty | New recording starts. | +| `part` | yes | byte offset (decimal) | bytes to follow | raw chunk | Periodic upload. | +| `tail` | yes | `0` | header length (≤1024) | first ≤1024 bytes of file | Recording finished; uploaded after final `part`. | +| `remove` | yes | — | — | empty | Recording aborted. | + +`file` is the basename, e.g. `2025-04-12_14-22-01.mp4`. + +**Response:** any JSON object. If it contains `{"error": "..."}`, the client +aborts the recording session and logs the error. + +--- + +## 9. Generic file download + +Used by the auto-update / installer-fetch path +([src/hbbs_http/downloader.rs](../src/hbbs_http/downloader.rs)). + +- The URL is **arbitrary**; not necessarily on the API server. Path-format + agnostic. +- The client first sends `HEAD` to read `Content-Length`, then `GET` to stream + the body. Both calls go through the same TLS-fallback machinery as the API + client. +- The server **MUST** return `Content-Length` on the HEAD response; without it + the download is aborted with `"Failed to get content length"`. +- Streamed responses (chunked transfer-encoding) are fine for the GET. + +--- + +## 10. Plugin signature service + +For installations that ship signed plugins. + +### `POST /lic/web/api/plugin-sign` + +- **Source:** [src/plugin/callback_msg.rs:282-296](../src/plugin/callback_msg.rs#L282-L296) +- **Auth:** none +- **Headers:** `Content-Type: application/json` (set automatically by `.json()`) +- **Timeout:** 10 s + +**Request body** (`PluginSignReq` at +[src/plugin/callback_msg.rs:82-87](../src/plugin/callback_msg.rs#L82-L87)): + +```json +{ + "plugin_id": "string", + "version": "string", + "msg": [/* byte array; serde will encode Vec as JSON array of u8 */] +} +``` + +**Response** (`PluginSignResp`): + +```json +{ "signed_msg": [/* byte array */] } +``` + +The bytes are passed verbatim into the plugin's +`handle_signature_verification` entry point. + +--- + +## 11. CLI device assignment (`--assign`) + +Triggered from a terminal: `rustdesk --assign --token [...]`. Used by +mass-deploy scripts to register a freshly-installed agent into a tenant. + +### `POST /api/devices/cli` + +- **Source:** [src/core_main.rs:519-616](../src/core_main.rs#L519-L616) +- **Auth:** `Authorization: Bearer ` (passed via `--token`) +- **Headers:** `Content-Type: application/json` + +**Request body** (only `id`/`uuid` are mandatory; at least one of the optional +fields must be present, see CLI help): + +```json +{ + "id": "", + "uuid": "", + "user_name": "...", // optional + "strategy_name": "...", // optional + "address_book_name": "...", // optional + "address_book_tag": "...", // optional + "address_book_alias": "...", // optional + "address_book_password": "...", // optional + "address_book_note": "...", // optional + "device_group_name": "...", // optional + "note": "...", // optional + "device_username": "...", // optional + "device_name": "..." // optional +} +``` + +**Response:** plain text. Empty body ⇒ `Done!` is printed; otherwise the body +is printed verbatim to stdout. No structured error contract. + +--- + +## 12. Endpoint index + +| # | Method | Path | Auth | Notes | +|---|--------|------------------------------------|--------|----------------------------------------------| +| 1 | GET | `/api/login-options` | none | Probe + SSO list | +| 2 | POST | `/api/login` | none | Username/password, 2FA, SSO completion | +| 3 | POST | `/api/currentUser` | Bearer | Refresh profile | +| 4 | POST | `/api/logout` | Bearer | Best-effort | +| 5 | POST | `/api/oidc/auth` | none | Begin device-flow | +| 6 | GET | `/api/oidc/auth-query` | none | Poll device-flow | +| 7 | POST | `/api/ab/settings` | Bearer | `max_peer_one_ab` | +| 8 | POST | `/api/ab/personal` | Bearer | Personal AB GUID; 404 ⇒ legacy mode | +| 9 | POST | `/api/ab/shared/profiles` | Bearer | Paginated AB list | +|10 | POST | `/api/ab/peers?ab=` | Bearer | Paginated peer list | +|11 | POST | `/api/ab/tags/{guid}` | Bearer | All tags | +|12 | POST | `/api/ab/peer/add/{guid}` | Bearer | Add one peer | +|13 | PUT | `/api/ab/peer/update/{guid}` | Bearer | Partial update of one peer | +|14 | DELETE | `/api/ab/peer/{guid}` | Bearer | Bulk delete by ID list | +|15 | POST | `/api/ab/tag/add/{guid}` | Bearer | Add one tag | +|16 | PUT | `/api/ab/tag/rename/{guid}` | Bearer | Rename | +|17 | PUT | `/api/ab/tag/update/{guid}` | Bearer | Set color | +|18 | DELETE | `/api/ab/tag/{guid}` | Bearer | Bulk delete by name list | +|19 | GET | `/api/ab` | Bearer | Legacy AB blob (gzip) | +|20 | POST | `/api/ab` | Bearer | Legacy AB blob save | +|21 | GET | `/api/device-group/accessible` | Bearer | Paginated; errors silently tolerated | +|22 | GET | `/api/users` | Bearer | Paginated, `accessible=&status=1` | +|23 | GET | `/api/peers` | Bearer | Paginated, `accessible=&status=1` | +|24 | POST | `/api/heartbeat` | none | Every 15 s (3 s when active) | +|25 | POST | `/api/sysinfo_ver` | none | Cache probe | +|26 | POST | `/api/sysinfo` | none | Bare-string response | +|27 | POST | `/api/audit/conn` | none | Connection start | +|28 | POST | `/api/audit/file` | none | File transfer summary | +|29 | POST | `/api/audit/alarm` | none | Security alarms | +|30 | PUT | `/api/audit` | Bearer | Update note on a conn audit row | +|31 | POST | `/api/record` | none | `?type=new\|part\|tail\|remove&file=&offset=&length=` | +|32 | POST | `/api/devices/cli` | Bearer | Used by `rustdesk --assign` | +|33 | POST | `/lic/web/api/plugin-sign` | none | Plugin signature | +|34 | HEAD | (configured `api-server`) probe | none | Performed once on startup against `/api/login-options` | +|35 | HEAD+GET | (arbitrary URL) | none | Generic downloader; HEAD must return `Content-Length` | + +--- + +## 13. Minimum viable backend + +To stand up a backend that a stock RustDesk client can use end-to-end, in +priority order: + +1. **Connectivity probe** — answer `HEAD /api/login-options` with `200 OK`. +2. **Auth core** — implement `GET /api/login-options`, `POST /api/login`, + `POST /api/logout`, `POST /api/currentUser`. Return `access_token` on + successful login. +3. **Heartbeat & sysinfo** — `POST /api/heartbeat`, `POST /api/sysinfo`, + `POST /api/sysinfo_ver`. Without these the agent loop logs errors but + continues; with them, the server has live device tracking. +4. **Address book** — pick one mode: + - *Modern*: `POST /api/ab/settings`, `POST /api/ab/personal`, + `POST /api/ab/shared/profiles`, `POST /api/ab/peers`, + `POST /api/ab/tags/{guid}`, plus the per-peer / per-tag mutation set + (§4.6, §4.7). + - *Legacy*: respond `404` to `/api/ab/personal`, then implement + `GET /api/ab` + `POST /api/ab` (§4.8). +5. **Group view** — `GET /api/users`, `GET /api/peers`, + `GET /api/device-group/accessible`. Without these the device-tab is empty. +6. **Audit** — implement the four audit routes (§7) only if you want + server-side logging; the client tolerates 4xx/5xx silently. +7. **Optional**: `/api/record`, `/api/devices/cli`, `/api/oidc/*`, + `/lic/web/api/plugin-sign`. + +A backend that returns `{ "error": "..." }` with a clear message on any +unsupported endpoint will produce reasonable UX in the client.