1039 lines
36 KiB
Markdown
1039 lines
36 KiB
Markdown
# 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 <access_token>
|
||
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": "<message>" }` 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=<page>&pageSize=<size>
|
||
```
|
||
|
||
The client always uses `pageSize=100` and iterates pages until
|
||
`current * pageSize >= total`. Response:
|
||
|
||
```json
|
||
{
|
||
"total": <int>,
|
||
"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": "<device name from client, or browser name+version>"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 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://<rendezvous-host>` (port 21114 if specified inline).
|
||
|
||
The client makes a `HEAD` request against `<base>/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/<provider>",
|
||
"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/<name>` 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/<provider>` — exposed in the UI as an SSO button labelled `<provider>`.
|
||
- `common-oidc/<json-array>` — 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": "<peer id>", "uuid": "<base64 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": "<peer id>", "uuid": "<base64 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": "<provider name>", // matches an entry from /api/login-options
|
||
"id": "<peer id>",
|
||
"uuid": "<base64 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": "<stringified JSON response>"
|
||
}
|
||
```
|
||
|
||
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=<guid>`
|
||
|
||
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=<guid>`
|
||
- **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": "<stringified JSON of {tags,peers,tag_colors}>",
|
||
"licensed_devices": 100
|
||
}
|
||
```
|
||
|
||
`data` decoded:
|
||
|
||
```json
|
||
{
|
||
"tags": ["tag1", ...],
|
||
"peers": [ { "id": "...", "alias": "...", ... }, ... ],
|
||
"tag_colors": "<stringified JSON map: { \"tag1\": -16776961, ... }>"
|
||
}
|
||
```
|
||
|
||
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": "<stringified JSON of {tags,peers,tag_colors}>" }
|
||
```
|
||
|
||
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": "<base64 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": { "<key>": "<value>", ... },
|
||
"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": "<base64 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": "<peer id of this client>",
|
||
"uuid": "<base64 uuid of this client>",
|
||
...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": "<remote peer id>",
|
||
"type": 0, // 0 = RemoteSend, 1 = RemoteReceive
|
||
"path": "C:\\path\\to\\dir",
|
||
"is_file": false,
|
||
"info": "<stringified JSON>"
|
||
}
|
||
```
|
||
|
||
`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": "<stringified JSON, free-form>"
|
||
}
|
||
```
|
||
|
||
`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": "<audit-row guid as returned by /api/audit/conn>",
|
||
"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<u8>` — 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<u8> 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 <bearer> [...]`. 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 <token>` (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": "<this device's peer id>",
|
||
"uuid": "<base64 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=<guid>` | 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.
|