mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
Merge pull request #343 from UNITRONIX/release/3.5.4-hotfix
hotfix: BetterDesk 3.5.4 — Address Book ACL and role CHECK (#342)
This commit is contained in:
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Bump patch version
|
||||
id: bump
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Bump version (minor release or hotfix patch)
|
||||
id: bump
|
||||
|
||||
@@ -52,6 +52,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
- name: Verify product version parity
|
||||
run: node scripts/bump-version.js --verify
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: web-nodejs/package-lock.json
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ web-nodejs/data/
|
||||
# --- Temporary files & caches ---
|
||||
*.tmp
|
||||
*.temp
|
||||
.tmp-*
|
||||
.tmp-dep-test/
|
||||
/tmp/
|
||||
.cache/
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
## Summary
|
||||
|
||||
Production release **BetterDesk 3.4** — merge `dev` → `main` (stable channel).
|
||||
|
||||
Highlights for operators:
|
||||
- **RustDesk client LDAP/AD login (#218, #260)** — desktop app uses same directory auth as the web console
|
||||
- **Pre-3.4 security hardening** — npm ci/audit, govulncheck, WebSocket auth, relay limits, logging redaction
|
||||
- **RustDesk client sessions (#242)** — DB-backed tokens (7-day default, sliding renewal)
|
||||
- **Linux HTTP/HTTPS toggle fixes (#219)**
|
||||
- **Web Remote file transfer (#217)** and related UX improvements
|
||||
- **Update channel** — stable (`main`) vs development (`dev`) in Settings → Updates
|
||||
- **LDAP operator guide** — `docs/wiki/LDAP-AD.md`
|
||||
|
||||
`CHANGELOG.md` `[Unreleased]` section is populated for CI version bump to **3.4.0** on merge.
|
||||
|
||||
## Pre-release checklist
|
||||
|
||||
See [docs/PRE_RELEASE_CHECKLIST.md](docs/PRE_RELEASE_CHECKLIST.md).
|
||||
|
||||
- [ ] Go build/test/vet pass
|
||||
- [ ] `web-nodejs` npm test + i18n check
|
||||
- [ ] CHANGELOG `[Unreleased]` reviewed
|
||||
- [ ] After merge: verify tag `v3.4.0`, GitHub Release, sync `main` → `dev`
|
||||
- [ ] Reply on #260 when stable is live
|
||||
|
||||
## Test plan
|
||||
|
||||
- [ ] Settings → Updates on stable channel pulls from `main` after release
|
||||
- [ ] LDAP: web console + RustDesk client login with AD credentials (#218)
|
||||
- [ ] LDAP Test connection in Settings → Authentication → LDAP
|
||||
- [ ] RustDesk client session persists beyond 24h (#242)
|
||||
- [ ] Panel login, critical pages, update install path smoke test
|
||||
|
||||
Fixes #260
|
||||
@@ -1,8 +1,41 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
---
|
||||
|
||||
## [3.5.4] — 2026-08-01
|
||||
|
||||
### Fixed
|
||||
- **Address Book ACL bypass (restricted users) (#342):** `GET /api/ab` (and personal AB / tags) now filters peers and fleet tags through the same device-group / folder ACL as `/api/peers/list`, so org shared address-book merge and stale entries no longer expose out-of-scope machines. Unscoped `GET /api/peers` (without `accessible`/`pageSize`) applies the same ACL for non-admin roles. Ships via panel update (Go server + console).
|
||||
- **Legacy SQLite role CHECK blocking Phase 52 sync (#342):** upgraded `users` tables that still had `CHECK (role IN ('admin','operator','viewer'))` are rebuilt on Go `Migrate()` so `super_admin` / `global_admin` / `server_admin` / `pro` sync correctly. Installer/docs creators no longer add the old CHECK. Ships via panel update (Go server restart/migrate).
|
||||
- **MeshAgent `.msh` `bad size` (#336):** `GET /api/mesh/download.msh` no longer embeds the static 40-hex MeshID placeholder. Panel/API now emit a stable per-group 96-hex (SHA-384) `MeshID` (optional `mesh_id` query still accepted when 64/96 hex). Ships via panel update (Go restart). Verify: download `.msh` → `MeshID=` is `0x` + 96 hex chars; MeshAgent no longer exits with `bad size`.
|
||||
|
||||
---
|
||||
|
||||
## [3.5.3] — 2026-08-01
|
||||
|
||||
### Fixed
|
||||
- **Dashboard Copy deploy string with invalid/placeholder public key (#340):** client config / deploy string / QR now require a valid Ed25519 key (base64 → 32 bytes), reject placeholders, and fall back to the live Go `GET /api/server-key` when `id_ed25519.pub` is missing or bad. Windows installer also sets `PUB_KEY_PATH` in the console NSSM environment. Ships via panel update (re-run `betterdesk.ps1` service setup to refresh NSSM env on Windows).
|
||||
|
||||
### Changed
|
||||
- **Runtime EOL refresh:** Docker/CI/installers use **Node.js 24 LTS** (`engines` ≥22); Go build images **golang:1.26-alpine**; server runtime **alpine:3.22**. Patch bumps for console (`axios`, `nodemailer`, `ws`, `pg`) and Go modules (incl. `modernc.org/sqlite`). SNMP bridge depends on **`pysnmp` ≥7.1** (replaces `pysnmplib`). See `docs/development/DEPENDENCY_UPGRADE_BACKLOG.md` for deferred Express 5 / native majors.
|
||||
|
||||
### Docs
|
||||
- Refreshed `docs/architecture/PROJECT_STRUCTURE.md` for the current Go + Node layout; dependency upgrade backlog added.
|
||||
|
||||
---
|
||||
|
||||
## [3.5.2] — 2026-07-31
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
---
|
||||
|
||||
## [3.5.1] — 2026-07-31
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
@@ -2508,3 +2541,8 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
[3.4.14]: https://github.com/UNITRONIX/BetterDesk/compare/v3.4.13...v3.4.14
|
||||
[3.4.15]: https://github.com/UNITRONIX/BetterDesk/compare/v3.4.14...v3.4.15
|
||||
[3.5.0]: https://github.com/UNITRONIX/BetterDesk/compare/v3.4.15...v3.5.0
|
||||
[3.5.1]: https://github.com/UNITRONIX/BetterDesk/compare/v3.5.0...v3.5.1
|
||||
[3.5.2]: https://github.com/UNITRONIX/BetterDesk/compare/v3.5.1...v3.5.2
|
||||
[3.5.3]: https://github.com/UNITRONIX/BetterDesk/compare/v3.5.2...v3.5.3
|
||||
[3.5.4]: https://github.com/UNITRONIX/BetterDesk/compare/v3.5.3...v3.5.4
|
||||
[3.5.5]: https://github.com/UNITRONIX/BetterDesk/compare/v3.5.4...v3.5.5
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@
|
||||
# 5000 - Web Console (Node.js admin panel)
|
||||
|
||||
# ============= Stage 1: Build Go server =============
|
||||
FROM golang:1.25-alpine AS go-builder
|
||||
FROM golang:1.26-alpine AS go-builder
|
||||
|
||||
# Retry apk in case of transient DNS failures (common on AlmaLinux/CentOS Docker)
|
||||
RUN apk add --no-cache git || { sleep 2 && apk add --no-cache git; }
|
||||
@@ -36,7 +36,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
-o /betterdesk-server .
|
||||
|
||||
# ============= Stage 2: Build Node.js console =============
|
||||
FROM node:20-alpine AS node-builder
|
||||
FROM node:24-alpine AS node-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -51,11 +51,11 @@ RUN npm ci --omit=dev
|
||||
# Note: supervisord requires root to manage child processes with user= directive.
|
||||
# Both betterdesk-server and betterdesk-console run as non-root 'betterdesk' user
|
||||
# via supervisord configuration (user=betterdesk).
|
||||
FROM node:20-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
LABEL maintainer="UNITRONIX"
|
||||
LABEL description="BetterDesk — All-in-One (Go Server + Node.js Console)"
|
||||
LABEL version="3.5.0"
|
||||
LABEL version="3.5.4"
|
||||
|
||||
# Install runtime packages (retry for transient DNS failures)
|
||||
RUN apk add --no-cache \
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
# Stage 2: Production image with only runtime deps
|
||||
|
||||
# ---- Build stage ----
|
||||
FROM node:20-alpine AS build
|
||||
FROM node:24-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -21,11 +21,11 @@ COPY web-nodejs/package.json web-nodejs/package-lock.json* ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# ---- Production stage ----
|
||||
FROM node:20-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
LABEL maintainer="UNITRONIX"
|
||||
LABEL description="BetterDesk Console - Web Management Panel"
|
||||
LABEL version="3.5.0"
|
||||
LABEL version="3.5.4"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@
|
||||
# Build: docker build -f Dockerfile.server -t betterdesk-server:local .
|
||||
|
||||
# ---- Build stage ----
|
||||
FROM golang:1.25-alpine AS builder
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git || { sleep 2 && apk add --no-cache git; }
|
||||
|
||||
@@ -30,11 +30,11 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
-o /betterdesk-server .
|
||||
|
||||
# ---- Runtime stage ----
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.22
|
||||
|
||||
LABEL maintainer="UNITRONIX"
|
||||
LABEL description="BetterDesk Server - RustDesk-compatible signal + relay"
|
||||
LABEL version="3.5.0"
|
||||
LABEL version="3.5.4"
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -1469,7 +1469,7 @@ The Go server sends a UDP broadcast magic packet (6× `0xFF` + 16× MAC address)
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| **Runtime** | Node.js 18+ |
|
||||
| **Runtime** | Node.js 22+ |
|
||||
| **Framework** | Express.js |
|
||||
| **Templates** | EJS |
|
||||
| **Database** | better-sqlite3 |
|
||||
@@ -1580,7 +1580,7 @@ CDAP Device (Agent/Bridge) ──WebSocket──► Go Server (:21122/cdap)
|
||||
| SDK | Language | Status |
|
||||
|-----|----------|--------|
|
||||
| [Python SDK](sdks/python/) | Python 3.9+ | ✅ Stable |
|
||||
| [Node.js SDK](sdks/nodejs/) | Node.js 18+ | ✅ Stable |
|
||||
| [Node.js SDK](sdks/nodejs/) | Node.js 22+ | ✅ Stable |
|
||||
|
||||
| Bridge | Protocol | Status |
|
||||
|--------|----------|--------|
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
#===============================================================================
|
||||
#
|
||||
# BetterDesk Console Manager v3.5.0
|
||||
# BetterDesk Console Manager v3.5.4
|
||||
# All-in-One Interactive Tool for Docker
|
||||
#
|
||||
# Features:
|
||||
@@ -28,7 +28,7 @@
|
||||
set -e
|
||||
|
||||
# Version
|
||||
VERSION="3.5.0"
|
||||
VERSION="3.5.4"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Default paths (can be overridden by environment variables)
|
||||
@@ -1784,13 +1784,13 @@ repair_named_volume_permissions() {
|
||||
fi
|
||||
|
||||
print_info "Repairing Docker volume permissions: $volume"
|
||||
docker run --rm -v "$volume:/target" alpine:3.20 sh -c '
|
||||
docker run --rm -v "$volume:/target" alpine:3.22 sh -c '
|
||||
set -e
|
||||
mkdir -p /target
|
||||
chown -R 10001:10001 /target
|
||||
chmod -R u+rwX,g+rwX /target
|
||||
' >/dev/null 2>&1 || {
|
||||
print_warning "Could not repair volume $volume (Docker may need to pull alpine:3.20)"
|
||||
print_warning "Could not repair volume $volume (Docker may need to pull alpine:3.22)"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.5.0
|
||||
3.5.4
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/unitronix/betterdesk-server/auth"
|
||||
"github.com/unitronix/betterdesk-server/db"
|
||||
)
|
||||
|
||||
@@ -256,3 +257,50 @@ func (s *Server) mergeOrgAddressBooksIntoAB(r *http.Request, data string) string
|
||||
}
|
||||
return mergeAddressBookJSON(data, overlays...)
|
||||
}
|
||||
|
||||
// filterAddressBookPeersByVisibleSet strips known server peers outside the caller's
|
||||
// device-group ACL. Peers not present in knownPeers (user-typed remote IDs) are kept.
|
||||
// When visible is nil, no filtering is applied (unrestricted / admin).
|
||||
func filterAddressBookPeersByVisibleSet(data string, visible map[string]bool, knownPeers map[string]*db.Peer) string {
|
||||
if visible == nil {
|
||||
return data
|
||||
}
|
||||
ab := parseAddressBookMap(data)
|
||||
peers := toPeerSlice(ab["peers"])
|
||||
filtered := make([]map[string]any, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
id, _ := p["id"].(string)
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, known := knownPeers[id]; known && !visible[id] {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
ab["peers"] = peersToAny(filtered)
|
||||
out, err := json.Marshal(ab)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// applyDeviceScopeToAddressBook filters Address Book peers with the same ACL as
|
||||
// /api/peers/list (device groups, folders, peer grants, Restricted default).
|
||||
func (s *Server) applyDeviceScopeToAddressBook(r *http.Request, username, role, data string) string {
|
||||
if auth.IsProRole(role) {
|
||||
return data
|
||||
}
|
||||
if auth.IsSuperAdminRole(role) || role == auth.RoleGlobalAdmin || role == auth.RoleServerAdmin {
|
||||
return data
|
||||
}
|
||||
user := s.rustDeskUserForGroups(r, username, role)
|
||||
if user == nil {
|
||||
return data
|
||||
}
|
||||
peerByID, _ := s.loadRustDeskPeerByID(username, role)
|
||||
visible := s.rustDeskVisiblePeerSet(user, role, peerByID)
|
||||
return filterAddressBookPeersByVisibleSet(data, visible, peerByID)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/unitronix/betterdesk-server/db"
|
||||
)
|
||||
|
||||
func TestMergeAddressBookJSON(t *testing.T) {
|
||||
@@ -67,3 +69,32 @@ func TestOrgSharedAddressBookEnabledFromValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAddressBookPeersByVisibleSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := `{"peers":[{"id":"A","alias":"Allowed"},{"id":"B","alias":"Denied"},{"id":"REMOTE","alias":"Typed"}],"tags":["X"]}`
|
||||
known := map[string]*db.Peer{
|
||||
"A": {ID: "A"},
|
||||
"B": {ID: "B"},
|
||||
}
|
||||
visible := map[string]bool{"A": true}
|
||||
|
||||
got := filterAddressBookPeersByVisibleSet(data, visible, known)
|
||||
ab := parseAddressBookMap(got)
|
||||
peers := toPeerSlice(ab["peers"])
|
||||
if len(peers) != 2 {
|
||||
t.Fatalf("peer count = %d, want 2 (A + REMOTE); data=%s", len(peers), got)
|
||||
}
|
||||
ids := map[string]bool{}
|
||||
for _, p := range peers {
|
||||
ids[p["id"].(string)] = true
|
||||
}
|
||||
if !ids["A"] || !ids["REMOTE"] || ids["B"] {
|
||||
t.Fatalf("unexpected peers after filter: %v", ids)
|
||||
}
|
||||
|
||||
if unchanged := filterAddressBookPeersByVisibleSet(data, nil, known); unchanged != data {
|
||||
t.Fatalf("nil visible should leave data unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@ func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
body.Role = auth.RoleViewer
|
||||
}
|
||||
if !auth.ValidRole(body.Role) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid role (admin, operator, viewer)"})
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid role"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -400,6 +400,8 @@ func (s *Server) handleClientAddressBook(w http.ResponseWriter, r *http.Request)
|
||||
// RustDesk legacy AB reads tags from GET /api/ab (not /api/ab/tags).
|
||||
data = s.syncServerTagsIntoAddressBook(data, r, username, role)
|
||||
}
|
||||
// Enforce device-group / folder ACL on Address Book peers (org merge + stale entries).
|
||||
data = s.applyDeviceScopeToAddressBook(r, username, role, data)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"data": data, "licensed_devices": 0})
|
||||
|
||||
case http.MethodPost:
|
||||
@@ -453,6 +455,7 @@ func (s *Server) handleClientAddressBookPersonal(w http.ResponseWriter, r *http.
|
||||
if !auth.IsProRole(role) {
|
||||
data = s.mergeAdminTagsIntoAB(data)
|
||||
}
|
||||
data = s.applyDeviceScopeToAddressBook(r, username, role, data)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"data": data})
|
||||
|
||||
case http.MethodPost:
|
||||
|
||||
@@ -370,6 +370,120 @@ func TestHandleClientAddressBookMergesOrgSharedBook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleClientAddressBookScopesOrgPeersByACL(t *testing.T) {
|
||||
database := testSetupDB(t)
|
||||
defer database.Close()
|
||||
|
||||
now := time.Now().UTC()
|
||||
for _, id := range []string{"ALLOW1", "DENY1"} {
|
||||
if err := database.UpsertPeer(&db.Peer{
|
||||
ID: id, UUID: "uuid-" + id, Hostname: id, Status: "ONLINE", LastOnline: now,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.CreateOrganization(&db.Organization{
|
||||
ID: "org-acl", Name: "ACL Org", Slug: "acl-org", CreatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user := &db.User{Username: "scoped", PasswordHash: "hash", Role: "operator"}
|
||||
if err := database.CreateUser(user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := database.LinkUserToOrg("org-acl", user.ID, "operator"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
orgAB := `{"peers":[{"id":"ALLOW1","alias":"Ok"},{"id":"DENY1","alias":"Hidden"}],"tags":["Org"]}`
|
||||
if err := database.SaveOrgAddressBook("org-acl", "legacy", orgAB, "admin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.SaveAddressBook("scoped", "legacy", `{"peers":[],"tags":[]}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
panel := &mockPanelACLStore{
|
||||
userIDs: map[string]int64{"scoped": 42},
|
||||
restrictedDefault: true,
|
||||
groups: []db.PanelDeviceGroup{{
|
||||
ID: 1, GUID: "dg-a", Name: "Devices-A",
|
||||
AllowedUsers: []string{"scoped"},
|
||||
}},
|
||||
members: map[int64][]string{1: {"ALLOW1"}},
|
||||
}
|
||||
|
||||
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
||||
srv.SetPanelStore(panel)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ab", nil)
|
||||
ctx := context.WithValue(req.Context(), ctxKeyUsername, "scoped")
|
||||
ctx = context.WithValue(ctx, ctxKeyRole, "operator")
|
||||
ctx = context.WithValue(ctx, ctxKeyUser, user)
|
||||
req = req.WithContext(ctx)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
srv.handleClientAddressBook(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := resp["data"].(string)
|
||||
if !bytes.Contains([]byte(data), []byte("ALLOW1")) {
|
||||
t.Fatalf("expected allowed peer in AB, got %s", data)
|
||||
}
|
||||
if bytes.Contains([]byte(data), []byte("DENY1")) {
|
||||
t.Fatalf("denied peer must not appear in scoped AB, got %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
// mockPanelACLStore implements db.PanelSyncStore for Address Book ACL tests.
|
||||
type mockPanelACLStore struct {
|
||||
userIDs map[string]int64
|
||||
restrictedDefault bool
|
||||
groups []db.PanelDeviceGroup
|
||||
members map[int64][]string
|
||||
peerGrants map[int64][]string
|
||||
userGroups map[int64][]string
|
||||
}
|
||||
|
||||
func (m *mockPanelACLStore) GetUserIDByUsername(username string) (int64, error) {
|
||||
if id, ok := m.userIDs[username]; ok {
|
||||
return id, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListPanelDeviceGroups() ([]db.PanelDeviceGroup, error) {
|
||||
return m.groups, nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListDeviceGroupMemberPeerIDs(deviceGroupID int64) ([]string, error) {
|
||||
return m.members[deviceGroupID], nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListDeviceGroupGUIDsForPeer(peerID string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListUserGroupGUIDsForUser(userID int64) ([]string, error) {
|
||||
return m.userGroups[userID], nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListUserPeerGrants(userID int64) ([]string, error) {
|
||||
return m.peerGrants[userID], nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListFolders() ([]db.PanelFolder, error) { return nil, nil }
|
||||
func (m *mockPanelACLStore) ListFolderAssignments() (map[string]int64, error) {
|
||||
return map[string]int64{}, nil
|
||||
}
|
||||
func (m *mockPanelACLStore) ListPeerSysinfo() (map[string]db.ConsolePeerSysinfo, error) {
|
||||
return map[string]db.ConsolePeerSysinfo{}, nil
|
||||
}
|
||||
func (m *mockPanelACLStore) FolderGroupAccess(folderID int64) ([]string, []string, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (m *mockPanelACLStore) DeviceScopeDefaultRestricted() bool { return m.restrictedDefault }
|
||||
|
||||
func TestDecodeClientAbPostBodyInvalidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/ab", bytes.NewReader([]byte("{not-json")))
|
||||
|
||||
@@ -545,17 +545,21 @@ func (s *Server) collectRustDeskTags(r *http.Request, username string, role stri
|
||||
seen[strings.ToLower(strings.TrimSpace(tag))] = true
|
||||
}
|
||||
|
||||
if peers, err := s.db.ListPeers(false); err == nil {
|
||||
for _, p := range peers {
|
||||
if p == nil || p.Banned || p.SoftDeleted {
|
||||
continue
|
||||
}
|
||||
for _, tag := range splitPeerTags(p.Tags) {
|
||||
low := strings.ToLower(tag)
|
||||
if !seen[low] {
|
||||
ab.Tags = append(ab.Tags, tag)
|
||||
seen[low] = true
|
||||
}
|
||||
peerByID, _ := s.loadRustDeskPeerByID(username, role)
|
||||
user := s.rustDeskUserForGroups(r, username, role)
|
||||
visible := s.rustDeskVisiblePeerSet(user, role, peerByID)
|
||||
for id, p := range peerByID {
|
||||
if p == nil || p.Banned || p.SoftDeleted {
|
||||
continue
|
||||
}
|
||||
if visible != nil && !visible[id] {
|
||||
continue
|
||||
}
|
||||
for _, tag := range splitPeerTags(p.Tags) {
|
||||
low := strings.ToLower(tag)
|
||||
if !seen[low] {
|
||||
ab.Tags = append(ab.Tags, tag)
|
||||
seen[low] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,10 +787,23 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Data scoping: org-scoped users only see their org's devices
|
||||
orgID := getOrgIDFromCtx(r)
|
||||
username := getUsernameFromCtx(r)
|
||||
role := getRoleFromCtx(r)
|
||||
needsDeviceACL := username != "" && role != "" &&
|
||||
!auth.IsSuperAdminRole(role) && role != auth.RoleGlobalAdmin && role != auth.RoleServerAdmin
|
||||
|
||||
var peers []*db.Peer
|
||||
var total int
|
||||
var err error
|
||||
if paginated {
|
||||
// When device-group ACL applies, load the full candidate set first so
|
||||
// pagination cannot leak peers on later pages from an unscoped DB query.
|
||||
if needsDeviceACL {
|
||||
if orgID != "" {
|
||||
peers, err = s.db.ListPeersForOrg(orgID, includeDeleted)
|
||||
} else {
|
||||
peers, err = s.db.ListPeers(includeDeleted)
|
||||
}
|
||||
} else if paginated {
|
||||
if orgID != "" {
|
||||
peers, total, err = s.db.ListPeersForOrgPaginated(orgID, includeDeleted, limit, offset)
|
||||
} else {
|
||||
@@ -806,6 +819,41 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if needsDeviceACL {
|
||||
peerByID := make(map[string]*db.Peer, len(peers))
|
||||
for _, p := range peers {
|
||||
if p != nil {
|
||||
peerByID[p.ID] = p
|
||||
}
|
||||
}
|
||||
user := s.rustDeskUserForGroups(r, username, role)
|
||||
if visible := s.rustDeskVisiblePeerSet(user, role, peerByID); visible != nil {
|
||||
filtered := make([]*db.Peer, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
if p != nil && visible[p.ID] {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
peers = filtered
|
||||
}
|
||||
total = len(peers)
|
||||
if paginated {
|
||||
start := offset
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if start > len(peers) {
|
||||
peers = nil
|
||||
} else {
|
||||
end := start + limit
|
||||
if end > len(peers) {
|
||||
end = len(peers)
|
||||
}
|
||||
peers = peers[start:end]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich with live online status and status tier from memory map.
|
||||
// Issue #138 hardening: override db.Peer.Status (string) with an int
|
||||
// so any RustDesk client that reaches this handler without the
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -559,6 +560,10 @@ func (s *SQLiteDB) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.migrateUsersDropLegacyRoleCheck(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.migrateBillingOrgContracts(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -566,6 +571,93 @@ func (s *SQLiteDB) Migrate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// usersHasLegacyRoleCheck detects upgraded SQLite installs that still enforce
|
||||
// CHECK (role IN ('admin', 'operator', 'viewer')) from the pre-Phase-52 schema.
|
||||
func (s *SQLiteDB) usersHasLegacyRoleCheck() bool {
|
||||
var sqlText string
|
||||
err := s.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='users'`).Scan(&sqlText)
|
||||
if err != nil || strings.TrimSpace(sqlText) == "" {
|
||||
return false
|
||||
}
|
||||
low := strings.ToLower(sqlText)
|
||||
return strings.Contains(low, "check") &&
|
||||
strings.Contains(low, "role") &&
|
||||
strings.Contains(low, "'admin'") &&
|
||||
strings.Contains(low, "'viewer'")
|
||||
}
|
||||
|
||||
// migrateUsersDropLegacyRoleCheck rebuilds the users table without the legacy
|
||||
// three-role CHECK so Phase 52 roles (super_admin, global_admin, …) can sync.
|
||||
func (s *SQLiteDB) migrateUsersDropLegacyRoleCheck() error {
|
||||
if !s.usersHasLegacyRoleCheck() {
|
||||
return nil
|
||||
}
|
||||
log.Printf("[db] migrating users table: dropping legacy role CHECK constraint")
|
||||
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("db: begin users role-check migration: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if _, err := tx.Exec(`PRAGMA foreign_keys=OFF`); err != nil {
|
||||
return fmt.Errorf("db: disable foreign_keys for users rebuild: %w", err)
|
||||
}
|
||||
|
||||
const createUsersNew = `CREATE TABLE users_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'viewer',
|
||||
auth_provider TEXT NOT NULL DEFAULT 'local',
|
||||
totp_secret TEXT DEFAULT '',
|
||||
totp_enabled INTEGER DEFAULT 0,
|
||||
totp_recovery_codes TEXT DEFAULT NULL,
|
||||
is_server_admin INTEGER DEFAULT 0,
|
||||
guid TEXT DEFAULT '',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
last_login TEXT DEFAULT ''
|
||||
)`
|
||||
if _, err := tx.Exec(createUsersNew); err != nil {
|
||||
return fmt.Errorf("db: create users_new: %w", err)
|
||||
}
|
||||
|
||||
copySQL := `INSERT INTO users_new (
|
||||
id, username, password_hash, role, auth_provider, totp_secret, totp_enabled,
|
||||
totp_recovery_codes, is_server_admin, guid, created_at, last_login
|
||||
)
|
||||
SELECT
|
||||
id, username, password_hash, role,
|
||||
COALESCE(auth_provider, 'local'),
|
||||
COALESCE(totp_secret, ''),
|
||||
COALESCE(totp_enabled, 0),
|
||||
totp_recovery_codes,
|
||||
COALESCE(is_server_admin, 0),
|
||||
COALESCE(guid, ''),
|
||||
COALESCE(created_at, datetime('now')),
|
||||
COALESCE(last_login, '')
|
||||
FROM users`
|
||||
if _, err := tx.Exec(copySQL); err != nil {
|
||||
return fmt.Errorf("db: copy users → users_new: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DROP TABLE users`); err != nil {
|
||||
return fmt.Errorf("db: drop legacy users: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`ALTER TABLE users_new RENAME TO users`); err != nil {
|
||||
return fmt.Errorf("db: rename users_new: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username)`); err != nil {
|
||||
return fmt.Errorf("db: recreate users username index: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`PRAGMA foreign_keys=ON`); err != nil {
|
||||
return fmt.Errorf("db: re-enable foreign_keys: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("db: commit users role-check migration: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateBillingOrgContracts copies legacy billing_org_contracts rows into billing_contracts.
|
||||
func (s *SQLiteDB) migrateBillingOrgContracts() error {
|
||||
var name string
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -908,3 +909,58 @@ func TestMigrateBackfillsNullUserTimestamps(t *testing.T) {
|
||||
t.Error("totp_secret still NULL after Migrate backfill")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateUsersDropLegacyRoleCheck(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "legacy-role-check.db")
|
||||
raw, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatalf("open raw: %v", err)
|
||||
}
|
||||
_, err = raw.Exec(`
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'viewer',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
last_login TEXT DEFAULT '',
|
||||
CHECK (role IN ('admin', 'operator', 'viewer'))
|
||||
);
|
||||
INSERT INTO users (username, password_hash, role) VALUES ('op', 'hash', 'operator');
|
||||
`)
|
||||
if err != nil {
|
||||
raw.Close()
|
||||
t.Fatalf("seed legacy users: %v", err)
|
||||
}
|
||||
raw.Close()
|
||||
|
||||
db, err := OpenSQLite(path)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenSQLite: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err := db.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
if db.usersHasLegacyRoleCheck() {
|
||||
t.Fatal("legacy role CHECK still present after Migrate")
|
||||
}
|
||||
|
||||
// Phase 52 roles must insert successfully after migration.
|
||||
if err := db.CreateUser(&User{Username: "ga", PasswordHash: "hash", Role: "global_admin"}); err != nil {
|
||||
t.Fatalf("CreateUser global_admin after migration: %v", err)
|
||||
}
|
||||
got, err := db.GetUser("ga")
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("GetUser global_admin: user=%v err=%v", got, err)
|
||||
}
|
||||
if got.Role != "global_admin" {
|
||||
t.Fatalf("role = %q, want global_admin", got.Role)
|
||||
}
|
||||
|
||||
// Idempotent second migrate
|
||||
if err := db.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate second: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -5,29 +5,29 @@ go 1.25.0
|
||||
toolchain go1.26.5
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/go-ldap/ldap/v3 v3.4.14
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
golang.org/x/crypto v0.52.0
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
modernc.org/sqlite v1.48.1
|
||||
modernc.org/sqlite v1.55.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
modernc.org/libc v1.74.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
+30
-30
@@ -2,17 +2,17 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
@@ -27,8 +27,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -56,18 +56,18 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
@@ -76,30 +76,30 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA=
|
||||
modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.5.0
|
||||
3.5.4
|
||||
|
||||
@@ -1016,12 +1016,17 @@ func TestClientTokenAuthorizesViewerOnlyPunch(t *testing.T) {
|
||||
srv, database := newTestSignalServer(t, config.EnrollmentModeOpen)
|
||||
putOnlinePeer(srv, "TGTOK1", "203.0.113.95", 52000, peer.ConnTCP)
|
||||
|
||||
user := &db.User{Username: "tokuser", PasswordHash: "hash", Role: "admin"}
|
||||
if err := database.CreateUser(user); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
|
||||
token := strings.Repeat("ab", 32) // 64 hex chars
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
hash := hex.EncodeToString(sum[:])
|
||||
if err := database.CreateClientSession(&db.ClientSession{
|
||||
TokenHash: hash,
|
||||
UserID: 1,
|
||||
UserID: user.ID,
|
||||
ClientID: "TOKINIT1",
|
||||
ClientUUID: "tok-uuid",
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Format("2006-01-02 15:04:05"),
|
||||
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
#Requires -RunAsAdministrator
|
||||
<#
|
||||
.SYNOPSIS
|
||||
BetterDesk Console Manager v3.5.0 - All-in-One Interactive Tool for Windows
|
||||
BetterDesk Console Manager v3.5.4 - All-in-One Interactive Tool for Windows
|
||||
|
||||
.DESCRIPTION
|
||||
Features:
|
||||
@@ -102,7 +102,7 @@ param(
|
||||
# Configuration
|
||||
#===============================================================================
|
||||
|
||||
$script:VERSION = "3.5.0"
|
||||
$script:VERSION = "3.5.4"
|
||||
$script:ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
# Auto mode flags
|
||||
@@ -1191,15 +1191,15 @@ function Install-NodeJs {
|
||||
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
|
||||
if ($nodeCmd) {
|
||||
$nodeVersion = (node --version) -replace 'v', '' -split '\.' | Select-Object -First 1
|
||||
if ([int]$nodeVersion -ge 18) {
|
||||
if ([int]$nodeVersion -ge 22) {
|
||||
Print-Success "Node.js v$(node --version) already installed"
|
||||
return $true
|
||||
} else {
|
||||
Print-Warning "Node.js version $nodeVersion is too old (need 18+). Upgrading..."
|
||||
Print-Warning "Node.js version $nodeVersion is too old (need 22+). Upgrading..."
|
||||
}
|
||||
}
|
||||
|
||||
Print-Step "Installing Node.js 20 LTS..."
|
||||
Print-Step "Installing Node.js 24 LTS..."
|
||||
|
||||
# Try winget first (Windows 10/11)
|
||||
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
|
||||
@@ -1233,7 +1233,7 @@ function Install-NodeJs {
|
||||
|
||||
# Manual download as last resort
|
||||
Print-Warning "Automatic installation not available."
|
||||
Print-Info "Please install Node.js 20 LTS manually from: https://nodejs.org/"
|
||||
Print-Info "Please install Node.js 24 LTS manually from: https://nodejs.org/"
|
||||
Print-Info "After installation, restart the script."
|
||||
return $false
|
||||
}
|
||||
@@ -2089,6 +2089,7 @@ function Setup-Services {
|
||||
"KEYS_PATH=$script:RUSTDESK_PATH",
|
||||
"DATA_DIR=$script:CONSOLE_PATH\data",
|
||||
"DB_PATH=$script:RUSTDESK_PATH\db_v2.sqlite3",
|
||||
"PUB_KEY_PATH=$script:RUSTDESK_PATH\id_ed25519.pub",
|
||||
"API_KEY_PATH=$script:RUSTDESK_PATH\.api_key",
|
||||
"HBBS_API_URL=${apiScheme}://localhost:$($script:API_PORT)/api",
|
||||
"BETTERDESK_API_URL=${apiScheme}://localhost:$($script:API_PORT)/api",
|
||||
@@ -2331,8 +2332,7 @@ cursor.execute('''
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
CHECK (role IN ('admin', 'operator', 'viewer'))
|
||||
is_active INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
''')
|
||||
|
||||
|
||||
+9
-9
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
#===============================================================================
|
||||
#
|
||||
# BetterDesk Console Manager v3.5.0
|
||||
# BetterDesk Console Manager v3.5.4
|
||||
# All-in-One Interactive Tool for Linux
|
||||
#
|
||||
# Features:
|
||||
@@ -36,7 +36,7 @@
|
||||
set -e
|
||||
|
||||
# Version
|
||||
VERSION="3.5.0"
|
||||
VERSION="3.5.4"
|
||||
# Bump when installer control-flow changes must apply mid-session after Update (#219).
|
||||
BETTERDESK_SH_REVISION="20260725-console-start-306"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -2985,15 +2985,15 @@ install_nodejs() {
|
||||
# Check if Node.js is already installed and version is sufficient
|
||||
if command -v node &> /dev/null; then
|
||||
local node_version=$(node --version | sed 's/v//' | cut -d'.' -f1)
|
||||
if [ "$node_version" -ge 18 ]; then
|
||||
if [ "$node_version" -ge 22 ]; then
|
||||
print_success "Node.js v$(node --version) already installed"
|
||||
return 0
|
||||
else
|
||||
print_warning "Node.js version $node_version is too old (need 18+). Upgrading..."
|
||||
print_warning "Node.js version $node_version is too old (need 22+). Upgrading..."
|
||||
fi
|
||||
fi
|
||||
|
||||
print_step "Installing Node.js 20 LTS..."
|
||||
print_step "Installing Node.js 24 LTS..."
|
||||
|
||||
# Detect OS and install Node.js. The NodeSource setup script is downloaded
|
||||
# to a temp file and validated before execution (H5 audit fix): we do NOT
|
||||
@@ -3051,15 +3051,15 @@ install_nodejs() {
|
||||
# Detect OS and install Node.js
|
||||
if command -v apt-get &> /dev/null; then
|
||||
# Debian/Ubuntu - use NodeSource
|
||||
_fetch_and_run_nodesource "https://deb.nodesource.com/setup_20.x" || return 1
|
||||
_fetch_and_run_nodesource "https://deb.nodesource.com/setup_24.x" || return 1
|
||||
apt-get install -y -qq nodejs
|
||||
elif command -v dnf &> /dev/null; then
|
||||
# Fedora/RHEL 8+
|
||||
_fetch_and_run_nodesource "https://rpm.nodesource.com/setup_20.x" || return 1
|
||||
_fetch_and_run_nodesource "https://rpm.nodesource.com/setup_24.x" || return 1
|
||||
dnf install -y -q nodejs
|
||||
elif command -v yum &> /dev/null; then
|
||||
# RHEL/CentOS 7
|
||||
_fetch_and_run_nodesource "https://rpm.nodesource.com/setup_20.x" || return 1
|
||||
_fetch_and_run_nodesource "https://rpm.nodesource.com/setup_24.x" || return 1
|
||||
yum install -y -q nodejs
|
||||
elif command -v pacman &> /dev/null; then
|
||||
# Arch Linux
|
||||
@@ -3068,7 +3068,7 @@ install_nodejs() {
|
||||
# Alpine Linux
|
||||
apk add --no-cache nodejs npm
|
||||
else
|
||||
print_error "Cannot install Node.js automatically. Please install Node.js 18+ manually."
|
||||
print_error "Cannot install Node.js automatically. Please install Node.js 22+ manually."
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Periodically polls SNMP OIDs and pushes values to BetterDesk via CDAP.
|
||||
Supports counter-rate computation, timetick formatting, and byte formatting.
|
||||
|
||||
Usage:
|
||||
pip install betterdesk-cdap pysnmplib
|
||||
pip install betterdesk-cdap pysnmp
|
||||
python bridge_snmp.py --config config.json
|
||||
"""
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
pysnmplib>=5.0.0
|
||||
pysnmp>=7.1.0
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
services:
|
||||
server:
|
||||
image: ghcr.io/unitronix/betterdesk-server:${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
image: ghcr.io/unitronix/betterdesk-server:${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
container_name: betterdesk-server
|
||||
hostname: betterdesk-server
|
||||
command: ["/usr/local/bin/betterdesk-server", "-mode", "all", "-api-port", "21114", "-key-file", "/opt/rustdesk/id_ed25519"]
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
console:
|
||||
image: ghcr.io/unitronix/betterdesk-console:${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
image: ghcr.io/unitronix/betterdesk-console:${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
container_name: betterdesk-console
|
||||
# Shares server network stack — panel and RustDesk ports use MACVLAN_IPV4.
|
||||
network_mode: service:server
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
- DOCKER=true
|
||||
- BETTERDESK_UPDATE_MODE=image
|
||||
- BETTERDESK_DOCKER_LAYOUT=split
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
- TZ=${TZ:-UTC}
|
||||
depends_on:
|
||||
# service_started (not healthy): avoids deadlock with auth.db on first boot (#138, #186).
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
services:
|
||||
betterdesk:
|
||||
image: ghcr.io/unitronix/betterdesk:${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
image: ghcr.io/unitronix/betterdesk:${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
container_name: betterdesk
|
||||
hostname: betterdesk
|
||||
volumes:
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
- DOCKER=true
|
||||
- BETTERDESK_UPDATE_MODE=image
|
||||
- BETTERDESK_DOCKER_LAYOUT=single
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
- AUTH_DB_PATH=/app/data/auth.db
|
||||
- INIT_ADMIN_USER=${ADMIN_USERNAME:-admin}
|
||||
- INIT_ADMIN_PASS=${ADMIN_PASSWORD:-}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
services:
|
||||
betterdesk:
|
||||
image: ghcr.io/unitronix/betterdesk:${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
image: ghcr.io/unitronix/betterdesk:${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
container_name: betterdesk
|
||||
hostname: betterdesk
|
||||
ports:
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
- DOCKER=true
|
||||
- BETTERDESK_UPDATE_MODE=image
|
||||
- BETTERDESK_DOCKER_LAYOUT=single
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
- DB_TYPE=${DB_TYPE:-sqlite}
|
||||
- DATABASE_URL=${DATABASE_URL:-}
|
||||
- DB_URL=${DATABASE_URL:-}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
services:
|
||||
# BetterDesk Server (Go) — handles signal, relay, and API
|
||||
server:
|
||||
image: ghcr.io/unitronix/betterdesk-server:${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
image: ghcr.io/unitronix/betterdesk-server:${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
container_name: betterdesk-server
|
||||
hostname: betterdesk-server
|
||||
command: ["/usr/local/bin/betterdesk-server", "-mode", "all", "-api-port", "21114", "-key-file", "/opt/rustdesk/id_ed25519"]
|
||||
@@ -93,7 +93,7 @@ services:
|
||||
|
||||
# BetterDesk Console — Web Management Interface
|
||||
console:
|
||||
image: ghcr.io/unitronix/betterdesk-console:${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
image: ghcr.io/unitronix/betterdesk-console:${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
container_name: betterdesk-console
|
||||
hostname: betterdesk-console
|
||||
ports:
|
||||
@@ -130,7 +130,7 @@ services:
|
||||
- DOCKER=true
|
||||
- BETTERDESK_UPDATE_MODE=image
|
||||
- BETTERDESK_DOCKER_LAYOUT=split
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.0}
|
||||
- BETTERDESK_IMAGE_TAG=${BETTERDESK_IMAGE_TAG:-3.5.4}
|
||||
networks:
|
||||
- betterdesk-net
|
||||
depends_on:
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e
|
||||
|
||||
echo "========================================"
|
||||
echo " BetterDesk Console - Container Startup"
|
||||
echo " Version: ${BETTERDESK_IMAGE_VERSION:-3.5.0} (Node.js)"
|
||||
echo " Version: ${BETTERDESK_IMAGE_VERSION:-3.5.4} (Node.js)"
|
||||
echo "========================================"
|
||||
|
||||
# Public Docker examples use ADMIN_*; the Node.js console seeds from DEFAULT_ADMIN_*.
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
|
||||
echo "========================================"
|
||||
echo " BetterDesk All-in-One Container"
|
||||
echo " Version: ${BETTERDESK_IMAGE_VERSION:-3.5.0}"
|
||||
echo " Version: ${BETTERDESK_IMAGE_VERSION:-3.5.4}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Components:"
|
||||
|
||||
@@ -1,223 +1,74 @@
|
||||
# BetterDesk Console - Project Structure
|
||||
# BetterDesk — Project Structure
|
||||
|
||||
This document describes the organization of the BetterDesk Console project.
|
||||
Current layout of the BetterDesk monorepo (Go server + Node.js console). Legacy Flask / `hbbs-patch` trees were removed in the 2.x → 3.x era.
|
||||
|
||||
## 📁 Directory Structure
|
||||
## Top-level layout
|
||||
|
||||
```
|
||||
BetterDeskConsole/
|
||||
│
|
||||
├── 📄 README.md # Main project documentation
|
||||
├── 📄 LICENSE # AGPL-3.0 License
|
||||
├── 📄 VERSION # Current version (1.2.0-v8)
|
||||
├── 📄 .gitignore # Git ignore rules
|
||||
├── 📄 CHANGELOG.md # Version history
|
||||
│
|
||||
├── 🔧 install.sh # Main installation script (uses precompiled binaries)
|
||||
├── 🔧 update.sh # Update script for existing installations
|
||||
├── 🔧 restore_hbbs.sh # Restore original HBBS (rollback script)
|
||||
│
|
||||
├── 📁 web/ # Web Console Application
|
||||
│ ├── app.py # Flask backend with ban management
|
||||
│ ├── app_demo.py # Demo version (no database)
|
||||
│ ├── requirements.txt # Python dependencies
|
||||
│ ├── betterdesk.service # Systemd service file
|
||||
│ ├── templates/ # HTML templates
|
||||
│ │ └── index.html # Main dashboard
|
||||
│ └── static/ # Static assets
|
||||
│ ├── style.css # Glassmorphism stylesheet
|
||||
│ ├── script.js # JavaScript frontend
|
||||
│ └── MATERIAL_ICONS.md # Material Icons attribution
|
||||
│
|
||||
├── 📁 hbbs-patch/ # HBBS Server Modifications
|
||||
│ ├── README.md # Patch documentation overview
|
||||
│ ├── QUICKSTART.md # Quick setup guide
|
||||
│ ├── BAN_ENFORCEMENT.md # Ban enforcement technical docs (v8)
|
||||
│ ├── BAN_CHECK_PATCH.md # Legacy patch documentation
|
||||
│ ├── SECURITY_AUDIT.md # Security audit report
|
||||
│ │
|
||||
│ ├── 📁 bin/ # Precompiled Binaries (NEW in v8)
|
||||
│ │ ├── hbbs-v8 # Signal server with bidirectional bans
|
||||
│ │ ├── hbbr-v8 # Relay server with bidirectional bans
|
||||
│ │ └── README.md # Binary documentation
|
||||
│ │
|
||||
│ ├── 📁 src/ # Source code patches (reference)
|
||||
│ │ ├── database.rs # Ban check functions
|
||||
│ │ ├── http_api.rs # REST API endpoints
|
||||
│ │ ├── main.rs # Main entry point
|
||||
│ │ ├── peer.rs # Peer management with ban checks
|
||||
│ │ ├── rendezvous_server.rs # Punch hole with dual ban check
|
||||
│ │ └── relay_server.rs # Relay with dual ban check (not included in v8)
|
||||
│ │
|
||||
│ ├── build.sh # Automated build script (for rebuilding)
|
||||
│ ├── deploy-v8.sh # Deployment script for v8
|
||||
│ ├── deploy-v6.ps1 # Windows deployment (legacy)
|
||||
│ ├── deploy.ps1 # Windows deployment (legacy)
|
||||
│ └── test_ban_enforcement.ps1 # Ban enforcement test script
|
||||
│
|
||||
│ ├── build.sh # Automated build script
|
||||
│ ├── install.sh # Installation script
|
||||
│ ├── database_patch.rs # Database code snippet
|
||||
│ ├── peer_patch.rs # Peer registration code snippet
|
||||
│ └── src/ # Full source code patches
|
||||
│ ├── database.rs # Modified database module
|
||||
│ ├── peer.rs # Modified peer module
|
||||
│ └── http_api.rs # HTTP API module
|
||||
│
|
||||
├── 📁 migrations/ # Database Migrations
|
||||
│ ├── v1.0.1_soft_delete.py # Soft delete system
|
||||
│ └── v1.1.0_device_bans.py # Device banning columns
|
||||
│
|
||||
├── 📁 screenshots/ # Project Screenshots
|
||||
│ ├── README.md # Screenshot descriptions
|
||||
│ └── *.png # UI screenshots
|
||||
│
|
||||
├── 📁 docs/ # 📚 Documentation Hub
|
||||
│ ├── README.md # Documentation index
|
||||
│ ├── CHANGELOG.md # Version history
|
||||
│ ├── RELEASE_NOTES_v1.2.0.md # Latest release details
|
||||
│ ├── CONTRIBUTING.md # Contribution guidelines
|
||||
│ ├── DEPRECATION_NOTICE.md # Deprecated features info
|
||||
│ ├── DEVELOPMENT_ROADMAP.md # Future plans
|
||||
│ ├── UPDATE_GUIDE.md # How to update
|
||||
│ ├── UPDATE_REFERENCE.md # Detailed update procedures
|
||||
│ ├── QUICKSTART_UPDATE.md # Quick update instructions
|
||||
│ └── GITHUB_RELEASE_CHECKLIST.md # Release process checklist
|
||||
│
|
||||
├── 📁 dev_modules/ # 🛠️ Development Tools
|
||||
│ ├── README.md # Developer tools documentation
|
||||
│ ├── check_database.py # Database inspection tool
|
||||
│ ├── test_ban_api.sh # API testing script
|
||||
│ └── update.ps1 # PowerShell update script (Windows)
|
||||
│
|
||||
└── 📁 deprecated/ # ⚠️ Obsolete Components
|
||||
├── README.md # Deprecation information
|
||||
├── ban_enforcer.py # Old Python ban daemon (v1.1.0)
|
||||
├── install_ban_enforcer.sh # Old installation script
|
||||
├── rustdesk-ban-enforcer.service # Old systemd service
|
||||
├── BAN_ENFORCER.md # Old documentation
|
||||
└── BAN_ENFORCER_TEST.md # Old testing guide
|
||||
BetterDesk/
|
||||
├── betterdesk-server/ # Go signal + relay + HTTP API (single binary)
|
||||
├── web-nodejs/ # Express admin console (EJS + vanilla JS)
|
||||
├── rdclient-desktop/ # Tauri v2 operator desktop shell (RdClient)
|
||||
├── sdks/
|
||||
│ ├── nodejs/ # CDAP Node.js SDK (betterdesk-cdap)
|
||||
│ └── python/ # CDAP Python SDK
|
||||
├── bridges/ # Reference CDAP bridges (modbus, snmp, rest-webhook)
|
||||
├── docker/ # Entrypoints, supervisord, helpers
|
||||
├── docs/ # Architecture, setup, security, wiki
|
||||
├── scripts/ # Version bump, toolchain, wiki sync
|
||||
├── contrib/ # Community contrib (e.g. FreeBSD rc.d)
|
||||
├── .github/ # CI, Dependabot, CodeQL
|
||||
├── Dockerfile # All-in-one image (Go server + Node console)
|
||||
├── Dockerfile.server # Go server image
|
||||
├── Dockerfile.console # Node console image
|
||||
├── betterdesk.sh / .ps1 # Native install / update
|
||||
├── betterdesk-docker.sh # Docker-oriented installer
|
||||
├── CHANGELOG.md
|
||||
└── VERSION
|
||||
```
|
||||
|
||||
## 📂 Folder Purposes
|
||||
**Out of scope / unsupported for day-to-day work:** `betterdesk-agent/`, `betterdesk-agent-client/`, `betterdesk-support-agent/` (no active support).
|
||||
|
||||
### Core Directories
|
||||
## Core components
|
||||
|
||||
#### `web/`
|
||||
Flask-based web management console with:
|
||||
- Device listing and management
|
||||
- Real-time status monitoring
|
||||
- Ban/unban interface
|
||||
- RESTful HTTP API
|
||||
### `betterdesk-server/`
|
||||
Clean-room Go implementation replacing RustDesk `hbbs`+`hbbr`: UDP/TCP/WS signal, relay, REST API, JWT/RBAC, SQLite/PostgreSQL, CDAP gateway, MeshCentral compat. Pure Go (no CGO). Module: `go.mod` (toolchain pinned).
|
||||
|
||||
#### `hbbs-patch/`
|
||||
Modified RustDesk HBBS server with:
|
||||
- Native ban enforcement
|
||||
- HTTP status API
|
||||
- Automated build scripts
|
||||
- Complete documentation
|
||||
### `web-nodejs/`
|
||||
Node.js management panel: devices, users, policies, updates, remote viewer, i18n (26 locales). Talks to the Go API. Runtime: Node.js **22+** (Docker/CI/installers target **24 LTS**).
|
||||
|
||||
#### `migrations/`
|
||||
Database schema evolution scripts:
|
||||
- Soft delete system (v1.0.1)
|
||||
- Device banning columns (v1.1.0)
|
||||
- Future migrations go here
|
||||
### `rdclient-desktop/`
|
||||
Tauri 2 desktop client that hosts the panel remote UI. Vendored `wry` patch + documented glib/`RUSTSEC` ignore until GTK stack migration.
|
||||
|
||||
### Documentation
|
||||
### `sdks/` + `bridges/`
|
||||
CDAP client libraries and sample industrial/IoT bridges. SNMP bridge uses official **`pysnmp` 7.x** (not the legacy `pysnmplib` fork).
|
||||
|
||||
#### `docs/`
|
||||
**Comprehensive project documentation**:
|
||||
- Release notes and changelogs
|
||||
- Update and contribution guides
|
||||
- Roadmap and future plans
|
||||
- GitHub release procedures
|
||||
### Docker / install
|
||||
- Images: `node:24-alpine`, `golang:1.26-alpine`, server runtime `alpine:3.22+`
|
||||
- Compose files at repo root (`docker-compose*.yml`)
|
||||
- Updates: panel Settings → Updates (`updateService.js`) or `betterdesk.sh` / `betterdesk.ps1`
|
||||
|
||||
Keep this folder for:
|
||||
- Understanding project history
|
||||
- Planning updates
|
||||
- Contributing to project
|
||||
- Creating new releases
|
||||
## Documentation hub (`docs/`)
|
||||
|
||||
### Development
|
||||
| Area | Path |
|
||||
|------|------|
|
||||
| Branching / versioning | `docs/important/branching-and-versioning.md` |
|
||||
| Update flow | `docs/important/betterdesk-update-flow.md` |
|
||||
| Docker | `docs/docker/` |
|
||||
| Security audits | `docs/security/` |
|
||||
| Wiki mirror | `docs/wiki/` |
|
||||
| Dependency upgrade backlog | `docs/development/DEPENDENCY_UPGRADE_BACKLOG.md` |
|
||||
|
||||
#### `dev_modules/`
|
||||
**Tools for developers and testing**:
|
||||
- Database inspection utilities
|
||||
- API testing scripts
|
||||
- Development-specific scripts
|
||||
## Historical note
|
||||
|
||||
Use this folder when:
|
||||
- Testing new features
|
||||
- Debugging issues
|
||||
- Validating database state
|
||||
- Developing contributions
|
||||
Older docs and installers may still mention Flask consoles, `hbbs-patch/`, or a `deprecated/ban_enforcer.py` tree. Those components are **not** in this repository anymore. Use Go server + `web-nodejs` only.
|
||||
|
||||
#### `deprecated/`
|
||||
**Obsolete components (DO NOT USE)**:
|
||||
- Ban Enforcer Python daemon (replaced in v1.2.0)
|
||||
- Related installation scripts
|
||||
- Old documentation
|
||||
## For contributors
|
||||
|
||||
Kept for:
|
||||
- Historical reference
|
||||
- Emergency rollback
|
||||
- Understanding system evolution
|
||||
|
||||
⚠️ **Do not use deprecated components in new installations!**
|
||||
|
||||
## 🎯 For New Users
|
||||
|
||||
Start with these files in order:
|
||||
|
||||
1. **[README.md](README.md)** - Project overview and features
|
||||
2. **[install.sh](install.sh)** - Install web console
|
||||
3. **[hbbs-patch/QUICKSTART.md](hbbs-patch/QUICKSTART.md)** - Install HBBS patch
|
||||
4. **[docs/CHANGELOG.md](../development/CHANGELOG.md)** - Version history
|
||||
|
||||
## 🔄 For Existing Users
|
||||
|
||||
When updating:
|
||||
|
||||
1. **[docs/UPDATE_GUIDE.md](../setup/UPDATE_GUIDE.md)** - General update process
|
||||
2. **[update.sh](update.sh)** - Run automated update
|
||||
3. **[docs/CHANGELOG.md](../development/CHANGELOG.md)** - See what changed
|
||||
|
||||
## 🤝 For Contributors
|
||||
|
||||
Before contributing:
|
||||
|
||||
1. **[docs/CONTRIBUTING.md](../development/CONTRIBUTING.md)** - Contribution guidelines
|
||||
2. **[docs/DEVELOPMENT_ROADMAP.md](../enterprise/ENTERPRISE_ROADMAP.md)** - Planned features
|
||||
3. **[dev_modules/](dev_modules/)** - Development tools
|
||||
|
||||
## 📋 File Naming Conventions
|
||||
|
||||
- **UPPERCASE.md** - Important documentation files
|
||||
- **lowercase.sh** - Shell scripts (Linux/macOS)
|
||||
- **lowercase.ps1** - PowerShell scripts (Windows)
|
||||
- **lowercase.py** - Python scripts
|
||||
- **lowercase.rs** - Rust source files
|
||||
|
||||
## 🚫 What NOT to Commit
|
||||
|
||||
See [.gitignore](.gitignore) for full list:
|
||||
- `__pycache__/` - Python bytecode
|
||||
- `target/` - Rust build artifacts
|
||||
- `*.sqlite3` - Database files
|
||||
- `*.log` - Log files
|
||||
- `*.key`, `*.pem` - Private keys
|
||||
- `.env` - Environment secrets
|
||||
|
||||
## 📦 Clean Repository
|
||||
|
||||
This structure ensures:
|
||||
- ✅ Clear separation of concerns
|
||||
- ✅ Easy navigation for new users
|
||||
- ✅ Organized documentation
|
||||
- ✅ Developer-friendly tooling
|
||||
- ✅ Historical preservation
|
||||
- ✅ Professional appearance
|
||||
1. Work on **`dev`** by default (see branching docs).
|
||||
2. Run `web-nodejs` → `npm test`; `betterdesk-server` → `go test ./...` (+ `govulncheck`).
|
||||
3. Keep locale keys in sync across all `web-nodejs/lang/*.json` files.
|
||||
|
||||
---
|
||||
|
||||
Last updated: v1.2.0 (January 5, 2026)
|
||||
Last updated: 2026-07-31 (runtime EOL + dependency audit)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Dependency upgrade backlog (non-agent)
|
||||
|
||||
Tracked after the July 2026 dependency audit. **Do not** land these majors in the same PR as runtime EOL or patch bumps — one major (or closely related stack) per PR, with full panel/server tests.
|
||||
|
||||
Agents (`betterdesk-agent*`) are out of scope until support resumes.
|
||||
|
||||
## Deferred majors — `web-nodejs`
|
||||
|
||||
| Package | Current | Target | Notes |
|
||||
|---------|---------|--------|-------|
|
||||
| express | 4.x | 5.x | Breaking routing / `res.send` signatures; migrate with [Express 5 guide](https://expressjs.com/en/guide/migrating-5/). Likely pulls `path-to-regexp` override rewrite. |
|
||||
| helmet | 7.x | 8.x | Prefer with Express 5 |
|
||||
| express-rate-limit | 7.x | 8.x | Prefer with Express 5 |
|
||||
| csrf-csrf | 3.x | 4.x | Auth/CSRF regression suite |
|
||||
| bcrypt | 5.x | 6.x | Native rebuild; sheds deprecated `node-gyp` transitive tree |
|
||||
| better-sqlite3 | 11.x | 13.x | Native + Alpine musl rebuild |
|
||||
| ejs | 3.x | 6.x | Template audit |
|
||||
| otplib | 12.x | 13.x | **npm-deprecated** at 12; 2FA/TOTP tests required |
|
||||
| protobufjs | 7.x | 8.x | RdClient / proto paths |
|
||||
| jest | 29.x | 30.x | Dev/CI only |
|
||||
|
||||
## Deferred / blocked — `rdclient-desktop`
|
||||
|
||||
| Item | Notes |
|
||||
|------|-------|
|
||||
| **RUSTSEC-2024-0429 (glib)** | Ignored in CI/Dependabot until Tauri/GTK stack can move to glib 0.20+. Do not force-upgrade transitive GTK3 crates. |
|
||||
| Vendored `wry` + git `brotli` patches | Leave until WebView regression testing is scheduled. |
|
||||
|
||||
## Done in audit follow-up (2026-07)
|
||||
|
||||
- Node **24** (Docker, CI, installers); `engines` **>=22**
|
||||
- Alpine server runtime **3.22**; Go build image **1.26**
|
||||
- Patch bumps: axios, nodemailer, ws, pg; Go direct deps including `modernc.org/sqlite`
|
||||
- SNMP bridge: `pysnmplib` → **`pysnmp` >=7.1**
|
||||
- Removed orphan `betterdesk-mgmt/package-lock.json`
|
||||
|
||||
## Related deferred security (not package majors)
|
||||
|
||||
See `docs/security/LOGIN_API_SECURITY_AUDIT_2026-04-26.md`: RustDesk access-token hashing migration; CSP hardening (`unsafe-inline` / `unsafe-eval`).
|
||||
@@ -304,8 +304,7 @@ cursor.execute('''
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
|
||||
created_at DATETIME NOT NULL,
|
||||
last_login DATETIME,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
CHECK (role IN ('admin', 'operator', 'viewer'))
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1
|
||||
)
|
||||
''')
|
||||
|
||||
|
||||
@@ -69,12 +69,16 @@ class SNMPBridge(CDAPBridge):
|
||||
group=oid_cfg.get("group", "SNMP")))
|
||||
|
||||
async def collect_metrics(self):
|
||||
from pysnmplib.hlapi.asyncio import get_cmd, SnmpEngine, CommunityData, UdpTransportTarget, ObjectType, ObjectIdentity
|
||||
from pysnmp.hlapi.v3arch.asyncio import (
|
||||
get_cmd, SnmpEngine, CommunityData, UdpTransportTarget,
|
||||
ContextData, ObjectType, ObjectIdentity,
|
||||
)
|
||||
values = {}
|
||||
for oid_cfg in self.oids:
|
||||
target = await UdpTransportTarget.create((self.target_ip, 161))
|
||||
err_ind, err_status, _, var_binds = await get_cmd(
|
||||
SnmpEngine(), CommunityData(self.community),
|
||||
UdpTransportTarget((self.target_ip, 161)),
|
||||
target, ContextData(),
|
||||
ObjectType(ObjectIdentity(oid_cfg["oid"]))
|
||||
)
|
||||
if not err_ind and not err_status:
|
||||
|
||||
@@ -60,7 +60,7 @@ cd src-tauri
|
||||
cargo tauri build
|
||||
```
|
||||
|
||||
**Requirements:** Rust 1.70+, Node.js 18+, WebView2 (Windows), webkit2gtk (Linux)
|
||||
**Requirements:** Rust 1.70+, Node.js 22+, WebView2 (Windows), webkit2gtk (Linux)
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ BetterDesk supports **Linux (bare-metal)**, **Windows (PowerShell)**, and **Dock
|
||||
- 1 CPU core, 512 MB RAM minimum (2 cores, 2 GB recommended)
|
||||
- Root access (sudo)
|
||||
- Open ports: 21114-21119 TCP, 21116 UDP, 5000 TCP (web console)
|
||||
- Node.js 18+ (auto-installed by script)
|
||||
- Node.js 22+ (auto-installed by script; installer targets Node.js 24 LTS)
|
||||
|
||||
### Windows
|
||||
- Windows 10/11 or Windows Server 2019+
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ Source: [sdks/python/](https://github.com/UNITRONIX/BetterDesk/tree/main/sdks/py
|
||||
|
||||
## Node.js SDK
|
||||
|
||||
**Package:** `betterdesk-cdap` · **Requires:** Node.js 18+
|
||||
**Package:** `betterdesk-cdap` · **Requires:** Node.js 22+
|
||||
|
||||
```bash
|
||||
npm install betterdesk-cdap
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
# --docker | --native Installation mode (default: docker)
|
||||
# --split Legacy two-container layout (server + console images)
|
||||
# --install-dir PATH Install directory (default: /opt/betterdesk)
|
||||
# --version TAG Docker image tag / release baseline (default: 3.5.0)
|
||||
# --version TAG Docker image tag / release baseline (default: 3.5.4)
|
||||
# --branch BRANCH Git branch for native install (default: main)
|
||||
# --relay-mode auto|local|public Relay auto-detection strategy
|
||||
# --relay-servers IP[:port] Fixed relay address (overrides --relay-mode)
|
||||
@@ -39,7 +39,7 @@ set -euo pipefail
|
||||
VERSION="1.0.0"
|
||||
BETTERDESK_REPO="${BETTERDESK_REPO:-UNITRONIX/BetterDesk}"
|
||||
BETTERDESK_BRANCH="${BETTERDESK_BRANCH:-main}"
|
||||
BETTERDESK_VERSION="${BETTERDESK_VERSION:-3.5.0}"
|
||||
BETTERDESK_VERSION="${BETTERDESK_VERSION:-3.5.4}"
|
||||
BETTERDESK_RAW_BASE="${BETTERDESK_RAW_BASE:-https://raw.githubusercontent.com/${BETTERDESK_REPO}/${BETTERDESK_BRANCH}}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/betterdesk}"
|
||||
INSTALL_MODE="docker"
|
||||
|
||||
Generated
+63
-48
@@ -8,13 +8,13 @@
|
||||
"name": "rdclient-desktop",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.6.2"
|
||||
"@tauri-apps/cli": "^2.11.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
|
||||
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz",
|
||||
"integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -28,23 +28,23 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.4",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.4",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.4",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.4",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.4",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.4",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.4",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.4",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.4",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
|
||||
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz",
|
||||
"integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -59,9 +59,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
|
||||
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz",
|
||||
"integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -76,9 +76,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
|
||||
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz",
|
||||
"integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -93,13 +93,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz",
|
||||
"integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -110,13 +113,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz",
|
||||
"integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -127,13 +133,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz",
|
||||
"integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -144,13 +153,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz",
|
||||
"integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -161,13 +173,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz",
|
||||
"integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -178,9 +193,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz",
|
||||
"integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -195,9 +210,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz",
|
||||
"integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -212,9 +227,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz",
|
||||
"integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"build": "tauri build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.6.2"
|
||||
"@tauri-apps/cli": "^2.11.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ install_apt() {
|
||||
# package conflicts with it). Install only if neither node nor npm exist.
|
||||
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
|
||||
log "Installing Node.js LTS via NodeSource"
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
|
||||
apt-get install -y nodejs
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+37
-36
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "betterdesk-console",
|
||||
"version": "3.4.12",
|
||||
"version": "3.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "betterdesk-console",
|
||||
"version": "3.4.12",
|
||||
"version": "3.5.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"axios": "^1.19.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -20,24 +20,25 @@
|
||||
"express-session": "^1.18.1",
|
||||
"helmet": "^7.2.0",
|
||||
"multer": "^2.2.0",
|
||||
"nodemailer": "^9.0.1",
|
||||
"nodemailer": "^9.0.3",
|
||||
"otplib": "^12.0.1",
|
||||
"pg": "^8.22.0",
|
||||
"protobufjs": "^7.6.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"ws": "^8.21.0"
|
||||
"ws": "^8.21.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^7.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty": "^1.0.0",
|
||||
"pg": "^8.13.0"
|
||||
"pg": "^8.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1449,13 +1450,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
|
||||
"integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"form-data": "^4.0.6",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
@@ -4423,9 +4424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
|
||||
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
|
||||
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -4682,15 +4683,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
|
||||
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.12.0",
|
||||
"pg-pool": "^3.13.0",
|
||||
"pg-protocol": "^1.13.0",
|
||||
"pg-connection-string": "^2.14.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
"pg-protocol": "^1.15.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
@@ -4698,7 +4699,7 @@
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.3.0"
|
||||
"pg-cloudflare": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
@@ -4710,16 +4711,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
|
||||
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
|
||||
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
|
||||
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@@ -4734,9 +4735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
|
||||
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peerDependencies": {
|
||||
@@ -4744,9 +4745,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
|
||||
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
|
||||
"integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@@ -6187,9 +6188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "betterdesk-console",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.4",
|
||||
"description": "BetterDesk Console - Professional Web Management Panel for BetterDesk Server",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"author": "UNITRONIX",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"axios": "^1.19.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -37,17 +37,17 @@
|
||||
"express-session": "^1.18.1",
|
||||
"helmet": "^7.2.0",
|
||||
"multer": "^2.2.0",
|
||||
"nodemailer": "^9.0.1",
|
||||
"nodemailer": "^9.0.3",
|
||||
"otplib": "^12.0.1",
|
||||
"protobufjs": "^7.6.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"ws": "^8.21.0"
|
||||
"ws": "^8.21.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty": "^1.0.0",
|
||||
"pg": "^8.13.0"
|
||||
"pg": "^8.22.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.6",
|
||||
@@ -64,6 +64,6 @@
|
||||
"supertest": "^7.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ router.get('/api/stats', requireAuth, async (req, res) => {
|
||||
// Get server health
|
||||
const hbbsHealth = await serverBackend.getHealth();
|
||||
|
||||
// Get public key info
|
||||
const publicKey = keyService.getPublicKey();
|
||||
|
||||
// Get public key info (file or live Go key)
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -148,7 +148,7 @@ router.get('/api/dashboard/client-config', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const queryHost = typeof req.query.host === 'string' ? req.query.host : '';
|
||||
const endpoints = clientConfigHost.resolveRustDeskEndpoints(req, queryHost);
|
||||
const clientConfig = keyService.getClientConfig(endpoints);
|
||||
const clientConfig = await keyService.getClientConfig(endpoints);
|
||||
const qr = await keyService.getServerConfigQR(endpoints);
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -112,8 +112,13 @@ function finalizeBundleBrandingSync(input) {
|
||||
* installation registers on its own and receives a unique device_token
|
||||
* after operator approval (managed enrollment).
|
||||
*/
|
||||
function finalizeBundleBranding(input) {
|
||||
async function finalizeBundleBranding(input) {
|
||||
const branding = finalizeBundleBrandingSync(input);
|
||||
const pubKey = (await keyService.resolvePublicKey()) || '';
|
||||
if (branding.server) {
|
||||
branding.server.public_key = pubKey;
|
||||
}
|
||||
branding.server_key = pubKey;
|
||||
// Strip legacy shared tokens from older bundles on save/rebuild.
|
||||
delete branding.enrollment_token;
|
||||
delete branding.has_enrollment_token;
|
||||
@@ -189,14 +194,14 @@ router.get('/api/generator/bundles/:bundleId', requireAuth, requireAdmin, async
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/generator/defaults', requireAuth, requireAdmin, (req, res) => {
|
||||
router.get('/api/generator/defaults', requireAuth, requireAdmin, async (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
server_host: conn.defaultServerHost(),
|
||||
use_https: conn.defaultUseHttps(),
|
||||
api_port: conn.defaultApiPort(),
|
||||
public_key: keyService.getPublicKey() || '',
|
||||
public_key: (await keyService.resolvePublicKey()) || '',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -231,7 +236,7 @@ router.post('/api/generator/bundles', requireAuth, requireAdmin, async (req, res
|
||||
}
|
||||
const normalized = productType === 'rdclient'
|
||||
? { ...base, bundle_id: bundleId, server_url: base.panel_url }
|
||||
: finalizeBundleBranding(base);
|
||||
: await finalizeBundleBranding(base);
|
||||
if (productType !== 'rdclient') {
|
||||
normalized.bundle_id = bundleId;
|
||||
normalized.product_name = productType === 'agent-client'
|
||||
@@ -271,7 +276,7 @@ router.put('/api/generator/bundles/:bundleId', requireAuth, requireAdmin, async
|
||||
if (!valid) {
|
||||
return res.status(400).json({ success: false, error: req.t('generator.errors.validation_failed'), errors, details: errors });
|
||||
}
|
||||
const normalized = finalizeBundleBranding(base);
|
||||
const normalized = await finalizeBundleBranding(base);
|
||||
normalized.bundle_id = req.params.bundleId;
|
||||
normalized.product_name = normalizeProductType(existing.product_type) === 'agent-client'
|
||||
? (normalized.company_name ? `${normalized.company_name} Agent` : 'BetterDesk Agent')
|
||||
@@ -511,9 +516,9 @@ router.get('/api/d/:publicId/download/:platform/:arch/:format', async (req, res)
|
||||
// Legacy TOML config generator (deprecated, kept for compatibility)
|
||||
// =========================================================================
|
||||
|
||||
router.get('/api/generator/config', requireAuth, (req, res) => {
|
||||
router.get('/api/generator/config', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const publicKey = keyService.getPublicKey();
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -528,13 +533,13 @@ router.get('/api/generator/config', requireAuth, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/api/generator/generate-config', requireAuth, (req, res) => {
|
||||
router.post('/api/generator/generate-config', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { serverHost, serverPort, relayHost, relayPort, clientName } = req.body;
|
||||
if (!serverHost) {
|
||||
return res.status(400).json({ success: false, error: 'Server host is required' });
|
||||
}
|
||||
const publicKey = keyService.getPublicKey();
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
const lines = [];
|
||||
lines.push(`rendezvous_server = ${serverHost}:${serverPort || 21116}`);
|
||||
if (relayHost) lines.push(`relay_server = ${relayHost}:${relayPort || 21117}`);
|
||||
|
||||
@@ -39,17 +39,17 @@ router.get('/keys', requireAuth, (req, res) => {
|
||||
/**
|
||||
* GET /api/keys/public - Get public key
|
||||
*/
|
||||
router.get('/api/keys/public', requireAuth, (req, res) => {
|
||||
router.get('/api/keys/public', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const publicKey = keyService.getPublicKey();
|
||||
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
|
||||
if (!publicKey) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: req.t('keys.not_found')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -100,17 +100,17 @@ router.get('/api/keys/public/qr', requireAuth, async (req, res) => {
|
||||
/**
|
||||
* GET /api/keys/public/download - Download public key file
|
||||
*/
|
||||
router.get('/api/keys/public/download', requireAuth, (req, res) => {
|
||||
router.get('/api/keys/public/download', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const publicKey = keyService.getPublicKey();
|
||||
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
|
||||
if (!publicKey) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: req.t('keys.not_found')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="id_ed25519.pub"');
|
||||
res.send(publicKey);
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const db = require('../services/database');
|
||||
const config = require('../config/config');
|
||||
const { requirePermission } = require('../middleware/auth');
|
||||
@@ -57,13 +56,12 @@ function getClientIp(req) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the server public key from disk (base64).
|
||||
* Read the server public key (validated file, then live Go key).
|
||||
*/
|
||||
function getServerPublicKey() {
|
||||
async function getServerPublicKey() {
|
||||
try {
|
||||
if (fs.existsSync(config.pubKeyPath)) {
|
||||
return fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
}
|
||||
const keyService = require('../services/keyService');
|
||||
return (await keyService.resolvePublicKey()) || '';
|
||||
} catch (_) { /* ignore */ }
|
||||
return '';
|
||||
}
|
||||
@@ -78,14 +76,14 @@ function generateDeviceAccessToken() {
|
||||
/**
|
||||
* Build the server config payload returned to devices upon approval.
|
||||
*/
|
||||
function buildServerConfig() {
|
||||
async function buildServerConfig() {
|
||||
const protocol = config.httpsEnabled ? 'https' : 'http';
|
||||
const consoleUrl = `${protocol}://0.0.0.0:${config.port}`;
|
||||
|
||||
return {
|
||||
console_url: consoleUrl,
|
||||
server_address: `0.0.0.0:21116`,
|
||||
server_key: getServerPublicKey(),
|
||||
server_key: await getServerPublicKey(),
|
||||
access_token: generateDeviceAccessToken(),
|
||||
};
|
||||
}
|
||||
@@ -283,7 +281,7 @@ router.put('/api/registrations/:id/approve', requirePermission('enrollment.appro
|
||||
}
|
||||
|
||||
// Build server config — use actual server address from the request
|
||||
const serverConfig = buildServerConfig();
|
||||
const serverConfig = await buildServerConfig();
|
||||
|
||||
// Replace 0.0.0.0 with the actual hostname / IP the admin is accessing
|
||||
const actualHost = req.headers.host?.split(':')[0] || req.hostname || 'localhost';
|
||||
|
||||
@@ -78,13 +78,13 @@ function getRemoteRelay() {
|
||||
try { return require('../services/remoteRelay'); } catch { return null; }
|
||||
}
|
||||
|
||||
// Read server public key on each viewer render (Go may write id_ed25519.pub after
|
||||
// console start; avoid caching empty/stale key across the process lifetime).
|
||||
function getServerPubKey() {
|
||||
// Resolve server public key on each viewer render (file may be stale/wrong;
|
||||
// fall back to live Go /api/server-key — issue #340).
|
||||
async function resolveServerPubKey() {
|
||||
try {
|
||||
return keyService.getPublicKey() || '';
|
||||
return (await keyService.resolvePublicKey()) || '';
|
||||
} catch (err) {
|
||||
console.warn('Warning: Could not read server public key:', err.message);
|
||||
console.warn('Warning: Could not resolve server public key:', err.message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ router.get('/remote/:deviceId', rdClientPageLimiter, requireRemoteAccess, async
|
||||
activePage: 'remote',
|
||||
deviceId: deviceId,
|
||||
device: device || { id: deviceId, hostname: '', platform: '', note: '' },
|
||||
serverPubKey: getServerPubKey(),
|
||||
serverPubKey: await resolveServerPubKey(),
|
||||
capabilities,
|
||||
guestToken: req.guestToken || getGuestTokenFromQuery(req) || '',
|
||||
layout: 'viewer'
|
||||
|
||||
@@ -381,9 +381,8 @@ async function getConsoleDeviceContext(user) {
|
||||
|
||||
try {
|
||||
context.devices = await serverBackend.getAllDevices({});
|
||||
if (!canBrowseDeviceInventory(user)) {
|
||||
context.devices = await filterDevicesForRustDeskUser(user, context.devices);
|
||||
}
|
||||
// Always apply device-group / folder ACL (including operators with device.view).
|
||||
context.devices = await filterDevicesForRustDeskUser(user, context.devices);
|
||||
} catch (err) {
|
||||
console.warn('[API:AB] Failed to read panel devices:', err.message);
|
||||
}
|
||||
@@ -403,10 +402,26 @@ async function buildSyncedAddressBook(user, abType) {
|
||||
// Previously this was true for admin/operator users, causing "ghost" entries
|
||||
// that reappear after deletion. The "Available Devices" tab shows all server
|
||||
// devices via /api/peers/list — the AB should only contain user-added entries.
|
||||
return addressBookSync.mergeAddressBookData(abData, {
|
||||
let merged = addressBookSync.mergeAddressBookData(abData, {
|
||||
...context,
|
||||
includeDevices: false
|
||||
});
|
||||
|
||||
// Strip org/stale peers outside device-group ACL (same scope as peer list).
|
||||
try {
|
||||
const allDevices = await serverBackend.getAllDevices({});
|
||||
const scope = await deviceGroupService.getDeviceScopeForUser(db, user, allDevices);
|
||||
if (scope) {
|
||||
merged = addressBookSync.filterAddressBookPeersByScope(merged, {
|
||||
visibleIds: scope,
|
||||
knownDeviceIds: (allDevices || []).map(d => d && d.id)
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[API:AB] Failed to apply device scope to address book for ${user && user.username}:`, err.message);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
async function getSyncedAddressBookTags(user) {
|
||||
@@ -1596,19 +1611,11 @@ function cleanupTfaSessions(sessions) {
|
||||
* This key is used by clients to verify peer identity (signed_id_pk).
|
||||
* Public key is inherently safe to expose — no auth required.
|
||||
*/
|
||||
router.get('/api/server-key', (req, res) => {
|
||||
router.get('/api/server-key', async (req, res) => {
|
||||
try {
|
||||
if (!fs.existsSync(config.pubKeyPath)) {
|
||||
return res.json({ key: '' });
|
||||
}
|
||||
const key = fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
// Validate: should decode to 32 bytes (Ed25519 public key)
|
||||
const decoded = Buffer.from(key, 'base64');
|
||||
if (decoded.length !== 32) {
|
||||
console.warn('[API:SERVER-KEY] Invalid RS public key length:', decoded.length);
|
||||
return res.json({ key: '' });
|
||||
}
|
||||
return res.json({ key });
|
||||
const keyService = require('../services/keyService');
|
||||
const key = await keyService.resolvePublicKey();
|
||||
return res.json({ key: key || '' });
|
||||
} catch (err) {
|
||||
console.warn('[API:SERVER-KEY] Error reading public key:', err.message);
|
||||
return res.json({ key: '' });
|
||||
@@ -1619,12 +1626,13 @@ router.get('/api/server-key', (req, res) => {
|
||||
* GET /api/server-key/fingerprint
|
||||
* Returns SHA-256 fingerprint of RS public key for out-of-band verification.
|
||||
*/
|
||||
router.get('/api/server-key/fingerprint', (req, res) => {
|
||||
router.get('/api/server-key/fingerprint', async (req, res) => {
|
||||
try {
|
||||
if (!fs.existsSync(config.pubKeyPath)) {
|
||||
const keyService = require('../services/keyService');
|
||||
const key = await keyService.resolvePublicKey();
|
||||
if (!key) {
|
||||
return res.json({ fingerprint: '', algorithm: 'SHA-256' });
|
||||
}
|
||||
const key = fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
const hash = crypto.createHash('sha256').update(Buffer.from(key, 'base64')).digest('hex');
|
||||
return res.json({
|
||||
fingerprint: hash.match(/.{2}/g).join(':').toUpperCase(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* BetterDesk Console - Key Service
|
||||
* Reads public key and API key from filesystem
|
||||
* Reads public key and API key from filesystem; resolves live Go key as fallback.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
@@ -8,21 +8,114 @@ const QRCode = require('qrcode');
|
||||
const config = require('../config/config');
|
||||
const conn = require('./agentBundleConnection');
|
||||
|
||||
const ED25519_PUBLIC_KEY_BYTES = 32;
|
||||
const GO_KEY_CACHE_TTL_MS = 30_000;
|
||||
|
||||
/** @type {{ key: string|null, at: number }} */
|
||||
let goKeyCache = { key: null, at: 0 };
|
||||
|
||||
/**
|
||||
* Read public key from file
|
||||
* True when value is a valid RustDesk server public key (base64 → 32 bytes).
|
||||
* Rejects empty values, unresolved env tokens, and obvious placeholders.
|
||||
* @param {unknown} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidRustDeskPublicKey(value) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const key = value.trim();
|
||||
if (!key) return false;
|
||||
if (/__[^_\s]+__/.test(key)) return false;
|
||||
if (/placeholder/i.test(key)) return false;
|
||||
if (/^YOUR[_-]?PUBLIC[_-]?KEY$/i.test(key)) return false;
|
||||
if (/\s/.test(key)) return false;
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(key, 'base64');
|
||||
// Reject non-canonical base64 (padding / alphabet mismatch)
|
||||
if (decoded.length !== ED25519_PUBLIC_KEY_BYTES) return false;
|
||||
const reencoded = decoded.toString('base64');
|
||||
// Allow missing padding on input by comparing without '='
|
||||
if (reencoded.replace(/=+$/, '') !== key.replace(/=+$/, '')) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and validate public key from the configured pubkey file.
|
||||
* Invalid / placeholder content is treated as missing (never returned to clients).
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getPublicKey() {
|
||||
try {
|
||||
if (fs.existsSync(config.pubKeyPath)) {
|
||||
return fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
if (!fs.existsSync(config.pubKeyPath)) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
const raw = fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
if (!raw) return null;
|
||||
if (!isValidRustDeskPublicKey(raw)) {
|
||||
console.warn(
|
||||
`Public key at ${config.pubKeyPath} is not a valid Ed25519 key ` +
|
||||
`(length=${raw.length}); ignoring for client deploy/config.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
} catch (err) {
|
||||
console.warn('Could not read public key:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the live rendezvous public key from the Go server.
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function fetchPublicKeyFromGo() {
|
||||
try {
|
||||
const betterdeskApi = require('./betterdeskApi');
|
||||
const resp = await betterdeskApi.apiClient.get('/server-key', { timeout: 5000 });
|
||||
const key = typeof resp.data?.key === 'string' ? resp.data.key.trim() : '';
|
||||
if (isValidRustDeskPublicKey(key)) {
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch public key from Go /api/server-key:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the server public key: validated file first, then live Go API (cached).
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function resolvePublicKey() {
|
||||
// Prefer module.exports so tests can spy on getPublicKey.
|
||||
const fromFile = module.exports.getPublicKey();
|
||||
if (fromFile) return fromFile;
|
||||
|
||||
const now = Date.now();
|
||||
if (goKeyCache.key && (now - goKeyCache.at) < GO_KEY_CACHE_TTL_MS) {
|
||||
return goKeyCache.key;
|
||||
}
|
||||
|
||||
const fromGo = await fetchPublicKeyFromGo();
|
||||
if (fromGo) {
|
||||
goKeyCache = { key: fromGo, at: now };
|
||||
return fromGo;
|
||||
}
|
||||
|
||||
goKeyCache = { key: null, at: now };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Test helper — clears Go key cache. */
|
||||
function _resetGoKeyCacheForTests() {
|
||||
goKeyCache = { key: null, at: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key (masked for display)
|
||||
*/
|
||||
@@ -65,10 +158,12 @@ function apiUrlForHost(host, useHttps) {
|
||||
/**
|
||||
* RustDesk client config JSON payload: { host, relay, api, key }
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
* @param {{ useHttps?: boolean }} [options]
|
||||
* @param {{ useHttps?: boolean, publicKey?: string }} [options]
|
||||
*/
|
||||
function buildRustDeskConfigPayload(endpointsOrHost, options = {}) {
|
||||
const pubKey = getPublicKey() || '';
|
||||
const pubKey = options.publicKey !== undefined
|
||||
? (isValidRustDeskPublicKey(options.publicKey) ? String(options.publicKey).trim() : '')
|
||||
: (module.exports.getPublicKey() || '');
|
||||
const useHttps = options.useHttps ?? conn.defaultUseHttps();
|
||||
|
||||
if (typeof endpointsOrHost === 'string') {
|
||||
@@ -92,6 +187,18 @@ function buildRustDeskConfigPayload(endpointsOrHost, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Like buildRustDeskConfigPayload but resolves the live public key (file → Go).
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
* @param {{ useHttps?: boolean, publicKey?: string }} [options]
|
||||
*/
|
||||
async function buildRustDeskConfigPayloadAsync(endpointsOrHost, options = {}) {
|
||||
const publicKey = options.publicKey !== undefined
|
||||
? options.publicKey
|
||||
: (await resolvePublicKey()) || '';
|
||||
return buildRustDeskConfigPayload(endpointsOrHost, { ...options, publicKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* QR / deep-link format: rustdesk://config/<standard-base64-json>
|
||||
*/
|
||||
@@ -113,17 +220,17 @@ function encodeRustDeskCliConfigString(payload) {
|
||||
|
||||
/**
|
||||
* Generate QR code containing the RustDesk configuration URI.
|
||||
* Format: rustdesk://config/<base64-encoded-json>
|
||||
* Format: rustdesk://config/<base64-json>
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
*/
|
||||
async function getServerConfigQR(endpointsOrHost) {
|
||||
const pubKey = getPublicKey();
|
||||
const pubKey = await resolvePublicKey();
|
||||
if (!pubKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const configPayload = buildRustDeskConfigPayload(endpointsOrHost);
|
||||
const configPayload = await buildRustDeskConfigPayloadAsync(endpointsOrHost, { publicKey: pubKey });
|
||||
const configUri = encodeRustDeskConfigUri(configPayload);
|
||||
|
||||
const qrDataUrl = await QRCode.toDataURL(configUri, {
|
||||
@@ -145,10 +252,11 @@ async function getServerConfigQR(endpointsOrHost) {
|
||||
|
||||
/**
|
||||
* Build the RustDesk client fields operators need to enter manually.
|
||||
* Uses validated file key, then live Go /api/server-key as fallback (#340).
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
*/
|
||||
function getClientConfig(endpointsOrHost) {
|
||||
const payload = buildRustDeskConfigPayload(endpointsOrHost);
|
||||
async function getClientConfig(endpointsOrHost) {
|
||||
const payload = await buildRustDeskConfigPayloadAsync(endpointsOrHost);
|
||||
const publicKey = payload.key;
|
||||
|
||||
return {
|
||||
@@ -166,7 +274,7 @@ function getClientConfig(endpointsOrHost) {
|
||||
* Generate QR code for public key (legacy — raw key text)
|
||||
*/
|
||||
async function getPublicKeyQR() {
|
||||
const pubKey = getPublicKey();
|
||||
const pubKey = await resolvePublicKey();
|
||||
if (!pubKey) {
|
||||
return null;
|
||||
}
|
||||
@@ -192,9 +300,9 @@ async function getPublicKeyQR() {
|
||||
/**
|
||||
* Get server configuration info
|
||||
*/
|
||||
function getServerConfig() {
|
||||
async function getServerConfig() {
|
||||
return {
|
||||
publicKey: getPublicKey(),
|
||||
publicKey: await resolvePublicKey(),
|
||||
apiKeyMasked: getApiKey(true),
|
||||
hbbsApiUrl: config.hbbsApiUrl,
|
||||
dbPath: config.dbPath,
|
||||
@@ -204,15 +312,19 @@ function getServerConfig() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidRustDeskPublicKey,
|
||||
getPublicKey,
|
||||
resolvePublicKey,
|
||||
getApiKey,
|
||||
getPublicKeyQR,
|
||||
getServerConfigQR,
|
||||
getClientConfig,
|
||||
getServerConfig,
|
||||
buildRustDeskConfigPayload,
|
||||
buildRustDeskConfigPayloadAsync,
|
||||
encodeRustDeskConfigUri,
|
||||
encodeRustDeskCliConfigString,
|
||||
normalizeHostInput,
|
||||
apiUrlForHost,
|
||||
_resetGoKeyCacheForTests,
|
||||
};
|
||||
|
||||
@@ -190,11 +190,38 @@ function collectPeerTagUpdates(data, options = {}) {
|
||||
return updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip known server peers outside the caller's device-group ACL.
|
||||
* Peers not in knownDeviceIds (user-typed remote IDs) are kept.
|
||||
* When visibleIds is null/undefined, no filtering is applied.
|
||||
*/
|
||||
function filterAddressBookPeersByScope(data, options = {}) {
|
||||
const ab = parseAddressBookData(data);
|
||||
const visibleIds = options.visibleIds;
|
||||
if (!visibleIds) {
|
||||
return JSON.stringify(ab);
|
||||
}
|
||||
const known = new Set(
|
||||
(Array.isArray(options.knownDeviceIds) ? options.knownDeviceIds : [])
|
||||
.map(id => String(id || '').trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
ab.peers = ab.peers.filter(peer => {
|
||||
if (!peer || typeof peer !== 'object') return false;
|
||||
const id = String(peer.id || '').trim();
|
||||
if (!id) return false;
|
||||
if (known.has(id) && !visibleIds.has(id)) return false;
|
||||
return true;
|
||||
});
|
||||
return JSON.stringify(ab);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeTags,
|
||||
parseAddressBookData,
|
||||
filterFolderTags,
|
||||
mergeAddressBookData,
|
||||
filterAddressBookPeersByScope,
|
||||
collectVisibleTags,
|
||||
collectPeerTagUpdates
|
||||
};
|
||||
|
||||
@@ -27,7 +27,9 @@ describe('keyService RustDesk config encoding', () => {
|
||||
});
|
||||
|
||||
it('buildRustDeskConfigPayload normalizes host input', () => {
|
||||
const payload = keyService.buildRustDeskConfigPayload('https://desk.example.com:8443/path');
|
||||
const payload = keyService.buildRustDeskConfigPayload('https://desk.example.com:8443/path', {
|
||||
publicKey: '',
|
||||
});
|
||||
expect(payload.host).toBe('desk.example.com');
|
||||
expect(payload.relay).toBe('desk.example.com');
|
||||
});
|
||||
@@ -37,9 +39,84 @@ describe('keyService RustDesk config encoding', () => {
|
||||
host: 'remote.example.com',
|
||||
relay: 'relay.example.com',
|
||||
api: 'https://api.example.com',
|
||||
});
|
||||
}, { publicKey: '' });
|
||||
expect(payload.host).toBe('remote.example.com');
|
||||
expect(payload.relay).toBe('relay.example.com');
|
||||
expect(payload.api).toBe('https://api.example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyService public key validation (#340)', () => {
|
||||
// 32 zero bytes → canonical base64 (44 chars with padding)
|
||||
const validKey = Buffer.alloc(32, 0).toString('base64');
|
||||
|
||||
afterEach(() => {
|
||||
keyService._resetGoKeyCacheForTests();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('isValidRustDeskPublicKey accepts canonical 32-byte base64', () => {
|
||||
expect(keyService.isValidRustDeskPublicKey(validKey)).toBe(true);
|
||||
});
|
||||
|
||||
it('isValidRustDeskPublicKey rejects placeholders and junk', () => {
|
||||
expect(keyService.isValidRustDeskPublicKey('')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('v1.4.9_public_key_placeholder...')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('YOUR_PUBLIC_KEY')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('__PUB_KEY_PATH__')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('not-base64!!!')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey(Buffer.alloc(16).toString('base64'))).toBe(false);
|
||||
});
|
||||
|
||||
it('getClientConfig embeds valid public key in deploy string', async () => {
|
||||
const payload = keyService.buildRustDeskConfigPayload('desk.example.com', { publicKey: validKey });
|
||||
const deploy = keyService.encodeRustDeskCliConfigString(payload);
|
||||
const json = Buffer.from(deploy.split('').reverse().join(''), 'base64').toString('utf8');
|
||||
const decoded = JSON.parse(json);
|
||||
expect(decoded.key).toBe(validKey);
|
||||
expect(decoded.host).toBe('desk.example.com');
|
||||
expect(payload.key).toBe(validKey);
|
||||
});
|
||||
|
||||
it('buildRustDeskConfigPayload drops invalid publicKey override', () => {
|
||||
const payload = keyService.buildRustDeskConfigPayload('desk.example.com', {
|
||||
publicKey: 'v1.4.9_public_key_placeholder...',
|
||||
});
|
||||
expect(payload.key).toBe('');
|
||||
});
|
||||
|
||||
it('getClientConfig with invalid file key and Go fallback fills deploy string', async () => {
|
||||
jest.spyOn(keyService, 'getPublicKey').mockReturnValue(null);
|
||||
const betterdeskApi = require('../services/betterdeskApi');
|
||||
jest.spyOn(betterdeskApi.apiClient, 'get').mockResolvedValue({ data: { key: validKey } });
|
||||
|
||||
const config = await keyService.getClientConfig({
|
||||
host: '203.0.113.10',
|
||||
relay: '203.0.113.10',
|
||||
api: 'http://203.0.113.10:21114',
|
||||
});
|
||||
|
||||
expect(config.has_public_key).toBe(true);
|
||||
expect(config.public_key).toBe(validKey);
|
||||
expect(config.deploy_config_string).toBeTruthy();
|
||||
|
||||
const json = Buffer.from(
|
||||
config.deploy_config_string.split('').reverse().join(''),
|
||||
'base64'
|
||||
).toString('utf8');
|
||||
expect(JSON.parse(json).key).toBe(validKey);
|
||||
});
|
||||
|
||||
it('getClientConfig leaves deploy string empty when no valid key exists', async () => {
|
||||
jest.spyOn(keyService, 'getPublicKey').mockReturnValue(null);
|
||||
const betterdeskApi = require('../services/betterdeskApi');
|
||||
jest.spyOn(betterdeskApi.apiClient, 'get').mockResolvedValue({
|
||||
data: { key: 'v1.4.9_public_key_placeholder...' },
|
||||
});
|
||||
|
||||
const config = await keyService.getClientConfig('desk.example.com');
|
||||
expect(config.has_public_key).toBe(false);
|
||||
expect(config.public_key).toBe('');
|
||||
expect(config.deploy_config_string).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,4 +89,27 @@ describe('rustdeskAddressBookSync', () => {
|
||||
{ id: '123456789', tags: ['Client', 'Windows', 'Servers'] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters known out-of-scope peers while keeping typed remote IDs', () => {
|
||||
const result = JSON.parse(sync.filterAddressBookPeersByScope(JSON.stringify({
|
||||
peers: [
|
||||
{ id: 'ALLOW1', alias: 'Ok' },
|
||||
{ id: 'DENY1', alias: 'Hidden' },
|
||||
{ id: 'REMOTE', alias: 'Typed' }
|
||||
],
|
||||
tags: ['X']
|
||||
}), {
|
||||
visibleIds: new Set(['ALLOW1']),
|
||||
knownDeviceIds: ['ALLOW1', 'DENY1']
|
||||
}));
|
||||
|
||||
expect(result.peers.map(p => p.id)).toEqual(['ALLOW1', 'REMOTE']);
|
||||
});
|
||||
|
||||
it('leaves address book unchanged when visibleIds is null', () => {
|
||||
const raw = JSON.stringify({ peers: [{ id: 'A' }], tags: [] });
|
||||
expect(sync.filterAddressBookPeersByScope(raw, { visibleIds: null })).toBe(
|
||||
JSON.stringify({ peers: [{ id: 'A' }], tags: [] })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user