mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-09 17:16:46 +00:00
Add CI, build scripts, docs, and dev utilities
Add a full automated build pipeline, interactive build scripts, documentation and development utilities. - Add GitHub Actions workflow (.github/workflows/build.yml) to build BetterDesk (hbbs/hbbr) for Linux x64, Linux ARM64 and Windows x64, upload artifacts, optionally create a release and update repository binaries. - Add interactive build scripts for Windows and Unix (build-betterdesk.ps1, build-betterdesk.sh) supporting download, patch application, cross-build and checksum generation. - Add build and status docs (docs/BUILD_GUIDE.md, docs/STATUS_TRACKING_v3.md) and update project README/instructions (.github/copilot-instructions.md) with new items and updated timestamp. - Add development utilities under dev_modules/ (multiple database/patch/test helpers) and move test_generator.py into dev_modules/test_generator.py. - Update .gitignore to exclude archive/ folder. - Modify hbbs-patch-v2 sources (database.rs, http_api.rs, main.rs, peer.rs and add rendezvous_server.rs) and remove legacy fixed files; update web UI backend and static files (web/app.py, web/static/*, web/templates/index.html). These changes set up CI/CD for multi-platform builds, provide local build tooling, expand docs for v3 status tracking and ID-change features, and add developer test/patch utilities.
This commit is contained in:
+121
-20
@@ -5,7 +5,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 📊 Stan Projektu (aktualizacja: 2026-02-06)
|
||||
## 📊 Stan Projektu (aktualizacja: 2026-02-07)
|
||||
|
||||
### Wersja Instalatorów
|
||||
|
||||
@@ -84,6 +84,9 @@ Rustdesk-FreeConsole/
|
||||
│ ├── hbbs-windows-x86_64.exe # Signal server Windows
|
||||
│ ├── hbbr-windows-x86_64.exe # Relay server Windows
|
||||
│ └── src/ # Source code for modifications
|
||||
├── docs/ # Documentation (English)
|
||||
├── dev_modules/ # Development & testing utilities
|
||||
├── archive/ # Archived files (not in git)
|
||||
├── Dockerfile.* # Docker images
|
||||
├── docker-compose.yml # Docker orchestration
|
||||
└── migrations/ # Database migrations
|
||||
@@ -163,30 +166,128 @@ sudo apt-get install -y build-essential libsqlite3-dev pkg-config libssl-dev git
|
||||
13. [x] **Naprawiono DNS issues** - dodano fallback DNS w Dockerfile.console (AlmaLinux/CentOS)
|
||||
14. [x] Zaktualizowano DOCKER_TROUBLESHOOTING.md z nowymi rozwiązaniami
|
||||
|
||||
### ✅ Ukończone (2026-02-07)
|
||||
15. [x] **Stworzono build-betterdesk.sh** - interaktywny skrypt do kompilacji (Linux/macOS)
|
||||
16. [x] **Stworzono build-betterdesk.ps1** - interaktywny skrypt do kompilacji (Windows)
|
||||
17. [x] **Stworzono GitHub Actions workflow** - automatyczna kompilacja multi-platform (.github/workflows/build.yml)
|
||||
18. [x] **Stworzono BUILD_GUIDE.md** - dokumentacja budowania ze źródeł
|
||||
19. [x] **System statusu v3.0** - konfigurowalny timeout, nowe statusy (Online/Degraded/Critical/Offline)
|
||||
20. [x] **Nowe endpointy API** - /api/config, /api/peers/stats, /api/server/stats
|
||||
21. [x] **Dokumentacja v3.0** - STATUS_TRACKING_v3.md
|
||||
22. [x] **Zmiana ID urządzenia** - moduł id_change.rs, endpoint POST /api/peers/:id/change-id
|
||||
23. [x] **Dokumentacja ID Change** - docs/ID_CHANGE_FEATURE.md
|
||||
|
||||
### 🔜 Do Zrobienia (priorytety)
|
||||
1. [ ] **Auto-update workflow** - GitHub Actions do automatycznego pobierania nowej wersji RustDesk i aplikowania patchy
|
||||
2. [ ] Dodać ARM64 binarki dla Linux (Raspberry Pi)
|
||||
3. [ ] Dodać automatyczne CI/CD builds (GitHub Actions)
|
||||
4. [ ] Ulepszyć dokumentację instalacji Windows
|
||||
5. [ ] Dodać testy jednostkowe dla HTTP API
|
||||
1. [ ] Kompilacja binarek v3.0.0 z nowymi plikami źródłowymi
|
||||
2. [ ] WebSocket real-time push dla statusu
|
||||
3. [ ] Dodać testy jednostkowe dla HTTP API
|
||||
4. [ ] Integracja id_change.rs z rendezvous_server_core.rs
|
||||
|
||||
### 🔄 Planowany Auto-Update Workflow
|
||||
---
|
||||
|
||||
**Cel:** Automatyczne aktualizowanie bazy RustDesk Server z zachowaniem patchy BetterDesk
|
||||
## 🔄 System Statusu v3.0
|
||||
|
||||
**Proces:**
|
||||
1. GitHub Actions sprawdza nowe tagi w `rustdesk/rustdesk-server`
|
||||
2. Klonuje nową wersję i aplikuje patche z `hbbs-patch-v2/src/`
|
||||
3. Próbuje skompilować (`cargo build --release`)
|
||||
4. Uruchamia testy funkcjonalne API
|
||||
5. Jeśli sukces → tworzy PR z nową wersją
|
||||
6. Jeśli błąd → tworzy Issue z logiem błędów
|
||||
### Nowe Pliki Źródłowe
|
||||
|
||||
**Pliki do aplikowania:**
|
||||
- `src/main.rs` - dodaje `--api-port` i uruchamia HTTP API
|
||||
- `src/http_api.rs` - cały moduł HTTP API (nowy plik)
|
||||
| Plik | Opis |
|
||||
|------|------|
|
||||
| `peer_v3.rs` | Ulepszony system statusu z konfigurowalnymi timeoutami |
|
||||
| `database_v3.rs` | Rozszerzona baza danych z server_config |
|
||||
| `http_api_v3.rs` | Nowe endpointy API dla konfiguracji |
|
||||
|
||||
**Ryzyko:** Zmiany w API axum/sqlx między wersjami RustDesk
|
||||
### Konfiguracja przez Zmienne Środowiskowe
|
||||
|
||||
```bash
|
||||
PEER_TIMEOUT_SECS=15 # Timeout dla offline (domyślnie 15s)
|
||||
HEARTBEAT_INTERVAL_SECS=3 # Interwał sprawdzania (domyślnie 3s)
|
||||
HEARTBEAT_WARNING_THRESHOLD=2 # Próg dla DEGRADED
|
||||
HEARTBEAT_CRITICAL_THRESHOLD=4 # Próg dla CRITICAL
|
||||
```
|
||||
|
||||
### Nowe Statusy Urządzeń
|
||||
|
||||
```
|
||||
ONLINE → Wszystko OK
|
||||
DEGRADED → 2-3 pominięte heartbeaty
|
||||
CRITICAL → 4+ pominięte, wkrótce offline
|
||||
OFFLINE → Przekroczony timeout
|
||||
```
|
||||
|
||||
### Dokumentacja
|
||||
|
||||
Pełna dokumentacja: [docs/STATUS_TRACKING_v3.md](docs/STATUS_TRACKING_v3.md)
|
||||
|
||||
---
|
||||
|
||||
## � Zmiana ID Urządzenia
|
||||
|
||||
### Endpoint API
|
||||
|
||||
```
|
||||
POST /api/peers/:old_id/change-id
|
||||
Content-Type: application/json
|
||||
X-API-Key: <api-key>
|
||||
|
||||
{ "new_id": "NEWID123" }
|
||||
```
|
||||
|
||||
### Pliki Źródłowe
|
||||
|
||||
| Plik | Opis |
|
||||
|------|------|
|
||||
| `id_change.rs` | Moduł obsługi zmiany ID przez protokół klienta |
|
||||
| `database_v3.rs` | Funkcje `change_peer_id()`, `get_peer_id_history()` |
|
||||
| `http_api_v3.rs` | Endpoint POST `/api/peers/:id/change-id` |
|
||||
|
||||
### Walidacja
|
||||
|
||||
- **Długość ID**: 6-16 znaków
|
||||
- **Dozwolone znaki**: A-Z, 0-9, `-`, `_`
|
||||
- **Unikatowość**: Nowe ID nie może być zajęte
|
||||
- **Rate limiting** (klient): 5 min cooldown
|
||||
|
||||
### Dokumentacja
|
||||
|
||||
Pełna dokumentacja: [docs/ID_CHANGE_FEATURE.md](docs/ID_CHANGE_FEATURE.md)
|
||||
|
||||
---
|
||||
|
||||
## �🔨 Skrypty Budowania
|
||||
|
||||
### Interaktywne skrypty kompilacji
|
||||
|
||||
| Skrypt | Platforma | Opis |
|
||||
|--------|-----------|------|
|
||||
| `build-betterdesk.sh` | Linux/macOS | Interaktywny build z wyborem wersji/platformy |
|
||||
| `build-betterdesk.ps1` | Windows | Interaktywny build PowerShell |
|
||||
|
||||
### Użycie
|
||||
|
||||
```bash
|
||||
# Linux - tryb interaktywny
|
||||
./build-betterdesk.sh
|
||||
|
||||
# Linux - tryb automatyczny
|
||||
./build-betterdesk.sh --auto
|
||||
|
||||
# Windows - tryb interaktywny
|
||||
.\build-betterdesk.ps1
|
||||
|
||||
# Windows - tryb automatyczny
|
||||
.\build-betterdesk.ps1 -Auto
|
||||
```
|
||||
|
||||
### GitHub Actions CI/CD
|
||||
|
||||
Workflow `.github/workflows/build.yml` automatycznie:
|
||||
- Buduje binarki dla Linux x64, Linux ARM64, Windows x64
|
||||
- Uruchamia się przy zmianach w `hbbs-patch-v2/src/**`
|
||||
- Pozwala na ręczne uruchomienie z wyborem wersji
|
||||
- Opcjonalnie tworzy GitHub Release
|
||||
|
||||
### Dokumentacja
|
||||
|
||||
Pełna dokumentacja budowania: [docs/BUILD_GUIDE.md](docs/BUILD_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -227,4 +328,4 @@ sudo apt-get install -y build-essential libsqlite3-dev pkg-config libssl-dev git
|
||||
|
||||
---
|
||||
|
||||
*Ostatnia aktualizacja: 2026-02-04 przez GitHub Copilot*
|
||||
*Ostatnia aktualizacja: 2026-02-07 przez GitHub Copilot*
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
# =============================================================================
|
||||
# BetterDesk Server - Automated Build Pipeline
|
||||
# =============================================================================
|
||||
# This workflow builds BetterDesk enhanced binaries for multiple platforms.
|
||||
#
|
||||
# Triggers:
|
||||
# - Push to 'main' with changes to hbbs-patch-v2/src/**
|
||||
# - Manual dispatch with version selection
|
||||
# - Pull requests for testing (no artifact upload)
|
||||
#
|
||||
# Platforms:
|
||||
# - Linux x86_64
|
||||
# - Linux aarch64 (ARM64)
|
||||
# - Windows x86_64
|
||||
# =============================================================================
|
||||
|
||||
name: Build BetterDesk Binaries
|
||||
|
||||
on:
|
||||
# Manual trigger with version selection
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rustdesk_version:
|
||||
description: 'RustDesk Server version to build from'
|
||||
required: true
|
||||
default: '1.1.14'
|
||||
type: string
|
||||
upload_release:
|
||||
description: 'Upload as GitHub Release'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
# Auto-trigger on source changes
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'hbbs-patch-v2/src/**'
|
||||
- '.github/workflows/build.yml'
|
||||
|
||||
# PR testing
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'hbbs-patch-v2/src/**'
|
||||
|
||||
env:
|
||||
RUSTDESK_VERSION: ${{ github.event.inputs.rustdesk_version || '1.1.14' }}
|
||||
BETTERDESK_VERSION: '2.0.0'
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# ==========================================================================
|
||||
# Linux x86_64 Build
|
||||
# ==========================================================================
|
||||
build-linux-x64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout BetterDesk
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev pkg-config libssl-dev
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-action@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Clone RustDesk Server
|
||||
run: |
|
||||
git clone --depth 1 --branch ${{ env.RUSTDESK_VERSION }} https://github.com/rustdesk/rustdesk-server.git
|
||||
cd rustdesk-server
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Apply BetterDesk modifications
|
||||
run: |
|
||||
# Copy all modification files
|
||||
for file in main.rs http_api.rs database.rs database_fixed.rs peer.rs peer_fixed.rs rendezvous_server_core.rs; do
|
||||
if [ -f "hbbs-patch-v2/src/$file" ]; then
|
||||
cp "hbbs-patch-v2/src/$file" "rustdesk-server/src/$file"
|
||||
echo "Applied: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Build binaries
|
||||
working-directory: rustdesk-server
|
||||
run: |
|
||||
cargo build --release -p hbbs
|
||||
cargo build --release -p hbbr
|
||||
|
||||
- name: Prepare artifacts
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp rustdesk-server/target/release/hbbs artifacts/hbbs-linux-x86_64
|
||||
cp rustdesk-server/target/release/hbbr artifacts/hbbr-linux-x86_64
|
||||
chmod +x artifacts/*
|
||||
|
||||
# Generate checksums
|
||||
cd artifacts
|
||||
sha256sum * > SHA256SUMS-linux-x86_64.txt
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: betterdesk-linux-x86_64
|
||||
path: artifacts/*
|
||||
retention-days: 30
|
||||
|
||||
# ==========================================================================
|
||||
# Linux ARM64 Build
|
||||
# ==========================================================================
|
||||
build-linux-arm64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout BetterDesk
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential pkg-config libssl-dev
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-action@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Clone RustDesk Server
|
||||
run: |
|
||||
git clone --depth 1 --branch ${{ env.RUSTDESK_VERSION }} https://github.com/rustdesk/rustdesk-server.git
|
||||
cd rustdesk-server
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Apply BetterDesk modifications
|
||||
run: |
|
||||
for file in main.rs http_api.rs database.rs database_fixed.rs peer.rs peer_fixed.rs rendezvous_server_core.rs; do
|
||||
if [ -f "hbbs-patch-v2/src/$file" ]; then
|
||||
cp "hbbs-patch-v2/src/$file" "rustdesk-server/src/$file"
|
||||
echo "Applied: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Configure cross-compilation
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat >> rustdesk-server/.cargo/config.toml << EOF
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
EOF
|
||||
|
||||
- name: Build binaries
|
||||
working-directory: rustdesk-server
|
||||
env:
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||
run: |
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p hbbs
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p hbbr
|
||||
|
||||
- name: Prepare artifacts
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp rustdesk-server/target/aarch64-unknown-linux-gnu/release/hbbs artifacts/hbbs-linux-aarch64
|
||||
cp rustdesk-server/target/aarch64-unknown-linux-gnu/release/hbbr artifacts/hbbr-linux-aarch64
|
||||
chmod +x artifacts/*
|
||||
|
||||
cd artifacts
|
||||
sha256sum * > SHA256SUMS-linux-aarch64.txt
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: betterdesk-linux-aarch64
|
||||
path: artifacts/*
|
||||
retention-days: 30
|
||||
|
||||
# ==========================================================================
|
||||
# Windows x86_64 Build
|
||||
# ==========================================================================
|
||||
build-windows-x64:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout BetterDesk
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-action@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Clone RustDesk Server
|
||||
run: |
|
||||
git clone --depth 1 --branch ${{ env.RUSTDESK_VERSION }} https://github.com/rustdesk/rustdesk-server.git
|
||||
cd rustdesk-server
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Apply BetterDesk modifications
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = @("main.rs", "http_api.rs", "database.rs", "database_fixed.rs", "peer.rs", "peer_fixed.rs", "rendezvous_server_core.rs")
|
||||
foreach ($file in $files) {
|
||||
$src = "hbbs-patch-v2/src/$file"
|
||||
if (Test-Path $src) {
|
||||
Copy-Item $src "rustdesk-server/src/$file" -Force
|
||||
Write-Host "Applied: $file"
|
||||
}
|
||||
}
|
||||
|
||||
- name: Build binaries
|
||||
working-directory: rustdesk-server
|
||||
run: |
|
||||
cargo build --release -p hbbs
|
||||
cargo build --release -p hbbr
|
||||
|
||||
- name: Prepare artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path artifacts
|
||||
Copy-Item rustdesk-server/target/release/hbbs.exe artifacts/hbbs-windows-x86_64.exe
|
||||
Copy-Item rustdesk-server/target/release/hbbr.exe artifacts/hbbr-windows-x86_64.exe
|
||||
|
||||
# Generate checksums
|
||||
cd artifacts
|
||||
Get-ChildItem -File | ForEach-Object {
|
||||
$hash = (Get-FileHash $_.Name -Algorithm SHA256).Hash
|
||||
"$hash $($_.Name)" | Out-File -Append SHA256SUMS-windows-x86_64.txt
|
||||
}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: betterdesk-windows-x86_64
|
||||
path: artifacts/*
|
||||
retention-days: 30
|
||||
|
||||
# ==========================================================================
|
||||
# Create Release (optional)
|
||||
# ==========================================================================
|
||||
create-release:
|
||||
needs: [build-linux-x64, build-linux-arm64, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.upload_release == 'true'
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-artifacts
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
|
||||
# Flatten all artifacts
|
||||
find all-artifacts -type f -exec cp {} release/ \;
|
||||
|
||||
# Generate combined checksums
|
||||
cd release
|
||||
cat SHA256SUMS-*.txt > SHA256SUMS.txt
|
||||
rm SHA256SUMS-*.txt
|
||||
|
||||
ls -la
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: v${{ env.BETTERDESK_VERSION }}-${{ github.run_number }}
|
||||
name: BetterDesk v${{ env.BETTERDESK_VERSION }} (Build ${{ github.run_number }})
|
||||
body: |
|
||||
## BetterDesk Server Binaries
|
||||
|
||||
Based on RustDesk Server v${{ env.RUSTDESK_VERSION }}
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | Signal Server (hbbs) | Relay Server (hbbr) |
|
||||
|----------|---------------------|---------------------|
|
||||
| Linux x86_64 | `hbbs-linux-x86_64` | `hbbr-linux-x86_64` |
|
||||
| Linux ARM64 | `hbbs-linux-aarch64` | `hbbr-linux-aarch64` |
|
||||
| Windows | `hbbs-windows-x86_64.exe` | `hbbr-windows-x86_64.exe` |
|
||||
|
||||
### Installation
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
chmod +x hbbs-linux-x86_64 hbbr-linux-x86_64
|
||||
./hbbs-linux-x86_64 -k _ --api-port 21114
|
||||
./hbbr-linux-x86_64
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\hbbs-windows-x86_64.exe -k _ --api-port 21114
|
||||
.\hbbr-windows-x86_64.exe
|
||||
```
|
||||
|
||||
See [README.md](https://github.com/${{ github.repository }}/blob/main/README.md) for full installation instructions.
|
||||
|
||||
files: release/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ==========================================================================
|
||||
# Update Repository Binaries
|
||||
# ==========================================================================
|
||||
update-repo-binaries:
|
||||
needs: [build-linux-x64, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download Linux x64 artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: betterdesk-linux-x86_64
|
||||
path: new-binaries/linux-x64
|
||||
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: betterdesk-windows-x86_64
|
||||
path: new-binaries/windows
|
||||
|
||||
- name: Update repository binaries
|
||||
run: |
|
||||
# Update Linux binaries
|
||||
cp new-binaries/linux-x64/hbbs-linux-x86_64 hbbs-patch-v2/
|
||||
cp new-binaries/linux-x64/hbbr-linux-x86_64 hbbs-patch-v2/
|
||||
chmod +x hbbs-patch-v2/hbbs-linux-x86_64 hbbs-patch-v2/hbbr-linux-x86_64
|
||||
|
||||
# Update Windows binaries
|
||||
cp new-binaries/windows/hbbs-windows-x86_64.exe hbbs-patch-v2/
|
||||
cp new-binaries/windows/hbbr-windows-x86_64.exe hbbs-patch-v2/
|
||||
|
||||
# Update checksums
|
||||
cd hbbs-patch-v2
|
||||
echo "# BetterDesk Binary Checksums" > CHECKSUMS.md
|
||||
echo "" >> CHECKSUMS.md
|
||||
echo "Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> CHECKSUMS.md
|
||||
echo "RustDesk Base: ${{ env.RUSTDESK_VERSION }}" >> CHECKSUMS.md
|
||||
echo "Build: #${{ github.run_number }}" >> CHECKSUMS.md
|
||||
echo "" >> CHECKSUMS.md
|
||||
echo "\`\`\`" >> CHECKSUMS.md
|
||||
sha256sum hbbs-* hbbr-* 2>/dev/null | grep -v ".md" >> CHECKSUMS.md
|
||||
echo "\`\`\`" >> CHECKSUMS.md
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add hbbs-patch-v2/
|
||||
git diff --staged --quiet || git commit -m "chore: Update BetterDesk binaries (Build #${{ github.run_number }})"
|
||||
git push
|
||||
@@ -59,6 +59,9 @@ test_*.sh
|
||||
verify_*.sh
|
||||
.security_scan_report.md
|
||||
|
||||
# Archive folder (contains old files with potential sensitive data)
|
||||
archive/
|
||||
|
||||
# Backups
|
||||
*.backup
|
||||
*backup*/
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
# =============================================================================
|
||||
# BetterDesk Server - Interactive Build Script (Windows)
|
||||
# =============================================================================
|
||||
# This script automates building BetterDesk enhanced binaries from source.
|
||||
# It handles downloading RustDesk sources, applying BetterDesk modifications,
|
||||
# and compiling the final binaries.
|
||||
#
|
||||
# Usage:
|
||||
# .\build-betterdesk.ps1 # Interactive mode
|
||||
# .\build-betterdesk.ps1 -Auto # Non-interactive (use defaults)
|
||||
# .\build-betterdesk.ps1 -Clean # Clean build directory
|
||||
# .\build-betterdesk.ps1 -Help # Show help
|
||||
#
|
||||
# Requirements:
|
||||
# - Rust toolchain (rustup)
|
||||
# - Visual Studio Build Tools with C++ support
|
||||
# - Git
|
||||
# =============================================================================
|
||||
|
||||
param(
|
||||
[switch]$Auto,
|
||||
[switch]$Clean,
|
||||
[string]$Version = "",
|
||||
[string]$Platform = "",
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Configuration
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$BuildDir = Join-Path $ScriptDir "build"
|
||||
$PatchesDir = Join-Path $ScriptDir "hbbs-patch-v2\src"
|
||||
$OutputDir = Join-Path $ScriptDir "hbbs-patch-v2"
|
||||
|
||||
# Default RustDesk version
|
||||
$DefaultRustDeskVersion = "1.1.14"
|
||||
$RustDeskRepo = "https://github.com/rustdesk/rustdesk-server.git"
|
||||
|
||||
# State
|
||||
$RustDeskVersion = $DefaultRustDeskVersion
|
||||
$TargetPlatform = "windows-x64"
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
function Write-Header {
|
||||
param([string]$Message)
|
||||
Write-Host ""
|
||||
Write-Host "╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||||
Write-Host "║ $Message" -ForegroundColor Cyan
|
||||
Write-Host "╚══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-Success { param([string]$Msg) Write-Host "✓ $Msg" -ForegroundColor Green }
|
||||
function Write-Error2 { param([string]$Msg) Write-Host "✗ $Msg" -ForegroundColor Red }
|
||||
function Write-Warning2 { param([string]$Msg) Write-Host "⚠ $Msg" -ForegroundColor Yellow }
|
||||
function Write-Info { param([string]$Msg) Write-Host "ℹ $Msg" -ForegroundColor Blue }
|
||||
function Write-Step { param([string]$Msg) Write-Host "→ $Msg" -ForegroundColor Cyan }
|
||||
|
||||
function Show-Help {
|
||||
Write-Host "BetterDesk Server - Build Script (Windows)"
|
||||
Write-Host ""
|
||||
Write-Host "Usage: .\build-betterdesk.ps1 [OPTIONS]"
|
||||
Write-Host ""
|
||||
Write-Host "Options:"
|
||||
Write-Host " -Auto Non-interactive mode (use default settings)"
|
||||
Write-Host " -Clean Clean build directory and exit"
|
||||
Write-Host " -Version VER Specify RustDesk version (default: $DefaultRustDeskVersion)"
|
||||
Write-Host " -Platform PLT Target platform: windows-x64, linux-x64"
|
||||
Write-Host " -Help Show this help message"
|
||||
Write-Host ""
|
||||
Write-Host "Examples:"
|
||||
Write-Host " .\build-betterdesk.ps1 # Interactive build"
|
||||
Write-Host " .\build-betterdesk.ps1 -Auto # Build with defaults"
|
||||
Write-Host " .\build-betterdesk.ps1 -Version 1.1.15 # Build specific version"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Dependency Checks
|
||||
# =============================================================================
|
||||
|
||||
function Test-Dependencies {
|
||||
Write-Header "Checking Dependencies"
|
||||
|
||||
$missing = 0
|
||||
|
||||
# Check Rust
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if ($cargo) {
|
||||
$rustVersion = & rustc --version 2>&1
|
||||
Write-Success "Rust/Cargo: $rustVersion"
|
||||
} else {
|
||||
Write-Error2 "Rust/Cargo not found"
|
||||
Write-Host " Install from: https://rustup.rs/"
|
||||
$missing++
|
||||
}
|
||||
|
||||
# Check Git
|
||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||||
if ($git) {
|
||||
$gitVersion = & git --version 2>&1
|
||||
Write-Success "Git: $gitVersion"
|
||||
} else {
|
||||
Write-Error2 "Git not found"
|
||||
Write-Host " Install from: https://git-scm.com/"
|
||||
$missing++
|
||||
}
|
||||
|
||||
# Check Visual Studio Build Tools
|
||||
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vsWhere) {
|
||||
$vsPath = & $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>&1
|
||||
if ($vsPath) {
|
||||
Write-Success "Visual Studio Build Tools found"
|
||||
} else {
|
||||
Write-Warning2 "Visual Studio C++ Build Tools may not be installed"
|
||||
Write-Host " Install from: https://visualstudio.microsoft.com/visual-cpp-build-tools/"
|
||||
}
|
||||
} else {
|
||||
Write-Warning2 "Could not verify Visual Studio Build Tools"
|
||||
}
|
||||
|
||||
if ($missing -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Error2 "Missing dependencies. Please install them first."
|
||||
Write-Host ""
|
||||
Write-Host "Required:"
|
||||
Write-Host " 1. Rust: https://rustup.rs/"
|
||||
Write-Host " 2. Git: https://git-scm.com/"
|
||||
Write-Host " 3. Visual Studio Build Tools with C++ support"
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "All dependencies satisfied!"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Interactive Configuration
|
||||
# =============================================================================
|
||||
|
||||
function Get-Configuration {
|
||||
Write-Header "Build Configuration"
|
||||
|
||||
# Select RustDesk version
|
||||
Write-Host "Available RustDesk versions:" -ForegroundColor White
|
||||
Write-Host " 1) 1.1.14 (stable, recommended)"
|
||||
Write-Host " 2) 1.1.13 (older stable)"
|
||||
Write-Host " 3) Custom (enter version)"
|
||||
Write-Host ""
|
||||
|
||||
if ($Auto) {
|
||||
$script:RustDeskVersion = $DefaultRustDeskVersion
|
||||
Write-Info "Auto mode: Using version $RustDeskVersion"
|
||||
} elseif ($Version) {
|
||||
$script:RustDeskVersion = $Version
|
||||
} else {
|
||||
$choice = Read-Host "Select version [1]"
|
||||
switch ($choice) {
|
||||
"2" { $script:RustDeskVersion = "1.1.13" }
|
||||
"3" { $script:RustDeskVersion = Read-Host "Enter version (e.g., 1.1.15)" }
|
||||
default { $script:RustDeskVersion = $DefaultRustDeskVersion }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Selected RustDesk version: $RustDeskVersion"
|
||||
Write-Host ""
|
||||
|
||||
# Select target platform
|
||||
Write-Host "Target platform:" -ForegroundColor White
|
||||
Write-Host " 1) Windows x86_64 (native)"
|
||||
Write-Host " 2) Linux x86_64 (cross-compile, requires WSL)"
|
||||
Write-Host ""
|
||||
|
||||
if ($Auto) {
|
||||
$script:TargetPlatform = "windows-x64"
|
||||
Write-Info "Auto mode: Building for $TargetPlatform"
|
||||
} elseif ($Platform) {
|
||||
$script:TargetPlatform = $Platform
|
||||
} else {
|
||||
$choice = Read-Host "Select platform [1]"
|
||||
switch ($choice) {
|
||||
"2" { $script:TargetPlatform = "linux-x64" }
|
||||
default { $script:TargetPlatform = "windows-x64" }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Target platform: $TargetPlatform"
|
||||
Write-Host ""
|
||||
|
||||
# Confirm
|
||||
if (-not $Auto) {
|
||||
Write-Host "Build Summary:" -ForegroundColor White
|
||||
Write-Host " RustDesk Version: $RustDeskVersion"
|
||||
Write-Host " Target Platform: $TargetPlatform"
|
||||
Write-Host " Build Directory: $BuildDir"
|
||||
Write-Host " Output Directory: $OutputDir"
|
||||
Write-Host ""
|
||||
$confirm = Read-Host "Continue with build? [Y/n]"
|
||||
if ($confirm -match "^[Nn]$") {
|
||||
Write-Host "Build cancelled."
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Download RustDesk Sources
|
||||
# =============================================================================
|
||||
|
||||
function Get-RustDeskSources {
|
||||
Write-Header "Downloading RustDesk Server Sources"
|
||||
|
||||
$sourceDir = Join-Path $BuildDir "rustdesk-server-$RustDeskVersion"
|
||||
|
||||
if (Test-Path $sourceDir) {
|
||||
Write-Info "Source directory exists: $sourceDir"
|
||||
|
||||
if (-not $Auto) {
|
||||
$redownload = Read-Host "Re-download sources? [y/N]"
|
||||
if ($redownload -notmatch "^[Yy]$") {
|
||||
Write-Success "Using existing sources"
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Write-Info "Auto mode: Using existing sources"
|
||||
return
|
||||
}
|
||||
|
||||
Remove-Item $sourceDir -Recurse -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path $BuildDir)) {
|
||||
New-Item -ItemType Directory -Path $BuildDir | Out-Null
|
||||
}
|
||||
|
||||
Push-Location $BuildDir
|
||||
|
||||
Write-Step "Cloning rustdesk-server repository..."
|
||||
& git clone --depth 1 --branch $RustDeskVersion $RustDeskRepo "rustdesk-server-$RustDeskVersion"
|
||||
|
||||
Set-Location "rustdesk-server-$RustDeskVersion"
|
||||
|
||||
Write-Step "Initializing submodules..."
|
||||
& git submodule update --init --recursive
|
||||
|
||||
Pop-Location
|
||||
|
||||
Write-Success "RustDesk sources downloaded successfully"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Apply BetterDesk Modifications
|
||||
# =============================================================================
|
||||
|
||||
function Apply-Modifications {
|
||||
Write-Header "Applying BetterDesk Modifications"
|
||||
|
||||
$sourceDir = Join-Path $BuildDir "rustdesk-server-$RustDeskVersion"
|
||||
|
||||
if (-not (Test-Path $sourceDir)) {
|
||||
Write-Error2 "Source directory not found: $sourceDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Push-Location $sourceDir
|
||||
|
||||
# List of files to copy from patches
|
||||
$patchFiles = @(
|
||||
"main.rs",
|
||||
"http_api.rs",
|
||||
"database.rs",
|
||||
"database_fixed.rs",
|
||||
"peer.rs",
|
||||
"peer_fixed.rs",
|
||||
"rendezvous_server_core.rs"
|
||||
)
|
||||
|
||||
Write-Step "Copying BetterDesk modifications..."
|
||||
|
||||
foreach ($file in $patchFiles) {
|
||||
$patchPath = Join-Path $PatchesDir $file
|
||||
if (Test-Path $patchPath) {
|
||||
$targetPath = Join-Path "src" $file
|
||||
Copy-Item $patchPath $targetPath -Force
|
||||
Write-Success "Applied: $file"
|
||||
} else {
|
||||
Write-Warning2 "Patch file not found: $file"
|
||||
}
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
|
||||
Write-Success "BetterDesk modifications applied successfully"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Build Binaries
|
||||
# =============================================================================
|
||||
|
||||
function Build-Binaries {
|
||||
Write-Header "Building BetterDesk Binaries"
|
||||
|
||||
$sourceDir = Join-Path $BuildDir "rustdesk-server-$RustDeskVersion"
|
||||
Push-Location $sourceDir
|
||||
|
||||
$targetFlag = ""
|
||||
$binarySuffix = "-windows-x86_64.exe"
|
||||
|
||||
if ($TargetPlatform -eq "linux-x64") {
|
||||
Write-Step "Setting up Linux cross-compilation..."
|
||||
& rustup target add x86_64-unknown-linux-gnu
|
||||
$targetFlag = "--target x86_64-unknown-linux-gnu"
|
||||
$binarySuffix = "-linux-x86_64"
|
||||
}
|
||||
|
||||
Write-Step "Building HBBS (Signal Server)..."
|
||||
if ($targetFlag) {
|
||||
& cargo build --release $targetFlag -p hbbs
|
||||
} else {
|
||||
& cargo build --release -p hbbs
|
||||
}
|
||||
|
||||
Write-Step "Building HBBR (Relay Server)..."
|
||||
if ($targetFlag) {
|
||||
& cargo build --release $targetFlag -p hbbr
|
||||
} else {
|
||||
& cargo build --release -p hbbr
|
||||
}
|
||||
|
||||
# Find binaries
|
||||
$targetDir = "target\release"
|
||||
if ($targetFlag) {
|
||||
$targetDir = "target\$($targetFlag -replace '--target ','')\release"
|
||||
}
|
||||
|
||||
$hbbsBinary = Join-Path $targetDir "hbbs.exe"
|
||||
$hbbrBinary = Join-Path $targetDir "hbbr.exe"
|
||||
|
||||
if ($TargetPlatform -eq "linux-x64") {
|
||||
$hbbsBinary = Join-Path $targetDir "hbbs"
|
||||
$hbbrBinary = Join-Path $targetDir "hbbr"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $hbbsBinary) -or -not (Test-Path $hbbrBinary)) {
|
||||
Write-Error2 "Build failed - binaries not found"
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Build completed successfully!"
|
||||
|
||||
# Copy to output directory
|
||||
Write-Step "Copying binaries to output directory..."
|
||||
|
||||
$hbbsOutput = Join-Path $OutputDir "hbbs$binarySuffix"
|
||||
$hbbrOutput = Join-Path $OutputDir "hbbr$binarySuffix"
|
||||
|
||||
Copy-Item $hbbsBinary $hbbsOutput -Force
|
||||
Copy-Item $hbbrBinary $hbbrOutput -Force
|
||||
|
||||
Pop-Location
|
||||
|
||||
Write-Success "Binaries saved to:"
|
||||
Write-Host " - $hbbsOutput"
|
||||
Write-Host " - $hbbrOutput"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Generate Checksums
|
||||
# =============================================================================
|
||||
|
||||
function New-Checksums {
|
||||
Write-Header "Generating Checksums"
|
||||
|
||||
$checksumsFile = Join-Path $OutputDir "CHECKSUMS.md"
|
||||
$dateNow = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
$content = @"
|
||||
# BetterDesk Server - Binary Checksums
|
||||
|
||||
Generated: $dateNow
|
||||
RustDesk Base Version: $RustDeskVersion
|
||||
BetterDesk Version: 2.0.0
|
||||
|
||||
## SHA256 Checksums
|
||||
|
||||
``````
|
||||
"@
|
||||
|
||||
Get-ChildItem $OutputDir -Filter "hbbs-*" | ForEach-Object {
|
||||
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
|
||||
$content += "$hash $($_.Name)`n"
|
||||
}
|
||||
|
||||
Get-ChildItem $OutputDir -Filter "hbbr-*" | ForEach-Object {
|
||||
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
|
||||
$content += "$hash $($_.Name)`n"
|
||||
}
|
||||
|
||||
$content += "``````"
|
||||
|
||||
$content | Out-File $checksumsFile -Encoding UTF8
|
||||
|
||||
Write-Success "Checksums saved to: $checksumsFile"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Clean Build
|
||||
# =============================================================================
|
||||
|
||||
function Clear-Build {
|
||||
Write-Header "Cleaning Build Directory"
|
||||
|
||||
if (Test-Path $BuildDir) {
|
||||
Write-Step "Removing: $BuildDir"
|
||||
Remove-Item $BuildDir -Recurse -Force
|
||||
Write-Success "Build directory cleaned"
|
||||
} else {
|
||||
Write-Info "Build directory does not exist"
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
# Handle help
|
||||
if ($Help) {
|
||||
Show-Help
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Handle clean mode
|
||||
if ($Clean) {
|
||||
Clear-Build
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Banner
|
||||
Write-Host ""
|
||||
Write-Host "╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||||
Write-Host "║ BetterDesk Server - Build from Source (Windows) ║" -ForegroundColor Cyan
|
||||
Write-Host "║ Enhanced RustDesk with HTTP API & Management ║" -ForegroundColor Cyan
|
||||
Write-Host "╚══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Run build steps
|
||||
Test-Dependencies
|
||||
Get-Configuration
|
||||
Get-RustDeskSources
|
||||
Apply-Modifications
|
||||
Build-Binaries
|
||||
New-Checksums
|
||||
|
||||
# Final message
|
||||
Write-Header "Build Complete!"
|
||||
|
||||
Write-Host "BetterDesk binaries have been built successfully!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Output location: $OutputDir"
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host " 1. Test the binaries:"
|
||||
Write-Host " cd $OutputDir"
|
||||
Write-Host " .\hbbs-windows-x86_64.exe --help"
|
||||
Write-Host ""
|
||||
Write-Host " 2. Run the installer to deploy:"
|
||||
Write-Host " .\install-improved.ps1"
|
||||
Write-Host ""
|
||||
Write-Host " 3. Or manually start the servers:"
|
||||
Write-Host " .\hbbs-windows-x86_64.exe -k _ --api-port 21114"
|
||||
Write-Host " .\hbbr-windows-x86_64.exe"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Main
|
||||
@@ -0,0 +1,548 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# BetterDesk Server - Interactive Build Script
|
||||
# =============================================================================
|
||||
# This script automates building BetterDesk enhanced binaries from source.
|
||||
# It handles downloading RustDesk sources, applying BetterDesk modifications,
|
||||
# and compiling the final binaries.
|
||||
#
|
||||
# Usage:
|
||||
# ./build-betterdesk.sh # Interactive mode
|
||||
# ./build-betterdesk.sh --auto # Non-interactive (use defaults)
|
||||
# ./build-betterdesk.sh --clean # Clean build directory
|
||||
# ./build-betterdesk.sh --help # Show help
|
||||
#
|
||||
# Requirements:
|
||||
# - Rust toolchain (rustup)
|
||||
# - Build essentials (gcc, make)
|
||||
# - SQLite3 development files
|
||||
# - OpenSSL development files
|
||||
# - Git
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
PATCHES_DIR="$SCRIPT_DIR/hbbs-patch-v2/src"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/hbbs-patch-v2"
|
||||
|
||||
# Default RustDesk version (tag-based)
|
||||
DEFAULT_RUSTDESK_VERSION="1.1.14"
|
||||
RUSTDESK_REPO="https://github.com/rustdesk/rustdesk-server.git"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Command line options
|
||||
AUTO_MODE=false
|
||||
CLEAN_MODE=false
|
||||
TARGET_PLATFORM=""
|
||||
RUSTDESK_VERSION=""
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}$1${NC}"
|
||||
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
print_error() { echo -e "${RED}✗${NC} $1"; }
|
||||
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
||||
print_step() { echo -e "${CYAN}→${NC} $1"; }
|
||||
|
||||
show_help() {
|
||||
echo "BetterDesk Server - Build Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --auto Non-interactive mode (use default settings)"
|
||||
echo " --clean Clean build directory and exit"
|
||||
echo " --version VER Specify RustDesk version (default: $DEFAULT_RUSTDESK_VERSION)"
|
||||
echo " --platform PLT Target platform: linux-x64, linux-arm64, windows-x64"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Interactive build"
|
||||
echo " $0 --auto # Build with defaults"
|
||||
echo " $0 --version 1.1.15 # Build specific version"
|
||||
echo " $0 --platform linux-arm64 # Cross-compile for ARM64"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Dependency Checks
|
||||
# =============================================================================
|
||||
|
||||
check_dependencies() {
|
||||
print_header "Checking Dependencies"
|
||||
|
||||
local missing=0
|
||||
|
||||
# Check Rust
|
||||
if command -v cargo &> /dev/null; then
|
||||
print_success "Rust/Cargo: $(rustc --version 2>/dev/null | head -1)"
|
||||
else
|
||||
print_error "Rust/Cargo not found"
|
||||
echo " Install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
# Check Git
|
||||
if command -v git &> /dev/null; then
|
||||
print_success "Git: $(git --version)"
|
||||
else
|
||||
print_error "Git not found"
|
||||
echo " Install: sudo apt-get install git"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
# Check pkg-config
|
||||
if command -v pkg-config &> /dev/null; then
|
||||
print_success "pkg-config found"
|
||||
else
|
||||
print_error "pkg-config not found"
|
||||
echo " Install: sudo apt-get install pkg-config"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
# Check SQLite3
|
||||
if pkg-config --exists sqlite3 2>/dev/null; then
|
||||
print_success "SQLite3 development files found"
|
||||
else
|
||||
print_error "SQLite3 development files not found"
|
||||
echo " Install: sudo apt-get install libsqlite3-dev"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
# Check OpenSSL
|
||||
if pkg-config --exists openssl 2>/dev/null; then
|
||||
print_success "OpenSSL development files found"
|
||||
else
|
||||
print_error "OpenSSL development files not found"
|
||||
echo " Install: sudo apt-get install libssl-dev"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
# Check build essentials
|
||||
if command -v gcc &> /dev/null; then
|
||||
print_success "GCC: $(gcc --version | head -1)"
|
||||
else
|
||||
print_error "GCC not found"
|
||||
echo " Install: sudo apt-get install build-essential"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if [ $missing -ne 0 ]; then
|
||||
echo ""
|
||||
print_error "Missing dependencies. Please install them first."
|
||||
echo ""
|
||||
echo "Quick install (Debian/Ubuntu):"
|
||||
echo " sudo apt-get update"
|
||||
echo " sudo apt-get install -y build-essential pkg-config libsqlite3-dev libssl-dev git curl"
|
||||
echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "All dependencies satisfied!"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Interactive Configuration
|
||||
# =============================================================================
|
||||
|
||||
interactive_config() {
|
||||
print_header "Build Configuration"
|
||||
|
||||
# Select RustDesk version
|
||||
echo -e "${BOLD}Available RustDesk versions:${NC}"
|
||||
echo " 1) 1.1.14 (stable, recommended)"
|
||||
echo " 2) 1.1.13 (older stable)"
|
||||
echo " 3) Custom (enter version)"
|
||||
echo ""
|
||||
|
||||
if [ "$AUTO_MODE" = true ]; then
|
||||
RUSTDESK_VERSION="$DEFAULT_RUSTDESK_VERSION"
|
||||
print_info "Auto mode: Using version $RUSTDESK_VERSION"
|
||||
else
|
||||
read -p "Select version [1]: " version_choice
|
||||
case "$version_choice" in
|
||||
2) RUSTDESK_VERSION="1.1.13" ;;
|
||||
3)
|
||||
read -p "Enter version (e.g., 1.1.15): " RUSTDESK_VERSION
|
||||
;;
|
||||
*) RUSTDESK_VERSION="$DEFAULT_RUSTDESK_VERSION" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
print_success "Selected RustDesk version: $RUSTDESK_VERSION"
|
||||
echo ""
|
||||
|
||||
# Select target platform
|
||||
echo -e "${BOLD}Target platform:${NC}"
|
||||
echo " 1) Linux x86_64 (native)"
|
||||
echo " 2) Linux ARM64 (cross-compile)"
|
||||
echo " 3) Windows x86_64 (cross-compile, requires mingw)"
|
||||
echo ""
|
||||
|
||||
if [ "$AUTO_MODE" = true ]; then
|
||||
TARGET_PLATFORM="linux-x64"
|
||||
print_info "Auto mode: Building for $TARGET_PLATFORM"
|
||||
else
|
||||
read -p "Select platform [1]: " platform_choice
|
||||
case "$platform_choice" in
|
||||
2) TARGET_PLATFORM="linux-arm64" ;;
|
||||
3) TARGET_PLATFORM="windows-x64" ;;
|
||||
*) TARGET_PLATFORM="linux-x64" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
print_success "Target platform: $TARGET_PLATFORM"
|
||||
echo ""
|
||||
|
||||
# Confirm
|
||||
if [ "$AUTO_MODE" = false ]; then
|
||||
echo -e "${BOLD}Build Summary:${NC}"
|
||||
echo " RustDesk Version: $RUSTDESK_VERSION"
|
||||
echo " Target Platform: $TARGET_PLATFORM"
|
||||
echo " Build Directory: $BUILD_DIR"
|
||||
echo " Output Directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
read -p "Continue with build? [Y/n] " confirm
|
||||
if [[ "$confirm" =~ ^[Nn]$ ]]; then
|
||||
echo "Build cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Download RustDesk Sources
|
||||
# =============================================================================
|
||||
|
||||
download_rustdesk() {
|
||||
print_header "Downloading RustDesk Server Sources"
|
||||
|
||||
local source_dir="$BUILD_DIR/rustdesk-server-$RUSTDESK_VERSION"
|
||||
|
||||
if [ -d "$source_dir" ]; then
|
||||
print_info "Source directory exists: $source_dir"
|
||||
|
||||
if [ "$AUTO_MODE" = false ]; then
|
||||
read -p "Re-download sources? [y/N] " redownload
|
||||
if [[ ! "$redownload" =~ ^[Yy]$ ]]; then
|
||||
print_success "Using existing sources"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
print_info "Auto mode: Using existing sources"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -rf "$source_dir"
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
print_step "Cloning rustdesk-server repository..."
|
||||
git clone --depth 1 --branch "$RUSTDESK_VERSION" "$RUSTDESK_REPO" "rustdesk-server-$RUSTDESK_VERSION"
|
||||
|
||||
cd "rustdesk-server-$RUSTDESK_VERSION"
|
||||
|
||||
print_step "Initializing submodules..."
|
||||
git submodule update --init --recursive
|
||||
|
||||
print_success "RustDesk sources downloaded successfully"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Apply BetterDesk Modifications
|
||||
# =============================================================================
|
||||
|
||||
apply_modifications() {
|
||||
print_header "Applying BetterDesk Modifications"
|
||||
|
||||
local source_dir="$BUILD_DIR/rustdesk-server-$RUSTDESK_VERSION"
|
||||
|
||||
if [ ! -d "$source_dir" ]; then
|
||||
print_error "Source directory not found: $source_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$source_dir"
|
||||
|
||||
# List of files to copy from patches
|
||||
local patch_files=(
|
||||
"main.rs"
|
||||
"http_api.rs"
|
||||
"database.rs"
|
||||
"database_fixed.rs"
|
||||
"peer.rs"
|
||||
"peer_fixed.rs"
|
||||
"rendezvous_server_core.rs"
|
||||
)
|
||||
|
||||
print_step "Copying BetterDesk modifications..."
|
||||
|
||||
for file in "${patch_files[@]}"; do
|
||||
if [ -f "$PATCHES_DIR/$file" ]; then
|
||||
# Determine target directory
|
||||
case "$file" in
|
||||
main.rs)
|
||||
cp "$PATCHES_DIR/$file" "src/main.rs"
|
||||
print_success "Applied: main.rs (HTTP API integration)"
|
||||
;;
|
||||
http_api.rs)
|
||||
cp "$PATCHES_DIR/$file" "src/http_api.rs"
|
||||
print_success "Applied: http_api.rs (REST API module)"
|
||||
;;
|
||||
database*.rs|peer*.rs|rendezvous_server_core.rs)
|
||||
cp "$PATCHES_DIR/$file" "src/$file"
|
||||
print_success "Applied: $file"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
print_warning "Patch file not found: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Update Cargo.toml to include new dependencies
|
||||
print_step "Updating Cargo.toml with BetterDesk dependencies..."
|
||||
|
||||
# Check if we need to add axum dependency
|
||||
if ! grep -q "axum" Cargo.toml; then
|
||||
print_info "Adding HTTP API dependencies to Cargo.toml..."
|
||||
|
||||
# Add dependencies before [features] or at end of [dependencies]
|
||||
sed -i '/^\[dependencies\]/a \
|
||||
# BetterDesk HTTP API dependencies\
|
||||
axum = { version = "0.5", features = ["ws"] }\
|
||||
chrono = { version = "0.4", features = ["serde"] }' Cargo.toml
|
||||
|
||||
print_success "Updated Cargo.toml"
|
||||
else
|
||||
print_info "Cargo.toml already has required dependencies"
|
||||
fi
|
||||
|
||||
print_success "BetterDesk modifications applied successfully"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Build Binaries
|
||||
# =============================================================================
|
||||
|
||||
build_binaries() {
|
||||
print_header "Building BetterDesk Binaries"
|
||||
|
||||
local source_dir="$BUILD_DIR/rustdesk-server-$RUSTDESK_VERSION"
|
||||
cd "$source_dir"
|
||||
|
||||
# Set up cross-compilation if needed
|
||||
case "$TARGET_PLATFORM" in
|
||||
linux-arm64)
|
||||
print_step "Setting up ARM64 cross-compilation..."
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
local target_flag="--target aarch64-unknown-linux-gnu"
|
||||
local binary_suffix="-linux-arm64"
|
||||
;;
|
||||
windows-x64)
|
||||
print_step "Setting up Windows cross-compilation..."
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
local target_flag="--target x86_64-pc-windows-gnu"
|
||||
local binary_suffix="-windows-x86_64.exe"
|
||||
;;
|
||||
*)
|
||||
local target_flag=""
|
||||
local binary_suffix="-linux-x86_64"
|
||||
;;
|
||||
esac
|
||||
|
||||
print_step "Building HBBS (Signal Server)..."
|
||||
cargo build --release $target_flag -p hbbs
|
||||
|
||||
print_step "Building HBBR (Relay Server)..."
|
||||
cargo build --release $target_flag -p hbbr
|
||||
|
||||
# Find binaries
|
||||
local target_dir="target"
|
||||
if [ -n "$target_flag" ]; then
|
||||
target_dir="target/$(echo $target_flag | sed 's/--target //')"
|
||||
fi
|
||||
|
||||
local hbbs_binary="$target_dir/release/hbbs"
|
||||
local hbbr_binary="$target_dir/release/hbbr"
|
||||
|
||||
if [ "$TARGET_PLATFORM" = "windows-x64" ]; then
|
||||
hbbs_binary="${hbbs_binary}.exe"
|
||||
hbbr_binary="${hbbr_binary}.exe"
|
||||
fi
|
||||
|
||||
if [ ! -f "$hbbs_binary" ] || [ ! -f "$hbbr_binary" ]; then
|
||||
print_error "Build failed - binaries not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Build completed successfully!"
|
||||
|
||||
# Copy to output directory
|
||||
print_step "Copying binaries to output directory..."
|
||||
|
||||
cp "$hbbs_binary" "$OUTPUT_DIR/hbbs$binary_suffix"
|
||||
cp "$hbbr_binary" "$OUTPUT_DIR/hbbr$binary_suffix"
|
||||
|
||||
chmod +x "$OUTPUT_DIR/hbbs$binary_suffix" "$OUTPUT_DIR/hbbr$binary_suffix"
|
||||
|
||||
print_success "Binaries saved to:"
|
||||
echo " - $OUTPUT_DIR/hbbs$binary_suffix"
|
||||
echo " - $OUTPUT_DIR/hbbr$binary_suffix"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Generate Checksums
|
||||
# =============================================================================
|
||||
|
||||
generate_checksums() {
|
||||
print_header "Generating Checksums"
|
||||
|
||||
cd "$OUTPUT_DIR"
|
||||
|
||||
local checksums_file="CHECKSUMS.md"
|
||||
local date_now=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
cat > "$checksums_file" << EOF
|
||||
# BetterDesk Server - Binary Checksums
|
||||
|
||||
Generated: $date_now
|
||||
RustDesk Base Version: $RUSTDESK_VERSION
|
||||
BetterDesk Version: 2.0.0
|
||||
|
||||
## SHA256 Checksums
|
||||
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
for binary in hbbs-* hbbr-*; do
|
||||
if [ -f "$binary" ]; then
|
||||
sha256sum "$binary" >> "$checksums_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo '```' >> "$checksums_file"
|
||||
|
||||
print_success "Checksums saved to: $OUTPUT_DIR/$checksums_file"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Clean Build
|
||||
# =============================================================================
|
||||
|
||||
clean_build() {
|
||||
print_header "Cleaning Build Directory"
|
||||
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
print_step "Removing: $BUILD_DIR"
|
||||
rm -rf "$BUILD_DIR"
|
||||
print_success "Build directory cleaned"
|
||||
else
|
||||
print_info "Build directory does not exist"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
main() {
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--auto)
|
||||
AUTO_MODE=true
|
||||
shift
|
||||
;;
|
||||
--clean)
|
||||
CLEAN_MODE=true
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
RUSTDESK_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
TARGET_PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Handle clean mode
|
||||
if [ "$CLEAN_MODE" = true ]; then
|
||||
clean_build
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Banner
|
||||
echo ""
|
||||
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}BetterDesk Server - Build from Source${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} Enhanced RustDesk with HTTP API & Management ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# Run build steps
|
||||
check_dependencies
|
||||
interactive_config
|
||||
download_rustdesk
|
||||
apply_modifications
|
||||
build_binaries
|
||||
generate_checksums
|
||||
|
||||
# Final message
|
||||
print_header "Build Complete!"
|
||||
|
||||
echo -e "${GREEN}BetterDesk binaries have been built successfully!${NC}"
|
||||
echo ""
|
||||
echo "Output location: $OUTPUT_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Test the binaries:"
|
||||
echo " cd $OUTPUT_DIR"
|
||||
echo " ./hbbs-linux-x86_64 --help"
|
||||
echo ""
|
||||
echo " 2. Run the installer to deploy:"
|
||||
echo " sudo ./install-improved.sh"
|
||||
echo ""
|
||||
echo " 3. Or manually start the servers:"
|
||||
echo " ./hbbs-linux-x86_64 -k _ --api-port 21114 &"
|
||||
echo " ./hbbr-linux-x86_64 &"
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+15
-30
@@ -6,45 +6,30 @@ This directory contains development and testing utilities for BetterDesk Console
|
||||
|
||||
### Testing Scripts
|
||||
- **test_ban_api.sh** - Test script for ban/unban API endpoints
|
||||
- Tests device banning functionality
|
||||
- Validates API responses
|
||||
- Usage: `./test_ban_api.sh`
|
||||
- **test_change_id.py** - Test ID change API endpoint (requires HBBS_API_KEY environment variable)
|
||||
- **test_id_change.sh** - Test ID change via Web Console API (requires ADMIN_PASS environment variable)
|
||||
- **test_generator.py** - Test script for RustDesk Client Generator
|
||||
|
||||
### Diagnostic Tools
|
||||
- **check_database.py** - Database inspection and validation tool
|
||||
- Checks database schema
|
||||
- Verifies migrations applied
|
||||
- Lists devices and their status
|
||||
- Usage: `python3 check_database.py`
|
||||
|
||||
- **check_and_fix_database.sh** - **Database Schema Checker & Fixer** ⭐
|
||||
- Automatically detects database location
|
||||
- Validates all required tables and columns
|
||||
- Fixes missing or incorrect schema
|
||||
- Creates backup before making changes
|
||||
- Creates admin user if missing
|
||||
- Usage: `sudo ./check_and_fix_database.sh [database_path]`
|
||||
- Example: `sudo ./check_and_fix_database.sh /opt/rustdesk/db_v2.sqlite3`
|
||||
- **Use this if you have login problems!**
|
||||
|
||||
- Use this if you have login problems!
|
||||
- **diagnose_offline_status.sh** - **Diagnose offline status issues** ⭐
|
||||
- Use this if devices show as offline incorrectly!
|
||||
- **fix_peer_columns.sh** - **Quick Fix for Device Errors** ⭐
|
||||
- Adds missing columns to peer table (updated_at, deleted_at, etc.)
|
||||
- Fixes "no such column: updated_at" error
|
||||
- Usage: `sudo ./fix_peer_columns.sh [database_path]`
|
||||
- **Use this if you get 500 errors when editing devices!**
|
||||
- Use this if you get 500 errors when editing devices!
|
||||
- **fix_systemd_services.sh** - **Fix Systemd Services for API Binaries** ⭐
|
||||
- Use this if RustDesk services still use original binaries!
|
||||
- **fix_database.py** - Fix database.rs imports and change_id function
|
||||
|
||||
- **fix_systemd_services.sh** - **Fix Systemd Services for API Binaries** ⭐ NEW
|
||||
- Scans for all RustDesk systemd services (hbbs.service, rustdesksignal.service, etc.)
|
||||
- Updates ExecStart to use API-enabled binaries (hbbs-v8-api, hbbr-v8-api)
|
||||
- Creates backup of original service files
|
||||
- Usage: `sudo ./fix_systemd_services.sh [rustdesk_path]`
|
||||
- Example: `sudo ./fix_systemd_services.sh /opt/rustdesk`
|
||||
- **Use this if RustDesk services still use original binaries after installation!**
|
||||
### Patching Tools
|
||||
- **patch_rendezvous.py** - Patch rendezvous_server.rs to call touch_peer on RegisterPeer
|
||||
- **patch_id_change.py** - Patch rendezvous_server.rs for ID change via old_id field
|
||||
- **patch_peer_remove.py** - Patch peer.rs to add remove function
|
||||
- **patch_database_simple.py** - Add change_id function to database.rs
|
||||
|
||||
### Development Scripts
|
||||
- **update.ps1** - PowerShell update script (Windows development environment)
|
||||
- Alternative to update.sh for Windows
|
||||
- Updates web console components
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix database.rs imports and change_id function"""
|
||||
|
||||
with open("database.rs", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Fix import
|
||||
old_import = '''use sqlx::Row;\\nuse sqlx::{'''
|
||||
new_import = '''use sqlx::{
|
||||
Row,'''
|
||||
|
||||
content = content.replace(old_import, new_import)
|
||||
|
||||
# Also fix the get call and closure
|
||||
old_code = ''' let prev: Option<String> = r.get("previous_ids");
|
||||
prev.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default()'''
|
||||
|
||||
new_code = ''' let prev: Option<String> = r.get("previous_ids");
|
||||
prev.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default()'''
|
||||
|
||||
# Just rewrite the function properly
|
||||
old_func = ''' /// Change device ID in database
|
||||
pub async fn change_id(&self, old_id: &str, new_id: &str) -> ResultType<()> {
|
||||
let old = old_id.to_string();
|
||||
let new = new_id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
let mut opt = SqliteConnectOptions::from_str(&url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
// Get current previous_ids
|
||||
let row = sqlx::query("SELECT previous_ids FROM peer WHERE id = ?")
|
||||
.bind(&old)
|
||||
.fetch_optional(&mut conn)
|
||||
.await?;
|
||||
|
||||
let mut prev_ids: Vec<String> = if let Some(r) = row {
|
||||
let prev: Option<String> = r.get("previous_ids");
|
||||
prev.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Add old_id to history
|
||||
if !prev_ids.contains(&old) {
|
||||
prev_ids.push(old.clone());
|
||||
}
|
||||
|
||||
let prev_json = serde_json::to_string(&prev_ids).unwrap_or("[]".to_string());
|
||||
|
||||
// Update ID
|
||||
sqlx::query("UPDATE peer SET id = ?, previous_ids = ?, id_changed_at = datetime('now') WHERE id = ?")
|
||||
.bind(&new)
|
||||
.bind(&prev_json)
|
||||
.bind(&old)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::info!("Database: Changed ID {} -> {}", old, new);
|
||||
Ok(())
|
||||
}'''
|
||||
|
||||
new_func = ''' /// Change device ID in database
|
||||
pub async fn change_id(&self, old_id: &str, new_id: &str) -> ResultType<()> {
|
||||
let old = old_id.to_string();
|
||||
let new = new_id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
let mut opt = SqliteConnectOptions::from_str(&url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
// Get current previous_ids
|
||||
let row: Option<(Option<String>,)> = sqlx::query_as("SELECT previous_ids FROM peer WHERE id = ?")
|
||||
.bind(&old)
|
||||
.fetch_optional(&mut conn)
|
||||
.await?;
|
||||
|
||||
let mut prev_ids: Vec<String> = match row {
|
||||
Some((Some(s),)) => serde_json::from_str(&s).unwrap_or_default(),
|
||||
_ => Vec::new()
|
||||
};
|
||||
|
||||
// Add old_id to history
|
||||
if !prev_ids.contains(&old) {
|
||||
prev_ids.push(old.clone());
|
||||
}
|
||||
|
||||
let prev_json = serde_json::to_string(&prev_ids).unwrap_or("[]".to_string());
|
||||
|
||||
// Update ID
|
||||
sqlx::query("UPDATE peer SET id = ?, previous_ids = ?, id_changed_at = datetime('now') WHERE id = ?")
|
||||
.bind(&new)
|
||||
.bind(&prev_json)
|
||||
.bind(&old)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::info!("Database: Changed ID {} -> {}", old, new);
|
||||
Ok(())
|
||||
}'''
|
||||
|
||||
content = content.replace(old_func, new_func)
|
||||
|
||||
with open("database.rs", "w") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Fixed database.rs")
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch database.rs to add change_id function - simplified version"""
|
||||
|
||||
with open("database.rs", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Find place to add change_id function (after is_device_banned)
|
||||
marker = ''' pub async fn is_device_banned(&self, id: &str) -> ResultType<bool> {'''
|
||||
|
||||
new_function = ''' /// Change device ID in database (for ID change feature)
|
||||
pub async fn change_id(&self, old_id: &str, new_id: &str) -> ResultType<()> {
|
||||
let old = old_id.to_string();
|
||||
let new = new_id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
let mut opt = SqliteConnectOptions::from_str(&url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
// Simple update - just change the ID and record timestamp
|
||||
// The previous_ids tracking is already handled in http_api.rs
|
||||
sqlx::query("UPDATE peer SET id = ?, id_changed_at = datetime('now') WHERE id = ?")
|
||||
.bind(&new)
|
||||
.bind(&old)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::info!("Database: Changed ID {} -> {}", old, new);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
''' + marker
|
||||
|
||||
if marker in content:
|
||||
content = content.replace(marker, new_function)
|
||||
with open("database.rs", "w") as f:
|
||||
f.write(content)
|
||||
print("SUCCESS: Added change_id function to database.rs")
|
||||
else:
|
||||
print("ERROR: Could not find target marker in database.rs")
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch rendezvous_server.rs to support ID change via old_id field in RegisterPk"""
|
||||
|
||||
with open("rendezvous_server.rs", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the RegisterPk handler and add old_id support
|
||||
old_code = ''' Some(rendezvous_message::Union::RegisterPk(rk)) => {
|
||||
if rk.uuid.is_empty() || rk.pk.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let id = rk.id;'''
|
||||
|
||||
new_code = ''' Some(rendezvous_message::Union::RegisterPk(rk)) => {
|
||||
if rk.uuid.is_empty() || rk.pk.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle ID change request (old_id is set)
|
||||
if !rk.old_id.is_empty() {
|
||||
log::info!("ID change request: {} -> {}", rk.old_id, rk.id);
|
||||
|
||||
// Validate new ID format
|
||||
if rk.id.len() < 6 || rk.id.len() > 16 {
|
||||
log::warn!("ID change rejected: invalid new ID length");
|
||||
return send_rk_res(socket, addr, register_pk_response::Result::INVALID_ID_FORMAT).await;
|
||||
}
|
||||
|
||||
// Check if new ID already exists
|
||||
if self.pm.is_in_memory(&rk.id).await {
|
||||
log::warn!("ID change rejected: new ID {} already exists", rk.id);
|
||||
return send_rk_res(socket, addr, register_pk_response::Result::ID_EXISTS).await;
|
||||
}
|
||||
|
||||
// Check if old ID exists and UUID matches
|
||||
if let Some(old_peer) = self.pm.get_in_memory(&rk.old_id).await {
|
||||
let old_uuid = old_peer.read().await.uuid.clone();
|
||||
if old_uuid != rk.uuid {
|
||||
log::warn!("ID change rejected: UUID mismatch for {}", rk.old_id);
|
||||
return send_rk_res(socket, addr, UUID_MISMATCH).await;
|
||||
}
|
||||
|
||||
// Perform ID change in database
|
||||
match self.pm.db.change_id(&rk.old_id, &rk.id).await {
|
||||
Ok(_) => {
|
||||
log::info!("ID changed successfully: {} -> {}", rk.old_id, rk.id);
|
||||
// Remove old peer from memory, it will re-register with new ID
|
||||
self.pm.remove(&rk.old_id).await;
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_register_pk_response(RegisterPkResponse {
|
||||
result: register_pk_response::Result::OK.into(),
|
||||
..Default::default()
|
||||
});
|
||||
return socket.send(&msg_out, addr).await;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("ID change failed: {}", e);
|
||||
return send_rk_res(socket, addr, register_pk_response::Result::SERVER_ERROR).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("ID change rejected: old ID {} not found", rk.old_id);
|
||||
return send_rk_res(socket, addr, register_pk_response::Result::SERVER_ERROR).await;
|
||||
}
|
||||
}
|
||||
|
||||
let id = rk.id;'''
|
||||
|
||||
if old_code in content:
|
||||
content = content.replace(old_code, new_code)
|
||||
with open("rendezvous_server.rs", "w") as f:
|
||||
f.write(content)
|
||||
print("SUCCESS: Patched RegisterPk handler to support ID change")
|
||||
else:
|
||||
print("ERROR: Could not find target code block")
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch peer.rs to add remove function"""
|
||||
|
||||
with open("peer.rs", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Find place to add remove function (after is_in_memory)
|
||||
marker = ''' #[inline]
|
||||
pub(crate) async fn is_in_memory(&self, id: &str) -> bool {
|
||||
self.map.read().await.contains_key(id)
|
||||
}'''
|
||||
|
||||
new_code = ''' #[inline]
|
||||
pub(crate) async fn is_in_memory(&self, id: &str) -> bool {
|
||||
self.map.read().await.contains_key(id)
|
||||
}
|
||||
|
||||
/// Remove peer from memory (used during ID change)
|
||||
pub(crate) async fn remove(&self, id: &str) {
|
||||
self.map.write().await.remove(id);
|
||||
log::debug!("Removed peer {} from memory", id);
|
||||
}'''
|
||||
|
||||
if marker in content:
|
||||
content = content.replace(marker, new_code)
|
||||
with open("peer.rs", "w") as f:
|
||||
f.write(content)
|
||||
print("SUCCESS: Added remove function to peer.rs")
|
||||
else:
|
||||
print("ERROR: Could not find target marker in peer.rs")
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch rendezvous_server.rs to call touch_peer on RegisterPeer"""
|
||||
import re
|
||||
|
||||
with open("rendezvous_server.rs", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Add touch_peer call before creating the response message in update_addr
|
||||
old = '''let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_register_peer_response'''
|
||||
|
||||
new = '''// Update database status for this peer
|
||||
self.pm.touch_peer(&id).await;
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_register_peer_response'''
|
||||
|
||||
content = content.replace(old, new)
|
||||
|
||||
with open("rendezvous_server.rs", "w") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Patched rendezvous_server.rs to call touch_peer!")
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for ID change API endpoint
|
||||
|
||||
Usage:
|
||||
python test_change_id.py [old_id] [new_id]
|
||||
|
||||
Note: Configure API_KEY before running (get from /opt/rustdesk/.api_key)
|
||||
"""
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
old_id = sys.argv[1] if len(sys.argv) > 1 else "TESTID002"
|
||||
new_id = sys.argv[2] if len(sys.argv) > 2 else "TESTID003"
|
||||
|
||||
# Get API key from environment or file
|
||||
api_key = os.environ.get('HBBS_API_KEY', '')
|
||||
if not api_key:
|
||||
print("ERROR: Set HBBS_API_KEY environment variable")
|
||||
print(" Linux: export HBBS_API_KEY=$(cat /opt/rustdesk/.api_key)")
|
||||
print(" Windows: $env:HBBS_API_KEY = Get-Content C:\\rustdesk\\.api_key")
|
||||
sys.exit(1)
|
||||
|
||||
r = requests.post(
|
||||
f'http://localhost:21114/api/peers/{old_id}/change-id',
|
||||
json={'new_id': new_id},
|
||||
headers={'X-API-Key': api_key}
|
||||
)
|
||||
print(f"Status: {r.status_code}")
|
||||
print(f"Response: {r.text}")
|
||||
@@ -0,0 +1,302 @@
|
||||
# BetterDesk - Building from Source
|
||||
|
||||
This guide explains how to build BetterDesk enhanced binaries from source code.
|
||||
|
||||
## Overview
|
||||
|
||||
BetterDesk is built on top of the official RustDesk Server with additional modifications:
|
||||
- HTTP API for device management
|
||||
- Real-time online status tracking
|
||||
- Device banning capabilities
|
||||
- Enhanced database schema
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
# Interactive build
|
||||
./build-betterdesk.sh
|
||||
|
||||
# Automatic build with defaults
|
||||
./build-betterdesk.sh --auto
|
||||
```
|
||||
|
||||
### Windows
|
||||
```powershell
|
||||
# Interactive build
|
||||
.\build-betterdesk.ps1
|
||||
|
||||
# Automatic build with defaults
|
||||
.\build-betterdesk.ps1 -Auto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual Build Process
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### Linux
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev pkg-config libssl-dev git
|
||||
|
||||
# Install Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source ~/.cargo/env
|
||||
```
|
||||
|
||||
#### Windows
|
||||
1. Install [Rust](https://rustup.rs/)
|
||||
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with C++ support
|
||||
3. Install [Git](https://git-scm.com/)
|
||||
|
||||
### Step 1: Clone RustDesk Server
|
||||
|
||||
```bash
|
||||
# Clone specific version
|
||||
git clone --depth 1 --branch 1.1.14 https://github.com/rustdesk/rustdesk-server.git
|
||||
cd rustdesk-server
|
||||
|
||||
# Initialize submodules
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Step 2: Apply BetterDesk Modifications
|
||||
|
||||
Copy the modification files from `hbbs-patch-v2/src/` to the RustDesk source:
|
||||
|
||||
```bash
|
||||
# From the Rustdesk-FreeConsole directory
|
||||
cp hbbs-patch-v2/src/main.rs rustdesk-server/src/
|
||||
cp hbbs-patch-v2/src/http_api.rs rustdesk-server/src/
|
||||
cp hbbs-patch-v2/src/database.rs rustdesk-server/src/
|
||||
cp hbbs-patch-v2/src/peer.rs rustdesk-server/src/
|
||||
```
|
||||
|
||||
### Step 3: Build Binaries
|
||||
|
||||
```bash
|
||||
cd rustdesk-server
|
||||
|
||||
# Build Signal Server (hbbs)
|
||||
cargo build --release -p hbbs
|
||||
|
||||
# Build Relay Server (hbbr)
|
||||
cargo build --release -p hbbr
|
||||
```
|
||||
|
||||
### Step 4: Locate Binaries
|
||||
|
||||
After successful build, binaries are located in:
|
||||
- `target/release/hbbs` (Linux) or `target/release/hbbs.exe` (Windows)
|
||||
- `target/release/hbbr` (Linux) or `target/release/hbbr.exe` (Windows)
|
||||
|
||||
---
|
||||
|
||||
## Cross-Compilation
|
||||
|
||||
### Linux ARM64 (from x86_64)
|
||||
|
||||
```bash
|
||||
# Install cross-compiler
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
|
||||
# Add Rust target
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
# Configure linker
|
||||
cat >> .cargo/config.toml << EOF
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
EOF
|
||||
|
||||
# Build
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p hbbs
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p hbbr
|
||||
```
|
||||
|
||||
### Windows from Linux (using cross)
|
||||
|
||||
```bash
|
||||
# Install cross
|
||||
cargo install cross
|
||||
|
||||
# Build Windows binaries
|
||||
cross build --release --target x86_64-pc-windows-gnu -p hbbs
|
||||
cross build --release --target x86_64-pc-windows-gnu -p hbbr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modification Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `main.rs` | Entry point with `--api-port` argument, HTTP API startup |
|
||||
| `http_api.rs` | Full HTTP API implementation (list, online status, ban) |
|
||||
| `database.rs` | Database operations with BetterDesk extensions |
|
||||
| `peer.rs` | Peer management with additional fields |
|
||||
|
||||
### Key Modifications in main.rs
|
||||
|
||||
```rust
|
||||
// Added command-line argument
|
||||
#[arg(long, value_name = "PORT", help = "HTTP API port for BetterDesk Console")]
|
||||
api_port: Option<u16>,
|
||||
|
||||
// API startup in main()
|
||||
if let Some(port) = opt.api_port {
|
||||
let db_path = db_dir.clone();
|
||||
tokio::spawn(async move {
|
||||
http_api::start_api_server(port, db_path).await;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features in http_api.rs
|
||||
|
||||
```rust
|
||||
// Endpoints
|
||||
GET /api/peers - List all registered devices
|
||||
GET /api/peers/online - List currently online devices
|
||||
POST /api/peers/{id}/ban - Ban a device
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GitHub Actions CI/CD
|
||||
|
||||
The project includes automated builds via GitHub Actions.
|
||||
|
||||
### Automatic Triggers
|
||||
- Changes to `hbbs-patch-v2/src/**` on the `main` branch
|
||||
- Changes to `.github/workflows/build.yml`
|
||||
|
||||
### Manual Trigger
|
||||
1. Go to Actions tab in GitHub
|
||||
2. Select "Build BetterDesk Binaries"
|
||||
3. Click "Run workflow"
|
||||
4. Optionally select RustDesk version and release options
|
||||
|
||||
### Artifacts
|
||||
Built binaries are available as workflow artifacts for 30 days.
|
||||
|
||||
---
|
||||
|
||||
## Build Script Options
|
||||
|
||||
### Linux (build-betterdesk.sh)
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--auto` | Non-interactive mode with defaults |
|
||||
| `--clean` | Clean build directory |
|
||||
| `--version VERSION` | Specify RustDesk version |
|
||||
| `--platform PLATFORM` | Target: linux-x64, linux-arm64, windows-x64 |
|
||||
| `--help` | Show help |
|
||||
|
||||
### Windows (build-betterdesk.ps1)
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-Auto` | Non-interactive mode |
|
||||
| `-Clean` | Clean build directory |
|
||||
| `-Version VERSION` | Specify RustDesk version |
|
||||
| `-Platform PLATFORM` | Target platform |
|
||||
| `-Help` | Show help |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails with SQLite Errors
|
||||
|
||||
```bash
|
||||
# Linux - Install SQLite dev package
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
# Or use bundled SQLite
|
||||
cargo build --release --features bundled
|
||||
```
|
||||
|
||||
### Missing OpenSSL
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo apt-get install libssl-dev pkg-config
|
||||
|
||||
# Windows: Usually bundled, but if needed:
|
||||
# Install via vcpkg or use openssl-sys
|
||||
```
|
||||
|
||||
### Axum Version Mismatch
|
||||
|
||||
BetterDesk uses axum 0.5.x. If upgrading RustDesk base version, check Cargo.toml for axum version changes.
|
||||
|
||||
### Cross-Compilation Fails
|
||||
|
||||
```bash
|
||||
# Ensure correct linker is configured
|
||||
# For ARM64:
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verifying Builds
|
||||
|
||||
### Check Binary Version
|
||||
|
||||
```bash
|
||||
./hbbs-linux-x86_64 --version
|
||||
```
|
||||
|
||||
### Verify API Port Support
|
||||
|
||||
```bash
|
||||
./hbbs-linux-x86_64 --help | grep api-port
|
||||
```
|
||||
|
||||
Should show:
|
||||
```
|
||||
--api-port <PORT> HTTP API port for BetterDesk Console
|
||||
```
|
||||
|
||||
### Test API Functionality
|
||||
|
||||
```bash
|
||||
# Start server with API
|
||||
./hbbs-linux-x86_64 -k _ --api-port 21114 &
|
||||
|
||||
# Test API
|
||||
curl http://localhost:21114/api/peers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing Modifications
|
||||
|
||||
1. Make changes in `hbbs-patch-v2/src/`
|
||||
2. Test locally with build scripts
|
||||
3. Submit PR with updated source files
|
||||
4. CI will automatically build and test
|
||||
|
||||
See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for full guidelines.
|
||||
|
||||
---
|
||||
|
||||
## Version Tracking
|
||||
|
||||
| Component | Version |
|
||||
|-----------|---------|
|
||||
| RustDesk Server Base | 1.1.14 |
|
||||
| BetterDesk HTTP API | 2.0.0 |
|
||||
| Install Scripts | 1.5.x |
|
||||
|
||||
When updating RustDesk base version:
|
||||
1. Test API compatibility
|
||||
2. Update build scripts
|
||||
3. Rebuild all platform binaries
|
||||
4. Update CHECKSUMS.md
|
||||
+29
-16
@@ -2,27 +2,40 @@
|
||||
|
||||
This directory contains comprehensive documentation for BetterDesk Console.
|
||||
|
||||
## Contents
|
||||
## Quick Start
|
||||
- **[INSTALLATION_V1.4.0.md](INSTALLATION_V1.4.0.md)** - Installation guide
|
||||
- **[UPDATE_GUIDE.md](UPDATE_GUIDE.md)** - Updating existing installations
|
||||
- **[TROUBLESHOOTING_EN.md](TROUBLESHOOTING_EN.md)** - Common issues & solutions
|
||||
|
||||
### Release Documentation
|
||||
- **CHANGELOG.md** - Complete version history with all changes
|
||||
- **RELEASE_NOTES_v1.2.0.md** - Detailed release notes for v1.2.0
|
||||
- **DEPRECATION_NOTICE.md** - Information about deprecated features
|
||||
## Docker
|
||||
- **[DOCKER_SUPPORT.md](DOCKER_SUPPORT.md)** - Docker installation guide
|
||||
- **[DOCKER_TROUBLESHOOTING.md](DOCKER_TROUBLESHOOTING.md)** - Docker-specific issues
|
||||
|
||||
### Guides
|
||||
- **CONTRIBUTING.md** - Contribution guidelines for developers
|
||||
- **UPDATE_GUIDE.md** - How to update existing installations
|
||||
- **UPDATE_REFERENCE.md** - Detailed update procedures
|
||||
- **QUICKSTART_UPDATE.md** - Quick update instructions
|
||||
## Features
|
||||
- **[CLIENT_GENERATOR.md](CLIENT_GENERATOR.md)** - Custom client generator
|
||||
- **[CLIENT_GENERATOR_QUICKSTART_EN.md](CLIENT_GENERATOR_QUICKSTART_EN.md)** - Client generator quick start
|
||||
- **[ID_CHANGE_FEATURE.md](ID_CHANGE_FEATURE.md)** - Device ID change feature
|
||||
- **[STATUS_TRACKING_v3.md](STATUS_TRACKING_v3.md)** - Device status tracking system
|
||||
|
||||
### Development
|
||||
- **DEVELOPMENT_ROADMAP.md** - Future features and development plans
|
||||
- **GITHUB_RELEASE_CHECKLIST.md** - Checklist for creating GitHub releases
|
||||
## Performance
|
||||
- **[GPU_OPTIMIZATION_EN.md](GPU_OPTIMIZATION_EN.md)** - GPU optimization
|
||||
- **[GPU_FIX_QUICKSTART_EN.md](GPU_FIX_QUICKSTART_EN.md)** - Quick GPU fix guide
|
||||
- **[OPTIMIZATION_SUMMARY_EN.md](OPTIMIZATION_SUMMARY_EN.md)** - Performance optimization summary
|
||||
|
||||
## Main Documentation
|
||||
## Security & Troubleshooting
|
||||
- **[KEY_TROUBLESHOOTING.md](KEY_TROUBLESHOOTING.md)** - Key and encryption issues
|
||||
- **[PORT_SECURITY.md](PORT_SECURITY.md)** - Port configuration & security
|
||||
- **[QUICK_FIX_EN.md](QUICK_FIX_EN.md)** - Quick fixes for common issues
|
||||
|
||||
For general information, see the main [README.md](../README.md) in the root directory.
|
||||
## Development
|
||||
- **[BUILD_GUIDE.md](BUILD_GUIDE.md)** - Building from source
|
||||
- **[CONTRIBUTING.md](CONTRIBUTING.md)** - Contribution guidelines
|
||||
- **[PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)** - Project structure overview
|
||||
- **[CHANGELOG.md](CHANGELOG.md)** - Version history
|
||||
|
||||
## Additional
|
||||
- **[TRANSLATION_SUMMARY.md](TRANSLATION_SUMMARY.md)** - Documentation translation status
|
||||
|
||||
## HBBS Patch Documentation
|
||||
|
||||
HBBS-specific documentation is located in the [hbbs-patch/](../hbbs-patch/) directory.
|
||||
HBBS-specific documentation is in [hbbs-patch-v2/](../hbbs-patch-v2/).
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# BetterDesk Server v3.0 - Ulepszony System Statusu
|
||||
|
||||
## Przegląd Zmian
|
||||
|
||||
Wersja 3.0 wprowadza znacząco ulepszony system śledzenia statusu urządzeń:
|
||||
|
||||
### Kluczowe Ulepszenia
|
||||
|
||||
| Cecha | v2.0 | v3.0 |
|
||||
|-------|------|------|
|
||||
| Timeout wykrywania offline | Stały 30s | **Konfigurowalny (domyślnie 15s)** |
|
||||
| Interwał heartbeat | Stały 5s | **Konfigurowalny (domyślnie 3s)** |
|
||||
| Statusy urządzeń | Online/Offline | **Online/Degraded/Critical/Offline** |
|
||||
| Statystyki | Brak | **Pełne statystyki z API** |
|
||||
| Konfiguracja runtime | Brak | **Przez API i zmienne środowiskowe** |
|
||||
|
||||
---
|
||||
|
||||
## Nowe Statusy Urządzeń
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ ONLINE │ Wszystko OK, heartbeat otrzymany │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ DEGRADED │ 2-3 pominięte heartbeaty │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ CRITICAL │ 4+ pominięte heartbeaty, wkrótce offline │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ OFFLINE │ Przekroczony timeout, brak połączenia │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
### Zmienne Środowiskowe
|
||||
|
||||
| Zmienna | Domyślnie | Opis |
|
||||
|---------|-----------|------|
|
||||
| `PEER_TIMEOUT_SECS` | 15 | Sekundy do uznania za offline |
|
||||
| `HEARTBEAT_INTERVAL_SECS` | 3 | Interwał sprawdzania statusu |
|
||||
| `HEARTBEAT_WARNING_THRESHOLD` | 2 | Ilość pominiętych HB dla DEGRADED |
|
||||
| `HEARTBEAT_CRITICAL_THRESHOLD` | 4 | Ilość pominiętych HB dla CRITICAL |
|
||||
| `HEARTBEAT_VERBOSE` | false | Szczegółowe logowanie |
|
||||
| `DB_SYNC_INTERVAL_SECS` | 5 | Interwał synchronizacji z DB |
|
||||
|
||||
### Konfiguracja przez Systemd
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/rustdesksignal.service
|
||||
[Service]
|
||||
Environment="PEER_TIMEOUT_SECS=10"
|
||||
Environment="HEARTBEAT_INTERVAL_SECS=2"
|
||||
Environment="HEARTBEAT_VERBOSE=true"
|
||||
```
|
||||
|
||||
### Konfiguracja przez Docker
|
||||
|
||||
```yaml
|
||||
services:
|
||||
hbbs:
|
||||
environment:
|
||||
- PEER_TIMEOUT_SECS=10
|
||||
- HEARTBEAT_INTERVAL_SECS=2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nowe Endpointy API
|
||||
|
||||
### GET /api/config
|
||||
Pobiera aktualną konfigurację serwera.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"peer_timeout_secs": 15,
|
||||
"heartbeat_interval_secs": 3,
|
||||
"warning_threshold": 2,
|
||||
"critical_threshold": 4,
|
||||
"verbose_logging": false,
|
||||
"db_sync_interval_secs": 5
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/config
|
||||
Aktualizuje konfigurację (zapisuje do bazy danych).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"key": "peer_timeout_secs",
|
||||
"value": "10"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": true,
|
||||
"version": "3.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/peers/stats
|
||||
Zwraca szczegółowe statystyki urządzeń.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total_peers": 150,
|
||||
"online_peers": 87,
|
||||
"offline_peers": 63,
|
||||
"banned_peers": 2,
|
||||
"total_heartbeats": 1548762
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/server/stats
|
||||
Statystyki serwera.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uptime_seconds": 86400,
|
||||
"memory_usage_mb": 45.2,
|
||||
"api_version": "3.0.0",
|
||||
"db_version": "3.0"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nowe Kolumny Bazy Danych
|
||||
|
||||
Migracja automatycznie dodaje:
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| `last_heartbeat` | DATETIME | Czas ostatniego heartbeatu |
|
||||
| `heartbeat_count` | INTEGER | Licznik heartbeatów |
|
||||
| `previous_ids` | TEXT (JSON) | Historia poprzednich ID |
|
||||
| `id_changed_at` | DATETIME | Data zmiany ID |
|
||||
|
||||
### Tabela server_config
|
||||
|
||||
Nowa tabela dla konfiguracji runtime:
|
||||
|
||||
```sql
|
||||
CREATE TABLE server_config (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rekomendowane Ustawienia
|
||||
|
||||
### Środowisko Produkcyjne (stabilność)
|
||||
```bash
|
||||
PEER_TIMEOUT_SECS=15
|
||||
HEARTBEAT_INTERVAL_SECS=3
|
||||
```
|
||||
|
||||
### Środowisko Krytyczne (szybka reakcja)
|
||||
```bash
|
||||
PEER_TIMEOUT_SECS=8
|
||||
HEARTBEAT_INTERVAL_SECS=2
|
||||
```
|
||||
|
||||
### Środowisko z Wolnymi Łączami
|
||||
```bash
|
||||
PEER_TIMEOUT_SECS=30
|
||||
HEARTBEAT_INTERVAL_SECS=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migracja z v2.0
|
||||
|
||||
1. **Backup bazy danych:**
|
||||
```bash
|
||||
cp /opt/rustdesk/db_v2.sqlite3 /opt/rustdesk/db_v2.sqlite3.backup
|
||||
```
|
||||
|
||||
2. **Wymiana binarek:**
|
||||
```bash
|
||||
sudo ./install-improved.sh --fix
|
||||
```
|
||||
|
||||
3. **Restart usług:**
|
||||
```bash
|
||||
sudo systemctl restart rustdesksignal rustdeskrelay
|
||||
```
|
||||
|
||||
Migracja schematu bazy danych wykonuje się automatycznie przy pierwszym uruchomieniu.
|
||||
|
||||
---
|
||||
|
||||
## Pliki Źródłowe v3.0
|
||||
|
||||
| Plik | Opis |
|
||||
|------|------|
|
||||
| `peer_v3.rs` | Ulepszony system statusu peer |
|
||||
| `database_v3.rs` | Rozszerzona obsługa bazy danych |
|
||||
| `http_api_v3.rs` | Nowe endpointy API |
|
||||
|
||||
### Użycie w Kompilacji
|
||||
|
||||
Aby użyć nowych plików, zamień oryginalne:
|
||||
|
||||
```bash
|
||||
cp hbbs-patch-v2/src/peer_v3.rs hbbs-patch-v2/src/peer.rs
|
||||
cp hbbs-patch-v2/src/database_v3.rs hbbs-patch-v2/src/database.rs
|
||||
cp hbbs-patch-v2/src/http_api_v3.rs hbbs-patch-v2/src/http_api.rs
|
||||
```
|
||||
|
||||
Następnie skompiluj:
|
||||
```bash
|
||||
./build-betterdesk.sh --auto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Znane Ograniczenia
|
||||
|
||||
1. **Zmiany konfiguracji przez API** wymagają restartu dla niektórych parametrów
|
||||
2. **WebSocket real-time push** nie jest jeszcze zaimplementowany (planowane w v3.1)
|
||||
3. **Zmiana ID urządzeń** jest przygotowana w bazie danych ale wymaga zmian w kliencie RustDesk
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v3.0.0 (2026-02-06)
|
||||
- Konfigurowalny timeout offline
|
||||
- Konfigurowalny interwał heartbeat
|
||||
- Statusy pośrednie (DEGRADED, CRITICAL)
|
||||
- Nowe endpointy API (/api/config, /api/peers/stats, /api/server/stats)
|
||||
- Automatyczne migracje bazy danych
|
||||
- Tabela server_config dla runtime configuration
|
||||
- Batch operations dla lepszej wydajności
|
||||
+143
-294
@@ -1,10 +1,9 @@
|
||||
// Enhanced database.rs with retry logic and circuit breaker
|
||||
use async_trait::async_trait;
|
||||
use hbb_common::{log, ResultType};
|
||||
use hbb_common::{log, ResultType, tokio};
|
||||
use sqlx::{
|
||||
sqlite::SqliteConnectOptions, ConnectOptions, Connection, Error as SqlxError, SqliteConnection,
|
||||
};
|
||||
use std::{ops::DerefMut, str::FromStr, sync::Arc, sync::atomic::{AtomicBool, AtomicU32, Ordering}};
|
||||
use std::{ops::DerefMut, str::FromStr, sync::Arc};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
type Pool = deadpool::managed::Pool<DbPool>;
|
||||
@@ -17,38 +16,11 @@ pub struct DbPool {
|
||||
impl deadpool::managed::Manager for DbPool {
|
||||
type Type = SqliteConnection;
|
||||
type Error = SqlxError;
|
||||
|
||||
async fn create(&self) -> Result<SqliteConnection, SqlxError> {
|
||||
let mut opt = SqliteConnectOptions::from_str(&self.url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
|
||||
// Retry logic with exponential backoff
|
||||
let mut attempts = 0;
|
||||
let max_attempts = 3;
|
||||
|
||||
loop {
|
||||
match SqliteConnection::connect_with(&opt).await {
|
||||
Ok(conn) => {
|
||||
if attempts > 0 {
|
||||
log::info!("Database connection established after {} attempts", attempts + 1);
|
||||
}
|
||||
return Ok(conn);
|
||||
}
|
||||
Err(e) => {
|
||||
attempts += 1;
|
||||
if attempts >= max_attempts {
|
||||
log::error!("Failed to connect to database after {} attempts: {}", max_attempts, e);
|
||||
return Err(e);
|
||||
}
|
||||
let wait_ms = 100 * (2_u64.pow(attempts));
|
||||
log::warn!("Database connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||
attempts, max_attempts, wait_ms, e);
|
||||
tokio::time::sleep(Duration::from_millis(wait_ms)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
SqliteConnection::connect_with(&opt).await
|
||||
}
|
||||
|
||||
async fn recycle(
|
||||
&self,
|
||||
obj: &mut SqliteConnection,
|
||||
@@ -57,75 +29,10 @@ impl deadpool::managed::Manager for DbPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Circuit breaker to prevent database overload
|
||||
#[derive(Clone)]
|
||||
struct CircuitBreaker {
|
||||
failure_count: Arc<AtomicU32>,
|
||||
last_failure: Arc<tokio::sync::Mutex<Option<Instant>>>,
|
||||
is_open: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl CircuitBreaker {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
failure_count: Arc::new(AtomicU32::new(0)),
|
||||
last_failure: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
is_open: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call<F, T, E>(&self, f: F) -> Result<T, E>
|
||||
where
|
||||
F: std::future::Future<Output = Result<T, E>>,
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
// Check if circuit is open
|
||||
if self.is_open.load(Ordering::Relaxed) {
|
||||
let mut last = self.last_failure.lock().await;
|
||||
if let Some(time) = *last {
|
||||
// Auto-recover after 30 seconds
|
||||
if time.elapsed() > Duration::from_secs(30) {
|
||||
log::info!("Circuit breaker: attempting recovery");
|
||||
self.is_open.store(false, Ordering::Relaxed);
|
||||
self.failure_count.store(0, Ordering::Relaxed);
|
||||
*last = None;
|
||||
} else {
|
||||
log::warn!("Circuit breaker is OPEN - blocking database operations");
|
||||
// For now, still try but log the state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match f.await {
|
||||
Ok(result) => {
|
||||
// Success - reset failure count
|
||||
let prev = self.failure_count.swap(0, Ordering::Relaxed);
|
||||
if prev > 0 {
|
||||
log::info!("Database operation succeeded, failure count reset");
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
Err(e) => {
|
||||
let count = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
log::error!("Database operation failed (failure #{}) : {}", count, e);
|
||||
|
||||
// Open circuit after 5 consecutive failures
|
||||
if count >= 5 {
|
||||
log::error!("Circuit breaker OPENED after {} consecutive failures", count);
|
||||
self.is_open.store(true, Ordering::Relaxed);
|
||||
*self.last_failure.lock().await = Some(Instant::now());
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pool: Pool,
|
||||
circuit_breaker: CircuitBreaker,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -142,94 +49,61 @@ pub struct Peer {
|
||||
impl Database {
|
||||
pub async fn new(url: &str) -> ResultType<Database> {
|
||||
if !std::path::Path::new(url).exists() {
|
||||
log::info!("Creating new database file: {}", url);
|
||||
std::fs::File::create(url).ok();
|
||||
}
|
||||
|
||||
let n: usize = std::env::var("MAX_DATABASE_CONNECTIONS")
|
||||
.unwrap_or_else(|_| "5".to_owned()) // Increased default from 1 to 5
|
||||
.unwrap_or_else(|_| "5".to_owned()) // Increased from 1 to 5
|
||||
.parse()
|
||||
.unwrap_or(5);
|
||||
|
||||
log::info!("Initializing database with {} connection(s)", n);
|
||||
|
||||
log::info!("MAX_DATABASE_CONNECTIONS={}", n);
|
||||
let pool = Pool::new(
|
||||
DbPool {
|
||||
url: url.to_owned(),
|
||||
},
|
||||
n,
|
||||
);
|
||||
|
||||
// Test connection with retry
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
match pool.get().await {
|
||||
Ok(_) => {
|
||||
log::info!("Database connection pool initialized successfully");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
attempts += 1;
|
||||
if attempts >= 5 {
|
||||
log::error!("Failed to initialize database pool after {} attempts", attempts);
|
||||
return Err(e.into());
|
||||
}
|
||||
log::warn!("Database pool test failed (attempt {}/5), retrying...", attempts);
|
||||
tokio::time::sleep(Duration::from_millis(500 * attempts as u64)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = pool.get().await?; // test
|
||||
let db = Database {
|
||||
pool,
|
||||
circuit_breaker: CircuitBreaker::new(),
|
||||
pool,
|
||||
url: url.to_owned(),
|
||||
};
|
||||
|
||||
db.create_tables().await?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
async fn create_tables(&self) -> ResultType<()> {
|
||||
log::debug!("Creating database tables if not exist...");
|
||||
|
||||
self.circuit_breaker.call(async {
|
||||
sqlx::query!(
|
||||
"
|
||||
create table if not exists peer (
|
||||
guid blob primary key not null,
|
||||
id varchar(100) not null,
|
||||
uuid blob not null,
|
||||
pk blob not null,
|
||||
created_at datetime not null default(current_timestamp),
|
||||
user blob,
|
||||
status tinyint,
|
||||
note varchar(300),
|
||||
info text not null
|
||||
) without rowid;
|
||||
create unique index if not exists index_peer_id on peer (id);
|
||||
create index if not exists index_peer_user on peer (user);
|
||||
create index if not exists index_peer_created_at on peer (created_at);
|
||||
create index if not exists index_peer_status on peer (status);
|
||||
sqlx::query!(
|
||||
"
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await
|
||||
}).await?;
|
||||
|
||||
log::debug!("Database tables ready");
|
||||
create table if not exists peer (
|
||||
guid blob primary key not null,
|
||||
id varchar(100) not null,
|
||||
uuid blob not null,
|
||||
pk blob not null,
|
||||
created_at datetime not null default(current_timestamp),
|
||||
user blob,
|
||||
status tinyint,
|
||||
note varchar(300),
|
||||
info text not null
|
||||
) without rowid;
|
||||
create unique index if not exists index_peer_id on peer (id);
|
||||
create index if not exists index_peer_user on peer (user);
|
||||
create index if not exists index_peer_created_at on peer (created_at);
|
||||
create index if not exists index_peer_status on peer (status);
|
||||
"
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_peer(&self, id: &str) -> ResultType<Option<Peer>> {
|
||||
self.circuit_breaker.call(async {
|
||||
Ok(sqlx::query_as!(
|
||||
Peer,
|
||||
"select guid, id, uuid, pk, user, status, info from peer where id = ?",
|
||||
id
|
||||
)
|
||||
.fetch_optional(self.pool.get().await?.deref_mut())
|
||||
.await?)
|
||||
}).await
|
||||
Ok(sqlx::query_as!(
|
||||
Peer,
|
||||
"select guid, id, uuid, pk, user, status, info from peer where id = ?",
|
||||
id
|
||||
)
|
||||
.fetch_optional(self.pool.get().await?.deref_mut())
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn insert_peer(
|
||||
@@ -240,21 +114,18 @@ impl Database {
|
||||
info: &str,
|
||||
) -> ResultType<Vec<u8>> {
|
||||
let guid = uuid::Uuid::new_v4().as_bytes().to_vec();
|
||||
|
||||
self.circuit_breaker.call(async {
|
||||
sqlx::query!(
|
||||
"insert into peer(guid, id, uuid, pk, info) values(?, ?, ?, ?, ?)",
|
||||
guid,
|
||||
id,
|
||||
uuid,
|
||||
pk,
|
||||
info
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
|
||||
Ok(guid.clone())
|
||||
}).await
|
||||
sqlx::query!(
|
||||
"insert into peer(guid, id, uuid, pk, info, status, last_online) values(?, ?, ?, ?, ?, 1, datetime('now'))",
|
||||
guid,
|
||||
id,
|
||||
uuid,
|
||||
pk,
|
||||
info
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
log::info!("New peer {} inserted with status=1 (online)", id);
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
pub async fn update_pk(
|
||||
@@ -264,151 +135,129 @@ impl Database {
|
||||
pk: &[u8],
|
||||
info: &str,
|
||||
) -> ResultType<()> {
|
||||
self.circuit_breaker.call(async {
|
||||
sqlx::query!(
|
||||
"update peer set id=?, pk=?, info=? where guid=?",
|
||||
id,
|
||||
pk,
|
||||
info,
|
||||
guid
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}).await
|
||||
sqlx::query!(
|
||||
"update peer set id=?, pk=?, info=?, status=1, last_online=datetime('now') where guid=?",
|
||||
id,
|
||||
pk,
|
||||
info,
|
||||
guid
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
log::debug!("Peer {} updated pk, set status=1, last_online=now", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a device is banned in the database (with retry logic)
|
||||
pub async fn is_device_banned(&self, id: &str) -> ResultType<bool> {
|
||||
use sqlx::Row;
|
||||
/// Set device status to online and update last_online timestamp
|
||||
/// Called when device registers or sends heartbeat
|
||||
pub async fn set_online(&self, id: &str) {
|
||||
let id_owned = id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
self.circuit_breaker.call(async {
|
||||
let r = sqlx::query("SELECT is_banned FROM peer WHERE id = ? AND is_deleted = 0")
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
|
||||
if let Some(row) = r {
|
||||
let banned: i32 = row.try_get("is_banned")?;
|
||||
Ok(banned == 1)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Set peer as online in database (async, non-blocking)
|
||||
pub async fn set_online(&self, id: &str) -> ResultType<()> {
|
||||
let id = id.to_owned();
|
||||
let db = self.clone();
|
||||
|
||||
// Fire and forget - don't block the caller
|
||||
// Fire and forget - don't block the main flow
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = db._set_online_internal(&id).await {
|
||||
log::error!("Failed to set peer {} as online: {}", id, e);
|
||||
if let Err(e) = Self::set_online_internal(&url, &id_owned).await {
|
||||
log::warn!("Failed to set {} online: {}", id_owned, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn set_online_internal(url: &str, id: &str) -> ResultType<()> {
|
||||
let mut opt = SqliteConnectOptions::from_str(url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 1, last_online = datetime('now') WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::trace!("Set {} online, last_online updated", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _set_online_internal(&self, id: &str) -> ResultType<()> {
|
||||
self.circuit_breaker.call(async {
|
||||
sqlx::query("UPDATE peer SET last_online = datetime('now') WHERE id = ? AND is_deleted = 0")
|
||||
.bind(id)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
|
||||
/// Set peer as offline in database (async, non-blocking)
|
||||
pub async fn set_offline(&self, id: &str) -> ResultType<()> {
|
||||
let id = id.to_owned();
|
||||
let db = self.clone();
|
||||
/// Set device status to offline
|
||||
/// Called when device times out or disconnects
|
||||
pub async fn set_offline(&self, id: &str) {
|
||||
let id_owned = id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
// Fire and forget - don't block the caller
|
||||
// Fire and forget
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = db._set_offline_internal(&id).await {
|
||||
log::error!("Failed to set peer {} as offline: {}", id, e);
|
||||
if let Err(e) = Self::set_offline_internal(&url, &id_owned).await {
|
||||
log::warn!("Failed to set {} offline: {}", id_owned, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn set_offline_internal(url: &str, id: &str) -> ResultType<()> {
|
||||
let mut opt = SqliteConnectOptions::from_str(url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 0 WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::debug!("Set {} offline", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _set_offline_internal(&self, id: &str) -> ResultType<()> {
|
||||
self.circuit_breaker.call(async {
|
||||
sqlx::query("UPDATE peer SET last_online = NULL WHERE id = ? AND is_deleted = 0")
|
||||
.bind(id)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}).await
|
||||
/// Set all devices offline - called on server startup to reset stale status
|
||||
pub async fn set_all_offline(&self) -> ResultType<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 0 WHERE status = 1"
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
|
||||
log::info!("Reset all devices to offline status on startup");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Batch update online status for multiple peers (more efficient)
|
||||
/// Set multiple devices offline in a single transaction (batch operation)
|
||||
pub async fn batch_set_offline(&self, ids: &[String]) -> ResultType<()> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("Batch setting {} peers as offline", ids.len());
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
self.circuit_breaker.call(async {
|
||||
let mut conn = self.pool.get().await?;
|
||||
let mut tx = conn.begin().await?;
|
||||
|
||||
for id in ids {
|
||||
sqlx::query("UPDATE peer SET last_online = NULL WHERE id = ? AND is_deleted = 0")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hbb_common::tokio;
|
||||
|
||||
#[test]
|
||||
fn test_insert() {
|
||||
insert();
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn insert() {
|
||||
let db = super::Database::new("test_v2.sqlite3").await.unwrap();
|
||||
let mut jobs = vec![];
|
||||
|
||||
for i in 0..1000 {
|
||||
let cloned = db.clone();
|
||||
let id = i.to_string();
|
||||
let a = tokio::spawn(async move {
|
||||
let empty_vec = Vec::new();
|
||||
cloned
|
||||
.insert_peer(&id, &empty_vec, &empty_vec, "")
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
jobs.push(a);
|
||||
for id in ids {
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 0 WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.execute(conn.deref_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
for i in 0..1000 {
|
||||
let cloned = db.clone();
|
||||
let id = i.to_string();
|
||||
let a = tokio::spawn(async move {
|
||||
cloned.get_peer(&id).await.unwrap();
|
||||
});
|
||||
jobs.push(a);
|
||||
}
|
||||
log::debug!("Batch set {} devices offline", ids.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a device is banned in the database
|
||||
/// Returns true if device has is_banned=1, false otherwise
|
||||
/// Uses synchronous rusqlite to avoid nested Tokio runtime panic
|
||||
pub async fn is_device_banned(&self, id: &str) -> ResultType<bool> {
|
||||
let db_path = self.url.clone();
|
||||
let id = id.to_string();
|
||||
|
||||
hbb_common::futures::future::join_all(jobs).await;
|
||||
// Use spawn_blocking to run synchronous rusqlite code
|
||||
let result = tokio::task::spawn_blocking(move || -> ResultType<bool> {
|
||||
let conn = rusqlite::Connection::open(&db_path)?;
|
||||
let mut stmt = conn.prepare("SELECT is_banned FROM peer WHERE id = ?")?;
|
||||
let is_banned: Option<i32> = stmt
|
||||
.query_row([&id], |row| row.get(0))
|
||||
.unwrap_or(None);
|
||||
Ok(is_banned == Some(1))
|
||||
}).await?;
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use hbb_common::{log, ResultType, tokio};
|
||||
use sqlx::{
|
||||
sqlite::SqliteConnectOptions, ConnectOptions, Connection, Error as SqlxError, SqliteConnection,
|
||||
};
|
||||
use std::{ops::DerefMut, str::FromStr, sync::Arc};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
type Pool = deadpool::managed::Pool<DbPool>;
|
||||
|
||||
pub struct DbPool {
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl deadpool::managed::Manager for DbPool {
|
||||
type Type = SqliteConnection;
|
||||
type Error = SqlxError;
|
||||
async fn create(&self) -> Result<SqliteConnection, SqlxError> {
|
||||
let mut opt = SqliteConnectOptions::from_str(&self.url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
SqliteConnection::connect_with(&opt).await
|
||||
}
|
||||
async fn recycle(
|
||||
&self,
|
||||
obj: &mut SqliteConnection,
|
||||
) -> deadpool::managed::RecycleResult<SqlxError> {
|
||||
Ok(obj.ping().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pool: Pool,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Peer {
|
||||
pub guid: Vec<u8>,
|
||||
pub id: String,
|
||||
pub uuid: Vec<u8>,
|
||||
pub pk: Vec<u8>,
|
||||
pub user: Option<Vec<u8>>,
|
||||
pub info: String,
|
||||
pub status: Option<i64>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn new(url: &str) -> ResultType<Database> {
|
||||
if !std::path::Path::new(url).exists() {
|
||||
std::fs::File::create(url).ok();
|
||||
}
|
||||
let n: usize = std::env::var("MAX_DATABASE_CONNECTIONS")
|
||||
.unwrap_or_else(|_| "5".to_owned()) // Increased from 1 to 5
|
||||
.parse()
|
||||
.unwrap_or(5);
|
||||
log::info!("MAX_DATABASE_CONNECTIONS={}", n);
|
||||
let pool = Pool::new(
|
||||
DbPool {
|
||||
url: url.to_owned(),
|
||||
},
|
||||
n,
|
||||
);
|
||||
let _ = pool.get().await?; // test
|
||||
let db = Database {
|
||||
pool,
|
||||
url: url.to_owned(),
|
||||
};
|
||||
db.create_tables().await?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
async fn create_tables(&self) -> ResultType<()> {
|
||||
sqlx::query!(
|
||||
"
|
||||
create table if not exists peer (
|
||||
guid blob primary key not null,
|
||||
id varchar(100) not null,
|
||||
uuid blob not null,
|
||||
pk blob not null,
|
||||
created_at datetime not null default(current_timestamp),
|
||||
user blob,
|
||||
status tinyint,
|
||||
note varchar(300),
|
||||
info text not null
|
||||
) without rowid;
|
||||
create unique index if not exists index_peer_id on peer (id);
|
||||
create index if not exists index_peer_user on peer (user);
|
||||
create index if not exists index_peer_created_at on peer (created_at);
|
||||
create index if not exists index_peer_status on peer (status);
|
||||
"
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_peer(&self, id: &str) -> ResultType<Option<Peer>> {
|
||||
Ok(sqlx::query_as!(
|
||||
Peer,
|
||||
"select guid, id, uuid, pk, user, status, info from peer where id = ?",
|
||||
id
|
||||
)
|
||||
.fetch_optional(self.pool.get().await?.deref_mut())
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn insert_peer(
|
||||
&self,
|
||||
id: &str,
|
||||
uuid: &[u8],
|
||||
pk: &[u8],
|
||||
info: &str,
|
||||
) -> ResultType<Vec<u8>> {
|
||||
let guid = uuid::Uuid::new_v4().as_bytes().to_vec();
|
||||
sqlx::query!(
|
||||
"insert into peer(guid, id, uuid, pk, info, status, last_online) values(?, ?, ?, ?, ?, 1, datetime('now'))",
|
||||
guid,
|
||||
id,
|
||||
uuid,
|
||||
pk,
|
||||
info
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
log::info!("New peer {} inserted with status=1 (online)", id);
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
pub async fn update_pk(
|
||||
&self,
|
||||
guid: &Vec<u8>,
|
||||
id: &str,
|
||||
pk: &[u8],
|
||||
info: &str,
|
||||
) -> ResultType<()> {
|
||||
sqlx::query!(
|
||||
"update peer set id=?, pk=?, info=?, status=1, last_online=datetime('now') where guid=?",
|
||||
id,
|
||||
pk,
|
||||
info,
|
||||
guid
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
log::debug!("Peer {} updated pk, set status=1, last_online=now", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set device status to online and update last_online timestamp
|
||||
/// Called when device registers or sends heartbeat
|
||||
pub async fn set_online(&self, id: &str) {
|
||||
let id_owned = id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
// Fire and forget - don't block the main flow
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::set_online_internal(&url, &id_owned).await {
|
||||
log::warn!("Failed to set {} online: {}", id_owned, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn set_online_internal(url: &str, id: &str) -> ResultType<()> {
|
||||
let mut opt = SqliteConnectOptions::from_str(url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 1, last_online = datetime('now') WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::trace!("Set {} online, last_online updated", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set device status to offline
|
||||
/// Called when device times out or disconnects
|
||||
pub async fn set_offline(&self, id: &str) {
|
||||
let id_owned = id.to_string();
|
||||
let url = self.url.clone();
|
||||
|
||||
// Fire and forget
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::set_offline_internal(&url, &id_owned).await {
|
||||
log::warn!("Failed to set {} offline: {}", id_owned, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn set_offline_internal(url: &str, id: &str) -> ResultType<()> {
|
||||
let mut opt = SqliteConnectOptions::from_str(url).unwrap();
|
||||
opt.log_statements(log::LevelFilter::Debug);
|
||||
let mut conn = SqliteConnection::connect_with(&opt).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 0 WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
log::debug!("Set {} offline", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set all devices offline - called on server startup to reset stale status
|
||||
pub async fn set_all_offline(&self) -> ResultType<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 0 WHERE status = 1"
|
||||
)
|
||||
.execute(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
|
||||
log::info!("Reset all devices to offline status on startup");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set multiple devices offline in a single transaction (batch operation)
|
||||
pub async fn batch_set_offline(&self, ids: &[String]) -> ResultType<()> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = self.pool.get().await?;
|
||||
|
||||
for id in ids {
|
||||
sqlx::query!(
|
||||
"UPDATE peer SET status = 0 WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.execute(conn.deref_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
log::debug!("Batch set {} devices offline", ids.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a device is banned in the database
|
||||
/// Returns true if device has is_banned=1, false otherwise
|
||||
/// Uses synchronous rusqlite to avoid nested Tokio runtime panic
|
||||
pub async fn is_device_banned(&self, id: &str) -> ResultType<bool> {
|
||||
let db_path = self.url.clone();
|
||||
let id = id.to_string();
|
||||
|
||||
// Use spawn_blocking to run synchronous rusqlite code
|
||||
let result = tokio::task::spawn_blocking(move || -> ResultType<bool> {
|
||||
let conn = rusqlite::Connection::open(&db_path)?;
|
||||
let mut stmt = conn.prepare("SELECT is_banned FROM peer WHERE id = ?")?;
|
||||
let is_banned: Option<i32> = stmt
|
||||
.query_row([&id], |row| row.get(0))
|
||||
.unwrap_or(None);
|
||||
Ok(is_banned == Some(1))
|
||||
}).await?;
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
+192
-13
@@ -1,14 +1,17 @@
|
||||
// BetterDesk HTTP API v2.0.0
|
||||
// BetterDesk HTTP API v2.1.0
|
||||
// Compatible with axum 0.5 and sqlx 0.6
|
||||
// Added: POST /api/peers/:id/change-id endpoint
|
||||
|
||||
extern crate serde_json;
|
||||
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
extract::{Extension, Path},
|
||||
http::{StatusCode, HeaderMap},
|
||||
response::Json,
|
||||
routing::get,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::{sqlite::SqlitePool, Row};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
@@ -47,6 +50,19 @@ struct HealthStatus {
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChangeIdRequest {
|
||||
new_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChangeIdResponse {
|
||||
old_id: String,
|
||||
new_id: String,
|
||||
changed_at: String,
|
||||
previous_ids: Vec<String>,
|
||||
}
|
||||
|
||||
fn verify_api_key(headers: &HeaderMap, state: &ApiState) -> Result<(), StatusCode> {
|
||||
match headers.get("X-API-Key") {
|
||||
Some(key) => {
|
||||
@@ -68,16 +84,43 @@ fn get_current_timestamp() -> String {
|
||||
chrono::Utc::now().to_rfc3339()
|
||||
}
|
||||
|
||||
/// Check if a timestamp string is within the last N seconds (default 60s)
|
||||
/// Supports formats: "YYYY-MM-DD HH:MM:SS" (SQLite) and RFC3339
|
||||
fn is_online_recently(timestamp: &Option<String>, timeout_secs: i64) -> bool {
|
||||
match timestamp {
|
||||
Some(ts) => {
|
||||
// Try SQLite format first: "2026-02-06 14:00:27"
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M:%S") {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let diff = now.signed_duration_since(dt);
|
||||
return diff.num_seconds() < timeout_secs;
|
||||
}
|
||||
// Try RFC3339 format
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts) {
|
||||
let now = chrono::Utc::now();
|
||||
let diff = now.signed_duration_since(dt);
|
||||
return diff.num_seconds() < timeout_secs;
|
||||
}
|
||||
// If we can't parse, assume offline
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default timeout for online status (60 seconds)
|
||||
const ONLINE_TIMEOUT_SECS: i64 = 60;
|
||||
|
||||
async fn get_online_peers(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<Arc<ApiState>>,
|
||||
) -> Result<Json<ApiResponse<Vec<PeerStatus>>>, StatusCode> {
|
||||
verify_api_key(&headers, &state)?;
|
||||
|
||||
hbb_common::log::debug!("API: Fetching online peers");
|
||||
hbb_common::log::debug!("API: Fetching all peers");
|
||||
|
||||
match sqlx::query(
|
||||
"SELECT id, note, last_online FROM peer WHERE (status IS NULL OR status = 0) AND is_deleted = 0"
|
||||
"SELECT id, note, last_online FROM peer WHERE is_deleted = 0"
|
||||
)
|
||||
.fetch_all(&state.db_pool)
|
||||
.await
|
||||
@@ -89,7 +132,7 @@ async fn get_online_peers(
|
||||
let id: String = row.get("id");
|
||||
let note: Option<String> = row.get("note");
|
||||
let last_online: Option<String> = row.get("last_online");
|
||||
let online = last_online.is_some();
|
||||
let online = is_online_recently(&last_online, ONLINE_TIMEOUT_SECS);
|
||||
|
||||
peers.push(PeerStatus {
|
||||
id,
|
||||
@@ -160,7 +203,7 @@ async fn get_peer_details(
|
||||
let id: String = row.get("id");
|
||||
let note: Option<String> = row.get("note");
|
||||
let last_online: Option<String> = row.get("last_online");
|
||||
let online = last_online.is_some();
|
||||
let online = is_online_recently(&last_online, ONLINE_TIMEOUT_SECS);
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
@@ -194,6 +237,140 @@ async fn get_peer_details(
|
||||
}
|
||||
}
|
||||
|
||||
/// Change peer ID (admin endpoint)
|
||||
/// POST /api/peers/:id/change-id
|
||||
/// Body: { "new_id": "NEW123456" }
|
||||
async fn change_peer_id(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<Arc<ApiState>>,
|
||||
Path(old_id): Path<String>,
|
||||
Json(payload): Json<ChangeIdRequest>,
|
||||
) -> Result<Json<ApiResponse<ChangeIdResponse>>, StatusCode> {
|
||||
verify_api_key(&headers, &state)?;
|
||||
|
||||
let new_id = payload.new_id.trim().to_uppercase();
|
||||
let old_id = old_id.trim().to_uppercase();
|
||||
|
||||
hbb_common::log::info!("API: Change ID request: {} -> {}", old_id, new_id);
|
||||
|
||||
// Validate new ID format (6-16 chars, alphanumeric/dash/underscore)
|
||||
if new_id.len() < 6 || new_id.len() > 16 {
|
||||
return Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some("New ID must be 6-16 characters".to_string()),
|
||||
timestamp: get_current_timestamp(),
|
||||
}));
|
||||
}
|
||||
|
||||
if !new_id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
|
||||
return Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some("New ID can only contain letters, numbers, dash and underscore".to_string()),
|
||||
timestamp: get_current_timestamp(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Check if old_id exists
|
||||
let old_peer = sqlx::query("SELECT previous_ids FROM peer WHERE id = ? AND is_deleted = 0")
|
||||
.bind(&old_id)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
let old_row = match old_peer {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => {
|
||||
return Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(format!("Peer '{}' not found", old_id)),
|
||||
timestamp: get_current_timestamp(),
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
return Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(format!("Database error: {}", e)),
|
||||
timestamp: get_current_timestamp(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Check if new_id already exists
|
||||
let new_exists = sqlx::query("SELECT 1 FROM peer WHERE id = ? AND is_deleted = 0")
|
||||
.bind(&new_id)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
if let Ok(Some(_)) = new_exists {
|
||||
return Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(format!("ID '{}' is already in use", new_id)),
|
||||
timestamp: get_current_timestamp(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Get and update previous_ids
|
||||
let previous_ids_str: String = old_row.try_get("previous_ids").unwrap_or_default();
|
||||
let mut previous_ids: Vec<String> = if previous_ids_str.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
serde_json::from_str(&previous_ids_str).unwrap_or_default()
|
||||
};
|
||||
previous_ids.push(old_id.clone());
|
||||
let updated_history = serde_json::to_string(&previous_ids).unwrap_or_default();
|
||||
|
||||
let now = get_current_timestamp();
|
||||
|
||||
// Perform the update
|
||||
let result = sqlx::query(
|
||||
"UPDATE peer SET id = ?, previous_ids = ?, id_changed_at = ? WHERE id = ? AND is_deleted = 0"
|
||||
)
|
||||
.bind(&new_id)
|
||||
.bind(&updated_history)
|
||||
.bind(&now)
|
||||
.bind(&old_id)
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(res) if res.rows_affected() > 0 => {
|
||||
hbb_common::log::info!("API: ID changed successfully: {} -> {}", old_id, new_id);
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
data: Some(ChangeIdResponse {
|
||||
old_id,
|
||||
new_id,
|
||||
changed_at: now,
|
||||
previous_ids,
|
||||
}),
|
||||
error: None,
|
||||
timestamp: get_current_timestamp(),
|
||||
}))
|
||||
}
|
||||
Ok(_) => {
|
||||
Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some("No rows affected".to_string()),
|
||||
timestamp: get_current_timestamp(),
|
||||
}))
|
||||
}
|
||||
Err(e) => {
|
||||
hbb_common::log::error!("API: Failed to change ID: {}", e);
|
||||
Ok(Json(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(format!("Failed to change ID: {}", e)),
|
||||
timestamp: get_current_timestamp(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_or_generate_api_key() -> String {
|
||||
if let Ok(key) = fs::read_to_string(API_KEY_FILE) {
|
||||
let key = key.trim().to_string();
|
||||
@@ -243,7 +420,7 @@ pub async fn start_api_server(db_path: String, port: u16) -> Result<(), Box<dyn
|
||||
|
||||
// Try to connect, but don't fail if DB doesn't exist yet
|
||||
let connect_options = match SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path)) {
|
||||
Ok(opts) => opts.read_only(true).create_if_missing(false),
|
||||
Ok(opts) => opts.read_only(false).create_if_missing(false),
|
||||
Err(e) => {
|
||||
hbb_common::log::error!("API: Invalid database path: {}", e);
|
||||
return Err(e.into());
|
||||
@@ -257,7 +434,7 @@ pub async fn start_api_server(db_path: String, port: u16) -> Result<(), Box<dyn
|
||||
// Wait and retry
|
||||
hbb_common::tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
let opts = SqliteConnectOptions::from_str(&format!("sqlite://{}", db_path))?
|
||||
.read_only(true)
|
||||
.read_only(false)
|
||||
.create_if_missing(false);
|
||||
SqlitePool::connect_with(opts).await?
|
||||
}
|
||||
@@ -277,6 +454,7 @@ pub async fn start_api_server(db_path: String, port: u16) -> Result<(), Box<dyn
|
||||
.route("/api/health", get(health_check))
|
||||
.route("/api/peers", get(get_online_peers))
|
||||
.route("/api/peers/:id", get(get_peer_details))
|
||||
.route("/api/peers/:id/change-id", post(change_peer_id))
|
||||
.layer(Extension(state));
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
@@ -285,9 +463,10 @@ pub async fn start_api_server(db_path: String, port: u16) -> Result<(), Box<dyn
|
||||
hbb_common::log::info!("HTTP API Server on port {}", port);
|
||||
hbb_common::log::info!("========================================");
|
||||
hbb_common::log::info!("Endpoints:");
|
||||
hbb_common::log::info!(" GET /api/health");
|
||||
hbb_common::log::info!(" GET /api/peers");
|
||||
hbb_common::log::info!(" GET /api/peers/:id");
|
||||
hbb_common::log::info!(" GET /api/health");
|
||||
hbb_common::log::info!(" GET /api/peers");
|
||||
hbb_common::log::info!(" GET /api/peers/:id");
|
||||
hbb_common::log::info!(" POST /api/peers/:id/change-id");
|
||||
hbb_common::log::info!("========================================");
|
||||
|
||||
// axum 0.5 uses Server::bind
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// BetterDesk Enhanced Server v2.0.0
|
||||
// BetterDesk Enhanced Server v2.1.1
|
||||
// Based on RustDesk Server 1.1.14 with HTTP API
|
||||
// https://tools.ietf.org/rfc/rfc5128.txt
|
||||
//
|
||||
// Features:
|
||||
// - HTTP API with peer management
|
||||
// - Peer ID change with history tracking
|
||||
// - Database with soft-delete support
|
||||
|
||||
use flexi_logger::*;
|
||||
use hbb_common::{bail, config::RENDEZVOUS_PORT, ResultType, tokio};
|
||||
@@ -9,7 +14,7 @@ use hbbs::{common::*, *};
|
||||
mod http_api;
|
||||
|
||||
const RMEM: usize = 0;
|
||||
const API_PORT: u16 = 21120;
|
||||
const API_PORT: u16 = 21114;
|
||||
|
||||
fn main() -> ResultType<()> {
|
||||
let _logger = Logger::try_with_env_or_str("info")?
|
||||
@@ -30,7 +35,7 @@ fn main() -> ResultType<()> {
|
||||
-k, --key=[KEY] 'Only allow the client with the same key'
|
||||
-a, --api-port=[NUMBER(default={API_PORT})] 'Sets the HTTP API port'",
|
||||
);
|
||||
init_args(&args, "hbbs", "BetterDesk Enhanced Server v2.0.0");
|
||||
init_args(&args, "hbbs", "BetterDesk Enhanced Server v2.1.1");
|
||||
|
||||
let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::<i32>()?;
|
||||
if port < 3 {
|
||||
@@ -41,7 +46,7 @@ fn main() -> ResultType<()> {
|
||||
let api_port = get_arg("api-port").parse::<u16>().unwrap_or(API_PORT);
|
||||
|
||||
hbb_common::log::info!("========================================");
|
||||
hbb_common::log::info!(" BetterDesk Enhanced Server v2.0.0");
|
||||
hbb_common::log::info!(" BetterDesk Enhanced Server v2.1.1");
|
||||
hbb_common::log::info!(" Based on RustDesk Server 1.1.14");
|
||||
hbb_common::log::info!("========================================");
|
||||
hbb_common::log::info!(" Signal Port: {}", port);
|
||||
|
||||
+220
-196
@@ -1,4 +1,3 @@
|
||||
// Enhanced peer.rs with improved heartbeat and monitoring
|
||||
use crate::common::*;
|
||||
use crate::database;
|
||||
use hbb_common::{
|
||||
@@ -6,6 +5,7 @@ use hbb_common::{
|
||||
log,
|
||||
rendezvous_proto::*,
|
||||
tokio::sync::{Mutex, RwLock},
|
||||
tokio,
|
||||
ResultType,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@@ -26,6 +26,10 @@ pub const IP_CHANGE_DUR_X2: u64 = IP_CHANGE_DUR * 2;
|
||||
pub const DAY_SECONDS: u64 = 3600 * 24;
|
||||
pub const IP_BLOCK_DUR: u64 = 60;
|
||||
|
||||
// Status tracking constants
|
||||
const HEARTBEAT_TIMEOUT_SECS: u64 = 15; // Mark offline after 15s without heartbeat (was 30s)
|
||||
const CLEANUP_INTERVAL_SECS: u64 = 60; // Check for stale peers every 60s
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct PeerInfo {
|
||||
#[serde(default)]
|
||||
@@ -35,57 +39,44 @@ pub(crate) struct PeerInfo {
|
||||
pub(crate) struct Peer {
|
||||
pub(crate) socket_addr: SocketAddr,
|
||||
pub(crate) last_reg_time: Instant,
|
||||
pub(crate) last_heartbeat: Instant, // New: track last heartbeat separately
|
||||
pub(crate) guid: Vec<u8>,
|
||||
pub(crate) uuid: Bytes,
|
||||
pub(crate) pk: Bytes,
|
||||
pub(crate) info: PeerInfo,
|
||||
pub(crate) reg_pk: (u32, Instant),
|
||||
pub(crate) connection_quality: ConnectionQuality, // New: track connection quality
|
||||
// Track last heartbeat for online status
|
||||
pub(crate) last_heartbeat: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConnectionQuality {
|
||||
pub(crate) last_response_time: Duration,
|
||||
pub(crate) missed_heartbeats: u32,
|
||||
pub(crate) total_heartbeats: u64,
|
||||
}
|
||||
|
||||
impl Default for ConnectionQuality {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_response_time: Duration::from_millis(0),
|
||||
missed_heartbeats: 0,
|
||||
total_heartbeats: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
impl Default for Peer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
socket_addr: "0.0.0.0:0".parse().unwrap(),
|
||||
last_reg_time: get_expired_time(),
|
||||
last_heartbeat: get_expired_time(),
|
||||
guid: Vec::new(),
|
||||
uuid: Bytes::new(),
|
||||
pk: Bytes::new(),
|
||||
info: Default::default(),
|
||||
reg_pk: (0, get_expired_time()),
|
||||
connection_quality: Default::default(),
|
||||
last_heartbeat: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type LockPeer = Arc<RwLock<Peer>>;
|
||||
|
||||
/// Statistics about online peers
|
||||
pub struct PeerStats {
|
||||
pub total: usize,
|
||||
pub healthy: usize,
|
||||
pub degraded: usize,
|
||||
pub critical: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PeerMap {
|
||||
map: Arc<RwLock<HashMap<String, LockPeer>>>,
|
||||
pub(crate) db: database::Database,
|
||||
last_cleanup: Arc<RwLock<Instant>>,
|
||||
}
|
||||
|
||||
impl PeerMap {
|
||||
@@ -104,15 +95,105 @@ impl PeerMap {
|
||||
}
|
||||
db
|
||||
});
|
||||
log::info!("DB_URL={}", db);
|
||||
|
||||
let database = database::Database::new(&db).await?;
|
||||
|
||||
// Reset all devices to offline on startup (clean slate)
|
||||
if let Err(e) = database.set_all_offline().await {
|
||||
log::warn!("Failed to reset devices to offline: {}", e);
|
||||
}
|
||||
|
||||
log::info!("Initializing PeerMap with DB: {}", db);
|
||||
let pm = Self {
|
||||
map: Default::default(),
|
||||
db: database::Database::new(&db).await?,
|
||||
last_cleanup: Arc::new(RwLock::new(Instant::now())),
|
||||
db: database,
|
||||
};
|
||||
|
||||
// Start background task to check for stale peers and set them offline
|
||||
let pm_clone = pm.clone();
|
||||
tokio::spawn(async move {
|
||||
pm_clone.status_cleanup_loop().await;
|
||||
});
|
||||
|
||||
Ok(pm)
|
||||
}
|
||||
|
||||
/// Background loop to detect stale peers and mark them offline
|
||||
async fn status_cleanup_loop(&self) {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(CLEANUP_INTERVAL_SECS));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let now = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(HEARTBEAT_TIMEOUT_SECS);
|
||||
let mut stale_peers = Vec::new();
|
||||
|
||||
// Find stale peers
|
||||
{
|
||||
let map = self.map.read().await;
|
||||
for (id, peer) in map.iter() {
|
||||
let peer_data = peer.read().await;
|
||||
if now.duration_since(peer_data.last_heartbeat) > timeout {
|
||||
stale_peers.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set stale peers offline and remove from memory
|
||||
if !stale_peers.is_empty() {
|
||||
log::info!("Marking {} stale peers as offline", stale_peers.len());
|
||||
|
||||
// Batch update database
|
||||
if let Err(e) = self.db.batch_set_offline(&stale_peers).await {
|
||||
log::error!("Failed to batch set offline: {}", e);
|
||||
}
|
||||
|
||||
// Remove from memory map
|
||||
{
|
||||
let mut map = self.map.write().await;
|
||||
for id in &stale_peers {
|
||||
map.remove(id);
|
||||
log::debug!("Removed stale peer {} from memory", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup IP blocker and IP changes maps
|
||||
self.cleanup_ip_maps().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanup stale entries from IP maps
|
||||
async fn cleanup_ip_maps(&self) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Cleanup IP_BLOCKER
|
||||
{
|
||||
let mut blocker = IP_BLOCKER.lock().await;
|
||||
blocker.retain(|_, ((_, t1), (_, t2))| {
|
||||
now.duration_since(*t1).as_secs() < IP_BLOCK_DUR &&
|
||||
now.duration_since(*t2).as_secs() < DAY_SECONDS
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup IP_CHANGES
|
||||
{
|
||||
let mut changes = IP_CHANGES.lock().await;
|
||||
changes.retain(|_, (t, _)| {
|
||||
now.duration_since(*t).as_secs() < IP_CHANGE_DUR_X2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Update heartbeat and set device online
|
||||
pub(crate) async fn touch_peer(&self, id: &str) {
|
||||
if let Some(peer) = self.map.read().await.get(id) {
|
||||
peer.write().await.last_heartbeat = Instant::now();
|
||||
}
|
||||
// Update database status
|
||||
self.db.set_online(id).await;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn update_pk(
|
||||
@@ -124,7 +205,22 @@ impl PeerMap {
|
||||
pk: Bytes,
|
||||
ip: String,
|
||||
) -> register_pk_response::Result {
|
||||
log::debug!("update_pk {} {:?}", id, addr);
|
||||
log::info!("update_pk {} {:?} {:?} {:?}", id, addr, uuid, pk);
|
||||
|
||||
// BAN CHECK: Verify device is not banned before registration
|
||||
match self.db.is_device_banned(&id).await {
|
||||
Ok(true) => {
|
||||
log::warn!("Registration REJECTED for device {}: DEVICE IS BANNED", id);
|
||||
self.map.write().await.remove(&id);
|
||||
return register_pk_response::Result::UUID_MISMATCH;
|
||||
}
|
||||
Ok(false) => {
|
||||
log::debug!("Ban check passed for device {}", id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check ban status for device {}: {}. Allowing (fail-open)", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
let (info_str, guid) = {
|
||||
let mut w = peer.write().await;
|
||||
@@ -132,11 +228,8 @@ impl PeerMap {
|
||||
w.uuid = uuid.clone();
|
||||
w.pk = pk.clone();
|
||||
w.last_reg_time = Instant::now();
|
||||
w.last_heartbeat = Instant::now();
|
||||
w.last_heartbeat = Instant::now(); // Update heartbeat on registration
|
||||
w.info.ip = ip;
|
||||
w.connection_quality.total_heartbeats += 1;
|
||||
w.connection_quality.missed_heartbeats = 0; // Reset on successful registration
|
||||
|
||||
(
|
||||
serde_json::to_string(&w.info).unwrap_or_default(),
|
||||
w.guid.clone(),
|
||||
@@ -151,7 +244,6 @@ impl PeerMap {
|
||||
}
|
||||
Ok(guid) => {
|
||||
peer.write().await.guid = guid;
|
||||
log::info!("New peer registered: {}", id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -159,13 +251,11 @@ impl PeerMap {
|
||||
log::error!("db.update_pk failed: {}", err);
|
||||
return register_pk_response::Result::SERVER_ERROR;
|
||||
}
|
||||
log::debug!("Peer {} updated", id);
|
||||
log::info!("pk updated instead of insert");
|
||||
}
|
||||
|
||||
// Set peer status to online (async, non-blocking)
|
||||
if let Err(err) = self.db.set_online(&id).await {
|
||||
log::error!("db.set_online failed for {}: {}", id, err);
|
||||
}
|
||||
// Device just registered, mark as online
|
||||
self.db.set_online(&id).await;
|
||||
|
||||
register_pk_response::Result::OK
|
||||
}
|
||||
@@ -176,16 +266,21 @@ impl PeerMap {
|
||||
if p.is_some() {
|
||||
return p;
|
||||
} else if let Ok(Some(v)) = self.db.get_peer(id).await {
|
||||
// BAN CHECK: Do not load banned devices into memory
|
||||
if let Ok(true) = self.db.is_device_banned(id).await {
|
||||
log::warn!("Blocked loading banned device {} from database", id);
|
||||
return None;
|
||||
}
|
||||
let peer = Peer {
|
||||
guid: v.guid,
|
||||
uuid: v.uuid.into(),
|
||||
pk: v.pk.into(),
|
||||
info: serde_json::from_str::<PeerInfo>(&v.info).unwrap_or_default(),
|
||||
last_heartbeat: Instant::now(),
|
||||
..Default::default()
|
||||
};
|
||||
let peer = Arc::new(RwLock::new(peer));
|
||||
self.map.write().await.insert(id.to_owned(), peer.clone());
|
||||
log::debug!("Peer {} loaded from database", id);
|
||||
return Some(peer);
|
||||
}
|
||||
None
|
||||
@@ -215,141 +310,8 @@ impl PeerMap {
|
||||
self.map.read().await.contains_key(id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn set_offline(&self, id: &str) {
|
||||
if let Err(err) = self.db.set_offline(id).await {
|
||||
log::error!("db.set_offline failed for {}: {}", id, err);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced peer checking with configurable timeout and better logging
|
||||
pub(crate) async fn check_online_peers(&self) {
|
||||
// Get configurable timeout from environment (default 15 seconds)
|
||||
let timeout_secs = std::env::var("PEER_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(15); // Reduced from 20 to 15 seconds
|
||||
|
||||
let now = Instant::now();
|
||||
let mut offline_peers = Vec::new();
|
||||
let mut total_peers = 0;
|
||||
let mut degraded_peers = 0;
|
||||
|
||||
{
|
||||
let map = self.map.read().await;
|
||||
total_peers = map.len();
|
||||
|
||||
for (id, peer) in map.iter() {
|
||||
let p = peer.read().await;
|
||||
let last_heartbeat = p.last_heartbeat;
|
||||
let elapsed = now.duration_since(last_heartbeat).as_secs();
|
||||
|
||||
// Check connection quality
|
||||
if p.connection_quality.missed_heartbeats > 2 {
|
||||
degraded_peers += 1;
|
||||
log::warn!("Peer {} has degraded connection (missed {} heartbeats)",
|
||||
id, p.connection_quality.missed_heartbeats);
|
||||
}
|
||||
|
||||
if elapsed > timeout_secs {
|
||||
offline_peers.push((id.clone(), elapsed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !offline_peers.is_empty() {
|
||||
log::info!("Found {} offline peers (total: {}, degraded: {})",
|
||||
offline_peers.len(), total_peers, degraded_peers);
|
||||
|
||||
// Batch offline updates for better performance
|
||||
let ids: Vec<String> = offline_peers.iter().map(|(id, _)| id.clone()).collect();
|
||||
if let Err(e) = self.db.batch_set_offline(&ids).await {
|
||||
log::error!("Batch offline update failed: {}", e);
|
||||
// Fallback to individual updates
|
||||
for (id, elapsed) in &offline_peers {
|
||||
log::info!("Setting peer {} as offline (last seen {}s ago)", id, elapsed);
|
||||
self.set_offline(id).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from memory
|
||||
let mut map = self.map.write().await;
|
||||
for (id, _) in offline_peers {
|
||||
map.remove(&id);
|
||||
}
|
||||
} else if total_peers > 0 {
|
||||
log::debug!("All {} peers are online", total_peers);
|
||||
}
|
||||
|
||||
// Periodic cleanup of stale entries
|
||||
self.periodic_cleanup().await;
|
||||
}
|
||||
|
||||
/// Periodic cleanup of old entries to prevent memory leaks
|
||||
async fn periodic_cleanup(&self) {
|
||||
let mut last = self.last_cleanup.write().await;
|
||||
|
||||
// Run cleanup every 5 minutes
|
||||
if last.elapsed().as_secs() < 300 {
|
||||
return;
|
||||
}
|
||||
|
||||
log::info!("Running periodic cleanup...");
|
||||
*last = Instant::now();
|
||||
|
||||
// Cleanup IP blocker
|
||||
let mut ip_blocker = IP_BLOCKER.lock().await;
|
||||
let before = ip_blocker.len();
|
||||
ip_blocker.retain(|_, (a, b)| {
|
||||
a.1.elapsed().as_secs() <= IP_BLOCK_DUR
|
||||
|| b.1.elapsed().as_secs() <= DAY_SECONDS
|
||||
});
|
||||
let removed = before - ip_blocker.len();
|
||||
if removed > 0 {
|
||||
log::info!("Cleaned up {} entries from IP blocker", removed);
|
||||
}
|
||||
drop(ip_blocker);
|
||||
|
||||
// Cleanup IP changes
|
||||
let mut ip_changes = IP_CHANGES.lock().await;
|
||||
let before = ip_changes.len();
|
||||
ip_changes.retain(|_, v| v.0.elapsed().as_secs() < IP_CHANGE_DUR_X2 && v.1.len() > 1);
|
||||
let removed = before - ip_changes.len();
|
||||
if removed > 0 {
|
||||
log::info!("Cleaned up {} entries from IP changes tracker", removed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update heartbeat for a peer (lightweight operation)
|
||||
#[inline]
|
||||
pub(crate) async fn update_heartbeat(&self, id: &str) -> bool {
|
||||
if let Some(peer) = self.get_in_memory(id).await {
|
||||
let mut p = peer.write().await;
|
||||
p.last_heartbeat = Instant::now();
|
||||
p.connection_quality.total_heartbeats += 1;
|
||||
p.connection_quality.missed_heartbeats = 0;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a missed heartbeat
|
||||
#[inline]
|
||||
pub(crate) async fn record_missed_heartbeat(&self, id: &str) {
|
||||
if let Some(peer) = self.get_in_memory(id).await {
|
||||
let mut p = peer.write().await;
|
||||
p.connection_quality.missed_heartbeats += 1;
|
||||
if p.connection_quality.missed_heartbeats > 3 {
|
||||
log::warn!("Peer {} has missed {} consecutive heartbeats",
|
||||
id, p.connection_quality.missed_heartbeats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find peer ID by socket address
|
||||
#[inline]
|
||||
pub(crate) async fn find_by_addr(&self, addr: SocketAddr) -> Option<String> {
|
||||
/// Find device ID by socket address (for ban enforcement)
|
||||
pub(crate) async fn get_id_by_addr(&self, addr: SocketAddr) -> Option<String> {
|
||||
let map = self.map.read().await;
|
||||
for (id, peer) in map.iter() {
|
||||
let peer_addr = peer.read().await.socket_addr;
|
||||
@@ -360,36 +322,98 @@ impl PeerMap {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get statistics about current peers
|
||||
pub(crate) async fn get_stats(&self) -> PeerMapStats {
|
||||
/// Get statistics about online peers
|
||||
pub(crate) async fn get_stats(&self) -> PeerStats {
|
||||
let map = self.map.read().await;
|
||||
let total = map.len();
|
||||
let now = Instant::now();
|
||||
|
||||
let timeout_secs = std::env::var("PEER_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(15);
|
||||
let warning_threshold = std::env::var("HEARTBEAT_WARNING_THRESHOLD")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(2);
|
||||
let critical_threshold = std::env::var("HEARTBEAT_CRITICAL_THRESHOLD")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(4);
|
||||
let heartbeat_interval = std::env::var("HEARTBEAT_INTERVAL_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(3);
|
||||
|
||||
let mut healthy = 0;
|
||||
let mut degraded = 0;
|
||||
let mut critical = 0;
|
||||
|
||||
for (_, peer) in map.iter() {
|
||||
let p = peer.read().await;
|
||||
match p.connection_quality.missed_heartbeats {
|
||||
0..=1 => healthy += 1,
|
||||
2..=3 => degraded += 1,
|
||||
_ => critical += 1,
|
||||
for (_id, peer) in map.iter() {
|
||||
if let Ok(p) = peer.try_read() {
|
||||
let elapsed = now.duration_since(p.last_heartbeat).as_secs();
|
||||
if elapsed <= timeout_secs {
|
||||
let missed = elapsed / heartbeat_interval;
|
||||
if missed >= critical_threshold {
|
||||
critical += 1;
|
||||
} else if missed >= warning_threshold {
|
||||
degraded += 1;
|
||||
} else {
|
||||
healthy += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PeerMapStats {
|
||||
total,
|
||||
healthy,
|
||||
degraded,
|
||||
critical,
|
||||
PeerStats { total, healthy, degraded, critical }
|
||||
}
|
||||
|
||||
/// Check online peers and mark offline ones
|
||||
pub(crate) async fn check_online_peers(&self) {
|
||||
let timeout_secs = std::env::var("PEER_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(15);
|
||||
|
||||
let now = Instant::now();
|
||||
let mut offline_peers = Vec::new();
|
||||
let mut online_peers = Vec::new();
|
||||
|
||||
{
|
||||
let map = self.map.read().await;
|
||||
for (id, peer) in map.iter() {
|
||||
let p = peer.read().await;
|
||||
let elapsed = now.duration_since(p.last_heartbeat).as_secs();
|
||||
|
||||
if elapsed > timeout_secs {
|
||||
offline_peers.push(id.clone());
|
||||
} else {
|
||||
online_peers.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update online devices in database
|
||||
for id in &online_peers {
|
||||
self.db.set_online(id).await;
|
||||
}
|
||||
|
||||
// Mark offline devices
|
||||
if !offline_peers.is_empty() {
|
||||
log::info!("Setting {} peers as offline (timeout {}s)", offline_peers.len(), timeout_secs);
|
||||
|
||||
if let Err(e) = self.db.batch_set_offline(&offline_peers).await {
|
||||
log::error!("Batch offline update failed: {}", e);
|
||||
for id in &offline_peers {
|
||||
self.db.set_offline(id).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from memory
|
||||
let mut map = self.map.write().await;
|
||||
for id in offline_peers {
|
||||
map.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PeerMapStats {
|
||||
pub total: usize,
|
||||
pub healthy: usize,
|
||||
pub degraded: usize,
|
||||
pub critical: usize,
|
||||
}
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
use crate::common::*;
|
||||
use crate::database;
|
||||
use hbb_common::{
|
||||
bytes::Bytes,
|
||||
log,
|
||||
rendezvous_proto::*,
|
||||
tokio::sync::{Mutex, RwLock},
|
||||
tokio,
|
||||
ResultType,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, collections::HashSet, net::SocketAddr, sync::Arc, time::Instant};
|
||||
|
||||
type IpBlockMap = HashMap<String, ((u32, Instant), (HashSet<String>, Instant))>;
|
||||
type UserStatusMap = HashMap<Vec<u8>, Arc<(Option<Vec<u8>>, bool)>>;
|
||||
type IpChangesMap = HashMap<String, (Instant, HashMap<String, i32>)>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub(crate) static ref IP_BLOCKER: Mutex<IpBlockMap> = Default::default();
|
||||
pub(crate) static ref USER_STATUS: RwLock<UserStatusMap> = Default::default();
|
||||
pub(crate) static ref IP_CHANGES: Mutex<IpChangesMap> = Default::default();
|
||||
}
|
||||
|
||||
pub const IP_CHANGE_DUR: u64 = 180;
|
||||
pub const IP_CHANGE_DUR_X2: u64 = IP_CHANGE_DUR * 2;
|
||||
pub const DAY_SECONDS: u64 = 3600 * 24;
|
||||
pub const IP_BLOCK_DUR: u64 = 60;
|
||||
|
||||
// Status tracking constants
|
||||
const HEARTBEAT_TIMEOUT_SECS: u64 = 15; // Mark offline after 15s without heartbeat (was 30s)
|
||||
const CLEANUP_INTERVAL_SECS: u64 = 60; // Check for stale peers every 60s
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct PeerInfo {
|
||||
#[serde(default)]
|
||||
pub(crate) ip: String,
|
||||
}
|
||||
|
||||
pub(crate) struct Peer {
|
||||
pub(crate) socket_addr: SocketAddr,
|
||||
pub(crate) last_reg_time: Instant,
|
||||
pub(crate) guid: Vec<u8>,
|
||||
pub(crate) uuid: Bytes,
|
||||
pub(crate) pk: Bytes,
|
||||
pub(crate) info: PeerInfo,
|
||||
pub(crate) reg_pk: (u32, Instant),
|
||||
// Track last heartbeat for online status
|
||||
pub(crate) last_heartbeat: Instant,
|
||||
}
|
||||
|
||||
impl Default for Peer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
socket_addr: "0.0.0.0:0".parse().unwrap(),
|
||||
last_reg_time: get_expired_time(),
|
||||
guid: Vec::new(),
|
||||
uuid: Bytes::new(),
|
||||
pk: Bytes::new(),
|
||||
info: Default::default(),
|
||||
reg_pk: (0, get_expired_time()),
|
||||
last_heartbeat: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type LockPeer = Arc<RwLock<Peer>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PeerMap {
|
||||
map: Arc<RwLock<HashMap<String, LockPeer>>>,
|
||||
pub(crate) db: database::Database,
|
||||
}
|
||||
|
||||
impl PeerMap {
|
||||
pub(crate) async fn new() -> ResultType<Self> {
|
||||
let db = std::env::var("DB_URL").unwrap_or({
|
||||
let mut db = "db_v2.sqlite3".to_owned();
|
||||
#[cfg(all(windows, not(debug_assertions)))]
|
||||
{
|
||||
if let Some(path) = hbb_common::config::Config::icon_path().parent() {
|
||||
db = format!("{}\\{}", path.to_str().unwrap_or("."), db);
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
db = format!("./{db}");
|
||||
}
|
||||
db
|
||||
});
|
||||
log::info!("DB_URL={}", db);
|
||||
|
||||
let database = database::Database::new(&db).await?;
|
||||
|
||||
// Reset all devices to offline on startup (clean slate)
|
||||
if let Err(e) = database.set_all_offline().await {
|
||||
log::warn!("Failed to reset devices to offline: {}", e);
|
||||
}
|
||||
|
||||
let pm = Self {
|
||||
map: Default::default(),
|
||||
db: database,
|
||||
};
|
||||
|
||||
// Start background task to check for stale peers and set them offline
|
||||
let pm_clone = pm.clone();
|
||||
tokio::spawn(async move {
|
||||
pm_clone.status_cleanup_loop().await;
|
||||
});
|
||||
|
||||
Ok(pm)
|
||||
}
|
||||
|
||||
/// Background loop to detect stale peers and mark them offline
|
||||
async fn status_cleanup_loop(&self) {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(CLEANUP_INTERVAL_SECS));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let now = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(HEARTBEAT_TIMEOUT_SECS);
|
||||
let mut stale_peers = Vec::new();
|
||||
|
||||
// Find stale peers
|
||||
{
|
||||
let map = self.map.read().await;
|
||||
for (id, peer) in map.iter() {
|
||||
let peer_data = peer.read().await;
|
||||
if now.duration_since(peer_data.last_heartbeat) > timeout {
|
||||
stale_peers.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set stale peers offline and remove from memory
|
||||
if !stale_peers.is_empty() {
|
||||
log::info!("Marking {} stale peers as offline", stale_peers.len());
|
||||
|
||||
// Batch update database
|
||||
if let Err(e) = self.db.batch_set_offline(&stale_peers).await {
|
||||
log::error!("Failed to batch set offline: {}", e);
|
||||
}
|
||||
|
||||
// Remove from memory map
|
||||
{
|
||||
let mut map = self.map.write().await;
|
||||
for id in &stale_peers {
|
||||
map.remove(id);
|
||||
log::debug!("Removed stale peer {} from memory", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup IP blocker and IP changes maps
|
||||
self.cleanup_ip_maps().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanup stale entries from IP maps
|
||||
async fn cleanup_ip_maps(&self) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Cleanup IP_BLOCKER
|
||||
{
|
||||
let mut blocker = IP_BLOCKER.lock().await;
|
||||
blocker.retain(|_, ((_, t1), (_, t2))| {
|
||||
now.duration_since(*t1).as_secs() < IP_BLOCK_DUR &&
|
||||
now.duration_since(*t2).as_secs() < DAY_SECONDS
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup IP_CHANGES
|
||||
{
|
||||
let mut changes = IP_CHANGES.lock().await;
|
||||
changes.retain(|_, (t, _)| {
|
||||
now.duration_since(*t).as_secs() < IP_CHANGE_DUR_X2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Update heartbeat and set device online
|
||||
pub(crate) async fn touch_peer(&self, id: &str) {
|
||||
if let Some(peer) = self.map.read().await.get(id) {
|
||||
peer.write().await.last_heartbeat = Instant::now();
|
||||
}
|
||||
// Update database status
|
||||
self.db.set_online(id).await;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn update_pk(
|
||||
&mut self,
|
||||
id: String,
|
||||
peer: LockPeer,
|
||||
addr: SocketAddr,
|
||||
uuid: Bytes,
|
||||
pk: Bytes,
|
||||
ip: String,
|
||||
) -> register_pk_response::Result {
|
||||
log::info!("update_pk {} {:?} {:?} {:?}", id, addr, uuid, pk);
|
||||
|
||||
// BAN CHECK: Verify device is not banned before registration
|
||||
match self.db.is_device_banned(&id).await {
|
||||
Ok(true) => {
|
||||
log::warn!("Registration REJECTED for device {}: DEVICE IS BANNED", id);
|
||||
self.map.write().await.remove(&id);
|
||||
return register_pk_response::Result::UUID_MISMATCH;
|
||||
}
|
||||
Ok(false) => {
|
||||
log::debug!("Ban check passed for device {}", id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check ban status for device {}: {}. Allowing (fail-open)", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
let (info_str, guid) = {
|
||||
let mut w = peer.write().await;
|
||||
w.socket_addr = addr;
|
||||
w.uuid = uuid.clone();
|
||||
w.pk = pk.clone();
|
||||
w.last_reg_time = Instant::now();
|
||||
w.last_heartbeat = Instant::now(); // Update heartbeat on registration
|
||||
w.info.ip = ip;
|
||||
(
|
||||
serde_json::to_string(&w.info).unwrap_or_default(),
|
||||
w.guid.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
if guid.is_empty() {
|
||||
match self.db.insert_peer(&id, &uuid, &pk, &info_str).await {
|
||||
Err(err) => {
|
||||
log::error!("db.insert_peer failed: {}", err);
|
||||
return register_pk_response::Result::SERVER_ERROR;
|
||||
}
|
||||
Ok(guid) => {
|
||||
peer.write().await.guid = guid;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Err(err) = self.db.update_pk(&guid, &id, &pk, &info_str).await {
|
||||
log::error!("db.update_pk failed: {}", err);
|
||||
return register_pk_response::Result::SERVER_ERROR;
|
||||
}
|
||||
log::info!("pk updated instead of insert");
|
||||
}
|
||||
|
||||
// Device just registered, mark as online
|
||||
self.db.set_online(&id).await;
|
||||
|
||||
register_pk_response::Result::OK
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn get(&self, id: &str) -> Option<LockPeer> {
|
||||
let p = self.map.read().await.get(id).cloned();
|
||||
if p.is_some() {
|
||||
return p;
|
||||
} else if let Ok(Some(v)) = self.db.get_peer(id).await {
|
||||
// BAN CHECK: Do not load banned devices into memory
|
||||
if let Ok(true) = self.db.is_device_banned(id).await {
|
||||
log::warn!("Blocked loading banned device {} from database", id);
|
||||
return None;
|
||||
}
|
||||
let peer = Peer {
|
||||
guid: v.guid,
|
||||
uuid: v.uuid.into(),
|
||||
pk: v.pk.into(),
|
||||
info: serde_json::from_str::<PeerInfo>(&v.info).unwrap_or_default(),
|
||||
last_heartbeat: Instant::now(),
|
||||
..Default::default()
|
||||
};
|
||||
let peer = Arc::new(RwLock::new(peer));
|
||||
self.map.write().await.insert(id.to_owned(), peer.clone());
|
||||
return Some(peer);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn get_or(&self, id: &str) -> LockPeer {
|
||||
if let Some(p) = self.get(id).await {
|
||||
return p;
|
||||
}
|
||||
let mut w = self.map.write().await;
|
||||
if let Some(p) = w.get(id) {
|
||||
return p.clone();
|
||||
}
|
||||
let tmp = LockPeer::default();
|
||||
w.insert(id.to_owned(), tmp.clone());
|
||||
tmp
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn get_in_memory(&self, id: &str) -> Option<LockPeer> {
|
||||
self.map.read().await.get(id).cloned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) async fn is_in_memory(&self, id: &str) -> bool {
|
||||
self.map.read().await.contains_key(id)
|
||||
}
|
||||
|
||||
/// Find device ID by socket address (for ban enforcement)
|
||||
pub(crate) async fn get_id_by_addr(&self, addr: SocketAddr) -> Option<String> {
|
||||
let map = self.map.read().await;
|
||||
for (id, peer) in map.iter() {
|
||||
let peer_addr = peer.read().await.socket_addr;
|
||||
if peer_addr == addr {
|
||||
return Some(id.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get statistics about online peers
|
||||
pub(crate) async fn get_stats(&self) -> (usize, usize) {
|
||||
let map = self.map.read().await;
|
||||
let total = map.len();
|
||||
let now = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(HEARTBEAT_TIMEOUT_SECS);
|
||||
let online = map.values()
|
||||
.filter(|p| {
|
||||
// Check if we can read without blocking
|
||||
if let Ok(peer) = p.try_read() {
|
||||
now.duration_since(peer.last_heartbeat) <= timeout
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.count();
|
||||
(total, online)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+230
-42
@@ -1,5 +1,6 @@
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, g, send_file
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
import requests
|
||||
@@ -318,57 +319,95 @@ def client_generator():
|
||||
@require_auth
|
||||
@limiter.exempt # Authenticated users bypass rate limit
|
||||
def get_devices():
|
||||
"""Fetch all devices from the database with online status from HBBS API."""
|
||||
"""Fetch all devices from the database with online status based on last_online."""
|
||||
try:
|
||||
# Try to get status from HBBS API
|
||||
online_ids = set()
|
||||
api_device_info = {}
|
||||
try:
|
||||
headers = {}
|
||||
if HBBS_API_KEY:
|
||||
headers['X-API-Key'] = HBBS_API_KEY
|
||||
|
||||
response = requests.get(f'{HBBS_API_URL}/peers', headers=headers, timeout=2)
|
||||
if response.status_code == 200:
|
||||
api_data = response.json()
|
||||
if api_data.get('success') and api_data.get('data'):
|
||||
for peer in api_data['data']:
|
||||
device_id = peer.get('id')
|
||||
if device_id:
|
||||
api_device_info[device_id] = peer
|
||||
if peer.get('online'):
|
||||
online_ids.add(device_id)
|
||||
elif response.status_code == 401:
|
||||
print(f"Warning: HBBS API authentication failed. Check API key.")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not connect to HBBS API: {e}")
|
||||
# Get server config for timeout settings
|
||||
config = get_server_config()
|
||||
peer_timeout_secs = config.get('peer_timeout_secs', 60)
|
||||
warning_threshold = config.get('warning_threshold', 2)
|
||||
critical_threshold = config.get('critical_threshold', 4)
|
||||
heartbeat_interval = config.get('heartbeat_interval_secs', 5)
|
||||
|
||||
# Get devices from database
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
guid, id, uuid, pk, created_at, user, status, note, info,
|
||||
is_banned, banned_at, banned_by, ban_reason
|
||||
FROM peer
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
# Check if last_online column exists
|
||||
cursor.execute("PRAGMA table_info(peer)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
has_last_online = 'last_online' in columns
|
||||
|
||||
if has_last_online:
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
guid, id, uuid, pk, created_at, user, status, note, info,
|
||||
is_banned, banned_at, banned_by, ban_reason,
|
||||
previous_ids, id_changed_at, last_online
|
||||
FROM peer
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
else:
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
guid, id, uuid, pk, created_at, user, status, note, info,
|
||||
is_banned, banned_at, banned_by, ban_reason,
|
||||
previous_ids, id_changed_at
|
||||
FROM peer
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
devices = []
|
||||
now = datetime.now()
|
||||
|
||||
for row in cursor.fetchall():
|
||||
device_id = row['id']
|
||||
|
||||
# IMPORTANT: API /peers has a bug in http_api.rs:
|
||||
# 1. Query: "WHERE (status IS NULL OR status = 0)" - returns only offline
|
||||
# 2. Hardcoded: online = false - always false for all
|
||||
# Due to these bugs, the API is not suitable for determining online status
|
||||
#
|
||||
# SOLUTION: We use ONLY the status field from database (updated by HBBS)
|
||||
# status = 1 = online, status = 0/NULL = offline
|
||||
# This is the source of truth - HBBS updates this field in real-time
|
||||
# Determine online status based on last_online timestamp
|
||||
online = False
|
||||
status_detail = 'offline'
|
||||
last_online = None
|
||||
|
||||
online = row['status'] == 1
|
||||
if has_last_online and row['last_online']:
|
||||
last_online = row['last_online']
|
||||
try:
|
||||
# Parse timestamp - handle both formats
|
||||
if isinstance(last_online, str):
|
||||
if 'T' in last_online:
|
||||
last_online_dt = datetime.fromisoformat(last_online.replace('Z', '+00:00').replace('+00:00', ''))
|
||||
else:
|
||||
last_online_dt = datetime.strptime(last_online, '%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
last_online_dt = last_online
|
||||
|
||||
# Calculate time since last activity
|
||||
seconds_since = (now - last_online_dt).total_seconds()
|
||||
|
||||
if seconds_since <= peer_timeout_secs:
|
||||
# Within timeout - check for degraded/critical states
|
||||
missed_heartbeats = int(seconds_since / heartbeat_interval)
|
||||
|
||||
if missed_heartbeats >= critical_threshold:
|
||||
online = True
|
||||
status_detail = 'critical'
|
||||
elif missed_heartbeats >= warning_threshold:
|
||||
online = True
|
||||
status_detail = 'degraded'
|
||||
else:
|
||||
online = True
|
||||
status_detail = 'online'
|
||||
else:
|
||||
status_detail = 'offline'
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse last_online for {device_id}: {e}")
|
||||
# Fallback to status field
|
||||
online = row['status'] == 1
|
||||
status_detail = 'online' if online else 'offline'
|
||||
else:
|
||||
# No last_online - fallback to status field
|
||||
online = row['status'] == 1
|
||||
status_detail = 'online' if online else 'offline'
|
||||
|
||||
device = {
|
||||
'guid': row['guid'].hex() if row['guid'] else '',
|
||||
@@ -379,17 +418,21 @@ def get_devices():
|
||||
'user': row['user'].hex() if row['user'] else '',
|
||||
'status': row['status'],
|
||||
'online': online,
|
||||
'status_detail': status_detail,
|
||||
'last_online': last_online,
|
||||
'note': row['note'] or '',
|
||||
'info': row['info'] or '',
|
||||
'is_banned': row['is_banned'] == 1,
|
||||
'banned_at': row['banned_at'],
|
||||
'banned_by': row['banned_by'] or '',
|
||||
'ban_reason': row['ban_reason'] or ''
|
||||
'ban_reason': row['ban_reason'] or '',
|
||||
'previous_ids': json.loads(row['previous_ids']) if row['previous_ids'] else [],
|
||||
'id_changed_at': row['id_changed_at'] or ''
|
||||
}
|
||||
devices.append(device)
|
||||
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'devices': devices})
|
||||
return jsonify({'success': True, 'devices': devices, 'config': config})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@@ -435,8 +478,23 @@ def update_device(device_id):
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device ID already exists'}), 409
|
||||
|
||||
# Get current previous_ids and add old_id to history
|
||||
cursor.execute('SELECT previous_ids FROM peer WHERE id = ? AND is_deleted = 0', (device_id,))
|
||||
row = cursor.fetchone()
|
||||
previous_ids = []
|
||||
if row and row[0]:
|
||||
try:
|
||||
previous_ids = json.loads(row[0]) if row[0] else []
|
||||
except:
|
||||
previous_ids = []
|
||||
previous_ids.append(device_id)
|
||||
|
||||
updates.append('id = ?')
|
||||
params.append(data['new_id'])
|
||||
updates.append('previous_ids = ?')
|
||||
params.append(json.dumps(previous_ids))
|
||||
updates.append('id_changed_at = ?')
|
||||
params.append(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
if not updates:
|
||||
conn.close()
|
||||
@@ -551,6 +609,136 @@ def get_stats():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# Default server configuration
|
||||
DEFAULT_SERVER_CONFIG = {
|
||||
'peer_timeout_secs': 60,
|
||||
'heartbeat_interval_secs': 5,
|
||||
'warning_threshold': 2,
|
||||
'critical_threshold': 4
|
||||
}
|
||||
|
||||
def ensure_server_config_table():
|
||||
"""Ensure server_config table exists."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS server_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_server_config():
|
||||
"""Get server configuration from database."""
|
||||
ensure_server_config_table()
|
||||
config = DEFAULT_SERVER_CONFIG.copy()
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT key, value FROM server_config')
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
for row in rows:
|
||||
key = row['key']
|
||||
if key in config:
|
||||
try:
|
||||
config[key] = int(row['value'])
|
||||
except ValueError:
|
||||
config[key] = row['value']
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load server config: {e}")
|
||||
return config
|
||||
|
||||
def save_server_config(config):
|
||||
"""Save server configuration to database."""
|
||||
ensure_server_config_table()
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
for key, value in config.items():
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO server_config (key, value, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
''', (key, str(value)))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.route('/api/server/config', methods=['GET'])
|
||||
@require_auth
|
||||
@limiter.exempt
|
||||
def get_server_config_endpoint():
|
||||
"""Get server configuration."""
|
||||
try:
|
||||
config = get_server_config()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'config': config
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/server/config', methods=['POST'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN)
|
||||
def update_server_config_endpoint():
|
||||
"""Update server configuration (admin only)."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'No data provided'}), 400
|
||||
|
||||
# Validate and sanitize config values
|
||||
config = get_server_config()
|
||||
|
||||
if 'peer_timeout_secs' in data:
|
||||
val = int(data['peer_timeout_secs'])
|
||||
if val < 10 or val > 300:
|
||||
return jsonify({'success': False, 'error': 'peer_timeout_secs must be between 10 and 300'}), 400
|
||||
config['peer_timeout_secs'] = val
|
||||
|
||||
if 'heartbeat_interval_secs' in data:
|
||||
val = int(data['heartbeat_interval_secs'])
|
||||
if val < 1 or val > 30:
|
||||
return jsonify({'success': False, 'error': 'heartbeat_interval_secs must be between 1 and 30'}), 400
|
||||
config['heartbeat_interval_secs'] = val
|
||||
|
||||
if 'warning_threshold' in data:
|
||||
val = int(data['warning_threshold'])
|
||||
if val < 1 or val > 10:
|
||||
return jsonify({'success': False, 'error': 'warning_threshold must be between 1 and 10'}), 400
|
||||
config['warning_threshold'] = val
|
||||
|
||||
if 'critical_threshold' in data:
|
||||
val = int(data['critical_threshold'])
|
||||
if val < 2 or val > 20:
|
||||
return jsonify({'success': False, 'error': 'critical_threshold must be between 2 and 20'}), 400
|
||||
config['critical_threshold'] = val
|
||||
|
||||
# Validate thresholds relationship
|
||||
if config['warning_threshold'] >= config['critical_threshold']:
|
||||
return jsonify({'success': False, 'error': 'warning_threshold must be less than critical_threshold'}), 400
|
||||
|
||||
save_server_config(config)
|
||||
|
||||
# Log the change
|
||||
user_id = g.user.get('id') if g.user else None
|
||||
log_audit(user_id, 'config_change', None, f"Server config updated: {config}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Configuration saved successfully',
|
||||
'config': config
|
||||
})
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': 'Invalid numeric value'}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/device/<device_id>/ban', methods=['POST'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN, ROLE_OPERATOR)
|
||||
|
||||
+196
-10
@@ -6,6 +6,7 @@ let authToken = null;
|
||||
let userRole = null;
|
||||
let username = null;
|
||||
let publicKeyCache = null;
|
||||
let refreshIntervalId = null;
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -22,15 +23,45 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadDevices();
|
||||
loadStats();
|
||||
|
||||
// Auto-refresh dashboard every 5 seconds
|
||||
setInterval(() => {
|
||||
// Load server config first, then start auto-refresh with configured interval
|
||||
loadServerConfigAndStartRefresh();
|
||||
});
|
||||
|
||||
// Load server config and start auto-refresh with configured interval
|
||||
async function loadServerConfigAndStartRefresh() {
|
||||
try {
|
||||
const response = await fetch('/api/server/config', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.config) {
|
||||
serverConfig = data.config;
|
||||
console.log('Server config loaded:', serverConfig);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load server config, using defaults');
|
||||
}
|
||||
|
||||
// Start auto-refresh with configured interval (default 5 seconds, min 3 seconds)
|
||||
const refreshInterval = Math.max(3, serverConfig.heartbeat_interval_secs || 5) * 1000;
|
||||
console.log('Starting auto-refresh every', refreshInterval, 'ms');
|
||||
|
||||
// Clear any existing interval
|
||||
if (refreshIntervalId) {
|
||||
clearInterval(refreshIntervalId);
|
||||
}
|
||||
|
||||
refreshIntervalId = setInterval(() => {
|
||||
const dashboardSection = document.getElementById('dashboard');
|
||||
if (dashboardSection && dashboardSection.classList.contains('active')) {
|
||||
loadDevices();
|
||||
loadStats();
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
// Authentication check
|
||||
function checkAuth() {
|
||||
@@ -90,6 +121,9 @@ function setupSidebar() {
|
||||
if (sectionId === 'users' && userRole === 'admin') {
|
||||
loadUsers();
|
||||
}
|
||||
if (sectionId === 'settings' && userRole === 'admin') {
|
||||
loadServerSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -222,6 +256,34 @@ function renderDevices(devices) {
|
||||
const canEdit = userRole === 'admin' || userRole === 'operator';
|
||||
const canBan = userRole === 'admin' || userRole === 'operator';
|
||||
|
||||
// Handle detailed status (online/degraded/critical/offline)
|
||||
const statusDetail = device.status_detail || (device.online ? 'online' : 'offline');
|
||||
let statusClass = 'status-inactive';
|
||||
let statusIcon = 'fa-circle';
|
||||
let statusText = 'Offline';
|
||||
|
||||
switch (statusDetail) {
|
||||
case 'online':
|
||||
statusClass = 'status-active';
|
||||
statusIcon = 'fa-circle';
|
||||
statusText = 'Online';
|
||||
break;
|
||||
case 'degraded':
|
||||
statusClass = 'status-warning';
|
||||
statusIcon = 'fa-exclamation-circle';
|
||||
statusText = 'Degraded';
|
||||
break;
|
||||
case 'critical':
|
||||
statusClass = 'status-critical';
|
||||
statusIcon = 'fa-exclamation-triangle';
|
||||
statusText = 'Critical';
|
||||
break;
|
||||
default:
|
||||
statusClass = 'status-inactive';
|
||||
statusIcon = 'fa-circle';
|
||||
statusText = 'Offline';
|
||||
}
|
||||
|
||||
return `
|
||||
<tr ${rowClass}>
|
||||
<td>
|
||||
@@ -230,9 +292,9 @@ function renderDevices(devices) {
|
||||
</td>
|
||||
<td>${escapeHtml(device.note) || '<span style="color: var(--text-secondary);">No note</span>'}</td>
|
||||
<td>
|
||||
<span class="status-badge ${device.online ? 'status-active' : 'status-inactive'}">
|
||||
<i class="fas fa-circle"></i>
|
||||
${device.online ? 'Online' : 'Offline'}
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas ${statusIcon}"></i>
|
||||
${statusText}
|
||||
</span>
|
||||
</td>
|
||||
<td>${formatDate(device.created_at)}</td>
|
||||
@@ -341,9 +403,14 @@ function showDetails(deviceId) {
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Status:</div>
|
||||
<div class="detail-value">
|
||||
<span class="status-badge ${device.online ? 'status-active' : 'status-inactive'}">
|
||||
<i class="fas fa-circle"></i>
|
||||
${device.online ? 'Online' : 'Offline'}
|
||||
<span class="status-badge ${device.status_detail === 'online' || device.status_detail === 'degraded' || device.status_detail === 'critical' ?
|
||||
(device.status_detail === 'degraded' ? 'status-warning' :
|
||||
device.status_detail === 'critical' ? 'status-critical' : 'status-active') : 'status-inactive'}">
|
||||
<i class="fas ${device.status_detail === 'degraded' ? 'fa-exclamation-circle' :
|
||||
device.status_detail === 'critical' ? 'fa-exclamation-triangle' : 'fa-circle'}"></i>
|
||||
${device.status_detail === 'degraded' ? 'Degraded' :
|
||||
device.status_detail === 'critical' ? 'Critical' :
|
||||
device.online ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -388,6 +455,23 @@ function editDevice(deviceId) {
|
||||
document.getElementById('editNewId').value = '';
|
||||
document.getElementById('editNote').value = device.note || '';
|
||||
|
||||
// Show ID history if available
|
||||
const historyGroup = document.getElementById('idHistoryGroup');
|
||||
const historyList = document.getElementById('idHistoryList');
|
||||
|
||||
if (device.previous_ids && device.previous_ids.length > 0) {
|
||||
historyGroup.style.display = 'block';
|
||||
historyList.innerHTML = device.previous_ids.map(id =>
|
||||
`<span class="id-history-item">${escapeHtml(id)}</span>`
|
||||
).join(' → ');
|
||||
if (device.id_changed_at) {
|
||||
historyList.innerHTML += `<br><small class="text-muted">Last changed: ${device.id_changed_at}</small>`;
|
||||
}
|
||||
} else {
|
||||
historyGroup.style.display = 'none';
|
||||
historyList.innerHTML = '';
|
||||
}
|
||||
|
||||
openModal('editModal');
|
||||
}
|
||||
|
||||
@@ -839,6 +923,108 @@ window.onclick = function(event) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVER SETTINGS (ADMIN ONLY)
|
||||
// ============================================================================
|
||||
|
||||
// Global server config cache
|
||||
let serverConfig = {
|
||||
peer_timeout_secs: 60,
|
||||
heartbeat_interval_secs: 5,
|
||||
warning_threshold: 2,
|
||||
critical_threshold: 4
|
||||
};
|
||||
|
||||
async function loadServerSettings() {
|
||||
if (!checkAuth()) return;
|
||||
if (userRole !== 'admin') return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/server/config', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.config) {
|
||||
serverConfig = data.config;
|
||||
|
||||
// Update form fields
|
||||
document.getElementById('peerTimeoutSecs').value = serverConfig.peer_timeout_secs || 60;
|
||||
document.getElementById('heartbeatIntervalSecs').value = serverConfig.heartbeat_interval_secs || 5;
|
||||
document.getElementById('warningThreshold').value = serverConfig.warning_threshold || 2;
|
||||
document.getElementById('criticalThreshold').value = serverConfig.critical_threshold || 4;
|
||||
|
||||
console.log('Server settings loaded:', serverConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading server settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveServerSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!checkAuth()) return;
|
||||
if (userRole !== 'admin') {
|
||||
showToast('Only admins can change server settings', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = {
|
||||
peer_timeout_secs: parseInt(document.getElementById('peerTimeoutSecs').value) || 60,
|
||||
heartbeat_interval_secs: parseInt(document.getElementById('heartbeatIntervalSecs').value) || 5,
|
||||
warning_threshold: parseInt(document.getElementById('warningThreshold').value) || 2,
|
||||
critical_threshold: parseInt(document.getElementById('criticalThreshold').value) || 4
|
||||
};
|
||||
|
||||
// Validation
|
||||
if (newConfig.peer_timeout_secs < 10 || newConfig.peer_timeout_secs > 300) {
|
||||
showToast('Peer timeout must be between 10 and 300 seconds', 'error');
|
||||
return;
|
||||
}
|
||||
if (newConfig.heartbeat_interval_secs < 1 || newConfig.heartbeat_interval_secs > 30) {
|
||||
showToast('Heartbeat interval must be between 1 and 30 seconds', 'error');
|
||||
return;
|
||||
}
|
||||
if (newConfig.critical_threshold <= newConfig.warning_threshold) {
|
||||
showToast('Critical threshold must be greater than warning threshold', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/server/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newConfig)
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
serverConfig = newConfig;
|
||||
showToast('Server settings saved successfully');
|
||||
} else {
|
||||
showToast('Error: ' + (data.error || 'Failed to save settings'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving server settings:', error);
|
||||
showToast('Failed to save server settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Get peer timeout for online status calculation (in milliseconds)
|
||||
function getPeerTimeoutMs() {
|
||||
return (serverConfig.peer_timeout_secs || 60) * 1000;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
@@ -366,6 +366,16 @@ body {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-critical {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: rgba(245, 87, 108, 0.2);
|
||||
color: var(--danger-color);
|
||||
@@ -562,6 +572,37 @@ textarea.form-control {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ID History Display */
|
||||
.id-history-list {
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.id-history-item {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 4px;
|
||||
margin: 0.1rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: var(--warning-color);
|
||||
margin-top: 0.5rem;
|
||||
|
||||
@@ -550,8 +550,50 @@
|
||||
<!-- Settings Section -->
|
||||
<div class="content-section" id="settings">
|
||||
<div class="settings-container">
|
||||
<h1 class="page-title">Account Settings</h1>
|
||||
<h1 class="page-title">Settings</h1>
|
||||
|
||||
<!-- Server Settings Card (Admin Only) -->
|
||||
<div class="settings-card admin-only" id="serverSettingsCard">
|
||||
<h3><i class="fas fa-server"></i> Server Settings</h3>
|
||||
<form id="serverSettingsForm" onsubmit="saveServerSettings(event)">
|
||||
<div class="form-group">
|
||||
<label for="peerTimeoutSecs">Peer Offline Timeout (seconds)</label>
|
||||
<input type="number" id="peerTimeoutSecs" min="10" max="300" value="60">
|
||||
<small style="color: rgba(255, 255, 255, 0.6); font-size: 0.8rem;">
|
||||
Time before a device is marked as offline (10-300 seconds)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="heartbeatIntervalSecs">Status Check Interval (seconds)</label>
|
||||
<input type="number" id="heartbeatIntervalSecs" min="1" max="30" value="5">
|
||||
<small style="color: rgba(255, 255, 255, 0.6); font-size: 0.8rem;">
|
||||
How often to check device status (1-30 seconds)
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="warningThreshold">Warning Threshold (missed heartbeats)</label>
|
||||
<input type="number" id="warningThreshold" min="1" max="10" value="2">
|
||||
<small style="color: rgba(255, 255, 255, 0.6); font-size: 0.8rem;">
|
||||
Number of missed heartbeats before "Degraded" status
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="criticalThreshold">Critical Threshold (missed heartbeats)</label>
|
||||
<input type="number" id="criticalThreshold" min="2" max="20" value="4">
|
||||
<small style="color: rgba(255, 255, 255, 0.6); font-size: 0.8rem;">
|
||||
Number of missed heartbeats before "Critical" status
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Server Settings
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="loadServerSettings()">
|
||||
<i class="fas fa-sync"></i> Reload
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Account Settings Card -->
|
||||
<div class="settings-card">
|
||||
<h3><i class="fas fa-key"></i> Change Password</h3>
|
||||
<form id="passwordForm" onsubmit="changePassword(event)">
|
||||
@@ -730,6 +772,11 @@
|
||||
<div class="form-group">
|
||||
<label for="editNewId">New Device ID (optional)</label>
|
||||
<input type="text" id="editNewId" class="form-control" placeholder="Leave empty to keep current ID">
|
||||
<small class="form-text text-muted">6-16 characters, letters/numbers/dash/underscore</small>
|
||||
</div>
|
||||
<div class="form-group" id="idHistoryGroup" style="display: none;">
|
||||
<label><i class="fas fa-history"></i> Previous IDs</label>
|
||||
<div id="idHistoryList" class="id-history-list"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editNote">Note</label>
|
||||
|
||||
Reference in New Issue
Block a user