Release v1.5.0: authentication, UI overhaul, docs cleanup
Major update introducing authentication (bcrypt password hashing, role-based access, session tokens), new sidebar UI, user management, settings, and extended About page. API now uses X-API-Key authentication and is LAN-accessible. Updated installation and update scripts, added migration for auth system, and removed deprecated/archived documentation and scripts. Documentation and screenshots updated to reflect new features and security model.
@@ -21,7 +21,7 @@ The enhanced installer is primarily designed for **native RustDesk installations
|
||||
#### Option 1: Continue with Native Installation (Not Recommended)
|
||||
|
||||
You can choose to install BetterDesk Console alongside your Docker installation, but this may cause conflicts:
|
||||
- Port conflicts (21115, 21116, 21117, 21114)
|
||||
- Port conflicts (21115, 21116, 21117, 21120)
|
||||
- Database access issues
|
||||
- Service management complexity
|
||||
|
||||
@@ -62,7 +62,7 @@ services:
|
||||
- "21115:21115"
|
||||
- "21116:21116/tcp"
|
||||
- "21116:21116/udp"
|
||||
- "21114:21114" # API port
|
||||
- "21120:21120" # API port
|
||||
```
|
||||
|
||||
### Detecting Your Setup
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# 🚀 BetterDesk Console
|
||||
# 🚀 BetterDesk Console
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**A modern, feature-rich web management console for RustDesk with real-time device monitoring and bidirectional ban enforcement**
|
||||
|
||||
@@ -60,14 +61,14 @@
|
||||
|
||||
- **Glassmorphism Design**: Sleek, modern UI with blur effects and gradients
|
||||
- **Material Icons**: Google Material Design icons (fully offline)
|
||||
- **Responsive Layout**: Works on desktop, tablet, and mobile
|
||||
- **Responsive Layout**: Works on desktop and tablet
|
||||
- **Dark Theme**: Easy on the eyes, perfect for NOC environments
|
||||
- **Real-Time Updates**: Auto-refresh device status
|
||||
- **Search & Filter**: Quickly find devices in large deployments
|
||||
|
||||
### 🔧 Enhanced HBBS Server
|
||||
|
||||
- **HTTP API**: RESTful API on port 21120 (localhost only, not exposed to internet)
|
||||
- **HTTP API**: RESTful API on port 21120 with X-API-Key authentication (LAN accessible)
|
||||
- **Real-Time Status**: Memory-based device status (no database lag)
|
||||
- **Authentic Algorithm**: Uses RustDesk's official 30-second timeout logic
|
||||
- **Thread-Safe**: Shared PeerMap with Arc/RwLock for concurrent access
|
||||
@@ -98,6 +99,21 @@
|
||||
|
||||
### 🛡️ Security & Reliability
|
||||
|
||||
- **Authentication System (v1.5.0)**:
|
||||
- User login with bcrypt password hashing
|
||||
- Role-based access control (Admin, Operator, Viewer)
|
||||
- Session management with 24-hour tokens
|
||||
- **\ud83c\udf10 Sidebar navigation** with 5 main sections (Dashboard, Public Key, Settings, User Management, About)
|
||||
- **\ud83d\udd11 Password-protected public key access** - requires password verification
|
||||
- **\u2699\ufe0f Settings page** with password change functionality
|
||||
- **\ud83d\udc65 User management panel** (admin only) - create, edit, delete, activate/deactivate users
|
||||
- **\ud83d\udcdd Extended About page** with open source credits and license information
|
||||
- Audit logging for all actions
|
||||
- **API Security (v1.4.0)**:
|
||||
- X-API-Key header authentication for HBBS API
|
||||
- 64-character random API keys
|
||||
- Secure key storage with 600 permissions
|
||||
- LAN accessible (0.0.0.0) with authentication protection
|
||||
- **Input Validation**: Comprehensive validation for all user inputs
|
||||
- **XSS Protection**: Sanitization of user-provided content
|
||||
- **SQL Injection Prevention**: Parameterized queries throughout
|
||||
@@ -131,9 +147,21 @@
|
||||

|
||||
*Detailed device information modal*
|
||||
|
||||
### Mobile Responsive
|
||||

|
||||
*Fully responsive design for mobile devices*
|
||||
### Public Key Management
|
||||

|
||||
*Secure public key access with password protection*
|
||||
|
||||
### Settings
|
||||

|
||||
*User settings and password management*
|
||||
|
||||
### User Management
|
||||

|
||||
*Multi-user administration panel*
|
||||
|
||||
### About
|
||||

|
||||
*System information and version details*
|
||||
|
||||
---
|
||||
|
||||
@@ -142,7 +170,7 @@
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ RustDesk Clients │
|
||||
│ (Desktop, Mobile, Web) │
|
||||
│ (Desktop, Tablet, Web) │
|
||||
└──────────────────────┬──────────────────────────────────┘
|
||||
│ Heartbeat (~30-45s)
|
||||
▼
|
||||
@@ -164,7 +192,7 @@
|
||||
┌────────────────┐ ┌─────────────────┐
|
||||
│ HTTP API │ │ SQLite DB │
|
||||
│ (Port 21120) │ │ (Persistence) │
|
||||
│ (Localhost) │ │ │
|
||||
│ (LAN Access) │ │ │
|
||||
└────────┬───────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -223,6 +251,31 @@ sudo ./install-improved.sh
|
||||
- ✅ **Dynamic .pub file scanning** - works with any public key filename
|
||||
- ✅ **Multiple backup options** - automatic, manual, or existing backup
|
||||
- ✅ **Key regeneration with warnings** - prevents accidental key changes
|
||||
- ✅ **API key generation** - automatic X-API-Key authentication setup
|
||||
- ✅ **LAN access configuration** - web console and API accessible on network
|
||||
|
||||
### 🔄 Updating Existing Installation
|
||||
|
||||
If you already have BetterDesk Console installed and want to upgrade to v1.4.0 with authentication:
|
||||
|
||||
```bash
|
||||
cd Rustdesk-FreeConsole
|
||||
|
||||
# Make the update script executable
|
||||
chmod +x update-to-v1.4.0.sh
|
||||
|
||||
# Run as root
|
||||
sudo ./update-to-v1.4.0.sh
|
||||
```
|
||||
|
||||
**Update features:**
|
||||
- ✅ Automatic backup before changes
|
||||
- ✅ Database migration to add authentication tables
|
||||
- ✅ API key generation and configuration
|
||||
- ✅ Preserves existing configuration
|
||||
- ✅ Creates default admin user (if needed)
|
||||
- ✅ Updates systemd services for LAN access
|
||||
- ✅ Rollback capability if update fails
|
||||
|
||||
### 🪟 Windows Installation
|
||||
|
||||
@@ -253,14 +306,14 @@ The installers automatically use the correct binaries for your platform:
|
||||
|
||||
### 🔑 Key Protection (IMPORTANT!)
|
||||
|
||||
**v9+ includes comprehensive encryption key protection:**
|
||||
**v1.5.0+ includes comprehensive encryption key protection:**
|
||||
|
||||
⚠️ **Your RustDesk encryption keys are CRITICAL!**
|
||||
- Losing keys = ALL clients disconnected
|
||||
- Changing keys = "Key mismatch" errors on all devices
|
||||
- Keys must be backed up before any installation
|
||||
|
||||
**BetterDesk v9+ automatically:**
|
||||
**BetterDesk v1.5.0+ automatically:**
|
||||
- ✅ Detects existing encryption keys
|
||||
- ✅ Scans for ANY `.pub` file (not just `id_ed25519.pub`)
|
||||
- ✅ Offers multiple backup options (automatic, manual, existing)
|
||||
@@ -282,35 +335,25 @@ Options:
|
||||
|
||||
**If you experience "Key mismatch" errors:**
|
||||
```bash
|
||||
# Use the repair tool
|
||||
sudo bash repair-keys.sh
|
||||
|
||||
# Or restore from automatic backup
|
||||
# Restore from automatic backup
|
||||
BACKUP=$(ls -d /opt/rustdesk-backup-* | sort | tail -1)
|
||||
sudo cp $BACKUP/id_ed25519* /opt/rustdesk/
|
||||
sudo systemctl restart rustdesksignal
|
||||
```
|
||||
|
||||
📖 **Full guide**: [docs/KEY_TROUBLESHOOTING.md](docs/KEY_TROUBLESHOOTING.md)
|
||||
🚑 **Quick fixes**: [docs/QUICK_FIX.md](docs/QUICK_FIX.md)
|
||||
📖 **Full guide**: [docs/KEY_TROUBLESHOOTING.md](docs/KEY_TROUBLESHOOTING.md)
|
||||
|
||||
### What's New in v1.3.0 (Latest)
|
||||
### What's New in v1.5.0 (Latest)
|
||||
|
||||
- **🔑 Encryption Key Protection**: Automatic detection and preservation of existing keys
|
||||
- **🔍 Dynamic Key Scanning**: Web console finds any `.pub` file automatically
|
||||
- **💾 Enhanced Backup System**: Multiple backup options with verification
|
||||
- **🐳 Improved Docker Support**: Better detection and handling of Docker installations
|
||||
- **🔧 Key Repair Tool**: New `repair-keys.sh` utility for troubleshooting
|
||||
- **📚 Comprehensive Documentation**: KEY_TROUBLESHOOTING.md and QUICK_FIX.md guides
|
||||
- **⚠️ Visual Warnings**: Clear indicators for dangerous operations
|
||||
- **✅ Pre-flight Checks**: Validation before any destructive operations
|
||||
|
||||
### What's New in v1.1.0
|
||||
|
||||
- **Device Banning System**: Ban/unban devices with reason tracking
|
||||
- **Soft Delete**: Devices marked as deleted instead of permanent removal
|
||||
- **Enhanced Security**: Input validation, XSS protection, SQL injection prevention
|
||||
- **Improved UI**: Visual ban indicators, new statistics card, confirmation dialogs
|
||||
- **🔐 Authentication System**: User login with bcrypt password hashing
|
||||
- **👥 Role-Based Access Control**: Admin, Operator, and Viewer roles
|
||||
- **🌐 Sidebar Navigation**: Modern UI with 5 main sections
|
||||
- **🔑 Password-Protected Public Key**: Requires verification to view
|
||||
- **⚙️ Settings Page**: Change password functionality with token regeneration
|
||||
- **👤 User Management**: Admin panel to create, edit, delete users
|
||||
- **📝 Extended About Page**: Open source credits and license info
|
||||
- **🛡️ CSRF Protection**: Flask-WTF security
|
||||
- **⏱️ Rate Limiting**: 5 login attempts per minute
|
||||
|
||||
### 🔒 Manual Installation on SSH Server (Security Update)
|
||||
|
||||
@@ -330,28 +373,7 @@ sudo bash ~/MANUAL_INSTALL.sh
|
||||
2. Backup old binaries (timestamped)
|
||||
3. Install new binaries from `~/build/hbbs-patch/rustdesk-server/target/release/`
|
||||
4. Restart services
|
||||
5. Verify:
|
||||
- HTTP API listening on `127.0.0.1:21120` (localhost only)
|
||||
- RustDesk ports 21115-21117 operational
|
||||
- Port 21120 NOT accessible from external network
|
||||
|
||||
**Post-installation verification:**
|
||||
```bash
|
||||
# On server (should work)
|
||||
curl http://localhost:21120/api/health
|
||||
|
||||
# From outside (should FAIL with "Connection refused")
|
||||
curl http://SERVER_IP:21120/api/health
|
||||
```
|
||||
|
||||
**To access API remotely, use SSH tunnel:**
|
||||
```bash
|
||||
# Create tunnel
|
||||
ssh -L 21120:localhost:21120 your-user@your-server
|
||||
|
||||
# Then access locally
|
||||
curl http://localhost:21120/api/health
|
||||
```
|
||||
5. Verify API is responding on port 21120
|
||||
|
||||
---
|
||||
|
||||
@@ -446,22 +468,32 @@ sudo bash repair-keys.sh
|
||||
|
||||
### HBBS API Port
|
||||
|
||||
Default: `21120` (localhost only - not exposed to internet)
|
||||
Default: `21120` (LAN accessible with X-API-Key authentication)
|
||||
|
||||
**Security**: The API is bound to `127.0.0.1` (localhost) by design. This means:
|
||||
- ✅ API is only accessible from the same machine
|
||||
- ✅ Cannot be accessed from network/internet even without firewall
|
||||
- ✅ Web console connects locally or via SSH tunnel
|
||||
- ✅ No risk of unauthorized data exposure
|
||||
**Security (v1.4.0)**: The API now supports LAN access with proper authentication:
|
||||
- \u2705 Binds to `0.0.0.0:21120` (accessible on LAN)
|
||||
- \u2705 Requires X-API-Key header for all requests
|
||||
- \u2705 64-character random API key generated during installation
|
||||
- \u2705 Key stored securely in `/opt/rustdesk/.api_key` with 600 permissions
|
||||
- \u2705 Web console automatically uses API key
|
||||
- \u2705 No authentication = no access (secure by design)
|
||||
|
||||
To change, edit `/etc/systemd/system/rustdesksignal.service`:
|
||||
**API Key Location**: `/opt/rustdesk/.api_key`
|
||||
|
||||
To change port, edit `/etc/systemd/system/rustdesksignal.service`:
|
||||
```ini
|
||||
ExecStart=/opt/rustdesk/hbbs -k _ -p 21115 --api-port 21115
|
||||
ExecStart=/opt/rustdesk/hbbs -k _ -p 21115 --api-port 21120
|
||||
```
|
||||
|
||||
### Web Console Port
|
||||
|
||||
Default: `5000`
|
||||
Default: `5000` (accessible on LAN)
|
||||
|
||||
The web console binds to `0.0.0.0:5000` for LAN access and includes:
|
||||
- User authentication (bcrypt passwords)
|
||||
- Session management (24-hour tokens)
|
||||
- Role-based access control
|
||||
- Audit logging
|
||||
|
||||
To change, edit `/opt/BetterDeskConsole/app.py`:
|
||||
```python
|
||||
@@ -471,12 +503,13 @@ app.run(host='0.0.0.0', port=5000)
|
||||
### Firewall Configuration
|
||||
|
||||
```bash
|
||||
# Allow web console (if needed externally)
|
||||
sudo ufw allow 5000/tcp
|
||||
# Allow web console on LAN
|
||||
sudo ufw allow from 192.168.0.0/16 to any port 5000 proto tcp
|
||||
|
||||
# HBBS API (usually internal only)
|
||||
# Port 21120 does NOT need to be opened - it's localhost only!
|
||||
# Only open RustDesk ports:
|
||||
# Allow HBBS API on LAN (if needed for external tools)
|
||||
sudo ufw allow from 192.168.0.0/16 to any port 21120 proto tcp
|
||||
|
||||
# Standard RustDesk ports
|
||||
sudo ufw allow 21115/tcp
|
||||
sudo ufw allow 21116/tcp
|
||||
sudo ufw allow 21116/udp
|
||||
@@ -489,15 +522,29 @@ sudo ufw allow 21117/tcp
|
||||
|
||||
### Base URL
|
||||
```
|
||||
http://localhost:21120/api
|
||||
http://<server-ip>:21120/api
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
**All API requests require X-API-Key header:**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY_HERE" http://192.168.1.100:21120/api/health
|
||||
```
|
||||
|
||||
**API Key Location**: `/opt/rustdesk/.api_key`
|
||||
|
||||
To retrieve your API key:
|
||||
```bash
|
||||
sudo cat /opt/rustdesk/.api_key
|
||||
```
|
||||
**Note**: API is bound to localhost only and cannot be accessed from external networks.
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### Health Check
|
||||
```http
|
||||
GET /api/health
|
||||
Headers: X-API-Key: <your-api-key>
|
||||
```
|
||||
|
||||
**Response**:
|
||||
@@ -512,6 +559,7 @@ GET /api/health
|
||||
#### List All Peers
|
||||
```http
|
||||
GET /api/peers
|
||||
Headers: X-API-Key: <your-api-key>
|
||||
```
|
||||
|
||||
**Response**:
|
||||
@@ -534,6 +582,14 @@ GET /api/peers
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response (No/Invalid API Key)**:
|
||||
```json
|
||||
{
|
||||
"error": "Unauthorized: Invalid or missing API key"
|
||||
}
|
||||
```
|
||||
Status Code: 401
|
||||
|
||||
### Status Detection Algorithm
|
||||
|
||||
A device is considered **online** if:
|
||||
@@ -574,7 +630,10 @@ BetterDeskConsole/
|
||||
│ ├── dashboard.png
|
||||
│ ├── devices-list.png
|
||||
│ ├── device-details.png
|
||||
│ └── mobile-view.png
|
||||
│ ├── public_key_page.png
|
||||
│ ├── settings_page.png
|
||||
│ ├── user_mgmt.png
|
||||
│ └── about.png
|
||||
├── web/ # Web console
|
||||
│ ├── app.py # Flask application
|
||||
│ ├── app_demo.py # Demo with mock data
|
||||
@@ -782,23 +841,14 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
||||
### Additional Documentation ([docs/](docs/))
|
||||
- **[CHANGELOG.md](docs/CHANGELOG.md)** - Complete version history
|
||||
- **[CONTRIBUTING.md](docs/CONTRIBUTING.md)** - How to contribute
|
||||
- **[DEPRECATION_NOTICE.md](docs/DEPRECATION_NOTICE.md)** - Deprecated features info
|
||||
- **[RELEASE_NOTES_v1.2.0.md](docs/RELEASE_NOTES_v1.2.0.md)** - Latest release notes
|
||||
- **[RELEASE_NOTES_v1.3.0.md](docs/RELEASE_NOTES_v1.3.0.md)** - Version 1.3.0 release notes
|
||||
- **[INSTALLATION_V1.4.0.md](docs/INSTALLATION_V1.4.0.md)** - Detailed installation guide
|
||||
- **[UPDATE_GUIDE.md](docs/UPDATE_GUIDE.md)** - Update instructions
|
||||
- **[DEVELOPMENT_ROADMAP.md](docs/DEVELOPMENT_ROADMAP.md)** - Future plans
|
||||
- **[PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md)** - Project structure overview
|
||||
- **[PROJECT_ORGANIZATION.md](docs/PROJECT_ORGANIZATION.md)** - Project organization details
|
||||
- **[KEY_TROUBLESHOOTING.md](docs/KEY_TROUBLESHOOTING.md)** - Key troubleshooting guide
|
||||
- **[PORT_SECURITY.md](docs/PORT_SECURITY.md)** - Port security information
|
||||
- **[GITHUB_RELEASE_GUIDE.md](docs/GITHUB_RELEASE_GUIDE.md)** - GitHub release guide
|
||||
- **[RELEASE_READY.md](docs/RELEASE_READY.md)** - Release readiness checklist
|
||||
- **[SECURITY_CLEANUP_REPORT.md](docs/SECURITY_CLEANUP_REPORT.md)** - Security cleanup report
|
||||
- **[SECURITY_PLACEHOLDERS.md](docs/SECURITY_PLACEHOLDERS.md)** - Security placeholders
|
||||
- **[SECURITY_URGENT.md](docs/SECURITY_URGENT.md)** - Urgent security items
|
||||
- **[PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md)** - Project structure overview
|
||||
|
||||
### Technical Documentation
|
||||
- **[hbbs-patch/](hbbs-patch/)** - HBBS modification documentation
|
||||
- **[archive/](archive/)** - Archived scripts and old files
|
||||
- **[dev_modules/](dev_modules/)** - Development and testing tools
|
||||
|
||||
---
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
v9 (1.3.0-secure)
|
||||
v1.5.0
|
||||
|
||||
BetterDesk Console Version 9
|
||||
Enhanced Installation Script with Encryption Key Protection
|
||||
BetterDesk Console Version 1.5.0
|
||||
Authentication System & Modern UI
|
||||
|
||||
New in v9:
|
||||
- 🔑 Comprehensive encryption key protection
|
||||
- 🔍 Dynamic .pub file scanning (works with any key filename)
|
||||
- 💾 Enhanced backup system with multiple options
|
||||
- 🐳 Improved Docker detection and handling
|
||||
New in v1.5.0:
|
||||
- 🔐 Authentication system with bcrypt password hashing
|
||||
- 👥 Role-based access control (Admin, Operator, Viewer)
|
||||
- 🎨 Sidebar navigation with 5 main sections
|
||||
- 🔑 Password-protected public key access
|
||||
- ⚙️ Settings page with password change functionality
|
||||
- 👤 User Management panel (admin only)
|
||||
- 🛡️ CSRF protection and rate limiting
|
||||
- 📊 Extended About page with open source credits
|
||||
- 🔧 New repair-keys.sh diagnostic tool
|
||||
- 📚 Complete troubleshooting documentation
|
||||
- ⚠️ Visual warnings for dangerous operations
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
# 🎉 Release Readiness Checklist - BetterDesk Console v1.2.0-v8
|
||||
|
||||
## ✅ Code & Binaries
|
||||
|
||||
- [x] **Precompiled binaries** included in `hbbs-patch/bin/`
|
||||
- [x] hbbs-v8 (9.5 MB) - Signal server with bidirectional ban enforcement
|
||||
- [x] hbbr-v8 (5.0 MB) - Relay server with bidirectional ban enforcement
|
||||
- [x] SHA256 checksums documented
|
||||
|
||||
- [x] **Web console** fully functional
|
||||
- [x] Flask backend with ban management
|
||||
- [x] Modern glassmorphism UI
|
||||
- [x] Material Icons (offline)
|
||||
- [x] Device management, banning, notes
|
||||
|
||||
- [x] **Installation system** verified
|
||||
- [x] install.sh uses precompiled binaries
|
||||
- [x] Automatic backup of existing files
|
||||
- [x] Service restart functionality
|
||||
- [x] No compilation required
|
||||
- [x] Reduced dependencies (no Rust/git needed)
|
||||
|
||||
## ✅ Documentation
|
||||
|
||||
- [x] **Main README.md** updated
|
||||
- [x] Version badge: 1.2.0-v8
|
||||
- [x] Bidirectional ban enforcement description
|
||||
- [x] Precompiled binaries mentioned
|
||||
- [x] Installation time: 2-3 minutes
|
||||
- [x] No compilation requirements
|
||||
|
||||
- [x] **CHANGELOG.md** updated
|
||||
- [x] Version 1.2.0-v8 entry
|
||||
- [x] Bidirectional ban enforcement details
|
||||
- [x] Precompiled binaries explanation
|
||||
- [x] Migration notes
|
||||
|
||||
- [x] **LICENSE** appropriate
|
||||
- [x] AGPL-3.0 (compatible with RustDesk)
|
||||
- [x] Copyright attribution
|
||||
|
||||
- [x] **Technical documentation**
|
||||
- [x] hbbs-patch/BAN_ENFORCEMENT.md - Bidirectional bans
|
||||
- [x] hbbs-patch/SECURITY_AUDIT.md - Security review
|
||||
- [x] hbbs-patch/bin/README.md - Binary documentation
|
||||
- [x] hbbs-patch/bin/CHECKSUMS.md - SHA256 verification
|
||||
- [x] docs/INSTALLATION_V8.md - Complete installation guide
|
||||
- [x] PROJECT_STRUCTURE.md - Updated structure
|
||||
|
||||
## ✅ Security & Privacy
|
||||
|
||||
- [x] **No sensitive data** in files
|
||||
- [x] SSH credentials removed (0 instances found)
|
||||
- [x] IP addresses replaced with placeholders
|
||||
- [x] All occurrences: YOUR_SERVER_IP, YOUR_SSH_USER
|
||||
|
||||
- [x] **No git history** with sensitive data
|
||||
- [x] Not a git repository (clean start possible)
|
||||
|
||||
- [x] **Security documentation**
|
||||
- [x] SECURITY_AUDIT.md - Vulnerability assessment
|
||||
- [x] SECURITY_PLACEHOLDERS.md - Guide for users
|
||||
- [x] SECURITY_CLEANUP_REPORT.md - Cleanup summary
|
||||
|
||||
- [x] **.gitignore** comprehensive
|
||||
- [x] Credentials patterns
|
||||
- [x] Backup files
|
||||
- [x] Old binary versions
|
||||
- [x] Sensitive data patterns
|
||||
|
||||
## ✅ Code Quality
|
||||
|
||||
- [x] **Functional verification**
|
||||
- [x] Bidirectional ban enforcement working
|
||||
- [x] Web console operational
|
||||
- [x] Database migrations included
|
||||
- [x] Service files present
|
||||
|
||||
- [x] **Clean codebase**
|
||||
- [x] Old binaries removed (v2-v5)
|
||||
- [x] Deprecated code in separate directory
|
||||
- [x] No TODO or FIXME markers in critical code
|
||||
|
||||
## ✅ Repository Structure
|
||||
|
||||
```
|
||||
BetterDeskConsole/
|
||||
├── ✅ README.md (updated)
|
||||
├── ✅ LICENSE (AGPL-3.0)
|
||||
├── ✅ VERSION (1.2.0-v8)
|
||||
├── ✅ CHANGELOG.md (v8 entry)
|
||||
├── ✅ .gitignore (comprehensive)
|
||||
├── ✅ PROJECT_STRUCTURE.md (updated)
|
||||
│
|
||||
├── ✅ install.sh (precompiled binaries)
|
||||
├── ✅ update.sh (for upgrades)
|
||||
├── ✅ restore_hbbs.sh (rollback)
|
||||
│
|
||||
├── ✅ web/ (Flask console)
|
||||
│ ├── ✅ app.py (ban management)
|
||||
│ ├── ✅ requirements.txt
|
||||
│ ├── ✅ betterdesk.service
|
||||
│ ├── ✅ templates/index.html
|
||||
│ └── ✅ static/ (CSS, JS, icons)
|
||||
│
|
||||
├── ✅ hbbs-patch/
|
||||
│ ├── ✅ bin/ (NEW - precompiled)
|
||||
│ │ ├── ✅ hbbs-v8 (9.5 MB)
|
||||
│ │ ├── ✅ hbbr-v8 (5.0 MB)
|
||||
│ │ ├── ✅ README.md
|
||||
│ │ └── ✅ CHECKSUMS.md
|
||||
│ │
|
||||
│ ├── ✅ src/ (source patches)
|
||||
│ ├── ✅ build.sh (rebuild script)
|
||||
│ ├── ✅ deploy-v8.sh
|
||||
│ ├── ✅ BAN_ENFORCEMENT.md (v8)
|
||||
│ ├── ✅ SECURITY_AUDIT.md
|
||||
│ └── ✅ test scripts
|
||||
│
|
||||
├── ✅ docs/
|
||||
│ ├── ✅ INSTALLATION_V8.md
|
||||
│ ├── ✅ UPDATE_GUIDE.md
|
||||
│ └── ✅ other guides
|
||||
│
|
||||
├── ✅ migrations/ (database)
|
||||
└── ✅ screenshots/ (UI examples)
|
||||
```
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total Files**: ~100+
|
||||
- **Lines of Code**: ~10,000+
|
||||
- **Documentation**: 15+ markdown files
|
||||
- **Installation Time**: 2-3 minutes (vs 20 min before)
|
||||
- **Dependencies Removed**: git, cargo, rustc (~500 MB saved)
|
||||
- **Binary Size**: 14.5 MB total (hbbs + hbbr)
|
||||
- **Ban Enforcement**: 100% effective, bidirectional
|
||||
|
||||
## 🚀 Ready for Publication
|
||||
|
||||
### GitHub Release Steps
|
||||
|
||||
1. **Initialize git repository**
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: BetterDesk Console v1.2.0-v8"
|
||||
```
|
||||
|
||||
2. **Create GitHub repository**
|
||||
```bash
|
||||
gh repo create BetterDeskConsole --public --source=. --remote=origin
|
||||
```
|
||||
|
||||
3. **Push to GitHub**
|
||||
```bash
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
4. **Create release**
|
||||
```bash
|
||||
gh release create v1.2.0-v8 \
|
||||
--title "BetterDesk Console v1.2.0-v8 - Precompiled Binaries + Bidirectional Bans" \
|
||||
--notes "See CHANGELOG.md for details" \
|
||||
hbbs-patch/bin/hbbs-v8 \
|
||||
hbbs-patch/bin/hbbr-v8
|
||||
```
|
||||
|
||||
5. **Tag binaries**
|
||||
```bash
|
||||
git tag -a v1.2.0-v8 -m "Version 1.2.0-v8 with precompiled binaries"
|
||||
git push origin v1.2.0-v8
|
||||
```
|
||||
|
||||
## 🎯 Next Steps (Post-Release)
|
||||
|
||||
1. **Community Engagement**
|
||||
- [ ] Submit to RustDesk community forum
|
||||
- [ ] Reddit post in r/selfhosted
|
||||
- [ ] Tweet about release
|
||||
|
||||
2. **Monitoring**
|
||||
- [ ] Watch for issues/bug reports
|
||||
- [ ] Monitor installation success rate
|
||||
- [ ] Gather user feedback
|
||||
|
||||
3. **Future Improvements**
|
||||
- [ ] Multi-architecture binaries (ARM64)
|
||||
- [ ] Docker container
|
||||
- [ ] Web console authentication
|
||||
- [ ] Automated testing suite
|
||||
|
||||
## ✅ Final Verification
|
||||
|
||||
Run these commands before publishing:
|
||||
|
||||
```bash
|
||||
# 1. Verify no sensitive data
|
||||
grep -r "192.168.0.110" . --exclude-dir=.git
|
||||
grep -r "unitronix@" . --exclude-dir=.git
|
||||
|
||||
# 2. Verify binaries exist
|
||||
ls -lh hbbs-patch/bin/hbbs-v8 hbbs-patch/bin/hbbr-v8
|
||||
|
||||
# 3. Verify checksums
|
||||
sha256sum hbbs-patch/bin/*-v8
|
||||
|
||||
# 4. Test installer (dry run)
|
||||
bash -n install.sh
|
||||
|
||||
# 5. Verify documentation links
|
||||
find docs -name "*.md" -exec grep -l "YOUR_SERVER_IP" {} \;
|
||||
```
|
||||
|
||||
## 📝 Release Notes Draft
|
||||
|
||||
```markdown
|
||||
# BetterDesk Console v1.2.0-v8
|
||||
|
||||
## 🚀 Major Changes
|
||||
|
||||
- **Precompiled Binaries**: Installation now takes 2-3 minutes (vs 20 minutes)
|
||||
- **Bidirectional Ban Enforcement**: Banned devices blocked in BOTH directions
|
||||
- **No Compilation Required**: Removed Rust toolchain dependency
|
||||
- **Simplified Installation**: Just Python3 + pip3 needed
|
||||
|
||||
## 📦 What's Included
|
||||
|
||||
- HBBS v8 (9.5 MB) - Signal server with bidirectional bans
|
||||
- HBBR v8 (5.0 MB) - Relay server
|
||||
- Web management console (Flask + Material Design)
|
||||
- Complete documentation
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
sudo chmod +x install.sh
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- [Installation Guide](docs/INSTALLATION_V8.md)
|
||||
- [Ban Enforcement Technical Docs](hbbs-patch/BAN_ENFORCEMENT.md)
|
||||
- [Security Audit](hbbs-patch/SECURITY_AUDIT.md)
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
- SHA256 checksums provided
|
||||
- Full source code available
|
||||
- AGPL-3.0 license
|
||||
- Security audit included
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ **READY FOR RELEASE**
|
||||
|
||||
All systems go! 🎉
|
||||
@@ -1,161 +0,0 @@
|
||||
# 🔒 Bezpieczeństwo Danych - Raport Czyszczenia
|
||||
|
||||
**Data:** 6 stycznia 2026
|
||||
**Status:** ✅ ZAKOŃCZONE
|
||||
|
||||
---
|
||||
|
||||
## 📊 Podsumowanie Zmian
|
||||
|
||||
### Pliki Zabezpieczone (18 plików):
|
||||
1. ✅ README.md
|
||||
2. ✅ hbbs-patch/deploy.ps1
|
||||
3. ✅ hbbs-patch/deploy-v6.ps1
|
||||
4. ✅ hbbs-patch/deploy-v8.sh
|
||||
5. ✅ hbbs-patch/QUICKSTART.md
|
||||
6. ✅ hbbs-patch/BAN_ENFORCEMENT.md
|
||||
7. ✅ hbbs-patch/test_ban_enforcement.ps1
|
||||
8. ✅ hbbs-patch/diagnose_ban.ps1
|
||||
9. ✅ docs/UPDATE_REFERENCE.md
|
||||
10. ✅ docs/UPDATE_GUIDE.md
|
||||
11. ✅ docs/QUICKSTART_UPDATE.md
|
||||
12. ✅ dev_modules/update.ps1
|
||||
13. ✅ dev_modules/test_ban_api.sh
|
||||
14. ✅ deprecated/BAN_ENFORCER_TEST.md (częściowo)
|
||||
15. ✅ .gitignore (zaktualizowany)
|
||||
16. ✅ SECURITY_PLACEHOLDERS.md (nowy)
|
||||
17. ✅ SECURITY_AUDIT.md (stworzony wcześniej)
|
||||
18. ✅ Ten raport
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Zamienione Dane
|
||||
|
||||
| Dane Wrażliwe | Placeholder | Wystąpienia |
|
||||
|---------------|-------------|-------------|
|
||||
| `192.168.0.110` | `YOUR_SERVER_IP` | ~150+ |
|
||||
| `unitronix` | `YOUR_SSH_USER` | ~150+ |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Pozostałe Pliki
|
||||
|
||||
### Deprecated (Przestarzałe pliki - ~33 wystąpienia)
|
||||
Pliki w katalogu `deprecated/` zostały częściowo zaktualizowane, ale zawierają starą dokumentację która nie jest już używana:
|
||||
- `deprecated/BAN_ENFORCER.md` - stary system banowania
|
||||
- `deprecated/BAN_ENFORCER_TEST.md` - stare testy
|
||||
|
||||
**Rekomendacja:** Te pliki są przestarzałe i nie powinny być używane. Rozważ:
|
||||
1. Całkowite usunięcie katalogu `deprecated/` przed publikacją
|
||||
2. Lub dokończenie czyszczenia tych plików
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Zabezpieczenia Wdrożone
|
||||
|
||||
### 1. Placeholders w Kodzie ✅
|
||||
Wszystkie aktywne pliki używają placeholderów zamiast rzeczywistych danych.
|
||||
|
||||
### 2. Dokumentacja Bezpieczeństwa ✅
|
||||
- [SECURITY_PLACEHOLDERS.md](SECURITY_PLACEHOLDERS.md) - instrukcja użycia
|
||||
- [SECURITY_AUDIT.md](hbbs-patch/SECURITY_AUDIT.md) - audyt bezpieczeństwa
|
||||
|
||||
### 3. .gitignore Zaktualizowany ✅
|
||||
Dodano ochronę przed przypadkowym commit'em:
|
||||
```gitignore
|
||||
.env
|
||||
.env.local
|
||||
config.local.*
|
||||
*_local.sh
|
||||
*_local.ps1
|
||||
```
|
||||
|
||||
### 4. Szablony Konfiguracji ✅
|
||||
Użytkownicy mogą bezpiecznie tworzyć lokalne pliki konfiguracyjne.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Co Dalej?
|
||||
|
||||
### Przed publikacją na GitHub:
|
||||
|
||||
1. **Sprawdź historię git:**
|
||||
```bash
|
||||
git log --all --full-history -- "*" | grep -i "192.168"
|
||||
```
|
||||
|
||||
2. **Jeśli znajdziesz wrażliwe dane w historii:**
|
||||
```bash
|
||||
# UWAGA: To przepisze całą historię!
|
||||
git filter-branch --tree-filter 'find . -type f -exec sed -i "s/192.168.0.110/YOUR_SERVER_IP/g" {} \;' HEAD
|
||||
```
|
||||
|
||||
Lub użyj BFG Repo-Cleaner:
|
||||
```bash
|
||||
bfg --replace-text passwords.txt
|
||||
git reflog expire --expire=now --all
|
||||
git gc --prune=now --aggressive
|
||||
```
|
||||
|
||||
3. **Usuń deprecated/ przed publikacją:**
|
||||
```bash
|
||||
git rm -r deprecated/
|
||||
git commit -m "Remove deprecated files with sensitive data"
|
||||
```
|
||||
|
||||
4. **Przeglądnij każdy plik przed push:**
|
||||
```bash
|
||||
git diff --name-only origin/main
|
||||
```
|
||||
|
||||
5. **Weryfikacja finalna:**
|
||||
```bash
|
||||
# Sprawdź czy nie ma więcej wrażliwych danych
|
||||
grep -r "192.168.0.110" .
|
||||
grep -r "unitronix@" .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Przed Publikacją
|
||||
|
||||
- [ ] Usunięto katalog `deprecated/` lub wyczyszczono go z danych
|
||||
- [ ] Sprawdzono historię git pod kątem wrażliwych danych
|
||||
- [ ] Przeczytano [SECURITY_PLACEHOLDERS.md](SECURITY_PLACEHOLDERS.md)
|
||||
- [ ] Zweryfikowano że wszystkie przykłady używają placeholderów
|
||||
- [ ] Zaktualizowano README.md z linkiem do SECURITY_PLACEHOLDERS.md
|
||||
- [ ] Przetestowano czy skrypty działają po zamianie placeholderów
|
||||
- [ ] Dodano badge "Security" do README.md
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Bezpieczne Praktyki
|
||||
|
||||
### DO:
|
||||
✅ Używaj zmiennych środowiskowych
|
||||
✅ Twórz lokalne pliki konfiguracyjne (z .gitignore)
|
||||
✅ Regularnie sprawdzaj czy nie commit'ujesz wrażliwych danych
|
||||
✅ Używaj SSH keys zamiast haseł
|
||||
|
||||
### NIE RÓB:
|
||||
❌ Nie commituj plików `.env`
|
||||
❌ Nie wklejaj prawdziwych IP w issue/PR
|
||||
❌ Nie udostępniaj zrzutów ekranu z danymi
|
||||
❌ Nie hardcoduj credentials w kodzie
|
||||
|
||||
---
|
||||
|
||||
## 📞 Kontakt
|
||||
|
||||
Jeśli znajdziesz jakieś wrażliwe dane które pominąłem:
|
||||
1. **NIE** zgłaszaj ich publicznie w issue
|
||||
2. Wyślij prywatną wiadomość do maintainera
|
||||
3. Lub stwórz private security advisory na GitHub
|
||||
|
||||
---
|
||||
|
||||
**Status Bezpieczeństwa:** 🟢 BEZPIECZNY do publikacji (po wykonaniu checklist)
|
||||
|
||||
---
|
||||
|
||||
*Raport wygenerowany automatycznie przez GitHub Copilot*
|
||||
@@ -1,219 +0,0 @@
|
||||
# Security Placeholders - Configuration Guide
|
||||
|
||||
## 🔐 About Placeholders
|
||||
|
||||
This repository contains **placeholders** instead of actual server credentials for security reasons. Before using any scripts or following the documentation, you must replace these placeholders with your actual values.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Placeholders Used
|
||||
|
||||
| Placeholder | Description | Example Value |
|
||||
|------------|-------------|---------------|
|
||||
| `YOUR_SERVER_IP` | Your RustDesk server IP address | `192.168.1.100` or `server.example.com` |
|
||||
| `YOUR_SSH_USER` | SSH username for server access | `admin`, `rustdesk`, etc. |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 How to Replace Placeholders
|
||||
|
||||
### Option 1: Manual Replacement (Recommended for beginners)
|
||||
|
||||
When you see a command like this:
|
||||
```bash
|
||||
ssh YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
```
|
||||
|
||||
Replace it with your actual values:
|
||||
```bash
|
||||
ssh admin@192.168.1.100
|
||||
```
|
||||
|
||||
### Option 2: Global Find & Replace (For advanced users)
|
||||
|
||||
If you want to configure multiple files at once:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
# Navigate to project directory
|
||||
cd C:\path\to\BetterDeskConsole
|
||||
|
||||
# Replace server IP
|
||||
(Get-ChildItem -Recurse -Include *.md,*.ps1,*.sh).ForEach{
|
||||
(Get-Content $_.FullName) -replace 'YOUR_SERVER_IP', '192.168.1.100' |
|
||||
Set-Content $_.FullName
|
||||
}
|
||||
|
||||
# Replace SSH user
|
||||
(Get-ChildItem -Recurse -Include *.md,*.ps1,*.sh).ForEach{
|
||||
(Get-Content $_.FullName) -replace 'YOUR_SSH_USER', 'admin' |
|
||||
Set-Content $_.FullName
|
||||
}
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
# Replace server IP
|
||||
find . -type f \( -name "*.md" -o -name "*.ps1" -o -name "*.sh" \) \
|
||||
-exec sed -i 's/YOUR_SERVER_IP/192.168.1.100/g' {} +
|
||||
|
||||
# Replace SSH user
|
||||
find . -type f \( -name "*.md" -o -name "*.ps1" -o -name "*.sh" \) \
|
||||
-exec sed -i 's/YOUR_SSH_USER/admin/g' {} +
|
||||
```
|
||||
|
||||
### Option 3: Environment Variables (Most secure)
|
||||
|
||||
Set environment variables instead of hardcoding values:
|
||||
|
||||
**PowerShell:**
|
||||
```powershell
|
||||
$env:RUSTDESK_SERVER="192.168.1.100"
|
||||
$env:RUSTDESK_USER="admin"
|
||||
|
||||
# Use in scripts
|
||||
.\update.ps1 -RemoteHost $env:RUSTDESK_SERVER -RemoteUser $env:RUSTDESK_USER
|
||||
```
|
||||
|
||||
**Bash:**
|
||||
```bash
|
||||
export RUSTDESK_SERVER="192.168.1.100"
|
||||
export RUSTDESK_USER="admin"
|
||||
|
||||
# Use in scripts
|
||||
ssh $RUSTDESK_USER@$RUSTDESK_SERVER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Containing Placeholders
|
||||
|
||||
The following files contain placeholders that may need to be replaced:
|
||||
|
||||
### Documentation
|
||||
- [README.md](README.md)
|
||||
- [docs/UPDATE_GUIDE.md](docs/UPDATE_GUIDE.md)
|
||||
- [docs/UPDATE_REFERENCE.md](docs/UPDATE_REFERENCE.md)
|
||||
- [docs/QUICKSTART_UPDATE.md](docs/QUICKSTART_UPDATE.md)
|
||||
- [hbbs-patch/QUICKSTART.md](hbbs-patch/QUICKSTART.md)
|
||||
- [hbbs-patch/BAN_ENFORCEMENT.md](hbbs-patch/BAN_ENFORCEMENT.md)
|
||||
|
||||
### Scripts
|
||||
- [hbbs-patch/deploy.ps1](hbbs-patch/deploy.ps1)
|
||||
- [hbbs-patch/deploy-v6.ps1](hbbs-patch/deploy-v6.ps1)
|
||||
- [hbbs-patch/deploy-v8.sh](hbbs-patch/deploy-v8.sh)
|
||||
- [hbbs-patch/test_ban_enforcement.ps1](hbbs-patch/test_ban_enforcement.ps1)
|
||||
- [hbbs-patch/diagnose_ban.ps1](hbbs-patch/diagnose_ban.ps1)
|
||||
- [dev_modules/update.ps1](dev_modules/update.ps1)
|
||||
- [dev_modules/test_ban_api.sh](dev_modules/test_ban_api.sh)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security Warnings
|
||||
|
||||
### DO NOT:
|
||||
- ❌ Commit files with real credentials to public repositories
|
||||
- ❌ Share screenshots containing real IP addresses or usernames
|
||||
- ❌ Push configuration files with actual server details
|
||||
|
||||
### DO:
|
||||
- ✅ Keep placeholders in version control
|
||||
- ✅ Use environment variables for sensitive data
|
||||
- ✅ Create a local `.env` file (add to `.gitignore`)
|
||||
- ✅ Document your actual values in a secure password manager
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Best Practices
|
||||
|
||||
### 1. Create a Local Configuration File
|
||||
|
||||
Create `.env` file (excluded from git):
|
||||
```bash
|
||||
# .env - DO NOT COMMIT THIS FILE
|
||||
RUSTDESK_SERVER_IP=192.168.1.100
|
||||
RUSTDESK_SSH_USER=admin
|
||||
RUSTDESK_DB_PATH=/opt/rustdesk/db_v2.sqlite3
|
||||
```
|
||||
|
||||
### 2. Add to .gitignore
|
||||
|
||||
```gitignore
|
||||
# Sensitive configuration
|
||||
.env
|
||||
.env.local
|
||||
config.local.ps1
|
||||
*_local.sh
|
||||
```
|
||||
|
||||
### 3. Use Configuration Templates
|
||||
|
||||
Create `config.template` files:
|
||||
```powershell
|
||||
# config.template.ps1
|
||||
$ServerIP = "YOUR_SERVER_IP"
|
||||
$SSHUser = "YOUR_SSH_USER"
|
||||
```
|
||||
|
||||
Then copy and customize:
|
||||
```powershell
|
||||
Copy-Item config.template.ps1 config.local.ps1
|
||||
# Edit config.local.ps1 with your values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
```
|
||||
|
||||
2. **Configure your credentials:**
|
||||
|
||||
**Option A - Environment Variables (Recommended):**
|
||||
```powershell
|
||||
# Windows
|
||||
$env:RUSTDESK_SERVER="192.168.1.100"
|
||||
$env:RUSTDESK_USER="admin"
|
||||
```
|
||||
|
||||
**Option B - Direct Replacement:**
|
||||
Follow "Option 2: Global Find & Replace" above
|
||||
|
||||
3. **Test connection:**
|
||||
```bash
|
||||
ssh YOUR_SSH_USER@YOUR_SERVER_IP # Replace placeholders!
|
||||
```
|
||||
|
||||
4. **Run scripts:**
|
||||
```powershell
|
||||
# After replacing placeholders
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you have questions about configuration:
|
||||
1. Check the [main README](README.md)
|
||||
2. Review [UPDATE_GUIDE.md](docs/UPDATE_GUIDE.md)
|
||||
3. See [Security Audit](hbbs-patch/SECURITY_AUDIT.md)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
Before running any script, verify:
|
||||
- [ ] All `YOUR_SERVER_IP` replaced with actual IP
|
||||
- [ ] All `YOUR_SSH_USER` replaced with actual username
|
||||
- [ ] SSH connection works: `ssh YOUR_SSH_USER@YOUR_SERVER_IP`
|
||||
- [ ] Server paths are correct: `/opt/rustdesk/`, `/opt/BetterDeskConsole/`
|
||||
- [ ] No actual credentials committed to git
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Security is not just about technology—it's about practice. Always think before you commit! 🔐
|
||||
@@ -1,427 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# BetterDesk Console - Installation Script v8
|
||||
#
|
||||
# This script installs the enhanced RustDesk HBBS/HBBR servers with
|
||||
# bidirectional ban enforcement and web management console.
|
||||
#
|
||||
# Features:
|
||||
# - Automatic backup of existing RustDesk installation
|
||||
# - Precompiled HBBS/HBBR binaries with ban enforcement (no compilation needed)
|
||||
# - Bidirectional ban checking (source + target devices)
|
||||
# - Installs Flask web console with glassmorphism UI
|
||||
# - Configures systemd services
|
||||
# - Uses Google Material Icons (offline)
|
||||
#
|
||||
# Ban Enforcement Features (v8):
|
||||
# - Prevents banned devices from initiating connections (source check)
|
||||
# - Prevents connections to banned devices (target check)
|
||||
# - Real-time database sync
|
||||
# - Works for both P2P and relay connections
|
||||
#
|
||||
# Author: GitHub Copilot
|
||||
# License: MIT
|
||||
#############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
RUSTDESK_DIR="/opt/rustdesk"
|
||||
BACKUP_DIR="/opt/rustdesk-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
CONSOLE_DIR="/opt/BetterDeskConsole"
|
||||
TEMP_DIR="/tmp/betterdesk-install"
|
||||
HBBS_API_PORT=21114
|
||||
VERSION="v8" # Current version with bidirectional ban enforcement
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}\n"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
print_header "Checking Dependencies"
|
||||
|
||||
local missing_deps=()
|
||||
|
||||
# Check for required commands (removed cargo - using precompiled binaries)
|
||||
for cmd in python3 pip3 curl systemctl; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
missing_deps+=($cmd)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing_deps[@]} -ne 0 ]; then
|
||||
print_error "Missing dependencies: ${missing_deps[*]}"
|
||||
echo ""
|
||||
echo "Please install missing dependencies:"
|
||||
echo " Ubuntu/Debian: sudo apt install python3 python3-pip curl systemd"
|
||||
echo " CentOS/RHEL: sudo yum install python3 python3-pip curl systemd"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "All dependencies found"
|
||||
}
|
||||
|
||||
backup_rustdesk() {
|
||||
print_header "Backing Up Existing RustDesk Installation"
|
||||
|
||||
if [ ! -d "$RUSTDESK_DIR" ]; then
|
||||
print_warning "No existing RustDesk installation found at $RUSTDESK_DIR"
|
||||
print_info "Will proceed with fresh installation"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Found existing RustDesk installation${NC}"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " 1) Create automatic backup to $BACKUP_DIR"
|
||||
echo " 2) I have already created a manual backup"
|
||||
echo " 3) Skip backup (not recommended)"
|
||||
echo ""
|
||||
read -p "Choose option [1-3]: " backup_choice
|
||||
|
||||
case $backup_choice in
|
||||
1)
|
||||
print_info "Creating backup..."
|
||||
cp -r "$RUSTDESK_DIR" "$BACKUP_DIR"
|
||||
print_success "Backup created at: $BACKUP_DIR"
|
||||
;;
|
||||
2)
|
||||
print_info "Using manual backup"
|
||||
;;
|
||||
3)
|
||||
print_warning "Skipping backup - YOU ARE RESPONSIBLE FOR ANY DATA LOSS"
|
||||
read -p "Are you SURE? Type 'yes' to continue: " confirm
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
print_error "Installation cancelled"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
print_error "Invalid option"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_binaries() {
|
||||
print_header "Installing Enhanced HBBS/HBBR $VERSION"
|
||||
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local bin_dir="$script_dir/hbbs-patch/bin"
|
||||
|
||||
if [ ! -f "$bin_dir/hbbs-$VERSION" ] || [ ! -f "$bin_dir/hbbr-$VERSION" ]; then
|
||||
print_error "Precompiled binaries not found in: $bin_dir"
|
||||
print_info "Expected files: hbbs-$VERSION, hbbr-$VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create RustDesk directory if it doesn't exist
|
||||
mkdir -p "$RUSTDESK_DIR"
|
||||
|
||||
# Stop existing services
|
||||
print_info "Stopping RustDesk services..."
|
||||
systemctl stop rustdesksignal.service 2>/dev/null || true
|
||||
systemctl stop rustdeskrelay.service 2>/dev/null || true
|
||||
pkill -9 hbbs 2>/dev/null || true
|
||||
pkill -9 hbbr 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Backup existing binaries
|
||||
if [ -f "$RUSTDESK_DIR/hbbs" ]; then
|
||||
print_info "Backing up old hbbs..."
|
||||
cp "$RUSTDESK_DIR/hbbs" "$RUSTDESK_DIR/hbbs.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
if [ -f "$RUSTDESK_DIR/hbbr" ]; then
|
||||
print_info "Backing up old hbbr..."
|
||||
cp "$RUSTDESK_DIR/hbbr" "$RUSTDESK_DIR/hbbr.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
# Install new binaries
|
||||
print_info "Installing HBBS $VERSION (with bidirectional ban enforcement)..."
|
||||
cp "$bin_dir/hbbs-$VERSION" "$RUSTDESK_DIR/hbbs"
|
||||
chmod +x "$RUSTDESK_DIR/hbbs"
|
||||
|
||||
print_info "Installing HBBR $VERSION..."
|
||||
cp "$bin_dir/hbbr-$VERSION" "$RUSTDESK_DIR/hbbr"
|
||||
chmod +x "$RUSTDESK_DIR/hbbr"
|
||||
|
||||
print_success "Binaries installed successfully"
|
||||
|
||||
# Restart services
|
||||
print_info "Restarting RustDesk services..."
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
systemctl start rustdesksignal.service 2>/dev/null || true
|
||||
systemctl start rustdeskrelay.service 2>/dev/null || true
|
||||
|
||||
# Wait for services to start
|
||||
sleep 3
|
||||
|
||||
# Verify services
|
||||
local services_ok=true
|
||||
if systemctl is-active --quiet rustdesksignal.service; then
|
||||
print_success "HBBS service is running"
|
||||
else
|
||||
print_warning "HBBS service not running (may need manual start)"
|
||||
services_ok=false
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet rustdeskrelay.service 2>/dev/null; then
|
||||
print_success "HBBR service is running"
|
||||
else
|
||||
print_info "HBBR service not configured (optional)"
|
||||
fi
|
||||
|
||||
# Display version info
|
||||
echo ""
|
||||
print_info "HBBS/HBBR version: $VERSION"
|
||||
print_info "Features:"
|
||||
echo " ✓ Bidirectional ban enforcement"
|
||||
echo " ✓ Source device ban check (prevents banned devices from initiating connections)"
|
||||
echo " ✓ Target device ban check (prevents connections to banned devices)"
|
||||
echo " ✓ Real-time ban database sync"
|
||||
echo ""
|
||||
}
|
||||
|
||||
clone_rustdesk_server() {
|
||||
# This function is no longer needed - using precompiled binaries
|
||||
print_info "Using precompiled binaries - skipping source clone"
|
||||
}
|
||||
|
||||
apply_patches() {
|
||||
# This function is no longer needed - binaries are pre-patched
|
||||
print_info "Binaries are pre-patched - skipping patch application"
|
||||
}
|
||||
|
||||
compile_hbbs() {
|
||||
# This function is no longer needed - using precompiled binaries
|
||||
print_info "Using precompiled binaries - skipping compilation"
|
||||
}
|
||||
|
||||
install_hbbs() {
|
||||
# This function has been replaced by install_binaries()
|
||||
# Kept for compatibility but redirects to new function
|
||||
print_info "Redirecting to install_binaries()..."
|
||||
}
|
||||
|
||||
run_database_migrations() {
|
||||
print_header "Running Database Migrations"
|
||||
|
||||
local db_path="$RUSTDESK_DIR/db_v2.sqlite3"
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local migrations_dir="$script_dir/migrations"
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "$db_path" ]; then
|
||||
print_warning "Database not found at $db_path"
|
||||
print_info "Database will be created automatically when HBBS starts"
|
||||
print_info "Skipping migrations - they will be applied on first run"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create backup of database
|
||||
local backup_file="$db_path.backup-$(date +%Y%m%d-%H%M%S)"
|
||||
print_info "Creating database backup..."
|
||||
cp "$db_path" "$backup_file"
|
||||
print_success "Database backed up to: $backup_file"
|
||||
|
||||
# Check if migrations directory exists
|
||||
if [ ! -d "$migrations_dir" ]; then
|
||||
print_error "Migrations directory not found: $migrations_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run v1.0.1 migration (soft delete)
|
||||
print_info "Running migration v1.0.1 (soft delete)..."
|
||||
if python3 "$migrations_dir/v1.0.1_soft_delete.py"; then
|
||||
print_success "Migration v1.0.1 completed successfully"
|
||||
else
|
||||
print_warning "Migration v1.0.1 failed or already applied"
|
||||
fi
|
||||
|
||||
# Run v1.1.0 migration (device bans)
|
||||
print_info "Running migration v1.1.0 (device bans)..."
|
||||
if python3 "$migrations_dir/v1.1.0_device_bans.py"; then
|
||||
print_success "Migration v1.1.0 completed successfully"
|
||||
else
|
||||
print_warning "Migration v1.1.0 failed or already applied"
|
||||
fi
|
||||
|
||||
print_success "Database migrations completed"
|
||||
echo ""
|
||||
}
|
||||
|
||||
install_web_console() {
|
||||
print_header "Installing Web Management Console"
|
||||
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local web_dir="$script_dir/web"
|
||||
|
||||
if [ ! -d "$web_dir" ]; then
|
||||
print_error "Web directory not found: $web_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create console directory
|
||||
mkdir -p "$CONSOLE_DIR"
|
||||
|
||||
# Copy files
|
||||
print_info "Copying web console files..."
|
||||
cp -r "$web_dir"/* "$CONSOLE_DIR/"
|
||||
|
||||
# Install Python dependencies
|
||||
print_info "Installing Python dependencies..."
|
||||
pip3 install -r "$CONSOLE_DIR/requirements.txt"
|
||||
|
||||
# Create systemd service
|
||||
print_info "Creating systemd service..."
|
||||
cat > /etc/systemd/system/betterdesk.service <<EOF
|
||||
[Unit]
|
||||
Description=BetterDesk Console - RustDesk Web Management
|
||||
After=network.target rustdesksignal.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$CONSOLE_DIR
|
||||
ExecStart=/usr/bin/python3 $CONSOLE_DIR/app.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Enable and start service
|
||||
systemctl daemon-reload
|
||||
systemctl enable betterdesk.service
|
||||
systemctl start betterdesk.service
|
||||
|
||||
# Wait for service to be ready
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet betterdesk.service; then
|
||||
print_success "Web console service is running"
|
||||
else
|
||||
print_error "Web console service failed to start"
|
||||
systemctl status betterdesk.service
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_installation() {
|
||||
print_header "Testing Installation"
|
||||
|
||||
# Test HBBS API
|
||||
print_info "Testing HBBS HTTP API..."
|
||||
if curl -s "http://localhost:$HBBS_API_PORT/api/health" | grep -q "success"; then
|
||||
print_success "HBBS API is responding"
|
||||
else
|
||||
print_error "HBBS API is not responding"
|
||||
fi
|
||||
|
||||
# Test Web Console
|
||||
print_info "Testing Web Console..."
|
||||
if curl -s "http://localhost:5000" > /dev/null; then
|
||||
print_success "Web Console is accessible"
|
||||
else
|
||||
print_error "Web Console is not accessible"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
print_header "Cleaning Up"
|
||||
|
||||
print_info "Removing temporary files..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
print_success "Cleanup completed"
|
||||
}
|
||||
|
||||
show_summary() {
|
||||
print_header "Installation Complete!"
|
||||
|
||||
echo -e "${GREEN}BetterDesk Console has been successfully installed!${NC}"
|
||||
echo ""
|
||||
echo "Access points:"
|
||||
echo " • Web Console: http://$(hostname -I | awk '{print $1}'):5000"
|
||||
echo " • HBBS API: http://localhost:$HBBS_API_PORT/api/health"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
echo " • HBBS: sudo systemctl status rustdesksignal.service"
|
||||
echo " • Web Console: sudo systemctl status betterdesk.service"
|
||||
echo ""
|
||||
|
||||
if [ -d "$BACKUP_DIR" ]; then
|
||||
echo "Backup location:"
|
||||
echo " • $BACKUP_DIR"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Documentation:"
|
||||
echo " • README.md in the installation directory"
|
||||
echo " • GitHub: https://github.com/UNITRONIX/Rustdesk-FreeConsole"
|
||||
echo ""
|
||||
|
||||
print_info "Enjoy your enhanced RustDesk experience!"
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
main() {
|
||||
clear
|
||||
print_header "BetterDesk Console Installer $VERSION"
|
||||
echo "This script will install:"
|
||||
echo " • Enhanced RustDesk HBBS/HBBR with bidirectional ban enforcement"
|
||||
echo " • Web Management Console with Material Design"
|
||||
echo " • Real-time device status monitoring"
|
||||
echo ""
|
||||
echo "Installation method: Precompiled binaries (no compilation required)"
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
check_dependencies
|
||||
backup_rustdesk
|
||||
install_binaries
|
||||
run_database_migrations
|
||||
install_web_console
|
||||
test_installation
|
||||
cleanup
|
||||
show_summary
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Przywróć oryginalny HBBS (bez patcha) i restartuj serwis
|
||||
|
||||
echo "Stopping rustdesksignal service..."
|
||||
systemctl stop rustdesksignal
|
||||
|
||||
echo "Restoring original HBBS binary..."
|
||||
cp /opt/rustdesk/hbbs.backup /opt/rustdesk/hbbs
|
||||
chmod +x /opt/rustdesk/hbbs
|
||||
|
||||
echo "Starting rustdesksignal service..."
|
||||
systemctl start rustdesksignal
|
||||
|
||||
echo "Checking service status..."
|
||||
systemctl status rustdesksignal --no-pager -l
|
||||
|
||||
echo ""
|
||||
echo "HBBS restored successfully!"
|
||||
echo "Ban Enforcer daemon will continue to provide 95% ban protection."
|
||||
@@ -1,399 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# BetterDesk Console - Update Script (v1.1.0)
|
||||
#
|
||||
# This script updates existing BetterDesk installation to version 1.1.0
|
||||
# with device banning system and soft delete functionality.
|
||||
#
|
||||
# Features:
|
||||
# - Automatic database backup before migration
|
||||
# - Executes database migrations (v1.0.1 soft delete + v1.1.0 bans)
|
||||
# - Updates web console files (app.py, script.js, index.html)
|
||||
# - Restarts BetterDesk service
|
||||
# - Verifies installation
|
||||
#
|
||||
# Requirements:
|
||||
# - Existing BetterDesk Console installation
|
||||
# - Root/sudo access
|
||||
# - Python 3.x with Flask
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./update.sh [OPTIONS]
|
||||
# sudo ./update.sh --rustdesk-dir /custom/path/rustdesk
|
||||
# sudo ./update.sh --console-dir /custom/path/console
|
||||
#
|
||||
# Author: GitHub Copilot
|
||||
# License: MIT
|
||||
#############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default configuration
|
||||
RUSTDESK_DIR="/opt/rustdesk"
|
||||
CONSOLE_DIR="/opt/BetterDeskConsole"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--rustdesk-dir)
|
||||
RUSTDESK_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--console-dir)
|
||||
CONSOLE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: sudo ./update.sh [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --rustdesk-dir PATH Path to RustDesk installation (default: /opt/rustdesk)"
|
||||
echo " --console-dir PATH Path to BetterDesk Console (default: /opt/BetterDeskConsole)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " sudo ./update.sh"
|
||||
echo " sudo ./update.sh --rustdesk-dir /custom/rustdesk"
|
||||
echo " sudo ./update.sh --console-dir /var/www/betterdesk"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set derived paths
|
||||
DB_PATH="$RUSTDESK_DIR/db_v2.sqlite3"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_DIR="/opt/betterdesk-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}\n"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${CYAN}→ $1${NC}"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root (use sudo)"
|
||||
fi
|
||||
|
||||
print_header "BetterDesk Console - Update to v1.1.0"
|
||||
|
||||
echo -e "${CYAN}Configuration:${NC}"
|
||||
echo " RustDesk directory: $RUSTDESK_DIR"
|
||||
echo " Console directory: $CONSOLE_DIR"
|
||||
echo " Database path: $DB_PATH"
|
||||
echo ""
|
||||
echo -e "${CYAN}This update includes:${NC}"
|
||||
echo " • Soft delete system for devices (v1.0.1)"
|
||||
echo " • Device banning system (v1.1.0)"
|
||||
echo " • Enhanced UI with ban controls"
|
||||
echo " • Input validation and security improvements"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠ WARNING: This will modify the database and restart services${NC}"
|
||||
echo ""
|
||||
read -p "Continue with update? [y/N]: " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Update cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if BetterDesk is installed
|
||||
print_header "Step 1: Checking Installation"
|
||||
|
||||
if [ ! -d "$CONSOLE_DIR" ]; then
|
||||
print_error "BetterDesk Console not found at $CONSOLE_DIR"
|
||||
fi
|
||||
print_success "Found BetterDesk Console"
|
||||
|
||||
if [ ! -f "$DB_PATH" ]; then
|
||||
print_error "Database not found at $DB_PATH"
|
||||
fi
|
||||
print_success "Found database"
|
||||
|
||||
# Check if service exists
|
||||
if ! systemctl list-unit-files | grep -q "betterdesk.service"; then
|
||||
print_warning "BetterDesk service not found, will skip restart"
|
||||
SERVICE_EXISTS=false
|
||||
else
|
||||
print_success "Found BetterDesk service"
|
||||
SERVICE_EXISTS=true
|
||||
fi
|
||||
|
||||
# Create backup
|
||||
print_header "Step 2: Creating Backup"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
print_info "Backup directory: $BACKUP_DIR"
|
||||
|
||||
# Backup database
|
||||
print_info "Backing up database..."
|
||||
cp "$DB_PATH" "$BACKUP_DIR/db_v2.sqlite3.backup"
|
||||
print_success "Database backed up"
|
||||
|
||||
# Backup web files
|
||||
print_info "Backing up web console files..."
|
||||
if [ -f "$CONSOLE_DIR/app.py" ]; then
|
||||
cp "$CONSOLE_DIR/app.py" "$BACKUP_DIR/app.py.backup"
|
||||
fi
|
||||
if [ -f "$CONSOLE_DIR/static/script.js" ]; then
|
||||
cp "$CONSOLE_DIR/static/script.js" "$BACKUP_DIR/script.js.backup"
|
||||
fi
|
||||
if [ -f "$CONSOLE_DIR/templates/index.html" ]; then
|
||||
cp "$CONSOLE_DIR/templates/index.html" "$BACKUP_DIR/index.html.backup"
|
||||
fi
|
||||
print_success "Web files backed up"
|
||||
|
||||
echo ""
|
||||
print_success "Backup completed: $BACKUP_DIR"
|
||||
|
||||
# Execute migrations
|
||||
print_header "Step 3: Database Migration"
|
||||
|
||||
print_info "Running migration v1.0.1 (soft delete)..."
|
||||
if [ -f "$SCRIPT_DIR/migrations/v1.0.1_soft_delete.py" ]; then
|
||||
python3 "$SCRIPT_DIR/migrations/v1.0.1_soft_delete.py" <<EOF
|
||||
y
|
||||
EOF
|
||||
print_success "Migration v1.0.1 completed"
|
||||
else
|
||||
print_warning "Migration v1.0.1 script not found, skipping"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_info "Running migration v1.1.0 (device bans)..."
|
||||
if [ -f "$SCRIPT_DIR/migrations/v1.1.0_device_bans.py" ]; then
|
||||
python3 "$SCRIPT_DIR/migrations/v1.1.0_device_bans.py" <<EOF
|
||||
y
|
||||
EOF
|
||||
print_success "Migration v1.1.0 completed"
|
||||
else
|
||||
print_error "Migration v1.1.0 script not found at $SCRIPT_DIR/migrations/"
|
||||
fi
|
||||
|
||||
# Update web files
|
||||
print_header "Step 4: Updating Web Console Files"
|
||||
|
||||
if [ ! -d "$SCRIPT_DIR/web" ]; then
|
||||
print_error "Web directory not found at $SCRIPT_DIR/web"
|
||||
fi
|
||||
|
||||
# Update app.py
|
||||
if [ -f "$SCRIPT_DIR/web/app.py" ]; then
|
||||
print_info "Updating app.py..."
|
||||
cp "$SCRIPT_DIR/web/app.py" "$CONSOLE_DIR/app.py"
|
||||
print_success "app.py updated"
|
||||
else
|
||||
print_error "app.py not found in $SCRIPT_DIR/web/"
|
||||
fi
|
||||
|
||||
# Update script.js
|
||||
if [ -f "$SCRIPT_DIR/web/static/script.js" ]; then
|
||||
print_info "Updating script.js..."
|
||||
mkdir -p "$CONSOLE_DIR/static"
|
||||
cp "$SCRIPT_DIR/web/static/script.js" "$CONSOLE_DIR/static/script.js"
|
||||
print_success "script.js updated"
|
||||
else
|
||||
print_error "script.js not found in $SCRIPT_DIR/web/static/"
|
||||
fi
|
||||
|
||||
# Update index.html
|
||||
if [ -f "$SCRIPT_DIR/web/templates/index.html" ]; then
|
||||
print_info "Updating index.html..."
|
||||
mkdir -p "$CONSOLE_DIR/templates"
|
||||
cp "$SCRIPT_DIR/web/templates/index.html" "$CONSOLE_DIR/templates/index.html"
|
||||
print_success "index.html updated"
|
||||
else
|
||||
print_error "index.html not found in $SCRIPT_DIR/web/templates/"
|
||||
fi
|
||||
|
||||
# Set proper permissions
|
||||
print_info "Setting permissions..."
|
||||
chown -R $(stat -c '%U:%G' "$CONSOLE_DIR") "$CONSOLE_DIR" 2>/dev/null || true
|
||||
print_success "Permissions set"
|
||||
|
||||
# Restart service
|
||||
if [ "$SERVICE_EXISTS" = true ]; then
|
||||
print_header "Step 5: Restarting Service"
|
||||
|
||||
print_info "Stopping BetterDesk service..."
|
||||
systemctl stop betterdesk
|
||||
sleep 2
|
||||
|
||||
print_info "Starting BetterDesk service..."
|
||||
systemctl start betterdesk
|
||||
sleep 3
|
||||
|
||||
if systemctl is-active --quiet betterdesk; then
|
||||
print_success "BetterDesk service is running"
|
||||
else
|
||||
print_error "Failed to start BetterDesk service"
|
||||
fi
|
||||
else
|
||||
print_header "Step 5: Service Restart (Skipped)"
|
||||
print_warning "Please restart BetterDesk manually"
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
print_header "Step 6: Verification"
|
||||
|
||||
# Check database schema
|
||||
print_info "Verifying database schema..."
|
||||
COLUMNS=$(sqlite3 "$DB_PATH" "PRAGMA table_info(peer);" | wc -l)
|
||||
if [ "$COLUMNS" -ge 16 ]; then
|
||||
print_success "Database schema updated (16+ columns)"
|
||||
else
|
||||
print_warning "Database may not have all new columns ($COLUMNS found)"
|
||||
fi
|
||||
|
||||
# Check if web console is accessible (if service is running)
|
||||
if [ "$SERVICE_EXISTS" = true ]; then
|
||||
print_info "Checking web console..."
|
||||
sleep 2
|
||||
if curl -s http://localhost:5000/api/stats > /dev/null 2>&1; then
|
||||
print_success "Web console is responding"
|
||||
else
|
||||
print_warning "Web console may not be responding on port 5000"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Ban Enforcer (optional but recommended)
|
||||
print_header "Step 7: Install Ban Enforcer (Optional)"
|
||||
|
||||
echo -e "${CYAN}Ban Enforcer blocks banned devices from connecting to RustDesk.${NC}"
|
||||
echo -e "${CYAN}RustDesk server doesn't check is_banned column by default.${NC}"
|
||||
echo ""
|
||||
echo -e "Without enforcer: Devices show as banned in UI but can still connect"
|
||||
echo -e "With enforcer: Devices are actively blocked from connecting"
|
||||
echo ""
|
||||
|
||||
if [ -f "$CONSOLE_DIR/ban_enforcer.py" ] && systemctl is-active --quiet rustdesk-ban-enforcer 2>/dev/null; then
|
||||
print_info "Ban Enforcer is already installed and running"
|
||||
read -p "Do you want to update it? (y/N): " UPDATE_ENFORCER
|
||||
if [[ "$UPDATE_ENFORCER" =~ ^[Yy]$ ]]; then
|
||||
print_info "Updating Ban Enforcer..."
|
||||
cp ban_enforcer.py "$CONSOLE_DIR/"
|
||||
chmod +x "$CONSOLE_DIR/ban_enforcer.py"
|
||||
systemctl restart rustdesk-ban-enforcer
|
||||
print_success "Ban Enforcer updated"
|
||||
fi
|
||||
else
|
||||
read -p "Install Ban Enforcer? (y/N): " INSTALL_ENFORCER
|
||||
|
||||
if [[ "$INSTALL_ENFORCER" =~ ^[Yy]$ ]]; then
|
||||
if [ -f "ban_enforcer.py" ] && [ -f "rustdesk-ban-enforcer.service" ]; then
|
||||
print_info "Installing Ban Enforcer..."
|
||||
|
||||
# Copy files
|
||||
cp ban_enforcer.py "$CONSOLE_DIR/"
|
||||
chmod +x "$CONSOLE_DIR/ban_enforcer.py"
|
||||
|
||||
# Configure and install service
|
||||
sed "s|Environment=\"DB_PATH=/opt/rustdesk/db_v2.sqlite3\"|Environment=\"DB_PATH=$DB_PATH\"|g" \
|
||||
rustdesk-ban-enforcer.service > /tmp/rustdesk-ban-enforcer.service
|
||||
cp /tmp/rustdesk-ban-enforcer.service /etc/systemd/system/
|
||||
chmod 644 /etc/systemd/system/rustdesk-ban-enforcer.service
|
||||
rm /tmp/rustdesk-ban-enforcer.service
|
||||
|
||||
# Enable and start
|
||||
systemctl daemon-reload
|
||||
systemctl enable rustdesk-ban-enforcer
|
||||
systemctl start rustdesk-ban-enforcer
|
||||
|
||||
sleep 2
|
||||
if systemctl is-active --quiet rustdesk-ban-enforcer; then
|
||||
print_success "Ban Enforcer installed and running"
|
||||
else
|
||||
print_warning "Ban Enforcer installed but failed to start"
|
||||
echo "Check logs: sudo journalctl -u rustdesk-ban-enforcer -n 50"
|
||||
fi
|
||||
else
|
||||
print_warning "Ban Enforcer files not found in current directory"
|
||||
print_info "You can install it later using: ./install_ban_enforcer.sh"
|
||||
fi
|
||||
else
|
||||
print_info "Ban Enforcer installation skipped"
|
||||
print_warning "Banned devices will show in UI but may still connect"
|
||||
echo ""
|
||||
echo "To install later, run:"
|
||||
echo " sudo ./install_ban_enforcer.sh"
|
||||
echo ""
|
||||
echo "Or manually:"
|
||||
echo " sudo cp ban_enforcer.py $CONSOLE_DIR/"
|
||||
echo " sudo cp rustdesk-ban-enforcer.service /etc/systemd/system/"
|
||||
echo " sudo systemctl daemon-reload"
|
||||
echo " sudo systemctl enable --now rustdesk-ban-enforcer"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Final summary
|
||||
print_header "Update Complete!"
|
||||
|
||||
echo -e "${GREEN}✓ Database migrated to v1.1.0${NC}"
|
||||
echo -e "${GREEN}✓ Web console files updated${NC}"
|
||||
echo -e "${GREEN}✓ Backup created: $BACKUP_DIR${NC}"
|
||||
if [ "$SERVICE_EXISTS" = true ]; then
|
||||
echo -e "${GREEN}✓ Service restarted${NC}"
|
||||
fi
|
||||
if systemctl is-active --quiet rustdesk-ban-enforcer 2>/dev/null; then
|
||||
echo -e "${GREEN}✓ Ban Enforcer active${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${CYAN}New Features:${NC}"
|
||||
echo " • Soft delete for devices (is_deleted, deleted_at, updated_at)"
|
||||
echo " • Device banning system (is_banned, banned_at, banned_by, ban_reason)"
|
||||
echo " • Ban/Unban buttons in web interface"
|
||||
echo " • Enhanced input validation and security"
|
||||
echo " • Banned devices statistics card"
|
||||
if systemctl is-active --quiet rustdesk-ban-enforcer 2>/dev/null; then
|
||||
echo " • Active connection blocking for banned devices ✓"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${CYAN}Access the console:${NC}"
|
||||
echo " http://localhost:5000"
|
||||
echo ""
|
||||
if systemctl is-active --quiet rustdesk-ban-enforcer 2>/dev/null; then
|
||||
echo -e "${CYAN}Ban Enforcer Status:${NC}"
|
||||
echo " Service: $(systemctl is-active rustdesk-ban-enforcer)"
|
||||
echo " Logs: sudo journalctl -u rustdesk-ban-enforcer -f"
|
||||
echo ""
|
||||
fi
|
||||
echo -e "${YELLOW}Rollback Instructions (if needed):${NC}"
|
||||
echo " 1. Stop service: sudo systemctl stop betterdesk"
|
||||
echo " 2. Restore database: sudo cp $BACKUP_DIR/db_v2.sqlite3.backup $DB_PATH"
|
||||
echo " 3. Restore files: sudo cp $BACKUP_DIR/*.backup $CONSOLE_DIR/"
|
||||
echo " 4. Start service: sudo systemctl start betterdesk"
|
||||
echo ""
|
||||
print_success "Update completed successfully!"
|
||||
@@ -5,6 +5,168 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.0] - 2026-01-11
|
||||
|
||||
### 🔐 Security & Authentication Update
|
||||
|
||||
**Major Security Enhancement**: Added comprehensive authentication system for both web console and HBBS HTTP API, plus LAN access capabilities.
|
||||
|
||||
### Added
|
||||
|
||||
#### Authentication System
|
||||
- **User Login System**:
|
||||
- bcrypt password hashing (cost 12)
|
||||
- 24-hour session tokens stored in database
|
||||
- Login page with secure password handling
|
||||
- Session validation on all protected routes
|
||||
|
||||
- **Role-Based Access Control (RBAC)**:
|
||||
- Three roles: admin, operator, viewer
|
||||
- Role-specific permissions for device management
|
||||
- Admin-only access to user management
|
||||
- Audit logging for all actions
|
||||
|
||||
- **User Management Panel**:
|
||||
- Create/edit/delete users
|
||||
- Activate/deactivate accounts
|
||||
- Role assignment
|
||||
- Password reset functionality
|
||||
- User list with status indicators
|
||||
|
||||
#### API Security
|
||||
- **X-API-Key Authentication**:
|
||||
- 64-character random API keys generated during installation
|
||||
- Middleware verification on all HBBS API endpoints
|
||||
- Stored securely in `/opt/rustdesk/.api_key` with 600 permissions
|
||||
- Automatic key injection by web console
|
||||
- 401 Unauthorized for missing/invalid keys
|
||||
|
||||
- **LAN Access**:
|
||||
- HBBS API now binds to `0.0.0.0:21120` (accessible on LAN)
|
||||
- Web console binds to `0.0.0.0:5000` (accessible on LAN)
|
||||
- Protected by authentication (API key + user login)
|
||||
- External tools can use API with X-API-Key header
|
||||
|
||||
#### Web Console Features
|
||||
- **Sidebar Navigation**: Modern sidebar menu with icon-based navigation
|
||||
- **Password-Protected Key Access**: Public key requires password verification
|
||||
- **Password Change**: Users can change their passwords in Settings
|
||||
- **Removed Devices Tab**: Simplified interface, devices managed on Dashboard
|
||||
- **Expanded About Page**: Credits, GitHub links, open source information
|
||||
- **Enhanced Settings**: Password change, session management, preferences
|
||||
|
||||
#### Installation & Updates
|
||||
- **API Key Generation**: Automatic during installation via `openssl rand`
|
||||
- **Environment Variables**: HBBS_API_KEY, FLASK_HOST, FLASK_PORT, FLASK_DEBUG
|
||||
- **Service Configuration**: Updated systemd services with new environment vars
|
||||
- **Update Script**: `update-to-v1.4.0.sh` for existing installations
|
||||
- **Backward Compatibility**: Preserves existing configurations during update
|
||||
|
||||
### Changed
|
||||
|
||||
- **API Binding**: Changed from `127.0.0.1` (localhost-only) to `0.0.0.0` (LAN-accessible)
|
||||
- **Authentication Required**: All API endpoints now require X-API-Key header
|
||||
- **Web Console Access**: Requires user login instead of open access
|
||||
- **Session Management**: 24-hour sessions instead of permanent access
|
||||
- **Database Schema**: Added `users`, `sessions`, `audit_log` tables
|
||||
|
||||
### Security
|
||||
|
||||
- ✅ **Authentication**: All services protected by authentication
|
||||
- ✅ **Encrypted Passwords**: bcrypt hashing for user passwords
|
||||
- ✅ **API Key Auth**: X-API-Key header prevents unauthorized API access
|
||||
- ✅ **Session Tokens**: Time-limited tokens with automatic expiration
|
||||
- ✅ **Audit Trail**: All administrative actions logged
|
||||
- ✅ **Secure Storage**: API keys with 600 permissions
|
||||
- ✅ **XSS Protection**: Input sanitization throughout
|
||||
- ✅ **SQL Injection Prevention**: Parameterized queries only
|
||||
- ✅ **CSRF Protection**: Session-based validation
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Files Modified**:
|
||||
- `hbbs-patch/src/http_api.rs` - Added X-API-Key middleware
|
||||
- `web/app_v14.py` - Added authentication, user management, API key loading
|
||||
- `web/auth.py` - Password hashing, session management, user CRUD
|
||||
- `web/templates/login.html` - New login page
|
||||
- `web/templates/index_v14.html` - Sidebar navigation, user management UI
|
||||
- `web/static/script_v14.js` - User management, password change, key verification
|
||||
- `install-improved.sh` - API key generation and service configuration
|
||||
- `update-to-v1.4.0.sh` - Update script for existing installations
|
||||
|
||||
- **Database Migration**: `migrations/v1.4.0_auth_system.py`
|
||||
- Creates `users` table with roles
|
||||
- Creates `sessions` table for session management
|
||||
- Creates `audit_log` table for action tracking
|
||||
- Generates default admin user
|
||||
|
||||
- **API Endpoints** (all require X-API-Key):
|
||||
- `GET /api/health` - Health check
|
||||
- `GET /api/peers` - List all peers with online status
|
||||
|
||||
- **Web Endpoints** (all require login except `/login`):
|
||||
- `GET /login` - Login page
|
||||
- `POST /login` - Authenticate user
|
||||
- `GET /logout` - End session
|
||||
- `GET /` - Dashboard
|
||||
- `GET /api/users` - List users (admin only)
|
||||
- `POST /api/users` - Create user (admin only)
|
||||
- `PUT /api/users/<id>` - Update user (admin only)
|
||||
- `DELETE /api/users/<id>` - Delete user (admin only)
|
||||
- `POST /api/change-password` - Change password
|
||||
- `POST /api/verify-password` - Verify password for key access
|
||||
|
||||
### Migration Path
|
||||
|
||||
**For new installations:**
|
||||
```bash
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
- Automatically generates API key
|
||||
- Configures services for LAN access
|
||||
- Creates default admin user
|
||||
|
||||
**For existing installations:**
|
||||
```bash
|
||||
sudo ./update-to-v1.4.0.sh
|
||||
```
|
||||
- Creates automatic backup
|
||||
- Runs database migration
|
||||
- Generates API key if not exists
|
||||
- Updates systemd services
|
||||
- Preserves existing configuration
|
||||
- Rollback capability on failure
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated `README.md` with authentication instructions
|
||||
- Updated `hbbs-patch/README.md` with API security documentation
|
||||
- Updated `hbbs-patch/SECURITY_AUDIT.md` with v1.4.0 security review
|
||||
- Updated `docs/PORT_SECURITY.md` with LAN access notes
|
||||
- Added API key retrieval instructions
|
||||
|
||||
### Known Issues
|
||||
|
||||
- 26 Pylance type warnings in `app_v14.py` for `log_audit()` parameters (non-critical)
|
||||
- API key must be manually distributed to external tools
|
||||
|
||||
### Upgrade Notes
|
||||
|
||||
**Breaking Changes:**
|
||||
- Existing API clients must add `X-API-Key` header
|
||||
- Web console now requires user login
|
||||
- Sessions expire after 24 hours
|
||||
|
||||
**Recommended Actions After Upgrade:**
|
||||
1. Login with default admin credentials (shown after migration)
|
||||
2. Change admin password immediately
|
||||
3. Delete `/opt/BetterDeskConsole/admin_credentials.txt`
|
||||
4. Create additional users with appropriate roles
|
||||
5. Update external tools with API key from `/opt/rustdesk/.api_key`
|
||||
6. Configure firewall for LAN access if needed
|
||||
|
||||
---
|
||||
|
||||
## [1.3.0-secure] - 2026-01-10
|
||||
|
||||
### 🔒 Security Update: Localhost-Only API Binding
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# ⚠️ Deprecation Notice
|
||||
|
||||
## Ban Enforcer (Python Daemon) - DEPRECATED
|
||||
|
||||
**Effective Date**: January 5, 2026
|
||||
**Version**: 1.2.0
|
||||
|
||||
---
|
||||
|
||||
### Status: OBSOLETE
|
||||
|
||||
The Python-based `ban_enforcer.py` daemon has been **replaced** by native HBBS ban checking as of version 1.2.0.
|
||||
|
||||
### What Changed?
|
||||
|
||||
**Old System (v1.1.0):**
|
||||
- External Python daemon running every 2 seconds
|
||||
- Cleared UUID/PK of banned devices
|
||||
- ~95% effectiveness due to race conditions
|
||||
- Required systemd service management
|
||||
- Additional process overhead
|
||||
|
||||
**New System (v1.2.0):**
|
||||
- Native ban check integrated into HBBS server
|
||||
- Checks `is_banned` during device registration
|
||||
- **100% effectiveness** - no race conditions
|
||||
- No external processes needed
|
||||
- Minimal performance impact (~1ms per check)
|
||||
|
||||
### Migration Guide
|
||||
|
||||
If you're using Ban Enforcer from v1.1.0:
|
||||
|
||||
1. **Install Patched HBBS Binary**
|
||||
```bash
|
||||
cd hbbs-patch
|
||||
./build.sh
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
2. **Stop Ban Enforcer Service**
|
||||
```bash
|
||||
sudo systemctl stop rustdesk-ban-enforcer
|
||||
sudo systemctl disable rustdesk-ban-enforcer
|
||||
```
|
||||
|
||||
3. **Verify Ban Functionality**
|
||||
- Ban a test device through web console
|
||||
- Try to connect from that device
|
||||
- Connection should be rejected with "UUID mismatch" error
|
||||
|
||||
4. **Remove Service (Optional)**
|
||||
```bash
|
||||
sudo rm /etc/systemd/system/rustdesk-ban-enforcer.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
### Why Keep the Files?
|
||||
|
||||
The Ban Enforcer code remains in the repository for:
|
||||
- **Reference**: Understanding the evolution of the ban system
|
||||
- **Rollback**: Emergency fallback if needed
|
||||
- **Educational**: Learning how external enforcement worked
|
||||
- **Historical**: Documenting the project's development
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Do NOT use Ban Enforcer for new installations.** Always use the native HBBS ban check (v1.2.0+).
|
||||
|
||||
For existing users: Migrate to the native system at your earliest convenience for improved reliability and performance.
|
||||
|
||||
---
|
||||
|
||||
### Files Affected
|
||||
|
||||
These files are now **deprecated** but kept for reference:
|
||||
- `ban_enforcer.py` - Python daemon script
|
||||
- `install_ban_enforcer.sh` - Installation script
|
||||
- `rustdesk-ban-enforcer.service` - Systemd service file
|
||||
- `BAN_ENFORCER.md` - Documentation
|
||||
- `BAN_ENFORCER_TEST.md` - Testing guide
|
||||
|
||||
### Support
|
||||
|
||||
Ban Enforcer will **NOT** receive:
|
||||
- Bug fixes
|
||||
- Security updates
|
||||
- Feature enhancements
|
||||
- Compatibility updates
|
||||
|
||||
All future development focuses on the native HBBS ban check system.
|
||||
|
||||
---
|
||||
|
||||
**For questions or issues**, please refer to:
|
||||
- Native ban system: [hbbs-patch/BAN_CHECK_PATCH.md](hbbs-patch/BAN_CHECK_PATCH.md)
|
||||
- Quick setup: [hbbs-patch/QUICKSTART.md](hbbs-patch/QUICKSTART.md)
|
||||
- General issues: [GitHub Issues](https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues)
|
||||
@@ -1,869 +0,0 @@
|
||||
# BetterDesk Console - Enhanced Features Roadmap & Implementation Plan
|
||||
|
||||
## Project Vision
|
||||
|
||||
Transform BetterDesk Console from a monitoring tool into a **full-featured Enterprise Remote Access Management System** with advanced security, device management, and access control capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Development Phases
|
||||
|
||||
### Phase 1: Bug Fixes & Stability (v1.0.1) - **1-2 Days**
|
||||
**Status**: Ready to implement
|
||||
**Complexity**: Low
|
||||
|
||||
#### Features
|
||||
- [ ] Fix device deletion functionality
|
||||
- [ ] Improve device ID change handling (add warnings)
|
||||
- [ ] Add confirmation dialogs for destructive operations
|
||||
- [ ] Better error messages and user feedback
|
||||
- [ ] Input validation on all forms
|
||||
|
||||
#### Technical Implementation
|
||||
- **Files to modify**: `web/app.py`, `web/static/script.js`
|
||||
- **Database**: Add soft-delete flag instead of hard delete
|
||||
- **UI**: Bootstrap modal confirmations
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Authentication & Basic Security (v1.1) - **3-5 Days**
|
||||
**Status**: Architecture defined
|
||||
**Complexity**: Medium
|
||||
|
||||
#### Features
|
||||
- [ ] User authentication system (login/logout)
|
||||
- [ ] Session management with Flask-Login
|
||||
- [ ] Password hashing (bcrypt)
|
||||
- [ ] Role-based access control (Admin, Viewer, Operator)
|
||||
- [ ] User management page
|
||||
- [ ] Audit logs for all actions
|
||||
|
||||
#### Technical Implementation
|
||||
```python
|
||||
# New database tables
|
||||
users(id, username, password_hash, role, created_at, last_login)
|
||||
sessions(id, user_id, token, expires_at, ip_address)
|
||||
audit_logs(id, user_id, action, target, timestamp, details)
|
||||
```
|
||||
|
||||
**Security Considerations**:
|
||||
- HTTPS enforcement (self-signed cert generation)
|
||||
- CSRF protection (Flask-WTF)
|
||||
- Rate limiting (Flask-Limiter)
|
||||
- Secure session cookies (httpOnly, secure, sameSite)
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Device Banning System (v1.2) - **5-7 Days**
|
||||
**Status**: Architecture designed
|
||||
**Complexity**: High
|
||||
|
||||
#### Features
|
||||
- [ ] **Bidirectional Ban Logic**:
|
||||
- Banned device cannot connect to ANY device
|
||||
- Any device cannot connect to banned device
|
||||
- Ban applies to both peer and target
|
||||
|
||||
- [ ] Web console ban management:
|
||||
- Ban/unban devices from UI
|
||||
- Temporary bans (time-limited)
|
||||
- Ban reasons and notes
|
||||
- Ban history log
|
||||
|
||||
- [ ] HBBS-level enforcement:
|
||||
- Check ban status before allowing connection
|
||||
- Real-time ban list synchronization
|
||||
- Connection rejection with custom message
|
||||
|
||||
#### HBBS Modifications Required
|
||||
|
||||
**New File**: `src/ban_manager.rs`
|
||||
```rust
|
||||
pub struct BanManager {
|
||||
banned_ids: Arc<RwLock<HashSet<String>>>,
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl BanManager {
|
||||
// Check if connection should be allowed
|
||||
pub async fn is_connection_allowed(&self, peer_id: &str, target_id: &str) -> bool {
|
||||
let banned = self.banned_ids.read().await;
|
||||
// Block if either peer or target is banned
|
||||
!banned.contains(peer_id) && !banned.contains(target_id)
|
||||
}
|
||||
|
||||
// Real-time ban list updates
|
||||
pub async fn sync_from_db(&self) {
|
||||
// Periodic database sync
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modify**: `src/rendezvous_server.rs`
|
||||
```rust
|
||||
// In handle_request or connection logic
|
||||
if !ban_manager.is_connection_allowed(&peer_id, &target_id).await {
|
||||
return Err(ConnectionBlocked::Banned);
|
||||
}
|
||||
```
|
||||
|
||||
**New HTTP API Endpoints**:
|
||||
```
|
||||
POST /api/device/ban - Ban a device
|
||||
POST /api/device/unban - Unban a device
|
||||
GET /api/bans - List all banned devices
|
||||
```
|
||||
|
||||
**Database Schema**:
|
||||
```sql
|
||||
CREATE TABLE device_bans (
|
||||
id INTEGER PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
banned_by TEXT,
|
||||
reason TEXT,
|
||||
banned_at TIMESTAMP,
|
||||
expires_at TIMESTAMP NULL,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE INDEX idx_device_bans_device_id ON device_bans(device_id);
|
||||
CREATE INDEX idx_device_bans_active ON device_bans(is_active);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: API Key Management (v1.3) - **4-6 Days**
|
||||
**Status**: Design phase
|
||||
**Complexity**: Medium-High
|
||||
|
||||
#### Features
|
||||
- [ ] **API Key System**:
|
||||
- Generate unique API keys for devices
|
||||
- Key-based authentication (instead of ID only)
|
||||
- Key rotation and expiration
|
||||
- Scoped permissions per key
|
||||
|
||||
- [ ] **Quick Connect Codes**:
|
||||
- Generate short-lived connection codes (6-8 chars)
|
||||
- QR code generation for mobile
|
||||
- One-time use codes
|
||||
- Time-limited validity (15 minutes default)
|
||||
|
||||
#### Technical Design
|
||||
|
||||
**API Key Format**:
|
||||
```
|
||||
BDC_[type]_[random32chars]_[checksum4]
|
||||
|
||||
Example: BDC_DEV_a3f9c2d8e1b4f7a2c5d8e1b4f7a2_9x4k
|
||||
```
|
||||
|
||||
**Quick Connect Code Format**:
|
||||
```
|
||||
[A-Z0-9]{8} - Human readable, expires in 15min
|
||||
Example: K7M9P2X5
|
||||
```
|
||||
|
||||
**Database Schema**:
|
||||
```sql
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER PRIMARY KEY,
|
||||
key_hash TEXT NOT NULL UNIQUE,
|
||||
device_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
scopes TEXT, -- JSON array: ["connect", "view", "control"]
|
||||
created_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE quick_connect_codes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
device_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
used_at TIMESTAMP NULL,
|
||||
used_by TEXT NULL
|
||||
);
|
||||
```
|
||||
|
||||
**HBBS Modifications**:
|
||||
```rust
|
||||
// New authentication middleware
|
||||
pub async fn verify_api_key(key: &str) -> Option<AuthContext> {
|
||||
// Hash key, check database
|
||||
// Return device permissions
|
||||
}
|
||||
```
|
||||
|
||||
**Web UI Features**:
|
||||
- API key management page
|
||||
- Copy/revoke/regenerate buttons
|
||||
- Usage statistics per key
|
||||
- QR code generator for quick connect
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Device Validation & Approval (v1.4) - **3-4 Days**
|
||||
**Status**: Design phase
|
||||
**Complexity**: Medium
|
||||
|
||||
#### Features
|
||||
- [ ] **New Device Approval Workflow**:
|
||||
- First-time connections require admin approval
|
||||
- Pending devices list in web console
|
||||
- Approve/reject with reason
|
||||
- Automatic approval rules (whitelist subnets)
|
||||
|
||||
- [ ] **Device Fingerprinting**:
|
||||
- Hardware ID verification
|
||||
- OS/version tracking
|
||||
- Detect device ID changes
|
||||
- Alert on suspicious behavior
|
||||
|
||||
#### HBBS Modifications
|
||||
|
||||
**New File**: `src/device_validator.rs`
|
||||
```rust
|
||||
pub struct DeviceValidator {
|
||||
pending_devices: Arc<RwLock<HashMap<String, PendingDevice>>>,
|
||||
db: Database,
|
||||
}
|
||||
|
||||
pub struct PendingDevice {
|
||||
id: String,
|
||||
fingerprint: String,
|
||||
first_seen: Instant,
|
||||
ip_address: String,
|
||||
os_info: String,
|
||||
}
|
||||
|
||||
impl DeviceValidator {
|
||||
pub async fn validate_device(&self, device: &Device) -> ValidationResult {
|
||||
// Check if device is approved
|
||||
// Check fingerprint matches
|
||||
// Check for suspicious changes
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modified Connection Flow**:
|
||||
```
|
||||
1. Device connects → Generate fingerprint
|
||||
2. Check if approved in database
|
||||
3. If not approved → Add to pending_devices
|
||||
4. If approved → Check fingerprint matches
|
||||
5. If mismatch → Flag for re-validation
|
||||
```
|
||||
|
||||
**Web API**:
|
||||
```
|
||||
GET /api/devices/pending - List pending approvals
|
||||
POST /api/devices/approve/:id - Approve device
|
||||
POST /api/devices/reject/:id - Reject device
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Security Hardening (v1.5) - **5-7 Days**
|
||||
**Status**: Security audit required
|
||||
**Complexity**: High
|
||||
|
||||
#### Features
|
||||
- [ ] **Per-Device Security Keys**:
|
||||
- Unique encryption keys per device pair
|
||||
- Key exchange protocol
|
||||
- Encrypted connection metadata
|
||||
- Certificate pinning
|
||||
|
||||
- [ ] **Network Security**:
|
||||
- IP whitelist/blacklist per device
|
||||
- Geofencing (country-based restrictions)
|
||||
- Connection rate limiting
|
||||
- DDoS protection
|
||||
|
||||
- [ ] **Monitoring & Alerts**:
|
||||
- Failed connection attempts tracking
|
||||
- Brute force detection
|
||||
- Email/webhook notifications
|
||||
- Security dashboard
|
||||
|
||||
#### Technical Implementation
|
||||
|
||||
**Device-Specific Keys**:
|
||||
```sql
|
||||
CREATE TABLE device_keys (
|
||||
id INTEGER PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
private_key_encrypted TEXT, -- Only if server-managed
|
||||
key_type TEXT, -- 'ed25519', 'rsa2048'
|
||||
created_at TIMESTAMP,
|
||||
rotated_at TIMESTAMP,
|
||||
expires_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Connection Rules**:
|
||||
```sql
|
||||
CREATE TABLE connection_rules (
|
||||
id INTEGER PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
rule_type TEXT, -- 'ip_whitelist', 'ip_blacklist', 'geo_allow', 'geo_deny'
|
||||
rule_value TEXT, -- IP range, country code, etc.
|
||||
priority INTEGER,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
);
|
||||
```
|
||||
|
||||
**HBBS Integration**:
|
||||
```rust
|
||||
// In connection handler
|
||||
let security_check = SecurityManager::validate_connection(
|
||||
&peer_id,
|
||||
&target_id,
|
||||
&client_ip,
|
||||
&client_key
|
||||
).await;
|
||||
|
||||
if !security_check.allowed {
|
||||
log_security_event(&security_check);
|
||||
return Err(SecurityViolation);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Modified Desktop Client (v2.0) - **15-30 Days**
|
||||
**Status**: Research phase
|
||||
**Complexity**: Very High
|
||||
|
||||
⚠️ **WARNING**: This requires deep RustDesk client modification and compilation
|
||||
|
||||
#### Features
|
||||
- [ ] **API Connection Mode**:
|
||||
- Connect using API key instead of ID
|
||||
- Quick connect code support
|
||||
- QR code scanner for mobile
|
||||
- Server-managed connection presets
|
||||
|
||||
- [ ] **Enhanced Client UI**:
|
||||
- BetterDesk branding option
|
||||
- Pre-configured server settings
|
||||
- Connection history sync
|
||||
- Favorite devices from web console
|
||||
|
||||
- [ ] **Client-Side Security**:
|
||||
- Hardware-based device fingerprint
|
||||
- Certificate validation
|
||||
- Encrypted credentials storage
|
||||
- Auto-lock on idle
|
||||
|
||||
#### Development Challenges
|
||||
|
||||
**RustDesk Client Architecture**:
|
||||
- Flutter-based UI (Dart)
|
||||
- Rust backend (sciter-rs or flutter_rust_bridge)
|
||||
- Native platform code (C++ for Windows/Linux)
|
||||
- Complex build system (requires: Rust, Flutter, LLVM, Visual Studio)
|
||||
|
||||
**Required Modifications**:
|
||||
|
||||
1. **Connection Protocol** (`src/client.rs`):
|
||||
```rust
|
||||
// Add API key authentication
|
||||
pub enum ConnectionMethod {
|
||||
DeviceId(String),
|
||||
ApiKey(String),
|
||||
QuickConnect(String),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn connect_with_api_key(&mut self, key: &str) -> Result<()> {
|
||||
// Validate key with HBBS
|
||||
// Retrieve target device info
|
||||
// Establish connection
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **UI Modifications** (`flutter/lib/`):
|
||||
```dart
|
||||
// Add quick connect input
|
||||
class QuickConnectScreen extends StatelessWidget {
|
||||
// QR scanner
|
||||
// Code input field
|
||||
// Recent connections from server
|
||||
}
|
||||
```
|
||||
|
||||
3. **Build Process**:
|
||||
```bash
|
||||
# Windows
|
||||
./build.py --skip-cargo
|
||||
vcpkg install ...
|
||||
|
||||
# Linux
|
||||
cargo build --release
|
||||
flutter build linux
|
||||
```
|
||||
|
||||
**Branding Customization**:
|
||||
- Replace icons/logos
|
||||
- Custom color scheme
|
||||
- "Powered by RustDesk" attribution
|
||||
- BetterDesk splash screen
|
||||
|
||||
---
|
||||
|
||||
### Phase 8: Smart Client Generator (v2.1) - **7-10 Days**
|
||||
**Status**: Concept phase
|
||||
**Complexity**: High
|
||||
|
||||
#### Features
|
||||
- [ ] **Portable Client Generator**:
|
||||
- Generate pre-configured client executables
|
||||
- Single-server mode (only connects to your HBBS)
|
||||
- Embedded API key
|
||||
- Custom branding per client
|
||||
- No manual configuration needed
|
||||
|
||||
- [ ] **Client Types**:
|
||||
- **Support Client**: For technicians, includes admin features
|
||||
- **User Client**: For end users, simplified interface
|
||||
- **Kiosk Client**: Auto-connect mode, full-screen, unattended
|
||||
|
||||
#### Technical Approach
|
||||
|
||||
**Option A: Configuration File Embedding**
|
||||
```bash
|
||||
# Generate client with embedded config
|
||||
./generate_client.sh \
|
||||
--server hbbs.example.com \
|
||||
--api-key BDC_DEV_... \
|
||||
--branding ./branding.json \
|
||||
--output betterdesk-client.exe
|
||||
```
|
||||
|
||||
**Option B: Server-Side Generation**
|
||||
- Web UI: "Generate Client" button
|
||||
- Backend builds custom client on-demand
|
||||
- Downloads pre-configured executable
|
||||
- Requires: CI/CD pipeline, build servers, code signing
|
||||
|
||||
**Implementation** (Realistic Approach):
|
||||
```python
|
||||
# In web console
|
||||
@app.route('/api/generate-client', methods=['POST'])
|
||||
def generate_client():
|
||||
# Create custom config file
|
||||
config = {
|
||||
"custom-rendezvous-server": "hbbs.example.com:21116",
|
||||
"api-key": request.json['api_key'],
|
||||
"client-name": request.json['name'],
|
||||
}
|
||||
|
||||
# Package with client binary
|
||||
# Return download link
|
||||
return jsonify({"download_url": "/downloads/client-xyz.exe"})
|
||||
```
|
||||
|
||||
**Simpler Alternative** (More Realistic):
|
||||
- Generate configuration file only (.toml or .json)
|
||||
- User downloads standard RustDesk client
|
||||
- Imports configuration file
|
||||
- Client applies settings automatically
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### Current Security Posture
|
||||
✅ Local-only by default (127.0.0.1)
|
||||
✅ SQLite database (file-based)
|
||||
✅ No external dependencies
|
||||
❌ No authentication
|
||||
❌ No encryption at rest
|
||||
❌ No HTTPS enforcement
|
||||
|
||||
### Enhanced Security Roadmap
|
||||
|
||||
#### Immediate (v1.1)
|
||||
- [ ] Flask session security (secret key)
|
||||
- [ ] HTTPS with self-signed cert
|
||||
- [ ] Password hashing (bcrypt, cost=12)
|
||||
- [ ] Input sanitization (prevent SQL injection)
|
||||
- [ ] XSS protection (Content Security Policy)
|
||||
- [ ] CSRF tokens on all forms
|
||||
|
||||
#### Short-term (v1.2-1.3)
|
||||
- [ ] API rate limiting (per IP, per user)
|
||||
- [ ] Audit logging (all admin actions)
|
||||
- [ ] Database encryption at rest (SQLCipher)
|
||||
- [ ] Secrets management (dotenv, Vault)
|
||||
- [ ] Security headers (HSTS, X-Frame-Options)
|
||||
|
||||
#### Long-term (v2.0+)
|
||||
- [ ] Certificate-based authentication
|
||||
- [ ] Hardware security module (HSM) support
|
||||
- [ ] Multi-factor authentication (TOTP)
|
||||
- [ ] Intrusion detection system (IDS)
|
||||
- [ ] Compliance reporting (SOC 2, ISO 27001)
|
||||
|
||||
### Network Architecture
|
||||
|
||||
**Default Setup** (Local Only):
|
||||
```
|
||||
[Web Console :5000] ← localhost only
|
||||
[HBBS API :21114] ← localhost only
|
||||
[HBBS Server :21115-21119] ← 0.0.0.0 (RustDesk clients)
|
||||
```
|
||||
|
||||
**Production Setup** (Exposed):
|
||||
```
|
||||
Internet
|
||||
↓
|
||||
[Reverse Proxy: Nginx/Caddy]
|
||||
├─ :443 → Web Console :5000 (HTTPS + Auth)
|
||||
└─ :21115-21119 → HBBS (RustDesk protocol)
|
||||
|
||||
Internal Network
|
||||
↓
|
||||
[HBBS API :21114] ← localhost only (no external access)
|
||||
```
|
||||
|
||||
**Firewall Rules**:
|
||||
```bash
|
||||
# Allow RustDesk clients (required)
|
||||
ufw allow 21115:21119/tcp
|
||||
|
||||
# Web console (only if needed externally)
|
||||
ufw allow 443/tcp
|
||||
|
||||
# Block API port (internal only)
|
||||
ufw deny 21114/tcp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Development Timeline Estimates
|
||||
|
||||
### Realistic Timeline (Single Developer)
|
||||
- **Phase 1 (v1.0.1)**: 2 days
|
||||
- **Phase 2 (v1.1)**: 5 days
|
||||
- **Phase 3 (v1.2)**: 7 days
|
||||
- **Phase 4 (v1.3)**: 6 days
|
||||
- **Phase 5 (v1.4)**: 4 days
|
||||
- **Phase 6 (v1.5)**: 7 days
|
||||
- **Phase 7 (v2.0)**: 30 days (client modification)
|
||||
- **Phase 8 (v2.1)**: 10 days
|
||||
|
||||
**Total**: ~71 days (~3.5 months of full-time development)
|
||||
|
||||
### Team-Based Timeline (3 developers)
|
||||
- **Backend Developer**: HBBS modifications, API
|
||||
- **Frontend Developer**: Web console UI/UX
|
||||
- **Client Developer**: Desktop client modifications
|
||||
|
||||
**Total**: ~30-40 days (~2 months with parallel work)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technical Stack Analysis
|
||||
|
||||
### Current Stack
|
||||
- **Backend**: Python 3.8+, Flask 3.0
|
||||
- **Frontend**: HTML5, CSS3, JavaScript (ES6+)
|
||||
- **HBBS**: Rust 1.70+, Axum, Tokio
|
||||
- **Database**: SQLite 3
|
||||
- **Icons**: Material Icons (offline)
|
||||
|
||||
### Required Additions for Advanced Features
|
||||
|
||||
#### Python Packages
|
||||
```txt
|
||||
Flask-Login==0.6.3 # User authentication
|
||||
Flask-WTF==1.2.1 # CSRF protection
|
||||
Flask-Limiter==3.5.0 # Rate limiting
|
||||
bcrypt==4.1.2 # Password hashing
|
||||
PyJWT==2.8.0 # JWT tokens
|
||||
cryptography==41.0.7 # Encryption utilities
|
||||
qrcode==7.4.2 # QR code generation
|
||||
Pillow==10.1.0 # Image processing
|
||||
python-dotenv==1.0.0 # Environment variables
|
||||
```
|
||||
|
||||
#### Rust Crates (HBBS)
|
||||
```toml
|
||||
[dependencies]
|
||||
# Existing
|
||||
axum = { version = "0.7", features = ["http1", "json", "tokio"] }
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# New additions
|
||||
jsonwebtoken = "9.2" # JWT validation
|
||||
argon2 = "0.5" # Password hashing
|
||||
sha2 = "0.10" # Hashing
|
||||
hex = "0.4" # Hex encoding
|
||||
uuid = { version = "1.6", features = ["v4"] } # UUID generation
|
||||
chrono = "0.4" # Timestamp handling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 Database Schema Evolution
|
||||
|
||||
### Current Schema
|
||||
```sql
|
||||
-- From RustDesk original
|
||||
CREATE TABLE peer (
|
||||
guid TEXT PRIMARY KEY,
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
uuid TEXT,
|
||||
pk BLOB,
|
||||
created_at INTEGER,
|
||||
user TEXT,
|
||||
status INTEGER,
|
||||
note TEXT,
|
||||
info TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### Enhanced Schema (v1.x)
|
||||
|
||||
```sql
|
||||
-- Users table (v1.1)
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
email TEXT,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'viewer', -- admin, operator, viewer
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP,
|
||||
failed_login_attempts INTEGER DEFAULT 0,
|
||||
locked_until TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sessions table (v1.1)
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
last_activity TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Audit logs (v1.1)
|
||||
CREATE TABLE audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
action TEXT NOT NULL, -- login, ban_device, approve_device, etc.
|
||||
target_type TEXT, -- device, user, api_key
|
||||
target_id TEXT,
|
||||
details TEXT, -- JSON
|
||||
ip_address TEXT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Device bans (v1.2)
|
||||
CREATE TABLE device_bans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
banned_by INTEGER,
|
||||
reason TEXT,
|
||||
ban_type TEXT DEFAULT 'manual', -- manual, auto, temporary
|
||||
banned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
unbanned_at TIMESTAMP,
|
||||
unbanned_by INTEGER,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
FOREIGN KEY (banned_by) REFERENCES users(id),
|
||||
FOREIGN KEY (unbanned_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_device_bans_device_id ON device_bans(device_id, is_active);
|
||||
|
||||
-- API keys (v1.3)
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key_id TEXT NOT NULL UNIQUE, -- Public identifier
|
||||
key_hash TEXT NOT NULL UNIQUE, -- SHA256 of actual key
|
||||
device_id TEXT,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
scopes TEXT, -- JSON array
|
||||
created_by INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
FOREIGN KEY (device_id) REFERENCES peer(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Quick connect codes (v1.3)
|
||||
CREATE TABLE quick_connect_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
device_id TEXT NOT NULL,
|
||||
created_by INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used_at TIMESTAMP,
|
||||
used_by TEXT, -- Device ID that used the code
|
||||
ip_address TEXT,
|
||||
FOREIGN KEY (device_id) REFERENCES peer(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Device approvals (v1.4)
|
||||
CREATE TABLE device_approvals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
fingerprint TEXT,
|
||||
os_info TEXT,
|
||||
ip_address TEXT,
|
||||
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
approved_at TIMESTAMP,
|
||||
approved_by INTEGER,
|
||||
rejected_at TIMESTAMP,
|
||||
rejected_by INTEGER,
|
||||
rejection_reason TEXT,
|
||||
status TEXT DEFAULT 'pending', -- pending, approved, rejected
|
||||
FOREIGN KEY (approved_by) REFERENCES users(id),
|
||||
FOREIGN KEY (rejected_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Connection rules (v1.5)
|
||||
CREATE TABLE connection_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT,
|
||||
rule_type TEXT NOT NULL, -- ip_whitelist, ip_blacklist, geo_allow, geo_deny, time_window
|
||||
rule_value TEXT NOT NULL,
|
||||
priority INTEGER DEFAULT 0,
|
||||
created_by INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
FOREIGN KEY (device_id) REFERENCES peer(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Security events (v1.5)
|
||||
CREATE TABLE security_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_type TEXT NOT NULL, -- failed_auth, banned_connection, suspicious_activity
|
||||
severity TEXT DEFAULT 'info', -- info, warning, critical
|
||||
device_id TEXT,
|
||||
ip_address TEXT,
|
||||
details TEXT, -- JSON
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
acknowledged_at TIMESTAMP,
|
||||
acknowledged_by INTEGER,
|
||||
FOREIGN KEY (acknowledged_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Device keys (v1.5)
|
||||
CREATE TABLE device_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
key_type TEXT NOT NULL, -- ed25519, rsa2048
|
||||
public_key TEXT NOT NULL,
|
||||
private_key_encrypted TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
rotated_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
FOREIGN KEY (device_id) REFERENCES peer(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Implementation Priority Matrix
|
||||
|
||||
### Must Have (Critical)
|
||||
1. ✅ Bug fixes (v1.0.1)
|
||||
2. ✅ Authentication (v1.1)
|
||||
3. ✅ Device banning (v1.2)
|
||||
|
||||
### Should Have (High Value)
|
||||
4. ✅ API keys (v1.3)
|
||||
5. ✅ Device validation (v1.4)
|
||||
6. ✅ Security hardening (v1.5)
|
||||
|
||||
### Nice to Have (Future)
|
||||
7. ⚠️ Modified client (v2.0) - Complex, requires client build expertise
|
||||
8. ⚠️ Client generator (v2.1) - Depends on v2.0
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Immediate Next Steps
|
||||
|
||||
### What I Can Implement Now (Today/Tomorrow)
|
||||
|
||||
**Phase 1: v1.0.1 Bug Fixes** ✅ Ready
|
||||
- Fix device deletion
|
||||
- Add confirmation dialogs
|
||||
- Better error messages
|
||||
- Input validation
|
||||
|
||||
Shall I proceed with implementing Phase 1 now? This includes:
|
||||
1. Soft-delete mechanism for devices
|
||||
2. Confirmation modals with JavaScript
|
||||
3. Improved error handling in Flask
|
||||
4. Input validation and sanitization
|
||||
|
||||
---
|
||||
|
||||
## 📞 Questions for You
|
||||
|
||||
Before proceeding with advanced features:
|
||||
|
||||
1. **Client Modification**: Are you prepared to compile RustDesk client from source? This requires:
|
||||
- Windows: Visual Studio 2019+, LLVM, vcpkg
|
||||
- Linux: GCC, Flutter SDK, Rust toolchain
|
||||
- ~50GB disk space for build dependencies
|
||||
|
||||
2. **Security Requirements**:
|
||||
- Will this be deployed on public internet or private network only?
|
||||
- Do you need compliance certifications (SOC 2, ISO)?
|
||||
- Multi-factor authentication required?
|
||||
|
||||
3. **Scale**:
|
||||
- How many devices do you expect? (10s, 100s, 1000s?)
|
||||
- How many concurrent connections?
|
||||
- Single server or distributed?
|
||||
|
||||
4. **Development Resources**:
|
||||
- Solo developer or team?
|
||||
- Timeline expectations (weeks/months)?
|
||||
- Budget for infrastructure (build servers, testing devices)?
|
||||
|
||||
---
|
||||
|
||||
## 📝 Recommendation
|
||||
|
||||
**Start with Phases 1-6** (v1.0.1 - v1.5):
|
||||
- These are achievable without client modification
|
||||
- Provide 80% of requested functionality
|
||||
- Solid foundation for future enhancements
|
||||
- Can be completed in ~30-40 days
|
||||
|
||||
**Defer Phases 7-8** (v2.0+) until:
|
||||
- Core features are stable
|
||||
- User feedback collected
|
||||
- Client build infrastructure established
|
||||
- Clear ROI for client modifications
|
||||
|
||||
Shall I proceed with implementing **Phase 1 (v1.0.1)** now?
|
||||
@@ -1,214 +0,0 @@
|
||||
# 📋 GitHub Release Checklist - v1.2.0
|
||||
|
||||
## Pre-Release Checks
|
||||
|
||||
### ✅ Code & Documentation
|
||||
- [x] All code changes committed
|
||||
- [x] CHANGELOG.md updated with v1.2.0 entry
|
||||
- [x] README.md updated with new features
|
||||
- [x] VERSION file created (1.2.0)
|
||||
- [x] RELEASE_NOTES_v1.2.0.md created
|
||||
- [x] DEPRECATION_NOTICE.md created
|
||||
- [x] HBBS patch documentation complete
|
||||
- [x] Build scripts tested and functional
|
||||
|
||||
### ✅ Testing
|
||||
- [x] Web console loads and functions
|
||||
- [x] Device listing works
|
||||
- [x] Ban/unban functionality tested
|
||||
- [x] HBBS patch compiles successfully
|
||||
- [x] Native ban check works (100% effectiveness)
|
||||
- [x] API endpoints respond correctly
|
||||
- [x] Database migrations tested
|
||||
|
||||
### ✅ Files & Structure
|
||||
- [x] .gitignore comprehensive (Python, Rust, OS, IDE)
|
||||
- [x] LICENSE file present
|
||||
- [x] README.md complete with badges
|
||||
- [x] requirements.txt present
|
||||
- [x] All scripts executable permissions set
|
||||
- [x] No sensitive data in repository
|
||||
- [x] No large binaries committed
|
||||
|
||||
### ✅ HBBS Patch
|
||||
- [x] build.sh script complete
|
||||
- [x] install.sh script exists
|
||||
- [x] QUICKSTART.md guide written
|
||||
- [x] BAN_CHECK_PATCH.md technical doc
|
||||
- [x] Patch files (database_patch.rs, peer_patch.rs)
|
||||
- [x] README.md in hbbs-patch/
|
||||
|
||||
---
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
### 1. Prepare Repository
|
||||
|
||||
```bash
|
||||
# Ensure you're on main branch
|
||||
git checkout main
|
||||
|
||||
# Pull latest changes (if team environment)
|
||||
git pull origin main
|
||||
|
||||
# Verify all changes staged
|
||||
git status
|
||||
|
||||
# Final commit if needed
|
||||
git add .
|
||||
git commit -m "Release v1.2.0 - Native HBBS Ban Check"
|
||||
|
||||
# Tag the release
|
||||
git tag -a v1.2.0 -m "Version 1.2.0 - Native Guardian"
|
||||
|
||||
# Push with tags
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### 2. Create GitHub Release
|
||||
|
||||
Go to: `https://github.com/YOUR_USERNAME/betterdesk-console/releases/new`
|
||||
|
||||
**Tag version**: `v1.2.0`
|
||||
|
||||
**Release title**: `v1.2.0 - Native Guardian 🔒`
|
||||
|
||||
**Description**: Copy from `RELEASE_NOTES_v1.2.0.md`
|
||||
|
||||
**Attachments**: None needed (source code auto-attached)
|
||||
|
||||
**Options**:
|
||||
- [x] Set as the latest release
|
||||
- [ ] Set as a pre-release (only if beta)
|
||||
- [ ] Create a discussion for this release (optional)
|
||||
|
||||
### 3. Post-Release
|
||||
|
||||
- [ ] Verify release appears on GitHub
|
||||
- [ ] Test installation from fresh clone
|
||||
- [ ] Update project website (if applicable)
|
||||
- [ ] Announce on social media/forums
|
||||
- [ ] Monitor GitHub issues for bug reports
|
||||
|
||||
---
|
||||
|
||||
## Key Files Checklist
|
||||
|
||||
### Root Directory
|
||||
- [x] README.md (updated)
|
||||
- [x] CHANGELOG.md (v1.2.0 entry)
|
||||
- [x] LICENSE (MIT)
|
||||
- [x] VERSION (1.2.0)
|
||||
- [x] RELEASE_NOTES_v1.2.0.md
|
||||
- [x] DEPRECATION_NOTICE.md
|
||||
- [x] CONTRIBUTING.md
|
||||
- [x] .gitignore
|
||||
- [x] install.sh
|
||||
- [x] update.sh
|
||||
- [x] check_database.py
|
||||
|
||||
### Web Console (web/)
|
||||
- [x] app.py (Flask backend)
|
||||
- [x] requirements.txt
|
||||
- [x] templates/index.html
|
||||
- [x] static/style.css
|
||||
- [x] static/script.js
|
||||
|
||||
### Migrations (migrations/)
|
||||
- [x] v1.0.1_soft_delete.py
|
||||
- [x] v1.1.0_device_bans.py
|
||||
- [x] README.md (if exists)
|
||||
|
||||
### HBBS Patch (hbbs-patch/)
|
||||
- [x] build.sh (automated build)
|
||||
- [x] install.sh (installation script)
|
||||
- [x] QUICKSTART.md (user guide)
|
||||
- [x] BAN_CHECK_PATCH.md (technical docs)
|
||||
- [x] README.md (patch overview)
|
||||
- [x] database_patch.rs (code snippet)
|
||||
- [x] peer_patch.rs (code snippet)
|
||||
|
||||
### Deprecated (kept for reference)
|
||||
- [x] ban_enforcer.py
|
||||
- [x] install_ban_enforcer.sh
|
||||
- [x] rustdesk-ban-enforcer.service
|
||||
- [x] BAN_ENFORCER.md
|
||||
- [x] BAN_ENFORCER_TEST.md
|
||||
|
||||
---
|
||||
|
||||
## Release Notes Preview
|
||||
|
||||
Copy this for GitHub Release:
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Major Update: Native HBBS Ban Check
|
||||
|
||||
Version 1.2.0 replaces the external Python ban enforcer with native ban checking integrated directly into the HBBS server.
|
||||
|
||||
### Key Features
|
||||
- ✅ **100% Reliable**: No race conditions or timing windows
|
||||
- ✅ **Zero Maintenance**: No external daemon to manage
|
||||
- ✅ **Better Performance**: Minimal overhead (~1ms per check)
|
||||
- ✅ **Native Integration**: Built into HBBS source code
|
||||
|
||||
### What's Changed
|
||||
- Device bans now enforced at registration level in HBBS
|
||||
- Ban Enforcer (Python daemon) deprecated
|
||||
- New build system for compiling patched HBBS
|
||||
- Complete documentation and migration guides
|
||||
|
||||
### Upgrade Path
|
||||
Existing users (v1.1.0) should:
|
||||
1. Build patched HBBS binary
|
||||
2. Install on server
|
||||
3. Disable Ban Enforcer service
|
||||
|
||||
See [RELEASE_NOTES_v1.2.0.md](RELEASE_NOTES_v1.2.0.md) for full details.
|
||||
|
||||
---
|
||||
|
||||
## Security & Compatibility
|
||||
- ✅ No database schema changes
|
||||
- ✅ Backward compatible API
|
||||
- ✅ Works with all RustDesk client versions
|
||||
- ✅ Tested with RustDesk Server v1.1.14
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
- 📖 [Quick Start Guide](hbbs-patch/QUICKSTART.md)
|
||||
- 🔧 [HBBS Patch Technical Docs](hbbs-patch/BAN_CHECK_PATCH.md)
|
||||
- 📝 [Full Changelog](CHANGELOG.md)
|
||||
- ⚠️ [Migration Guide](DEPRECATION_NOTICE.md)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: v1.1.0...v1.2.0
|
||||
|
||||
---
|
||||
|
||||
## Notes for Repository Maintainer
|
||||
|
||||
### Before Publishing:
|
||||
1. Replace `YOUR_USERNAME` in URLs with actual GitHub username
|
||||
2. Update repository links in documentation
|
||||
3. Ensure all scripts have executable permissions
|
||||
4. Test fresh installation on clean system
|
||||
5. Verify all documentation links work
|
||||
|
||||
### After Publishing:
|
||||
1. Monitor GitHub Issues for immediate bugs
|
||||
2. Be ready to create hotfix (v1.2.1) if critical issues found
|
||||
3. Update any external documentation/websites
|
||||
4. Consider creating demo video/screenshots
|
||||
|
||||
### Future Considerations (v1.3.0+):
|
||||
- WebSocket support for real-time updates
|
||||
- Device groups/tags
|
||||
- Advanced filtering and sorting
|
||||
- Device statistics/charts
|
||||
- Multi-user authentication
|
||||
- Restore from soft delete
|
||||
- Bulk operations
|
||||
@@ -1,339 +0,0 @@
|
||||
# ✅ Projekt Gotowy do Publikacji na GitHub
|
||||
|
||||
**Data weryfikacji:** 10 stycznia 2026
|
||||
**Status:** ✅ READY TO RELEASE
|
||||
|
||||
---
|
||||
|
||||
## 📦 Nowa Wersja
|
||||
|
||||
### v1.3.0-secure
|
||||
|
||||
**Poprzednia wersja:** v1.2.0-v8
|
||||
**Nowa wersja:** v1.3.0-secure
|
||||
|
||||
**Dlaczego taka nazwa:**
|
||||
- `1.3.0` - Semantic versioning (minor update z breaking change)
|
||||
- `-secure` - Wyraźnie wskazuje na fokus bezpieczeństwa
|
||||
- Jasny komunikat dla użytkowników o charakterze wydania
|
||||
|
||||
**Alternatywne nazwy (do wyboru):**
|
||||
- `v1.3.0-secure` ⭐ **POLECANA** - najlepsza dla tego release
|
||||
- `v1.3.0-localhost` - alternatywa skupiona na funkcjonalności
|
||||
- `v1.3.0` - tradycyjna bez suffixu
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Główne Zmiany
|
||||
|
||||
### Bezpieczeństwo API
|
||||
|
||||
**Port:** 21114 → 21120
|
||||
- Unika konfliktu z RustDesk Pro
|
||||
- Jasno wskazuje na localhost-only service
|
||||
|
||||
**Binding:** 0.0.0.0 → 127.0.0.1
|
||||
- API dostępne TYLKO z localhost
|
||||
- Zero ekspozycji sieciowej
|
||||
- Connection refused z sieci (bezpieczne)
|
||||
|
||||
**Nowe parametry:**
|
||||
- `--api-port 21120` - konfiguracja przez CLI
|
||||
- SSH tunnel support dla zdalnego dostępu
|
||||
|
||||
---
|
||||
|
||||
## ✅ Weryfikacja Bezpieczeństwa
|
||||
|
||||
### Kompletna ✓
|
||||
|
||||
- [x] **Brak IP 192.168.0.110** w README.md (0 wystąpień)
|
||||
- [x] **Brak haseł/kluczy** w dokumentacji (0 wystąpień)
|
||||
- [x] **Username "UNITRONIX"** tylko w URL GitHub (6 wystąpień - OK)
|
||||
- [x] **Baza danych** NIE w repozytorium (tylko kod obsługi)
|
||||
- [x] **Pliki .gitignore** poprawnie skonfigurowane (*.db, *.sqlite3)
|
||||
- [x] **Prywatne dane** całkowicie wyczyszczone
|
||||
|
||||
### Archiwalne pliki (bezpieczne)
|
||||
|
||||
Znalezione wystąpienia IP/username tylko w:
|
||||
- `SECURITY_CLEANUP_REPORT.md` - dokumentacja procesu czyszczenia
|
||||
- `RELEASE_READY.md` - instrukcje weryfikacji
|
||||
- `archive/` - folder archiwalny
|
||||
|
||||
Wszystkie są **dokumentacją bezpieczeństwa**, nie faktycznymi danymi.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Zawartość Release
|
||||
|
||||
### Binaria (23.67 MB total)
|
||||
|
||||
**Linux (x86_64):**
|
||||
- `hbbs-v8-api` - 9.59 MB (SHA256: 7B09A6C0...)
|
||||
- `hbbr-v8-api` - 4.73 MB (SHA256: DF1B3FD3...)
|
||||
- Data: 10.01.2026 10:25 UTC
|
||||
- Zawiera: localhost-only binding, port 21120
|
||||
|
||||
**Windows (x64):**
|
||||
- `hbbs-v8-api.exe` - 6.58 MB (SHA256: EE1AB9C3...)
|
||||
- `hbbr-v8-api.exe` - 2.76 MB (SHA256: 37F452AE...)
|
||||
- Data: 10.01.2026 04:42 UTC
|
||||
- Kompatybilne z nową konfiguracją
|
||||
|
||||
**Lokalizacja:** `hbbs-patch/bin-with-api/`
|
||||
|
||||
### Dokumentacja
|
||||
|
||||
**Zaktualizowane:**
|
||||
- ✅ `VERSION` → 1.3.0-secure
|
||||
- ✅ `CHANGELOG.md` → nowy wpis z v1.3.0-secure
|
||||
- ✅ `README.md` → badge wersji + security badge
|
||||
- ✅ `hbbs-patch/bin-with-api/CHECKSUMS.md` → sumy SHA256
|
||||
- ✅ `RELEASE_NOTES_v1.3.0.md` → kompletne release notes
|
||||
|
||||
**Istniejące (bez zmian):**
|
||||
- `README.md` - 656 linii (zaktualizowany port 21120)
|
||||
- `CHANGELOG.md` - 397 linii (z nowym entry)
|
||||
- `PORT_SECURITY.md` - 337 linii
|
||||
- `CONTRIBUTING.md` - dokumentacja dla contributors
|
||||
- `LICENSE` - MIT License
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Kroki Publikacji
|
||||
|
||||
### 1. Przegląd Końcowy (opcjonalny)
|
||||
|
||||
```bash
|
||||
# Sprawdź stan Git
|
||||
git status
|
||||
|
||||
# Przejrzyj zmiany
|
||||
git diff
|
||||
|
||||
# Zweryfikuj binaria
|
||||
sha256sum hbbs-patch/bin-with-api/hbbs-v8-api
|
||||
```
|
||||
|
||||
### 2. Commit Zmian
|
||||
|
||||
```bash
|
||||
# Dodaj wszystkie pliki
|
||||
git add .
|
||||
|
||||
# Commit z opisem
|
||||
git commit -m "Release v1.3.0-secure: Localhost-only API binding
|
||||
|
||||
Major Changes:
|
||||
- Changed API port from 21114 to 21120
|
||||
- API now binds to localhost (127.0.0.1) only
|
||||
- Added --api-port CLI parameter
|
||||
- Updated all documentation
|
||||
- Added CHECKSUMS.md for binary verification
|
||||
|
||||
Security:
|
||||
- Zero network exposure for API
|
||||
- Connection refused from external networks
|
||||
- No private data in documentation
|
||||
- SSH tunnel support for remote access
|
||||
|
||||
Binaries:
|
||||
- Updated Linux binaries (10.01.2026 10:25)
|
||||
- Windows binaries compatible (retained from v1.2.0-v8)
|
||||
- Total size: 23.67 MB
|
||||
- SHA256 checksums included
|
||||
|
||||
Documentation:
|
||||
- Updated README with security badges
|
||||
- New RELEASE_NOTES_v1.3.0.md
|
||||
- Complete PORT_SECURITY.md guide
|
||||
- Migration instructions from v1.2.0-v8"
|
||||
```
|
||||
|
||||
### 3. Utwórz Tag
|
||||
|
||||
```bash
|
||||
# Utwórz annotated tag
|
||||
git tag -a v1.3.0-secure -m "Release v1.3.0-secure
|
||||
|
||||
Localhost-Only API Binding
|
||||
|
||||
This release focuses on security enhancement:
|
||||
- API port changed from 21114 to 21120
|
||||
- API binds exclusively to localhost (127.0.0.1)
|
||||
- Zero network exposure
|
||||
- SSH tunnel support for remote access
|
||||
|
||||
Full release notes: RELEASE_NOTES_v1.3.0.md
|
||||
"
|
||||
|
||||
# Weryfikuj tag
|
||||
git tag -l -n9 v1.3.0-secure
|
||||
```
|
||||
|
||||
### 4. Push do GitHub
|
||||
|
||||
```bash
|
||||
# Push commits
|
||||
git push origin main
|
||||
|
||||
# Push tags
|
||||
git push origin --tags
|
||||
|
||||
# Lub wszystko razem
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### 5. Utwórz GitHub Release
|
||||
|
||||
**Na stronie GitHub:**
|
||||
|
||||
1. Idź do: **Releases** → **Create a new release**
|
||||
|
||||
2. **Tag version:** `v1.3.0-secure`
|
||||
|
||||
3. **Release title:** `v1.3.0-secure - Localhost-Only API Binding`
|
||||
|
||||
4. **Description:** (skopiuj z RELEASE_NOTES_v1.3.0.md)
|
||||
|
||||
```markdown
|
||||
## 🔒 Security Enhancement Release
|
||||
|
||||
### What's New
|
||||
|
||||
**API Port:** 21114 → 21120
|
||||
**API Binding:** 0.0.0.0 → 127.0.0.1 (localhost only)
|
||||
|
||||
This release eliminates network exposure of the HTTP API.
|
||||
|
||||
### Key Features
|
||||
|
||||
✅ Zero network exposure - API accessible only from localhost
|
||||
✅ SSH tunnel support for remote access
|
||||
✅ No port forwarding needed for 21120
|
||||
✅ Updated binaries with security code
|
||||
✅ Complete documentation with migration guide
|
||||
|
||||
### Installation
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
.\install-improved.ps1 # Run as Administrator
|
||||
```
|
||||
|
||||
### Upgrade from v1.2.0-v8
|
||||
|
||||
Automatic:
|
||||
```bash
|
||||
cd Rustdesk-FreeConsole
|
||||
git pull
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
|
||||
### Full Documentation
|
||||
|
||||
- [Complete Release Notes](RELEASE_NOTES_v1.3.0.md)
|
||||
- [Changelog](CHANGELOG.md)
|
||||
- [Port Security Guide](PORT_SECURITY.md)
|
||||
- [Binary Checksums](hbbs-patch/bin-with-api/CHECKSUMS.md)
|
||||
```
|
||||
|
||||
5. **Attach Binaries** (opcjonalnie):
|
||||
- Można dodać binaria jako assets
|
||||
- Lub pozostawić w repozytorium (już są w bin-with-api/)
|
||||
|
||||
6. **Publish release**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Statystyki Projektu
|
||||
|
||||
### Pliki
|
||||
|
||||
```
|
||||
📁 BetterDeskConsole/
|
||||
├── 📄 README.md (656 linii)
|
||||
├── 📄 CHANGELOG.md (397 linii)
|
||||
├── 📄 VERSION (1.3.0-secure)
|
||||
├── 📄 LICENSE (MIT)
|
||||
├── 📄 CONTRIBUTING.md
|
||||
├── 📄 PORT_SECURITY.md (337 linii)
|
||||
├── 📄 RELEASE_NOTES_v1.3.0.md (nowy)
|
||||
├── 📁 hbbs-patch/
|
||||
│ ├── 📁 bin-with-api/ (4 binaria, 23.67 MB)
|
||||
│ │ └── CHECKSUMS.md (nowy)
|
||||
│ ├── 📁 src/ (kod źródłowy Rust)
|
||||
│ └── 📄 README.md, build.sh, deploy-v8.sh
|
||||
├── 📁 web/
|
||||
│ ├── app.py (konsola Flask)
|
||||
│ ├── 📁 templates/ (HTML)
|
||||
│ └── 📁 static/ (CSS, JS, Material Icons)
|
||||
├── 📁 docs/ (dokumentacja)
|
||||
├── 📁 migrations/ (skrypty migracji bazy)
|
||||
└── 📁 dev_modules/ (narzędzia deweloperskie)
|
||||
```
|
||||
|
||||
### Rozmiar
|
||||
|
||||
- **Binaria:** 23.67 MB
|
||||
- **Całość projektu:** ~30-35 MB (z dokumentacją)
|
||||
- **Web assets:** Offline-ready (Material Icons included)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Co Dalej
|
||||
|
||||
### Po Publikacji
|
||||
|
||||
1. **Ogłoszenie:**
|
||||
- Dodaj post na GitHub Discussions
|
||||
- Powiadom użytkowników o bezpieczeństwie
|
||||
|
||||
2. **Monitorowanie:**
|
||||
- Sprawdzaj GitHub Issues
|
||||
- Odpowiadaj na pytania o migrację
|
||||
|
||||
3. **Social Media** (opcjonalnie):
|
||||
- Tweet o security update
|
||||
- Post na Reddit r/rustdesk
|
||||
|
||||
### Przyszłe Wersje
|
||||
|
||||
**v1.4.0** (sugestie):
|
||||
- API authentication (JWT tokens)
|
||||
- Rate limiting dla API
|
||||
- HTTPS dla web console
|
||||
- Audit logging
|
||||
|
||||
---
|
||||
|
||||
## 📞 Kontakt
|
||||
|
||||
**Issues:** https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues
|
||||
**Discussions:** https://github.com/UNITRONIX/Rustdesk-FreeConsole/discussions
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Finalna
|
||||
|
||||
- [x] VERSION zaktualizowany
|
||||
- [x] CHANGELOG zaktualizowany
|
||||
- [x] README zaktualizowany (badges)
|
||||
- [x] CHECKSUMS.md utworzony
|
||||
- [x] RELEASE_NOTES_v1.3.0.md utworzony
|
||||
- [x] Binaria zweryfikowane (SHA256)
|
||||
- [x] Bezpieczeństwo sprawdzone (no private data)
|
||||
- [x] Dokumentacja kompletna
|
||||
- [x] Git ready (clean state)
|
||||
|
||||
**🎉 PROJEKT GOTOWY DO PUBLIKACJI! 🎉**
|
||||
@@ -0,0 +1,354 @@
|
||||
# 🚀 BetterDesk Console v1.4.0 - Installation & Update Guide
|
||||
|
||||
## 📋 What's New in v1.4.0
|
||||
|
||||
### 🔐 Authentication System
|
||||
- **User login with username/password**
|
||||
- **Session management** (24-hour sessions)
|
||||
- **Password hashing** with bcrypt
|
||||
- **Default admin account** created automatically
|
||||
|
||||
### 👥 Role-Based Access Control
|
||||
- **Admin**: Full access (manage users, devices, settings)
|
||||
- **Operator**: Can ban/unban, edit devices, view audit log
|
||||
- **Viewer**: Read-only access
|
||||
|
||||
### 🎨 New UI Features
|
||||
- **Sidebar navigation menu** with glassmorphism design
|
||||
- **Responsive design** for mobile/tablet
|
||||
- **User profile display** in sidebar
|
||||
- **Better page organization**
|
||||
|
||||
### 🔒 Security Improvements
|
||||
- **All API endpoints protected** with authentication
|
||||
- **Audit logging** for all operations
|
||||
- **XSS protection** with MarkupSafe
|
||||
- **Session expiry** and automatic cleanup
|
||||
- **CSRF protection ready** (can be enabled)
|
||||
|
||||
---
|
||||
|
||||
## 🆕 New Installation
|
||||
|
||||
### Quick Install (Recommended)
|
||||
|
||||
```bash
|
||||
# Download and run installer
|
||||
curl -O https://raw.githubusercontent.com/UNITRONIX/Rustdesk-FreeConsole/main/install-improved.sh
|
||||
chmod +x install-improved.sh
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
1. Install RustDesk HBBS/HBBR servers (if not present)
|
||||
2. Install web console with authentication
|
||||
3. Create database with auth tables
|
||||
4. Generate default admin credentials
|
||||
5. Configure systemd services
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Updating from v1.3.0 or older
|
||||
|
||||
### Automatic Update (Recommended)
|
||||
|
||||
```bash
|
||||
# Download update script
|
||||
cd /path/to/Rustdesk-FreeConsole
|
||||
chmod +x update-to-v1.4.0.sh
|
||||
sudo ./update-to-v1.4.0.sh
|
||||
```
|
||||
|
||||
### What the Update Script Does:
|
||||
|
||||
1. **Detects current version** automatically
|
||||
2. **Creates backup** of:
|
||||
- Web console files
|
||||
- Database
|
||||
- Configuration
|
||||
3. **Installs new dependencies**:
|
||||
- `bcrypt` (password hashing)
|
||||
- `markupsafe` (XSS protection)
|
||||
4. **Updates web files**:
|
||||
- New `auth.py` module
|
||||
- Updated `app.py` with auth endpoints
|
||||
- New `login.html` template
|
||||
- Updated `index.html` with sidebar
|
||||
- New CSS and JavaScript files
|
||||
5. **Runs database migration**:
|
||||
- Adds `users` table
|
||||
- Adds `sessions` table
|
||||
- Adds `audit_log` table
|
||||
- Creates default admin user
|
||||
6. **Restarts services**
|
||||
7. **Shows admin credentials** (if new)
|
||||
|
||||
### Manual Update Steps
|
||||
|
||||
If you prefer manual update:
|
||||
|
||||
```bash
|
||||
# 1. Backup
|
||||
sudo cp -r /opt/BetterDeskConsole /opt/BetterDeskConsole.backup
|
||||
sudo cp /opt/rustdesk/db_v2.sqlite3 /opt/rustdesk/db_v2.sqlite3.backup
|
||||
|
||||
# 2. Install dependencies
|
||||
sudo pip3 install bcrypt markupsafe --break-system-packages
|
||||
|
||||
# 3. Copy new files
|
||||
cd /path/to/Rustdesk-FreeConsole
|
||||
sudo cp web/auth.py /opt/BetterDeskConsole/web/
|
||||
sudo cp web/app_v14.py /opt/BetterDeskConsole/web/app.py
|
||||
sudo cp web/templates/login.html /opt/BetterDeskConsole/web/templates/
|
||||
sudo cp web/templates/index_v14.html /opt/BetterDeskConsole/web/templates/index.html
|
||||
sudo cp web/static/sidebar.css /opt/BetterDeskConsole/web/static/
|
||||
sudo cp web/static/sidebar.js /opt/BetterDeskConsole/web/static/
|
||||
sudo cp web/static/script_v14.js /opt/BetterDeskConsole/web/static/script.js
|
||||
|
||||
# 4. Run migration
|
||||
sudo python3 migrations/v1.4.0_auth_system.py
|
||||
|
||||
# 5. Restart service
|
||||
sudo systemctl restart betterdesk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 First Login
|
||||
|
||||
After installation/update, you'll receive default admin credentials:
|
||||
|
||||
```
|
||||
========================================
|
||||
DEFAULT ADMIN CREDENTIALS:
|
||||
========================================
|
||||
Username: admin
|
||||
Password: <randomly-generated-password>
|
||||
========================================
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT:**
|
||||
1. **Save these credentials** in a secure location
|
||||
2. **Login immediately** and change the password
|
||||
3. **Delete credentials file**: `sudo rm /opt/BetterDeskConsole/admin_credentials.txt`
|
||||
|
||||
### Accessing the Console
|
||||
|
||||
```bash
|
||||
# Local access (on server)
|
||||
http://localhost:5000
|
||||
|
||||
# Remote access (via SSH tunnel - RECOMMENDED)
|
||||
ssh -L 8080:localhost:5000 user@your-server
|
||||
# Then open: http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 👥 User Management
|
||||
|
||||
### Creating Additional Users (Admin Only)
|
||||
|
||||
You can create additional users via Python console:
|
||||
|
||||
```python
|
||||
cd /opt/BetterDeskConsole/web
|
||||
python3 -c "
|
||||
from auth import create_user, ROLE_ADMIN, ROLE_OPERATOR, ROLE_VIEWER
|
||||
|
||||
# Create operator
|
||||
create_user('operator1', 'SecurePassword123', ROLE_OPERATOR)
|
||||
|
||||
# Create viewer
|
||||
create_user('viewer1', 'SecurePassword123', ROLE_VIEWER)
|
||||
|
||||
print('Users created successfully')
|
||||
"
|
||||
```
|
||||
|
||||
### User Roles Explained
|
||||
|
||||
| Role | Permissions |
|
||||
|------|-------------|
|
||||
| **Admin** | Full access: manage users, devices, settings, view audit log |
|
||||
| **Operator** | Ban/unban devices, edit device info, view audit log |
|
||||
| **Viewer** | Read-only: view devices and statistics |
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
### 1. Change Default Password
|
||||
|
||||
```
|
||||
1. Login with default credentials
|
||||
2. Click your name in sidebar
|
||||
3. Go to Settings
|
||||
4. Click "Change Password"
|
||||
5. Enter current password and new password
|
||||
```
|
||||
|
||||
### 2. Use SSH Tunnel (Recommended)
|
||||
|
||||
**Never expose port 5000 to the internet!**
|
||||
|
||||
```bash
|
||||
# From your local machine
|
||||
ssh -L 8080:localhost:5000 user@your-server
|
||||
|
||||
# Keep this terminal open
|
||||
# Access console at: http://localhost:8080
|
||||
```
|
||||
|
||||
### 3. Firewall Configuration
|
||||
|
||||
```bash
|
||||
# Block external access to console
|
||||
sudo ufw deny 5000
|
||||
|
||||
# Allow only SSH
|
||||
sudo ufw allow 22
|
||||
|
||||
# Allow RustDesk ports
|
||||
sudo ufw allow 21115:21117/tcp
|
||||
sudo ufw allow 21116/udp
|
||||
```
|
||||
|
||||
### 4. Regular Backups
|
||||
|
||||
```bash
|
||||
# Backup database
|
||||
sudo cp /opt/rustdesk/db_v2.sqlite3 /backup/db_v2.sqlite3.$(date +%Y%m%d)
|
||||
|
||||
# Backup web console
|
||||
sudo tar -czf /backup/betterdesk-$(date +%Y%m%d).tar.gz /opt/BetterDeskConsole
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Login Issues
|
||||
|
||||
**Problem:** "Invalid username or password"
|
||||
|
||||
**Solutions:**
|
||||
1. Check credentials file: `sudo cat /opt/BetterDeskConsole/admin_credentials.txt`
|
||||
2. Reset admin password:
|
||||
```python
|
||||
cd /opt/BetterDeskConsole/web
|
||||
python3 -c "
|
||||
from auth import reset_password
|
||||
reset_password(1, 'NewPassword123') # User ID 1 is admin
|
||||
print('Password reset successfully')
|
||||
"
|
||||
```
|
||||
|
||||
### Session Expired
|
||||
|
||||
**Problem:** Automatically logged out
|
||||
|
||||
**Solution:** Sessions expire after 24 hours. This is normal security behavior.
|
||||
|
||||
### Migration Errors
|
||||
|
||||
**Problem:** Database migration fails
|
||||
|
||||
**Solutions:**
|
||||
1. Check database permissions:
|
||||
```bash
|
||||
ls -la /opt/rustdesk/db_v2.sqlite3
|
||||
sudo chown root:root /opt/rustdesk/db_v2.sqlite3
|
||||
```
|
||||
|
||||
2. Restore from backup if needed:
|
||||
```bash
|
||||
sudo cp /opt/rustdesk/db_v2.sqlite3.backup-pre-v1.4.0 /opt/rustdesk/db_v2.sqlite3
|
||||
```
|
||||
|
||||
3. Try migration again:
|
||||
```bash
|
||||
sudo python3 migrations/v1.4.0_auth_system.py
|
||||
```
|
||||
|
||||
### Service Won't Start
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
sudo journalctl -u betterdesk -n 50 --no-pager
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Missing dependencies: `sudo pip3 install bcrypt markupsafe --break-system-packages`
|
||||
- Database locked: `sudo systemctl restart rustdesksignal`
|
||||
- Port conflict: `sudo netstat -tulpn | grep 5000`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Audit Log
|
||||
|
||||
View user actions:
|
||||
|
||||
```bash
|
||||
# View audit log in database
|
||||
sqlite3 /opt/rustdesk/db_v2.sqlite3 "
|
||||
SELECT
|
||||
u.username,
|
||||
a.action,
|
||||
a.device_id,
|
||||
a.timestamp,
|
||||
a.ip_address
|
||||
FROM audit_log a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
ORDER BY a.timestamp DESC
|
||||
LIMIT 50;
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Rollback to v1.3.0
|
||||
|
||||
If you need to rollback:
|
||||
|
||||
```bash
|
||||
# Stop service
|
||||
sudo systemctl stop betterdesk
|
||||
|
||||
# Restore backup
|
||||
sudo rm -rf /opt/BetterDeskConsole
|
||||
sudo cp -r /opt/BetterDeskConsole.backup /opt/BetterDeskConsole
|
||||
|
||||
# Restore database
|
||||
sudo cp /opt/rustdesk/db_v2.sqlite3.backup /opt/rustdesk/db_v2.sqlite3
|
||||
|
||||
# Restart service
|
||||
sudo systemctl start betterdesk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **GitHub Issues**: https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues
|
||||
- **Documentation**: See `docs/` folder
|
||||
- **Security Issues**: Use GitHub Security Advisories (private reporting)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Post-Installation Checklist
|
||||
|
||||
- [ ] Login with default credentials works
|
||||
- [ ] Changed admin password
|
||||
- [ ] Deleted credentials file
|
||||
- [ ] Configured firewall (block port 5000 externally)
|
||||
- [ ] Set up SSH tunnel for remote access
|
||||
- [ ] Tested device list loading
|
||||
- [ ] Tested ban/unban functionality
|
||||
- [ ] Created backup of database
|
||||
- [ ] Verified audit logging works
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 14 stycznia 2026*
|
||||
*Version: 1.4.0*
|
||||
@@ -1,306 +0,0 @@
|
||||
# Installation Guide - Version 8 (Precompiled Binaries)
|
||||
|
||||
## What's New in v8
|
||||
|
||||
### 🚀 Major Changes
|
||||
|
||||
1. **Precompiled Binaries** - No more compilation required!
|
||||
- Installation time reduced from ~20 minutes to ~2 minutes
|
||||
- No need for Rust/Cargo toolchain
|
||||
- Smaller dependency footprint
|
||||
- Faster deployments and updates
|
||||
|
||||
2. **Bidirectional Ban Enforcement**
|
||||
- Source ban check: Banned devices cannot initiate connections
|
||||
- Target ban check: Cannot connect to banned devices
|
||||
- Works for both P2P and relay connections
|
||||
- Real-time database sync
|
||||
|
||||
3. **Simplified Dependencies**
|
||||
- Removed: git, cargo (Rust toolchain)
|
||||
- Required: python3, pip3, curl, systemd
|
||||
- ~500MB disk space saved
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Method 1: Automatic Installation (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
|
||||
# Run installer
|
||||
sudo chmod +x install.sh
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
**What the installer does:**
|
||||
1. Checks dependencies (python3, pip3, curl, systemctl)
|
||||
2. Backs up existing RustDesk installation
|
||||
3. Installs precompiled HBBS/HBBR v8 binaries
|
||||
4. Installs web console with dependencies
|
||||
5. Configures systemd services
|
||||
6. Verifies installation
|
||||
|
||||
**Installation time:** ~2-3 minutes
|
||||
|
||||
### Method 2: Manual Installation
|
||||
|
||||
If you prefer manual installation or have a custom setup:
|
||||
|
||||
```bash
|
||||
# 1. Backup existing installation
|
||||
sudo cp /opt/rustdesk/hbbs /opt/rustdesk/hbbs.backup
|
||||
sudo cp /opt/rustdesk/hbbr /opt/rustdesk/hbbr.backup
|
||||
|
||||
# 2. Stop services
|
||||
sudo systemctl stop rustdesksignal.service
|
||||
sudo systemctl stop rustdeskrelay.service
|
||||
|
||||
# 3. Install v8 binaries
|
||||
sudo cp hbbs-patch/bin/hbbs-v8 /opt/rustdesk/hbbs
|
||||
sudo cp hbbs-patch/bin/hbbr-v8 /opt/rustdesk/hbbr
|
||||
sudo chmod +x /opt/rustdesk/hbbs /opt/rustdesk/hbbr
|
||||
|
||||
# 4. Start services
|
||||
sudo systemctl start rustdesksignal.service
|
||||
sudo systemctl start rustdeskrelay.service
|
||||
|
||||
# 5. Install web console
|
||||
sudo mkdir -p /opt/BetterDeskConsole
|
||||
sudo cp -r web/* /opt/BetterDeskConsole/
|
||||
sudo pip3 install -r web/requirements.txt
|
||||
|
||||
# 6. Create systemd service for web console
|
||||
sudo cp web/betterdesk.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable betterdesk.service
|
||||
sudo systemctl start betterdesk.service
|
||||
```
|
||||
|
||||
### Method 3: Upgrade from Previous Version
|
||||
|
||||
If you're upgrading from an older BetterDesk Console:
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
cd Rustdesk-FreeConsole
|
||||
git pull
|
||||
|
||||
# Run installer (it will detect existing installation and upgrade)
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
**What gets upgraded:**
|
||||
- HBBS/HBBR binaries (v8 with bidirectional bans)
|
||||
- Web console files
|
||||
- Python dependencies
|
||||
|
||||
**What stays the same:**
|
||||
- Your database (devices, bans, notes)
|
||||
- Configuration files
|
||||
- Service files (unless you choose to recreate)
|
||||
|
||||
## Verification
|
||||
|
||||
After installation, verify everything works:
|
||||
|
||||
### 1. Check Services
|
||||
|
||||
```bash
|
||||
# HBBS service
|
||||
sudo systemctl status rustdesksignal.service
|
||||
|
||||
# HBBR service (if using relay)
|
||||
sudo systemctl status rustdeskrelay.service
|
||||
|
||||
# Web console
|
||||
sudo systemctl status betterdesk.service
|
||||
```
|
||||
|
||||
### 2. Check Ports
|
||||
|
||||
```bash
|
||||
# RustDesk ports (should show hbbs/hbbr)
|
||||
sudo netstat -tlnp | grep -E "21115|21116|21117|21118|21119"
|
||||
|
||||
# Web console port (default: 5000)
|
||||
sudo netstat -tlnp | grep 5000
|
||||
```
|
||||
|
||||
### 3. Test Web Console
|
||||
|
||||
Open in browser:
|
||||
```
|
||||
http://YOUR_SERVER_IP:5000
|
||||
```
|
||||
|
||||
You should see the BetterDesk Console dashboard.
|
||||
|
||||
### 4. Test Ban Enforcement
|
||||
|
||||
```bash
|
||||
# Watch logs
|
||||
sudo tail -f /var/log/rustdesk/signalserver.log
|
||||
|
||||
# In web console: Ban a device
|
||||
# Try to connect from that device
|
||||
# You should see in logs:
|
||||
# "WARN Blocked loading banned device [ID] from database"
|
||||
# "Punch hole REJECTED - initiator [ID] is banned"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Services Won't Start
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
sudo journalctl -u rustdesksignal.service -n 50
|
||||
sudo journalctl -u betterdesk.service -n 50
|
||||
|
||||
# Check HBBS manually
|
||||
cd /opt/rustdesk
|
||||
./hbbs -k _ -r YOUR_SERVER_IP:21117
|
||||
```
|
||||
|
||||
### Web Console Connection Failed
|
||||
|
||||
```bash
|
||||
# Verify Python dependencies
|
||||
pip3 list | grep -E "flask|requests"
|
||||
|
||||
# Check if port 5000 is blocked
|
||||
sudo ufw allow 5000/tcp # if using ufw
|
||||
|
||||
# Test manually
|
||||
cd /opt/BetterDeskConsole
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### Ban Enforcement Not Working
|
||||
|
||||
```bash
|
||||
# Verify database has ban columns
|
||||
sqlite3 /opt/rustdesk/db_v2.sqlite3 "PRAGMA table_info(peer);"
|
||||
# Should show: is_banned, ban_reason, banned_by, banned_at
|
||||
|
||||
# Check if v8 binary is actually running
|
||||
ps aux | grep hbbs
|
||||
lsof -p $(pgrep hbbs) | grep db_v2.sqlite3 # should show database access
|
||||
```
|
||||
|
||||
### Binary Architecture Mismatch
|
||||
|
||||
If you get "cannot execute binary file":
|
||||
|
||||
```bash
|
||||
# Check your architecture
|
||||
uname -m # should be x86_64
|
||||
|
||||
# If you have ARM or different architecture, rebuild from source:
|
||||
cd hbbs-patch
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
If something goes wrong and you need to rollback:
|
||||
|
||||
```bash
|
||||
# Restore from automatic backup
|
||||
sudo cp /opt/rustdesk/hbbs.backup.TIMESTAMP /opt/rustdesk/hbbs
|
||||
sudo cp /opt/rustdesk/hbbr.backup.TIMESTAMP /opt/rustdesk/hbbr
|
||||
sudo systemctl restart rustdesksignal.service
|
||||
sudo systemctl restart rustdeskrelay.service
|
||||
|
||||
# Or restore from full backup directory
|
||||
sudo cp -r /opt/rustdesk-backup-TIMESTAMP/* /opt/rustdesk/
|
||||
sudo systemctl restart rustdesksignal.service
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To completely remove BetterDesk Console:
|
||||
|
||||
```bash
|
||||
# Stop services
|
||||
sudo systemctl stop betterdesk.service
|
||||
sudo systemctl disable betterdesk.service
|
||||
|
||||
# Remove web console
|
||||
sudo rm -rf /opt/BetterDeskConsole
|
||||
sudo rm /etc/systemd/system/betterdesk.service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Restore original RustDesk (if you have backup)
|
||||
sudo cp /opt/rustdesk/hbbs.backup /opt/rustdesk/hbbs
|
||||
sudo cp /opt/rustdesk/hbbr.backup /opt/rustdesk/hbbr
|
||||
sudo systemctl restart rustdesksignal.service
|
||||
sudo systemctl restart rustdeskrelay.service
|
||||
```
|
||||
|
||||
## Binary Information
|
||||
|
||||
The precompiled binaries are:
|
||||
|
||||
- **Source**: Compiled from RustDesk Server v1.1.14 + v8 patches
|
||||
- **Architecture**: Linux x86_64
|
||||
- **Compiled**: January 2026
|
||||
- **Size**:
|
||||
- HBBS: ~9.5 MB
|
||||
- HBBR: ~5.0 MB
|
||||
- **Patches**: 8 patches applied (see [build.sh](hbbs-patch/build.sh))
|
||||
- **Location**: `hbbs-patch/bin/`
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **Binary Authenticity**: All patches are documented and auditable
|
||||
2. **No Obfuscation**: Binaries compiled with standard Rust toolchain
|
||||
3. **Open Source**: Full source and build script available
|
||||
4. **Rebuild Option**: You can always rebuild from source using `build.sh`
|
||||
5. **Checksum Verification**: Generate checksums for your binaries:
|
||||
```bash
|
||||
sha256sum hbbs-patch/bin/*
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After successful installation:
|
||||
|
||||
1. **Configure Firewall**: Allow ports 21115-21119 and 5000
|
||||
2. **Setup SSL** (optional): Use nginx/caddy as reverse proxy for HTTPS
|
||||
3. **Create Admin Account**: Add authentication to web console (recommended)
|
||||
4. **Backup Strategy**: Setup automated backups of `/opt/rustdesk/db_v2.sqlite3`
|
||||
5. **Monitor Logs**: Setup log rotation and monitoring
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Documentation**: See [docs/](docs/) directory
|
||||
- **Security Audit**: [SECURITY_AUDIT.md](hbbs-patch/SECURITY_AUDIT.md)
|
||||
- **Ban Enforcement**: [BAN_ENFORCEMENT.md](hbbs-patch/BAN_ENFORCEMENT.md)
|
||||
- **Build from Source**: [build.sh](hbbs-patch/build.sh)
|
||||
- **Issues**: Create GitHub issue
|
||||
|
||||
## Performance
|
||||
|
||||
Expected performance with v8 precompiled binaries:
|
||||
|
||||
- **Installation**: ~2 minutes (vs ~20 min compilation)
|
||||
- **Memory**: Same as vanilla RustDesk (~50-100 MB)
|
||||
- **CPU**: Minimal impact (<1% on modern hardware)
|
||||
- **Ban Check**: ~1ms per connection attempt
|
||||
- **Database**: Same queries as before + 1 ban check
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **RustDesk Clients**: All versions compatible with v1.1.14 server
|
||||
- **Operating Systems**:
|
||||
- ✅ Ubuntu 20.04, 22.04, 24.04
|
||||
- ✅ Debian 11, 12
|
||||
- ✅ CentOS 8, 9
|
||||
- ✅ Rocky Linux 8, 9
|
||||
- ✅ Other x86_64 Linux distributions
|
||||
- **Python**: 3.8+ required
|
||||
- **Database**: SQLite 3
|
||||
@@ -1,295 +0,0 @@
|
||||
# 📁 Organizacja Projektu BetterDesk Console
|
||||
|
||||
## 📂 Struktura Katalogów
|
||||
|
||||
```
|
||||
BetterDesk-Console/
|
||||
│
|
||||
├── 📄 README.md # Główna dokumentacja projektu
|
||||
├── 📄 LICENSE # Licencja MIT
|
||||
├── 📄 VERSION # Wersja projektu (1.2.0-v8)
|
||||
├── 📄 CHANGELOG.md # Historia zmian
|
||||
├── 📄 CONTRIBUTING.md # Wytyczne dla kontrybutorów
|
||||
├── 📄 PROJECT_STRUCTURE.md # Opis struktury technicznej
|
||||
│
|
||||
├── 🔧 install-improved.sh # ⭐ Instalator Linux (v9 - UŻYWAJ TEGO)
|
||||
├── 🔧 install-improved.ps1 # ⭐ Instalator Windows (v9 - UŻYWAJ TEGO)
|
||||
│
|
||||
├── 📁 hbbs-patch/ # ⭐ Zmodyfikowane serwery HBBS/HBBR
|
||||
│ ├── 📁 src/ # Kod źródłowy modyfikacji
|
||||
│ │ ├── peer.rs # Zarządzanie peer-ami (20s timeout)
|
||||
│ │ ├── database.rs # Metody bazodanowe (ban checking)
|
||||
│ │ ├── http_api.rs # HTTP API (Axum, port 21114)
|
||||
│ │ ├── main.rs # Punkt wejścia HBBS
|
||||
│ │ └── rendezvous_server.rs # Główny serwer sygnałowy
|
||||
│ │
|
||||
│ ├── 📁 bin-with-api/ # ⭐ BINARIA Z HTTP API (używane przez instalatory)
|
||||
│ │ ├── hbbs-v8-api # Linux binary (10 MB)
|
||||
│ │ ├── hbbr-v8-api # Linux binary (4.9 MB)
|
||||
│ │ ├── hbbs-v8-api.exe # Windows binary (6.58 MB)
|
||||
│ │ └── hbbr-v8-api.exe # Windows binary (2.76 MB)
|
||||
│ │
|
||||
│ ├── 📁 bin/ # Stare binaria (fallback, bez API)
|
||||
│ ├── 📁 hbbs-ban-check-package/ # Backup kompilacji
|
||||
│ ├── 🔧 build.sh # Skrypt kompilacji Linux
|
||||
│ ├── 🔧 build-windows-local.ps1 # Skrypt kompilacji Windows
|
||||
│ └── 📄 README.md # Dokumentacja patchy
|
||||
│
|
||||
├── 📁 web/ # ⭐ Konsola webowa Flask
|
||||
│ ├── app.py # Główna aplikacja Flask
|
||||
│ ├── requirements.txt # Zależności Pythona
|
||||
│ ├── betterdesk.service # Systemd service (Linux)
|
||||
│ ├── 📁 templates/ # Szablony HTML
|
||||
│ │ └── index.html # Główny interfejs
|
||||
│ └── 📁 static/ # Zasoby statyczne
|
||||
│ ├── style.css # CSS (glassmorphism)
|
||||
│ ├── script.js # JavaScript
|
||||
│ └── MATERIAL_ICONS.md # Info o ikonach
|
||||
│
|
||||
├── 📁 docs/ # Szczegółowa dokumentacja
|
||||
│ ├── UPDATE_GUIDE.md # Instrukcje aktualizacji
|
||||
│ ├── INSTALLATION_V8.md # Instalacja v8
|
||||
│ ├── DEVELOPMENT_ROADMAP.md # Mapa rozwoju
|
||||
│ └── RELEASE_NOTES_v1.2.0.md # Notatki wydania
|
||||
│
|
||||
├── 📁 migrations/ # Migracje bazy danych
|
||||
│ ├── v1.0.1_soft_delete.py # Soft delete dla urządzeń
|
||||
│ └── v1.1.0_device_bans.py # System banowania
|
||||
│
|
||||
├── 📁 dev_modules/ # Narzędzia deweloperskie
|
||||
│ ├── check_database.py # Sprawdzanie DB
|
||||
│ ├── test_ban_api.sh # Testowanie API banów
|
||||
│ └── update.ps1 # Stary update script
|
||||
│
|
||||
├── 📁 screenshots/ # Zrzuty ekranu do dokumentacji
|
||||
│
|
||||
└── 📁 archive/ # ⭐ ARCHIWUM (stare/nieużywane pliki)
|
||||
├── hbbs-patch-backup-*/ # Stare backupy
|
||||
├── install.sh # Stary instalator (v1-v8)
|
||||
├── update.sh # Stary update script
|
||||
├── restore_hbbs.sh # Stary restore script
|
||||
└── *.md # Stara dokumentacja
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Która Wersja Instalatora?
|
||||
|
||||
### ✅ UŻYWAJ (Aktualne, zalecane)
|
||||
|
||||
| Plik | System | Wersja | Cechy |
|
||||
|------|--------|--------|-------|
|
||||
| **install-improved.sh** | Linux | v9 | Docker support, custom paths, --break-system-packages |
|
||||
| **install-improved.ps1** | Windows | v9 | Path detection, validation, Windows services |
|
||||
|
||||
### ⚠️ ARCHIWUM (Nieaktualne, tylko do referencji)
|
||||
|
||||
| Plik | System | Status |
|
||||
|------|--------|--------|
|
||||
| archive/install.sh | Linux | Zastąpiony przez install-improved.sh |
|
||||
| archive/update.sh | Linux | Zastąpiony przez install-improved.sh |
|
||||
| archive/restore_hbbs.sh | Linux | Przestarzały |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Kluczowe Pliki do Edycji
|
||||
|
||||
### Modyfikujesz funkcjonalność serwera?
|
||||
→ Edytuj: `hbbs-patch/src/*.rs`
|
||||
→ Kompiluj: `bash hbbs-patch/build.sh` (Linux) lub `.\hbbs-patch\build-windows-local.ps1` (Windows)
|
||||
|
||||
### Modyfikujesz interfejs webowy?
|
||||
→ Edytuj: `web/templates/index.html`, `web/static/style.css`, `web/static/script.js`
|
||||
→ Restart: `sudo systemctl restart betterdesk` (Linux)
|
||||
|
||||
### Modyfikujesz logikę Flask?
|
||||
→ Edytuj: `web/app.py`
|
||||
→ Restart: `sudo systemctl restart betterdesk` (Linux)
|
||||
|
||||
### Modyfikujesz instalator?
|
||||
→ Edytuj: `install-improved.sh` (Linux) lub `install-improved.ps1` (Windows)
|
||||
→ Test: Uruchom w środowisku testowym przed wdrożeniem
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Binaria - Ważne!
|
||||
|
||||
### Struktura `hbbs-patch/bin-with-api/`
|
||||
|
||||
```
|
||||
bin-with-api/
|
||||
├── hbbs-v8-api ← Linux (ELF 64-bit LSB executable, x86-64)
|
||||
├── hbbr-v8-api ← Linux (ELF 64-bit LSB executable, x86-64)
|
||||
├── hbbs-v8-api.exe ← Windows (PE32+ executable, x86-64)
|
||||
└── hbbr-v8-api.exe ← Windows (PE32+ executable, x86-64)
|
||||
```
|
||||
|
||||
### ⛔ NIGDY nie mieszaj binariów między platformami!
|
||||
|
||||
- **Linux installer** (`install-improved.sh`) używa plików **BEZ rozszerzenia .exe**
|
||||
- **Windows installer** (`install-improved.ps1`) używa plików **Z rozszerzeniem .exe**
|
||||
|
||||
### Skąd się biorą binaria?
|
||||
|
||||
```bash
|
||||
# Linux (kompilacja na serwerze SSH lub natywnym Linux)
|
||||
cd hbbs-patch
|
||||
bash build.sh
|
||||
|
||||
# Windows (kompilacja lokalna z Rust toolchain)
|
||||
cd hbbs-patch
|
||||
.\build-windows-local.ps1
|
||||
```
|
||||
|
||||
Po kompilacji binaria trafiają automatycznie do `bin-with-api/`.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Workflow Rozwoju
|
||||
|
||||
### 1. Zmiana kodu źródłowego
|
||||
|
||||
```bash
|
||||
# Edytuj pliki w hbbs-patch/src/
|
||||
nano hbbs-patch/src/peer.rs
|
||||
|
||||
# Kompiluj
|
||||
cd hbbs-patch
|
||||
bash build.sh # Linux
|
||||
# LUB
|
||||
.\build-windows-local.ps1 # Windows
|
||||
```
|
||||
|
||||
### 2. Testowanie lokalne
|
||||
|
||||
```bash
|
||||
# Zatrzymaj istniejące serwisy
|
||||
sudo systemctl stop rustdesksignal rustdeskrelay
|
||||
|
||||
# Uruchom nowe binaria ręcznie
|
||||
cd hbbs-patch/bin-with-api
|
||||
./hbbs-v8-api -h # Test
|
||||
|
||||
# Jeśli działa, zainstaluj
|
||||
cd ../..
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
|
||||
### 3. Wdrożenie produkcyjne
|
||||
|
||||
```bash
|
||||
# Utwórz backup (automatyczny w instalatorze)
|
||||
# Uruchom instalator
|
||||
sudo ./install-improved.sh
|
||||
|
||||
# Sprawdź logi
|
||||
sudo journalctl -u rustdesksignal -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Zależności między Komponentami
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Klient RustDesk │
|
||||
│ (Desktop/Mobile/Web) │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ heartbeat (~20-30s)
|
||||
▼
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ HBBS Server (hbbs-v8-api) │
|
||||
│ - peer.rs: zarządzanie połączeniami │
|
||||
│ - database.rs: ban checking │
|
||||
│ - http_api.rs: REST API (21114) │
|
||||
└─────────────┬───────────────────────────────┘
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────────┐
|
||||
│ SQLite │ │ Arc<PeerMap> │
|
||||
│ (bany, │◄────►│ (status w │
|
||||
│ devices)│ │ pamięci) │
|
||||
└─────────┘ └──────┬───────┘
|
||||
│ HTTP GET /api/peers
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Flask Web App │
|
||||
│ (port 5000) │
|
||||
│ - app.py │
|
||||
│ - templates/ │
|
||||
│ - static/ │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Utrzymanie i Troubleshooting
|
||||
|
||||
### Sprawdź status serwisów
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo systemctl status rustdesksignal
|
||||
sudo systemctl status rustdeskrelay
|
||||
sudo systemctl status betterdesk
|
||||
|
||||
# Windows
|
||||
Get-Service RustDesk*
|
||||
```
|
||||
|
||||
### Logi
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo journalctl -u rustdesksignal -f
|
||||
sudo journalctl -u betterdesk -f
|
||||
|
||||
# Windows
|
||||
Get-EventLog -LogName Application -Source RustDesk*
|
||||
```
|
||||
|
||||
### Restart po zmianach
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo systemctl restart rustdesksignal rustdeskrelay betterdesk
|
||||
|
||||
# Windows
|
||||
Restart-Service RustDesk*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Co Należy do Repozytorium?
|
||||
|
||||
### ✅ Commituj:
|
||||
- Kod źródłowy (`hbbs-patch/src/`)
|
||||
- Skrypty (`*.sh`, `*.ps1`)
|
||||
- Dokumentację (`*.md`)
|
||||
- Szablony i static files (`web/`)
|
||||
- Binaria w `hbbs-patch/bin-with-api/` (precompiled releases)
|
||||
|
||||
### ⛔ NIE commituj:
|
||||
- Katalogi kompilacji (`hbbs-patch/rustdesk-server-*/`, `target/`)
|
||||
- Pliki ZIP (`*.zip`, `*.tar.gz`)
|
||||
- Backupy (`*backup*`, `*.old`)
|
||||
- Logi (`*.log`)
|
||||
- Bazy danych (`*.sqlite3`, `*.db`)
|
||||
- Klucze prywatne (`id_*`)
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Pytania?
|
||||
|
||||
Jeśli coś jest niejasne:
|
||||
1. Sprawdź `README.md` - główna dokumentacja
|
||||
2. Zobacz `docs/` - szczegółowe instrukcje
|
||||
3. Przejrzyj `hbbs-patch/README.md` - technikalia
|
||||
4. Zajrzyj do `archive/` - historia projektu
|
||||
|
||||
**Podstawowe zasady:**
|
||||
- Wszystkie nowe funkcje dokumentuj w CHANGELOG.md
|
||||
- Testy przed wdrożeniem produkcyjnym
|
||||
- Backupy przed każdą większą zmianą
|
||||
- Binaria specyficzne dla platformy NIE są zamienne
|
||||
@@ -1,351 +0,0 @@
|
||||
# Quick Start - Update Scripts
|
||||
|
||||
## For Linux Users (Direct Access)
|
||||
|
||||
```bash
|
||||
# 1. Download/clone the repository
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
|
||||
# 2. Make script executable
|
||||
chmod +x update.sh
|
||||
|
||||
# 3. Run the update (default paths)
|
||||
sudo ./update.sh
|
||||
|
||||
# OR with custom paths:
|
||||
sudo ./update.sh --rustdesk-dir /custom/path/rustdesk
|
||||
sudo ./update.sh --console-dir /var/www/betterdesk
|
||||
sudo ./update.sh --rustdesk-dir /custom/rustdesk --console-dir /custom/console
|
||||
```
|
||||
|
||||
**Available options:**
|
||||
- `--rustdesk-dir PATH` - Custom RustDesk installation path
|
||||
- `--console-dir PATH` - Custom BetterDesk Console path
|
||||
- `--help` - Show usage information
|
||||
|
||||
**Example Output:**
|
||||
```
|
||||
========================================
|
||||
BetterDesk Console - Update to v1.1.0
|
||||
========================================
|
||||
|
||||
This update includes:
|
||||
• Soft delete system for devices (v1.0.1)
|
||||
• Device banning system (v1.1.0)
|
||||
• Enhanced UI with ban controls
|
||||
• Input validation and security improvements
|
||||
|
||||
⚠ WARNING: This will modify the database and restart services
|
||||
|
||||
Continue with update? [y/N]: y
|
||||
|
||||
========================================
|
||||
Step 1: Checking Installation
|
||||
========================================
|
||||
|
||||
✓ Found BetterDesk Console
|
||||
✓ Found database
|
||||
✓ Found BetterDesk service
|
||||
|
||||
========================================
|
||||
Step 2: Creating Backup
|
||||
========================================
|
||||
|
||||
→ Backup directory: /opt/betterdesk-backup-20260105-083000
|
||||
→ Backing up database...
|
||||
✓ Database backed up
|
||||
→ Backing up web console files...
|
||||
✓ Web files backed up
|
||||
|
||||
✓ Backup completed: /opt/betterdesk-backup-20260105-083000
|
||||
|
||||
========================================
|
||||
Step 3: Database Migration
|
||||
========================================
|
||||
|
||||
→ Running migration v1.0.1 (soft delete)...
|
||||
✓ Migration v1.0.1 completed
|
||||
|
||||
→ Running migration v1.1.0 (device bans)...
|
||||
✓ Migration v1.1.0 completed
|
||||
|
||||
========================================
|
||||
Update Complete!
|
||||
========================================
|
||||
|
||||
✓ Database migrated to v1.1.0
|
||||
✓ Web console files updated
|
||||
✓ Backup created: /opt/betterdesk-backup-20260105-083000
|
||||
✓ Service restarted
|
||||
|
||||
Access the console:
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## For Windows Users (Remote Update)
|
||||
|
||||
```powershell
|
||||
# 1. Open PowerShell as Administrator
|
||||
# 2. Navigate to project directory
|
||||
cd C:\Path\To\BetterDeskConsole
|
||||
|
||||
# 3. Run update script with your server details (default paths)
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER
|
||||
|
||||
# Optional: Custom RustDesk directory
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER -RustDeskPath "/custom/path/rustdesk"
|
||||
|
||||
# Optional: Custom console directory
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER -RemotePath "/var/www/betterdesk"
|
||||
|
||||
# Optional: All custom paths
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER -RemotePath "/var/www/betterdesk" -RustDeskPath "/custom/rustdesk"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `-RemoteHost` - Server IP or hostname (required)
|
||||
- `-RemoteUser` - SSH username (required)
|
||||
- `-RemotePath` - Console directory (default: `/opt/BetterDeskConsole`)
|
||||
- `-RustDeskPath` - RustDesk directory (default: `/opt/rustdesk`)
|
||||
- `-DbPath` - Database path (auto-set from RustDeskPath)
|
||||
|
||||
**Example Output:**
|
||||
```
|
||||
========================================
|
||||
BetterDesk Console - Update to v1.1.0
|
||||
========================================
|
||||
|
||||
Configuration:
|
||||
Remote host: YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
Console directory: /opt/BetterDeskConsole
|
||||
RustDesk directory: /opt/rustdesk
|
||||
Database path: /opt/rustdesk/db_v2.sqlite3
|
||||
|
||||
This update includes:
|
||||
• Soft delete system for devices (v1.0.1)
|
||||
• Device banning system (v1.1.0)
|
||||
• Enhanced UI with ban controls
|
||||
• Input validation and security improvements
|
||||
|
||||
⚠ This will modify the database and restart services
|
||||
|
||||
Continue with update? [y/N]: y
|
||||
|
||||
========================================
|
||||
Step 1: Checking Local Files
|
||||
========================================
|
||||
|
||||
✓ Found: v1.0.1_soft_delete.py
|
||||
✓ Found: v1.1.0_device_bans.py
|
||||
✓ Found: app.py
|
||||
✓ Found: script.js
|
||||
✓ Found: index.html
|
||||
|
||||
========================================
|
||||
Step 2: Testing SSH Connection
|
||||
========================================
|
||||
|
||||
→ Testing connection to YOUR_SERVER_IP...
|
||||
✓ SSH connection successful
|
||||
→ Checking remote installation...
|
||||
✓ BetterDesk installation found on remote server
|
||||
|
||||
========================================
|
||||
Step 3: Creating Remote Backup
|
||||
========================================
|
||||
|
||||
→ Creating backup directory: /opt/betterdesk-backup-20260105-083500
|
||||
✓ Backup created: /opt/betterdesk-backup-20260105-083500
|
||||
|
||||
========================================
|
||||
Step 4: Uploading Migration Scripts
|
||||
========================================
|
||||
|
||||
→ Uploading v1.0.1_soft_delete.py...
|
||||
✓ Uploaded v1.0.1_soft_delete.py
|
||||
→ Uploading v1.1.0_device_bans.py...
|
||||
✓ Uploaded v1.1.0_device_bans.py
|
||||
|
||||
========================================
|
||||
Step 5: Running Database Migrations
|
||||
========================================
|
||||
|
||||
→ Executing migration v1.0.1 (soft delete)...
|
||||
✓ Migration v1.0.1 completed
|
||||
|
||||
→ Executing migration v1.1.0 (device bans)...
|
||||
✓ Migration v1.1.0 completed
|
||||
|
||||
========================================
|
||||
Step 6: Updating Web Console Files
|
||||
========================================
|
||||
|
||||
→ Uploading app.py...
|
||||
✓ Updated app.py
|
||||
→ Uploading script.js...
|
||||
✓ Updated script.js
|
||||
→ Uploading index.html...
|
||||
✓ Updated index.html
|
||||
|
||||
========================================
|
||||
Step 7: Restarting BetterDesk Service
|
||||
========================================
|
||||
|
||||
✓ Service restarted successfully
|
||||
|
||||
========================================
|
||||
Step 8: Verification
|
||||
========================================
|
||||
|
||||
→ Verifying database schema...
|
||||
✓ Database schema updated (16 columns)
|
||||
→ Checking web console...
|
||||
✓ Web console is responding
|
||||
→ Total devices: 51
|
||||
→ Banned devices: 0
|
||||
|
||||
========================================
|
||||
Update Complete!
|
||||
========================================
|
||||
|
||||
✓ Database migrated to v1.1.0
|
||||
✓ Web console files updated
|
||||
✓ Backup created: /opt/betterdesk-backup-20260105-083500
|
||||
✓ Service restarted
|
||||
|
||||
New Features:
|
||||
• Soft delete for devices
|
||||
• Device banning system
|
||||
• Ban/Unban buttons in web interface
|
||||
• Enhanced input validation and security
|
||||
• Banned devices statistics card
|
||||
|
||||
Access the console:
|
||||
http://YOUR_SERVER_IP:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After update, verify the installation:
|
||||
|
||||
### Check Web Console
|
||||
Open in browser:
|
||||
```
|
||||
http://YOUR_SERVER_IP:5000
|
||||
```
|
||||
|
||||
You should see:
|
||||
- New "Banned" statistics card (5th card)
|
||||
- Ban/Unban buttons in device list
|
||||
- Visual indicators for banned devices
|
||||
|
||||
### Check API
|
||||
```bash
|
||||
curl http://localhost:5000/api/stats
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"stats": {
|
||||
"total": 51,
|
||||
"active": 14,
|
||||
"inactive": 37,
|
||||
"banned": 0,
|
||||
"with_notes": 21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Check Database Schema
|
||||
```bash
|
||||
sqlite3 /opt/rustdesk/db_v2.sqlite3 "PRAGMA table_info(peer);" | grep -E "is_banned|is_deleted"
|
||||
```
|
||||
|
||||
Should show:
|
||||
```
|
||||
9|is_deleted|INTEGER|0||0
|
||||
10|deleted_at|INTEGER|0||0
|
||||
11|updated_at|INTEGER|0||0
|
||||
12|is_banned|INTEGER|0||0
|
||||
13|banned_at|INTEGER|0||0
|
||||
14|banned_by|VARCHAR(100)|0||0
|
||||
15|ban_reason|TEXT|0||0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback (If Needed)
|
||||
|
||||
Both scripts create automatic backups. To rollback:
|
||||
|
||||
```bash
|
||||
# 1. Stop service
|
||||
sudo systemctl stop betterdesk
|
||||
|
||||
# 2. Find your backup directory
|
||||
ls -ltr /opt/ | grep betterdesk-backup
|
||||
|
||||
# 3. Restore database
|
||||
sudo cp /opt/betterdesk-backup-YYYYMMDD-HHMMSS/db_v2.sqlite3.backup /opt/rustdesk/db_v2.sqlite3
|
||||
|
||||
# 4. Restore web files
|
||||
sudo cp /opt/betterdesk-backup-YYYYMMDD-HHMMSS/app.py.backup /opt/BetterDeskConsole/app.py
|
||||
sudo cp /opt/betterdesk-backup-YYYYMMDD-HHMMSS/script.js.backup /opt/BetterDeskConsole/static/script.js
|
||||
sudo cp /opt/betterdesk-backup-YYYYMMDD-HHMMSS/index.html.backup /opt/BetterDeskConsole/templates/index.html
|
||||
|
||||
# 5. Start service
|
||||
sudo systemctl start betterdesk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Permission Denied (Linux)
|
||||
```bash
|
||||
chmod +x update.sh
|
||||
sudo ./update.sh
|
||||
```
|
||||
|
||||
### SSH Connection Failed (Windows)
|
||||
```powershell
|
||||
# Test SSH manually first
|
||||
ssh YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
|
||||
# If prompted for password, set up SSH keys:
|
||||
ssh-keygen
|
||||
ssh-copy-id YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
```
|
||||
|
||||
### PowerShell Execution Policy
|
||||
```powershell
|
||||
# Allow script execution
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
# Or run with bypass
|
||||
powershell -ExecutionPolicy Bypass -File update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER
|
||||
```
|
||||
|
||||
### Service Won't Start
|
||||
```bash
|
||||
# Check logs
|
||||
journalctl -u betterdesk -n 50 --no-pager
|
||||
|
||||
# Check if port 5000 is already in use
|
||||
sudo netstat -tlnp | grep 5000
|
||||
|
||||
# Try manual start
|
||||
cd /opt/BetterDeskConsole
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more help, see [UPDATE_GUIDE.md](UPDATE_GUIDE.md)
|
||||
@@ -1,220 +0,0 @@
|
||||
# 🔧 Quick Reference: Key Problems & Solutions
|
||||
|
||||
## 🚨 Most Common Issues
|
||||
|
||||
### Issue #1: "The keys do not match"
|
||||
|
||||
**Quick Fix:**
|
||||
```bash
|
||||
sudo bash repair-keys.sh
|
||||
# Select option 5: Restore from backup
|
||||
```
|
||||
|
||||
**If no backup exists:**
|
||||
```bash
|
||||
sudo bash repair-keys.sh
|
||||
# Select option 4: Regenerate keys
|
||||
# Then reconfigure ALL clients with new key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue #2: BetterDesk Shows Wrong Key
|
||||
|
||||
**Cause**: Multiple `.pub` files exist, or wrong file name
|
||||
|
||||
**Quick Fix** (automatic with v9+):
|
||||
```bash
|
||||
cd /path/to/Rustdesk-FreeConsole
|
||||
git pull
|
||||
sudo bash install-improved.sh
|
||||
# Select option to keep existing keys
|
||||
```
|
||||
|
||||
**Manual Fix**:
|
||||
```bash
|
||||
# Find all .pub files
|
||||
ls -lah /opt/rustdesk/*.pub
|
||||
|
||||
# Remove wrong ones (BACKUP FIRST!)
|
||||
sudo cp -r /opt/rustdesk /opt/rustdesk-backup-manual
|
||||
sudo rm /opt/rustdesk/wrong_key.pub
|
||||
|
||||
# Restart web console
|
||||
sudo systemctl restart betterdesk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue #3: Installation Broke My Working Setup
|
||||
|
||||
**Immediate Recovery:**
|
||||
```bash
|
||||
# Find most recent backup
|
||||
BACKUP=$(ls -d /opt/rustdesk-backup-* | sort | tail -1)
|
||||
echo "Using backup: $BACKUP"
|
||||
|
||||
# Stop services
|
||||
sudo systemctl stop rustdesksignal rustdeskrelay betterdesk
|
||||
|
||||
# Restore everything
|
||||
sudo cp -r $BACKUP/* /opt/rustdesk/
|
||||
|
||||
# Fix permissions
|
||||
sudo chmod 600 /opt/rustdesk/id_ed25519
|
||||
sudo chmod 644 /opt/rustdesk/*.pub
|
||||
|
||||
# Start services
|
||||
sudo systemctl start rustdesksignal rustdeskrelay betterdesk
|
||||
|
||||
# Verify
|
||||
cat /opt/rustdesk/id_ed25519.pub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Pre-Installation Checklist
|
||||
|
||||
**Before running `install-improved.sh`:**
|
||||
|
||||
- [ ] Create manual backup: `sudo cp -r /opt/rustdesk /opt/rustdesk-backup-$(date +%Y%m%d)`
|
||||
- [ ] Save current public key: `cat /opt/rustdesk/id_ed25519.pub > ~/rustdesk_key_backup.txt`
|
||||
- [ ] Note your RustDesk directory location
|
||||
- [ ] Check for multiple `.pub` files: `ls /opt/rustdesk/*.pub`
|
||||
- [ ] Verify services are running: `systemctl status rustdesksignal`
|
||||
|
||||
**During installation:**
|
||||
|
||||
- ✅ Choose **automatic backup** when prompted
|
||||
- ✅ Select **keep existing keys** when asked
|
||||
- ❌ Don't skip backups
|
||||
- ❌ Don't regenerate keys unless necessary
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker-Specific Issues
|
||||
|
||||
### Docker Installation Detected
|
||||
|
||||
**Problem**: Script warns about Docker but you want web console only
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
sudo bash install-improved.sh
|
||||
# Select option 2: "Install ONLY Web Console for existing Docker RustDesk"
|
||||
```
|
||||
|
||||
**Finding Docker volume path:**
|
||||
```bash
|
||||
# Find your RustDesk container
|
||||
docker ps | grep rustdesk
|
||||
|
||||
# Inspect volume mounts
|
||||
docker inspect <container_name> | grep -A 10 Mounts
|
||||
|
||||
# Typical locations:
|
||||
# - /var/lib/docker/volumes/rustdesk_data/_data
|
||||
# - /data (inside container)
|
||||
# - Custom bind mount specified in docker-compose.yml
|
||||
```
|
||||
|
||||
**Accessing keys in Docker:**
|
||||
```bash
|
||||
# Option 1: Exec into container
|
||||
docker exec -it <container_name> sh
|
||||
cat /data/id_ed25519.pub
|
||||
|
||||
# Option 2: Copy from container
|
||||
docker cp <container_name>:/data/id_ed25519.pub ~/rustdesk_key.txt
|
||||
cat ~/rustdesk_key.txt
|
||||
|
||||
# Option 3: Check volume on host
|
||||
sudo cat /var/lib/docker/volumes/rustdesk_data/_data/id_ed25519.pub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Using repair-keys.sh
|
||||
|
||||
**Location**: `/path/to/Rustdesk-FreeConsole/repair-keys.sh`
|
||||
|
||||
**Features:**
|
||||
|
||||
1. **Show Info** - Display all keys and their locations
|
||||
2. **Fix Permissions** - Automatically correct file permissions
|
||||
3. **Export Key** - Save public key to file for distribution
|
||||
4. **Regenerate** - Create new keys (last resort)
|
||||
5. **Restore** - Recover from backup
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
cd /path/to/Rustdesk-FreeConsole
|
||||
sudo bash repair-keys.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Emergency Contacts
|
||||
|
||||
**Can't Fix It? Need Help?**
|
||||
|
||||
1. Collect diagnostics:
|
||||
```bash
|
||||
sudo journalctl -u rustdesksignal -n 50 > ~/rustdesk_logs.txt
|
||||
ls -lah /opt/rustdesk/*.pub >> ~/rustdesk_logs.txt
|
||||
cat /opt/rustdesk/id_ed25519.pub >> ~/rustdesk_logs.txt
|
||||
```
|
||||
|
||||
2. Create GitHub issue:
|
||||
- https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues/new
|
||||
- Include output from above
|
||||
- Describe what you tried
|
||||
- Mention if this is Docker or native installation
|
||||
|
||||
3. Check existing issues:
|
||||
- Search for "key mismatch" or "keys do not match"
|
||||
- https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification After Fix
|
||||
|
||||
**Check everything works:**
|
||||
|
||||
```bash
|
||||
# 1. Services running
|
||||
systemctl status rustdesksignal rustdeskrelay betterdesk
|
||||
|
||||
# 2. Correct key displayed
|
||||
cat /opt/rustdesk/id_ed25519.pub
|
||||
|
||||
# 3. Web console shows same key
|
||||
curl -s http://localhost:5000 | grep -oP 'public-key.*?</div>' | head -1
|
||||
|
||||
# 4. API responding
|
||||
curl -s http://localhost:21114/api/health | jq .
|
||||
|
||||
# 5. Test client connection
|
||||
# (configure client with public key and try to connect)
|
||||
```
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ All services show "active (running)"
|
||||
- ✅ Public key file readable and contains valid key
|
||||
- ✅ Web console shows same key as file
|
||||
- ✅ API returns success
|
||||
- ✅ Client connects without errors
|
||||
|
||||
---
|
||||
|
||||
## 📚 Full Documentation
|
||||
|
||||
For complete troubleshooting guide, see:
|
||||
- [KEY_TROUBLESHOOTING.md](KEY_TROUBLESHOOTING.md) - Detailed solutions
|
||||
- [INSTALLATION_V8.md](INSTALLATION_V8.md) - Installation guide
|
||||
- [UPDATE_GUIDE.md](UPDATE_GUIDE.md) - Updating existing installation
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-01-13
|
||||
**Version**: BetterDesk v9+
|
||||
@@ -1,184 +0,0 @@
|
||||
# BetterDesk Console v1.2.0 - Release Notes
|
||||
|
||||
**Release Date**: January 5, 2026
|
||||
**Codename**: "Native Guardian"
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Version 1.2.0 marks a **major architectural improvement** in device ban enforcement. The external Python daemon has been replaced with native ban checking integrated directly into the HBBS server, providing 100% reliable ban enforcement with zero race conditions.
|
||||
|
||||
---
|
||||
|
||||
## 🔥 What's New
|
||||
|
||||
### Native HBBS Ban Check
|
||||
|
||||
The headline feature of this release is the **native ban enforcement system**:
|
||||
|
||||
- **100% Reliability**: Bans enforced at device registration - no timing windows
|
||||
- **Better Performance**: Single SQL query per registration (~1ms overhead)
|
||||
- **Zero Maintenance**: No external daemon process to manage
|
||||
- **Native Integration**: Ban check built into HBBS source code
|
||||
|
||||
**Technical Details:**
|
||||
- Modified `src/database.rs`: Added `is_device_banned()` method
|
||||
- Modified `src/peer.rs`: Registration logic checks ban status
|
||||
- Banned devices receive standard RustDesk `UUID_MISMATCH` error
|
||||
- Fail-open design: continues if database unavailable
|
||||
|
||||
### Build System
|
||||
|
||||
Complete tooling for compiling and deploying patched HBBS:
|
||||
|
||||
- `hbbs-patch/build.sh`: Automated build script with dependency checks
|
||||
- `hbbs-patch/install.sh`: One-command server installation
|
||||
- Full documentation: `QUICKSTART.md` and `BAN_CHECK_PATCH.md`
|
||||
- Supports both local and server-side compilation
|
||||
|
||||
### Documentation
|
||||
|
||||
- Comprehensive HBBS patch documentation
|
||||
- Migration guide from Ban Enforcer to native system
|
||||
- Technical deep-dive into ban check implementation
|
||||
- Build and deployment guides
|
||||
|
||||
---
|
||||
|
||||
## 📦 What's Included
|
||||
|
||||
### Web Console
|
||||
- Modern glassmorphism UI
|
||||
- Real-time device monitoring
|
||||
- Ban/unban management interface
|
||||
- Device notes and soft delete
|
||||
- RESTful API
|
||||
|
||||
### HBBS Patches
|
||||
- Native ban check integration
|
||||
- HTTP API for status queries
|
||||
- Database schema v1.1.0 support
|
||||
- Compatible with RustDesk v1.1.14
|
||||
|
||||
### Build Tools
|
||||
- Automated patch application
|
||||
- Rust compilation scripts
|
||||
- Installation automation
|
||||
- Backup and rollback support
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
|
||||
### Ban Enforcer Deprecated
|
||||
|
||||
The Python `ban_enforcer.py` daemon is now **obsolete**:
|
||||
|
||||
- ❌ No longer receives updates
|
||||
- ❌ Not recommended for new installations
|
||||
- ✅ Replaced by native HBBS ban check
|
||||
|
||||
**Migration Required**: Users on v1.1.0 should upgrade to native ban enforcement.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Upgrade Instructions
|
||||
|
||||
### New Installation
|
||||
|
||||
1. Clone repository
|
||||
2. Run web console installation: `./install.sh`
|
||||
3. Build HBBS patch: `cd hbbs-patch && ./build.sh`
|
||||
4. Install patched HBBS: `./install.sh`
|
||||
|
||||
### Upgrading from v1.1.0
|
||||
|
||||
1. Pull latest code: `git pull`
|
||||
2. Update web console: `./update.sh`
|
||||
3. Build and install HBBS patch (see above)
|
||||
4. Stop Ban Enforcer: `sudo systemctl stop rustdesk-ban-enforcer`
|
||||
5. Disable service: `sudo systemctl disable rustdesk-ban-enforcer`
|
||||
|
||||
**No database migration needed** - schema remains compatible.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance Comparison
|
||||
|
||||
| Metric | Ban Enforcer (v1.1.0) | Native Check (v1.2.0) |
|
||||
|--------|----------------------|----------------------|
|
||||
| Effectiveness | ~95% (race conditions) | **100%** (no windows) |
|
||||
| CPU Usage | 2s polling loop | Per-registration only |
|
||||
| Memory | ~50MB Python process | Integrated into HBBS |
|
||||
| Latency | 0-2s window | Immediate rejection |
|
||||
| Reliability | Daemon can crash | Built into server |
|
||||
| Maintenance | Separate service | Zero extra processes |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Requirements
|
||||
|
||||
### Build Environment
|
||||
- **Rust**: 1.90+ (for HBBS compilation)
|
||||
- **Python**: 3.8+ (for web console)
|
||||
- **Git**: For cloning repository
|
||||
- **Build Tools**: gcc, make, libclang
|
||||
|
||||
### Runtime
|
||||
- **HBBS**: Patched v1.1.14
|
||||
- **SQLite**: v3.x (included with HBBS)
|
||||
- **Flask**: 3.0.0 (web console)
|
||||
|
||||
### Server
|
||||
- **OS**: Linux (tested on Ubuntu 20.04+, Debian 11+)
|
||||
- **RAM**: 512MB minimum (1GB recommended)
|
||||
- **Disk**: 50MB for binaries + database space
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
None at release time.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- **Ban Enforcer files kept**: Remain in repo for reference and rollback
|
||||
- **Database compatible**: No schema changes from v1.1.0
|
||||
- **API unchanged**: Web console API remains backward compatible
|
||||
- **Client compatible**: Works with all RustDesk client versions
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- RustDesk team for the excellent open-source remote desktop
|
||||
- Community contributors for testing and feedback
|
||||
- Rust and Python communities for amazing tools
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- **Quick Start**: [QUICKSTART.md](QUICKSTART.md)
|
||||
- **HBBS Patch**: [hbbs-patch/BAN_CHECK_PATCH.md](hbbs-patch/BAN_CHECK_PATCH.md)
|
||||
- **API Docs**: [README.md#api-documentation](README.md#api-documentation)
|
||||
- **Migration**: [DEPRECATION_NOTICE.md](DEPRECATION_NOTICE.md)
|
||||
- **Changelog**: [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **Repository**: https://github.com/UNITRONIX/Rustdesk-FreeConsole
|
||||
- **Issues**: https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues
|
||||
- **RustDesk**: https://github.com/rustdesk/rustdesk
|
||||
- **License**: MIT
|
||||
|
||||
---
|
||||
|
||||
**Enjoy BetterDesk Console v1.2.0!** 🎉
|
||||
|
||||
For questions, issues, or contributions, please visit our GitHub repository.
|
||||
@@ -1,304 +0,0 @@
|
||||
# 🎉 Release Notes - BetterDesk Console v1.3.0-secure
|
||||
|
||||
**Release Date:** 10 stycznia 2026
|
||||
**Focus:** Security Enhancement - Localhost-Only API Binding
|
||||
|
||||
---
|
||||
|
||||
## 🔒 What's New
|
||||
|
||||
### Critical Security Enhancement
|
||||
|
||||
**API Port Changed: 21114 → 21120**
|
||||
- New port clearly indicates localhost-only service
|
||||
- Avoids conflict with RustDesk Pro (which uses 21114 for public API)
|
||||
- All documentation and examples updated
|
||||
|
||||
**Localhost-Only Binding: 0.0.0.0 → 127.0.0.1**
|
||||
- API now binds **exclusively** to localhost (127.0.0.1)
|
||||
- **Zero network exposure** - cannot be accessed from external networks
|
||||
- Connection attempts from network properly refused
|
||||
- No firewall configuration needed for port 21120
|
||||
|
||||
### New Features
|
||||
|
||||
✅ **--api-port Parameter**
|
||||
- Command-line configuration support
|
||||
- Flexible deployment options
|
||||
- Example: `hbbs --api-port 21120`
|
||||
|
||||
✅ **SSH Tunnel Support**
|
||||
- Remote access via secure tunnel
|
||||
- Instructions in README and PORT_SECURITY.md
|
||||
- Example: `ssh -L 21120:localhost:21120 user@server`
|
||||
|
||||
✅ **Updated Binaries**
|
||||
- Linux: hbbs-v8-api (9.59 MB), hbbr-v8-api (4.73 MB)
|
||||
- Built: 10.01.2026 10:25 UTC
|
||||
- Contains security code: "localhost only" binding
|
||||
- Windows binaries: Compatible (retained from previous build)
|
||||
|
||||
✅ **Enhanced Documentation**
|
||||
- PORT_SECURITY.md - Complete port analysis
|
||||
- Updated README with 6 security references
|
||||
- SSH tunnel instructions
|
||||
- Verification commands
|
||||
|
||||
---
|
||||
|
||||
## 📦 Download
|
||||
|
||||
### For Linux (Ubuntu 20.04+, Debian 11+)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
chmod +x install-improved.sh
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
|
||||
**Binaries included:**
|
||||
- `hbbs-patch/bin-with-api/hbbs-v8-api` (9.59 MB)
|
||||
- `hbbs-patch/bin-with-api/hbbr-v8-api` (4.73 MB)
|
||||
|
||||
### For Windows (Windows 10+, Server 2016+)
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
# Run as Administrator
|
||||
.\install-improved.ps1
|
||||
```
|
||||
|
||||
**Binaries included:**
|
||||
- `hbbs-patch/bin-with-api/hbbs-v8-api.exe` (6.58 MB)
|
||||
- `hbbs-patch/bin-with-api/hbbr-v8-api.exe` (2.76 MB)
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
### What's Protected
|
||||
|
||||
✅ **API Endpoints:**
|
||||
- `http://localhost:21120/api/health` - Service health check
|
||||
- `http://localhost:21120/api/peers` - Device list
|
||||
|
||||
✅ **Access Control:**
|
||||
- **Allowed:** localhost (127.0.0.1) only
|
||||
- **Blocked:** All network/internet access
|
||||
- **Firewall:** Port 21120 does NOT need to be opened
|
||||
|
||||
### What's Public (Unchanged)
|
||||
|
||||
RustDesk client ports remain publicly accessible (required):
|
||||
- TCP 21115 - HBBS Signal Server
|
||||
- TCP 21116 - HBBS Signal Server (NAT)
|
||||
- TCP 21117 - HBBR Relay Server
|
||||
- UDP 21116 - NAT Type Test
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Upgrade from v1.2.0-v8
|
||||
|
||||
### Automatic Upgrade (Recommended)
|
||||
|
||||
```bash
|
||||
cd Rustdesk-FreeConsole
|
||||
git pull
|
||||
sudo ./install-improved.sh
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Backups created automatically
|
||||
2. New binaries installed
|
||||
3. Systemd service updated (--api-port 21120)
|
||||
4. Web console updated (port 21120)
|
||||
5. Services restarted
|
||||
|
||||
### Manual Upgrade
|
||||
|
||||
**1. Update systemd service:**
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/rustdesksignal.service
|
||||
# Change: ExecStart=/opt/rustdesk/hbbs
|
||||
# To: ExecStart=/opt/rustdesk/hbbs --api-port 21120
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
**2. Update web console:**
|
||||
```bash
|
||||
sudo nano /opt/BetterDeskConsole/app.py
|
||||
# Change: HBBS_API_URL = 'http://localhost:21114/api'
|
||||
# To: HBBS_API_URL = 'http://localhost:21120/api'
|
||||
```
|
||||
|
||||
**3. Restart services:**
|
||||
```bash
|
||||
sudo systemctl restart rustdesksignal betterdesk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification
|
||||
|
||||
### 1. Check API Binding
|
||||
|
||||
```bash
|
||||
ss -tlnp | grep 21120
|
||||
```
|
||||
|
||||
**Expected:** `127.0.0.1:21120` (localhost only) ✅
|
||||
|
||||
### 2. Test Local Access
|
||||
|
||||
```bash
|
||||
curl http://localhost:21120/api/health
|
||||
```
|
||||
|
||||
**Expected:** `{"success":true,"data":"RustDesk API is running","error":null}` ✅
|
||||
|
||||
### 3. Test External Access
|
||||
|
||||
```bash
|
||||
curl http://YOUR_SERVER_IP:21120/api/health
|
||||
```
|
||||
|
||||
**Expected:** Connection refused ✅ (this is correct - security working)
|
||||
|
||||
### 4. Verify RustDesk Ports
|
||||
|
||||
```bash
|
||||
ss -tlnp | grep -E '21115|21116|21117'
|
||||
```
|
||||
|
||||
**Expected:** All ports listening on 0.0.0.0 (public access) ✅
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Remote Access
|
||||
|
||||
For remote API access (e.g., development workstation to production server):
|
||||
|
||||
### SSH Tunnel Method
|
||||
|
||||
```bash
|
||||
# Create tunnel
|
||||
ssh -L 21120:localhost:21120 user@your-server.com
|
||||
|
||||
# In another terminal, access API
|
||||
curl http://localhost:21120/api/health
|
||||
```
|
||||
|
||||
### Web Console Access
|
||||
|
||||
```bash
|
||||
# Tunnel both API and web console
|
||||
ssh -L 21120:localhost:21120 -L 5000:localhost:5000 user@your-server.com
|
||||
|
||||
# Open browser
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Checksums
|
||||
|
||||
Verify binary integrity with SHA256:
|
||||
|
||||
### Linux Binaries
|
||||
|
||||
```
|
||||
7B09A6C024188AF5AAC8E94C64B4B97D68A92ABF7F902B34A7D91A9D99E44558 hbbs-v8-api
|
||||
DF1B3FD3EF8793FD3A786E2BFBB330EE43A6C92D1A5915414F36011BE778E3FB hbbr-v8-api
|
||||
```
|
||||
|
||||
### Windows Binaries
|
||||
|
||||
```
|
||||
EE1AB9C341B078D852EA32ED33CCD8664BC6A3D6EA818D321529B9654C69CD74 hbbs-v8-api.exe
|
||||
37F452AE97407992DE1561B5F90747D9396E591C21E70B27897EEBEB652C1D25 hbbr-v8-api.exe
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
# Linux
|
||||
sha256sum hbbs-v8-api hbbr-v8-api
|
||||
|
||||
# Windows (PowerShell)
|
||||
Get-FileHash hbbs-v8-api.exe -Algorithm SHA256
|
||||
```
|
||||
|
||||
See [CHECKSUMS.md](hbbs-patch/bin-with-api/CHECKSUMS.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
None reported. All tests passed:
|
||||
- ✅ API responds on localhost
|
||||
- ✅ External access blocked
|
||||
- ✅ RustDesk clients connect normally
|
||||
- ✅ Web console operational
|
||||
- ✅ Ban enforcement working (bidirectional)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Full Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
### Summary of Changes
|
||||
|
||||
**Changed:**
|
||||
- API port: 21114 → 21120
|
||||
- API binding: 0.0.0.0 → 127.0.0.1
|
||||
- Systemd service: Added --api-port parameter
|
||||
- Web console: Updated to port 21120
|
||||
|
||||
**Added:**
|
||||
- PORT_SECURITY.md documentation
|
||||
- SSH tunnel instructions
|
||||
- CHECKSUMS.md for binary verification
|
||||
- Security badges in README
|
||||
|
||||
**Fixed:**
|
||||
- Port conflict with RustDesk Pro
|
||||
- Network security (eliminated accidental exposure)
|
||||
|
||||
**Security:**
|
||||
- Zero network exposure
|
||||
- Localhost-only API access
|
||||
- No private data in documentation
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
**Report issues:**
|
||||
- GitHub Issues: https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - See [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- **RustDesk Team** - Original server implementation
|
||||
- **Community** - Testing and feedback
|
||||
- **Contributors** - Security enhancements and documentation
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **Repository:** https://github.com/UNITRONIX/Rustdesk-FreeConsole
|
||||
- **Documentation:** [README.md](README.md)
|
||||
- **Security:** [PORT_SECURITY.md](PORT_SECURITY.md)
|
||||
- **RustDesk:** https://rustdesk.com/
|
||||
@@ -1,317 +0,0 @@
|
||||
# 🔑 BetterDesk v9 - Encryption Key Protection Update
|
||||
|
||||
## 📢 Important Update for All Users
|
||||
|
||||
**Version**: v9
|
||||
**Date**: January 13, 2026
|
||||
**Priority**: HIGH - Addresses critical user-reported issues
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ What Was The Problem?
|
||||
|
||||
We received multiple reports from users experiencing:
|
||||
|
||||
1. **"The keys do not match"** errors after BetterDesk installation
|
||||
2. **"Remote desktop is offline"** - intermittent connectivity issues
|
||||
3. **Public key mismatch** - WebConsole showing different key than expected
|
||||
4. **Connection breakage** - working setups broken after installation
|
||||
|
||||
### Root Causes Identified:
|
||||
|
||||
- ❌ Installation script didn't protect existing encryption keys
|
||||
- ❌ Keys could be accidentally regenerated during installation
|
||||
- ❌ Web console hardcoded `id_ed25519.pub` filename (some users had different names)
|
||||
- ❌ No warning before potentially destructive operations
|
||||
- ❌ Insufficient backup procedures
|
||||
|
||||
**User Quote:**
|
||||
> "After I shutdown BetterDesk and removed the folder, I used this to get it working again: rm -f /opt/rustdesk/id_ed25519* && ssh-keygen..."
|
||||
> — Affected User
|
||||
|
||||
---
|
||||
|
||||
## ✅ What We Fixed
|
||||
|
||||
### 1. 🔐 Comprehensive Key Protection
|
||||
|
||||
**Before v9:**
|
||||
```bash
|
||||
# Installation could silently regenerate keys
|
||||
# No warnings, no protection
|
||||
```
|
||||
|
||||
**After v9:**
|
||||
```bash
|
||||
🔑 EXISTING ENCRYPTION KEYS DETECTED 🔑
|
||||
Found: id_ed25519.pub
|
||||
|
||||
⚠️ CRITICAL: These keys authenticate your RustDesk server
|
||||
Changing keys = ALL clients disconnected
|
||||
|
||||
Options:
|
||||
1) Keep existing keys (RECOMMENDED)
|
||||
2) Regenerate keys (⚠️ BREAKS connections)
|
||||
3) Show key information
|
||||
```
|
||||
|
||||
### 2. 🔍 Dynamic Key File Scanning
|
||||
|
||||
**Before v9:**
|
||||
```python
|
||||
# Hardcoded path in web/app.py
|
||||
PUB_KEY_PATH = '/opt/rustdesk/id_ed25519.pub'
|
||||
# Failed if user had different filename!
|
||||
```
|
||||
|
||||
**After v9:**
|
||||
```python
|
||||
# Automatically scans for ANY .pub file
|
||||
def get_public_key():
|
||||
# Try default path first
|
||||
if os.path.exists(PUB_KEY_PATH):
|
||||
return f"[id_ed25519.pub] {content}"
|
||||
|
||||
# Scan for any .pub file
|
||||
for file in os.listdir(rustdesk_dir):
|
||||
if file.endswith('.pub'):
|
||||
return f"[{file}] {content}"
|
||||
```
|
||||
|
||||
### 3. 💾 Enhanced Backup System
|
||||
|
||||
**Before v9:**
|
||||
- Simple backup prompt
|
||||
- Easy to skip
|
||||
- No verification
|
||||
|
||||
**After v9:**
|
||||
- **Multiple options** (automatic, manual, existing backup)
|
||||
- **Visual warnings** with emojis and colors
|
||||
- **Backup verification** - checks size and contents
|
||||
- **Mandatory confirmation** for risky operations
|
||||
- **Key fingerprint display** for verification
|
||||
|
||||
### 4. 🔧 New Repair Tool
|
||||
|
||||
Created `repair-keys.sh` with features:
|
||||
- Show all key files and their details
|
||||
- Verify and fix permissions
|
||||
- Export public keys
|
||||
- Regenerate keys with backups
|
||||
- Restore from any backup
|
||||
|
||||
### 5. 📚 Comprehensive Documentation
|
||||
|
||||
New guides created:
|
||||
- `docs/KEY_TROUBLESHOOTING.md` - Complete troubleshooting guide
|
||||
- `docs/QUICK_FIX.md` - Fast solutions for common issues
|
||||
- Updated README with troubleshooting section
|
||||
|
||||
### 6. 🐳 Better Docker Handling
|
||||
|
||||
**Before v9:**
|
||||
```bash
|
||||
Docker detected? → Continue anyway? [y/N]
|
||||
# Easy to accidentally break Docker setup
|
||||
```
|
||||
|
||||
**After v9:**
|
||||
```bash
|
||||
🐳 Docker RustDesk installation detected!
|
||||
|
||||
Options:
|
||||
1) Exit and use Docker-compose (RECOMMENDED)
|
||||
2) Install ONLY Web Console for Docker
|
||||
3) Continue native (WILL NOT WORK WITH DOCKER)
|
||||
|
||||
Choose [1-3]:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆕 New Features in v9
|
||||
|
||||
| Feature | Description | Impact |
|
||||
|---------|-------------|--------|
|
||||
| **Key Detection** | Automatically finds existing keys | Prevents accidental overwrite |
|
||||
| **Multiple Backups** | 4 backup options with verification | Data safety |
|
||||
| **Dynamic Scanning** | Finds any `.pub` file, not just default | Works with custom key names |
|
||||
| **Visual Warnings** | Color-coded, emoji-enhanced alerts | Clear communication |
|
||||
| **repair-keys.sh** | Diagnostic and repair utility | Easy troubleshooting |
|
||||
| **Rollback Support** | Easy restoration from backups | Quick recovery |
|
||||
| **Permission Fixes** | Automatic permission correction | Resolves common issues |
|
||||
| **Docker Detection** | Smart handling of containerized installs | Prevents conflicts |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Upgrading to v9
|
||||
|
||||
### For New Installations
|
||||
|
||||
Simply use the latest version:
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
sudo bash install-improved.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
- ✅ Detect your existing RustDesk installation
|
||||
- ✅ Find and protect your encryption keys
|
||||
- ✅ Create automatic backups
|
||||
- ✅ Guide you through safe installation
|
||||
|
||||
### For Existing BetterDesk Users
|
||||
|
||||
If you already have BetterDesk installed:
|
||||
|
||||
```bash
|
||||
cd /path/to/Rustdesk-FreeConsole
|
||||
git pull # Get v9 updates
|
||||
sudo bash install-improved.sh
|
||||
```
|
||||
|
||||
**During upgrade:**
|
||||
- Select **Option 1**: Keep existing keys (RECOMMENDED)
|
||||
- Let the script create automatic backup
|
||||
- Verify everything works after installation
|
||||
|
||||
### For Users Who Had Issues
|
||||
|
||||
If BetterDesk broke your keys:
|
||||
|
||||
**Option 1: Restore from backup**
|
||||
```bash
|
||||
cd Rustdesk-FreeConsole
|
||||
sudo bash repair-keys.sh
|
||||
# Select: 5) Restore keys from backup
|
||||
```
|
||||
|
||||
**Option 2: Manual restore**
|
||||
```bash
|
||||
# Find backup
|
||||
ls -d /opt/rustdesk-backup-*
|
||||
|
||||
# Restore
|
||||
BACKUP=$(ls -d /opt/rustdesk-backup-* | sort | tail -1)
|
||||
sudo systemctl stop rustdesksignal rustdeskrelay
|
||||
sudo cp $BACKUP/id_ed25519* /opt/rustdesk/
|
||||
sudo chmod 600 /opt/rustdesk/id_ed25519
|
||||
sudo chmod 644 /opt/rustdesk/id_ed25519.pub
|
||||
sudo systemctl start rustdesksignal rustdeskrelay
|
||||
```
|
||||
|
||||
**Option 3: If no backup exists**
|
||||
```bash
|
||||
# Regenerate keys (will require reconfiguring ALL clients)
|
||||
sudo bash repair-keys.sh
|
||||
# Select: 4) Regenerate keys
|
||||
# Follow prompts and save new public key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 What To Do After Upgrading
|
||||
|
||||
### 1. Verify Keys
|
||||
|
||||
```bash
|
||||
# Check your current public key
|
||||
cat /opt/rustdesk/id_ed25519.pub
|
||||
|
||||
# Compare with WebConsole
|
||||
# Open http://your-server:5000
|
||||
# Key should match exactly
|
||||
```
|
||||
|
||||
### 2. Test Connections
|
||||
|
||||
- Open RustDesk client
|
||||
- Try connecting to a device
|
||||
- Should work without "key mismatch" errors
|
||||
|
||||
### 3. Save Your Key
|
||||
|
||||
```bash
|
||||
# Export for safekeeping
|
||||
cat /opt/rustdesk/id_ed25519.pub > ~/rustdesk_public_key_backup.txt
|
||||
|
||||
# Or use repair tool
|
||||
sudo bash repair-keys.sh
|
||||
# Select: 3) Export public key
|
||||
```
|
||||
|
||||
### 4. Verify Backups
|
||||
|
||||
```bash
|
||||
# Check automatic backups exist
|
||||
ls -lah /opt/rustdesk-backup-*
|
||||
|
||||
# Verify keys are in backup
|
||||
ls -lah /opt/rustdesk-backup-*/id_ed25519*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Prevention Checklist
|
||||
|
||||
Before **any** future RustDesk modifications:
|
||||
|
||||
- [ ] Backup keys manually: `sudo cp -r /opt/rustdesk /opt/rustdesk-backup-manual`
|
||||
- [ ] Save public key: `cat /opt/rustdesk/id_ed25519.pub > ~/key_backup.txt`
|
||||
- [ ] Note current key fingerprint
|
||||
- [ ] Test restoration procedure
|
||||
- [ ] Document any custom configurations
|
||||
|
||||
---
|
||||
|
||||
## 📊 What Users Are Saying
|
||||
|
||||
### Before v9:
|
||||
> ❌ "BetterDesk broke my RustDesk installation"
|
||||
> ❌ "Keys do not match - all clients disconnected"
|
||||
> ❌ "Had to regenerate keys and reconfigure 50+ devices"
|
||||
|
||||
### After v9:
|
||||
> ✅ "Installation preserved my keys perfectly"
|
||||
> ✅ "Clear warnings prevented me from making mistakes"
|
||||
> ✅ "Repair tool fixed my issue in 30 seconds"
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Additional Resources
|
||||
|
||||
- **Quick Fixes**: [docs/QUICK_FIX.md](docs/QUICK_FIX.md)
|
||||
- **Full Troubleshooting**: [docs/KEY_TROUBLESHOOTING.md](docs/KEY_TROUBLESHOOTING.md)
|
||||
- **Installation Guide**: [docs/INSTALLATION_V8.md](docs/INSTALLATION_V8.md)
|
||||
- **GitHub Issues**: https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues
|
||||
|
||||
---
|
||||
|
||||
## 💬 Feedback & Support
|
||||
|
||||
We want to hear from you!
|
||||
|
||||
- 🐛 **Report issues**: Open a GitHub issue
|
||||
- 💡 **Suggest features**: Start a discussion
|
||||
- ⭐ **Success story**: Share your experience
|
||||
- 🤝 **Contribute**: PRs welcome!
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Thank You
|
||||
|
||||
Special thanks to users who reported these issues and helped us improve BetterDesk:
|
||||
- Users who detailed their "key mismatch" problems
|
||||
- Community members who shared workarounds
|
||||
- Everyone who tested v9 pre-release
|
||||
|
||||
**Your feedback makes BetterDesk better! 🚀**
|
||||
|
||||
---
|
||||
|
||||
**Version**: v9
|
||||
**Release Date**: January 13, 2026
|
||||
**Compatibility**: RustDesk 1.1.14+
|
||||
**License**: MIT
|
||||
@@ -1,262 +0,0 @@
|
||||
# 🎉 Release Readiness Checklist - BetterDesk Console v1.2.0-v8
|
||||
|
||||
## ✅ Code & Binaries
|
||||
|
||||
- [x] **Precompiled binaries** included in `hbbs-patch/bin/`
|
||||
- [x] hbbs-v8 (9.5 MB) - Signal server with bidirectional ban enforcement
|
||||
- [x] hbbr-v8 (5.0 MB) - Relay server with bidirectional ban enforcement
|
||||
- [x] SHA256 checksums documented
|
||||
|
||||
- [x] **Web console** fully functional
|
||||
- [x] Flask backend with ban management
|
||||
- [x] Modern glassmorphism UI
|
||||
- [x] Material Icons (offline)
|
||||
- [x] Device management, banning, notes
|
||||
|
||||
- [x] **Installation system** verified
|
||||
- [x] install.sh uses precompiled binaries
|
||||
- [x] Automatic backup of existing files
|
||||
- [x] Service restart functionality
|
||||
- [x] No compilation required
|
||||
- [x] Reduced dependencies (no Rust/git needed)
|
||||
|
||||
## ✅ Documentation
|
||||
|
||||
- [x] **Main README.md** updated
|
||||
- [x] Version badge: 1.2.0-v8
|
||||
- [x] Bidirectional ban enforcement description
|
||||
- [x] Precompiled binaries mentioned
|
||||
- [x] Installation time: 2-3 minutes
|
||||
- [x] No compilation requirements
|
||||
|
||||
- [x] **CHANGELOG.md** updated
|
||||
- [x] Version 1.2.0-v8 entry
|
||||
- [x] Bidirectional ban enforcement details
|
||||
- [x] Precompiled binaries explanation
|
||||
- [x] Migration notes
|
||||
|
||||
- [x] **LICENSE** appropriate
|
||||
- [x] AGPL-3.0 (compatible with RustDesk)
|
||||
- [x] Copyright attribution
|
||||
|
||||
- [x] **Technical documentation**
|
||||
- [x] hbbs-patch/BAN_ENFORCEMENT.md - Bidirectional bans
|
||||
- [x] hbbs-patch/SECURITY_AUDIT.md - Security review
|
||||
- [x] hbbs-patch/bin/README.md - Binary documentation
|
||||
- [x] hbbs-patch/bin/CHECKSUMS.md - SHA256 verification
|
||||
- [x] docs/INSTALLATION_V8.md - Complete installation guide
|
||||
- [x] PROJECT_STRUCTURE.md - Updated structure
|
||||
|
||||
## ✅ Security & Privacy
|
||||
|
||||
- [x] **No sensitive data** in files
|
||||
- [x] SSH credentials removed (0 instances found)
|
||||
- [x] IP addresses replaced with placeholders
|
||||
- [x] All occurrences: YOUR_SERVER_IP, YOUR_SSH_USER
|
||||
|
||||
- [x] **No git history** with sensitive data
|
||||
- [x] Not a git repository (clean start possible)
|
||||
|
||||
- [x] **Security documentation**
|
||||
- [x] SECURITY_AUDIT.md - Vulnerability assessment
|
||||
- [x] SECURITY_PLACEHOLDERS.md - Guide for users
|
||||
- [x] SECURITY_CLEANUP_REPORT.md - Cleanup summary
|
||||
|
||||
- [x] **.gitignore** comprehensive
|
||||
- [x] Credentials patterns
|
||||
- [x] Backup files
|
||||
- [x] Old binary versions
|
||||
- [x] Sensitive data patterns
|
||||
|
||||
## ✅ Code Quality
|
||||
|
||||
- [x] **Functional verification**
|
||||
- [x] Bidirectional ban enforcement working
|
||||
- [x] Web console operational
|
||||
- [x] Database migrations included
|
||||
- [x] Service files present
|
||||
|
||||
- [x] **Clean codebase**
|
||||
- [x] Old binaries removed (v2-v5)
|
||||
- [x] Deprecated code in separate directory
|
||||
- [x] No TODO or FIXME markers in critical code
|
||||
|
||||
## ✅ Repository Structure
|
||||
|
||||
```
|
||||
BetterDeskConsole/
|
||||
├── ✅ README.md (updated)
|
||||
├── ✅ LICENSE (AGPL-3.0)
|
||||
├── ✅ VERSION (1.2.0-v8)
|
||||
├── ✅ CHANGELOG.md (v8 entry)
|
||||
├── ✅ .gitignore (comprehensive)
|
||||
├── ✅ PROJECT_STRUCTURE.md (updated)
|
||||
│
|
||||
├── ✅ install.sh (precompiled binaries)
|
||||
├── ✅ update.sh (for upgrades)
|
||||
├── ✅ restore_hbbs.sh (rollback)
|
||||
│
|
||||
├── ✅ web/ (Flask console)
|
||||
│ ├── ✅ app.py (ban management)
|
||||
│ ├── ✅ requirements.txt
|
||||
│ ├── ✅ betterdesk.service
|
||||
│ ├── ✅ templates/index.html
|
||||
│ └── ✅ static/ (CSS, JS, icons)
|
||||
│
|
||||
├── ✅ hbbs-patch/
|
||||
│ ├── ✅ bin/ (NEW - precompiled)
|
||||
│ │ ├── ✅ hbbs-v8 (9.5 MB)
|
||||
│ │ ├── ✅ hbbr-v8 (5.0 MB)
|
||||
│ │ ├── ✅ README.md
|
||||
│ │ └── ✅ CHECKSUMS.md
|
||||
│ │
|
||||
│ ├── ✅ src/ (source patches)
|
||||
│ ├── ✅ build.sh (rebuild script)
|
||||
│ ├── ✅ deploy-v8.sh
|
||||
│ ├── ✅ BAN_ENFORCEMENT.md (v8)
|
||||
│ ├── ✅ SECURITY_AUDIT.md
|
||||
│ └── ✅ test scripts
|
||||
│
|
||||
├── ✅ docs/
|
||||
│ ├── ✅ INSTALLATION_V8.md
|
||||
│ ├── ✅ UPDATE_GUIDE.md
|
||||
│ └── ✅ other guides
|
||||
│
|
||||
├── ✅ migrations/ (database)
|
||||
└── ✅ screenshots/ (UI examples)
|
||||
```
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total Files**: ~100+
|
||||
- **Lines of Code**: ~10,000+
|
||||
- **Documentation**: 15+ markdown files
|
||||
- **Installation Time**: 2-3 minutes (vs 20 min before)
|
||||
- **Dependencies Removed**: git, cargo, rustc (~500 MB saved)
|
||||
- **Binary Size**: 14.5 MB total (hbbs + hbbr)
|
||||
- **Ban Enforcement**: 100% effective, bidirectional
|
||||
|
||||
## 🚀 Ready for Publication
|
||||
|
||||
### GitHub Release Steps
|
||||
|
||||
1. **Initialize git repository**
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: BetterDesk Console v1.2.0-v8"
|
||||
```
|
||||
|
||||
2. **Create GitHub repository**
|
||||
```bash
|
||||
gh repo create BetterDeskConsole --public --source=. --remote=origin
|
||||
```
|
||||
|
||||
3. **Push to GitHub**
|
||||
```bash
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
4. **Create release**
|
||||
```bash
|
||||
gh release create v1.2.0-v8 \
|
||||
--title "BetterDesk Console v1.2.0-v8 - Precompiled Binaries + Bidirectional Bans" \
|
||||
--notes "See CHANGELOG.md for details" \
|
||||
hbbs-patch/bin/hbbs-v8 \
|
||||
hbbs-patch/bin/hbbr-v8
|
||||
```
|
||||
|
||||
5. **Tag binaries**
|
||||
```bash
|
||||
git tag -a v1.2.0-v8 -m "Version 1.2.0-v8 with precompiled binaries"
|
||||
git push origin v1.2.0-v8
|
||||
```
|
||||
|
||||
## 🎯 Next Steps (Post-Release)
|
||||
|
||||
1. **Community Engagement**
|
||||
- [ ] Submit to RustDesk community forum
|
||||
- [ ] Reddit post in r/selfhosted
|
||||
- [ ] Tweet about release
|
||||
|
||||
2. **Monitoring**
|
||||
- [ ] Watch for issues/bug reports
|
||||
- [ ] Monitor installation success rate
|
||||
- [ ] Gather user feedback
|
||||
|
||||
3. **Future Improvements**
|
||||
- [ ] Multi-architecture binaries (ARM64)
|
||||
- [ ] Docker container
|
||||
- [ ] Web console authentication
|
||||
- [ ] Automated testing suite
|
||||
|
||||
## ✅ Final Verification
|
||||
|
||||
Run these commands before publishing:
|
||||
|
||||
```bash
|
||||
# 1. Verify no sensitive data
|
||||
grep -r "192.168.0.110" . --exclude-dir=.git
|
||||
grep -r "unitronix@" . --exclude-dir=.git
|
||||
|
||||
# 2. Verify binaries exist
|
||||
ls -lh hbbs-patch/bin/hbbs-v8 hbbs-patch/bin/hbbr-v8
|
||||
|
||||
# 3. Verify checksums
|
||||
sha256sum hbbs-patch/bin/*-v8
|
||||
|
||||
# 4. Test installer (dry run)
|
||||
bash -n install.sh
|
||||
|
||||
# 5. Verify documentation links
|
||||
find docs -name "*.md" -exec grep -l "YOUR_SERVER_IP" {} \;
|
||||
```
|
||||
|
||||
## 📝 Release Notes Draft
|
||||
|
||||
```markdown
|
||||
# BetterDesk Console v1.2.0-v8
|
||||
|
||||
## 🚀 Major Changes
|
||||
|
||||
- **Precompiled Binaries**: Installation now takes 2-3 minutes (vs 20 minutes)
|
||||
- **Bidirectional Ban Enforcement**: Banned devices blocked in BOTH directions
|
||||
- **No Compilation Required**: Removed Rust toolchain dependency
|
||||
- **Simplified Installation**: Just Python3 + pip3 needed
|
||||
|
||||
## 📦 What's Included
|
||||
|
||||
- HBBS v8 (9.5 MB) - Signal server with bidirectional bans
|
||||
- HBBR v8 (5.0 MB) - Relay server
|
||||
- Web management console (Flask + Material Design)
|
||||
- Complete documentation
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
sudo chmod +x install.sh
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- [Installation Guide](docs/INSTALLATION_V8.md)
|
||||
- [Ban Enforcement Technical Docs](hbbs-patch/BAN_ENFORCEMENT.md)
|
||||
- [Security Audit](hbbs-patch/SECURITY_AUDIT.md)
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
- SHA256 checksums provided
|
||||
- Full source code available
|
||||
- AGPL-3.0 license
|
||||
- Security audit included
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ **READY FOR RELEASE**
|
||||
|
||||
All systems go! 🎉
|
||||
@@ -1,161 +0,0 @@
|
||||
# 🔒 Bezpieczeństwo Danych - Raport Czyszczenia
|
||||
|
||||
**Data:** 6 stycznia 2026
|
||||
**Status:** ✅ ZAKOŃCZONE
|
||||
|
||||
---
|
||||
|
||||
## 📊 Podsumowanie Zmian
|
||||
|
||||
### Pliki Zabezpieczone (18 plików):
|
||||
1. ✅ README.md
|
||||
2. ✅ hbbs-patch/deploy.ps1
|
||||
3. ✅ hbbs-patch/deploy-v6.ps1
|
||||
4. ✅ hbbs-patch/deploy-v8.sh
|
||||
5. ✅ hbbs-patch/QUICKSTART.md
|
||||
6. ✅ hbbs-patch/BAN_ENFORCEMENT.md
|
||||
7. ✅ hbbs-patch/test_ban_enforcement.ps1
|
||||
8. ✅ hbbs-patch/diagnose_ban.ps1
|
||||
9. ✅ docs/UPDATE_REFERENCE.md
|
||||
10. ✅ docs/UPDATE_GUIDE.md
|
||||
11. ✅ docs/QUICKSTART_UPDATE.md
|
||||
12. ✅ dev_modules/update.ps1
|
||||
13. ✅ dev_modules/test_ban_api.sh
|
||||
14. ✅ deprecated/BAN_ENFORCER_TEST.md (częściowo)
|
||||
15. ✅ .gitignore (zaktualizowany)
|
||||
16. ✅ SECURITY_PLACEHOLDERS.md (nowy)
|
||||
17. ✅ SECURITY_AUDIT.md (stworzony wcześniej)
|
||||
18. ✅ Ten raport
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Zamienione Dane
|
||||
|
||||
| Dane Wrażliwe | Placeholder | Wystąpienia |
|
||||
|---------------|-------------|-------------|
|
||||
| `192.168.0.110` | `YOUR_SERVER_IP` | ~150+ |
|
||||
| `unitronix` | `YOUR_SSH_USER` | ~150+ |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Pozostałe Pliki
|
||||
|
||||
### Deprecated (Przestarzałe pliki - ~33 wystąpienia)
|
||||
Pliki w katalogu `deprecated/` zostały częściowo zaktualizowane, ale zawierają starą dokumentację która nie jest już używana:
|
||||
- `deprecated/BAN_ENFORCER.md` - stary system banowania
|
||||
- `deprecated/BAN_ENFORCER_TEST.md` - stare testy
|
||||
|
||||
**Rekomendacja:** Te pliki są przestarzałe i nie powinny być używane. Rozważ:
|
||||
1. Całkowite usunięcie katalogu `deprecated/` przed publikacją
|
||||
2. Lub dokończenie czyszczenia tych plików
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Zabezpieczenia Wdrożone
|
||||
|
||||
### 1. Placeholders w Kodzie ✅
|
||||
Wszystkie aktywne pliki używają placeholderów zamiast rzeczywistych danych.
|
||||
|
||||
### 2. Dokumentacja Bezpieczeństwa ✅
|
||||
- [SECURITY_PLACEHOLDERS.md](SECURITY_PLACEHOLDERS.md) - instrukcja użycia
|
||||
- [SECURITY_AUDIT.md](hbbs-patch/SECURITY_AUDIT.md) - audyt bezpieczeństwa
|
||||
|
||||
### 3. .gitignore Zaktualizowany ✅
|
||||
Dodano ochronę przed przypadkowym commit'em:
|
||||
```gitignore
|
||||
.env
|
||||
.env.local
|
||||
config.local.*
|
||||
*_local.sh
|
||||
*_local.ps1
|
||||
```
|
||||
|
||||
### 4. Szablony Konfiguracji ✅
|
||||
Użytkownicy mogą bezpiecznie tworzyć lokalne pliki konfiguracyjne.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Co Dalej?
|
||||
|
||||
### Przed publikacją na GitHub:
|
||||
|
||||
1. **Sprawdź historię git:**
|
||||
```bash
|
||||
git log --all --full-history -- "*" | grep -i "192.168"
|
||||
```
|
||||
|
||||
2. **Jeśli znajdziesz wrażliwe dane w historii:**
|
||||
```bash
|
||||
# UWAGA: To przepisze całą historię!
|
||||
git filter-branch --tree-filter 'find . -type f -exec sed -i "s/192.168.0.110/YOUR_SERVER_IP/g" {} \;' HEAD
|
||||
```
|
||||
|
||||
Lub użyj BFG Repo-Cleaner:
|
||||
```bash
|
||||
bfg --replace-text passwords.txt
|
||||
git reflog expire --expire=now --all
|
||||
git gc --prune=now --aggressive
|
||||
```
|
||||
|
||||
3. **Usuń deprecated/ przed publikacją:**
|
||||
```bash
|
||||
git rm -r deprecated/
|
||||
git commit -m "Remove deprecated files with sensitive data"
|
||||
```
|
||||
|
||||
4. **Przeglądnij każdy plik przed push:**
|
||||
```bash
|
||||
git diff --name-only origin/main
|
||||
```
|
||||
|
||||
5. **Weryfikacja finalna:**
|
||||
```bash
|
||||
# Sprawdź czy nie ma więcej wrażliwych danych
|
||||
grep -r "192.168.0.110" .
|
||||
grep -r "unitronix@" .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist Przed Publikacją
|
||||
|
||||
- [ ] Usunięto katalog `deprecated/` lub wyczyszczono go z danych
|
||||
- [ ] Sprawdzono historię git pod kątem wrażliwych danych
|
||||
- [ ] Przeczytano [SECURITY_PLACEHOLDERS.md](SECURITY_PLACEHOLDERS.md)
|
||||
- [ ] Zweryfikowano że wszystkie przykłady używają placeholderów
|
||||
- [ ] Zaktualizowano README.md z linkiem do SECURITY_PLACEHOLDERS.md
|
||||
- [ ] Przetestowano czy skrypty działają po zamianie placeholderów
|
||||
- [ ] Dodano badge "Security" do README.md
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Bezpieczne Praktyki
|
||||
|
||||
### DO:
|
||||
✅ Używaj zmiennych środowiskowych
|
||||
✅ Twórz lokalne pliki konfiguracyjne (z .gitignore)
|
||||
✅ Regularnie sprawdzaj czy nie commit'ujesz wrażliwych danych
|
||||
✅ Używaj SSH keys zamiast haseł
|
||||
|
||||
### NIE RÓB:
|
||||
❌ Nie commituj plików `.env`
|
||||
❌ Nie wklejaj prawdziwych IP w issue/PR
|
||||
❌ Nie udostępniaj zrzutów ekranu z danymi
|
||||
❌ Nie hardcoduj credentials w kodzie
|
||||
|
||||
---
|
||||
|
||||
## 📞 Kontakt
|
||||
|
||||
Jeśli znajdziesz jakieś wrażliwe dane które pominąłem:
|
||||
1. **NIE** zgłaszaj ich publicznie w issue
|
||||
2. Wyślij prywatną wiadomość do maintainera
|
||||
3. Lub stwórz private security advisory na GitHub
|
||||
|
||||
---
|
||||
|
||||
**Status Bezpieczeństwa:** 🟢 BEZPIECZNY do publikacji (po wykonaniu checklist)
|
||||
|
||||
---
|
||||
|
||||
*Raport wygenerowany automatycznie przez GitHub Copilot*
|
||||
@@ -1,219 +0,0 @@
|
||||
# Security Placeholders - Configuration Guide
|
||||
|
||||
## 🔐 About Placeholders
|
||||
|
||||
This repository contains **placeholders** instead of actual server credentials for security reasons. Before using any scripts or following the documentation, you must replace these placeholders with your actual values.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Placeholders Used
|
||||
|
||||
| Placeholder | Description | Example Value |
|
||||
|------------|-------------|---------------|
|
||||
| `YOUR_SERVER_IP` | Your RustDesk server IP address | `192.168.1.100` or `server.example.com` |
|
||||
| `YOUR_SSH_USER` | SSH username for server access | `admin`, `rustdesk`, etc. |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 How to Replace Placeholders
|
||||
|
||||
### Option 1: Manual Replacement (Recommended for beginners)
|
||||
|
||||
When you see a command like this:
|
||||
```bash
|
||||
ssh YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
```
|
||||
|
||||
Replace it with your actual values:
|
||||
```bash
|
||||
ssh admin@192.168.1.100
|
||||
```
|
||||
|
||||
### Option 2: Global Find & Replace (For advanced users)
|
||||
|
||||
If you want to configure multiple files at once:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
# Navigate to project directory
|
||||
cd C:\path\to\BetterDeskConsole
|
||||
|
||||
# Replace server IP
|
||||
(Get-ChildItem -Recurse -Include *.md,*.ps1,*.sh).ForEach{
|
||||
(Get-Content $_.FullName) -replace 'YOUR_SERVER_IP', '192.168.1.100' |
|
||||
Set-Content $_.FullName
|
||||
}
|
||||
|
||||
# Replace SSH user
|
||||
(Get-ChildItem -Recurse -Include *.md,*.ps1,*.sh).ForEach{
|
||||
(Get-Content $_.FullName) -replace 'YOUR_SSH_USER', 'admin' |
|
||||
Set-Content $_.FullName
|
||||
}
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
# Replace server IP
|
||||
find . -type f \( -name "*.md" -o -name "*.ps1" -o -name "*.sh" \) \
|
||||
-exec sed -i 's/YOUR_SERVER_IP/192.168.1.100/g' {} +
|
||||
|
||||
# Replace SSH user
|
||||
find . -type f \( -name "*.md" -o -name "*.ps1" -o -name "*.sh" \) \
|
||||
-exec sed -i 's/YOUR_SSH_USER/admin/g' {} +
|
||||
```
|
||||
|
||||
### Option 3: Environment Variables (Most secure)
|
||||
|
||||
Set environment variables instead of hardcoding values:
|
||||
|
||||
**PowerShell:**
|
||||
```powershell
|
||||
$env:RUSTDESK_SERVER="192.168.1.100"
|
||||
$env:RUSTDESK_USER="admin"
|
||||
|
||||
# Use in scripts
|
||||
.\update.ps1 -RemoteHost $env:RUSTDESK_SERVER -RemoteUser $env:RUSTDESK_USER
|
||||
```
|
||||
|
||||
**Bash:**
|
||||
```bash
|
||||
export RUSTDESK_SERVER="192.168.1.100"
|
||||
export RUSTDESK_USER="admin"
|
||||
|
||||
# Use in scripts
|
||||
ssh $RUSTDESK_USER@$RUSTDESK_SERVER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📂 Files Containing Placeholders
|
||||
|
||||
The following files contain placeholders that may need to be replaced:
|
||||
|
||||
### Documentation
|
||||
- [README.md](README.md)
|
||||
- [docs/UPDATE_GUIDE.md](docs/UPDATE_GUIDE.md)
|
||||
- [docs/UPDATE_REFERENCE.md](docs/UPDATE_REFERENCE.md)
|
||||
- [docs/QUICKSTART_UPDATE.md](docs/QUICKSTART_UPDATE.md)
|
||||
- [hbbs-patch/QUICKSTART.md](hbbs-patch/QUICKSTART.md)
|
||||
- [hbbs-patch/BAN_ENFORCEMENT.md](hbbs-patch/BAN_ENFORCEMENT.md)
|
||||
|
||||
### Scripts
|
||||
- [hbbs-patch/deploy.ps1](hbbs-patch/deploy.ps1)
|
||||
- [hbbs-patch/deploy-v6.ps1](hbbs-patch/deploy-v6.ps1)
|
||||
- [hbbs-patch/deploy-v8.sh](hbbs-patch/deploy-v8.sh)
|
||||
- [hbbs-patch/test_ban_enforcement.ps1](hbbs-patch/test_ban_enforcement.ps1)
|
||||
- [hbbs-patch/diagnose_ban.ps1](hbbs-patch/diagnose_ban.ps1)
|
||||
- [dev_modules/update.ps1](dev_modules/update.ps1)
|
||||
- [dev_modules/test_ban_api.sh](dev_modules/test_ban_api.sh)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security Warnings
|
||||
|
||||
### DO NOT:
|
||||
- ❌ Commit files with real credentials to public repositories
|
||||
- ❌ Share screenshots containing real IP addresses or usernames
|
||||
- ❌ Push configuration files with actual server details
|
||||
|
||||
### DO:
|
||||
- ✅ Keep placeholders in version control
|
||||
- ✅ Use environment variables for sensitive data
|
||||
- ✅ Create a local `.env` file (add to `.gitignore`)
|
||||
- ✅ Document your actual values in a secure password manager
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Best Practices
|
||||
|
||||
### 1. Create a Local Configuration File
|
||||
|
||||
Create `.env` file (excluded from git):
|
||||
```bash
|
||||
# .env - DO NOT COMMIT THIS FILE
|
||||
RUSTDESK_SERVER_IP=192.168.1.100
|
||||
RUSTDESK_SSH_USER=admin
|
||||
RUSTDESK_DB_PATH=/opt/rustdesk/db_v2.sqlite3
|
||||
```
|
||||
|
||||
### 2. Add to .gitignore
|
||||
|
||||
```gitignore
|
||||
# Sensitive configuration
|
||||
.env
|
||||
.env.local
|
||||
config.local.ps1
|
||||
*_local.sh
|
||||
```
|
||||
|
||||
### 3. Use Configuration Templates
|
||||
|
||||
Create `config.template` files:
|
||||
```powershell
|
||||
# config.template.ps1
|
||||
$ServerIP = "YOUR_SERVER_IP"
|
||||
$SSHUser = "YOUR_SSH_USER"
|
||||
```
|
||||
|
||||
Then copy and customize:
|
||||
```powershell
|
||||
Copy-Item config.template.ps1 config.local.ps1
|
||||
# Edit config.local.ps1 with your values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/UNITRONIX/Rustdesk-FreeConsole.git
|
||||
cd Rustdesk-FreeConsole
|
||||
```
|
||||
|
||||
2. **Configure your credentials:**
|
||||
|
||||
**Option A - Environment Variables (Recommended):**
|
||||
```powershell
|
||||
# Windows
|
||||
$env:RUSTDESK_SERVER="192.168.1.100"
|
||||
$env:RUSTDESK_USER="admin"
|
||||
```
|
||||
|
||||
**Option B - Direct Replacement:**
|
||||
Follow "Option 2: Global Find & Replace" above
|
||||
|
||||
3. **Test connection:**
|
||||
```bash
|
||||
ssh YOUR_SSH_USER@YOUR_SERVER_IP # Replace placeholders!
|
||||
```
|
||||
|
||||
4. **Run scripts:**
|
||||
```powershell
|
||||
# After replacing placeholders
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you have questions about configuration:
|
||||
1. Check the [main README](README.md)
|
||||
2. Review [UPDATE_GUIDE.md](docs/UPDATE_GUIDE.md)
|
||||
3. See [Security Audit](hbbs-patch/SECURITY_AUDIT.md)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
Before running any script, verify:
|
||||
- [ ] All `YOUR_SERVER_IP` replaced with actual IP
|
||||
- [ ] All `YOUR_SSH_USER` replaced with actual username
|
||||
- [ ] SSH connection works: `ssh YOUR_SSH_USER@YOUR_SERVER_IP`
|
||||
- [ ] Server paths are correct: `/opt/rustdesk/`, `/opt/BetterDeskConsole/`
|
||||
- [ ] No actual credentials committed to git
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Security is not just about technology—it's about practice. Always think before you commit! 🔐
|
||||
@@ -1,323 +0,0 @@
|
||||
# 🚨 PILNE OSTRZEŻENIE BEZPIECZEŃSTWA
|
||||
|
||||
## ⚠️ KRYTYCZNE ZAGROŻENIE: Niezabezpieczone HTTP API
|
||||
|
||||
**Data wykrycia:** 10 stycznia 2026
|
||||
**Priorytet:** 🔴 KRYTYCZNY
|
||||
**Status:** Wymaga natychmiastowej naprawy przed wdrożeniem produkcyjnym
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Opis Problemu
|
||||
|
||||
HTTP API (port 21114) **NIE MA ŻADNEJ AUTENTYKACJI** i nasłuchuje na `0.0.0.0` (wszystkie interfejsy sieciowe).
|
||||
|
||||
### Co to oznacza?
|
||||
|
||||
```bash
|
||||
# KAŻDY w Twojej sieci może wykonać:
|
||||
curl http://YOUR_SERVER_IP:21114/api/peers
|
||||
|
||||
# I otrzyma:
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{"id": "123456789", "note": "CEO Laptop", "online": true},
|
||||
{"id": "987654321", "note": "Finance PC", "online": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Potencjalne konsekwencje:**
|
||||
- ✖️ Wyciek informacji o wszystkich urządzeniach w sieci
|
||||
- ✖️ Tracking online/offline statusu użytkowników
|
||||
- ✖️ Ekspozycja device IDs do potencjalnych ataków
|
||||
- ✖️ Naruszenie prywatności (GDPR/RODO)
|
||||
- ✖️ Reconnaissance dla atakujących
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ NATYCHMIASTOWE DZIAŁANIA
|
||||
|
||||
### Opcja 1: **Firewall (Najszybsze - 2 minuty)**
|
||||
|
||||
```bash
|
||||
# Linux (iptables)
|
||||
sudo iptables -A INPUT -p tcp --dport 21114 -s 127.0.0.1 -j ACCEPT
|
||||
sudo iptables -A INPUT -p tcp --dport 21114 -j DROP
|
||||
|
||||
# Lub (ufw)
|
||||
sudo ufw deny 21114
|
||||
sudo ufw allow from 127.0.0.1 to any port 21114
|
||||
|
||||
# Windows
|
||||
New-NetFirewallRule -DisplayName "Block HBBS API" -Direction Inbound -LocalPort 21114 -Protocol TCP -Action Block
|
||||
New-NetFirewallRule -DisplayName "Allow HBBS API Localhost" -Direction Inbound -LocalAddress 127.0.0.1 -LocalPort 21114 -Protocol TCP -Action Allow
|
||||
```
|
||||
|
||||
**Efekt:** API dostępne tylko lokalnie (localhost), konsola webowa działa, zewnętrzny dostęp zablokowany.
|
||||
|
||||
### Opcja 2: **Zmiana nasłuchiwania (5 minut)**
|
||||
|
||||
Edytuj `hbbs-patch/src/http_api.rs`:
|
||||
|
||||
```rust
|
||||
// PRZED (niebezpieczne):
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
// PO (bezpieczne):
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
```
|
||||
|
||||
Rekompiluj i wdroż:
|
||||
```bash
|
||||
cd hbbs-patch
|
||||
bash build.sh # Linux
|
||||
# LUB
|
||||
.\build-windows-local.ps1 # Windows
|
||||
|
||||
sudo systemctl restart rustdesksignal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 PEŁNE ZABEZPIECZENIE (Zalecane)
|
||||
|
||||
### 1. Autentykacja API Key
|
||||
|
||||
Edytuj `hbbs-patch/src/http_api.rs`:
|
||||
|
||||
```rust
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
http::{Request, StatusCode, header::HeaderMap},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use std::env;
|
||||
|
||||
// API Key middleware
|
||||
async fn check_api_key<B>(
|
||||
headers: HeaderMap,
|
||||
request: Request<B>,
|
||||
next: Next<B>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
// Pobierz klucz z zmiennej środowiskowej
|
||||
let expected_key = env::var("HBBS_API_KEY").unwrap_or_else(|_| {
|
||||
log::warn!("HBBS_API_KEY not set, using default (INSECURE!)");
|
||||
"CHANGE_ME_INSECURE_DEFAULT".to_string()
|
||||
});
|
||||
|
||||
// Sprawdź nagłówek X-API-Key
|
||||
if let Some(api_key) = headers.get("X-API-Key") {
|
||||
if api_key.to_str().ok() == Some(&expected_key) {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!("Unauthorized API access attempt from {:?}", request.uri());
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
|
||||
pub async fn start_api_server(/* ... */) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// ... existing code ...
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/health", get(health_check))
|
||||
.route("/api/peers", get(get_online_peers))
|
||||
.layer(middleware::from_fn(check_api_key)) // ← DODAJ TO
|
||||
.layer(axum::Extension(state));
|
||||
|
||||
// Opcjonalnie: bind tylko do localhost
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
|
||||
log::info!("HTTP API server listening on {} (with API Key auth)", addr);
|
||||
|
||||
// ... rest of code ...
|
||||
}
|
||||
```
|
||||
|
||||
**Aktualizacja `app.py`:**
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
# Na początku pliku
|
||||
API_KEY = os.environ.get('HBBS_API_KEY', 'CHANGE_ME_INSECURE_DEFAULT')
|
||||
|
||||
# W funkcjach wywołujących API:
|
||||
headers = {'X-API-Key': API_KEY}
|
||||
response = requests.get(f'{HBBS_API_URL}/peers', timeout=2, headers=headers)
|
||||
```
|
||||
|
||||
**Ustawienie klucza:**
|
||||
|
||||
```bash
|
||||
# Linux (dodaj do /etc/environment lub .bashrc)
|
||||
export HBBS_API_KEY="$(openssl rand -hex 32)"
|
||||
|
||||
# Systemd service
|
||||
sudo nano /etc/systemd/system/rustdesksignal.service
|
||||
# Dodaj linię:
|
||||
Environment="HBBS_API_KEY=your-secure-random-key-here"
|
||||
|
||||
# Flask service
|
||||
sudo nano /etc/systemd/system/betterdesk.service
|
||||
# Dodaj linię:
|
||||
Environment="HBBS_API_KEY=your-secure-random-key-here"
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart rustdesksignal betterdesk
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows (jako Administrator)
|
||||
[System.Environment]::SetEnvironmentVariable("HBBS_API_KEY", "your-secure-random-key", "Machine")
|
||||
|
||||
# Restart serwisów
|
||||
Restart-Service RustDesk*
|
||||
```
|
||||
|
||||
### 2. Rate Limiting
|
||||
|
||||
```bash
|
||||
pip install Flask-Limiter
|
||||
```
|
||||
|
||||
```python
|
||||
# app.py
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
limiter = Limiter(
|
||||
app=app,
|
||||
key_func=get_remote_address,
|
||||
default_limits=["200 per day", "50 per hour"],
|
||||
storage_uri="memory://"
|
||||
)
|
||||
|
||||
@app.route('/api/devices')
|
||||
@limiter.limit("30 per minute")
|
||||
def get_devices():
|
||||
# ... existing code ...
|
||||
```
|
||||
|
||||
### 3. CORS Protection
|
||||
|
||||
```bash
|
||||
cargo add tower-http --features cors
|
||||
```
|
||||
|
||||
```rust
|
||||
// http_api.rs
|
||||
use tower_http::cors::{CorsLayer, Any};
|
||||
use http::Method;
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin("http://localhost:5000".parse::<HeaderValue>().unwrap())
|
||||
.allow_methods([Method::GET])
|
||||
.allow_headers([HeaderName::from_static("x-api-key")]);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/peers", get(get_online_peers))
|
||||
.layer(cors)
|
||||
.layer(middleware::from_fn(check_api_key))
|
||||
.layer(axum::Extension(state));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ WERYFIKACJA ZABEZPIECZEŃ
|
||||
|
||||
### Test 1: Firewall działa
|
||||
|
||||
```bash
|
||||
# Z innego komputera w sieci:
|
||||
curl http://YOUR_SERVER_IP:21114/api/health
|
||||
# Powinno: Connection refused lub timeout
|
||||
|
||||
# Z serwera lokalnie:
|
||||
curl http://localhost:21114/api/health
|
||||
# Powinno: {"success":true,"data":"RustDesk API is running"}
|
||||
```
|
||||
|
||||
### Test 2: API Key działa
|
||||
|
||||
```bash
|
||||
# Bez klucza:
|
||||
curl http://localhost:21114/api/peers
|
||||
# Powinno: 401 Unauthorized
|
||||
|
||||
# Z kluczem:
|
||||
curl -H "X-API-Key: YOUR_KEY" http://localhost:21114/api/peers
|
||||
# Powinno: {"success":true,"data":[...]}
|
||||
```
|
||||
|
||||
### Test 3: Rate Limiting działa
|
||||
|
||||
```bash
|
||||
# Wyślij 50 requestów szybko:
|
||||
for i in {1..50}; do curl http://localhost:5000/api/devices; done
|
||||
# Po ~30 requestach powinno: 429 Too Many Requests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 CHECKLIST WDROŻENIA
|
||||
|
||||
### Minimalne zabezpieczenie (przed produkcją):
|
||||
- [ ] Firewall blokuje port 21114 z zewnątrz
|
||||
- [ ] HBBS API nasłuchuje tylko na 127.0.0.1
|
||||
- [ ] Logi monitorowane pod kątem podejrzanej aktywności
|
||||
|
||||
### Pełne zabezpieczenie (zalecane):
|
||||
- [ ] API Key authentication zaimplementowane
|
||||
- [ ] Rate limiting w Flask
|
||||
- [ ] CORS skonfigurowany
|
||||
- [ ] Klucze w zmiennych środowiskowych
|
||||
- [ ] HTTPS/TLS dla produkcji (certyfikat SSL)
|
||||
- [ ] Monitoring i alerty
|
||||
|
||||
---
|
||||
|
||||
## 🚦 BIEŻĄCY STATUS ZABEZPIECZEŃ
|
||||
|
||||
| Warstwa | Status | Uwagi |
|
||||
|---------|--------|-------|
|
||||
| SQL Injection | ✅ ZABEZPIECZONE | Parametryzowane zapytania |
|
||||
| XSS | ⚠️ CZĘŚCIOWE | Podstawowa sanityzacja |
|
||||
| Buffer Overflow | ✅ ZABEZPIECZONE | Rust type safety |
|
||||
| Race Conditions | ✅ ZABEZPIECZONE | Arc/RwLock |
|
||||
| **Authentication** | 🔴 **BRAK** | **WYMAGA NAPRAWY** |
|
||||
| Authorization | 🔴 BRAK | Wymaga naprawy |
|
||||
| Rate Limiting | 🔴 BRAK | Wymaga naprawy |
|
||||
| CORS | 🔴 BRAK | Wymaga naprawy |
|
||||
| HTTPS/TLS | ⚠️ OPCJONALNE | Zalecane dla WAN |
|
||||
|
||||
---
|
||||
|
||||
## 📞 DALSZE KROKI
|
||||
|
||||
1. **Natychmiast:** Zastosuj firewall (Opcja 1)
|
||||
2. **Dziś:** Zmień bind na 127.0.0.1 (Opcja 2)
|
||||
3. **W tym tygodniu:** Implementuj API Key authentication
|
||||
4. **Przy okazji:** Rate limiting + CORS
|
||||
|
||||
---
|
||||
|
||||
## ⚖️ ODPOWIEDZIALNOŚĆ
|
||||
|
||||
**Obecny stan:**
|
||||
System działa poprawnie funkcjonalnie, ale ma krytyczną lukę w zabezpieczeniach.
|
||||
**NIE WDRAŻAJ DO PRODUKCJI** bez zastosowania minimum Opcji 1 lub 2.
|
||||
|
||||
**Po zastosowaniu poprawek:**
|
||||
System bezpieczny dla użytku wewnętrznego w sieci lokalnej. Dla ekspozycji na internet dodatkowy HTTPS + hardening.
|
||||
|
||||
---
|
||||
|
||||
**Autor analizy:** GitHub Copilot
|
||||
**Data:** 10 stycznia 2026
|
||||
**Wersja dokumentu:** 1.0
|
||||
@@ -1,227 +0,0 @@
|
||||
# Update Scripts - Quick Reference
|
||||
|
||||
## 📋 Command Syntax
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
sudo ./update.sh [OPTIONS]
|
||||
```
|
||||
|
||||
### Windows
|
||||
```powershell
|
||||
.\update.ps1 -RemoteHost <IP> -RemoteUser <user> [OPTIONS]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Parameters
|
||||
|
||||
### Linux (update.sh)
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `--rustdesk-dir PATH` | RustDesk installation directory | `/opt/rustdesk` |
|
||||
| `--console-dir PATH` | BetterDesk Console directory | `/opt/BetterDeskConsole` |
|
||||
| `--help` | Show help message | - |
|
||||
|
||||
### Windows (update.ps1)
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-RemoteHost` | Server IP/hostname | **(required)** |
|
||||
| `-RemoteUser` | SSH username | **(required)** |
|
||||
| `-RemotePath` | BetterDesk Console directory | `/opt/BetterDeskConsole` |
|
||||
| `-RustDeskPath` | RustDesk installation directory | `/opt/rustdesk` |
|
||||
| `-DbPath` | Database file path | `{RustDeskPath}/db_v2.sqlite3` |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Usage Examples
|
||||
|
||||
### Basic Update (Default Paths)
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo ./update.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Custom RustDesk Directory
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo ./update.sh --rustdesk-dir /custom/rustdesk
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER -RustDeskPath "/custom/rustdesk"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Custom Console Directory
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo ./update.sh --console-dir /var/www/betterdesk
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER -RemotePath "/var/www/betterdesk"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Both Custom Directories
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo ./update.sh --rustdesk-dir /home/user/rustdesk --console-dir /home/user/console
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER `
|
||||
-RustDeskPath "/home/user/rustdesk" `
|
||||
-RemotePath "/home/user/console"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Custom Database Path (Windows only)
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
.\update.ps1 -RemoteHost YOUR_SERVER_IP -RemoteUser YOUR_SSH_USER `
|
||||
-DbPath "/custom/database/location/db.sqlite3"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Default File Locations
|
||||
|
||||
### Standard Installation
|
||||
```
|
||||
/opt/rustdesk/
|
||||
├── db_v2.sqlite3 # Database
|
||||
├── hbbs # HBBS binary
|
||||
└── id_ed25519.pub # Public key
|
||||
|
||||
/opt/BetterDeskConsole/
|
||||
├── app.py # Flask backend
|
||||
├── static/
|
||||
│ └── script.js # Frontend JS
|
||||
└── templates/
|
||||
└── index.html # UI template
|
||||
```
|
||||
|
||||
### Custom Installation Example
|
||||
```
|
||||
/home/admin/services/rustdesk/
|
||||
└── db_v2.sqlite3
|
||||
|
||||
/var/www/betterdesk/
|
||||
├── app.py
|
||||
├── static/
|
||||
└── templates/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Quick Verification
|
||||
|
||||
After update, check:
|
||||
|
||||
```bash
|
||||
# Check service
|
||||
systemctl status betterdesk
|
||||
|
||||
# Check database columns
|
||||
sqlite3 /opt/rustdesk/db_v2.sqlite3 "PRAGMA table_info(peer);" | grep -E "is_banned|is_deleted"
|
||||
|
||||
# Check web console
|
||||
curl http://localhost:5000/api/stats
|
||||
```
|
||||
|
||||
Expected API response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"stats": {
|
||||
"total": 51,
|
||||
"active": 14,
|
||||
"inactive": 37,
|
||||
"banned": 0,
|
||||
"with_notes": 21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔙 Rollback
|
||||
|
||||
If update fails, restore from backup:
|
||||
|
||||
```bash
|
||||
# Find backup
|
||||
ls -ltr /opt/ | grep betterdesk-backup
|
||||
|
||||
# Restore
|
||||
BACKUP_DIR="/opt/betterdesk-backup-YYYYMMDD-HHMMSS"
|
||||
sudo systemctl stop betterdesk
|
||||
sudo cp $BACKUP_DIR/db_v2.sqlite3.backup /opt/rustdesk/db_v2.sqlite3
|
||||
sudo cp $BACKUP_DIR/*.backup /opt/BetterDeskConsole/
|
||||
sudo systemctl start betterdesk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Linux: Permission Denied
|
||||
```bash
|
||||
chmod +x update.sh
|
||||
sudo ./update.sh
|
||||
```
|
||||
|
||||
### Windows: SSH Connection Failed
|
||||
```powershell
|
||||
# Test connection
|
||||
ssh YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
|
||||
# Set up SSH keys
|
||||
ssh-keygen
|
||||
ssh-copy-id YOUR_SSH_USER@YOUR_SERVER_IP
|
||||
```
|
||||
|
||||
### Windows: Execution Policy
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
### Database Migration Failed
|
||||
```bash
|
||||
# Check permissions
|
||||
ls -l /opt/rustdesk/db_v2.sqlite3
|
||||
|
||||
# Run manually
|
||||
sudo python3 migrations/v1.1.0_device_bans.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 See Also
|
||||
|
||||
- [UPDATE_GUIDE.md](UPDATE_GUIDE.md) - Full documentation
|
||||
- [QUICKSTART_UPDATE.md](QUICKSTART_UPDATE.md) - Detailed examples
|
||||
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||
- [README.md](README.md) - Main documentation
|
||||
@@ -1,149 +0,0 @@
|
||||
# HBBS Ban Check Patch
|
||||
|
||||
Modyfikacja RustDesk Server (hbbs) v1.1.14 dodająca sprawdzanie zbanowanych urządzeń.
|
||||
|
||||
## Zmiany
|
||||
|
||||
### 1. database.rs - Dodanie metody sprawdzania bana
|
||||
|
||||
Dodaj nową metodę do struktury `Database`:
|
||||
|
||||
```rust
|
||||
impl Database {
|
||||
// ... existing methods ...
|
||||
|
||||
/// Check if a device is banned
|
||||
pub async fn is_device_banned(&self, id: &str) -> ResultType<bool> {
|
||||
let result = sqlx::query!(
|
||||
"SELECT is_banned FROM peer WHERE id = ? AND is_deleted = 0",
|
||||
id
|
||||
)
|
||||
.fetch_optional(self.pool.get().await?.deref_mut())
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| r.is_banned.unwrap_or(0) == 1).unwrap_or(false))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. peer.rs - Sprawdzenie bana podczas rejestracji
|
||||
|
||||
Modyfikuj metodę `update_pk`:
|
||||
|
||||
```rust
|
||||
impl PeerMap {
|
||||
#[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);
|
||||
|
||||
// *** NOWE: Sprawdź czy urządzenie jest zbanowane ***
|
||||
match self.db.is_device_banned(&id).await {
|
||||
Ok(true) => {
|
||||
log::warn!("Registration rejected: device {} is BANNED", id);
|
||||
return register_pk_response::Result::UUID_MISMATCH; // Odrzuć rejestrację
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check ban status for {}: {}", id, e);
|
||||
// W razie błędu bazy, przepuść (fail-open)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ... reszta oryginalnego kodu ...
|
||||
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.info.ip = ip;
|
||||
(
|
||||
serde_json::to_string(&w.info).unwrap_or_default(),
|
||||
w.guid.clone(),
|
||||
)
|
||||
};
|
||||
// ... reszta metody bez zmian ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Migracja bazy danych
|
||||
|
||||
Upewnij się, że tabela `peer` ma kolumnę `is_banned`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE peer ADD COLUMN is_banned INTEGER DEFAULT 0;
|
||||
ALTER TABLE peer ADD COLUMN banned_at INTEGER;
|
||||
ALTER TABLE peer ADD COLUMN banned_by VARCHAR(100);
|
||||
ALTER TABLE peer ADD COLUMN ban_reason TEXT;
|
||||
CREATE INDEX IF NOT EXISTS idx_peer_is_banned ON peer(is_banned);
|
||||
```
|
||||
|
||||
## Kompilacja
|
||||
|
||||
```bash
|
||||
cd rustdesk-server
|
||||
cargo build --release --bin hbbs
|
||||
|
||||
# Skompilowany binarny:
|
||||
# target/release/hbbs
|
||||
```
|
||||
|
||||
## Instalacja
|
||||
|
||||
```bash
|
||||
# Backup starego
|
||||
sudo systemctl stop hbbs
|
||||
sudo cp /opt/rustdesk/hbbs /opt/rustdesk/hbbs.backup
|
||||
|
||||
# Zainstaluj nowy
|
||||
sudo cp target/release/hbbs /opt/rustdesk/
|
||||
sudo chmod +x /opt/rustdesk/hbbs
|
||||
|
||||
# Restart
|
||||
sudo systemctl start hbbs
|
||||
sudo systemctl status hbbs
|
||||
```
|
||||
|
||||
## Weryfikacja
|
||||
|
||||
```bash
|
||||
# Sprawdź logi
|
||||
sudo journalctl -u hbbs -f
|
||||
|
||||
# Podczas próby połączenia zbanowanego urządzenia:
|
||||
# "Registration rejected: device 123456789 is BANNED"
|
||||
```
|
||||
|
||||
## Jak to działa
|
||||
|
||||
1. **Klient próbuje się połączyć** → wysyła `RegisterPk` request
|
||||
2. **HBBS odbiera request** → wywołuje `update_pk()`
|
||||
3. **Sprawdzenie bazy** → `is_device_banned()` query
|
||||
4. **Jeśli is_banned=1** → zwraca `UUID_MISMATCH` (odrzucenie)
|
||||
5. **Klient dostaje błąd** → nie może się połączyć
|
||||
|
||||
## Różnice vs Ban Enforcer
|
||||
|
||||
| Ban Enforcer (stary) | HBBS Patch (nowy) |
|
||||
|---|---|
|
||||
| Czyści dane co 2s | Sprawdza przy każdej rejestracji |
|
||||
| Wyścig z RustDesk | Natywna integracja |
|
||||
| Możliwe "okna" | 100% skuteczność |
|
||||
| +1 demon | Bez dodatkowych procesów |
|
||||
| Modyfikuje bazę | Tylko odczyt |
|
||||
|
||||
## Uwagi
|
||||
|
||||
- Używamy `UUID_MISMATCH` jako kodu błędu (RustDesk go rozumie)
|
||||
- "Fail-open" - jeśli baza nie odpowiada, przepuszczamy ruch (bezpieczeństwo > dostępność)
|
||||
- Index na `is_banned` przyspiesza query
|
||||
- Kompatybilne z istniejącymi kolumnami bazy
|
||||
@@ -64,7 +64,7 @@ This matches the **exact same mechanism** used by the RustDesk desktop client to
|
||||
**Purpose**: Entry point for HBBS server
|
||||
|
||||
**Modifications**:
|
||||
- Added `api_port` parameter (default: 21114)
|
||||
- Added `api_port` parameter (default: 21120)
|
||||
- Passes API port to `RendezvousServer::start()`
|
||||
- No changes to core HBBS functionality
|
||||
|
||||
@@ -76,7 +76,7 @@ RendezvousServer::start(
|
||||
serial,
|
||||
&get_arg_or("key", "-".to_owned()),
|
||||
rmem,
|
||||
21114 // API port
|
||||
21120 // API port
|
||||
)?;
|
||||
```
|
||||
|
||||
@@ -175,7 +175,7 @@ RendezvousServer::start(
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ HTTP API Server │
|
||||
│ (Port 21114) │
|
||||
│ (Port 21120) │
|
||||
└─────────────┬───────────────┘
|
||||
│ Query status
|
||||
▼
|
||||
@@ -248,7 +248,7 @@ Binary output: `target/release/hbbs`
|
||||
|
||||
### Health Check:
|
||||
```bash
|
||||
curl http://localhost:21114/api/health
|
||||
curl http://localhost:21120/api/health
|
||||
```
|
||||
|
||||
Response:
|
||||
@@ -262,7 +262,7 @@ Response:
|
||||
|
||||
### List Peers:
|
||||
```bash
|
||||
curl http://localhost:21114/api/peers
|
||||
curl http://localhost:21120/api/peers
|
||||
```
|
||||
|
||||
Response:
|
||||
@@ -298,16 +298,62 @@ Response:
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **API Binding**: By default, API listens on `0.0.0.0:21114`
|
||||
- Consider using a firewall to restrict access
|
||||
- Or modify `http_api.rs` to bind to `127.0.0.1` only
|
||||
### API Authentication (v1.4.0+)
|
||||
|
||||
2. **No Authentication**: Current implementation has no API authentication
|
||||
- Suitable for internal networks
|
||||
- For public exposure, add authentication middleware
|
||||
**X-API-Key Authentication**: The HBBS API now requires authentication for all requests:
|
||||
|
||||
3. **CORS**: Enabled for all origins (`*`)
|
||||
- Modify `http_api.rs` CORS settings if needed
|
||||
1. **API Key Generation**: During installation, a 64-character random API key is generated:
|
||||
```bash
|
||||
openssl rand -base64 48 | tr -d '/+=' | cut -c1-64
|
||||
```
|
||||
|
||||
2. **Key Storage**: Stored securely in `/opt/rustdesk/.api_key` with 600 permissions
|
||||
|
||||
3. **Usage**: All API requests must include the `X-API-Key` header:
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://192.168.1.100:21120/api/health
|
||||
```
|
||||
|
||||
4. **Verification**: Middleware checks the header against stored key:
|
||||
```rust
|
||||
async fn verify_api_key(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let api_key = headers
|
||||
.get("X-API-Key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if api_key != state.api_key {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
```
|
||||
|
||||
### Network Access
|
||||
|
||||
1. **API Binding**: API listens on `0.0.0.0:21120` (LAN accessible)
|
||||
- Protected by X-API-Key authentication
|
||||
- Web console automatically provides key
|
||||
- External tools need API key from `/opt/rustdesk/.api_key`
|
||||
|
||||
2. **Firewall Recommendations**:
|
||||
```bash
|
||||
# Allow API on LAN only
|
||||
sudo ufw allow from 192.168.0.0/16 to any port 21120 proto tcp
|
||||
|
||||
# Or allow web console only (API via localhost)
|
||||
sudo ufw allow 5000/tcp
|
||||
```
|
||||
|
||||
3. **CORS**: Enabled for all origins with credentials support
|
||||
- Safe due to API key requirement
|
||||
- Modify `http_api.rs` if stricter CORS needed
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,10 +1,79 @@
|
||||
# Audyt Bezpieczeństwa - Modyfikacje RustDesk Server
|
||||
**Data:** 6 stycznia 2026
|
||||
**Wersja:** v8 (dwukierunkowe blokowanie banów)
|
||||
**Data audytu:** 6 stycznia 2026
|
||||
**Ostatnia aktualizacja:** 11 stycznia 2026 (v1.4.0 - dodano autentykację API)
|
||||
**Wersja:** v8 (dwukierunkowe blokowanie banów) + v1.4.0 (API key authentication)
|
||||
**Audytor:** GitHub Copilot
|
||||
|
||||
---
|
||||
|
||||
## ✅ Zmiany Bezpieczeństwa v1.4.0 (11 stycznia 2026)
|
||||
|
||||
### 🔐 Autentykacja API (X-API-Key)
|
||||
|
||||
**Rozwiązane zagrożenie:** Brak autentykacji HTTP API
|
||||
|
||||
**Implementacja:**
|
||||
1. **Generowanie klucza API**:
|
||||
- 64-znakowy losowy klucz przy instalacji
|
||||
- Algorytm: `openssl rand -base64 48 | tr -d '/+=' | cut -c1-64`
|
||||
- Przechowywany w `/opt/rustdesk/.api_key` z uprawnieniami 600
|
||||
|
||||
2. **Middleware weryfikacji** (http_api.rs):
|
||||
```rust
|
||||
async fn verify_api_key(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let api_key = headers
|
||||
.get("X-API-Key")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if api_key != state.api_key {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
```
|
||||
|
||||
3. **Wszystkie endpointy chronione**:
|
||||
- `/api/health` - wymaga X-API-Key
|
||||
- `/api/peers` - wymaga X-API-Key
|
||||
- Brak klucza = 401 Unauthorized
|
||||
- Nieprawidłowy klucz = 401 Unauthorized
|
||||
|
||||
4. **Dostęp LAN**:
|
||||
- API nasłuchuje na `0.0.0.0:21120` (dostępne w sieci LAN)
|
||||
- Konsola web automatycznie dodaje X-API-Key do wszystkich żądań
|
||||
- Zewnętrzne narzędzia muszą pobrać klucz z `/opt/rustdesk/.api_key`
|
||||
|
||||
**Status:** ✅ ZAIMPLEMENTOWANE
|
||||
|
||||
### 🌐 Konsola Web - System Uwierzytelniania
|
||||
|
||||
**Funkcje bezpieczeństwa:**
|
||||
1. **Logowanie użytkowników**:
|
||||
- Hashowanie haseł bcrypt (cost 12)
|
||||
- Tokeny sesji (24 godziny)
|
||||
- Kontrola dostępu oparta na rolach (admin/operator/viewer)
|
||||
|
||||
2. **Zarządzanie użytkownikami**:
|
||||
- Panel administracyjny do tworzenia/edycji/usuwania użytkowników
|
||||
- Audit log dla wszystkich akcji
|
||||
- Ochrona hasłem dostępu do klucza publicznego
|
||||
|
||||
3. **Ochrona danych**:
|
||||
- Parametryzowane zapytania SQL
|
||||
- Walidacja danych wejściowych
|
||||
- Ochrona XSS/CSRF
|
||||
|
||||
**Status:** ✅ ZAIMPLEMENTOWANE
|
||||
|
||||
---
|
||||
|
||||
## 1. Streszczenie Wykonawcze
|
||||
|
||||
### 🔴 Krytyczne zagrożenia: 2
|
||||
@@ -12,6 +81,8 @@
|
||||
### 🟡 Średnie zagrożenia: 2
|
||||
### 🟢 Niskie zagrożenia: 3
|
||||
|
||||
**Uwaga:** Zagrożenia poniżej dotyczą głównie mechanizmu banowania urządzeń, nie API HTTP.
|
||||
|
||||
---
|
||||
|
||||
## 2. Krytyczne Zagrożenia
|
||||
|
||||
@@ -1,120 +1,28 @@
|
||||
# Binary Checksums - v1.3.0-secure
|
||||
# HBBS/HBBR v8-api Binary Checksums
|
||||
# Generated: 2026-01-15 19:41:12
|
||||
# All binaries compiled with HTTP API integration on port 21120
|
||||
|
||||
Verification checksums for BetterDesk Console binaries with secure API (port 21120, localhost-only binding).
|
||||
## hbbs-v8-api (Linux x86_64)
|
||||
- **SHA256**: `DBCECD4DF6F3DFE13BD08DF1A03998A72D26FC6C168067D234A0EC7DCADDFA73`
|
||||
- **Size**: 9.61 MB
|
||||
- **Date**: 2026-01-15 19:12:42
|
||||
- **Features**: HTTP API, ban enforcement, fail-closed security
|
||||
|
||||
## SHA256 Checksums
|
||||
## hbbr-v8-api (Linux x86_64)
|
||||
- **SHA256**: `CA8B9D3E25E9EBE1C4BDA91E5E763B3715524E18B338C4FC4A07BC6B1F10B270`
|
||||
- **Size**: 3.03 MB
|
||||
- **Date**: 2026-01-15 19:12:43
|
||||
- **Features**: Relay server, fail-closed security
|
||||
|
||||
### Linux Binaries (x86_64)
|
||||
## hbbs-v8-api.exe (Windows x86_64)
|
||||
- **SHA256**: `BEF45AEA8D9320A09C5440EB39319D69042E5DFE5A45028EB169AB252A94A7A3`
|
||||
- **Size**: 7.23 MB
|
||||
- **Date**: 2026-01-15 19:41:12
|
||||
- **Features**: HTTP API, ban enforcement, fail-closed security
|
||||
|
||||
```
|
||||
7B09A6C024188AF5AAC8E94C64B4B97D68A92ABF7F902B34A7D91A9D99E44558 hbbs-v8-api
|
||||
DF1B3FD3EF8793FD3A786E2BFBB330EE43A6C92D1A5915414F36011BE778E3FB hbbr-v8-api
|
||||
```
|
||||
## hbbr-v8-api.exe (Windows x86_64)
|
||||
- **SHA256**: `164FA5508F9DCC09AB7FA95AA1F90960BE1B5D71849323D99686EA83307D181F`
|
||||
- **Size**: 2.75 MB
|
||||
- **Date**: 2026-01-15 19:40:43
|
||||
- **Features**: Relay server, fail-closed security
|
||||
|
||||
**Build Date:** 10.01.2026 10:25
|
||||
**Size:** HBBS 9.59 MB, HBBR 4.73 MB
|
||||
**Platform:** Linux x86_64 (Ubuntu 20.04+, Debian 11+)
|
||||
|
||||
### Windows Binaries (x64)
|
||||
|
||||
```
|
||||
EE1AB9C341B078D852EA32ED33CCD8664BC6A3D6EA818D321529B9654C69CD74 hbbs-v8-api.exe
|
||||
37F452AE97407992DE1561B5F90747D9396E591C21E70B27897EEBEB652C1D25 hbbr-v8-api.exe
|
||||
```
|
||||
|
||||
**Build Date:** 10.01.2026 04:42
|
||||
**Size:** HBBS 6.58 MB, HBBR 2.76 MB
|
||||
**Platform:** Windows x64 (Windows 10+, Server 2016+)
|
||||
|
||||
## Verification
|
||||
|
||||
### Linux/macOS
|
||||
|
||||
```bash
|
||||
# Verify single file
|
||||
sha256sum hbbs-v8-api
|
||||
# Compare with checksum above
|
||||
|
||||
# Verify all Linux binaries
|
||||
sha256sum hbbs-v8-api hbbr-v8-api
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
# Verify single file
|
||||
Get-FileHash hbbs-v8-api.exe -Algorithm SHA256
|
||||
|
||||
# Verify all Windows binaries
|
||||
Get-ChildItem *.exe | ForEach-Object { Get-FileHash $_.Name -Algorithm SHA256 }
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
All binaries (both Linux and Windows) include:
|
||||
|
||||
✅ **Port 21120 API** - Changed from default 21114
|
||||
✅ **Localhost-only binding** - API accessible only from 127.0.0.1
|
||||
✅ **--api-port parameter** - Command-line configuration support
|
||||
✅ **Zero network exposure** - API cannot be accessed from external networks
|
||||
✅ **Bidirectional ban enforcement** - Source + target ban checks
|
||||
✅ **Real-time database sync** - No restart required for ban changes
|
||||
|
||||
## Build Information
|
||||
|
||||
- **Base Version:** RustDesk Server 1.1.14
|
||||
- **Compiler:** cargo 1.92.0, rustc 1.92.0
|
||||
- **Source:** Modified hbbs-patch with security enhancements
|
||||
- **Configuration:**
|
||||
- HTTP API on port 21120 (instead of 21114)
|
||||
- Binding to 127.0.0.1 only (not 0.0.0.0)
|
||||
- API endpoints: `/api/health`, `/api/peers`
|
||||
|
||||
## Verification Log
|
||||
|
||||
```bash
|
||||
# Example successful verification
|
||||
$ sha256sum hbbs-v8-api
|
||||
7B09A6C024188AF5AAC8E94C64B4B97D68A92ABF7F902B34A7D91A9D99E44558 hbbs-v8-api
|
||||
✓ Checksum matches
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Checksum Mismatch
|
||||
|
||||
If checksums don't match:
|
||||
|
||||
1. **Re-download the binary** - May be corrupted during transfer
|
||||
2. **Check file size** - Should match sizes listed above
|
||||
3. **Verify platform** - Don't mix Linux/Windows binaries
|
||||
4. **Check Git LFS** - Ensure large files downloaded correctly
|
||||
|
||||
### Binary Won't Execute
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
chmod +x hbbs-v8-api hbbr-v8-api
|
||||
./hbbs-v8-api --help
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
# Run as Administrator
|
||||
.\hbbs-v8-api.exe --help
|
||||
```
|
||||
|
||||
## Change Log
|
||||
|
||||
### v1.3.0-secure (10.01.2026)
|
||||
|
||||
- Changed API port from 21114 to 21120
|
||||
- Added localhost-only binding (127.0.0.1)
|
||||
- Security enhancement: API not exposed to network
|
||||
- Added --api-port command-line parameter
|
||||
- Updated Linux binaries (10:25 UTC)
|
||||
- Retained Windows binaries from previous build (compatible)
|
||||
|
||||
### Previous Versions
|
||||
|
||||
See [CHANGELOG.md](../../CHANGELOG.md) for full history.
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# SHA256 Checksums for BetterDesk Console v8 Binaries
|
||||
|
||||
Generated: 2026-01-06
|
||||
|
||||
## Binaries
|
||||
|
||||
### hbbs-v8 (Signal Server)
|
||||
```
|
||||
SHA256: 402964335B0AA4B57E37FA52E55C41F386FE5C13487F9CA9319D6A03420A56AA
|
||||
Size: 9,501,528 bytes (9.5 MB)
|
||||
File: hbbs-v8
|
||||
```
|
||||
|
||||
### hbbr-v8 (Relay Server)
|
||||
```
|
||||
SHA256: 9C9CB8F1BF1C5800A7419592A24BA9A00ECBB075D903D2DB09B3F5F936DB3A71
|
||||
Size: 4,961,976 bytes (5.0 MB)
|
||||
File: hbbr-v8
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
### Linux/macOS
|
||||
```bash
|
||||
sha256sum hbbs-v8 hbbr-v8
|
||||
```
|
||||
|
||||
### Windows (PowerShell)
|
||||
```powershell
|
||||
Get-FileHash hbbs-v8, hbbr-v8 -Algorithm SHA256 | Format-Table
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
```
|
||||
402964335b0aa4b57e37fa52e55c41f386fe5c13487f9ca9319d6a03420a56aa hbbs-v8
|
||||
9c9cb8f1bf1c5800a7419592a24ba9a00ecbb075d903d2db09b3f5f936db3a71 hbbr-v8
|
||||
```
|
||||
|
||||
## Build Information
|
||||
|
||||
- **Base Version**: RustDesk Server v1.1.14
|
||||
- **Build Date**: 2026-01-06
|
||||
- **Architecture**: x86_64-unknown-linux-gnu
|
||||
- **Compiler**: rustc 1.75+ (stable channel)
|
||||
- **Patches Applied**: 8 (see build.sh)
|
||||
- **Build Script**: ../build.sh
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **Authenticity**: These binaries were compiled from open source code
|
||||
2. **Reproducibility**: You can rebuild using `../build.sh` and verify patches
|
||||
3. **Source Verification**: All patches documented in source files
|
||||
4. **No Obfuscation**: Standard Rust compilation, no custom modifications
|
||||
5. **Audit Available**: See ../SECURITY_AUDIT.md for security review
|
||||
|
||||
## Re-compilation
|
||||
|
||||
If you want to verify the binaries or compile for different architecture:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
./build.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Clone RustDesk Server v1.1.14
|
||||
2. Apply all 8 patches automatically
|
||||
3. Compile HBBS and HBBR
|
||||
4. Generate new binaries in build directory
|
||||
|
||||
Compare checksums of your compiled binaries with these to verify integrity.
|
||||
|
||||
## Version History
|
||||
|
||||
- **v8** (2026-01-06): Bidirectional ban enforcement
|
||||
- **v7** (2026-01-06): IP-based source tracking (not released)
|
||||
- **v6** (2026-01-05): HBBR ban enforcement (not released)
|
||||
- **v5** (2026-01-05): Relay server compilation (not released)
|
||||
- **v2-v4**: Development versions (not released)
|
||||
|
||||
Only v8 binaries are included in this repository for production use.
|
||||
@@ -1,141 +0,0 @@
|
||||
# Precompiled Binaries
|
||||
|
||||
This directory contains precompiled RustDesk server binaries with enhanced ban enforcement.
|
||||
|
||||
## Files
|
||||
|
||||
- **hbbs-v8** - Signal server (HBBS) with bidirectional ban enforcement
|
||||
- **hbbr-v8** - Relay server (HBBR) with bidirectional ban enforcement
|
||||
|
||||
## Features (v8)
|
||||
|
||||
### Bidirectional Ban Enforcement
|
||||
|
||||
The v8 binaries include comprehensive ban checking:
|
||||
|
||||
1. **Source Device Ban Check**
|
||||
- Prevents banned devices from initiating any connections
|
||||
- Blocks at punch hole request stage (P2P)
|
||||
- Blocks at relay request stage (relay connections)
|
||||
|
||||
2. **Target Device Ban Check**
|
||||
- Prevents connections to banned devices
|
||||
- Protects banned devices from receiving unwanted connection attempts
|
||||
|
||||
3. **Real-time Database Sync**
|
||||
- Ban status checked against SQLite database
|
||||
- Instant enforcement when devices are banned via web console
|
||||
- No server restart required
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Base Version**: RustDesk Server v1.1.14
|
||||
- **Compiled**: January 2026
|
||||
- **Architecture**: x86_64 Linux
|
||||
- **Dependencies**: rusqlite (for ban database access)
|
||||
- **Build Script**: [build.sh](../build.sh)
|
||||
|
||||
## Patches Applied
|
||||
|
||||
These binaries include the following patches:
|
||||
|
||||
1. **Cargo.toml**: Add rusqlite dependency
|
||||
2. **database.rs**: `is_device_banned()` async function
|
||||
3. **peer.rs**:
|
||||
- `update_pk()` - ban check at registration
|
||||
- `find_by_addr()` - map socket address to device ID
|
||||
4. **rendezvous_server.rs**: `handle_punch_hole_request()` - dual ban check
|
||||
5. **relay_server.rs**: `handle_relay_request()` - dual ban check
|
||||
|
||||
## Installation
|
||||
|
||||
These binaries are automatically used by [install.sh](../../install.sh):
|
||||
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
1. Create backup of existing binaries
|
||||
2. Copy hbbs-v8 and hbbr-v8 to /opt/rustdesk/
|
||||
3. Set correct permissions
|
||||
4. Restart services
|
||||
|
||||
## Manual Installation
|
||||
|
||||
If you prefer manual installation:
|
||||
|
||||
```bash
|
||||
# Backup existing binaries
|
||||
sudo cp /opt/rustdesk/hbbs /opt/rustdesk/hbbs.backup
|
||||
sudo cp /opt/rustdesk/hbbr /opt/rustdesk/hbbr.backup
|
||||
|
||||
# Install new binaries
|
||||
sudo cp hbbs-v8 /opt/rustdesk/hbbs
|
||||
sudo cp hbbr-v8 /opt/rustdesk/hbbr
|
||||
sudo chmod +x /opt/rustdesk/hbbs /opt/rustdesk/hbbr
|
||||
|
||||
# Restart services
|
||||
sudo systemctl restart rustdesksignal.service
|
||||
sudo systemctl restart rustdeskrelay.service
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After installation, verify ban enforcement:
|
||||
|
||||
```bash
|
||||
# Check logs for ban enforcement messages
|
||||
sudo tail -f /var/log/rustdesk/signalserver.log
|
||||
|
||||
# Ban a device via web console: http://YOUR_SERVER_IP:5000
|
||||
|
||||
# Look for these log messages:
|
||||
# - "WARN Blocked loading banned device [ID] from database"
|
||||
# - "Punch hole REJECTED - initiator [ID] is banned"
|
||||
# - "Punch hole REJECTED - target [ID] is banned"
|
||||
# - "Relay REJECTED - initiator [ID] is banned"
|
||||
# - "Relay REJECTED - target [ID] is banned"
|
||||
```
|
||||
|
||||
## Rebuild from Source
|
||||
|
||||
If you need to rebuild these binaries:
|
||||
|
||||
```bash
|
||||
cd ../
|
||||
./build.sh
|
||||
```
|
||||
|
||||
The build script will:
|
||||
1. Clone RustDesk Server v1.1.14
|
||||
2. Apply all patches automatically
|
||||
3. Compile HBBS and HBBR
|
||||
4. Create installation package
|
||||
|
||||
Build time: ~15-20 minutes on modern hardware
|
||||
|
||||
## Security Notes
|
||||
|
||||
- These binaries are compiled from audited source code
|
||||
- All patches are documented in [BAN_ENFORCEMENT.md](../BAN_ENFORCEMENT.md)
|
||||
- Security audit available: [SECURITY_AUDIT.md](../SECURITY_AUDIT.md)
|
||||
- No network calls except RustDesk protocol
|
||||
- Database access is read-only for ban checks
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **OS**: Linux x86_64 (tested on Ubuntu 20.04+, Debian 11+)
|
||||
- **RustDesk Client**: All versions compatible with v1.1.14 server
|
||||
- **Database**: SQLite 3 (db_v2.sqlite3 with ban columns)
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check [BAN_ENFORCEMENT.md](../BAN_ENFORCEMENT.md) for troubleshooting
|
||||
- Review [SECURITY_AUDIT.md](../SECURITY_AUDIT.md) for security concerns
|
||||
- See [QUICKSTART.md](../QUICKSTART.md) for quick setup guide
|
||||
|
||||
## License
|
||||
|
||||
Same as RustDesk Server: AGPLv3
|
||||
@@ -1,328 +0,0 @@
|
||||
# RustDesk HBBS/HBBR Windows Build Script (Local)
|
||||
#
|
||||
# This script builds RustDesk HBBS and HBBR for Windows locally
|
||||
# with HTTP API and ban enforcement features.
|
||||
#
|
||||
# Requirements:
|
||||
# - Windows 10/11
|
||||
# - Rust toolchain (cargo, rustc)
|
||||
# - Git
|
||||
#
|
||||
# Usage:
|
||||
# .\build-windows-local.ps1
|
||||
#
|
||||
# Output:
|
||||
# - hbbs-ban-check-package/hbbs.exe
|
||||
# - hbbs-ban-check-package/hbbr.exe
|
||||
# - bin-with-api/hbbs-v8-api.exe
|
||||
# - bin-with-api/hbbr-v8-api.exe
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SkipClone = $false,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$Clean = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Configuration
|
||||
$RUSTDESK_VERSION = "1.1.14"
|
||||
$GITHUB_REPO = "https://github.com/rustdesk/rustdesk-server.git"
|
||||
$OUTPUT_DIR = "hbbs-ban-check-package"
|
||||
$BIN_API_DIR = "bin-with-api"
|
||||
|
||||
function Write-Header {
|
||||
param([string]$Message)
|
||||
Write-Host "`n========================================" -ForegroundColor Cyan
|
||||
Write-Host $Message -ForegroundColor Cyan
|
||||
Write-Host "========================================`n" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Step, [string]$Message)
|
||||
Write-Host "[$Step] " -NoNewline -ForegroundColor Blue
|
||||
Write-Host $Message -ForegroundColor White
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host "? $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host "? $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host "? $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Header "RustDesk Windows Build Script (Local)"
|
||||
|
||||
# Step 1: Check Rust installation
|
||||
Write-Step "1/9" "Checking Rust installation..."
|
||||
try {
|
||||
$cargoVersion = cargo --version
|
||||
$rustcVersion = rustc --version
|
||||
Write-Host " Cargo: $cargoVersion"
|
||||
Write-Host " Rustc: $rustcVersion"
|
||||
Write-Success "Rust toolchain ready"
|
||||
}
|
||||
catch {
|
||||
Write-Error "Rust not found. Please install from https://rustup.rs/"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 2: Check Git
|
||||
Write-Step "2/9" "Checking Git..."
|
||||
try {
|
||||
$gitVersion = git --version
|
||||
Write-Host " $gitVersion"
|
||||
Write-Success "Git ready"
|
||||
}
|
||||
catch {
|
||||
Write-Error "Git not found. Please install Git for Windows"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 3: Clone or use existing source
|
||||
Write-Step "3/9" "Preparing RustDesk source..."
|
||||
|
||||
$sourceDir = "rustdesk-server-$RUSTDESK_VERSION"
|
||||
|
||||
if ($Clean -and (Test-Path $sourceDir)) {
|
||||
Write-Host " Cleaning old source directory..."
|
||||
Remove-Item -Path $sourceDir -Recurse -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path $sourceDir) -and -not $SkipClone) {
|
||||
Write-Host " Downloading RustDesk Server v$RUSTDESK_VERSION..."
|
||||
|
||||
$archiveFile = "rustdesk-server-$RUSTDESK_VERSION.zip"
|
||||
if (-not (Test-Path $archiveFile)) {
|
||||
$downloadUrl = "https://github.com/rustdesk/rustdesk-server/archive/refs/tags/$RUSTDESK_VERSION.zip"
|
||||
Write-Host " Downloading from GitHub..."
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $archiveFile -UseBasicParsing
|
||||
Write-Success "Download complete"
|
||||
}
|
||||
|
||||
Write-Host " Extracting archive..."
|
||||
Expand-Archive -Path $archiveFile -DestinationPath . -Force
|
||||
Write-Success "Source extracted"
|
||||
}
|
||||
elseif (Test-Path $sourceDir) {
|
||||
Write-Success "Source directory exists"
|
||||
}
|
||||
else {
|
||||
Write-Error "Source directory not found and -SkipClone specified"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 4: Initialize submodules
|
||||
Write-Step "4/9" "Initializing git submodules..."
|
||||
Push-Location $sourceDir
|
||||
|
||||
if (Test-Path ".git") {
|
||||
Write-Host " Updating submodules..."
|
||||
git submodule update --init --recursive 2>$null
|
||||
}
|
||||
|
||||
# Check if hbb_common is present
|
||||
if (-not (Test-Path "libs\hbb_common\Cargo.toml")) {
|
||||
Write-Warning "hbb_common not found, cloning directly..."
|
||||
|
||||
if (Test-Path "libs\hbb_common") {
|
||||
Remove-Item -Path "libs\hbb_common" -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host " Cloning hbb_common..."
|
||||
git clone --depth 1 https://github.com/rustdesk/hbb_common.git libs\hbb_common
|
||||
|
||||
if (Test-Path "libs\hbb_common\Cargo.toml") {
|
||||
Write-Success "hbb_common ready"
|
||||
}
|
||||
else {
|
||||
Write-Error "Failed to get hbb_common"
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Success "hbb_common present"
|
||||
}
|
||||
|
||||
# Step 5: Copy custom source files
|
||||
Write-Step "5/9" "Applying custom modifications..."
|
||||
|
||||
$srcDir = "..\src"
|
||||
|
||||
if (Test-Path "$srcDir\http_api.rs") {
|
||||
Write-Host " Copying http_api.rs..."
|
||||
Copy-Item "$srcDir\http_api.rs" "src\http_api.rs" -Force
|
||||
}
|
||||
else {
|
||||
Write-Error "http_api.rs not found in $srcDir"
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (Test-Path "$srcDir\main.rs") {
|
||||
Write-Host " Copying main.rs..."
|
||||
Copy-Item "$srcDir\main.rs" "src\main.rs" -Force
|
||||
}
|
||||
|
||||
if (Test-Path "$srcDir\peer.rs") {
|
||||
Write-Host " Copying peer.rs..."
|
||||
Copy-Item "$srcDir\peer.rs" "src\peer.rs" -Force
|
||||
}
|
||||
|
||||
if (Test-Path "$srcDir\rendezvous_server.rs") {
|
||||
Write-Host " Copying rendezvous_server.rs..."
|
||||
Copy-Item "$srcDir\rendezvous_server.rs" "src\rendezvous_server.rs" -Force
|
||||
}
|
||||
|
||||
Write-Success "Custom files applied"
|
||||
|
||||
# Step 6: Patch lib.rs
|
||||
Write-Step "6/9" "Patching lib.rs..."
|
||||
|
||||
$librsContent = Get-Content "src\lib.rs" -Raw
|
||||
|
||||
if ($librsContent -notmatch "pub mod http_api;") {
|
||||
Write-Host " Adding http_api module to lib.rs..."
|
||||
|
||||
# Find the last 'pub mod' line and add after it
|
||||
$lines = Get-Content "src\lib.rs"
|
||||
$newLines = @()
|
||||
$added = $false
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$newLines += $lines[$i]
|
||||
|
||||
if (-not $added -and $lines[$i] -match "^pub mod " -and ($i + 1 -lt $lines.Count) -and $lines[$i + 1] -notmatch "^pub mod ") {
|
||||
$newLines += "pub mod http_api;"
|
||||
$added = $true
|
||||
}
|
||||
}
|
||||
|
||||
$newLines | Set-Content "src\lib.rs"
|
||||
Write-Success "lib.rs patched"
|
||||
}
|
||||
else {
|
||||
Write-Success "lib.rs already patched"
|
||||
}
|
||||
|
||||
# Step 7: Update Cargo.toml
|
||||
Write-Step "7/9" "Updating Cargo.toml dependencies..."
|
||||
|
||||
$cargoContent = Get-Content "Cargo.toml" -Raw
|
||||
|
||||
$modified = $false
|
||||
|
||||
if ($cargoContent -notmatch 'axum = ') {
|
||||
Write-Host " Adding axum dependency..."
|
||||
$cargoContent = $cargoContent -replace '(\[dependencies\])', "`$1`naxum = `"0.5`""
|
||||
$modified = $true
|
||||
}
|
||||
|
||||
if ($cargoContent -notmatch 'sqlx = ') {
|
||||
Write-Host " Adding sqlx dependency..."
|
||||
$cargoContent = $cargoContent -replace '(\[dependencies\])', "`$1`nsqlx = { version = `"0.6`", features = [`"sqlite`", `"runtime-tokio-native-tls`"] }"
|
||||
$modified = $true
|
||||
}
|
||||
|
||||
if ($modified) {
|
||||
$cargoContent | Set-Content "Cargo.toml"
|
||||
Write-Success "Dependencies updated"
|
||||
}
|
||||
else {
|
||||
Write-Success "Dependencies already present"
|
||||
}
|
||||
|
||||
# Step 8: Build
|
||||
Write-Step "8/9" "Building for Windows (this may take several minutes)..."
|
||||
Write-Host ""
|
||||
|
||||
$env:RUSTFLAGS = "-C target-feature=+crt-static"
|
||||
|
||||
cargo build --release 2>&1 | ForEach-Object {
|
||||
if ($_ -match "Compiling|Finished|error|warning") {
|
||||
Write-Host $_
|
||||
}
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ""
|
||||
Write-Error "Build failed with exit code $LASTEXITCODE"
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Success "Build complete!"
|
||||
|
||||
# Step 9: Package binaries
|
||||
Write-Step "9/9" "Packaging binaries..."
|
||||
|
||||
Pop-Location # Back to hbbs-patch directory
|
||||
|
||||
# Create output directories
|
||||
if (-not (Test-Path $OUTPUT_DIR)) {
|
||||
New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null
|
||||
}
|
||||
|
||||
if (-not (Test-Path $BIN_API_DIR)) {
|
||||
New-Item -ItemType Directory -Path $BIN_API_DIR | Out-Null
|
||||
}
|
||||
|
||||
# Copy executables
|
||||
$hbbsPath = "$sourceDir\target\release\hbbs.exe"
|
||||
$hbbrPath = "$sourceDir\target\release\hbbr.exe"
|
||||
|
||||
if (Test-Path $hbbsPath) {
|
||||
Copy-Item $hbbsPath "$OUTPUT_DIR\hbbs.exe" -Force
|
||||
Copy-Item $hbbsPath "$BIN_API_DIR\hbbs-v8-api.exe" -Force
|
||||
|
||||
$hbbsSize = (Get-Item $hbbsPath).Length
|
||||
Write-Host " ? hbbs.exe ($([math]::Round($hbbsSize / 1MB, 2)) MB)"
|
||||
}
|
||||
else {
|
||||
Write-Error "hbbs.exe not found"
|
||||
}
|
||||
|
||||
if (Test-Path $hbbrPath) {
|
||||
Copy-Item $hbbrPath "$OUTPUT_DIR\hbbr.exe" -Force
|
||||
Copy-Item $hbbrPath "$BIN_API_DIR\hbbr-v8-api.exe" -Force
|
||||
|
||||
$hbbrSize = (Get-Item $hbbrPath).Length
|
||||
Write-Host " ? hbbr.exe ($([math]::Round($hbbrSize / 1MB, 2)) MB)"
|
||||
}
|
||||
else {
|
||||
Write-Error "hbbr.exe not found"
|
||||
}
|
||||
|
||||
Write-Success "Binaries packaged"
|
||||
|
||||
# Summary
|
||||
Write-Header "Build Complete!"
|
||||
|
||||
Write-Host "Windows binaries created:" -ForegroundColor Green
|
||||
Write-Host " ? $OUTPUT_DIR\hbbs.exe"
|
||||
Write-Host " ? $OUTPUT_DIR\hbbr.exe"
|
||||
Write-Host ""
|
||||
Write-Host "Also copied to installer directory:" -ForegroundColor Green
|
||||
Write-Host " ? $BIN_API_DIR\hbbs-v8-api.exe"
|
||||
Write-Host " ? $BIN_API_DIR\hbbr-v8-api.exe"
|
||||
Write-Host ""
|
||||
Write-Host "Features included:" -ForegroundColor Cyan
|
||||
Write-Host " ? HTTP API on port 21114"
|
||||
Write-Host " ? Real-time device status"
|
||||
Write-Host " ? Bidirectional ban enforcement"
|
||||
Write-Host " ? 20-second timeout synchronization"
|
||||
Write-Host ""
|
||||
Write-Host "Ready to use with install-improved.ps1!" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
@@ -1,287 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# RustDesk HBBS/HBBR Windows Cross-Compilation Build Script
|
||||
#
|
||||
# This script cross-compiles RustDesk HBBS and HBBR for Windows (x86_64)
|
||||
# with HTTP API and ban enforcement features.
|
||||
#
|
||||
# Requirements:
|
||||
# - Linux environment
|
||||
# - Rust toolchain with Windows target
|
||||
# - MinGW-w64 cross-compiler
|
||||
#
|
||||
# Usage:
|
||||
# bash build-windows.sh
|
||||
#
|
||||
# Output:
|
||||
# - hbbs-ban-check-package/hbbs.exe (Windows)
|
||||
# - hbbs-ban-check-package/hbbr.exe (Windows)
|
||||
#############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
RUSTDESK_VERSION="1.1.14"
|
||||
GITHUB_REPO="https://github.com/rustdesk/rustdesk-server.git"
|
||||
WINDOWS_TARGET="x86_64-pc-windows-gnu"
|
||||
OUTPUT_DIR="hbbs-ban-check-package"
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}RustDesk Windows Build Script${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Add cargo to PATH if not already there
|
||||
if [ -d "$HOME/.cargo/bin" ] && [[ ":$PATH:" != *":$HOME/.cargo/bin:"* ]]; then
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
echo "Added ~/.cargo/bin to PATH"
|
||||
fi
|
||||
|
||||
# Check if we're on Linux
|
||||
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
|
||||
echo -e "${RED}✗ This script must be run on Linux${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Check for Rust and add Windows target
|
||||
echo -e "${BLUE}[1/8] Checking Rust installation and Windows target...${NC}"
|
||||
|
||||
# Check if cargo is available
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo -e "${RED}✗ Cargo not found. Please install Rust.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Rust version: $(cargo --version)"
|
||||
|
||||
# Try to add Windows target (works with both rustup and standalone)
|
||||
if command -v rustup &> /dev/null; then
|
||||
echo "Using rustup to add Windows target..."
|
||||
rustup target add $WINDOWS_TARGET 2>/dev/null || true
|
||||
echo -e "${GREEN}✓ Windows target configured via rustup${NC}"
|
||||
else
|
||||
echo "Rustup not found - attempting standalone Rust cross-compilation..."
|
||||
echo -e "${YELLOW}⚠ Note: Without rustup, cross-compilation may require additional setup${NC}"
|
||||
fi
|
||||
|
||||
# Step 2: Check for MinGW cross-compiler
|
||||
echo -e "${BLUE}[2/8] Checking for MinGW-w64...${NC}"
|
||||
if ! command -v x86_64-w64-mingw32-gcc &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠ MinGW-w64 not found, attempting to install...${NC}"
|
||||
|
||||
if command -v apt-get &> /dev/null; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mingw-w64
|
||||
elif command -v yum &> /dev/null; then
|
||||
sudo yum install -y mingw64-gcc mingw64-gcc-c++
|
||||
else
|
||||
echo -e "${RED}✗ Please install MinGW-w64 manually${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v x86_64-w64-mingw32-gcc &> /dev/null; then
|
||||
echo -e "${GREEN}✓ MinGW-w64 found: $(x86_64-w64-mingw32-gcc --version | head -n1)${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ MinGW-w64 installation failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Clone or use existing rustdesk-server
|
||||
echo -e "${BLUE}[3/8] Preparing RustDesk source...${NC}"
|
||||
if [ ! -d "rustdesk-server-$RUSTDESK_VERSION" ]; then
|
||||
if [ ! -f "rustdesk-server-$RUSTDESK_VERSION.tar.gz" ]; then
|
||||
echo "Downloading RustDesk Server v$RUSTDESK_VERSION..."
|
||||
wget "https://github.com/rustdesk/rustdesk-server/archive/refs/tags/$RUSTDESK_VERSION.tar.gz" \
|
||||
-O "rustdesk-server-$RUSTDESK_VERSION.tar.gz"
|
||||
fi
|
||||
|
||||
echo "Extracting archive..."
|
||||
tar -xzf "rustdesk-server-$RUSTDESK_VERSION.tar.gz"
|
||||
fi
|
||||
|
||||
cd "rustdesk-server-$RUSTDESK_VERSION"
|
||||
|
||||
# Initialize git submodules (required for hbb_common)
|
||||
echo "Initializing git submodules..."
|
||||
if [ -d .git ]; then
|
||||
git submodule update --init --recursive 2>/dev/null || echo "Git submodules update failed (expected if not git clone)"
|
||||
fi
|
||||
|
||||
# Check if libs/hbb_common is empty and clone if needed
|
||||
if [ ! -f "libs/hbb_common/Cargo.toml" ]; then
|
||||
echo "hbb_common not found, cloning directly..."
|
||||
rm -rf libs/hbb_common
|
||||
mkdir -p libs/hbb_common
|
||||
|
||||
# Try cloning the actual dependency (rustdesk-hbb_common)
|
||||
if ! git clone --depth 1 https://github.com/rustdesk-org/rustdesk-hbb_common.git libs/hbb_common 2>/dev/null; then
|
||||
echo -e "${YELLOW}⚠ Using tarball instead of git clone${NC}"
|
||||
|
||||
# Download as tarball instead
|
||||
wget -q https://github.com/rustdesk-org/rustdesk-hbb_common/archive/refs/heads/master.tar.gz -O /tmp/hbb_common.tar.gz
|
||||
tar -xzf /tmp/hbb_common.tar.gz -C libs/
|
||||
mv libs/rustdesk-hbb_common-master libs/hbb_common
|
||||
rm /tmp/hbb_common.tar.gz
|
||||
fi
|
||||
|
||||
if [ -f "libs/hbb_common/Cargo.toml" ]; then
|
||||
echo -e "${GREEN}✓ hbb_common ready${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Failed to get hbb_common${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✓ hbb_common already present${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Source ready${NC}"
|
||||
|
||||
# Step 4: Copy custom source files
|
||||
echo -e "${BLUE}[4/8] Applying custom modifications...${NC}"
|
||||
|
||||
# Copy HTTP API module
|
||||
if [ -f "../src/http_api.rs" ]; then
|
||||
echo "Copying http_api.rs..."
|
||||
cp "../src/http_api.rs" "src/http_api.rs"
|
||||
else
|
||||
echo -e "${RED}✗ http_api.rs not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy modified main.rs
|
||||
if [ -f "../src/main.rs" ]; then
|
||||
echo "Copying main.rs..."
|
||||
cp "../src/main.rs" "src/main.rs"
|
||||
else
|
||||
echo -e "${RED}✗ main.rs not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy modified peer.rs
|
||||
if [ -f "../src/peer.rs" ]; then
|
||||
echo "Copying peer.rs..."
|
||||
cp "../src/peer.rs" "src/peer.rs"
|
||||
fi
|
||||
|
||||
# Copy modified rendezvous_server.rs
|
||||
if [ -f "../src/rendezvous_server.rs" ]; then
|
||||
echo "Copying rendezvous_server.rs..."
|
||||
cp "../src/rendezvous_server.rs" "src/rendezvous_server.rs"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Custom files copied${NC}"
|
||||
|
||||
# Step 5: Patch lib.rs to include HTTP API module
|
||||
echo -e "${BLUE}[5/8] Patching lib.rs...${NC}"
|
||||
if ! grep -q "pub mod http_api;" src/lib.rs; then
|
||||
# Add after the last 'pub mod' declaration
|
||||
sed -i '/^pub mod /a pub mod http_api;' src/lib.rs
|
||||
echo -e "${GREEN}✓ lib.rs patched${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ lib.rs already patched${NC}"
|
||||
fi
|
||||
|
||||
# Step 6: Update Cargo.toml dependencies
|
||||
echo -e "${BLUE}[6/8] Updating Cargo.toml...${NC}"
|
||||
|
||||
# Check if dependencies already exist
|
||||
if ! grep -q "axum = " Cargo.toml; then
|
||||
echo "Adding axum dependency..."
|
||||
sed -i '/\[dependencies\]/a axum = "0.5"' Cargo.toml
|
||||
fi
|
||||
|
||||
if ! grep -q "sqlx = " Cargo.toml; then
|
||||
echo "Adding sqlx dependency..."
|
||||
sed -i '/\[dependencies\]/a sqlx = { version = "0.6", features = ["sqlite", "runtime-tokio-native-tls"] }' Cargo.toml
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Dependencies updated${NC}"
|
||||
|
||||
# Step 7: Configure Cargo for Windows cross-compilation
|
||||
echo -e "${BLUE}[7/8] Configuring Cargo for Windows...${NC}"
|
||||
mkdir -p .cargo
|
||||
|
||||
cat > .cargo/config.toml << 'EOF'
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
ar = "x86_64-w64-mingw32-ar"
|
||||
|
||||
[build]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✓ Cargo configured${NC}"
|
||||
|
||||
# Step 8: Build for Windows
|
||||
echo -e "${BLUE}[8/8] Building for Windows (this may take several minutes)...${NC}"
|
||||
echo ""
|
||||
|
||||
# Set environment variables for cross-compilation
|
||||
export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc
|
||||
export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++
|
||||
export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc
|
||||
|
||||
# Build with release profile
|
||||
cargo build --release --target $WINDOWS_TARGET
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Build successful!${NC}"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${RED}✗ Build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 9: Package binaries
|
||||
echo -e "${BLUE}Packaging binaries...${NC}"
|
||||
cd ..
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Copy Windows executables
|
||||
cp "rustdesk-server-$RUSTDESK_VERSION/target/$WINDOWS_TARGET/release/hbbs.exe" "$OUTPUT_DIR/"
|
||||
cp "rustdesk-server-$RUSTDESK_VERSION/target/$WINDOWS_TARGET/release/hbbr.exe" "$OUTPUT_DIR/"
|
||||
|
||||
# Copy to bin-with-api for installer
|
||||
mkdir -p bin-with-api
|
||||
cp "$OUTPUT_DIR/hbbs.exe" "bin-with-api/hbbs-v8-api.exe"
|
||||
cp "$OUTPUT_DIR/hbbr.exe" "bin-with-api/hbbr-v8-api.exe"
|
||||
|
||||
# Calculate sizes
|
||||
hbbs_size=$(stat -f%z "$OUTPUT_DIR/hbbs.exe" 2>/dev/null || stat -c%s "$OUTPUT_DIR/hbbs.exe" 2>/dev/null)
|
||||
hbbr_size=$(stat -f%z "$OUTPUT_DIR/hbbr.exe" 2>/dev/null || stat -c%s "$OUTPUT_DIR/hbbr.exe" 2>/dev/null)
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}Build Complete!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo "Windows binaries created:"
|
||||
echo " • $OUTPUT_DIR/hbbs.exe ($(numfmt --to=iec $hbbs_size 2>/dev/null || echo "$hbbs_size bytes"))"
|
||||
echo " • $OUTPUT_DIR/hbbr.exe ($(numfmt --to=iec $hbbr_size 2>/dev/null || echo "$hbbr_size bytes"))"
|
||||
echo ""
|
||||
echo "Also copied to bin-with-api/ for installer:"
|
||||
echo " • bin-with-api/hbbs-v8-api.exe"
|
||||
echo " • bin-with-api/hbbr-v8-api.exe"
|
||||
echo ""
|
||||
echo -e "${BLUE}Features included:${NC}"
|
||||
echo " ✓ HTTP API on port 21114"
|
||||
echo " ✓ Real-time device status"
|
||||
echo " ✓ Bidirectional ban enforcement"
|
||||
echo " ✓ 20-second timeout synchronization"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: Windows binaries compiled on Linux using MinGW${NC}"
|
||||
echo -e "${YELLOW}Test them on a Windows system to verify functionality${NC}"
|
||||
echo ""
|
||||
@@ -1,501 +0,0 @@
|
||||
#!/bin/bash
|
||||
# HBBS Ban Check - Automatic Patch and Build Script
|
||||
#
|
||||
# This script:
|
||||
# 1. Clones RustDesk Server v1.1.14
|
||||
# 2. Applies ban check patches
|
||||
# 3. Compiles modified hbbs
|
||||
# 4. Creates installation package
|
||||
|
||||
set -e
|
||||
|
||||
# Source cargo environment if exists
|
||||
[ -f "$HOME/.cargo/env" ] && source "$HOME/.cargo/env"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}HBBS Ban Check - Build Script${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Check requirements
|
||||
echo -e "${YELLOW}[1/8] Checking requirements...${NC}"
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo -e "${RED}✗ Git not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Git${NC}"
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo -e "${RED}✗ Rust/Cargo not installed${NC}"
|
||||
echo "Install from: https://rustup.rs/"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Rust $(cargo --version)${NC}"
|
||||
|
||||
# Clone repository
|
||||
echo ""
|
||||
echo -e "${YELLOW}[2/8] Cloning RustDesk Server...${NC}"
|
||||
|
||||
if [ -d "rustdesk-server" ]; then
|
||||
echo "Removing existing directory..."
|
||||
rm -rf rustdesk-server
|
||||
fi
|
||||
|
||||
git clone --depth 1 --branch 1.1.14 --recurse-submodules https://github.com/rustdesk/rustdesk-server.git
|
||||
cd rustdesk-server
|
||||
|
||||
echo -e "${GREEN}✓ Cloned v1.1.14${NC}"
|
||||
|
||||
# Copy HTTP API and main.rs files
|
||||
echo ""
|
||||
echo -e "${YELLOW}[2.5/8] Copying HTTP API and main.rs files...${NC}"
|
||||
|
||||
# Copy the HTTP API module
|
||||
cp ../src/http_api.rs src/http_api.rs
|
||||
# Copy the updated main.rs with API support
|
||||
cp ../src/main.rs src/main.rs
|
||||
|
||||
# Add http_api module to lib.rs
|
||||
if ! grep -q "pub mod http_api;" src/lib.rs; then
|
||||
echo "pub mod http_api;" >> src/lib.rs
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ HTTP API and main.rs copied${NC}"
|
||||
|
||||
# Add rusqlite dependency
|
||||
echo ""
|
||||
echo -e "${YELLOW}[3/8] Adding rusqlite dependency...${NC}"
|
||||
|
||||
# Add rusqlite to Cargo.toml dependencies section
|
||||
# Using version 0.27 to match libsqlite3-sys 0.24 (same as used by SQLx)
|
||||
sed -i '/^\[dependencies\]/a rusqlite = { version = "0.27", features = ["bundled"] }' Cargo.toml
|
||||
|
||||
echo -e "${GREEN}✓ rusqlite added to Cargo.toml${NC}"
|
||||
|
||||
# Add HTTP API dependencies
|
||||
echo ""
|
||||
echo -e "${YELLOW}[3.5/8] Adding HTTP API dependencies...${NC}"
|
||||
|
||||
# Note: serde and tokio are already in dependencies, we just need axum and sqlx
|
||||
# Check if they already exist to avoid duplicates
|
||||
if ! grep -q "^axum =" Cargo.toml; then
|
||||
sed -i '/^\[dependencies\]/a axum = { version = "0.5", features = ["headers"] }' Cargo.toml
|
||||
fi
|
||||
if ! grep -q "^sqlx =" Cargo.toml; then
|
||||
sed -i '/^\[dependencies\]/a sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite"] }' Cargo.toml
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ HTTP API dependencies added${NC}"
|
||||
|
||||
# Apply database.rs patch
|
||||
echo ""
|
||||
echo -e "${YELLOW}[4/8] Applying database.rs patch...${NC}"
|
||||
|
||||
# Insert is_device_banned method using synchronous rusqlite to avoid nested runtime panic
|
||||
# Note: hbb_common re-exports both anyhow and tokio, so we use full paths
|
||||
sed -i '/pub async fn update_pk/,/^ }$/{
|
||||
/^ }$/ a\
|
||||
\
|
||||
/// 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 = "./db_v2.sqlite3";\
|
||||
let id = id.to_string();\
|
||||
\
|
||||
// Execute in blocking thread pool to avoid runtime conflict\
|
||||
let result = hbb_common::tokio::task::spawn_blocking(move || -> ResultType<bool> {\
|
||||
use rusqlite::{Connection, OptionalExtension};\
|
||||
\
|
||||
// Use READ_WRITE to support WAL mode (no actual writes performed)\
|
||||
let conn = Connection::open_with_flags(\
|
||||
db_path,\
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE\
|
||||
)?;\
|
||||
\
|
||||
let mut stmt = conn.prepare(\
|
||||
"SELECT is_banned FROM peer WHERE id = ? AND is_deleted = 0"\
|
||||
)?;\
|
||||
\
|
||||
let result: Option<i32> = stmt\
|
||||
.query_row([&id], |row| row.get(0))\
|
||||
.optional()?;\
|
||||
\
|
||||
Ok(result.map(|banned| banned == 1).unwrap_or(false))\
|
||||
})\
|
||||
.await\
|
||||
.map_err(|e| hbb_common::anyhow::anyhow!("Spawn blocking failed: {}", e))??;\
|
||||
\
|
||||
Ok(result)\
|
||||
}
|
||||
}' src/database.rs
|
||||
|
||||
echo -e "${GREEN}✓ database.rs patched${NC}"
|
||||
|
||||
# Apply peer.rs patch
|
||||
echo ""
|
||||
echo -e "${YELLOW}[5/8] Applying peer.rs patch...${NC}"
|
||||
|
||||
# Patch 1: Ban check in update_pk - reject and remove from memory
|
||||
sed -i '/log::info!("update_pk/a\
|
||||
\
|
||||
// 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);\
|
||||
// Remove from memory to prevent cached access\
|
||||
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);\
|
||||
}\
|
||||
}' src/peer.rs
|
||||
|
||||
# Patch 2: Ban check in get() - prevent loading banned devices from DB
|
||||
sed -i '/let peer = Peer {/i\
|
||||
// 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;\
|
||||
}' src/peer.rs
|
||||
|
||||
# Patch 3: Add method to find device ID by socket address
|
||||
# Add AFTER the closing brace of is_in_memory() method
|
||||
sed -i '/self\.map\.read()\.await\.contains_key(id)/,/^ }$/{
|
||||
/^ }$/a\
|
||||
\
|
||||
/// Find device ID by socket address (for ban enforcement)\
|
||||
/// Returns the ID of the peer with matching socket address, or None\
|
||||
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\
|
||||
}
|
||||
}' src/peer.rs
|
||||
|
||||
echo -e "${GREEN}✓ peer.rs patched${NC}"
|
||||
|
||||
# Apply rendezvous_server.rs patch - block banned devices from relay and P2P connections
|
||||
echo ""
|
||||
echo -e "${YELLOW}[6/8] Applying rendezvous_server.rs patches...${NC}"
|
||||
|
||||
# Patch 1: Block RequestRelay for banned devices (target only - more reliable)
|
||||
sed -i '/Some(rendezvous_message::Union::RequestRelay(mut rf)) => {/,/return true;/{
|
||||
/Some(rendezvous_message::Union::RequestRelay(mut rf)) => {/a\
|
||||
// BAN CHECK: Block relay if target is banned\
|
||||
match self.pm.db.is_device_banned(\&rf.id).await {\
|
||||
Ok(true) => {\
|
||||
log::warn!("Relay REJECTED - target {} is banned", rf.id);\
|
||||
return true;\
|
||||
}\
|
||||
Ok(false) => {},\
|
||||
Err(e) => {\
|
||||
log::error!("Ban check failed for relay target {}: {}", rf.id, e);\
|
||||
}\
|
||||
}\
|
||||
// BAN CHECK: Block relay if sender (uuid) is banned\
|
||||
if !rf.uuid.is_empty() {\
|
||||
match self.pm.db.is_device_banned(\&rf.uuid).await {\
|
||||
Ok(true) => {\
|
||||
log::warn!("Relay REJECTED - sender {} (uuid) is banned", rf.uuid);\
|
||||
return true;\
|
||||
}\
|
||||
Ok(false) => {},\
|
||||
Err(e) => {\
|
||||
log::error!("Ban check failed for sender {}: {}", rf.uuid, e);\
|
||||
}\
|
||||
}\
|
||||
}
|
||||
}' src/rendezvous_server.rs
|
||||
|
||||
# Patch 2: Block PunchHoleRequest for banned devices (target only - sender will be caught by message relay)
|
||||
sed -i '/async fn handle_punch_hole_request(/,/let id = ph.id;/{
|
||||
/let id = ph.id;/a\
|
||||
\
|
||||
// BAN CHECK 1: Block if TARGET device is banned\
|
||||
match self.pm.db.is_device_banned(\&id).await {\
|
||||
Ok(true) => {\
|
||||
log::warn!("Punch hole REJECTED - target {} is BANNED", id);\
|
||||
let mut msg_out = RendezvousMessage::new();\
|
||||
msg_out.set_punch_hole_response(PunchHoleResponse {\
|
||||
failure: punch_hole_response::Failure::OFFLINE.into(),\
|
||||
..Default::default()\
|
||||
});\
|
||||
return Ok((msg_out, None));\
|
||||
}\
|
||||
Ok(false) => {\
|
||||
log::debug!("Target ban check passed for {}", id);\
|
||||
}\
|
||||
Err(e) => {\
|
||||
log::error!("Failed to check target ban status for {}: {}", id, e);\
|
||||
}\
|
||||
}\
|
||||
\
|
||||
// BAN CHECK 2: Block if SOURCE device (initiating connection) is banned\
|
||||
if let Some(source_id) = self.pm.get_id_by_addr(addr).await {\
|
||||
match self.pm.db.is_device_banned(\&source_id).await {\
|
||||
Ok(true) => {\
|
||||
log::warn!("Punch hole REJECTED - source {} (from {}) is BANNED", source_id, addr);\
|
||||
let mut msg_out = RendezvousMessage::new();\
|
||||
msg_out.set_punch_hole_response(PunchHoleResponse {\
|
||||
failure: punch_hole_response::Failure::LICENSE_MISMATCH.into(),\
|
||||
..Default::default()\
|
||||
});\
|
||||
return Ok((msg_out, None));\
|
||||
}\
|
||||
Ok(false) => {\
|
||||
log::debug!("Source ban check passed for {}", source_id);\
|
||||
}\
|
||||
Err(e) => {\
|
||||
log::error!("Failed to check source ban status for {}: {}", source_id, e);\
|
||||
}\
|
||||
}\
|
||||
} else {\
|
||||
log::debug!("Could not find source device ID for address {}", addr);\
|
||||
}
|
||||
}' src/rendezvous_server.rs
|
||||
|
||||
echo -e "${GREEN}✓ rendezvous_server.rs patched${NC}"
|
||||
|
||||
# Patch to add API server support
|
||||
echo ""
|
||||
echo -e "${YELLOW}[6.4/8] Adding HTTP API server support...${NC}"
|
||||
|
||||
# 1. Update start() function signature to accept api_port
|
||||
sed -i 's/pub async fn start(port: i32, serial: i32, key: \&str, rmem: usize)/pub async fn start(port: i32, serial: i32, key: \&str, rmem: usize, api_port: u16)/' src/rendezvous_server.rs
|
||||
|
||||
# 2. Start API server in background task (add after PeerMap::new().await?)
|
||||
sed -i '/let pm = PeerMap::new().await?;/a\
|
||||
\
|
||||
// Start HTTP API server in background\
|
||||
let pm_clone = pm.clone();\
|
||||
let db_path = std::env::var("DB_URL").unwrap_or("./db_v2.sqlite3".to_string());\
|
||||
tokio::spawn(async move {\
|
||||
if let Err(e) = crate::http_api::start_api_server(db_path, api_port, Arc::new(pm_clone)).await {\
|
||||
log::error!("HTTP API server failed: {}", e);\
|
||||
}\
|
||||
});' src/rendezvous_server.rs
|
||||
|
||||
echo -e "${GREEN}✓ HTTP API server support added${NC}"
|
||||
|
||||
# NEW CRITICAL PATCH 3: Block RelayResponse - THE actual message relay
|
||||
echo ""
|
||||
echo -e "${YELLOW}[6.5/8] Adding RelayResponse ban check (CRITICAL)...${NC}"
|
||||
|
||||
# This blocks the actual data being relayed between devices
|
||||
sed -i '/Some(rendezvous_message::Union::RelayResponse(mut rr)) => {/a\
|
||||
// CRITICAL BAN CHECK: Block relay response if sender or target is banned\
|
||||
// This is where actual remote control data flows\
|
||||
let relay_id = rr.id();\
|
||||
if !relay_id.is_empty() {\
|
||||
// Check if relay ID (could be sender or target) is banned\
|
||||
match self.pm.db.is_device_banned(relay_id).await {\
|
||||
Ok(true) => {\
|
||||
log::warn!("RelayResponse BLOCKED - device {} is banned", relay_id);\
|
||||
return true;\
|
||||
}\
|
||||
Ok(false) => {},\
|
||||
Err(e) => {\
|
||||
log::error!("Ban check failed for relay {}: {}", relay_id, e);\
|
||||
}\
|
||||
}\
|
||||
}' src/rendezvous_server.rs
|
||||
|
||||
echo -e "${GREEN}✓ RelayResponse ban check added${NC}"
|
||||
|
||||
# NEW CRITICAL PATCH 4: Block HBBR relay_server.rs - actual data relay
|
||||
echo ""
|
||||
echo -e "${YELLOW}[6.6/8] Adding HBBR relay server ban check (MOST CRITICAL)...${NC}"
|
||||
|
||||
# Add ban check to make_pair_ before pairing
|
||||
# Strategy: Check if ANY device from the initiating IP is banned
|
||||
sed -i '/if let Some(rendezvous_message::Union::RequestRelay(rf)) = msg_in.union {/,/if !rf.uuid.is_empty() {/{
|
||||
/if !rf.uuid.is_empty() {/a\
|
||||
// CRITICAL BAN CHECK: Block relay if device from this IP is banned\
|
||||
// Strategy: Query database for all devices with recent activity from this IP\
|
||||
// and check if any of them is banned\
|
||||
let db_path = "./db_v2.sqlite3";\
|
||||
let client_ip = addr.ip().to_string();\
|
||||
let target_id = rf.id.clone();\
|
||||
\
|
||||
let is_banned = hbb_common::tokio::task::spawn_blocking(move || {\
|
||||
use rusqlite::{Connection, OptionalExtension};\
|
||||
\
|
||||
match Connection::open_with_flags(\
|
||||
db_path,\
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE\
|
||||
) {\
|
||||
Ok(conn) => {\
|
||||
// Check if target device is banned\
|
||||
if !target_id.is_empty() {\
|
||||
if let Ok(Some(Some(banned))) = conn\
|
||||
.prepare("SELECT is_banned FROM peer WHERE id = ? AND is_deleted = 0")\
|
||||
.and_then(|mut stmt| stmt.query_row([&target_id], |row| row.get::<_, Option<i32>>(0)).optional()) {\
|
||||
if banned == 1 {\
|
||||
log::warn!("HBBR Relay BLOCKED - target device {} is BANNED", target_id);\
|
||||
return true;\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
\
|
||||
// Check if ANY device from this IP is banned\
|
||||
// This catches the initiating device even if we don'\''t have its exact ID\
|
||||
let info_pattern = format!("%{}%", client_ip);\
|
||||
match conn.prepare(\
|
||||
"SELECT id, is_banned FROM peer WHERE info LIKE ? AND is_deleted = 0 LIMIT 10"\
|
||||
) {\
|
||||
Ok(mut stmt) => {\
|
||||
if let Ok(mut rows) = stmt.query([&info_pattern]) {\
|
||||
while let Ok(Some(row)) = rows.next() {\
|
||||
if let (Ok(id), Ok(Some(banned))) = (\
|
||||
row.get::<_, String>(0),\
|
||||
row.get::<_, Option<i32>>(1)\
|
||||
) {\
|
||||
if banned == 1 {\
|
||||
log::warn!("HBBR Relay BLOCKED - device {} from IP {} is BANNED", id, client_ip);\
|
||||
return true;\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
Err(_) => {}\
|
||||
}\
|
||||
false\
|
||||
}\
|
||||
Err(_) => false\
|
||||
}\
|
||||
}).await;\
|
||||
\
|
||||
match is_banned {\
|
||||
Ok(true) => {\
|
||||
log::warn!("HBBR Relay REJECTED from {}", addr);\
|
||||
return;\
|
||||
}\
|
||||
Ok(false) => {\
|
||||
log::debug!("HBBR Relay allowed from {}", addr);\
|
||||
}\
|
||||
Err(e) => {\
|
||||
log::error!("HBBR ban check spawn failed: {}", e);\
|
||||
}\
|
||||
}
|
||||
}' src/relay_server.rs
|
||||
|
||||
echo -e "${GREEN}✓ HBBR relay ban check added${NC}"
|
||||
|
||||
# Compile
|
||||
echo ""
|
||||
echo -e "${YELLOW}[7/8] Compiling hbbs...${NC}"
|
||||
echo "This may take 5-10 minutes on first build..."
|
||||
|
||||
cargo build --release --bin hbbs
|
||||
|
||||
if [ ! -f "target/release/hbbs" ]; then
|
||||
echo -e "${RED}✗ HBBS build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ HBBS compiled${NC}"
|
||||
|
||||
# Compile HBBR (relay server) - CRITICAL for ban enforcement
|
||||
echo ""
|
||||
echo -e "${YELLOW}[8/8] Compiling hbbr (relay server)...${NC}"
|
||||
echo "HBBR handles actual data relay - must have ban checks!"
|
||||
|
||||
cargo build --release --bin hbbr
|
||||
|
||||
if [ ! -f "target/release/hbbr" ]; then
|
||||
echo -e "${RED}✗ HBBR build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ HBBR compiled${NC}"
|
||||
echo -e "${GREEN}✓ All builds successful${NC}"
|
||||
|
||||
# Create package
|
||||
echo ""
|
||||
echo -e "${YELLOW}[9/8] Creating installation package...${NC}"
|
||||
|
||||
mkdir -p ../hbbs-ban-check-package
|
||||
cp target/release/hbbs ../hbbs-ban-check-package/
|
||||
cp target/release/hbbr ../hbbs-ban-check-package/
|
||||
cp /opt/rustdesk/id_ed25519.pub ../hbbs-ban-check-package/ 2>/dev/null || echo "Note: No existing public key"
|
||||
|
||||
# Create install script
|
||||
cat > ../hbbs-ban-check-package/install.sh << 'INSTALL_EOF'
|
||||
#!/bin/bash
|
||||
# HBBS Ban Check - Installation Script
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root (sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing HBBS with ban check..."
|
||||
|
||||
# Backup
|
||||
systemctl stop hbbs
|
||||
cp /opt/rustdesk/hbbs /opt/rustdesk/hbbs.backup.$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# Install
|
||||
cp hbbs /opt/rustdesk/
|
||||
chmod +x /opt/rustdesk/hbbs
|
||||
|
||||
# Restart
|
||||
systemctl start hbbs
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet hbbs; then
|
||||
echo "✓ HBBS with ban check installed successfully"
|
||||
journalctl -u hbbs -n 10 --no-pager
|
||||
else
|
||||
echo "✗ HBBS failed to start. Restoring backup..."
|
||||
systemctl stop hbbs
|
||||
cp /opt/rustdesk/hbbs.backup.* /opt/rustdesk/hbbs
|
||||
systemctl start hbbs
|
||||
exit 1
|
||||
fi
|
||||
INSTALL_EOF
|
||||
|
||||
chmod +x ../hbbs-ban-check-package/install.sh
|
||||
|
||||
echo -e "${GREEN}✓ Package created${NC}"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${GREEN}Build Complete!${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo "Package location:"
|
||||
echo " $(pwd)/../hbbs-ban-check-package/"
|
||||
echo ""
|
||||
echo "Installation:"
|
||||
echo " 1. Copy package to server:"
|
||||
echo " scp -r hbbs-ban-check-package/ user@server:/tmp/"
|
||||
echo ""
|
||||
echo " 2. On server, run:"
|
||||
echo " cd /tmp/hbbs-ban-check-package"
|
||||
echo " sudo ./install.sh"
|
||||
echo ""
|
||||
echo "Binary size: $(ls -lh target/release/hbbs | awk '{print $5}')"
|
||||
echo ""
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
# HTTP API dependencies
|
||||
axum = { version = "0.6", features = ["tokio"] }
|
||||
tower = "0.4"
|
||||
serde_json = "1.0"
|
||||
@@ -1,45 +0,0 @@
|
||||
// Patch dla database.rs - dodaje metodę is_device_banned()
|
||||
//
|
||||
// Wstaw ten kod na końcu implementacji `impl Database`
|
||||
// (przed zamykającym nawiasem klamrowym struktury)
|
||||
|
||||
/// Check if a device is banned in the database
|
||||
/// Returns true if device has is_banned=1, false otherwise
|
||||
/// Uses separate synchronous SQLite connection to avoid nested runtime panic
|
||||
pub async fn is_device_banned(&self, id: &str) -> ResultType<bool> {
|
||||
use std::sync::Arc;
|
||||
|
||||
// Database path - assuming same as used in HBBS (./db_v2.sqlite3)
|
||||
let db_path = "./db_v2.sqlite3";
|
||||
let id = id.to_string();
|
||||
|
||||
// Execute synchronous query in blocking thread pool
|
||||
// This avoids "Cannot start a runtime from within a runtime" error
|
||||
let result = tokio::task::spawn_blocking(move || -> ResultType<bool> {
|
||||
use rusqlite::Connection;
|
||||
|
||||
// Open synchronous connection (read-only)
|
||||
let conn = Connection::open_with_flags(
|
||||
db_path,
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
|
||||
)?;
|
||||
|
||||
// Query for ban status
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT is_banned FROM peer WHERE id = ? AND is_deleted = 0"
|
||||
)?;
|
||||
|
||||
let result: Option<i32> = stmt
|
||||
.query_row([&id], |row| row.get(0))
|
||||
.optional()?;
|
||||
|
||||
// Return true if banned (is_banned = 1), false otherwise
|
||||
Ok(result.map(|banned| banned == 1).unwrap_or(false))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Spawn blocking failed: {}", e))??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# Deploy HBBS v6 + HBBR with comprehensive ban enforcement
|
||||
# This script deploys both signal server (hbbs) and relay server (hbbr)
|
||||
|
||||
param(
|
||||
[string]$Server = "YOUR_SERVER_IP",
|
||||
[string]$User = "YOUR_SSH_USER"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n🚀 Deploying HBBS v6 + HBBR with comprehensive ban checks...`n" -ForegroundColor Cyan
|
||||
|
||||
# Step 1: Wait for compilation
|
||||
Write-Host "⏱️ Checking compilation status..." -ForegroundColor Yellow
|
||||
$maxWait = 20
|
||||
for ($i = 1; $i -le $maxWait; $i++) {
|
||||
$status = ssh "$User@$Server" "ps aux | grep -q '[c]argo build' && echo 'running' || echo 'done'"
|
||||
|
||||
if ($status -match "done") {
|
||||
Write-Host "✅ Compilation finished!" -ForegroundColor Green
|
||||
break
|
||||
}
|
||||
|
||||
if ($i -eq $maxWait) {
|
||||
Write-Host "❌ Compilation timeout" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host " Waiting... ($i/$maxWait)" -ForegroundColor Gray
|
||||
Start-Sleep -Seconds 30
|
||||
}
|
||||
|
||||
# Step 2: Verify binaries exist
|
||||
Write-Host "`n📦 Verifying binaries..." -ForegroundColor Yellow
|
||||
$result = ssh "$User@$Server" @"
|
||||
ls -lh /tmp/rustdesk-server/target/release/hbbs /tmp/rustdesk-server/target/release/hbbr 2>/dev/null | wc -l
|
||||
"@
|
||||
|
||||
if ($result -ne "2") {
|
||||
Write-Host "❌ Binaries not found! Compilation may have failed." -ForegroundColor Red
|
||||
Write-Host "`nChecking build log for errors..." -ForegroundColor Yellow
|
||||
ssh "$User@$Server" "tail -50 /tmp/build-v6-final.log | grep -A5 'error\[' || tail -30 /tmp/build-v6-final.log"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Both binaries found" -ForegroundColor Green
|
||||
|
||||
# Step 3: Stop services
|
||||
Write-Host "`n🛑 Stopping RustDesk services..." -ForegroundColor Yellow
|
||||
ssh -t "$User@$Server" @"
|
||||
sudo systemctl stop rustdesksignal
|
||||
sudo systemctl stop rustdeskrelay 2>/dev/null || true
|
||||
echo 'Services stopped'
|
||||
"@
|
||||
|
||||
# Step 4: Backup existing binaries
|
||||
Write-Host "`n💾 Creating backups..." -ForegroundColor Yellow
|
||||
ssh -t "$User@$Server" @"
|
||||
sudo cp /opt/rustdesk/hbbs /opt/rustdesk/hbbs.v6.backup 2>/dev/null && echo 'HBBS backup created' || echo 'No existing HBBS'
|
||||
sudo cp /opt/rustdesk/hbbr /opt/rustdesk/hbbr.v6.backup 2>/dev/null && echo 'HBBR backup created' || echo 'No existing HBBR'
|
||||
"@
|
||||
|
||||
# Step 5: Deploy new binaries
|
||||
Write-Host "`n📥 Deploying new binaries..." -ForegroundColor Yellow
|
||||
ssh -t "$User@$Server" @"
|
||||
sudo cp /tmp/rustdesk-server/target/release/hbbs /opt/rustdesk/hbbs
|
||||
sudo cp /tmp/rustdesk-server/target/release/hbbr /opt/rustdesk/hbbr
|
||||
sudo chmod +x /opt/rustdesk/hbbs /opt/rustdesk/hbbr
|
||||
sudo chown root:root /opt/rustdesk/hbbs /opt/rustdesk/hbbr
|
||||
echo 'Binaries deployed'
|
||||
"@
|
||||
|
||||
# Step 6: Start services
|
||||
Write-Host "`n▶️ Starting services..." -ForegroundColor Yellow
|
||||
ssh -t "$User@$Server" @"
|
||||
sudo systemctl start rustdesksignal
|
||||
sudo systemctl start rustdeskrelay 2>/dev/null || sudo systemctl start hbbr 2>/dev/null || echo 'Note: Relay service may have different name'
|
||||
sleep 3
|
||||
echo ''
|
||||
echo '=== HBBS Status ==='
|
||||
sudo systemctl status rustdesksignal --no-pager | head -12
|
||||
echo ''
|
||||
echo '=== HBBR Status ==='
|
||||
sudo systemctl status rustdeskrelay --no-pager 2>/dev/null || ps aux | grep '[h]bbr' || echo 'HBBR may not be running as service'
|
||||
"@
|
||||
|
||||
# Step 7: Verify deployment
|
||||
Write-Host "`n✅ Verifying deployment..." -ForegroundColor Green
|
||||
ssh "$User@$Server" @"
|
||||
echo ''
|
||||
echo 'Binary sizes:'
|
||||
ls -lh /opt/rustdesk/hbbs /opt/rustdesk/hbbr | awk '{print \$9, \$5}'
|
||||
echo ''
|
||||
echo 'Checking ban check strings in binaries:'
|
||||
strings /opt/rustdesk/hbbs | grep -E 'BLOCKED|Registration REJECTED' | head -3
|
||||
strings /opt/rustdesk/hbbr | grep -E 'HBBR Relay' | head -2
|
||||
"@
|
||||
|
||||
Write-Host "`n✅ Deployment complete!`n" -ForegroundColor Green
|
||||
Write-Host "📝 Next steps:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Monitor logs: ssh $User@$Server 'sudo tail -f /var/log/rustdesk/signalserver.log'" -ForegroundColor Gray
|
||||
Write-Host " 2. Test ban enforcement with device 1253021143" -ForegroundColor Gray
|
||||
Write-Host " 3. Check HBBR logs for 'HBBR Relay BLOCKED' messages`n" -ForegroundColor Gray
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Deploying HBBS + HBBR v8 (bidirectional ban check) ==="
|
||||
|
||||
# Backup
|
||||
echo "Creating backups..."
|
||||
cp /opt/rustdesk/hbbs /opt/rustdesk/hbbs.v7.backup 2>/dev/null || true
|
||||
cp /opt/rustdesk/hbbr /opt/rustdesk/hbbr.v7.backup 2>/dev/null || true
|
||||
|
||||
# Stop
|
||||
echo "Stopping old processes..."
|
||||
pkill -9 hbbs || true
|
||||
pkill -9 hbbr || true
|
||||
sleep 2
|
||||
|
||||
# Copy
|
||||
echo "Copying v8 binaries..."
|
||||
cp /tmp/hbbs-ban-check-package/hbbs /opt/rustdesk/hbbs
|
||||
cp /tmp/hbbs-ban-check-package/hbbr /opt/rustdesk/hbbr
|
||||
chmod +x /opt/rustdesk/hbbs /opt/rustdesk/hbbr
|
||||
|
||||
# Clear logs
|
||||
echo "Clearing logs..."
|
||||
echo "" > /var/log/rustdesk/signalserver.log
|
||||
echo "" > /var/log/rustdesk/hbbr.log
|
||||
|
||||
# Start HBBS
|
||||
echo "Starting HBBS v8..."
|
||||
cd /opt/rustdesk
|
||||
nohup ./hbbs -k _ -r YOUR_SERVER_IP:21117 >> /var/log/rustdesk/signalserver.log 2>&1 &
|
||||
sleep 2
|
||||
|
||||
# Start HBBR
|
||||
echo "Starting HBBR v8..."
|
||||
nohup ./hbbr -k _ >> /var/log/rustdesk/hbbr.log 2>&1 &
|
||||
sleep 2
|
||||
|
||||
# Verify
|
||||
echo ""
|
||||
echo "Processes:"
|
||||
ps aux | grep -E "hbbs|hbbr" | grep -v grep
|
||||
|
||||
echo ""
|
||||
echo "Ports:"
|
||||
netstat -tlnp 2>/dev/null | grep -E "21116|21117" || ss -tlnp | grep -E "21116|21117"
|
||||
|
||||
echo ""
|
||||
echo "=== Deployment v8 complete ==="
|
||||
echo "New features:"
|
||||
echo "- SOURCE device ban check (device initiating connection)"
|
||||
echo "- TARGET device ban check (device being connected to)"
|
||||
echo "- Bidirectional blocking"
|
||||
@@ -1,101 +0,0 @@
|
||||
# Quick HBBS Deployment Script
|
||||
# Deploys pre-compiled binary to RustDesk server
|
||||
# Usage: .\deploy.ps1 [binary-name]
|
||||
# Example: .\deploy.ps1 hbbs-v3-patched
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Binary = "hbbs-v3-patched",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Server = "YOUR_SSH_USER@YOUR_SERVER_IP"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "HBBS Quick Deployment" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check if binary exists
|
||||
$BinaryPath = Join-Path $PSScriptRoot $Binary
|
||||
if (-not (Test-Path $BinaryPath)) {
|
||||
Write-Host "[ERROR] Binary not found: $BinaryPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$FileSize = (Get-Item $BinaryPath).Length / 1MB
|
||||
Write-Host "[1/4] Found binary: $Binary ($([math]::Round($FileSize, 2)) MB)" -ForegroundColor Yellow
|
||||
|
||||
# Upload binary
|
||||
Write-Host "[2/4] Uploading to server..." -ForegroundColor Yellow
|
||||
scp $BinaryPath "${Server}:/tmp/hbbs-new"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Upload failed" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Upload complete" -ForegroundColor Green
|
||||
|
||||
# Deploy on server
|
||||
Write-Host "[3/4] Installing on server..." -ForegroundColor Yellow
|
||||
$DeployScript = @"
|
||||
# Stop service
|
||||
sudo systemctl stop rustdesksignal
|
||||
|
||||
# Backup current version
|
||||
BACKUP_NAME="hbbs.backup.`$(date +%s)"
|
||||
sudo cp /opt/rustdesk/hbbs /opt/rustdesk/`$BACKUP_NAME
|
||||
echo "Backed up to: `$BACKUP_NAME"
|
||||
|
||||
# Install new version
|
||||
sudo cp /tmp/hbbs-new /opt/rustdesk/hbbs
|
||||
sudo chmod +x /opt/rustdesk/hbbs
|
||||
sudo chown root:root /opt/rustdesk/hbbs
|
||||
|
||||
# Start service
|
||||
sudo systemctl start rustdesksignal
|
||||
|
||||
# Wait for startup
|
||||
sleep 2
|
||||
|
||||
# Check status
|
||||
sudo systemctl status rustdesksignal --no-pager
|
||||
"@
|
||||
|
||||
ssh -t $Server $DeployScript
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Deployment failed" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[4/4] Verifying deployment..." -ForegroundColor Yellow
|
||||
|
||||
# Check logs for errors
|
||||
$LogCheck = @"
|
||||
if sudo grep -i "error\|panic" /var/log/rustdesk/signalserver.error | tail -5 | grep -q .; then
|
||||
echo "WARNING: Errors found in logs"
|
||||
exit 1
|
||||
else
|
||||
echo "No errors in logs"
|
||||
fi
|
||||
"@
|
||||
|
||||
ssh $Server $LogCheck
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "Deployment Successful!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "[WARNING] Service started but errors detected in logs" -ForegroundColor Yellow
|
||||
Write-Host "Check logs: ssh $Server 'sudo tail -50 /var/log/rustdesk/signalserver.error'" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Available backups:" -ForegroundColor Cyan
|
||||
ssh $Server "ls -lh /opt/rustdesk/hbbs.backup.* 2>/dev/null | tail -5 || echo 'No backups found'"
|
||||
@@ -1,60 +0,0 @@
|
||||
# HBBS Ban Enforcement - Diagnostic Script
|
||||
# Tests both directions and verifies database access
|
||||
|
||||
param(
|
||||
[string]$Server = "YOUR_SSH_USER@YOUR_SERVER_IP",
|
||||
[string]$BannedDevice = "1253021143"
|
||||
)
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "HBBS Ban Diagnostic v4" -ForegroundColor Cyan
|
||||
Write-Host "========================================`n" -ForegroundColor Cyan
|
||||
|
||||
# 1. Check database location and permissions
|
||||
Write-Host "[1/5] Database Configuration" -ForegroundColor Yellow
|
||||
$dbInfo = ssh $Server "ls -lh /opt/rustdesk/db_v2.sqlite* && echo '' && file /opt/rustdesk/db_v2.sqlite3"
|
||||
Write-Host $dbInfo -ForegroundColor Gray
|
||||
|
||||
# 2. Check HBBS process
|
||||
Write-Host "`n[2/5] HBBS Process Status" -ForegroundColor Yellow
|
||||
$processInfo = ssh $Server "ps aux | grep '/opt/rustdesk/hbbs' | grep -v grep && echo '' && sudo readlink /proc/`$(pgrep -f '/opt/rustdesk/hbbs')/cwd"
|
||||
Write-Host $processInfo -ForegroundColor Gray
|
||||
|
||||
# 3. Check device ban status in database (via API)
|
||||
Write-Host "`n[3/5] Device Ban Status (API)" -ForegroundColor Yellow
|
||||
$apiCheck = ssh $Server "curl -s http://localhost:5000/api/devices 2>/dev/null | grep -A10 '$BannedDevice' | head -15"
|
||||
Write-Host $apiCheck -ForegroundColor Gray
|
||||
|
||||
# 4. Check recent logs for ban enforcement
|
||||
Write-Host "`n[4/5] Recent Ban Logs" -ForegroundColor Yellow
|
||||
$banLogs = ssh $Server "grep -E 'REJECTED|ban.*$BannedDevice|$BannedDevice.*ban' /var/log/rustdesk/signalserver.log | tail -10"
|
||||
if ($banLogs) {
|
||||
Write-Host $banLogs -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "No ban enforcement logs found" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 5. Check for registration attempts
|
||||
Write-Host "`n[5/5] Registration Attempts" -ForegroundColor Yellow
|
||||
$regLogs = ssh $Server "grep 'update_pk $BannedDevice' /var/log/rustdesk/signalserver.log | tail -5"
|
||||
if ($regLogs) {
|
||||
Write-Host "Device is still registering (should be blocked):" -ForegroundColor Red
|
||||
Write-Host $regLogs -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "No recent registration attempts" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Cyan
|
||||
Write-Host "Diagnostic Summary" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nExpected behavior after v4 deployment:" -ForegroundColor Yellow
|
||||
Write-Host "1. Device $BannedDevice should NOT appear in 'update_pk' logs" -ForegroundColor Gray
|
||||
Write-Host "2. Should see 'Registration REJECTED' messages" -ForegroundColor Gray
|
||||
Write-Host "3. Should see 'Relay REJECTED - initiator is banned' OR 'target is banned'" -ForegroundColor Gray
|
||||
Write-Host "4. Device should NOT be 'online' in API response" -ForegroundColor Gray
|
||||
|
||||
Write-Host "`nIf device still registers:" -ForegroundColor Yellow
|
||||
Write-Host "- Check if v4 binary is deployed" -ForegroundColor Gray
|
||||
Write-Host "- Restart HBBS service" -ForegroundColor Gray
|
||||
Write-Host "- Check error logs for database access issues" -ForegroundColor Gray
|
||||
@@ -1,15 +0,0 @@
|
||||
// Patch for peer.rs - add method to find device ID by socket address
|
||||
|
||||
// Add this method to PeerMap impl block (after is_in_memory method):
|
||||
|
||||
// Find device ID by socket address
|
||||
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
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Patch dla peer.rs - modyfikuje metodę update_pk
|
||||
//
|
||||
// Znajdź metodę `pub(crate) async fn update_pk(` w pliku peer.rs
|
||||
// i zastąp jej początek (pierwszych ~20 linii) poniższym kodem:
|
||||
|
||||
#[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);
|
||||
|
||||
// *** NOWE: Sprawdzenie czy urządzenie jest zbanowane ***
|
||||
// Zapytanie do bazy danych przed akceptacją rejestracji
|
||||
match self.db.is_device_banned(&id).await {
|
||||
Ok(true) => {
|
||||
// Urządzenie jest zbanowane - odrzuć rejestrację
|
||||
log::warn!("Registration REJECTED for device {}: DEVICE IS BANNED", id);
|
||||
return register_pk_response::Result::UUID_MISMATCH;
|
||||
}
|
||||
Ok(false) => {
|
||||
// Urządzenie nie jest zbanowane - kontynuuj normalnie
|
||||
log::debug!("Ban check passed for device {}", id);
|
||||
}
|
||||
Err(e) => {
|
||||
// Błąd zapytania do bazy - loguj ale przepuść (fail-open policy)
|
||||
log::error!("Failed to check ban status for device {}: {}. Allowing registration (fail-open)", id, e);
|
||||
// Kontynuuj rejestrację mimo błędu bazy
|
||||
}
|
||||
}
|
||||
// *** KONIEC NOWEGO KODU ***
|
||||
|
||||
// Oryginalna logika rejestracji (bez zmian):
|
||||
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.info.ip = ip;
|
||||
(
|
||||
serde_json::to_string(&w.info).unwrap_or_default(),
|
||||
w.guid.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// ... reszta metody pozostaje bez zmian ...
|
||||
@@ -0,0 +1,78 @@
|
||||
[package]
|
||||
name = "hbbs"
|
||||
version = "1.1.14"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
default-run = "hbbs"
|
||||
|
||||
[[bin]]
|
||||
name = "hbbr"
|
||||
path = "src/hbbr.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rustdesk-utils"
|
||||
path = "src/utils.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
lazy_static = "1.4"
|
||||
clap = "2"
|
||||
rust-ini = "0.18"
|
||||
minreq = { version = "2.4", features = ["punycode"] }
|
||||
machine-uid = "0.2"
|
||||
mac_address = "1.1.5"
|
||||
whoami = "1.2"
|
||||
base64 = "0.13"
|
||||
axum = { version = "0.6", features = ["headers", "tokio"] }
|
||||
tower = "0.4"
|
||||
sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "sqlite", "macros", "chrono", "json" ] }
|
||||
deadpool = "0.8"
|
||||
async-trait = "0.1"
|
||||
async-speed-limit = { git = "https://github.com/open-trade/async-speed-limit" }
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
bcrypt = "0.13"
|
||||
chrono = "0.4"
|
||||
jsonwebtoken = "8"
|
||||
headers = "0.3"
|
||||
once_cell = "1.8"
|
||||
sodiumoxide = "0.2"
|
||||
tokio-tungstenite = "0.17"
|
||||
tungstenite = "0.17"
|
||||
regex = "1.4"
|
||||
tower-http = { version = "0.3", features = ["fs", "trace", "cors"] }
|
||||
http = "0.2"
|
||||
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset", "dont_minimize_extra_stacks"] }
|
||||
ipnetwork = "0.20"
|
||||
local-ip-address = "0.5.1"
|
||||
dns-lookup = "1.0.8"
|
||||
ping = "0.4.0"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[build-dependencies]
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/hbb_common"]
|
||||
exclude = ["ui"]
|
||||
|
||||
#https://github.com/johnthagen/min-sized-rust
|
||||
#https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip # Default is 3, better performance
|
||||
#rpath = true # Not needed
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
http::StatusCode,
|
||||
http::{StatusCode, HeaderMap},
|
||||
response::Json,
|
||||
routing::get,
|
||||
Router,
|
||||
@@ -9,14 +9,15 @@ use serde::Serialize;
|
||||
use sqlx::{sqlite::SqlitePool, Row};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use crate::peer::PeerMap;
|
||||
use std::fs;
|
||||
|
||||
const REG_TIMEOUT: i32 = 20_000;
|
||||
const API_KEY_FILE: &str = "/opt/rustdesk/.api_key";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiState {
|
||||
pub db_pool: SqlitePool,
|
||||
pub peer_map: Arc<PeerMap>,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -33,10 +34,31 @@ struct ApiResponse<T> {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
// Middleware to verify API key
|
||||
fn verify_api_key(headers: &HeaderMap, state: &ApiState) -> Result<(), StatusCode> {
|
||||
match headers.get("X-API-Key") {
|
||||
Some(key) => {
|
||||
if key.to_str().unwrap_or("") == state.api_key {
|
||||
Ok(())
|
||||
} else {
|
||||
hbb_common::log::warn!("API: Invalid API key provided");
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
hbb_common::log::warn!("API: Missing X-API-Key header");
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_online_peers(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<Arc<ApiState>>,
|
||||
) -> Result<Json<ApiResponse<Vec<PeerStatus>>>, StatusCode> {
|
||||
match sqlx::query("SELECT id, note FROM peer")
|
||||
// Verify API key
|
||||
verify_api_key(&headers, &state)?;
|
||||
match sqlx::query("SELECT id, note, info FROM peer WHERE (status IS NULL OR status = 0)")
|
||||
.fetch_all(&state.db_pool)
|
||||
.await
|
||||
{
|
||||
@@ -46,14 +68,11 @@ async fn get_online_peers(
|
||||
for row in rows.iter() {
|
||||
let id: String = row.get("id");
|
||||
let note: Option<String> = row.get("note");
|
||||
let _info: String = row.get("info");
|
||||
|
||||
// Check real-time online status from PeerMap (same logic as RustDesk client)
|
||||
let online = if let Some(peer) = state.peer_map.get_in_memory(&id).await {
|
||||
let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i32;
|
||||
elapsed < REG_TIMEOUT
|
||||
} else {
|
||||
false
|
||||
};
|
||||
// For now, mark all devices as offline since we can't determine online status from DB
|
||||
// Real online status requires querying the in-memory PeerMap which isn't accessible here
|
||||
let online = false;
|
||||
|
||||
peers.push(PeerStatus {
|
||||
id,
|
||||
@@ -76,15 +95,66 @@ async fn get_online_peers(
|
||||
}
|
||||
}
|
||||
|
||||
async fn health_check() -> Json<ApiResponse<String>> {
|
||||
Json(ApiResponse {
|
||||
async fn health_check(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<Arc<ApiState>>,
|
||||
) -> Result<Json<ApiResponse<String>>, StatusCode> {
|
||||
// Verify API key
|
||||
verify_api_key(&headers, &state)?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
data: Some("RustDesk API is running".to_string()),
|
||||
error: None,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn start_api_server(db_path: String, port: u16, peer_map: Arc<PeerMap>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn load_or_generate_api_key() -> String {
|
||||
// Try to read from file first
|
||||
if let Ok(key) = fs::read_to_string(API_KEY_FILE) {
|
||||
let key = key.trim().to_string();
|
||||
if !key.is_empty() {
|
||||
hbb_common::log::info!("API: Loaded API key from {}", API_KEY_FILE);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new key
|
||||
use hbb_common::rand::Rng;
|
||||
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let mut rng = hbb_common::rand::thread_rng();
|
||||
let key: String = (0..64)
|
||||
.map(|_| {
|
||||
let idx = rng.gen_range(0..CHARSET.len());
|
||||
CHARSET[idx] as char
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Try to save to file
|
||||
if let Some(parent) = std::path::Path::new(API_KEY_FILE).parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
if let Err(e) = fs::write(API_KEY_FILE, &key) {
|
||||
hbb_common::log::warn!("API: Could not save API key to file: {}", e);
|
||||
} else {
|
||||
hbb_common::log::info!("API: Generated and saved new API key to {}", API_KEY_FILE);
|
||||
// Set file permissions to 600 (owner read/write only)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(metadata) = fs::metadata(API_KEY_FILE) {
|
||||
let mut perms = metadata.permissions();
|
||||
perms.set_mode(0o600);
|
||||
let _ = fs::set_permissions(API_KEY_FILE, perms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub async fn start_api_server(db_path: String, port: u16) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -94,9 +164,12 @@ pub async fn start_api_server(db_path: String, port: u16, peer_map: Arc<PeerMap>
|
||||
|
||||
let pool = SqlitePool::connect_with(connect_options).await?;
|
||||
|
||||
// Load or generate API key
|
||||
let api_key = load_or_generate_api_key();
|
||||
|
||||
let state = Arc::new(ApiState {
|
||||
db_pool: pool,
|
||||
peer_map,
|
||||
api_key: api_key.clone(),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
@@ -104,10 +177,12 @@ pub async fn start_api_server(db_path: String, port: u16, peer_map: Arc<PeerMap>
|
||||
.route("/api/peers", get(get_online_peers))
|
||||
.layer(axum::Extension(state));
|
||||
|
||||
// SECURITY: Bind only to localhost (127.0.0.1) - not exposed to internet
|
||||
// Web console connects locally, so no need for external access
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
hbb_common::log::info!("HTTP API server listening on {} (localhost only)", addr);
|
||||
// SECURITY UPDATE: Now binds to 0.0.0.0 (all interfaces) for LAN access
|
||||
// Protected by API key authentication (X-API-Key header required)
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
hbb_common::log::info!("HTTP API server listening on {} (LAN accessible, API key protected)", addr);
|
||||
hbb_common::log::info!("API key saved to: {}", API_KEY_FILE);
|
||||
hbb_common::log::info!("Use X-API-Key header with value from file for authentication");
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
use flexi_logger::*;
|
||||
use hbb_common::{bail, config::RENDEZVOUS_PORT, ResultType};
|
||||
use hbbs::{common::*, *};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod http_api;
|
||||
|
||||
const RMEM: usize = 0;
|
||||
const API_PORT: u16 = 21120; // Localhost-only API port (not exposed to internet)
|
||||
const API_PORT: u16 = 21120; // HTTP API port (LAN accessible with X-API-Key auth)
|
||||
|
||||
fn main() -> ResultType<()> {
|
||||
let _logger = Logger::try_with_env_or_str("info")?
|
||||
@@ -35,7 +38,18 @@ fn main() -> ResultType<()> {
|
||||
let serial: i32 = get_arg("serial").parse().unwrap_or(0);
|
||||
let api_port = get_arg("api-port").parse::<u16>().unwrap_or(API_PORT);
|
||||
|
||||
// Start HTTP API server in background
|
||||
// API reads device status directly from SQLite database
|
||||
std::thread::spawn(move || {
|
||||
hbb_common::tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let db_path = get_arg_or("db", "/opt/rustdesk/db_v2.sqlite3".to_owned());
|
||||
if let Err(e) = http_api::start_api_server(db_path, api_port).await {
|
||||
hbb_common::log::error!("HTTP API failed to start: {}", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
crate::common::check_software_update();
|
||||
RendezvousServer::start(port, serial, &get_arg_or("key", "-".to_owned()), rmem, api_port)?;
|
||||
RendezvousServer::start(port, serial, &get_arg_or("key", "-".to_owned()), rmem)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -713,7 +713,9 @@ impl RendezvousServer {
|
||||
log::debug!("Target ban check passed for device {}", id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check target ban status for {}: {}", id, e);
|
||||
log::error!("SECURITY: Database unavailable, blocking connection for safety: {}", e);
|
||||
// Return empty response to block connection (fail-closed)
|
||||
return Ok((RendezvousMessage::new(), None));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +736,9 @@ impl RendezvousServer {
|
||||
log::debug!("Source ban check passed for device {}", source_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check source ban status for {}: {}", source_id, e);
|
||||
log::error!("SECURITY: Database unavailable, blocking source connection for safety: {}", e);
|
||||
// Return empty response to block connection (fail-closed)
|
||||
return Ok((RendezvousMessage::new(), None));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
# Test Ban Enforcement - Comprehensive Testing Script
|
||||
# Tests bidirectional ban enforcement (initiator + target)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$BannedDeviceId = "58457133",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$NormalDeviceId = "1253021143",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$ApiUrl = "http://YOUR_SERVER_IP:5000",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Server = "YOUR_SSH_USER@YOUR_SERVER_IP"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
function Write-TestHeader {
|
||||
param([string]$Title)
|
||||
Write-Host "`n========================================" -ForegroundColor Cyan
|
||||
Write-Host $Title -ForegroundColor Cyan
|
||||
Write-Host "========================================`n" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-TestResult {
|
||||
param([string]$Test, [bool]$Passed, [string]$Details = "")
|
||||
if ($Passed) {
|
||||
Write-Host "[✓] $Test" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[✗] $Test" -ForegroundColor Red
|
||||
}
|
||||
if ($Details) {
|
||||
Write-Host " $Details" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
Write-TestHeader "HBBS Ban Enforcement Tests"
|
||||
Write-Host "Banned Device: $BannedDeviceId" -ForegroundColor Yellow
|
||||
Write-Host "Normal Device: $NormalDeviceId" -ForegroundColor Yellow
|
||||
Write-Host "API URL: $ApiUrl`n" -ForegroundColor Yellow
|
||||
|
||||
# Test 1: Check service status
|
||||
Write-TestHeader "[1/6] Service Status Check"
|
||||
$serviceStatus = ssh $Server "sudo systemctl is-active rustdesksignal" 2>$null
|
||||
$serviceRunning = $serviceStatus -eq "active"
|
||||
Write-TestResult "Service rustdesksignal is running" $serviceRunning $serviceStatus
|
||||
|
||||
if (-not $serviceRunning) {
|
||||
Write-Host "`nERROR: Service is not running. Starting service..." -ForegroundColor Red
|
||||
ssh -t $Server "sudo systemctl start rustdesksignal"
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
|
||||
# Test 2: Check HBBS version and binary
|
||||
Write-TestHeader "[2/6] Binary Version Check"
|
||||
$hbbsVersion = ssh $Server "ls -lh /opt/rustdesk/hbbs | awk '{print `$5, `$9}'" 2>$null
|
||||
Write-Host "Binary: $hbbsVersion" -ForegroundColor Gray
|
||||
|
||||
$backupCount = ssh $Server "ls -1 /opt/rustdesk/hbbs.backup.* 2>/dev/null | wc -l" 2>$null
|
||||
Write-Host "Backups available: $backupCount" -ForegroundColor Gray
|
||||
|
||||
# Test 3: Check API connectivity
|
||||
Write-TestHeader "[3/6] API Connectivity Check"
|
||||
try {
|
||||
$devices = Invoke-RestMethod -Uri "$ApiUrl/api/devices" -Method Get -TimeoutSec 5
|
||||
$deviceCount = $devices.Count
|
||||
Write-TestResult "API responding" $true "$deviceCount devices returned"
|
||||
} catch {
|
||||
Write-TestResult "API responding" $false $_.Exception.Message
|
||||
Write-Host "`nERROR: Cannot connect to API. Exiting..." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Test 4: Check ban status of test device
|
||||
Write-TestHeader "[4/6] Device Ban Status Check"
|
||||
$bannedDevice = $devices | Where-Object { $_.id -eq $BannedDeviceId }
|
||||
if ($bannedDevice) {
|
||||
$isBanned = $bannedDevice.is_banned -eq $true
|
||||
Write-TestResult "Device $BannedDeviceId found in API" $true
|
||||
Write-TestResult "Device $BannedDeviceId is banned" $isBanned "is_banned: $($bannedDevice.is_banned)"
|
||||
|
||||
if (-not $isBanned) {
|
||||
Write-Host "`nWARNING: Test device is not banned. Ban it first via console:" -ForegroundColor Yellow
|
||||
Write-Host " $ApiUrl" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-TestResult "Device $BannedDeviceId found in API" $false "Device not in database"
|
||||
}
|
||||
|
||||
# Test 5: Check logs for ban enforcement
|
||||
Write-TestHeader "[5/6] Log Analysis - Ban Enforcement"
|
||||
Write-Host "Checking last 50 ban-related log entries..." -ForegroundColor Gray
|
||||
|
||||
$banLogs = ssh $Server "grep -i 'ban\|reject' /var/log/rustdesk/signalserver.log 2>/dev/null | tail -50"
|
||||
if ($banLogs) {
|
||||
$recentBans = ($banLogs -split "`n" | Select-Object -Last 10) -join "`n"
|
||||
Write-Host "`nRecent ban activity:" -ForegroundColor Yellow
|
||||
Write-Host $recentBans -ForegroundColor Gray
|
||||
|
||||
# Count different types of rejections
|
||||
$registrationRejections = ($banLogs | Select-String "Registration REJECTED").Count
|
||||
$relayRejections = ($banLogs | Select-String "Relay.*REJECTED").Count
|
||||
$punchHoleRejections = ($banLogs | Select-String "Punch hole.*REJECTED").Count
|
||||
$initiatorBlocks = ($banLogs | Select-String "initiator.*banned").Count
|
||||
$targetBlocks = ($banLogs | Select-String "target.*banned").Count
|
||||
|
||||
Write-Host "`nSummary:" -ForegroundColor Cyan
|
||||
Write-Host " Registration rejections: $registrationRejections" -ForegroundColor Gray
|
||||
Write-Host " Relay rejections: $relayRejections" -ForegroundColor Gray
|
||||
Write-Host " Punch hole rejections: $punchHoleRejections" -ForegroundColor Gray
|
||||
Write-Host " Initiator blocks: $initiatorBlocks" -ForegroundColor Gray
|
||||
Write-Host " Target blocks: $targetBlocks" -ForegroundColor Gray
|
||||
|
||||
$bidirectionalWorks = ($initiatorBlocks -gt 0) -and ($targetBlocks -gt 0)
|
||||
Write-TestResult "Bidirectional ban enforcement active" $bidirectionalWorks
|
||||
} else {
|
||||
Write-TestResult "Log file accessible" $false "No ban logs found"
|
||||
}
|
||||
|
||||
# Test 6: Check for errors
|
||||
Write-TestHeader "[6/6] Error Log Check"
|
||||
$errorLogs = ssh $Server "grep -i 'error\|panic' /var/log/rustdesk/signalserver.error 2>/dev/null | tail -20"
|
||||
if ($errorLogs) {
|
||||
$recentErrors = ($errorLogs -split "`n" | Select-Object -Last 5) -join "`n"
|
||||
Write-Host "Recent errors found:" -ForegroundColor Red
|
||||
Write-Host $recentErrors -ForegroundColor Gray
|
||||
Write-TestResult "No critical errors" $false "Errors detected in logs"
|
||||
} else {
|
||||
Write-TestResult "No critical errors" $true "Error log is clean"
|
||||
}
|
||||
|
||||
# Test 7: Performance check
|
||||
Write-TestHeader "Performance Metrics"
|
||||
$cpuUsage = ssh $Server "ps -p `$(pgrep -f '/opt/rustdesk/hbbs') -o %cpu --no-headers | awk '{print `$1}'" 2>$null
|
||||
$memUsage = ssh $Server "ps -p `$(pgrep -f '/opt/rustdesk/hbbs') -o %mem --no-headers | awk '{print `$1}'" 2>$null
|
||||
|
||||
if ($cpuUsage) {
|
||||
Write-Host " CPU Usage: $cpuUsage%" -ForegroundColor Gray
|
||||
Write-Host " Memory Usage: $memUsage%" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host " Process metrics unavailable" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Final Summary
|
||||
Write-TestHeader "Test Summary"
|
||||
Write-Host "1. Service Status: " -NoNewline
|
||||
Write-Host $(if ($serviceRunning) { "PASS" } else { "FAIL" }) -ForegroundColor $(if ($serviceRunning) { "Green" } else { "Red" })
|
||||
|
||||
Write-Host "2. API Connectivity: " -NoNewline
|
||||
Write-Host $(if ($devices) { "PASS" } else { "FAIL" }) -ForegroundColor $(if ($devices) { "Green" } else { "Red" })
|
||||
|
||||
Write-Host "3. Ban Status Correct: " -NoNewline
|
||||
if ($bannedDevice) {
|
||||
$banCorrect = $bannedDevice.is_banned -eq $true
|
||||
Write-Host $(if ($banCorrect) { "PASS" } else { "WARN" }) -ForegroundColor $(if ($banCorrect) { "Green" } else { "Yellow" })
|
||||
} else {
|
||||
Write-Host "N/A" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host "4. Log Analysis: " -NoNewline
|
||||
Write-Host $(if ($banLogs) { "PASS" } else { "WARN" }) -ForegroundColor $(if ($banLogs) { "Green" } else { "Yellow" })
|
||||
|
||||
Write-Host "5. Error Check: " -NoNewline
|
||||
Write-Host $(if (-not $errorLogs) { "PASS" } else { "WARN" }) -ForegroundColor $(if (-not $errorLogs) { "Green" } else { "Yellow" })
|
||||
|
||||
Write-Host "`n========================================" -ForegroundColor Cyan
|
||||
Write-Host "Testing Complete!" -ForegroundColor Cyan
|
||||
Write-Host "========================================`n" -ForegroundColor Cyan
|
||||
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. If device $BannedDeviceId is not banned, ban it via console: $ApiUrl" -ForegroundColor Gray
|
||||
Write-Host "2. Try connecting FROM banned device to normal device" -ForegroundColor Gray
|
||||
Write-Host "3. Try connecting FROM normal device TO banned device" -ForegroundColor Gray
|
||||
Write-Host "4. Check logs for 'initiator banned' and 'target banned' messages:" -ForegroundColor Gray
|
||||
Write-Host " ssh $Server 'tail -f /var/log/rustdesk/signalserver.log | grep -i reject'" -ForegroundColor Gray
|
||||
@@ -1,9 +1,14 @@
|
||||
# BetterDesk Console - Windows Installation Script v9
|
||||
# BetterDesk Console - Windows Installation Script v1.5.0
|
||||
#
|
||||
# This script installs the enhanced RustDesk HBBS/HBBR servers with
|
||||
# bidirectional ban enforcement, HTTP API, and web management console.
|
||||
#
|
||||
# NEW in v9:
|
||||
# NEW in v1.5.0:
|
||||
# - Authentication system with bcrypt password hashing
|
||||
# - Role-based access control (Admin, Operator, Viewer)
|
||||
# - Sidebar navigation with multiple sections
|
||||
# - Password-protected public key access
|
||||
# - User management panel (admin only)
|
||||
# - Support for custom RustDesk installation directories
|
||||
# - Automatic verification of required RustDesk files
|
||||
# - Improved error handling and validation
|
||||
@@ -35,7 +40,7 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$VERSION = "v9"
|
||||
$VERSION = "v1.5.0"
|
||||
$BINARY_VERSION = "v8-api"
|
||||
$HBBS_API_PORT = 21114
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# BetterDesk Console - Enhanced Installation Script v9
|
||||
# BetterDesk Console - Enhanced Installation Script v1.5.0
|
||||
#
|
||||
# This script installs the enhanced RustDesk HBBS/HBBR servers with
|
||||
# bidirectional ban enforcement, HTTP API, and web management console.
|
||||
#
|
||||
# NEW in v9:
|
||||
# NEW in v1.5.0:
|
||||
# - Authentication system with bcrypt password hashing
|
||||
# - Role-based access control (Admin, Operator, Viewer)
|
||||
# - Sidebar navigation with multiple sections
|
||||
# - Password-protected public key access
|
||||
# - User management panel (admin only)
|
||||
# - CSRF protection and rate limiting
|
||||
# - Support for custom RustDesk installation directories
|
||||
# - Automatic verification of required RustDesk files
|
||||
# - --break-system-packages support for Docker/containerized environments
|
||||
# - Improved error handling and validation
|
||||
#
|
||||
# Features:
|
||||
# - Automatic backup of existing RustDesk installation
|
||||
@@ -21,8 +26,9 @@
|
||||
# - Configures systemd services
|
||||
# - Uses Google Material Icons (offline)
|
||||
#
|
||||
# Author: GitHub Copilot
|
||||
# License: MIT
|
||||
# Author: UNITRONIX
|
||||
# Repository: https://github.com/UNITRONIX/Rustdesk-FreeConsole
|
||||
# License: AGPL-3.0
|
||||
#############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
@@ -40,9 +46,10 @@ BACKUP_DIR=""
|
||||
CONSOLE_DIR="/opt/BetterDeskConsole"
|
||||
TEMP_DIR="/tmp/betterdesk-install"
|
||||
HBBS_API_PORT=21114
|
||||
VERSION="v9" # Current version with HTTP API
|
||||
VERSION="v1.5.0" # Current version with Authentication & User Management
|
||||
BINARY_VERSION="v8-api" # Binary file suffix
|
||||
PIP_EXTRA_ARGS="" # Will be set to --break-system-packages if needed
|
||||
FLASK_SECRET_KEY="" # Will be generated during installation
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
@@ -836,12 +843,31 @@ run_database_migrations() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run v1.4.0 migration (authentication system)
|
||||
if [ -f "$migrations_dir/v1.4.0_auth_system.py" ]; then
|
||||
print_info "Running migration v1.4.0 (authentication system)..."
|
||||
if python3 "$migrations_dir/v1.4.0_auth_system.py"; then
|
||||
print_success "Migration v1.4.0 completed"
|
||||
print_info "Default admin credentials created (check output above)"
|
||||
else
|
||||
print_warning "Migration v1.4.0 skipped (may be already applied)"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_success "Database migrations completed"
|
||||
echo ""
|
||||
}
|
||||
|
||||
generate_secret_key() {
|
||||
print_header "Generating Flask Secret Key"
|
||||
|
||||
# Generate a secure random secret key
|
||||
FLASK_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||
print_success "Secret key generated"
|
||||
}
|
||||
|
||||
install_web_console() {
|
||||
print_header "Installing Web Management Console"
|
||||
print_header "Installing Web Management Console v1.5.0"
|
||||
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local web_dir="$script_dir/web"
|
||||
@@ -853,18 +879,40 @@ install_web_console() {
|
||||
|
||||
# Create console directory
|
||||
mkdir -p "$CONSOLE_DIR"
|
||||
mkdir -p "$CONSOLE_DIR/templates"
|
||||
mkdir -p "$CONSOLE_DIR/static"
|
||||
|
||||
# Copy files
|
||||
print_info "Copying web console files..."
|
||||
cp -r "$web_dir"/* "$CONSOLE_DIR/"
|
||||
cp "$web_dir/app_v14.py" "$CONSOLE_DIR/app.py"
|
||||
cp "$web_dir/auth.py" "$CONSOLE_DIR/auth.py"
|
||||
cp "$web_dir/requirements.txt" "$CONSOLE_DIR/requirements.txt"
|
||||
|
||||
# Copy templates (use v1.5.0 interface)
|
||||
print_info "Installing v1.5.0 user interface..."
|
||||
cp "$web_dir/templates/index_v15.html" "$CONSOLE_DIR/templates/index_v15.html"
|
||||
cp "$web_dir/templates/login.html" "$CONSOLE_DIR/templates/login.html"
|
||||
|
||||
# Copy static files
|
||||
print_info "Installing JavaScript and CSS..."
|
||||
if [ -d "$web_dir/static" ]; then
|
||||
cp -r "$web_dir/static"/* "$CONSOLE_DIR/static/" 2>/dev/null || true
|
||||
fi
|
||||
cp "$web_dir/static/script_v15.js" "$CONSOLE_DIR/static/script_v15.js"
|
||||
|
||||
# Update app.py with correct RustDesk path
|
||||
if [ "$RUSTDESK_DIR" != "/opt/rustdesk" ]; then
|
||||
print_info "Updating console configuration for custom RustDesk path..."
|
||||
sed -i "s|'/opt/rustdesk/db_v2.sqlite3'|'$RUSTDESK_DIR/db_v2.sqlite3'|g" "$CONSOLE_DIR/app.py"
|
||||
sed -i "s|'/opt/rustdesk/id_ed25519.pub'|'$RUSTDESK_DIR/id_ed25519.pub'|g" "$CONSOLE_DIR/app.py"
|
||||
sed -i "s|'/opt/rustdesk/.api_key'|'$RUSTDESK_DIR/.api_key'|g" "$CONSOLE_DIR/app.py"
|
||||
sed -i "s|DB_PATH = '/opt/rustdesk/db_v2.sqlite3'|DB_PATH = '$RUSTDESK_DIR/db_v2.sqlite3'|g" "$CONSOLE_DIR/auth.py"
|
||||
fi
|
||||
|
||||
# Generate and configure Flask secret key
|
||||
print_info "Configuring Flask security..."
|
||||
generate_secret_key
|
||||
|
||||
# Install Python dependencies
|
||||
print_info "Installing Python dependencies..."
|
||||
if [ -n "$PIP_EXTRA_ARGS" ]; then
|
||||
@@ -874,11 +922,11 @@ install_web_console() {
|
||||
pip3 install -r "$CONSOLE_DIR/requirements.txt"
|
||||
fi
|
||||
|
||||
# Create systemd service
|
||||
# Create systemd service with environment variables
|
||||
print_info "Creating systemd service..."
|
||||
cat > /etc/systemd/system/betterdesk.service <<EOF
|
||||
[Unit]
|
||||
Description=BetterDesk Console - RustDesk Web Management
|
||||
Description=BetterDesk Console v1.5.0 - RustDesk Web Management
|
||||
After=network.target rustdesksignal.service
|
||||
|
||||
[Service]
|
||||
@@ -891,6 +939,17 @@ RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Create systemd override for environment variables
|
||||
print_info "Configuring Flask environment..."
|
||||
mkdir -p /etc/systemd/system/betterdesk.service.d
|
||||
cat > /etc/systemd/system/betterdesk.service.d/override.conf <<EOF
|
||||
[Service]
|
||||
Environment="FLASK_SECRET_KEY=$FLASK_SECRET_KEY"
|
||||
Environment="FLASK_HOST=0.0.0.0"
|
||||
Environment="FLASK_PORT=5000"
|
||||
Environment="FLASK_DEBUG=False"
|
||||
EOF
|
||||
|
||||
# Enable and start service
|
||||
@@ -971,6 +1030,7 @@ show_summary() {
|
||||
echo -e "${GREEN}BetterDesk Console $VERSION has been successfully installed!${NC}"
|
||||
echo ""
|
||||
echo "Installation details:"
|
||||
echo " • Version: BetterDesk Console v1.5.0"
|
||||
echo " • RustDesk Directory: $RUSTDESK_DIR"
|
||||
echo " • Console Directory: $CONSOLE_DIR"
|
||||
echo ""
|
||||
@@ -978,6 +1038,12 @@ show_summary() {
|
||||
echo " • Web Console: http://$(hostname -I | awk '{print $1}'):5000"
|
||||
echo " • HBBS API: http://localhost:$HBBS_API_PORT/api/health"
|
||||
echo ""
|
||||
echo "Authentication:"
|
||||
echo " • Login page: http://$(hostname -I | awk '{print $1}'):5000/login"
|
||||
echo " • Default user: admin"
|
||||
echo " • Check logs for initial password:"
|
||||
echo " sudo journalctl -u betterdesk.service | grep 'Admin password'"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
echo " • HBBS: sudo systemctl status rustdesksignal.service"
|
||||
echo " • HBBR: sudo systemctl status rustdeskrelay.service"
|
||||
@@ -999,17 +1065,29 @@ show_summary() {
|
||||
echo " • $HBBS_API_PORT - HTTP API"
|
||||
echo ""
|
||||
|
||||
echo "New features in v1.5.0:"
|
||||
echo " ✓ User authentication with bcrypt password hashing"
|
||||
echo " ✓ Role-based access control (Admin/Operator/Viewer)"
|
||||
echo " ✓ Sidebar navigation (Dashboard, Public Key, Settings, Users, About)"
|
||||
echo " ✓ Password-protected public key access"
|
||||
echo " ✓ User management panel (admin only)"
|
||||
echo " ✓ Password change functionality"
|
||||
echo " ✓ CSRF protection and rate limiting"
|
||||
echo " ✓ Audit logging for all actions"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " • View HBBS logs: sudo journalctl -u rustdesksignal -f"
|
||||
echo " • View console logs: sudo journalctl -u betterdesk -f"
|
||||
echo " • Restart HBBS: sudo systemctl restart rustdesksignal"
|
||||
echo " • Restart console: sudo systemctl restart betterdesk"
|
||||
echo " • View admin pass: sudo journalctl -u betterdesk | grep 'Admin password'"
|
||||
echo ""
|
||||
|
||||
print_info "Enjoy your enhanced RustDesk experience!"
|
||||
print_info "🎉 Enjoy your enhanced RustDesk experience!"
|
||||
echo ""
|
||||
echo "For support and documentation:"
|
||||
echo " • GitHub: https://github.com/UNITRONIX/Rustdesk-FreeConsole"
|
||||
echo " • Issues: https://github.com/UNITRONIX/Rustdesk-FreeConsole/issues"
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# BetterDesk Console - Installation Script v8
|
||||
#
|
||||
# This script installs the enhanced RustDesk HBBS/HBBR servers with
|
||||
# bidirectional ban enforcement and web management console.
|
||||
#
|
||||
# Features:
|
||||
# - Automatic backup of existing RustDesk installation
|
||||
# - Precompiled HBBS/HBBR binaries with ban enforcement (no compilation needed)
|
||||
# - Bidirectional ban checking (source + target devices)
|
||||
# - Installs Flask web console with glassmorphism UI
|
||||
# - Configures systemd services
|
||||
# - Uses Google Material Icons (offline)
|
||||
#
|
||||
# Ban Enforcement Features (v8):
|
||||
# - Prevents banned devices from initiating connections (source check)
|
||||
# - Prevents connections to banned devices (target check)
|
||||
# - Real-time database sync
|
||||
# - Works for both P2P and relay connections
|
||||
#
|
||||
# Author: GitHub Copilot
|
||||
# License: MIT
|
||||
#############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
RUSTDESK_DIR="/opt/rustdesk"
|
||||
BACKUP_DIR="/opt/rustdesk-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
CONSOLE_DIR="/opt/BetterDeskConsole"
|
||||
TEMP_DIR="/tmp/betterdesk-install"
|
||||
HBBS_API_PORT=21114
|
||||
VERSION="v8" # Current version with bidirectional ban enforcement
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}\n"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
print_header "Checking Dependencies"
|
||||
|
||||
local missing_deps=()
|
||||
|
||||
# Check for required commands (removed cargo - using precompiled binaries)
|
||||
for cmd in python3 pip3 curl systemctl; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
missing_deps+=($cmd)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing_deps[@]} -ne 0 ]; then
|
||||
print_error "Missing dependencies: ${missing_deps[*]}"
|
||||
echo ""
|
||||
echo "Please install missing dependencies:"
|
||||
echo " Ubuntu/Debian: sudo apt install python3 python3-pip curl systemd"
|
||||
echo " CentOS/RHEL: sudo yum install python3 python3-pip curl systemd"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "All dependencies found"
|
||||
}
|
||||
|
||||
backup_rustdesk() {
|
||||
print_header "Backing Up Existing RustDesk Installation"
|
||||
|
||||
if [ ! -d "$RUSTDESK_DIR" ]; then
|
||||
print_warning "No existing RustDesk installation found at $RUSTDESK_DIR"
|
||||
print_info "Will proceed with fresh installation"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Found existing RustDesk installation${NC}"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " 1) Create automatic backup to $BACKUP_DIR"
|
||||
echo " 2) I have already created a manual backup"
|
||||
echo " 3) Skip backup (not recommended)"
|
||||
echo ""
|
||||
read -p "Choose option [1-3]: " backup_choice
|
||||
|
||||
case $backup_choice in
|
||||
1)
|
||||
print_info "Creating backup..."
|
||||
cp -r "$RUSTDESK_DIR" "$BACKUP_DIR"
|
||||
print_success "Backup created at: $BACKUP_DIR"
|
||||
;;
|
||||
2)
|
||||
print_info "Using manual backup"
|
||||
;;
|
||||
3)
|
||||
print_warning "Skipping backup - YOU ARE RESPONSIBLE FOR ANY DATA LOSS"
|
||||
read -p "Are you SURE? Type 'yes' to continue: " confirm
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
print_error "Installation cancelled"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
print_error "Invalid option"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_binaries() {
|
||||
print_header "Installing Enhanced HBBS/HBBR $VERSION"
|
||||
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local bin_dir="$script_dir/hbbs-patch/bin"
|
||||
|
||||
if [ ! -f "$bin_dir/hbbs-$VERSION" ] || [ ! -f "$bin_dir/hbbr-$VERSION" ]; then
|
||||
print_error "Precompiled binaries not found in: $bin_dir"
|
||||
print_info "Expected files: hbbs-$VERSION, hbbr-$VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create RustDesk directory if it doesn't exist
|
||||
mkdir -p "$RUSTDESK_DIR"
|
||||
|
||||
# Stop existing services
|
||||
print_info "Stopping RustDesk services..."
|
||||
systemctl stop rustdesksignal.service 2>/dev/null || true
|
||||
systemctl stop rustdeskrelay.service 2>/dev/null || true
|
||||
pkill -9 hbbs 2>/dev/null || true
|
||||
pkill -9 hbbr 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Backup existing binaries
|
||||
if [ -f "$RUSTDESK_DIR/hbbs" ]; then
|
||||
print_info "Backing up old hbbs..."
|
||||
cp "$RUSTDESK_DIR/hbbs" "$RUSTDESK_DIR/hbbs.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
if [ -f "$RUSTDESK_DIR/hbbr" ]; then
|
||||
print_info "Backing up old hbbr..."
|
||||
cp "$RUSTDESK_DIR/hbbr" "$RUSTDESK_DIR/hbbr.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
# Install new binaries
|
||||
print_info "Installing HBBS $VERSION (with bidirectional ban enforcement)..."
|
||||
cp "$bin_dir/hbbs-$VERSION" "$RUSTDESK_DIR/hbbs"
|
||||
chmod +x "$RUSTDESK_DIR/hbbs"
|
||||
|
||||
print_info "Installing HBBR $VERSION..."
|
||||
cp "$bin_dir/hbbr-$VERSION" "$RUSTDESK_DIR/hbbr"
|
||||
chmod +x "$RUSTDESK_DIR/hbbr"
|
||||
|
||||
print_success "Binaries installed successfully"
|
||||
|
||||
# Restart services
|
||||
print_info "Restarting RustDesk services..."
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
systemctl start rustdesksignal.service 2>/dev/null || true
|
||||
systemctl start rustdeskrelay.service 2>/dev/null || true
|
||||
|
||||
# Wait for services to start
|
||||
sleep 3
|
||||
|
||||
# Verify services
|
||||
local services_ok=true
|
||||
if systemctl is-active --quiet rustdesksignal.service; then
|
||||
print_success "HBBS service is running"
|
||||
else
|
||||
print_warning "HBBS service not running (may need manual start)"
|
||||
services_ok=false
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet rustdeskrelay.service 2>/dev/null; then
|
||||
print_success "HBBR service is running"
|
||||
else
|
||||
print_info "HBBR service not configured (optional)"
|
||||
fi
|
||||
|
||||
# Display version info
|
||||
echo ""
|
||||
print_info "HBBS/HBBR version: $VERSION"
|
||||
print_info "Features:"
|
||||
echo " ✓ Bidirectional ban enforcement"
|
||||
echo " ✓ Source device ban check (prevents banned devices from initiating connections)"
|
||||
echo " ✓ Target device ban check (prevents connections to banned devices)"
|
||||
echo " ✓ Real-time ban database sync"
|
||||
echo ""
|
||||
}
|
||||
|
||||
clone_rustdesk_server() {
|
||||
# This function is no longer needed - using precompiled binaries
|
||||
print_info "Using precompiled binaries - skipping source clone"
|
||||
}
|
||||
|
||||
apply_patches() {
|
||||
# This function is no longer needed - binaries are pre-patched
|
||||
print_info "Binaries are pre-patched - skipping patch application"
|
||||
}
|
||||
|
||||
compile_hbbs() {
|
||||
# This function is no longer needed - using precompiled binaries
|
||||
print_info "Using precompiled binaries - skipping compilation"
|
||||
}
|
||||
|
||||
install_hbbs() {
|
||||
# This function has been replaced by install_binaries()
|
||||
# Kept for compatibility but redirects to new function
|
||||
print_info "Redirecting to install_binaries()..."
|
||||
}
|
||||
|
||||
run_database_migrations() {
|
||||
print_header "Running Database Migrations"
|
||||
|
||||
local db_path="$RUSTDESK_DIR/db_v2.sqlite3"
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local migrations_dir="$script_dir/migrations"
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "$db_path" ]; then
|
||||
print_warning "Database not found at $db_path"
|
||||
print_info "Database will be created automatically when HBBS starts"
|
||||
print_info "Skipping migrations - they will be applied on first run"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create backup of database
|
||||
local backup_file="$db_path.backup-$(date +%Y%m%d-%H%M%S)"
|
||||
print_info "Creating database backup..."
|
||||
cp "$db_path" "$backup_file"
|
||||
print_success "Database backed up to: $backup_file"
|
||||
|
||||
# Check if migrations directory exists
|
||||
if [ ! -d "$migrations_dir" ]; then
|
||||
print_error "Migrations directory not found: $migrations_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run v1.0.1 migration (soft delete)
|
||||
print_info "Running migration v1.0.1 (soft delete)..."
|
||||
if python3 "$migrations_dir/v1.0.1_soft_delete.py"; then
|
||||
print_success "Migration v1.0.1 completed successfully"
|
||||
else
|
||||
print_warning "Migration v1.0.1 failed or already applied"
|
||||
fi
|
||||
|
||||
# Run v1.1.0 migration (device bans)
|
||||
print_info "Running migration v1.1.0 (device bans)..."
|
||||
if python3 "$migrations_dir/v1.1.0_device_bans.py"; then
|
||||
print_success "Migration v1.1.0 completed successfully"
|
||||
else
|
||||
print_warning "Migration v1.1.0 failed or already applied"
|
||||
fi
|
||||
|
||||
print_success "Database migrations completed"
|
||||
echo ""
|
||||
}
|
||||
|
||||
install_web_console() {
|
||||
print_header "Installing Web Management Console"
|
||||
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local web_dir="$script_dir/web"
|
||||
|
||||
if [ ! -d "$web_dir" ]; then
|
||||
print_error "Web directory not found: $web_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create console directory
|
||||
mkdir -p "$CONSOLE_DIR"
|
||||
|
||||
# Copy files
|
||||
print_info "Copying web console files..."
|
||||
cp -r "$web_dir"/* "$CONSOLE_DIR/"
|
||||
|
||||
# Install Python dependencies
|
||||
print_info "Installing Python dependencies..."
|
||||
pip3 install -r "$CONSOLE_DIR/requirements.txt"
|
||||
|
||||
# Create systemd service
|
||||
print_info "Creating systemd service..."
|
||||
cat > /etc/systemd/system/betterdesk.service <<EOF
|
||||
[Unit]
|
||||
Description=BetterDesk Console - RustDesk Web Management
|
||||
After=network.target rustdesksignal.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$CONSOLE_DIR
|
||||
ExecStart=/usr/bin/python3 $CONSOLE_DIR/app.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Enable and start service
|
||||
systemctl daemon-reload
|
||||
systemctl enable betterdesk.service
|
||||
systemctl start betterdesk.service
|
||||
|
||||
# Wait for service to be ready
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet betterdesk.service; then
|
||||
print_success "Web console service is running"
|
||||
else
|
||||
print_error "Web console service failed to start"
|
||||
systemctl status betterdesk.service
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_installation() {
|
||||
print_header "Testing Installation"
|
||||
|
||||
# Test HBBS API
|
||||
print_info "Testing HBBS HTTP API..."
|
||||
if curl -s "http://localhost:$HBBS_API_PORT/api/health" | grep -q "success"; then
|
||||
print_success "HBBS API is responding"
|
||||
else
|
||||
print_error "HBBS API is not responding"
|
||||
fi
|
||||
|
||||
# Test Web Console
|
||||
print_info "Testing Web Console..."
|
||||
if curl -s "http://localhost:5000" > /dev/null; then
|
||||
print_success "Web Console is accessible"
|
||||
else
|
||||
print_error "Web Console is not accessible"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
print_header "Cleaning Up"
|
||||
|
||||
print_info "Removing temporary files..."
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
print_success "Cleanup completed"
|
||||
}
|
||||
|
||||
show_summary() {
|
||||
print_header "Installation Complete!"
|
||||
|
||||
echo -e "${GREEN}BetterDesk Console has been successfully installed!${NC}"
|
||||
echo ""
|
||||
echo "Access points:"
|
||||
echo " • Web Console: http://$(hostname -I | awk '{print $1}'):5000"
|
||||
echo " • HBBS API: http://localhost:$HBBS_API_PORT/api/health"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
echo " • HBBS: sudo systemctl status rustdesksignal.service"
|
||||
echo " • Web Console: sudo systemctl status betterdesk.service"
|
||||
echo ""
|
||||
|
||||
if [ -d "$BACKUP_DIR" ]; then
|
||||
echo "Backup location:"
|
||||
echo " • $BACKUP_DIR"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Documentation:"
|
||||
echo " • README.md in the installation directory"
|
||||
echo " • GitHub: https://github.com/UNITRONIX/Rustdesk-FreeConsole"
|
||||
echo ""
|
||||
|
||||
print_info "Enjoy your enhanced RustDesk experience!"
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
main() {
|
||||
clear
|
||||
print_header "BetterDesk Console Installer $VERSION"
|
||||
echo "This script will install:"
|
||||
echo " • Enhanced RustDesk HBBS/HBBR with bidirectional ban enforcement"
|
||||
echo " • Web Management Console with Material Design"
|
||||
echo " • Real-time device status monitoring"
|
||||
echo ""
|
||||
echo "Installation method: Precompiled binaries (no compilation required)"
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
check_dependencies
|
||||
backup_rustdesk
|
||||
install_binaries
|
||||
run_database_migrations
|
||||
install_web_console
|
||||
test_installation
|
||||
cleanup
|
||||
show_summary
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database Migration Script v1.4.0 - Authentication System
|
||||
Adds user management, sessions, and audit logging to BetterDesk Console
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import os
|
||||
import secrets
|
||||
import bcrypt
|
||||
from datetime import datetime
|
||||
|
||||
DB_PATH = '/opt/rustdesk/db_v2.sqlite3'
|
||||
BACKUP_SUFFIX = '.backup-pre-v1.4.0'
|
||||
|
||||
# Default admin credentials
|
||||
DEFAULT_ADMIN_USERNAME = 'admin'
|
||||
DEFAULT_ADMIN_PASSWORD = secrets.token_urlsafe(12) # Random password
|
||||
|
||||
|
||||
def backup_database():
|
||||
"""Create backup of database before migration"""
|
||||
backup_path = DB_PATH + BACKUP_SUFFIX
|
||||
|
||||
if os.path.exists(backup_path):
|
||||
print(f"⚠️ Backup already exists: {backup_path}")
|
||||
response = input("Overwrite? [y/N]: ").strip().lower()
|
||||
if response != 'y':
|
||||
print("❌ Migration cancelled")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📦 Creating backup: {backup_path}")
|
||||
|
||||
# Copy database file
|
||||
import shutil
|
||||
shutil.copy2(DB_PATH, backup_path)
|
||||
|
||||
print(f"✅ Backup created successfully")
|
||||
return backup_path
|
||||
|
||||
|
||||
def check_if_migration_needed(conn):
|
||||
"""Check if migration was already applied"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if users table exists
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='users'
|
||||
""")
|
||||
|
||||
if cursor.fetchone():
|
||||
print("ℹ️ Migration already applied (users table exists)")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def apply_migration(conn):
|
||||
"""Apply migration SQL"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("🔧 Creating users table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
|
||||
created_at DATETIME NOT NULL,
|
||||
last_login DATETIME,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
CHECK (role IN ('admin', 'operator', 'viewer'))
|
||||
)
|
||||
''')
|
||||
|
||||
print("🔧 Creating sessions table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token VARCHAR(64) PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
last_activity DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
print("🔧 Creating audit_log table...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
device_id VARCHAR(100),
|
||||
details TEXT,
|
||||
ip_address VARCHAR(50),
|
||||
timestamp DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
''')
|
||||
|
||||
print("🔧 Creating indexes...")
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_audit_device ON audit_log(device_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp)')
|
||||
|
||||
conn.commit()
|
||||
print("✅ Database schema updated")
|
||||
|
||||
|
||||
def create_default_admin(conn):
|
||||
"""Create default admin user"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if admin already exists
|
||||
cursor.execute("SELECT id FROM users WHERE username = ?", (DEFAULT_ADMIN_USERNAME,))
|
||||
if cursor.fetchone():
|
||||
print(f"ℹ️ Admin user '{DEFAULT_ADMIN_USERNAME}' already exists")
|
||||
return None
|
||||
|
||||
print(f"👤 Creating default admin user...")
|
||||
|
||||
# Hash password
|
||||
salt = bcrypt.gensalt()
|
||||
password_hash = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), salt).decode('utf-8')
|
||||
|
||||
# Insert admin user
|
||||
cursor.execute('''
|
||||
INSERT INTO users (username, password_hash, role, created_at, is_active)
|
||||
VALUES (?, ?, 'admin', ?, 1)
|
||||
''', (DEFAULT_ADMIN_USERNAME, password_hash, datetime.now()))
|
||||
|
||||
conn.commit()
|
||||
|
||||
print(f"✅ Default admin user created")
|
||||
return DEFAULT_ADMIN_PASSWORD
|
||||
|
||||
|
||||
def save_credentials(password):
|
||||
"""Save admin credentials to file"""
|
||||
creds_file = '/opt/BetterDeskConsole/admin_credentials.txt'
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(creds_file), exist_ok=True)
|
||||
|
||||
with open(creds_file, 'w') as f:
|
||||
f.write("=" * 60 + "\n")
|
||||
f.write("BetterDesk Console - Default Admin Credentials\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
f.write(f"Username: {DEFAULT_ADMIN_USERNAME}\n")
|
||||
f.write(f"Password: {password}\n\n")
|
||||
f.write("⚠️ IMPORTANT: Change this password immediately after first login!\n")
|
||||
f.write("=" * 60 + "\n")
|
||||
f.write(f"Created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
|
||||
os.chmod(creds_file, 0o600) # Read/write for owner only
|
||||
|
||||
print(f"📝 Credentials saved to: {creds_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not save credentials file: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("BetterDesk Console - Database Migration v1.4.0")
|
||||
print("Adding Authentication System")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Check if database exists
|
||||
if not os.path.exists(DB_PATH):
|
||||
print(f"❌ Database not found: {DB_PATH}")
|
||||
print(" Please run the installer first")
|
||||
sys.exit(1)
|
||||
|
||||
# Create backup
|
||||
backup_path = backup_database()
|
||||
|
||||
try:
|
||||
# Connect to database
|
||||
print(f"🔌 Connecting to database: {DB_PATH}")
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Check if migration needed
|
||||
if not check_if_migration_needed(conn):
|
||||
conn.close()
|
||||
print("\n✅ Database is already up to date")
|
||||
sys.exit(0)
|
||||
|
||||
# Apply migration
|
||||
print("\n🚀 Starting migration...")
|
||||
apply_migration(conn)
|
||||
|
||||
# Create default admin
|
||||
admin_password = create_default_admin(conn)
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ Migration completed successfully!")
|
||||
print("=" * 60)
|
||||
|
||||
if admin_password:
|
||||
print("\n🔐 DEFAULT ADMIN CREDENTIALS:")
|
||||
print("=" * 60)
|
||||
print(f" Username: {DEFAULT_ADMIN_USERNAME}")
|
||||
print(f" Password: {admin_password}")
|
||||
print("=" * 60)
|
||||
print("\n⚠️ IMPORTANT:")
|
||||
print(" 1. Save these credentials in a secure location")
|
||||
print(" 2. Change the password immediately after first login")
|
||||
print(" 3. Delete /opt/BetterDeskConsole/admin_credentials.txt after saving")
|
||||
print()
|
||||
|
||||
save_credentials(admin_password)
|
||||
|
||||
print(f"\n📦 Backup location: {backup_path}")
|
||||
print(" Keep this backup until you verify everything works correctly")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Migration failed: {e}")
|
||||
print(f"\n🔄 Restoring from backup...")
|
||||
|
||||
try:
|
||||
conn.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Restore backup
|
||||
import shutil
|
||||
shutil.copy2(backup_path, DB_PATH)
|
||||
|
||||
print(f"✅ Database restored from backup")
|
||||
print(f" Original backup preserved at: {backup_path}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,456 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# RustDesk Key Repair Tool
|
||||
#
|
||||
# This script helps diagnose and fix key-related issues in RustDesk
|
||||
# installations. Use it when experiencing "Key mismatch" errors.
|
||||
#
|
||||
# Author: UNITRONIX
|
||||
# License: MIT
|
||||
#############################################################################
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
RUSTDESK_DIR="/opt/rustdesk"
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}\n"
|
||||
}
|
||||
|
||||
print_success() { echo -e "${GREEN}✓ $1${NC}"; }
|
||||
print_error() { echo -e "${RED}✗ $1${NC}"; }
|
||||
print_warning() { echo -e "${YELLOW}⚠ $1${NC}"; }
|
||||
print_info() { echo -e "${BLUE}ℹ $1${NC}"; }
|
||||
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_rustdesk_directory() {
|
||||
if [ ! -d "$RUSTDESK_DIR" ]; then
|
||||
print_warning "Default RustDesk directory not found: $RUSTDESK_DIR"
|
||||
read -p "Enter your RustDesk installation directory: " custom_dir
|
||||
if [ -d "$custom_dir" ]; then
|
||||
RUSTDESK_DIR="$custom_dir"
|
||||
print_success "Using directory: $RUSTDESK_DIR"
|
||||
else
|
||||
print_error "Directory not found: $custom_dir"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_success "Found RustDesk directory: $RUSTDESK_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
show_key_info() {
|
||||
print_header "Current Key Information"
|
||||
|
||||
echo "📁 Directory: $RUSTDESK_DIR"
|
||||
echo ""
|
||||
|
||||
# List all key files with details
|
||||
if ls "$RUSTDESK_DIR"/*.pub &>/dev/null; then
|
||||
echo "🔑 Public key files (.pub):"
|
||||
for pubfile in "$RUSTDESK_DIR"/*.pub; do
|
||||
local size=$(stat -f%z "$pubfile" 2>/dev/null || stat -c%s "$pubfile" 2>/dev/null)
|
||||
local modified=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$pubfile" 2>/dev/null || stat -c "%y" "$pubfile" 2>/dev/null | cut -d'.' -f1)
|
||||
echo " ├─ $(basename $pubfile)"
|
||||
echo " │ ├─ Size: $size bytes"
|
||||
echo " │ ├─ Modified: $modified"
|
||||
echo " │ └─ Content:"
|
||||
cat "$pubfile" | sed 's/^/ │ /'
|
||||
echo ""
|
||||
done
|
||||
else
|
||||
print_warning "No .pub files found!"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check for private keys
|
||||
if ls "$RUSTDESK_DIR"/id_* 2>/dev/null | grep -v ".pub" &>/dev/null; then
|
||||
echo "🔐 Private key files:"
|
||||
for keyfile in "$RUSTDESK_DIR"/id_*; do
|
||||
if [[ ! "$keyfile" =~ \.pub$ ]] && [ -f "$keyfile" ]; then
|
||||
local size=$(stat -f%z "$keyfile" 2>/dev/null || stat -c%s "$keyfile" 2>/dev/null)
|
||||
local perms=$(stat -f "%Sp" "$keyfile" 2>/dev/null || stat -c "%a" "$keyfile" 2>/dev/null)
|
||||
echo " ├─ $(basename $keyfile)"
|
||||
echo " │ ├─ Size: $size bytes"
|
||||
echo " │ └─ Permissions: $perms"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check backups
|
||||
if ls "$RUSTDESK_DIR"/*.backup* &>/dev/null || ls "$RUSTDESK_DIR"/*-backup-* &>/dev/null; then
|
||||
echo "💾 Backup files found:"
|
||||
ls -lh "$RUSTDESK_DIR"/*.backup* "$RUSTDESK_DIR"/*-backup-* 2>/dev/null | awk '{print " ├─ " $9 " (" $5 ")"}'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check for backup directories
|
||||
if ls -d /opt/rustdesk-backup-* &>/dev/null; then
|
||||
echo "📦 Backup directories:"
|
||||
for backup_dir in /opt/rustdesk-backup-*; do
|
||||
echo " ├─ $backup_dir"
|
||||
if ls "$backup_dir"/*.pub &>/dev/null; then
|
||||
echo " │ └─ Contains .pub files ✓"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
verify_key_permissions() {
|
||||
print_header "Verifying Key Permissions"
|
||||
|
||||
local fixed=0
|
||||
|
||||
# Fix private key permissions (should be 600)
|
||||
for keyfile in "$RUSTDESK_DIR"/id_*; do
|
||||
if [[ ! "$keyfile" =~ \.pub$ ]] && [ -f "$keyfile" ]; then
|
||||
local current_perms=$(stat -f "%Sp" "$keyfile" 2>/dev/null || stat -c "%a" "$keyfile" 2>/dev/null)
|
||||
if [ "$current_perms" != "600" ] && [ "$current_perms" != "-rw-------" ]; then
|
||||
print_warning "Fixing permissions for $(basename $keyfile): $current_perms → 600"
|
||||
chmod 600 "$keyfile"
|
||||
((fixed++))
|
||||
else
|
||||
print_success "$(basename $keyfile): Permissions OK ($current_perms)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Fix public key permissions (should be 644)
|
||||
for pubfile in "$RUSTDESK_DIR"/*.pub; do
|
||||
if [ -f "$pubfile" ]; then
|
||||
local current_perms=$(stat -f "%Sp" "$pubfile" 2>/dev/null || stat -c "%a" "$pubfile" 2>/dev/null)
|
||||
if [ "$current_perms" != "644" ] && [ "$current_perms" != "-rw-r--r--" ]; then
|
||||
print_warning "Fixing permissions for $(basename $pubfile): $current_perms → 644"
|
||||
chmod 644 "$pubfile"
|
||||
((fixed++))
|
||||
else
|
||||
print_success "$(basename $pubfile): Permissions OK ($current_perms)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $fixed -gt 0 ]; then
|
||||
print_success "Fixed permissions for $fixed file(s)"
|
||||
else
|
||||
print_success "All permissions are correct"
|
||||
fi
|
||||
}
|
||||
|
||||
export_public_key() {
|
||||
print_header "Export Public Key"
|
||||
|
||||
local pub_files=("$RUSTDESK_DIR"/*.pub)
|
||||
|
||||
if [ ! -f "${pub_files[0]}" ]; then
|
||||
print_error "No public key files found!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Available public keys:"
|
||||
local i=1
|
||||
for pubfile in "$RUSTDESK_DIR"/*.pub; do
|
||||
if [ -f "$pubfile" ]; then
|
||||
echo " $i) $(basename $pubfile)"
|
||||
((i++))
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
if [ $i -eq 2 ]; then
|
||||
# Only one file, use it automatically
|
||||
pubfile="${pub_files[0]}"
|
||||
print_info "Using: $(basename $pubfile)"
|
||||
else
|
||||
read -p "Select key to export [1-$((i-1))]: " choice
|
||||
pubfile="${pub_files[$((choice-1))]}"
|
||||
fi
|
||||
|
||||
if [ ! -f "$pubfile" ]; then
|
||||
print_error "Invalid selection"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo "PUBLIC KEY (copy this to RustDesk clients):"
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
cat "$pubfile"
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Offer to save to file
|
||||
read -p "Save to file? [y/N]: " save_choice
|
||||
if [[ "$save_choice" =~ ^[Yy]$ ]]; then
|
||||
local export_file="$HOME/rustdesk_public_key_$(date +%Y%m%d_%H%M%S).txt"
|
||||
cat "$pubfile" > "$export_file"
|
||||
print_success "Saved to: $export_file"
|
||||
fi
|
||||
}
|
||||
|
||||
regenerate_keys() {
|
||||
print_header "Regenerate Keys"
|
||||
|
||||
echo -e "${RED}⚠️ WARNING: REGENERATING KEYS WILL BREAK ALL CLIENT CONNECTIONS ⚠️${NC}"
|
||||
echo ""
|
||||
echo "After regeneration, you MUST:"
|
||||
echo " 1. Stop and restart RustDesk services"
|
||||
echo " 2. Update ALL client configurations with new public key"
|
||||
echo " 3. Reconfigure each RustDesk client individually"
|
||||
echo ""
|
||||
echo "Impact:"
|
||||
echo " • All existing clients will be unable to connect"
|
||||
echo " • 'Key mismatch' errors will appear on all devices"
|
||||
echo " • Manual reconfiguration required for each client"
|
||||
echo ""
|
||||
read -p "Are you ABSOLUTELY SURE? Type 'REGENERATE' to confirm: " confirm
|
||||
|
||||
if [ "$confirm" != "REGENERATE" ]; then
|
||||
print_info "Operation cancelled"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Backup existing keys
|
||||
print_info "Creating backup of existing keys..."
|
||||
local backup_suffix="pre-regenerate-$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
for keyfile in "$RUSTDESK_DIR"/id_ed25519*; do
|
||||
if [ -f "$keyfile" ]; then
|
||||
cp "$keyfile" "$keyfile.$backup_suffix"
|
||||
print_success "Backed up: $(basename $keyfile)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Stop services
|
||||
print_info "Stopping RustDesk services..."
|
||||
systemctl stop rustdesksignal.service 2>/dev/null || true
|
||||
systemctl stop rustdeskrelay.service 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Remove old keys
|
||||
print_info "Removing old keys..."
|
||||
rm -f "$RUSTDESK_DIR/id_ed25519" "$RUSTDESK_DIR/id_ed25519.pub"
|
||||
|
||||
# Generate new keys
|
||||
print_info "Generating new ED25519 key pair..."
|
||||
if command -v ssh-keygen &>/dev/null; then
|
||||
ssh-keygen -t ed25519 -f "$RUSTDESK_DIR/id_ed25519" -N "" -C "rustdesk-server-$(date +%Y%m%d)"
|
||||
|
||||
if [ -f "$RUSTDESK_DIR/id_ed25519.pub" ]; then
|
||||
print_success "✓ New keys generated successfully!"
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo "NEW PUBLIC KEY (configure this in ALL RustDesk clients):"
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
cat "$RUSTDESK_DIR/id_ed25519.pub"
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Save to file
|
||||
local key_file="$HOME/NEW_RUSTDESK_KEY_$(date +%Y%m%d_%H%M%S).txt"
|
||||
cat "$RUSTDESK_DIR/id_ed25519.pub" > "$key_file"
|
||||
print_success "Key saved to: $key_file"
|
||||
|
||||
# Fix permissions
|
||||
chmod 600 "$RUSTDESK_DIR/id_ed25519"
|
||||
chmod 644 "$RUSTDESK_DIR/id_ed25519.pub"
|
||||
|
||||
# Restart services
|
||||
print_info "Starting RustDesk services..."
|
||||
systemctl start rustdesksignal.service
|
||||
systemctl start rustdeskrelay.service
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet rustdesksignal.service; then
|
||||
print_success "✓ Services restarted successfully"
|
||||
else
|
||||
print_error "Failed to restart services - check logs"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to generate keys"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_error "ssh-keygen not found - cannot generate keys"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_warning "⚠️ NEXT STEPS:"
|
||||
echo " 1. Copy the new public key above"
|
||||
echo " 2. Open RustDesk on each client device"
|
||||
echo " 3. Go to Settings → ID/Relay Server"
|
||||
echo " 4. Paste the new public key"
|
||||
echo " 5. Save and test connection"
|
||||
}
|
||||
|
||||
restore_from_backup() {
|
||||
print_header "Restore Keys from Backup"
|
||||
|
||||
# Find backups
|
||||
local backups=($(ls -d /opt/rustdesk-backup-* 2>/dev/null))
|
||||
local key_backups=($(ls "$RUSTDESK_DIR"/*.backup* "$RUSTDESK_DIR"/*-backup-* 2>/dev/null))
|
||||
|
||||
if [ ${#backups[@]} -eq 0 ] && [ ${#key_backups[@]} -eq 0 ]; then
|
||||
print_error "No backups found!"
|
||||
echo ""
|
||||
echo "Checked locations:"
|
||||
echo " • /opt/rustdesk-backup-*"
|
||||
echo " • $RUSTDESK_DIR/*.backup*"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Available backups:"
|
||||
echo ""
|
||||
|
||||
local options=()
|
||||
local i=1
|
||||
|
||||
# List directory backups
|
||||
for backup in "${backups[@]}"; do
|
||||
if [ -d "$backup" ]; then
|
||||
echo " $i) Directory backup: $(basename $backup)"
|
||||
if ls "$backup"/*.pub &>/dev/null; then
|
||||
echo " Contains: $(ls "$backup"/*.pub | wc -l) public key file(s)"
|
||||
fi
|
||||
options+=("$backup")
|
||||
((i++))
|
||||
fi
|
||||
done
|
||||
|
||||
# List key file backups
|
||||
for backup in "${key_backups[@]}"; do
|
||||
if [ -f "$backup" ]; then
|
||||
echo " $i) Key file: $(basename $backup)"
|
||||
options+=("$backup")
|
||||
((i++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
read -p "Select backup to restore [1-$((i-1))]: " choice
|
||||
|
||||
if [ $choice -lt 1 ] || [ $choice -ge $i ]; then
|
||||
print_error "Invalid selection"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local selected="${options[$((choice-1))]}"
|
||||
|
||||
echo ""
|
||||
print_warning "This will restore keys from:"
|
||||
echo " $selected"
|
||||
read -p "Continue? [y/N]: " confirm
|
||||
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
print_info "Operation cancelled"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Stop services
|
||||
print_info "Stopping services..."
|
||||
systemctl stop rustdesksignal.service 2>/dev/null || true
|
||||
systemctl stop rustdeskrelay.service 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Restore
|
||||
if [ -d "$selected" ]; then
|
||||
# Restore from directory
|
||||
print_info "Restoring from directory backup..."
|
||||
cp "$selected"/id_ed25519* "$RUSTDESK_DIR/" 2>/dev/null || true
|
||||
cp "$selected"/*.pub "$RUSTDESK_DIR/" 2>/dev/null || true
|
||||
else
|
||||
# Restore individual file
|
||||
print_info "Restoring key file..."
|
||||
local original_name=$(echo "$selected" | sed 's/\.[^.]*$//' | sed 's/-backup-[0-9]*$//')
|
||||
cp "$selected" "$original_name"
|
||||
fi
|
||||
|
||||
# Fix permissions
|
||||
chmod 600 "$RUSTDESK_DIR"/id_ed25519 2>/dev/null || true
|
||||
chmod 644 "$RUSTDESK_DIR"/*.pub 2>/dev/null || true
|
||||
|
||||
# Restart services
|
||||
print_info "Starting services..."
|
||||
systemctl start rustdesksignal.service
|
||||
systemctl start rustdeskrelay.service
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet rustdesksignal.service; then
|
||||
print_success "✓ Keys restored and services restarted!"
|
||||
echo ""
|
||||
echo "Current public key:"
|
||||
cat "$RUSTDESK_DIR"/*.pub 2>/dev/null | head -1
|
||||
else
|
||||
print_error "Failed to restart services - check logs"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main menu
|
||||
main() {
|
||||
clear
|
||||
print_header "🔧 RustDesk Key Repair Tool"
|
||||
|
||||
check_root
|
||||
detect_rustdesk_directory
|
||||
|
||||
echo ""
|
||||
echo "What would you like to do?"
|
||||
echo ""
|
||||
echo " 1) 📋 Show current key information"
|
||||
echo " 2) 🔐 Verify and fix key permissions"
|
||||
echo " 3) 📤 Export public key"
|
||||
echo " 4) 🔄 Regenerate keys (⚠️ BREAKS existing connections)"
|
||||
echo " 5) 💾 Restore keys from backup"
|
||||
echo " 6) 🚪 Exit"
|
||||
echo ""
|
||||
read -p "Choose option [1-6]: " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
show_key_info
|
||||
;;
|
||||
2)
|
||||
verify_key_permissions
|
||||
;;
|
||||
3)
|
||||
export_public_key
|
||||
;;
|
||||
4)
|
||||
regenerate_keys
|
||||
;;
|
||||
5)
|
||||
restore_from_backup
|
||||
;;
|
||||
6)
|
||||
print_info "Goodbye!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Invalid option"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press ENTER to exit..."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,460 @@
|
||||
#!/bin/bash
|
||||
|
||||
#############################################################################
|
||||
# BetterDesk Console - Update Script to v1.4.0
|
||||
#
|
||||
# This script updates existing BetterDesk Console installation to v1.4.0
|
||||
# with authentication system, sidebar menu, and security improvements.
|
||||
#
|
||||
# Features:
|
||||
# - Automatic version detection
|
||||
# - Safe database migration with backup
|
||||
# - Updates web console files
|
||||
# - Installs new dependencies (bcrypt, markupsafe)
|
||||
# - Preserves existing configuration
|
||||
# - Creates default admin user
|
||||
#
|
||||
# Author: GitHub Copilot + UNITRONIX
|
||||
# License: MIT
|
||||
#############################################################################
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
CONSOLE_DIR="/opt/BetterDeskConsole"
|
||||
WEB_DIR="$CONSOLE_DIR/web"
|
||||
BACKUP_DIR="/opt/betterdesk-backup-$(date +%Y%m%d-%H%M%S)"
|
||||
CURRENT_VERSION_FILE="$CONSOLE_DIR/VERSION"
|
||||
TARGET_VERSION="1.4.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}\n"
|
||||
}
|
||||
|
||||
print_success() { echo -e "${GREEN}✓ $1${NC}"; }
|
||||
print_error() { echo -e "${RED}✗ $1${NC}"; }
|
||||
print_warning() { echo -e "${YELLOW}⚠ $1${NC}"; }
|
||||
print_info() { echo -e "${BLUE}ℹ $1${NC}"; }
|
||||
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_current_version() {
|
||||
if [ -f "$CURRENT_VERSION_FILE" ]; then
|
||||
CURRENT_VERSION=$(cat "$CURRENT_VERSION_FILE")
|
||||
print_info "Current version: $CURRENT_VERSION"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to detect from files
|
||||
if [ -f "$WEB_DIR/app.py" ]; then
|
||||
if grep -q "require_auth" "$WEB_DIR/app.py"; then
|
||||
CURRENT_VERSION="1.4.0+"
|
||||
elif grep -q "is_banned" "$WEB_DIR/app.py"; then
|
||||
CURRENT_VERSION="1.3.0"
|
||||
else
|
||||
CURRENT_VERSION="1.2.0 or older"
|
||||
fi
|
||||
print_warning "Version file not found. Detected: $CURRENT_VERSION (approximate)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_error "Could not detect current version"
|
||||
return 1
|
||||
}
|
||||
|
||||
check_if_update_needed() {
|
||||
if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then
|
||||
print_info "Already running version $TARGET_VERSION"
|
||||
echo ""
|
||||
read -p "Force re-install? [y/N]: " force
|
||||
if [[ ! "$force" =~ ^[Yy]$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
print_success "Update needed: $CURRENT_VERSION → $TARGET_VERSION"
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
print_header "Creating Backup"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Backup web console
|
||||
if [ -d "$CONSOLE_DIR" ]; then
|
||||
print_info "Backing up web console..."
|
||||
cp -r "$CONSOLE_DIR" "$BACKUP_DIR/BetterDeskConsole"
|
||||
print_success "Web console backed up"
|
||||
fi
|
||||
|
||||
# Backup database
|
||||
if [ -f "/opt/rustdesk/db_v2.sqlite3" ]; then
|
||||
print_info "Backing up database..."
|
||||
cp "/opt/rustdesk/db_v2.sqlite3" "$BACKUP_DIR/db_v2.sqlite3"
|
||||
print_success "Database backed up"
|
||||
fi
|
||||
|
||||
print_success "Backup created at: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
print_header "Installing Dependencies"
|
||||
|
||||
# Detect Python environment
|
||||
PIP_EXTRA_ARGS=""
|
||||
if [ -f "/etc/debian_version" ] && python3 -c "import sys; exit(0 if sys.version_info >= (3,11) else 1)" 2>/dev/null; then
|
||||
PIP_EXTRA_ARGS="--break-system-packages"
|
||||
fi
|
||||
|
||||
# Install bcrypt
|
||||
if python3 -c "import bcrypt" 2>/dev/null; then
|
||||
print_success "bcrypt already installed"
|
||||
else
|
||||
print_info "Installing bcrypt..."
|
||||
pip3 install bcrypt $PIP_EXTRA_ARGS
|
||||
print_success "bcrypt installed"
|
||||
fi
|
||||
|
||||
# Install markupsafe
|
||||
if python3 -c "import markupsafe" 2>/dev/null; then
|
||||
print_success "markupsafe already installed"
|
||||
else
|
||||
print_info "Installing markupsafe..."
|
||||
pip3 install markupsafe $PIP_EXTRA_ARGS
|
||||
print_success "markupsafe installed"
|
||||
fi
|
||||
|
||||
# Install Flask-WTF for CSRF protection
|
||||
if python3 -c "import flask_wtf" 2>/dev/null; then
|
||||
print_success "Flask-WTF already installed"
|
||||
else
|
||||
print_info "Installing Flask-WTF..."
|
||||
pip3 install Flask-WTF $PIP_EXTRA_ARGS
|
||||
print_success "Flask-WTF installed"
|
||||
fi
|
||||
|
||||
# Install Flask-Limiter for rate limiting
|
||||
if python3 -c "import flask_limiter" 2>/dev/null; then
|
||||
print_success "Flask-Limiter already installed"
|
||||
else
|
||||
print_info "Installing Flask-Limiter..."
|
||||
pip3 install Flask-Limiter $PIP_EXTRA_ARGS
|
||||
print_success "Flask-Limiter installed"
|
||||
fi
|
||||
}
|
||||
|
||||
update_web_files() {
|
||||
print_header "Updating Web Console Files"
|
||||
|
||||
# Stop service if running
|
||||
if systemctl is-active --quiet betterdesk 2>/dev/null; then
|
||||
print_info "Stopping BetterDesk service..."
|
||||
systemctl stop betterdesk
|
||||
fi
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$WEB_DIR/templates"
|
||||
mkdir -p "$WEB_DIR/static"
|
||||
|
||||
# Copy new files
|
||||
print_info "Copying authentication module..."
|
||||
cp "$SCRIPT_DIR/web/auth.py" "$WEB_DIR/"
|
||||
|
||||
print_info "Copying updated app.py..."
|
||||
cp "$SCRIPT_DIR/web/app_v14.py" "$WEB_DIR/app.py"
|
||||
|
||||
print_info "Copying login template..."
|
||||
cp "$SCRIPT_DIR/web/templates/login.html" "$WEB_DIR/templates/"
|
||||
|
||||
print_info "Copying updated index template..."
|
||||
cp "$SCRIPT_DIR/web/templates/index_v14.html" "$WEB_DIR/templates/index.html"
|
||||
|
||||
print_info "Copying sidebar styles..."
|
||||
cp "$SCRIPT_DIR/web/static/sidebar.css" "$WEB_DIR/static/"
|
||||
|
||||
print_info "Copying sidebar JavaScript..."
|
||||
cp "$SCRIPT_DIR/web/static/sidebar.js" "$WEB_DIR/static/"
|
||||
|
||||
print_info "Copying updated script.js..."
|
||||
cp "$SCRIPT_DIR/web/static/script_v14.js" "$WEB_DIR/static/script.js"
|
||||
|
||||
# Set permissions
|
||||
chown -R root:root "$WEB_DIR"
|
||||
chmod 755 "$WEB_DIR"
|
||||
chmod 644 "$WEB_DIR"/*.py
|
||||
chmod 644 "$WEB_DIR"/templates/*.html
|
||||
chmod 644 "$WEB_DIR"/static/*
|
||||
|
||||
print_success "Web files updated"
|
||||
}
|
||||
|
||||
run_database_migration() {
|
||||
print_header "Running Database Migration"
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/migrations/v1.4.0_auth_system.py" ]; then
|
||||
print_error "Migration script not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Running migration v1.4.0..."
|
||||
python3 "$SCRIPT_DIR/migrations/v1.4.0_auth_system.py"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Database migration completed"
|
||||
|
||||
# Show admin credentials if generated
|
||||
if [ -f "$CONSOLE_DIR/admin_credentials.txt" ]; then
|
||||
echo ""
|
||||
print_warning "=" * 60
|
||||
print_warning "DEFAULT ADMIN CREDENTIALS GENERATED!"
|
||||
print_warning "=" * 60
|
||||
cat "$CONSOLE_DIR/admin_credentials.txt"
|
||||
echo ""
|
||||
print_warning "⚠️ IMPORTANT: Change the password immediately after first login!"
|
||||
print_warning "⚠️ Delete this file after saving credentials: $CONSOLE_DIR/admin_credentials.txt"
|
||||
fi
|
||||
else
|
||||
print_error "Database migration failed!"
|
||||
print_info "Check the error messages above"
|
||||
print_info "Your backup is at: $BACKUP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_version_file() {
|
||||
echo "$TARGET_VERSION" > "$CURRENT_VERSION_FILE"
|
||||
print_success "Version file updated to $TARGET_VERSION"
|
||||
}
|
||||
|
||||
configure_api_security() {
|
||||
print_header "Configuring API Security"
|
||||
|
||||
# Detect RustDesk directory
|
||||
RUSTDESK_DIR="/opt/rustdesk"
|
||||
if [ ! -d "$RUSTDESK_DIR" ]; then
|
||||
print_warning "RustDesk directory not found, trying alternate locations..."
|
||||
if [ -d "/var/lib/rustdesk" ]; then
|
||||
RUSTDESK_DIR="/var/lib/rustdesk"
|
||||
else
|
||||
print_error "Could not find RustDesk installation directory"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
API_KEY_FILE="$RUSTDESK_DIR/.api_key"
|
||||
|
||||
# Generate API key if it doesn't exist
|
||||
if [ -f "$API_KEY_FILE" ]; then
|
||||
print_info "API key already exists, keeping existing key"
|
||||
API_KEY=$(cat "$API_KEY_FILE")
|
||||
else
|
||||
print_info "Generating new API key..."
|
||||
API_KEY=$(openssl rand -base64 48 | tr -d '/+=' | cut -c1-64)
|
||||
echo -n "$API_KEY" > "$API_KEY_FILE"
|
||||
chmod 600 "$API_KEY_FILE"
|
||||
print_success "API key generated and saved to $API_KEY_FILE"
|
||||
fi
|
||||
|
||||
# Update betterdesk.service with API key environment variable
|
||||
SERVICE_FILE="/etc/systemd/system/betterdesk.service"
|
||||
if [ -f "$SERVICE_FILE" ]; then
|
||||
# Check if HBBS_API_KEY is already configured
|
||||
if grep -q "HBBS_API_KEY" "$SERVICE_FILE"; then
|
||||
print_info "Service file already contains HBBS_API_KEY"
|
||||
else
|
||||
print_info "Adding HBBS_API_KEY to betterdesk.service..."
|
||||
|
||||
# Add environment variable after [Service] section
|
||||
sed -i "/^\[Service\]/a Environment=\"HBBS_API_KEY=$API_KEY\"" "$SERVICE_FILE"
|
||||
|
||||
# Also ensure Flask is configured for LAN access
|
||||
if ! grep -q "FLASK_HOST=0.0.0.0" "$SERVICE_FILE"; then
|
||||
sed -i "/^\[Service\]/a Environment=\"FLASK_HOST=0.0.0.0\"" "$SERVICE_FILE"
|
||||
sed -i "/^\[Service\]/a Environment=\"FLASK_PORT=5000\"" "$SERVICE_FILE"
|
||||
sed -i "/^\[Service\]/a Environment=\"FLASK_DEBUG=False\"" "$SERVICE_FILE"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
print_success "Service file updated with API security configuration"
|
||||
fi
|
||||
else
|
||||
print_warning "betterdesk.service not found - skipping service configuration"
|
||||
fi
|
||||
|
||||
# Update rustdesksignal.service to bind API to LAN (0.0.0.0)
|
||||
HBBS_SERVICE="/etc/systemd/system/rustdesksignal.service"
|
||||
if [ -f "$HBBS_SERVICE" ]; then
|
||||
# Check if API is already configured for LAN access
|
||||
if grep -q "\-\-api-port" "$HBBS_SERVICE"; then
|
||||
print_info "HBBS API port already configured"
|
||||
|
||||
# Check if binding to 0.0.0.0
|
||||
if grep -q "0.0.0.0" "$HBBS_SERVICE"; then
|
||||
print_info "HBBS API already configured for LAN access"
|
||||
else
|
||||
print_warning "HBBS API found but may be localhost-only"
|
||||
print_info "Note: New HBBS binaries bind to 0.0.0.0 by default with API key authentication"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
print_success "API security configuration complete"
|
||||
echo ""
|
||||
print_info "Security notes:"
|
||||
echo " • API key stored in: $API_KEY_FILE"
|
||||
echo " • Web console uses API key for HBBS requests"
|
||||
echo " • HBBS API accessible on LAN with X-API-Key header authentication"
|
||||
echo " • Web console accessible on: http://$(hostname -I | awk '{print $1}'):5000"
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
print_header "Restarting Services"
|
||||
|
||||
# Restart BetterDesk web console
|
||||
if [ -f "/etc/systemd/system/betterdesk.service" ]; then
|
||||
print_info "Restarting BetterDesk service..."
|
||||
systemctl daemon-reload
|
||||
systemctl restart betterdesk
|
||||
systemctl status betterdesk --no-pager
|
||||
print_success "BetterDesk service restarted"
|
||||
else
|
||||
print_warning "BetterDesk service not found (manual start may be needed)"
|
||||
fi
|
||||
}
|
||||
|
||||
show_completion_message() {
|
||||
print_header "Update Complete!"
|
||||
|
||||
echo -e "${GREEN}✅ BetterDesk Console has been updated to v$TARGET_VERSION${NC}"
|
||||
echo ""
|
||||
echo "New Features:"
|
||||
echo " • User authentication with login system"
|
||||
echo " • Role-based access control (admin/operator/viewer)"
|
||||
echo " • Sidebar navigation menu with Users management"
|
||||
echo " • Password-protected public key access"
|
||||
echo " • Audit logging"
|
||||
echo " • API key authentication for HBBS API"
|
||||
echo " • LAN access for web console and API"
|
||||
echo " • Enhanced security across all components"
|
||||
echo ""
|
||||
echo "Security Enhancements:"
|
||||
echo " • Fail-closed policy for ban checks (HIGH)"
|
||||
echo " • CSRF protection with Flask-WTF"
|
||||
echo " • Rate limiting on login (5 per minute)"
|
||||
echo " • Password requirements: 8+ chars, letters + numbers"
|
||||
echo " • Content Security Policy headers"
|
||||
echo " • Secure audit logging"
|
||||
echo ""
|
||||
echo "Access your console:"
|
||||
echo " • Web Console: http://$(hostname -I | awk '{print $1}'):5000"
|
||||
echo " • HBBS API: http://$(hostname -I | awk '{print $1}'):21120/api/health"
|
||||
echo " • See admin credentials above (if new installation)"
|
||||
echo ""
|
||||
echo "Security:"
|
||||
echo " • API key: /opt/rustdesk/.api_key"
|
||||
echo " • All API requests now require X-API-Key header"
|
||||
echo ""
|
||||
echo "Backup location: $BACKUP_DIR"
|
||||
echo "Keep this backup until you verify everything works correctly!"
|
||||
echo ""
|
||||
|
||||
if [ -f "$CONSOLE_DIR/admin_credentials.txt" ]; then
|
||||
echo -e "${YELLOW}⚠️ Don't forget to:${NC}"
|
||||
echo " 1. Login with default credentials"
|
||||
echo " 2. Change the admin password"
|
||||
echo " 3. Delete $CONSOLE_DIR/admin_credentials.txt"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
rollback() {
|
||||
print_error "Update failed! Rolling back..."
|
||||
|
||||
if [ -d "$BACKUP_DIR/BetterDeskConsole" ]; then
|
||||
rm -rf "$CONSOLE_DIR"
|
||||
cp -r "$BACKUP_DIR/BetterDeskConsole" "$CONSOLE_DIR"
|
||||
print_success "Web console restored from backup"
|
||||
fi
|
||||
|
||||
if [ -f "$BACKUP_DIR/db_v2.sqlite3" ]; then
|
||||
cp "$BACKUP_DIR/db_v2.sqlite3" "/opt/rustdesk/db_v2.sqlite3"
|
||||
print_success "Database restored from backup"
|
||||
fi
|
||||
|
||||
# Restart services
|
||||
if [ -f "/etc/systemd/system/betterdesk.service" ]; then
|
||||
systemctl restart betterdesk
|
||||
fi
|
||||
|
||||
print_info "Rollback complete. Your backup is preserved at: $BACKUP_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MAIN EXECUTION
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
print_header "BetterDesk Console Update to v$TARGET_VERSION"
|
||||
|
||||
# Set trap for errors
|
||||
trap rollback ERR
|
||||
|
||||
# Checks
|
||||
check_root
|
||||
|
||||
# Detect current version
|
||||
if ! detect_current_version; then
|
||||
print_error "Installation directory not found: $CONSOLE_DIR"
|
||||
echo ""
|
||||
echo "This script updates existing BetterDesk Console installations."
|
||||
echo "For new installations, use install-improved.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if update needed
|
||||
check_if_update_needed
|
||||
|
||||
# Confirm update
|
||||
echo ""
|
||||
echo "This will update BetterDesk Console from $CURRENT_VERSION to $TARGET_VERSION"
|
||||
echo "A backup will be created before making any changes."
|
||||
echo ""
|
||||
read -p "Continue with update? [y/N]: " confirm
|
||||
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
print_info "Update cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Execute update steps
|
||||
create_backup
|
||||
install_dependencies
|
||||
update_web_files
|
||||
run_database_migration
|
||||
update_version_file
|
||||
configure_api_security
|
||||
restart_services
|
||||
show_completion_message
|
||||
|
||||
# Disable error trap
|
||||
trap - ERR
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -1,441 +0,0 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
import os
|
||||
import requests
|
||||
import re
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configuration
|
||||
DB_PATH = '/opt/rustdesk/db_v2.sqlite3'
|
||||
PUB_KEY_PATH = '/opt/rustdesk/id_ed25519.pub'
|
||||
# API is on localhost-only port 21120 (not exposed to internet)
|
||||
HBBS_API_URL = 'http://localhost:21120/api'
|
||||
|
||||
# Validation rules
|
||||
MAX_NOTE_LENGTH = 500
|
||||
MAX_DEVICE_ID_LENGTH = 50
|
||||
DEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_-]+$')
|
||||
|
||||
def validate_device_id(device_id):
|
||||
"""Validate device ID format and length."""
|
||||
if not device_id:
|
||||
return False, "Device ID cannot be empty"
|
||||
if len(device_id) > MAX_DEVICE_ID_LENGTH:
|
||||
return False, f"Device ID too long (max {MAX_DEVICE_ID_LENGTH} characters)"
|
||||
if not DEVICE_ID_PATTERN.match(device_id):
|
||||
return False, "Device ID can only contain letters, numbers, underscores and hyphens"
|
||||
return True, None
|
||||
|
||||
def validate_note(note):
|
||||
"""Validate note length."""
|
||||
if note and len(note) > MAX_NOTE_LENGTH:
|
||||
return False, f"Note too long (max {MAX_NOTE_LENGTH} characters)"
|
||||
return True, None
|
||||
|
||||
def sanitize_input(text):
|
||||
"""Basic sanitization of user input."""
|
||||
if not text:
|
||||
return text
|
||||
# Remove potential XSS patterns
|
||||
text = text.replace('<script>', '').replace('</script>', '')
|
||||
text = text.replace('<iframe>', '').replace('</iframe>', '')
|
||||
return text.strip()
|
||||
|
||||
def get_db_connection():
|
||||
"""Create a read-write connection to the SQLite database."""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get_public_key():
|
||||
"""Read the RustDesk public key from file - scans for any .pub file."""
|
||||
try:
|
||||
# First try default path
|
||||
if os.path.exists(PUB_KEY_PATH):
|
||||
with open(PUB_KEY_PATH, 'r') as f:
|
||||
key_content = f.read().strip()
|
||||
return f"[id_ed25519.pub] {key_content}"
|
||||
|
||||
# If default doesn't exist, scan for any .pub file in directory
|
||||
rustdesk_dir = os.path.dirname(PUB_KEY_PATH)
|
||||
if os.path.exists(rustdesk_dir):
|
||||
pub_files = [f for f in os.listdir(rustdesk_dir) if f.endswith('.pub')]
|
||||
if pub_files:
|
||||
# Use the first .pub file found
|
||||
pub_file_path = os.path.join(rustdesk_dir, pub_files[0])
|
||||
with open(pub_file_path, 'r') as f:
|
||||
key_content = f.read().strip()
|
||||
return f"[{pub_files[0]}] {key_content}"
|
||||
|
||||
return "❌ No public key file (.pub) found in RustDesk directory"
|
||||
except Exception as e:
|
||||
return f"Error reading key: {str(e)}"
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Render the main dashboard page."""
|
||||
public_key = get_public_key()
|
||||
return render_template('index.html', public_key=public_key)
|
||||
|
||||
@app.route('/api/devices', methods=['GET'])
|
||||
def get_devices():
|
||||
"""Fetch all devices from the database with online status from HBBS API."""
|
||||
try:
|
||||
# Try to get status from HBBS API
|
||||
online_ids = set()
|
||||
api_device_info = {}
|
||||
try:
|
||||
response = requests.get(f'{HBBS_API_URL}/peers', timeout=2)
|
||||
if response.status_code == 200:
|
||||
api_data = response.json()
|
||||
if api_data.get('success') and api_data.get('data'):
|
||||
# Collect online devices and their full info from API
|
||||
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)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not connect to HBBS API: {e}")
|
||||
|
||||
# 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
|
||||
''')
|
||||
|
||||
devices = []
|
||||
for row in cursor.fetchall():
|
||||
device_id = row['id']
|
||||
|
||||
# Determine online status
|
||||
# Use ONLY the API status (same logic as RustDesk desktop client)
|
||||
# This ensures consistency between web console and desktop client
|
||||
if device_id in api_device_info:
|
||||
# Device found in API - use the API's online status
|
||||
online = api_device_info[device_id].get('online', False)
|
||||
else:
|
||||
# Device not found in API - consider it offline
|
||||
# (If API is unreachable, fall back to database status)
|
||||
online = row['status'] == 1 if not api_device_info else False
|
||||
|
||||
device = {
|
||||
'guid': row['guid'].hex() if row['guid'] else '',
|
||||
'id': device_id,
|
||||
'uuid': row['uuid'].hex() if row['uuid'] else '',
|
||||
'pk': row['pk'].hex() if row['pk'] else '',
|
||||
'created_at': row['created_at'],
|
||||
'user': row['user'].hex() if row['user'] else '',
|
||||
'status': row['status'],
|
||||
'online': 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 ''
|
||||
}
|
||||
devices.append(device)
|
||||
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'devices': devices})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/device/<device_id>', methods=['PUT'])
|
||||
def update_device(device_id):
|
||||
"""Update a device's note and/or ID."""
|
||||
try:
|
||||
# Validate input device ID
|
||||
is_valid, error_msg = validate_device_id(device_id)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'No data provided'}), 400
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build dynamic update query based on provided fields
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if 'note' in data:
|
||||
# Validate and sanitize note
|
||||
is_valid, error_msg = validate_note(data['note'])
|
||||
if not is_valid:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
sanitized_note = sanitize_input(data['note'])
|
||||
updates.append('note = ?')
|
||||
params.append(sanitized_note)
|
||||
|
||||
if 'new_id' in data and data['new_id']:
|
||||
# Validate new device ID
|
||||
is_valid, error_msg = validate_device_id(data['new_id'])
|
||||
if not is_valid:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
# Check if new ID already exists
|
||||
cursor.execute('SELECT id FROM peer WHERE id = ? AND is_deleted = 0', (data['new_id'],))
|
||||
if cursor.fetchone():
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device ID already exists'}), 409
|
||||
|
||||
updates.append('id = ?')
|
||||
params.append(data['new_id'])
|
||||
|
||||
if not updates:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'No fields to update'}), 400
|
||||
|
||||
# Add updated_at timestamp
|
||||
updates.append('updated_at = ?')
|
||||
params.append(int(datetime.now().timestamp() * 1000))
|
||||
|
||||
params.append(device_id)
|
||||
query = f"UPDATE peer SET {', '.join(updates)} WHERE id = ? AND is_deleted = 0"
|
||||
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
conn.close()
|
||||
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': 'Device not found or already deleted'}), 404
|
||||
|
||||
return jsonify({'success': True, 'message': 'Device updated successfully'})
|
||||
except sqlite3.IntegrityError as e:
|
||||
return jsonify({'success': False, 'error': f'Database constraint violation: {str(e)}'}), 409
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Unexpected error: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/device/<device_id>', methods=['DELETE'])
|
||||
def delete_device(device_id):
|
||||
"""Soft delete a device from the database."""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Soft delete: set is_deleted=1 and timestamp
|
||||
deleted_at = int(datetime.now().timestamp() * 1000)
|
||||
cursor.execute(
|
||||
'UPDATE peer SET is_deleted = 1, deleted_at = ? WHERE id = ? AND is_deleted = 0',
|
||||
(deleted_at, device_id)
|
||||
)
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
conn.close()
|
||||
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': 'Device not found or already deleted'}), 404
|
||||
|
||||
return jsonify({'success': True, 'message': 'Device deleted successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/stats', methods=['GET'])
|
||||
def get_stats():
|
||||
"""Get statistics about the devices."""
|
||||
try:
|
||||
# Try to get online count from HBBS API
|
||||
online_count = 0
|
||||
try:
|
||||
response = requests.get(f'{HBBS_API_URL}/peers', timeout=2)
|
||||
if response.status_code == 200:
|
||||
api_data = response.json()
|
||||
if api_data.get('success') and api_data.get('data'):
|
||||
online_count = sum(1 for peer in api_data['data'] if peer.get('online'))
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not connect to HBBS API for stats: {e}")
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Total devices (excluding deleted)
|
||||
cursor.execute('SELECT COUNT(*) as total FROM peer WHERE is_deleted = 0')
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
# Banned devices count
|
||||
cursor.execute('SELECT COUNT(*) as banned FROM peer WHERE is_banned = 1 AND is_deleted = 0')
|
||||
banned = cursor.fetchone()['banned']
|
||||
|
||||
# If HBBS API didn't work, fallback to database status
|
||||
if online_count == 0:
|
||||
cursor.execute('SELECT COUNT(*) as active FROM peer WHERE status = 1 AND is_deleted = 0')
|
||||
online_count = cursor.fetchone()['active']
|
||||
|
||||
# Devices with notes (excluding deleted)
|
||||
cursor.execute('SELECT COUNT(*) as with_notes FROM peer WHERE note IS NOT NULL AND note != "" AND is_deleted = 0')
|
||||
with_notes = cursor.fetchone()['with_notes']
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'stats': {
|
||||
'total': total,
|
||||
'active': online_count,
|
||||
'inactive': total - online_count,
|
||||
'with_notes': with_notes,
|
||||
'banned': banned
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/hbbs/status', methods=['GET'])
|
||||
def hbbs_status():
|
||||
"""Check if HBBS API is available."""
|
||||
try:
|
||||
response = requests.get(f'{HBBS_API_URL}/health', timeout=2)
|
||||
if response.status_code == 200:
|
||||
return jsonify({'success': True, 'message': 'HBBS API is running', 'api_data': response.json()})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': f'HBBS API returned status {response.status_code}'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'HBBS API not available: {str(e)}'})
|
||||
|
||||
@app.route('/api/device/<device_id>/ban', methods=['POST'])
|
||||
def ban_device(device_id):
|
||||
"""Ban a device."""
|
||||
try:
|
||||
# Validate device ID
|
||||
is_valid, error_msg = validate_device_id(device_id)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate ban reason
|
||||
ban_reason = sanitize_input(data.get('reason', ''))
|
||||
if ban_reason and len(ban_reason) > 500:
|
||||
return jsonify({'success': False, 'error': 'Ban reason too long (max 500 characters)'}), 400
|
||||
|
||||
banned_by = sanitize_input(data.get('banned_by', 'admin'))
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if device exists and is not deleted
|
||||
cursor.execute('SELECT id, is_banned FROM peer WHERE id = ? AND is_deleted = 0', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
|
||||
if not device:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device not found or already deleted'}), 404
|
||||
|
||||
if device['is_banned'] == 1:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device is already banned'}), 409
|
||||
|
||||
# Ban the device
|
||||
banned_at = int(datetime.now().timestamp() * 1000)
|
||||
cursor.execute('''
|
||||
UPDATE peer
|
||||
SET is_banned = 1,
|
||||
banned_at = ?,
|
||||
banned_by = ?,
|
||||
ban_reason = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
''', (banned_at, banned_by, ban_reason, banned_at, device_id))
|
||||
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
conn.close()
|
||||
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': 'Failed to ban device'}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Device {device_id} banned successfully',
|
||||
'banned_at': banned_at,
|
||||
'banned_by': banned_by
|
||||
})
|
||||
|
||||
except sqlite3.Error as e:
|
||||
return jsonify({'success': False, 'error': f'Database error: {str(e)}'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Unexpected error: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/device/<device_id>/unban', methods=['POST'])
|
||||
def unban_device(device_id):
|
||||
"""Unban a device."""
|
||||
try:
|
||||
# Validate device ID
|
||||
is_valid, error_msg = validate_device_id(device_id)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if device exists and is banned
|
||||
cursor.execute('SELECT id, is_banned FROM peer WHERE id = ? AND is_deleted = 0', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
|
||||
if not device:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device not found or already deleted'}), 404
|
||||
|
||||
if device['is_banned'] == 0:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device is not banned'}), 409
|
||||
|
||||
# Unban the device
|
||||
updated_at = int(datetime.now().timestamp() * 1000)
|
||||
cursor.execute('''
|
||||
UPDATE peer
|
||||
SET is_banned = 0,
|
||||
banned_at = NULL,
|
||||
banned_by = NULL,
|
||||
ban_reason = NULL,
|
||||
updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
''', (updated_at, device_id))
|
||||
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
conn.close()
|
||||
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': 'Failed to unban device'}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Device {device_id} unbanned successfully'
|
||||
})
|
||||
|
||||
except sqlite3.Error as e:
|
||||
return jsonify({'success': False, 'error': f'Database error: {str(e)}'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'Unexpected error: {str(e)}'}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BetterDesk Console - Demo Version with Mock Data
|
||||
For creating screenshots without exposing real device information
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, jsonify, request
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Mock data for screenshots
|
||||
MOCK_DEVICES = [
|
||||
{"id": "1234567890", "note": "Production Server - NYC", "online": True, "created_at": "2025-12-01 10:30:00", "user": "admin", "info": '{"os": "Ubuntu 22.04", "version": "1.2.3"}'},
|
||||
{"id": "0987654321", "note": "Development Workstation", "online": True, "created_at": "2025-12-05 14:20:00", "user": "dev_team", "info": '{"os": "Windows 11", "version": "1.2.3"}'},
|
||||
{"id": "5555555555", "note": "Marketing Office PC", "online": False, "created_at": "2025-11-20 09:15:00", "user": "marketing", "info": '{"os": "macOS 14", "version": "1.2.2"}'},
|
||||
{"id": "7777777777", "note": "Database Server - LA", "online": True, "created_at": "2025-12-10 16:45:00", "user": "dba", "info": '{"os": "Ubuntu 24.04", "version": "1.2.3"}'},
|
||||
{"id": "9999999999", "note": "Sales Laptop", "online": False, "created_at": "2025-11-15 11:30:00", "user": "sales", "info": '{"os": "Windows 10", "version": "1.2.1"}'},
|
||||
{"id": "1111111111", "note": "Backup Server", "online": True, "created_at": "2025-12-08 13:00:00", "user": "admin", "info": '{"os": "Debian 12", "version": "1.2.3"}'},
|
||||
{"id": "2222222222", "note": "Test Environment", "online": True, "created_at": "2025-12-12 08:45:00", "user": "qa_team", "info": '{"os": "Ubuntu 22.04", "version": "1.2.3"}'},
|
||||
{"id": "3333333333", "note": "HR Department PC", "online": False, "created_at": "2025-11-25 10:00:00", "user": "hr", "info": '{"os": "Windows 11", "version": "1.2.2"}'},
|
||||
{"id": "4444444444", "note": "Web Server - EU", "online": True, "created_at": "2025-12-15 15:30:00", "user": "webmaster", "info": '{"os": "CentOS 9", "version": "1.2.3"}'},
|
||||
{"id": "6666666666", "note": "Design Workstation", "online": True, "created_at": "2025-12-03 12:20:00", "user": "design", "info": '{"os": "macOS 14", "version": "1.2.3"}'},
|
||||
{"id": "8888888888", "note": None, "online": False, "created_at": "2025-11-10 14:00:00", "user": None, "info": None},
|
||||
{"id": "1212121212", "note": None, "online": False, "created_at": "2025-11-05 09:30:00", "user": None, "info": None},
|
||||
]
|
||||
|
||||
MOCK_PUBLIC_KEY = "AGH8B3pM5QVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV="
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html', public_key=MOCK_PUBLIC_KEY)
|
||||
|
||||
@app.route('/api/devices', methods=['GET'])
|
||||
def get_devices():
|
||||
return jsonify({"success": True, "devices": MOCK_DEVICES, "error": None})
|
||||
|
||||
@app.route('/api/stats', methods=['GET'])
|
||||
def get_stats():
|
||||
total = len(MOCK_DEVICES)
|
||||
active = sum(1 for d in MOCK_DEVICES if d['online'])
|
||||
inactive = total - active
|
||||
with_notes = sum(1 for d in MOCK_DEVICES if d['note'])
|
||||
|
||||
return jsonify({
|
||||
"stats": {
|
||||
"total": total,
|
||||
"active": active,
|
||||
"inactive": inactive,
|
||||
"with_notes": with_notes
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/device/<device_id>', methods=['GET'])
|
||||
def get_device(device_id):
|
||||
device = next((d for d in MOCK_DEVICES if d['id'] == device_id), None)
|
||||
if device:
|
||||
return jsonify({"success": True, "device": device, "error": None})
|
||||
return jsonify({"success": False, "device": None, "error": "Device not found"}), 404
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("BetterDesk Console - DEMO MODE")
|
||||
print("=" * 60)
|
||||
print("This version uses mock data for screenshots.")
|
||||
print("Running on: http://localhost:5001")
|
||||
print("=" * 60)
|
||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||
@@ -0,0 +1,812 @@
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, g
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
import os
|
||||
import requests
|
||||
import re
|
||||
from flask_wtf.csrf import CSRFProtect, generate_csrf
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
# Import authentication module
|
||||
from auth import (
|
||||
require_auth, require_role, optional_auth,
|
||||
authenticate, create_session, verify_session, delete_session,
|
||||
log_audit, cleanup_expired_sessions,
|
||||
change_password,
|
||||
ROLE_ADMIN, ROLE_OPERATOR, ROLE_VIEWER,
|
||||
AuthError
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', os.urandom(32))
|
||||
app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Manual CSRF for API
|
||||
|
||||
# Initialize CSRF protection
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
# Initialize rate limiter
|
||||
limiter = Limiter(
|
||||
app=app,
|
||||
key_func=get_remote_address,
|
||||
default_limits=["1000 per hour", "100 per minute"],
|
||||
storage_uri="memory://"
|
||||
)
|
||||
|
||||
# Configuration
|
||||
DB_PATH = '/opt/rustdesk/db_v2.sqlite3'
|
||||
PUB_KEY_PATH = '/opt/rustdesk/id_ed25519.pub'
|
||||
API_KEY_PATH = '/opt/rustdesk/.api_key'
|
||||
HBBS_API_URL = 'http://localhost:21120/api'
|
||||
|
||||
# Load HBBS API key
|
||||
def get_hbbs_api_key():
|
||||
"""Load HBBS API key from file or environment variable."""
|
||||
# Try environment variable first
|
||||
api_key = os.environ.get('HBBS_API_KEY')
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
# Try reading from file
|
||||
try:
|
||||
if os.path.exists(API_KEY_PATH):
|
||||
with open(API_KEY_PATH, 'r') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read API key from {API_KEY_PATH}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
HBBS_API_KEY = get_hbbs_api_key()
|
||||
|
||||
# Validation rules
|
||||
MAX_NOTE_LENGTH = 500
|
||||
MAX_DEVICE_ID_LENGTH = 50
|
||||
DEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_-]+$')
|
||||
|
||||
|
||||
def validate_device_id(device_id):
|
||||
"""Validate device ID format and length."""
|
||||
if not device_id:
|
||||
return False, "Device ID cannot be empty"
|
||||
if len(device_id) > MAX_DEVICE_ID_LENGTH:
|
||||
return False, f"Device ID too long (max {MAX_DEVICE_ID_LENGTH} characters)"
|
||||
if not DEVICE_ID_PATTERN.match(device_id):
|
||||
return False, "Device ID can only contain letters, numbers, underscores and hyphens"
|
||||
return True, None
|
||||
|
||||
|
||||
def validate_note(note):
|
||||
"""Validate note length."""
|
||||
if note and len(note) > MAX_NOTE_LENGTH:
|
||||
return False, f"Note too long (max {MAX_NOTE_LENGTH} characters)"
|
||||
return True, None
|
||||
|
||||
|
||||
def validate_password_strength(password):
|
||||
"""Validate password strength - minimum 8 characters, letters and numbers."""
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
if not re.search(r'[A-Za-z]', password):
|
||||
return False, "Password must contain at least one letter"
|
||||
if not re.search(r'[0-9]', password):
|
||||
return False, "Password must contain at least one number"
|
||||
return True, None
|
||||
|
||||
|
||||
def sanitize_input(text):
|
||||
"""Basic sanitization of user input."""
|
||||
if not text:
|
||||
return text
|
||||
from markupsafe import escape
|
||||
return str(escape(text)).strip()
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
"""Create a read-write connection to the SQLite database."""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def get_public_key():
|
||||
"""Read the RustDesk public key from file."""
|
||||
try:
|
||||
if os.path.exists(PUB_KEY_PATH):
|
||||
with open(PUB_KEY_PATH, 'r') as f:
|
||||
key_content = f.read().strip()
|
||||
return f"[id_ed25519.pub] {key_content}"
|
||||
|
||||
rustdesk_dir = os.path.dirname(PUB_KEY_PATH)
|
||||
if os.path.exists(rustdesk_dir):
|
||||
pub_files = [f for f in os.listdir(rustdesk_dir) if f.endswith('.pub')]
|
||||
if pub_files:
|
||||
pub_file_path = os.path.join(rustdesk_dir, pub_files[0])
|
||||
with open(pub_file_path, 'r') as f:
|
||||
key_content = f.read().strip()
|
||||
return f"[{pub_files[0]}] {key_content}"
|
||||
|
||||
return "❌ No public key file (.pub) found in RustDesk directory"
|
||||
except Exception as e:
|
||||
return f"Error reading key: {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION ROUTES
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/login')
|
||||
def login_page():
|
||||
"""Render login page"""
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/api/auth/login', methods=['POST'])
|
||||
@limiter.limit("5 per minute")
|
||||
@csrf.exempt # CSRF exempt for login, validated by credentials
|
||||
def login():
|
||||
"""Login endpoint"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({'success': False, 'error': 'Username and password required'}), 400
|
||||
|
||||
# Note: Password strength is only validated on creation/change, not login
|
||||
# (to allow legacy accounts with weaker passwords to still login)
|
||||
|
||||
# Authenticate user
|
||||
user = authenticate(username, password)
|
||||
|
||||
# Create session
|
||||
token = create_session(user['id'])
|
||||
|
||||
# Log login
|
||||
log_audit(user['id'], 'login', None, f"Login from {request.remote_addr}", request.remote_addr)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'token': token,
|
||||
'username': user['username'],
|
||||
'role': user['role']
|
||||
})
|
||||
|
||||
except AuthError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 401
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': 'Login failed'}), 500
|
||||
|
||||
|
||||
@app.route('/api/auth/logout', methods=['POST'])
|
||||
@require_auth
|
||||
def logout():
|
||||
"""Logout endpoint"""
|
||||
try:
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
|
||||
# Log logout
|
||||
log_audit(g.user['user_id'], 'logout', None, 'User logged out', request.remote_addr)
|
||||
|
||||
# Delete session
|
||||
delete_session(token)
|
||||
|
||||
return jsonify({'success': True, 'message': 'Logged out successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/auth/verify', methods=['GET'])
|
||||
@require_auth
|
||||
def verify_token():
|
||||
"""Verify authentication token"""
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'user': {
|
||||
'username': g.user['username'],
|
||||
'role': g.user['role']
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN ROUTES
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Render the main dashboard page. Auth check done by JavaScript on client side."""
|
||||
return render_template('index_v15.html')
|
||||
|
||||
|
||||
@app.route('/api/devices', methods=['GET'])
|
||||
@require_auth
|
||||
@limiter.exempt # Authenticated users bypass rate limit
|
||||
def get_devices():
|
||||
"""Fetch all devices from the database with online status from HBBS API."""
|
||||
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 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
|
||||
''')
|
||||
|
||||
devices = []
|
||||
for row in cursor.fetchall():
|
||||
device_id = row['id']
|
||||
|
||||
if device_id in api_device_info:
|
||||
online = api_device_info[device_id].get('online', False)
|
||||
else:
|
||||
online = row['status'] == 1 if not api_device_info else False
|
||||
|
||||
device = {
|
||||
'guid': row['guid'].hex() if row['guid'] else '',
|
||||
'id': device_id,
|
||||
'uuid': row['uuid'].hex() if row['uuid'] else '',
|
||||
'pk': row['pk'].hex() if row['pk'] else '',
|
||||
'created_at': row['created_at'],
|
||||
'user': row['user'].hex() if row['user'] else '',
|
||||
'status': row['status'],
|
||||
'online': 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 ''
|
||||
}
|
||||
devices.append(device)
|
||||
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'devices': devices})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/device/<device_id>', methods=['PUT'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN, ROLE_OPERATOR)
|
||||
def update_device(device_id):
|
||||
"""Update a device's note and/or ID."""
|
||||
try:
|
||||
is_valid, error_msg = validate_device_id(device_id)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'No data provided'}), 400
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if 'note' in data:
|
||||
is_valid, error_msg = validate_note(data['note'])
|
||||
if not is_valid:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
sanitized_note = sanitize_input(data['note'])
|
||||
updates.append('note = ?')
|
||||
params.append(sanitized_note)
|
||||
|
||||
if 'new_id' in data and data['new_id']:
|
||||
is_valid, error_msg = validate_device_id(data['new_id'])
|
||||
if not is_valid:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
cursor.execute('SELECT id FROM peer WHERE id = ? AND is_deleted = 0', (data['new_id'],))
|
||||
if cursor.fetchone():
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device ID already exists'}), 409
|
||||
|
||||
updates.append('id = ?')
|
||||
params.append(data['new_id'])
|
||||
|
||||
if not updates:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'No fields to update'}), 400
|
||||
|
||||
updates.append('updated_at = ?')
|
||||
params.append(int(datetime.now().timestamp() * 1000))
|
||||
params.append(device_id)
|
||||
|
||||
query = f"UPDATE peer SET {', '.join(updates)} WHERE id = ? AND is_deleted = 0"
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
conn.close()
|
||||
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': 'Device not found'}), 404
|
||||
|
||||
# Log audit with selective data
|
||||
audit_details = []
|
||||
if 'note' in data:
|
||||
audit_details.append(f"note: {data.get('note', '')[:50]}...") # First 50 chars
|
||||
if 'new_id' in data:
|
||||
audit_details.append(f"new_id: {data['new_id']}")
|
||||
log_audit(g.user['user_id'], 'update_device', device_id,
|
||||
f"Updated: {', '.join(audit_details)}", request.remote_addr or 'unknown')
|
||||
|
||||
return jsonify({'success': True, 'message': 'Device updated successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/device/<device_id>', methods=['DELETE'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN, ROLE_OPERATOR)
|
||||
def delete_device(device_id):
|
||||
"""Soft delete a device from the database."""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
deleted_at = int(datetime.now().timestamp() * 1000)
|
||||
cursor.execute(
|
||||
'UPDATE peer SET is_deleted = 1, deleted_at = ? WHERE id = ? AND is_deleted = 0',
|
||||
(deleted_at, device_id)
|
||||
)
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
conn.close()
|
||||
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': 'Device not found'}), 404
|
||||
|
||||
# Log audit
|
||||
log_audit(g.user['user_id'], 'delete_device', device_id,
|
||||
'Device deleted', request.remote_addr or 'unknown')
|
||||
|
||||
return jsonify({'success': True, 'message': 'Device deleted successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/stats', methods=['GET'])
|
||||
@require_auth
|
||||
@limiter.exempt # Authenticated users bypass rate limit
|
||||
def get_stats():
|
||||
"""Get statistics about the devices."""
|
||||
try:
|
||||
online_count = 0
|
||||
try:
|
||||
response = requests.get(f'{HBBS_API_URL}/peers', timeout=2)
|
||||
if response.status_code == 200:
|
||||
api_data = response.json()
|
||||
if api_data.get('success') and api_data.get('data'):
|
||||
online_count = sum(1 for peer in api_data['data'] if peer.get('online'))
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not connect to HBBS API for stats: {e}")
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as total FROM peer WHERE is_deleted = 0')
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as banned FROM peer WHERE is_banned = 1 AND is_deleted = 0')
|
||||
banned = cursor.fetchone()['banned']
|
||||
|
||||
if online_count == 0:
|
||||
cursor.execute('SELECT COUNT(*) as active FROM peer WHERE status = 1 AND is_deleted = 0')
|
||||
online_count = cursor.fetchone()['active']
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as with_notes FROM peer WHERE note IS NOT NULL AND note != "" AND is_deleted = 0')
|
||||
with_notes = cursor.fetchone()['with_notes']
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'stats': {
|
||||
'total': total,
|
||||
'active': online_count,
|
||||
'inactive': total - online_count,
|
||||
'with_notes': with_notes,
|
||||
'banned': banned
|
||||
}
|
||||
})
|
||||
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)
|
||||
def ban_device(device_id):
|
||||
"""Ban a device."""
|
||||
try:
|
||||
is_valid, error_msg = validate_device_id(device_id)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
ban_reason = sanitize_input(data.get('reason', ''))
|
||||
if ban_reason and len(ban_reason) > 500:
|
||||
return jsonify({'success': False, 'error': 'Ban reason too long'}), 400
|
||||
|
||||
banned_by = g.user['username']
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT id, is_banned FROM peer WHERE id = ? AND is_deleted = 0', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
|
||||
if not device:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device not found'}), 404
|
||||
|
||||
if device['is_banned'] == 1:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device is already banned'}), 409
|
||||
|
||||
banned_at = int(datetime.now().timestamp() * 1000)
|
||||
cursor.execute('''
|
||||
UPDATE peer
|
||||
SET is_banned = 1, banned_at = ?, banned_by = ?, ban_reason = ?, updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
''', (banned_at, banned_by, ban_reason, banned_at, device_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Log audit
|
||||
log_audit(g.user['user_id'], 'ban_device', device_id,
|
||||
f"Banned device. Reason: {ban_reason}", request.remote_addr)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Device {device_id} banned successfully',
|
||||
'banned_at': banned_at,
|
||||
'banned_by': banned_by
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/device/<device_id>/unban', methods=['POST'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN, ROLE_OPERATOR)
|
||||
def unban_device(device_id):
|
||||
"""Unban a device."""
|
||||
try:
|
||||
is_valid, error_msg = validate_device_id(device_id)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT id, is_banned FROM peer WHERE id = ? AND is_deleted = 0', (device_id,))
|
||||
device = cursor.fetchone()
|
||||
|
||||
if not device:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device not found'}), 404
|
||||
|
||||
if device['is_banned'] == 0:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': 'Device is not banned'}), 409
|
||||
|
||||
updated_at = int(datetime.now().timestamp() * 1000)
|
||||
cursor.execute('''
|
||||
UPDATE peer
|
||||
SET is_banned = 0, banned_at = NULL, banned_by = NULL, ban_reason = NULL, updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0
|
||||
''', (updated_at, device_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Log audit
|
||||
log_audit(g.user['user_id'], 'unban_device', device_id,
|
||||
'Device unbanned', request.remote_addr)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Device {device_id} unbanned successfully'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# USER MANAGEMENT ROUTES (Admin only)
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/users', methods=['GET'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN)
|
||||
def list_all_users():
|
||||
"""List all users (admin only)"""
|
||||
try:
|
||||
from auth import list_users
|
||||
users = list_users()
|
||||
|
||||
# Don't send password hashes
|
||||
for user in users:
|
||||
user.pop('password_hash', None)
|
||||
|
||||
return jsonify({'success': True, 'users': users})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/users', methods=['POST'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN)
|
||||
def create_new_user():
|
||||
"""Create new user (admin only)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
username = sanitize_input(data.get('username', ''))
|
||||
password = data.get('password', '')
|
||||
role = data.get('role', ROLE_VIEWER)
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({'success': False, 'error': 'Username and password required'}), 400
|
||||
|
||||
# Validate password strength
|
||||
is_valid, error_msg = validate_password_strength(password)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
from auth import create_user
|
||||
user = create_user(username, password, role)
|
||||
|
||||
# Log audit
|
||||
log_audit(g.user['user_id'], 'create_user', None,
|
||||
f"Created user: {username} with role: {role}", request.remote_addr)
|
||||
|
||||
return jsonify({'success': True, 'message': 'User created successfully', 'user': user})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
|
||||
|
||||
@app.route('/api/users/<int:user_id>', methods=['PUT'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN)
|
||||
def update_user(user_id):
|
||||
"""Update user (admin only)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
action = data.get('action')
|
||||
|
||||
from auth import update_user_role, activate_user, deactivate_user, reset_password
|
||||
|
||||
if action == 'change_role':
|
||||
new_role = data.get('role')
|
||||
if not new_role:
|
||||
return jsonify({'success': False, 'error': 'Role required'}), 400
|
||||
|
||||
update_user_role(user_id, new_role)
|
||||
log_audit(g.user['user_id'], 'update_user_role', None,
|
||||
f"Changed role of user {user_id} to {new_role}", request.remote_addr)
|
||||
return jsonify({'success': True, 'message': 'User role updated'})
|
||||
|
||||
elif action == 'activate':
|
||||
activate_user(user_id)
|
||||
log_audit(g.user['user_id'], 'activate_user', None,
|
||||
f"Activated user {user_id}", request.remote_addr)
|
||||
return jsonify({'success': True, 'message': 'User activated'})
|
||||
|
||||
elif action == 'deactivate':
|
||||
deactivate_user(user_id)
|
||||
log_audit(g.user['user_id'], 'deactivate_user', None,
|
||||
f"Deactivated user {user_id}", request.remote_addr)
|
||||
return jsonify({'success': True, 'message': 'User deactivated'})
|
||||
|
||||
elif action == 'reset_password':
|
||||
new_password = data.get('password')
|
||||
if not new_password:
|
||||
return jsonify({'success': False, 'error': 'Password required'}), 400
|
||||
|
||||
# Validate password strength
|
||||
is_valid, error_msg = validate_password_strength(new_password)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
reset_password(user_id, new_password)
|
||||
log_audit(g.user['user_id'], 'reset_password', None,
|
||||
f"Reset password for user {user_id}", request.remote_addr)
|
||||
return jsonify({'success': True, 'message': 'Password reset successfully'})
|
||||
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Invalid action'}), 400
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
|
||||
|
||||
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
|
||||
@require_auth
|
||||
@require_role(ROLE_ADMIN)
|
||||
def delete_user_account(user_id):
|
||||
"""Delete user (admin only)"""
|
||||
try:
|
||||
# Prevent self-deletion
|
||||
if user_id == g.user['user_id']:
|
||||
return jsonify({'success': False, 'error': 'Cannot delete your own account'}), 400
|
||||
|
||||
from auth import delete_user
|
||||
delete_user(user_id)
|
||||
|
||||
log_audit(g.user['user_id'], 'delete_user', None,
|
||||
f"Deleted user {user_id}", request.remote_addr)
|
||||
|
||||
return jsonify({'success': True, 'message': 'User deleted successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PASSWORD CHANGE & KEY VERIFICATION
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/auth/change-password', methods=['POST'])
|
||||
@require_auth
|
||||
def change_user_password():
|
||||
"""Change current user's password"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
old_password = data.get('old_password', '')
|
||||
new_password = data.get('new_password', '')
|
||||
|
||||
if not old_password or not new_password:
|
||||
return jsonify({'success': False, 'error': 'Old and new password required'}), 400
|
||||
|
||||
# Validate password strength
|
||||
is_valid, error_msg = validate_password_strength(new_password)
|
||||
if not is_valid:
|
||||
return jsonify({'success': False, 'error': error_msg}), 400
|
||||
|
||||
# Change password and get new token
|
||||
new_token = change_password(g.user['user_id'], old_password, new_password)
|
||||
|
||||
log_audit(g.user['user_id'], 'change_password', None,
|
||||
'User changed password', request.remote_addr)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Password changed successfully',
|
||||
'token': new_token
|
||||
})
|
||||
except AuthError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': 'Password change failed'}), 500
|
||||
|
||||
|
||||
@app.route('/api/auth/verify-password', methods=['POST'])
|
||||
@require_auth
|
||||
def verify_password_endpoint():
|
||||
"""Verify user's password (for accessing protected content like public key)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not password:
|
||||
return jsonify({'success': False, 'error': 'Password required'}), 400
|
||||
|
||||
# Verify user's password
|
||||
from auth import verify_password as verify_pwd, get_auth_db
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT password_hash FROM users WHERE id = ?', (g.user['user_id'],))
|
||||
user = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not user or not verify_pwd(password, user['password_hash']):
|
||||
log_audit(g.user['user_id'], 'password_verify_failed', None,
|
||||
'Failed password verification', request.remote_addr)
|
||||
return jsonify({'success': False, 'error': 'Invalid password'}), 401
|
||||
|
||||
log_audit(g.user['user_id'], 'password_verify_success', None,
|
||||
'Password verified successfully', request.remote_addr)
|
||||
|
||||
return jsonify({'success': True, 'message': 'Password verified'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/public-key', methods=['GET'])
|
||||
@require_auth
|
||||
def get_public_key_endpoint():
|
||||
"""Get server public key (requires prior password verification in frontend)"""
|
||||
try:
|
||||
# Only admin and operator can view key
|
||||
if g.user['role'] not in [ROLE_ADMIN, ROLE_OPERATOR]:
|
||||
return jsonify({'success': False, 'error': 'Insufficient permissions'}), 403
|
||||
|
||||
public_key = get_public_key()
|
||||
|
||||
log_audit(g.user['user_id'], 'view_public_key', None,
|
||||
'Accessed public key', request.remote_addr)
|
||||
|
||||
return jsonify({'success': True, 'key': public_key})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# Cleanup expired sessions periodically
|
||||
@app.before_request
|
||||
def before_request():
|
||||
"""Run before each request"""
|
||||
# Cleanup expired sessions (every 100th request to avoid overhead)
|
||||
import random
|
||||
if random.randint(1, 100) == 1:
|
||||
try:
|
||||
cleanup_expired_sessions()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
"""Add security headers to all responses"""
|
||||
# Content Security Policy
|
||||
response.headers['Content-Security-Policy'] = (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-inline'; "
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; "
|
||||
"font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; "
|
||||
"img-src 'self' data:; "
|
||||
"connect-src 'self'"
|
||||
)
|
||||
# Additional security headers
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
return response
|
||||
|
||||
|
||||
# Note: Authentication is handled by @require_auth decorator on each endpoint
|
||||
# No global before_request check needed - let JavaScript on pages handle redirects
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
DEBUG = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
|
||||
HOST = os.environ.get('FLASK_HOST', '0.0.0.0')
|
||||
PORT = int(os.environ.get('FLASK_PORT', 5000))
|
||||
app.run(host=HOST, port=PORT, debug=DEBUG)
|
||||
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
Authentication and Authorization Module for BetterDesk Console
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import secrets
|
||||
import bcrypt
|
||||
import functools
|
||||
from datetime import datetime, timedelta
|
||||
from flask import request, jsonify, g
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Database path
|
||||
DB_PATH = '/opt/rustdesk/db_v2.sqlite3'
|
||||
|
||||
# Session expiry (24 hours)
|
||||
SESSION_EXPIRY_HOURS = 24
|
||||
|
||||
# Roles
|
||||
ROLE_ADMIN = 'admin'
|
||||
ROLE_OPERATOR = 'operator'
|
||||
ROLE_VIEWER = 'viewer'
|
||||
|
||||
ROLES_HIERARCHY = {
|
||||
ROLE_ADMIN: 3, # Full access
|
||||
ROLE_OPERATOR: 2, # Can ban/unban, edit devices
|
||||
ROLE_VIEWER: 1 # Read-only
|
||||
}
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
"""Custom exception for authentication errors"""
|
||||
pass
|
||||
|
||||
|
||||
def get_auth_db():
|
||||
"""Get database connection for auth operations"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash password using bcrypt"""
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
"""Verify password against hash"""
|
||||
return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
|
||||
|
||||
|
||||
def generate_session_token() -> str:
|
||||
"""Generate secure session token"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def create_user(username: str, password: str, role: str = ROLE_VIEWER) -> dict:
|
||||
"""Create a new user"""
|
||||
if role not in ROLES_HIERARCHY:
|
||||
raise AuthError(f"Invalid role: {role}")
|
||||
|
||||
password_hash = hash_password(password)
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO users (username, password_hash, role, created_at, is_active)
|
||||
VALUES (?, ?, ?, ?, 1)
|
||||
''', (username, password_hash, role, datetime.now()))
|
||||
|
||||
conn.commit()
|
||||
user_id = cursor.lastrowid
|
||||
|
||||
return {
|
||||
'id': user_id,
|
||||
'username': username,
|
||||
'role': role
|
||||
}
|
||||
except sqlite3.IntegrityError:
|
||||
raise AuthError("Username already exists")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def authenticate(username: str, password: str) -> dict:
|
||||
"""Authenticate user and return user data"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT id, username, password_hash, role, is_active
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
''', (username,))
|
||||
|
||||
user = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not user:
|
||||
raise AuthError("Invalid username or password")
|
||||
|
||||
if not user['is_active']:
|
||||
raise AuthError("Account is disabled")
|
||||
|
||||
if not verify_password(password, user['password_hash']):
|
||||
raise AuthError("Invalid username or password")
|
||||
|
||||
return {
|
||||
'id': user['id'],
|
||||
'username': user['username'],
|
||||
'role': user['role']
|
||||
}
|
||||
|
||||
|
||||
def create_session(user_id: int) -> str:
|
||||
"""Create a new session and return token"""
|
||||
token = generate_session_token()
|
||||
expires_at = datetime.now() + timedelta(hours=SESSION_EXPIRY_HOURS)
|
||||
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO sessions (token, user_id, created_at, expires_at, last_activity)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (token, user_id, datetime.now(), expires_at, datetime.now()))
|
||||
|
||||
# Update last login
|
||||
cursor.execute('''
|
||||
UPDATE users SET last_login = ? WHERE id = ?
|
||||
''', (datetime.now(), user_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def verify_session(token: str) -> dict:
|
||||
"""Verify session token and return user data"""
|
||||
if not token:
|
||||
raise AuthError("No authentication token provided")
|
||||
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT s.user_id, s.expires_at, u.username, u.role, u.is_active
|
||||
FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.token = ?
|
||||
''', (token,))
|
||||
|
||||
session = cursor.fetchone()
|
||||
|
||||
if not session:
|
||||
conn.close()
|
||||
raise AuthError("Invalid session token")
|
||||
|
||||
# Check if session expired
|
||||
expires_at = datetime.fromisoformat(session['expires_at'])
|
||||
if datetime.now() > expires_at:
|
||||
# Delete expired session
|
||||
cursor.execute('DELETE FROM sessions WHERE token = ?', (token,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
raise AuthError("Session expired")
|
||||
|
||||
# Check if user is active
|
||||
if not session['is_active']:
|
||||
conn.close()
|
||||
raise AuthError("Account is disabled")
|
||||
|
||||
# Update last activity
|
||||
cursor.execute('''
|
||||
UPDATE sessions SET last_activity = ? WHERE token = ?
|
||||
''', (datetime.now(), token))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
'user_id': session['user_id'],
|
||||
'username': session['username'],
|
||||
'role': session['role']
|
||||
}
|
||||
|
||||
|
||||
def delete_session(token: str):
|
||||
"""Delete session (logout)"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM sessions WHERE token = ?', (token,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def cleanup_expired_sessions():
|
||||
"""Remove expired sessions from database"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM sessions WHERE expires_at < ?', (datetime.now(),))
|
||||
deleted = cursor.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return deleted
|
||||
|
||||
|
||||
def log_audit(user_id: int, action: str, device_id: Optional[str] = None,
|
||||
details: Optional[str] = None, ip_address: Optional[str] = None):
|
||||
"""Log user action for audit trail"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO audit_log (user_id, action, device_id, details, ip_address, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (user_id, action, device_id, details, ip_address or 'unknown', datetime.now()))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> Optional[dict]:
|
||||
"""Get user data by ID"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT id, username, role, created_at, last_login, is_active
|
||||
FROM users WHERE id = ?
|
||||
''', (user_id,))
|
||||
|
||||
user = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
return dict(user)
|
||||
|
||||
|
||||
def list_users() -> list:
|
||||
"""List all users (admin only)"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT id, username, role, created_at, last_login, is_active
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
users = [dict(row) for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def update_user_role(user_id: int, new_role: str):
|
||||
"""Update user role (admin only)"""
|
||||
if new_role not in ROLES_HIERARCHY:
|
||||
raise AuthError(f"Invalid role: {new_role}")
|
||||
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('UPDATE users SET role = ? WHERE id = ?', (new_role, user_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def deactivate_user(user_id: int):
|
||||
"""Deactivate user account"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('UPDATE users SET is_active = 0 WHERE id = ?', (user_id,))
|
||||
# Also delete all sessions for this user
|
||||
cursor.execute('DELETE FROM sessions WHERE user_id = ?', (user_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def activate_user(user_id: int):
|
||||
"""Activate user account"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET is_active = 1 WHERE id = ?', (user_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_user(user_id: int):
|
||||
"""Delete user account (admin only)"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Delete all sessions first
|
||||
cursor.execute('DELETE FROM sessions WHERE user_id = ?', (user_id,))
|
||||
# Delete user
|
||||
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def change_password(user_id: int, old_password: str, new_password: str) -> str:
|
||||
"""Change user password and return new session token"""
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT password_hash FROM users WHERE id = ?', (user_id,))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if not user:
|
||||
conn.close()
|
||||
raise AuthError("User not found")
|
||||
|
||||
if not verify_password(old_password, user['password_hash']):
|
||||
conn.close()
|
||||
raise AuthError("Current password is incorrect")
|
||||
|
||||
new_hash = hash_password(new_password)
|
||||
cursor.execute('UPDATE users SET password_hash = ? WHERE id = ?', (new_hash, user_id))
|
||||
|
||||
# Invalidate all sessions
|
||||
cursor.execute('DELETE FROM sessions WHERE user_id = ?', (user_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Create new session
|
||||
return create_session(user_id)
|
||||
|
||||
|
||||
def reset_password(user_id: int, new_password: str):
|
||||
"""Reset user password (admin only)"""
|
||||
new_hash = hash_password(new_password)
|
||||
|
||||
conn = get_auth_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('UPDATE users SET password_hash = ? WHERE id = ?', (new_hash, user_id))
|
||||
# Invalidate all sessions for this user
|
||||
cursor.execute('DELETE FROM sessions WHERE user_id = ?', (user_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
# Flask decorators
|
||||
|
||||
def require_auth(f):
|
||||
"""Decorator to require authentication"""
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = request.headers.get('Authorization')
|
||||
|
||||
if token and token.startswith('Bearer '):
|
||||
token = token[7:] # Remove 'Bearer ' prefix
|
||||
|
||||
if not token:
|
||||
return jsonify({'success': False, 'error': 'No authorization token provided'}), 401
|
||||
|
||||
try:
|
||||
user_data = verify_session(token)
|
||||
g.user = user_data # Store in Flask's g object
|
||||
return f(*args, **kwargs)
|
||||
except AuthError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 401
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def require_role(*allowed_roles):
|
||||
"""Decorator to require specific role(s)"""
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not hasattr(g, 'user'):
|
||||
return jsonify({'success': False, 'error': 'Authentication required'}), 401
|
||||
|
||||
user_role = g.user['role']
|
||||
|
||||
if user_role not in allowed_roles:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Insufficient permissions. Required: {", ".join(allowed_roles)}'
|
||||
}), 403
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def optional_auth(f):
|
||||
"""Decorator for optional authentication (doesn't fail if not authenticated)"""
|
||||
@functools.wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = request.headers.get('Authorization')
|
||||
|
||||
if token and token.startswith('Bearer '):
|
||||
token = token[7:]
|
||||
try:
|
||||
user_data = verify_session(token)
|
||||
g.user = user_data
|
||||
except AuthError:
|
||||
g.user = None
|
||||
else:
|
||||
g.user = None
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
@@ -6,6 +6,9 @@ After=network.target
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/BetterDeskConsole
|
||||
Environment="FLASK_HOST=0.0.0.0"
|
||||
Environment="FLASK_PORT=5000"
|
||||
Environment="FLASK_DEBUG=False"
|
||||
ExecStart=/usr/bin/python3 /opt/BetterDeskConsole/app.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
Flask==3.0.0
|
||||
Flask-WTF==1.2.1
|
||||
Flask-Limiter==3.5.0
|
||||
requests==2.31.0
|
||||
bcrypt==4.1.2
|
||||
markupsafe==2.1.3
|
||||
|
||||
@@ -158,19 +158,19 @@ function showDetails(deviceId) {
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">GUID:</div>
|
||||
<div class="detail-value">${device.guid || 'N/A'}</div>
|
||||
<div class="detail-value">${escapeHtml(device.guid) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">UUID:</div>
|
||||
<div class="detail-value">${device.uuid || 'N/A'}</div>
|
||||
<div class="detail-value">${escapeHtml(device.uuid) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Public Key:</div>
|
||||
<div class="detail-value">${device.pk || 'N/A'}</div>
|
||||
<div class="detail-value">${escapeHtml(device.pk) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">User:</div>
|
||||
<div class="detail-value">${device.user || 'N/A'}</div>
|
||||
<div class="detail-value">${escapeHtml(device.user) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Status:</div>
|
||||
|
||||
@@ -0,0 +1,924 @@
|
||||
// BetterDesk Console - Main JavaScript with Authentication v1.4.0
|
||||
// Global variables
|
||||
let allDevices = [];
|
||||
let currentDeviceId = null;
|
||||
let authToken = null;
|
||||
let userRole = null;
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check authentication
|
||||
checkAuth();
|
||||
|
||||
// Load data
|
||||
loadDevices();
|
||||
loadStats();
|
||||
|
||||
// Auto-refresh every 2 seconds
|
||||
setInterval(() => {
|
||||
loadDevices();
|
||||
loadStats();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Authentication check
|
||||
function checkAuth() {
|
||||
authToken = localStorage.getItem('authToken');
|
||||
userRole = localStorage.getItem('role');
|
||||
|
||||
if (!authToken) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get auth headers for API calls
|
||||
function getAuthHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
};
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
function handleAuthError(error, response) {
|
||||
if (response && response.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
window.location.href = '/login';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load devices from API
|
||||
async function loadDevices() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/devices', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
allDevices = data.devices;
|
||||
renderDevices(allDevices);
|
||||
updateNavStats(allDevices);
|
||||
} else {
|
||||
showToast('Error loading devices: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to load devices', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load statistics
|
||||
async function loadStats() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stats', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('statTotal').textContent = data.stats.total;
|
||||
document.getElementById('statActive').textContent = data.stats.active;
|
||||
document.getElementById('statInactive').textContent = data.stats.inactive;
|
||||
document.getElementById('statBanned').textContent = data.stats.banned || 0;
|
||||
document.getElementById('statNotes').textContent = data.stats.with_notes;
|
||||
|
||||
// Update top bar stats
|
||||
const topTotal = document.getElementById('topTotalDevices');
|
||||
const topActive = document.getElementById('topActiveDevices');
|
||||
if (topTotal) topTotal.querySelector('span').textContent = data.stats.total;
|
||||
if (topActive) topActive.querySelector('span').textContent = data.stats.active;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update navigation stats
|
||||
function updateNavStats(devices) {
|
||||
const total = devices.length;
|
||||
const active = devices.filter(d => d.online).length;
|
||||
|
||||
const totalDevicesEl = document.querySelector('#totalDevices span');
|
||||
const activeDevicesEl = document.querySelector('#activeDevices span');
|
||||
|
||||
if (totalDevicesEl) totalDevicesEl.textContent = total;
|
||||
if (activeDevicesEl) activeDevicesEl.textContent = active;
|
||||
}
|
||||
|
||||
// Render devices table
|
||||
function renderDevices(devices) {
|
||||
const tbody = document.getElementById('devicesTableBody');
|
||||
|
||||
if (devices.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="loading">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No devices found</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = devices.map(device => {
|
||||
const isBanned = device.is_banned === true || device.is_banned === 1;
|
||||
const rowClass = isBanned ? 'style="opacity: 0.6; background: rgba(255, 0, 0, 0.05);"' : '';
|
||||
|
||||
// Check permissions for actions
|
||||
const canEdit = userRole === 'admin' || userRole === 'operator';
|
||||
const canBan = userRole === 'admin' || userRole === 'operator';
|
||||
|
||||
return `
|
||||
<tr ${rowClass}>
|
||||
<td>
|
||||
<strong>${escapeHtml(device.id)}</strong>
|
||||
${isBanned ? '<br><span class="status-badge" style="background: #e74c3c; font-size: 0.75rem; margin-top: 4px;"><i class="fas fa-ban"></i> BANNED</span>' : ''}
|
||||
</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>
|
||||
</td>
|
||||
<td>${formatDate(device.created_at)}</td>
|
||||
<td>
|
||||
<button class="action-btn connect" onclick="connectDevice('${escapeHtml(device.id)}')" title="Connect" ${isBanned ? 'disabled style="opacity: 0.3; cursor: not-allowed;"' : ''}>
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
<button class="action-btn details" onclick="showDetails('${escapeHtml(device.id)}')" title="Details">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
${canEdit ? `
|
||||
<button class="action-btn edit" onclick="editDevice('${escapeHtml(device.id)}')" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
${canBan ? (isBanned ?
|
||||
`<button class="action-btn" onclick="unbanDevice('${escapeHtml(device.id)}')" title="Unban" style="background: #27ae60;">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>` :
|
||||
`<button class="action-btn" onclick="banDevice('${escapeHtml(device.id)}')" title="Ban" style="background: #e74c3c;">
|
||||
<i class="fas fa-ban"></i>
|
||||
</button>`
|
||||
) : ''}
|
||||
${canEdit ? `
|
||||
<button class="action-btn delete" onclick="deleteDevice('${escapeHtml(device.id)}')" title="Delete">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// Filter devices by search
|
||||
function filterDevices() {
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
renderDevices(allDevices);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = allDevices.filter(device =>
|
||||
device.id.toLowerCase().includes(searchTerm) ||
|
||||
(device.note && device.note.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
|
||||
renderDevices(filtered);
|
||||
}
|
||||
|
||||
// Connect to device via rustdesk:// protocol
|
||||
function connectDevice(deviceId) {
|
||||
window.location.href = `rustdesk://${deviceId}`;
|
||||
showToast(`Connecting to ${deviceId}...`);
|
||||
}
|
||||
|
||||
// Show device details modal
|
||||
function showDetails(deviceId) {
|
||||
const device = allDevices.find(d => d.id === deviceId);
|
||||
if (!device) return;
|
||||
|
||||
const isBanned = device.is_banned === true || device.is_banned === 1;
|
||||
|
||||
const detailsContent = document.getElementById('detailsContent');
|
||||
detailsContent.innerHTML = `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">ID:</div>
|
||||
<div class="detail-value">${escapeHtml(device.id)}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">GUID:</div>
|
||||
<div class="detail-value">${escapeHtml(device.guid) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">UUID:</div>
|
||||
<div class="detail-value">${escapeHtml(device.uuid) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Public Key:</div>
|
||||
<div class="detail-value">${escapeHtml(device.pk) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">User:</div>
|
||||
<div class="detail-value">${escapeHtml(device.user) || 'N/A'}</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
${isBanned ? `
|
||||
<div class="detail-item" style="background: rgba(231, 76, 60, 0.1); padding: 12px; border-radius: 8px; margin: 12px 0;">
|
||||
<div class="detail-label" style="color: #e74c3c; font-weight: bold;"><i class="fas fa-ban"></i> BAN STATUS:</div>
|
||||
<div class="detail-value" style="color: #e74c3c; font-weight: bold;">BANNED</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Banned At:</div>
|
||||
<div class="detail-value">${device.banned_at ? formatDate(device.banned_at) : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Banned By:</div>
|
||||
<div class="detail-value">${escapeHtml(device.banned_by) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Ban Reason:</div>
|
||||
<div class="detail-value">${escapeHtml(device.ban_reason) || 'No reason provided'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Note:</div>
|
||||
<div class="detail-value">${escapeHtml(device.note) || 'No note'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Created:</div>
|
||||
<div class="detail-value">${formatDate(device.created_at)}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Info:</div>
|
||||
<div class="detail-value">${escapeHtml(device.info) || 'N/A'}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal('detailsModal');
|
||||
}
|
||||
|
||||
// Edit device
|
||||
function editDevice(deviceId) {
|
||||
const device = allDevices.find(d => d.id === deviceId);
|
||||
if (!device) return;
|
||||
|
||||
currentDeviceId = deviceId;
|
||||
document.getElementById('editDeviceId').value = deviceId;
|
||||
document.getElementById('editNewId').value = '';
|
||||
document.getElementById('editNote').value = device.note || '';
|
||||
|
||||
openModal('editModal');
|
||||
}
|
||||
|
||||
// Save device changes
|
||||
async function saveDevice() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
const newId = document.getElementById('editNewId').value.trim();
|
||||
const note = document.getElementById('editNote').value.trim();
|
||||
|
||||
if (note.length > 500) {
|
||||
showToast('Note is too long (max 500 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newId && newId.length > 50) {
|
||||
showToast('Device ID is too long (max 50 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newId && !/^[a-zA-Z0-9_-]+$/.test(newId)) {
|
||||
showToast('Device ID can only contain letters, numbers, underscores and hyphens', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newId && newId !== currentDeviceId) {
|
||||
if (!confirm(`⚠️ WARNING: Changing device ID!\n\nOld ID: ${currentDeviceId}\nNew ID: ${newId}\n\nAre you sure?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const data = { note };
|
||||
if (newId) data.new_id = newId;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${currentDeviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Device updated successfully');
|
||||
closeEditModal();
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to update device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete device
|
||||
function deleteDevice(deviceId) {
|
||||
currentDeviceId = deviceId;
|
||||
document.getElementById('deleteDeviceId').textContent = deviceId;
|
||||
openModal('deleteModal');
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
async function confirmDelete() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
const device = allDevices.find(d => d.id === currentDeviceId);
|
||||
|
||||
if (!confirm(`⚠️ DELETE DEVICE: ${currentDeviceId}\n\nAre you absolutely sure?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${currentDeviceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Device deleted successfully');
|
||||
closeDeleteModal();
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to delete device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Copy public key to clipboard
|
||||
function copyPublicKey() {
|
||||
const keyText = document.getElementById('publicKeyDisplay').textContent;
|
||||
navigator.clipboard.writeText(keyText).then(() => {
|
||||
showToast('Public key copied to clipboard');
|
||||
}).catch(err => {
|
||||
console.error('Error copying:', err);
|
||||
showToast('Failed to copy public key', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh devices manually
|
||||
async function refreshDevices() {
|
||||
showToast('Refreshing devices...');
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
function openModal(modalId) {
|
||||
document.getElementById(modalId).classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
closeModal('editModal');
|
||||
currentDeviceId = null;
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
closeModal('deleteModal');
|
||||
currentDeviceId = null;
|
||||
}
|
||||
|
||||
function closeDetailsModal() {
|
||||
closeModal('detailsModal');
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Toast notification
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.getElementById('toast');
|
||||
const icon = toast.querySelector('i');
|
||||
|
||||
if (type === 'error') {
|
||||
icon.className = 'fas fa-exclamation-circle';
|
||||
icon.style.color = 'var(--danger-color)';
|
||||
} else {
|
||||
icon.className = 'fas fa-check-circle';
|
||||
icon.style.color = 'var(--success-color)';
|
||||
}
|
||||
|
||||
document.getElementById('toastMessage').textContent = message;
|
||||
toast.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
// Ban device
|
||||
async function banDevice(deviceId) {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
const reason = prompt(`⚠️ BAN DEVICE: ${deviceId}\n\nEnter ban reason (optional):`);
|
||||
|
||||
if (reason === null) return;
|
||||
|
||||
if (reason && reason.length > 500) {
|
||||
showToast('Ban reason is too long (max 500 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to BAN device ${deviceId}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${deviceId}/ban`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
reason: reason || '',
|
||||
banned_by: 'admin'
|
||||
})
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Device ${deviceId} banned successfully`);
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to ban device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Unban device
|
||||
async function unbanDevice(deviceId) {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
if (!confirm(`✓ UNBAN DEVICE: ${deviceId}\n\nAre you sure?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${deviceId}/unban`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Device ${deviceId} unbanned successfully`);
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to unban device', 'error');
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// PUBLIC KEY VERIFICATION
|
||||
// ============================================================================
|
||||
|
||||
async function verifyPasswordForKey() {
|
||||
const password = document.getElementById('keyPassword').value;
|
||||
|
||||
if (!password) {
|
||||
showToast('Please enter your password', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/key/verify', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ password: password })
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('publicKeyDisplay').textContent = result.key;
|
||||
document.getElementById('keyPasswordForm').style.display = 'none';
|
||||
document.getElementById('keyDisplay').style.display = 'block';
|
||||
document.getElementById('keyPassword').value = '';
|
||||
showToast('Public key revealed');
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
document.getElementById('keyPassword').value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to verify password', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyPublicKey() {
|
||||
const keyText = document.getElementById('publicKeyDisplay').textContent;
|
||||
navigator.clipboard.writeText(keyText).then(() => {
|
||||
showToast('Public key copied to clipboard');
|
||||
}).catch(err => {
|
||||
showToast('Failed to copy', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PASSWORD CHANGE
|
||||
// ============================================================================
|
||||
|
||||
function showChangePasswordModal() {
|
||||
document.getElementById('changePasswordModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeChangePasswordModal() {
|
||||
document.getElementById('changePasswordModal').classList.remove('show');
|
||||
document.getElementById('currentPassword').value = '';
|
||||
document.getElementById('newPassword').value = '';
|
||||
document.getElementById('confirmPassword').value = '';
|
||||
}
|
||||
|
||||
async function confirmChangePassword() {
|
||||
const currentPassword = document.getElementById('currentPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
showToast('All fields are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
showToast('New password must be at least 6 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast('New passwords do not match', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
old_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Password changed successfully');
|
||||
closeChangePasswordModal();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to change password', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
async function loadUsers() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
renderUsers(data.users);
|
||||
} else {
|
||||
showToast('Error loading users: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to load users', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="no-data">No users found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = users.map(user => {
|
||||
const statusBadge = user.is_active ?
|
||||
'<span class="badge badge-success">Active</span>' :
|
||||
'<span class="badge badge-danger">Inactive</span>';
|
||||
|
||||
const roleColor = user.role === 'admin' ? 'danger' :
|
||||
user.role === 'operator' ? 'warning' : 'info';
|
||||
const roleBadge = `<span class="badge badge-${roleColor}">${user.role}</span>`;
|
||||
|
||||
const createdDate = user.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A';
|
||||
const lastLogin = user.last_login ? new Date(user.last_login).toLocaleString() : 'Never';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${user.username}</td>
|
||||
<td>${roleBadge}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${createdDate}</td>
|
||||
<td>${lastLogin}</td>
|
||||
<td class="actions-column">
|
||||
<button class="btn-icon" onclick="showEditUserModal(${user.id}, '${user.username}', '${user.role}')" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn-icon danger" onclick="showDeleteUserModal(${user.id}, '${user.username}')" title="Delete">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
${user.is_active ?
|
||||
`<button class="btn-icon" onclick="toggleUserStatus(${user.id}, false)" title="Deactivate">
|
||||
<i class="fas fa-user-slash"></i>
|
||||
</button>` :
|
||||
`<button class="btn-icon success" onclick="toggleUserStatus(${user.id}, true)" title="Activate">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</button>`
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Add User Modal
|
||||
function showAddUserModal() {
|
||||
document.getElementById('addUserModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeAddUserModal() {
|
||||
document.getElementById('addUserModal').classList.remove('show');
|
||||
document.getElementById('newUsername').value = '';
|
||||
document.getElementById('newUserPassword').value = '';
|
||||
document.getElementById('newUserRole').value = 'viewer';
|
||||
}
|
||||
|
||||
async function confirmAddUser() {
|
||||
const username = document.getElementById('newUsername').value.trim();
|
||||
const password = document.getElementById('newUserPassword').value;
|
||||
const role = document.getElementById('newUserRole').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showToast('Username and password are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showToast('Password must be at least 6 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ username, password, role })
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`User ${username} created successfully`);
|
||||
closeAddUserModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to create user', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit User Modal
|
||||
function showEditUserModal(userId, username, role) {
|
||||
document.getElementById('editUserId').value = userId;
|
||||
document.getElementById('editUserUsername').value = username;
|
||||
document.getElementById('editUserRole').value = role;
|
||||
document.getElementById('resetUserPassword').value = '';
|
||||
document.getElementById('editUserModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeEditUserModal() {
|
||||
document.getElementById('editUserModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function confirmEditUser() {
|
||||
const userId = document.getElementById('editUserId').value;
|
||||
const role = document.getElementById('editUserRole').value;
|
||||
const password = document.getElementById('resetUserPassword').value;
|
||||
|
||||
try {
|
||||
// Change role
|
||||
const roleResponse = await fetch(`/api/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ action: 'change_role', role })
|
||||
});
|
||||
|
||||
if (handleAuthError(null, roleResponse)) return;
|
||||
|
||||
const roleResult = await roleResponse.json();
|
||||
|
||||
if (!roleResult.success) {
|
||||
showToast('Error: ' + roleResult.error, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset password if provided
|
||||
if (password && password.length >= 6) {
|
||||
const passResponse = await fetch(`/api/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ action: 'reset_password', password })
|
||||
});
|
||||
|
||||
const passResult = await passResponse.json();
|
||||
|
||||
if (!passResult.success) {
|
||||
showToast('Role updated but password reset failed: ' + passResult.error, 'error');
|
||||
closeEditUserModal();
|
||||
loadUsers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showToast('User updated successfully');
|
||||
closeEditUserModal();
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to update user', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete User Modal
|
||||
function showDeleteUserModal(userId, username) {
|
||||
document.getElementById('deleteUserId').value = userId;
|
||||
document.getElementById('deleteUserUsername').textContent = username;
|
||||
document.getElementById('deleteUserModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeDeleteUserModal() {
|
||||
document.getElementById('deleteUserModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function confirmDeleteUser() {
|
||||
const userId = document.getElementById('deleteUserId').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('User deleted successfully');
|
||||
closeDeleteUserModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to delete user', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle User Status
|
||||
async function toggleUserStatus(userId, activate) {
|
||||
const action = activate ? 'activate' : 'deactivate';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`User ${activate ? 'activated' : 'deactivated'} successfully`);
|
||||
loadUsers();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to change user status', 'error');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
// BetterDesk Console v1.5.0 - Enhanced UI with Sidebar Navigation
|
||||
// Global variables
|
||||
let allDevices = [];
|
||||
let currentDeviceId = null;
|
||||
let authToken = null;
|
||||
let userRole = null;
|
||||
let username = null;
|
||||
let publicKeyCache = null;
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check authentication
|
||||
if (!checkAuth()) return;
|
||||
|
||||
// Setup user info in sidebar
|
||||
setupUserInfo();
|
||||
|
||||
// Setup sidebar navigation
|
||||
setupSidebar();
|
||||
|
||||
// Load initial data
|
||||
loadDevices();
|
||||
loadStats();
|
||||
|
||||
// Auto-refresh dashboard every 5 seconds
|
||||
setInterval(() => {
|
||||
const dashboardSection = document.getElementById('dashboard');
|
||||
if (dashboardSection && dashboardSection.classList.contains('active')) {
|
||||
loadDevices();
|
||||
loadStats();
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Authentication check
|
||||
function checkAuth() {
|
||||
authToken = localStorage.getItem('authToken');
|
||||
userRole = localStorage.getItem('role');
|
||||
username = localStorage.getItem('username');
|
||||
|
||||
if (!authToken) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Setup user info in sidebar
|
||||
function setupUserInfo() {
|
||||
document.getElementById('sidebarUsername').textContent = username || 'User';
|
||||
document.getElementById('sidebarRole').textContent = userRole || 'viewer';
|
||||
|
||||
// Show admin-only sections
|
||||
if (userRole === 'admin') {
|
||||
document.querySelectorAll('.admin-only').forEach(el => {
|
||||
el.style.display = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setup sidebar navigation
|
||||
function setupSidebar() {
|
||||
document.querySelectorAll('.sidebar-item').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const sectionId = this.dataset.section;
|
||||
|
||||
// Update active states
|
||||
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Show selected section
|
||||
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById(sectionId).classList.add('active');
|
||||
|
||||
// Load section-specific data
|
||||
if (sectionId === 'users' && userRole === 'admin') {
|
||||
loadUsers();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get auth headers for API calls
|
||||
function getAuthHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
};
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
function handleAuthError(error, response) {
|
||||
if (response && response.status === 401) {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
window.location.href = '/login';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DASHBOARD - DEVICE MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
// Load devices from API
|
||||
async function loadDevices() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/devices', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
allDevices = data.devices;
|
||||
renderDevices(allDevices);
|
||||
updateNavStats(allDevices);
|
||||
} else {
|
||||
showToast('Error loading devices: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to load devices', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load statistics
|
||||
async function loadStats() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stats', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('statTotal').textContent = data.stats.total;
|
||||
document.getElementById('statActive').textContent = data.stats.active;
|
||||
document.getElementById('statInactive').textContent = data.stats.inactive;
|
||||
document.getElementById('statBanned').textContent = data.stats.banned || 0;
|
||||
document.getElementById('statNotes').textContent = data.stats.with_notes;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update navigation stats
|
||||
function updateNavStats(devices) {
|
||||
const total = devices.length;
|
||||
const active = devices.filter(d => d.online).length;
|
||||
|
||||
const totalDevicesEl = document.querySelector('#totalDevices span');
|
||||
const activeDevicesEl = document.querySelector('#activeDevices span');
|
||||
|
||||
if (totalDevicesEl) totalDevicesEl.textContent = total;
|
||||
if (activeDevicesEl) activeDevicesEl.textContent = active;
|
||||
}
|
||||
|
||||
// Render devices table
|
||||
function renderDevices(devices) {
|
||||
const tbody = document.getElementById('devicesTableBody');
|
||||
|
||||
if (devices.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="loading">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<span>No devices found</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = devices.map(device => {
|
||||
const isBanned = device.is_banned === true || device.is_banned === 1;
|
||||
const rowClass = isBanned ? 'style="opacity: 0.6; background: rgba(255, 0, 0, 0.05);"' : '';
|
||||
|
||||
const canEdit = userRole === 'admin' || userRole === 'operator';
|
||||
const canBan = userRole === 'admin' || userRole === 'operator';
|
||||
|
||||
return `
|
||||
<tr ${rowClass}>
|
||||
<td>
|
||||
<strong>${escapeHtml(device.id)}</strong>
|
||||
${isBanned ? '<br><span class="status-badge" style="background: #e74c3c; font-size: 0.75rem; margin-top: 4px;"><i class="fas fa-ban"></i> BANNED</span>' : ''}
|
||||
</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>
|
||||
</td>
|
||||
<td>${formatDate(device.created_at)}</td>
|
||||
<td>
|
||||
<button class="action-btn connect" onclick="connectDevice('${escapeHtml(device.id)}')" title="Connect" ${isBanned ? 'disabled style="opacity: 0.3; cursor: not-allowed;"' : ''}>
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
<button class="action-btn details" onclick="showDetails('${escapeHtml(device.id)}')" title="Details">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
${canEdit ? `
|
||||
<button class="action-btn edit" onclick="editDevice('${escapeHtml(device.id)}')" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
${canBan ? (isBanned ?
|
||||
`<button class="action-btn" onclick="unbanDevice('${escapeHtml(device.id)}')" title="Unban" style="background: #27ae60;">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>` :
|
||||
`<button class="action-btn" onclick="banDevice('${escapeHtml(device.id)}')" title="Ban" style="background: #e74c3c;">
|
||||
<i class="fas fa-ban"></i>
|
||||
</button>`
|
||||
) : ''}
|
||||
${canEdit ? `
|
||||
<button class="action-btn delete" onclick="deleteDevice('${escapeHtml(device.id)}')" title="Delete">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// Filter devices by search
|
||||
function filterDevices() {
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
renderDevices(allDevices);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = allDevices.filter(device =>
|
||||
device.id.toLowerCase().includes(searchTerm) ||
|
||||
(device.note && device.note.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
|
||||
renderDevices(filtered);
|
||||
}
|
||||
|
||||
// Connect to device
|
||||
function connectDevice(deviceId) {
|
||||
window.location.href = `rustdesk://${deviceId}`;
|
||||
showToast(`Connecting to ${deviceId}...`);
|
||||
}
|
||||
|
||||
// Show device details
|
||||
function showDetails(deviceId) {
|
||||
const device = allDevices.find(d => d.id === deviceId);
|
||||
if (!device) return;
|
||||
|
||||
const isBanned = device.is_banned === true || device.is_banned === 1;
|
||||
|
||||
const detailsContent = document.getElementById('detailsContent');
|
||||
detailsContent.innerHTML = `
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">ID:</div>
|
||||
<div class="detail-value">${escapeHtml(device.id)}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">GUID:</div>
|
||||
<div class="detail-value">${escapeHtml(device.guid) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">UUID:</div>
|
||||
<div class="detail-value">${escapeHtml(device.uuid) || 'N/A'}</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
${isBanned ? `
|
||||
<div class="detail-item" style="background: rgba(231, 76, 60, 0.1); padding: 12px; border-radius: 8px; margin: 12px 0;">
|
||||
<div class="detail-label" style="color: #e74c3c; font-weight: bold;"><i class="fas fa-ban"></i> BAN STATUS:</div>
|
||||
<div class="detail-value" style="color: #e74c3c; font-weight: bold;">BANNED</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Banned At:</div>
|
||||
<div class="detail-value">${device.banned_at ? formatDate(device.banned_at) : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Banned By:</div>
|
||||
<div class="detail-value">${escapeHtml(device.banned_by) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Ban Reason:</div>
|
||||
<div class="detail-value">${escapeHtml(device.ban_reason) || 'No reason provided'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Note:</div>
|
||||
<div class="detail-value">${escapeHtml(device.note) || 'No note'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Created:</div>
|
||||
<div class="detail-value">${formatDate(device.created_at)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal('detailsModal');
|
||||
}
|
||||
|
||||
// Edit device
|
||||
function editDevice(deviceId) {
|
||||
const device = allDevices.find(d => d.id === deviceId);
|
||||
if (!device) return;
|
||||
|
||||
currentDeviceId = deviceId;
|
||||
document.getElementById('editDeviceId').value = deviceId;
|
||||
document.getElementById('editNewId').value = '';
|
||||
document.getElementById('editNote').value = device.note || '';
|
||||
|
||||
openModal('editModal');
|
||||
}
|
||||
|
||||
// Save device changes
|
||||
async function saveDevice() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
const newId = document.getElementById('editNewId').value.trim();
|
||||
const note = document.getElementById('editNote').value.trim();
|
||||
|
||||
if (note.length > 500) {
|
||||
showToast('Note is too long (max 500 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { note };
|
||||
if (newId) data.new_id = newId;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${currentDeviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Device updated successfully');
|
||||
closeEditModal();
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to update device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete device
|
||||
function deleteDevice(deviceId) {
|
||||
currentDeviceId = deviceId;
|
||||
document.getElementById('deleteDeviceId').textContent = deviceId;
|
||||
openModal('deleteModal');
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
async function confirmDelete() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${currentDeviceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Device deleted successfully');
|
||||
closeDeleteModal();
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to delete device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Ban device
|
||||
async function banDevice(deviceId) {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
const reason = prompt(`⚠️ BAN DEVICE: ${deviceId}\n\nEnter ban reason (optional):`);
|
||||
if (reason === null) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${deviceId}/ban`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
reason: reason || '',
|
||||
banned_by: username
|
||||
})
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Device ${deviceId} banned successfully`);
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to ban device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Unban device
|
||||
async function unbanDevice(deviceId) {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
if (!confirm(`✓ UNBAN DEVICE: ${deviceId}\n\nAre you sure?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/device/${deviceId}/unban`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Device ${deviceId} unbanned successfully`);
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to unban device', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh devices manually
|
||||
async function refreshDevices() {
|
||||
showToast('Refreshing devices...');
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC KEY SECTION
|
||||
// ============================================================================
|
||||
|
||||
async function verifyPasswordForKey() {
|
||||
const password = document.getElementById('keyPassword').value;
|
||||
|
||||
if (!password) {
|
||||
showToast('Please enter your password', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-password', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ password: password })
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Password correct, fetch public key
|
||||
const keyResponse = await fetch('/api/public-key', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, keyResponse)) return;
|
||||
|
||||
const keyData = await keyResponse.json();
|
||||
|
||||
if (keyData.success) {
|
||||
publicKeyCache = keyData.key;
|
||||
document.getElementById('publicKeyDisplay').textContent = keyData.key;
|
||||
document.getElementById('keyPasswordPrompt').style.display = 'none';
|
||||
document.getElementById('keyContent').style.display = 'block';
|
||||
document.getElementById('keyPassword').value = '';
|
||||
showToast('Public key revealed');
|
||||
} else {
|
||||
showToast('Error loading key: ' + keyData.error, 'error');
|
||||
}
|
||||
} else {
|
||||
showToast('Incorrect password', 'error');
|
||||
document.getElementById('keyPassword').value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to verify password', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyPublicKey() {
|
||||
const keyText = document.getElementById('publicKeyDisplay').textContent;
|
||||
navigator.clipboard.writeText(keyText).then(() => {
|
||||
showToast('Public key copied to clipboard');
|
||||
}).catch(err => {
|
||||
showToast('Failed to copy', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function lockKey() {
|
||||
publicKeyCache = null;
|
||||
document.getElementById('keyPasswordPrompt').style.display = 'block';
|
||||
document.getElementById('keyContent').style.display = 'none';
|
||||
document.getElementById('keyPassword').value = '';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SETTINGS - PASSWORD CHANGE
|
||||
// ============================================================================
|
||||
|
||||
async function changePassword(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentPassword = document.getElementById('currentPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast('New passwords do not match', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
showToast('Password must be at least 8 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
old_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('Password changed successfully');
|
||||
document.getElementById('passwordForm').reset();
|
||||
// New token issued, update local storage
|
||||
if (result.token) {
|
||||
authToken = result.token;
|
||||
localStorage.setItem('authToken', result.token);
|
||||
}
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to change password', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER MANAGEMENT (ADMIN ONLY)
|
||||
// ============================================================================
|
||||
|
||||
async function loadUsers() {
|
||||
if (!checkAuth()) return;
|
||||
if (userRole !== 'admin') return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
renderUsers(data.users);
|
||||
} else {
|
||||
showToast('Error loading users: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to load users', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="loading">No users found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = users.map(user => {
|
||||
const statusClass = user.is_active ? 'status-active' : 'status-inactive';
|
||||
const statusText = user.is_active ? 'Active' : 'Inactive';
|
||||
|
||||
let roleClass = 'role-viewer';
|
||||
if (user.role === 'admin') roleClass = 'role-admin';
|
||||
else if (user.role === 'operator') roleClass = 'role-operator';
|
||||
|
||||
const lastLogin = user.last_login ? formatDate(user.last_login) : 'Never';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(user.username)}</strong></td>
|
||||
<td><span class="role-badge ${roleClass}">${user.role}</span></td>
|
||||
<td>${lastLogin}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td>
|
||||
<button class="action-btn edit" onclick="showEditUserModal(${user.id}, '${escapeHtml(user.username)}', '${user.role}', ${user.is_active})" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" onclick="showDeleteUserModal(${user.id}, '${escapeHtml(user.username)}')" title="Delete">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Add User Modal
|
||||
function showAddUserModal() {
|
||||
openModal('addUserModal');
|
||||
}
|
||||
|
||||
function closeAddUserModal() {
|
||||
closeModal('addUserModal');
|
||||
document.getElementById('newUsername').value = '';
|
||||
document.getElementById('newUserPassword').value = '';
|
||||
document.getElementById('newUserRole').value = 'viewer';
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const username = document.getElementById('newUsername').value.trim();
|
||||
const password = document.getElementById('newUserPassword').value;
|
||||
const role = document.getElementById('newUserRole').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showToast('Username and password are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
showToast('Password must be at least 8 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ username, password, role })
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(`User ${username} created successfully`);
|
||||
closeAddUserModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to create user', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit/Delete User (placeholders - to be implemented with proper modals)
|
||||
function showEditUserModal(userId, username, role, isActive) {
|
||||
// TODO: Implement edit user modal
|
||||
showToast('Edit user functionality - coming soon');
|
||||
}
|
||||
|
||||
function showDeleteUserModal(userId, username) {
|
||||
if (!confirm(`⚠️ DELETE USER: ${username}\n\nAre you sure?`)) return;
|
||||
deleteUser(userId);
|
||||
}
|
||||
|
||||
async function deleteUser(userId) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (handleAuthError(null, response)) return;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('User deleted successfully');
|
||||
loadUsers();
|
||||
} else {
|
||||
showToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showToast('Failed to delete user', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MODAL FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function openModal(modalId) {
|
||||
document.getElementById(modalId).classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
closeModal('editModal');
|
||||
currentDeviceId = null;
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
closeModal('deleteModal');
|
||||
currentDeviceId = null;
|
||||
}
|
||||
|
||||
function closeDetailsModal() {
|
||||
closeModal('detailsModal');
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.getElementById('toast');
|
||||
const icon = toast.querySelector('i');
|
||||
|
||||
if (type === 'error') {
|
||||
icon.className = 'fas fa-exclamation-circle';
|
||||
icon.style.color = 'var(--danger-color)';
|
||||
} else {
|
||||
icon.className = 'fas fa-check-circle';
|
||||
icon.style.color = 'var(--success-color)';
|
||||
}
|
||||
|
||||
document.getElementById('toastMessage').textContent = message;
|
||||
toast.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/* Sidebar Styles for BetterDesk Console v1.4.0 */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
}
|
||||
|
||||
/* Layout adjustments for sidebar */
|
||||
body.has-sidebar {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar Container */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Sidebar Header */
|
||||
.sidebar-header {
|
||||
padding: 24px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-brand i {
|
||||
font-size: 28px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
/* Text always visible */
|
||||
}
|
||||
|
||||
/* Toggle button removed - sidebar always expanded */
|
||||
|
||||
/* Sidebar User Section */
|
||||
.sidebar-user {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Sidebar Menu */
|
||||
.sidebar-menu {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-item i {
|
||||
font-size: 18px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-item span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 60%;
|
||||
background: white;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
padding: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: #ff6b6b !important;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: rgba(255, 107, 107, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Top Navbar */
|
||||
.top-navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-badge i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stat-badge.active {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
/* Content Container */
|
||||
.content-container {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-content.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Action Bar */
|
||||
.action-bar {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
:root {
|
||||
--sidebar-width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nav-stats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Overlay when sidebar is open on mobile */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.mobile-open ~ .sidebar-overlay {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings Page Styles */
|
||||
.settings-container {
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
color: white;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Key Container */
|
||||
.key-container {
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.key-display {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
margin: 16px 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.key-display code {
|
||||
color: #4caf50;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* About Container */
|
||||
.about-container {
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.about-container h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.about-container p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.about-container ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.about-container li {
|
||||
padding: 8px 0;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.about-container li::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #4caf50;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/* Sidebar JavaScript for BetterDesk Console v1.4.0 */
|
||||
|
||||
// Initialize sidebar
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeSidebar();
|
||||
loadUserInfo();
|
||||
setupMenuNavigation();
|
||||
setupMobileMenu();
|
||||
});
|
||||
|
||||
function initializeSidebar() {
|
||||
// Sidebar is always expanded - no toggle needed
|
||||
console.log('Sidebar initialized (always expanded)');
|
||||
}
|
||||
|
||||
function loadUserInfo() {
|
||||
const username = localStorage.getItem('username') || 'User';
|
||||
const role = localStorage.getItem('role') || 'viewer';
|
||||
|
||||
// Update sidebar user info
|
||||
const usernameEl = document.getElementById('sidebarUsername');
|
||||
const userRoleEl = document.getElementById('sidebarUserRole');
|
||||
|
||||
if (usernameEl) {
|
||||
usernameEl.textContent = username;
|
||||
}
|
||||
|
||||
if (userRoleEl) {
|
||||
const roleNames = {
|
||||
'admin': 'Administrator',
|
||||
'operator': 'Operator',
|
||||
'viewer': 'Viewer'
|
||||
};
|
||||
userRoleEl.textContent = roleNames[role] || role;
|
||||
}
|
||||
|
||||
// Show/hide menu items based on role
|
||||
updateMenuVisibility(role);
|
||||
}
|
||||
|
||||
function updateMenuVisibility(role) {
|
||||
const menuUsers = document.getElementById('menuUsers');
|
||||
const menuAudit = document.getElementById('menuAudit');
|
||||
const menuSettings = document.getElementById('menuSettings');
|
||||
const menuKey = document.getElementById('menuKey');
|
||||
|
||||
// Admin sees everything
|
||||
if (role === 'admin') {
|
||||
if (menuUsers) menuUsers.style.display = 'flex';
|
||||
if (menuAudit) menuAudit.style.display = 'flex';
|
||||
if (menuSettings) menuSettings.style.display = 'flex';
|
||||
if (menuKey) menuKey.style.display = 'flex';
|
||||
}
|
||||
// Operator sees audit and settings
|
||||
else if (role === 'operator') {
|
||||
if (menuUsers) menuUsers.style.display = 'none';
|
||||
if (menuAudit) menuAudit.style.display = 'flex';
|
||||
if (menuSettings) menuSettings.style.display = 'flex';
|
||||
if (menuKey) menuKey.style.display = 'none';
|
||||
}
|
||||
// Viewer sees only settings
|
||||
else {
|
||||
if (menuUsers) menuUsers.style.display = 'none';
|
||||
if (menuAudit) menuAudit.style.display = 'none';
|
||||
if (menuSettings) menuSettings.style.display = 'flex';
|
||||
if (menuKey) menuKey.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setupMenuNavigation() {
|
||||
const menuItems = document.querySelectorAll('.menu-item[data-page]');
|
||||
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const page = this.dataset.page;
|
||||
|
||||
// Update active menu item
|
||||
menuItems.forEach(mi => mi.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Show corresponding page
|
||||
showPage(page);
|
||||
|
||||
// Close mobile menu if open
|
||||
closeMobileMenu();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showPage(pageName) {
|
||||
// Hide all pages
|
||||
const pages = document.querySelectorAll('.page-content');
|
||||
pages.forEach(page => page.classList.remove('active'));
|
||||
|
||||
// Show selected page
|
||||
const targetPage = document.getElementById(pageName + 'Page');
|
||||
if (targetPage) {
|
||||
targetPage.classList.add('active');
|
||||
}
|
||||
|
||||
// Update page title
|
||||
const pageTitles = {
|
||||
'dashboard': 'Device Management',
|
||||
'users': 'User Management',
|
||||
'audit': 'Audit Log',
|
||||
'settings': 'Settings',
|
||||
'key': 'Public Key',
|
||||
'about': 'About BetterDesk'
|
||||
};
|
||||
|
||||
const pageTitle = document.getElementById('pageTitle');
|
||||
if (pageTitle && pageTitles[pageName]) {
|
||||
pageTitle.textContent = pageTitles[pageName];
|
||||
}
|
||||
|
||||
// Load page-specific data
|
||||
if (pageName === 'dashboard') {
|
||||
if (typeof refreshDevices === 'function') {
|
||||
refreshDevices();
|
||||
}
|
||||
} else if (pageName === 'users') {
|
||||
if (typeof loadUsers === 'function') {
|
||||
loadUsers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupMobileMenu() {
|
||||
const mobileToggle = document.getElementById('mobileMenuToggle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
if (mobileToggle) {
|
||||
mobileToggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
|
||||
// Create/remove overlay
|
||||
if (sidebar.classList.contains('mobile-open')) {
|
||||
createOverlay();
|
||||
} else {
|
||||
removeOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createOverlay() {
|
||||
const existing = document.querySelector('.sidebar-overlay');
|
||||
if (existing) return;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'sidebar-overlay';
|
||||
overlay.addEventListener('click', closeMobileMenu);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function removeOverlay() {
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
sidebar.classList.remove('mobile-open');
|
||||
removeOverlay();
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (!confirm('Are you sure you want to logout?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('authToken');
|
||||
|
||||
// Call logout API
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
|
||||
// Clear local storage
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
|
||||
// Redirect to login
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
function showChangePasswordModal() {
|
||||
// TODO: Implement change password modal
|
||||
alert('Change password functionality coming soon!');
|
||||
}
|
||||
|
||||
// Export functions for use in other scripts
|
||||
window.sidebarFunctions = {
|
||||
showPage,
|
||||
logout,
|
||||
loadUserInfo,
|
||||
updateMenuVisibility
|
||||
};
|
||||
@@ -699,3 +699,173 @@ textarea.form-control {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
/* About Page Styles */
|
||||
.about-container {
|
||||
padding: 2rem;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.about-container h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.about-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.about-section h3 {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.about-section p {
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.about-section ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.about-section ul li {
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.about-section ul li i {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.25rem;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.about-section ul li strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(107, 114, 128, 0.4);
|
||||
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
|
||||
}
|
||||
|
||||
.github-link i {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* User Management Styles */
|
||||
.users-container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.users-container h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
/* Badge Styles */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: var(--warning-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: var(--info-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Settings Page Styles */
|
||||
.settings-container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-container h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(30, 30, 30, 0.5);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.settings-section p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Key Page Styles */
|
||||
.key-container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.key-container h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RustDesk Console - Dashboard</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Background gradient -->
|
||||
<div class="bg-gradient"></div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar glass-effect">
|
||||
<div class="nav-content">
|
||||
<div class="nav-brand">
|
||||
<i class="fas fa-desktop"></i>
|
||||
<span>RustDesk Console</span>
|
||||
</div>
|
||||
<div class="nav-stats">
|
||||
<div class="stat-badge" id="totalDevices">
|
||||
<i class="fas fa-server"></i>
|
||||
<span>0</span>
|
||||
</div>
|
||||
<div class="stat-badge active" id="activeDevices">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
<span>0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="container">
|
||||
<!-- Header Section -->
|
||||
<div class="header-section">
|
||||
<h1 class="page-title fade-in">Device Management</h1>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" onclick="showPublicKey()">
|
||||
<i class="fas fa-key"></i>
|
||||
Public Key
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="refreshDevices()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid fade-in" style="animation-delay: 0.1s;">
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Total Devices</div>
|
||||
<div class="stat-value" id="statTotal">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Active</div>
|
||||
<div class="stat-value" id="statActive">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<i class="fas fa-circle-xmark"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Inactive</div>
|
||||
<div class="stat-value" id="statInactive">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<i class="fas fa-ban"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Banned</div>
|
||||
<div class="stat-value" id="statBanned">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">With Notes</div>
|
||||
<div class="stat-value" id="statNotes">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Devices Table -->
|
||||
<div class="table-container glass-effect fade-in" style="animation-delay: 0.2s;">
|
||||
<div class="table-header">
|
||||
<h2><i class="fas fa-list"></i> Devices</h2>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="searchInput" placeholder="Search devices..." onkeyup="filterDevices()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="devices-table" id="devicesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Note</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="devicesTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading devices...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Device Modal -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-edit"></i> Edit Device</h2>
|
||||
<button class="modal-close" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="editDeviceId">Device ID</label>
|
||||
<input type="text" id="editDeviceId" readonly class="form-control">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editNote">Note</label>
|
||||
<textarea id="editNote" class="form-control" rows="3" placeholder="Enter device note..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeEditModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveDevice()">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="deleteModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-trash-alt"></i> Confirm Delete</h2>
|
||||
<button class="modal-close" onclick="closeDeleteModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete device <strong id="deleteDeviceId"></strong>?</p>
|
||||
<p class="warning-text">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDeleteModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal -->
|
||||
<div id="detailsModal" class="modal">
|
||||
<div class="modal-content glass-effect modal-large">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-info-circle"></i> Device Details</h2>
|
||||
<button class="modal-close" onclick="closeDetailsModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="detailsContent" class="details-grid"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDetailsModal()">
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Key Modal -->
|
||||
<div id="keyModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-key"></i> RustDesk Public Key</h2>
|
||||
<button class="modal-close" onclick="closeKeyModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="key-display">
|
||||
<code id="publicKeyDisplay">{{ public_key }}</code>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" onclick="copyPublicKey()">
|
||||
<i class="fas fa-copy"></i> Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
<div id="toast" class="toast glass-effect">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span id="toastMessage"></span>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BetterDesk Console - Dashboard</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='sidebar.css') }}">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<!-- Background gradient -->
|
||||
<div class="bg-gradient"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar glass-effect" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-brand">
|
||||
<i class="fas fa-desktop"></i>
|
||||
<span class="brand-text">BetterDesk</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-user">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="sidebarUsername">Admin</div>
|
||||
<div class="user-role" id="sidebarUserRole">Administrator</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-menu">
|
||||
<a href="#" class="menu-item active" data-page="dashboard">
|
||||
<i class="fas fa-gauge"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-page="users" id="menuUsers" style="display: none;">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>User Management</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-page="audit" id="menuAudit" style="display: none;">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
<span>Audit Log</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-page="settings" id="menuSettings">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-page="key" id="menuKey">
|
||||
<i class="fas fa-key"></i>
|
||||
<span>Public Key</span>
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-page="about">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>About</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="menu-item logout-btn" onclick="logout()">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<!-- Top Navigation -->
|
||||
<nav class="top-navbar glass-effect">
|
||||
<div class="nav-content">
|
||||
<button class="mobile-menu-toggle" id="mobileMenuToggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<h1 class="page-title" id="pageTitle">Device Management</h1>
|
||||
<div class="nav-stats">
|
||||
<div class="stat-badge" id="topTotalDevices">
|
||||
<i class="fas fa-server"></i>
|
||||
<span>0</span>
|
||||
</div>
|
||||
<div class="stat-badge active" id="topActiveDevices">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
<span>0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content Container -->
|
||||
<div class="content-container">
|
||||
<!-- Dashboard Page -->
|
||||
<div class="page-content active" id="dashboardPage">
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary" onclick="refreshDevices()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid fade-in">
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Total Devices</div>
|
||||
<div class="stat-value" id="statTotal">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Active</div>
|
||||
<div class="stat-value" id="statActive">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<i class="fas fa-circle-xmark"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Inactive</div>
|
||||
<div class="stat-value" id="statInactive">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<i class="fas fa-ban"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Banned</div>
|
||||
<div class="stat-value" id="statBanned">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">With Notes</div>
|
||||
<div class="stat-value" id="statNotes">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Devices Table -->
|
||||
<div class="table-container glass-effect">
|
||||
<div class="table-header">
|
||||
<h2><i class="fas fa-list"></i> Devices</h2>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="searchInput" placeholder="Search devices..." onkeyup="filterDevices()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="devices-table" id="devicesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Note</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="devicesTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading devices...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other pages will be added here -->
|
||||
<div class="page-content" id="devicesPage">
|
||||
<p>Devices management page (to be implemented)</p>
|
||||
</div>
|
||||
|
||||
<div class="page-content" id="usersPage">
|
||||
<div class="users-container glass-effect">
|
||||
<h2><i class="fas fa-users"></i> User Management</h2>
|
||||
<div class="action-bar" style="margin-bottom: 20px;">
|
||||
<button class="btn btn-primary" onclick="showAddUserModal()">
|
||||
<i class="fas fa-user-plus"></i> Add New User
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="devices-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Last Login</th>
|
||||
<th class="actions-column">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<tr>
|
||||
<td colspan="6" class="no-data">Loading users...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content" id="auditPage">
|
||||
<div class="audit-container glass-effect">
|
||||
<h2><i class="fas fa-clipboard-list"></i> Audit Log</h2>
|
||||
<p>Audit log page (to be implemented)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content" id="settingsPage">
|
||||
<div class="settings-container glass-effect">
|
||||
<h2><i class="fas fa-cog"></i> Settings</h2>
|
||||
<div class="settings-section">
|
||||
<h3>Account Security</h3>
|
||||
<p>Change your account password to keep your account secure.</p>
|
||||
<button class="btn btn-primary" onclick="showChangePasswordModal()">
|
||||
<i class="fas fa-lock"></i> Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content" id="keyPage">
|
||||
<div class="key-container glass-effect">
|
||||
<h2><i class="fas fa-key"></i> RustDesk Public Key</h2>
|
||||
<p style="margin-bottom: 20px;">For security reasons, please verify your password to view the public key.</p>
|
||||
<div class="form-group" id="keyPasswordForm">
|
||||
<label for="keyPassword">Enter Your Password</label>
|
||||
<input type="password" id="keyPassword" class="form-control" placeholder="Your password">
|
||||
<button class="btn btn-primary" onclick="verifyPasswordForKey()" style="margin-top: 15px;">
|
||||
<i class="fas fa-unlock"></i> Show Public Key
|
||||
</button>
|
||||
</div>
|
||||
<div id="keyDisplay" style="display: none;">
|
||||
<div class="key-display">
|
||||
<code id="publicKeyDisplay"></code>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="copyPublicKey()">
|
||||
<i class="fas fa-copy"></i> Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content" id="aboutPage">
|
||||
<div class="about-container glass-effect">
|
||||
<h2><i class="fas fa-info-circle"></i> About BetterDesk Console</h2>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>Version Information</h3>
|
||||
<p><strong>Version:</strong> 1.4.0</p>
|
||||
<p><strong>Build:</strong> v9</p>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><i class="fas fa-check-circle"></i> Real-time device monitoring and management</li>
|
||||
<li><i class="fas fa-check-circle"></i> Bidirectional ban enforcement (source + target)</li>
|
||||
<li><i class="fas fa-check-circle"></i> User authentication & role-based access control</li>
|
||||
<li><i class="fas fa-check-circle"></i> Comprehensive audit logging</li>
|
||||
<li><i class="fas fa-check-circle"></i> HTTP API for device status</li>
|
||||
<li><i class="fas fa-check-circle"></i> Modern glassmorphism UI design</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>Built With Open Source</h3>
|
||||
<p>This project is built using the following open source technologies:</p>
|
||||
<ul>
|
||||
<li><strong>RustDesk</strong> - Open source remote desktop software (AGPL-3.0)</li>
|
||||
<li><strong>Flask</strong> - Python web framework (BSD-3-Clause)</li>
|
||||
<li><strong>SQLite</strong> - Embedded database (Public Domain)</li>
|
||||
<li><strong>bcrypt</strong> - Password hashing library (Apache-2.0)</li>
|
||||
<li><strong>Font Awesome</strong> - Icon library (Font Awesome Free License)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>Repository & Documentation</h3>
|
||||
<p><strong>GitHub Repository:</strong></p>
|
||||
<p>
|
||||
<a href="https://github.com/UNITRONIX/Rustdesk-FreeConsole" target="_blank" class="github-link">
|
||||
<i class="fab fa-github"></i> github.com/UNITRONIX/Rustdesk-FreeConsole
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin-top: 15px;">
|
||||
Find documentation, installation guides, and source code in the repository.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>License</h3>
|
||||
<p>MIT License - Free to use and modify</p>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>Author</h3>
|
||||
<p>Developed by <strong>UNITRONIX</strong></p>
|
||||
<p>With contributions from the open source community</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals (same as before) -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-edit"></i> Edit Device</h2>
|
||||
<button class="modal-close" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="editDeviceId">Device ID</label>
|
||||
<input type="text" id="editDeviceId" readonly class="form-control">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editNote">Note</label>
|
||||
<textarea id="editNote" class="form-control" rows="3" placeholder="Enter device note..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeEditModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveDevice()">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="deleteModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-trash-alt"></i> Confirm Delete</h2>
|
||||
<button class="modal-close" onclick="closeDeleteModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete device <strong id="deleteDeviceId"></strong>?</p>
|
||||
<p class="warning-text">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDeleteModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="detailsModal" class="modal">
|
||||
<div class="modal-content glass-effect modal-large">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-info-circle"></i> Device Details</h2>
|
||||
<button class="modal-close" onclick="closeDetailsModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="detailsContent" class="details-grid"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDetailsModal()">
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div id="changePasswordModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-lock"></i> Change Password</h2>
|
||||
<button class="modal-close" onclick="closeChangePasswordModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword">Current Password</label>
|
||||
<input type="password" id="currentPassword" class="form-control" placeholder="Enter current password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">New Password</label>
|
||||
<input type="password" id="newPassword" class="form-control" placeholder="Enter new password (min 6 characters)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm New Password</label>
|
||||
<input type="password" id="confirmPassword" class="form-control" placeholder="Confirm new password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeChangePasswordModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="confirmChangePassword()">
|
||||
<i class="fas fa-save"></i> Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<div id="addUserModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-user-plus"></i> Add New User</h2>
|
||||
<button class="modal-close" onclick="closeAddUserModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="newUsername">Username</label>
|
||||
<input type="text" id="newUsername" class="form-control" placeholder="Enter username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newUserPassword">Password</label>
|
||||
<input type="password" id="newUserPassword" class="form-control" placeholder="Enter password (min 6 characters)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newUserRole">Role</label>
|
||||
<select id="newUserRole" class="form-control">
|
||||
<option value="viewer">Viewer (Read-only)</option>
|
||||
<option value="operator">Operator (Can ban/unban devices)</option>
|
||||
<option value="admin">Administrator (Full access)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeAddUserModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="confirmAddUser()">
|
||||
<i class="fas fa-save"></i> Create User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit User Modal -->
|
||||
<div id="editUserModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-user-edit"></i> Edit User</h2>
|
||||
<button class="modal-close" onclick="closeEditUserModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editUserId">
|
||||
<div class="form-group">
|
||||
<label for="editUserUsername">Username</label>
|
||||
<input type="text" id="editUserUsername" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editUserRole">Role</label>
|
||||
<select id="editUserRole" class="form-control">
|
||||
<option value="viewer">Viewer (Read-only)</option>
|
||||
<option value="operator">Operator (Can ban/unban devices)</option>
|
||||
<option value="admin">Administrator (Full access)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="resetUserPassword">Reset Password (optional)</label>
|
||||
<input type="password" id="resetUserPassword" class="form-control" placeholder="Leave empty to keep current password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeEditUserModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="confirmEditUser()">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete User Modal -->
|
||||
<div id="deleteUserModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-user-times"></i> Delete User</h2>
|
||||
<button class="modal-close" onclick="closeDeleteUserModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="deleteUserId">
|
||||
<p>Are you sure you want to delete user <strong id="deleteUserUsername"></strong>?</p>
|
||||
<p class="warning-text">This action cannot be undone. All user sessions will be terminated.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDeleteUserModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="confirmDeleteUser()">
|
||||
<i class="fas fa-trash-alt"></i> Delete User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
<div id="toast" class="toast glass-effect">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span id="toastMessage"></span>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script_v14.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='sidebar.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,829 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BetterDesk Console v1.5</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 260px;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 20px 0;
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 20px 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-logo i {
|
||||
font-size: 1.5rem;
|
||||
color: var(--primary-color, #667eea);
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.sidebar-user-name {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sidebar-user-role {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 4px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
color: #e74c3c;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
margin-left: 260px;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Public Key Section */
|
||||
.public-key-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.password-prompt {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.password-prompt h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.password-prompt input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.key-display-box {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.key-display-box code {
|
||||
color: #4facfe;
|
||||
word-break: break-all;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Settings Section */
|
||||
.settings-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-card h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #667eea);
|
||||
}
|
||||
|
||||
/* User Management Section */
|
||||
.users-table-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.users-table th {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.users-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.role-admin {
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.role-operator {
|
||||
background: rgba(52, 152, 219, 0.2);
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.role-viewer {
|
||||
background: rgba(149, 165, 166, 0.2);
|
||||
color: #95a5a6;
|
||||
}
|
||||
|
||||
/* About Section */
|
||||
.about-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.about-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.about-card h3 {
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.about-card p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.about-card a {
|
||||
color: var(--primary-color, #667eea);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.about-card a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.license-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.license-list li {
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.license-list li strong {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Background gradient -->
|
||||
<div class="bg-gradient"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">
|
||||
<i class="fas fa-shield-halved"></i>
|
||||
<span>BetterDesk v1.5</span>
|
||||
</div>
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-name" id="sidebarUsername">Loading...</div>
|
||||
<div class="sidebar-user-role" id="sidebarRole">...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-menu">
|
||||
<a class="sidebar-item active" data-section="dashboard">
|
||||
<i class="fas fa-th-large"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a class="sidebar-item" data-section="publickey">
|
||||
<i class="fas fa-key"></i>
|
||||
<span>Public Key</span>
|
||||
</a>
|
||||
<a class="sidebar-item" data-section="settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a class="sidebar-item admin-only" data-section="users" style="display: none;">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
<span>User Management</span>
|
||||
</a>
|
||||
<a class="sidebar-item" data-section="about">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>About</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="logout-btn" onclick="logout()">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<!-- Dashboard Section -->
|
||||
<div class="content-section active" id="dashboard">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar glass-effect">
|
||||
<div class="nav-content">
|
||||
<div class="nav-brand">
|
||||
<i class="fas fa-desktop"></i>
|
||||
<span>Device Management</span>
|
||||
</div>
|
||||
<div class="nav-stats">
|
||||
<div class="stat-badge" id="totalDevices">
|
||||
<i class="fas fa-server"></i>
|
||||
<span>0</span>
|
||||
</div>
|
||||
<div class="stat-badge active" id="activeDevices">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
<span>0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Container -->
|
||||
<div class="container">
|
||||
<!-- Header Section -->
|
||||
<div class="header-section">
|
||||
<h1 class="page-title fade-in">Device Dashboard</h1>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="refreshDevices()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid fade-in" style="animation-delay: 0.1s;">
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Total Devices</div>
|
||||
<div class="stat-value" id="statTotal">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Active</div>
|
||||
<div class="stat-value" id="statActive">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<i class="fas fa-circle-xmark"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Inactive</div>
|
||||
<div class="stat-value" id="statInactive">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||
<i class="fas fa-ban"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">Banned</div>
|
||||
<div class="stat-value" id="statBanned">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card glass-effect">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">With Notes</div>
|
||||
<div class="stat-value" id="statNotes">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Devices Table -->
|
||||
<div class="table-container glass-effect fade-in" style="animation-delay: 0.2s;">
|
||||
<div class="table-header">
|
||||
<h2><i class="fas fa-list"></i> Connected Devices</h2>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="searchInput" placeholder="Search devices..." onkeyup="filterDevices()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="devices-table" id="devicesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Note</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="devicesTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading devices...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Key Section -->
|
||||
<div class="content-section" id="publickey">
|
||||
<div class="public-key-container">
|
||||
<h1 class="page-title">Server Public Key</h1>
|
||||
|
||||
<div id="keyPasswordPrompt" class="password-prompt">
|
||||
<h3><i class="fas fa-lock"></i> Protected Content</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.7); margin-bottom: 20px;">
|
||||
Enter your password to view the server public key
|
||||
</p>
|
||||
<input type="password" id="keyPassword" placeholder="Enter your password" onkeypress="if(event.key==='Enter') verifyPasswordForKey()">
|
||||
<button class="btn btn-primary" onclick="verifyPasswordForKey()">
|
||||
<i class="fas fa-unlock"></i> Unlock Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="keyContent" style="display: none;">
|
||||
<div class="key-display-box">
|
||||
<h3 style="margin-bottom: 15px;">RustDesk Public Key</h3>
|
||||
<code id="publicKeyDisplay"></code>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" onclick="copyPublicKey()">
|
||||
<i class="fas fa-copy"></i> Copy to Clipboard
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-block" onclick="lockKey()" style="margin-top: 10px;">
|
||||
<i class="fas fa-lock"></i> Lock Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<div class="content-section" id="settings">
|
||||
<div class="settings-container">
|
||||
<h1 class="page-title">Account Settings</h1>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3><i class="fas fa-key"></i> Change Password</h3>
|
||||
<form id="passwordForm" onsubmit="changePassword(event)">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword">Current Password</label>
|
||||
<input type="password" id="currentPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">New Password</label>
|
||||
<input type="password" id="newPassword" required minlength="8">
|
||||
<small style="color: rgba(255, 255, 255, 0.6); font-size: 0.8rem;">
|
||||
Minimum 8 characters, must contain letters and numbers
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm New Password</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Update Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Section (Admin Only) -->
|
||||
<div class="content-section admin-only" id="users" style="display: none;">
|
||||
<div class="container">
|
||||
<h1 class="page-title">User Management</h1>
|
||||
|
||||
<div class="header-section">
|
||||
<h2></h2>
|
||||
<button class="btn btn-primary" onclick="showAddUserModal()">
|
||||
<i class="fas fa-user-plus"></i> Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="users-table-container">
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Last Login</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading users...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Section -->
|
||||
<div class="content-section" id="about">
|
||||
<div class="about-container">
|
||||
<h1 class="page-title">About BetterDesk Console</h1>
|
||||
|
||||
<div class="about-card">
|
||||
<h3><i class="fas fa-info-circle"></i> Project Information</h3>
|
||||
<p>
|
||||
<strong>BetterDesk Console v1.5.0</strong> - Advanced management interface for RustDesk Server
|
||||
</p>
|
||||
<p>
|
||||
A powerful, secure web console for managing RustDesk remote desktop server. Features include
|
||||
device management, user authentication with role-based access control, ban enforcement with
|
||||
fail-closed security policy, and comprehensive audit logging.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<h3><i class="fab fa-github"></i> Source Code</h3>
|
||||
<p>
|
||||
This project is open source and available on GitHub:
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/UNITRONIX/Rustdesk-FreeConsole" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i> github.com/UNITRONIX/Rustdesk-FreeConsole
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin-top: 15px; font-size: 0.9rem; color: rgba(255, 255, 255, 0.7);">
|
||||
Issues, feature requests, and contributions are welcome!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<h3><i class="fas fa-code-branch"></i> Open Source Components</h3>
|
||||
<p>This project is built with the following open source software:</p>
|
||||
<ul class="license-list">
|
||||
<li>
|
||||
<strong>RustDesk Server</strong> - AGPL-3.0 License<br>
|
||||
<small>Open source remote desktop server</small><br>
|
||||
<a href="https://github.com/rustdesk/rustdesk-server" target="_blank">github.com/rustdesk/rustdesk-server</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Flask</strong> - BSD-3-Clause License<br>
|
||||
<small>Lightweight web application framework for Python</small><br>
|
||||
<a href="https://flask.palletsprojects.com/" target="_blank">flask.palletsprojects.com</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Flask-WTF</strong> - BSD License<br>
|
||||
<small>CSRF protection and form validation</small><br>
|
||||
<a href="https://flask-wtf.readthedocs.io/" target="_blank">flask-wtf.readthedocs.io</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Flask-Limiter</strong> - MIT License<br>
|
||||
<small>Rate limiting extension for Flask</small><br>
|
||||
<a href="https://flask-limiter.readthedocs.io/" target="_blank">flask-limiter.readthedocs.io</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>bcrypt</strong> - Apache License 2.0<br>
|
||||
<small>Password hashing library</small><br>
|
||||
<a href="https://github.com/pyca/bcrypt/" target="_blank">github.com/pyca/bcrypt</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>SQLite</strong> - Public Domain<br>
|
||||
<small>Embedded database engine</small><br>
|
||||
<a href="https://www.sqlite.org/" target="_blank">sqlite.org</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Font Awesome</strong> - SIL OFL 1.1 / MIT License<br>
|
||||
<small>Icon library</small><br>
|
||||
<a href="https://fontawesome.com/" target="_blank">fontawesome.com</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<h3><i class="fas fa-shield-alt"></i> Security Features</h3>
|
||||
<ul class="license-list">
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>Authentication System</strong> - Secure login with bcrypt password hashing</li>
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>Role-Based Access Control</strong> - Admin, Operator, and Viewer roles</li>
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>CSRF Protection</strong> - Cross-Site Request Forgery prevention</li>
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>Rate Limiting</strong> - Protection against brute force attacks</li>
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>Fail-Closed Policy</strong> - Banned devices cannot connect even if service restarts</li>
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>Audit Logging</strong> - Complete history of user actions</li>
|
||||
<li><i class="fas fa-check" style="color: #27ae60;"></i> <strong>Content Security Policy</strong> - Protection against XSS attacks</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="about-card">
|
||||
<h3><i class="fas fa-heart"></i> Credits</h3>
|
||||
<p>
|
||||
Developed by <strong>UNITRONIX</strong><br>
|
||||
Special thanks to the RustDesk team and open source community.
|
||||
</p>
|
||||
<p style="margin-top: 15px; font-size: 0.85rem; color: rgba(255, 255, 255, 0.6);">
|
||||
© 2026 UNITRONIX. Released under AGPL-3.0 License.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Device Modal -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-edit"></i> Edit Device</h2>
|
||||
<button class="modal-close" onclick="closeEditModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="editDeviceId">Device ID</label>
|
||||
<input type="text" id="editDeviceId" readonly class="form-control">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editNote">Note</label>
|
||||
<textarea id="editNote" class="form-control" rows="3" placeholder="Enter device note..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeEditModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveDevice()">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="deleteModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-trash-alt"></i> Confirm Delete</h2>
|
||||
<button class="modal-close" onclick="closeDeleteModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete device <strong id="deleteDeviceId"></strong>?</p>
|
||||
<p class="warning-text">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDeleteModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash-alt"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal -->
|
||||
<div id="detailsModal" class="modal">
|
||||
<div class="modal-content glass-effect modal-large">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-info-circle"></i> Device Details</h2>
|
||||
<button class="modal-close" onclick="closeDetailsModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="detailsContent" class="details-grid"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeDetailsModal()">
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add User Modal (Admin) -->
|
||||
<div id="addUserModal" class="modal">
|
||||
<div class="modal-content glass-effect">
|
||||
<div class="modal-header">
|
||||
<h2><i class="fas fa-user-plus"></i> Add New User</h2>
|
||||
<button class="modal-close" onclick="closeAddUserModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="newUsername">Username</label>
|
||||
<input type="text" id="newUsername" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newUserPassword">Password</label>
|
||||
<input type="password" id="newUserPassword" class="form-control" required minlength="8">
|
||||
<small style="color: rgba(255, 255, 255, 0.6); font-size: 0.8rem;">
|
||||
Minimum 8 characters, must contain letters and numbers
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newUserRole">Role</label>
|
||||
<select id="newUserRole" class="form-control">
|
||||
<option value="viewer">Viewer (Read-only)</option>
|
||||
<option value="operator">Operator (Can ban/unban, edit devices)</option>
|
||||
<option value="admin">Admin (Full access)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeAddUserModal()">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="createUser()">
|
||||
<i class="fas fa-user-plus"></i> Create User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
<div id="toast" class="toast glass-effect">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span id="toastMessage"></span>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script_v15.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,378 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - BetterDesk Console</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<style>
|
||||
/* Login page specific styles - Dark Theme */
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #0f1419 0%, #1a1d2e 50%, #2d3748 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.login-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.1) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
bottom: -80px;
|
||||
left: -80px;
|
||||
animation: float 8s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) translateX(0px); }
|
||||
50% { transform: translateY(-20px) translateX(10px); }
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(26, 29, 46, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 48px;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 80px rgba(99, 102, 241, 0.15);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.login-logo i {
|
||||
font-size: 64px;
|
||||
color: #6366f1;
|
||||
margin-bottom: 16px;
|
||||
display: inline-block;
|
||||
text-shadow: 0 0 20px rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
.login-logo h1 {
|
||||
color: #f1f5f9;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.login-logo p {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: #cbd5e1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: rgba(15, 20, 25, 0.6);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #f1f5f9;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
background: rgba(15, 20, 25, 0.8);
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-login .spinner {
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid white;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.btn-login.loading .spinner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.error-message i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.login-card {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.login-logo h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.login-logo i {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-logo">
|
||||
<i class="fas fa-desktop"></i>
|
||||
<h1>BetterDesk Console</h1>
|
||||
<p>RustDesk Management Dashboard</p>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="errorMessage">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span id="errorText"></span>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" onsubmit="return false;">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-login" id="loginBtn">
|
||||
<span class="spinner"></span>
|
||||
<span id="loginBtnText">
|
||||
<i class="fas fa-sign-in-alt"></i> Sign In
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="version-info">
|
||||
Version 1.5 • Secure Auth System
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const errorText = document.getElementById('errorText');
|
||||
|
||||
// Check if already logged in
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
// Verify token
|
||||
fetch('/api/auth/verify', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
// Invalid token - clear storage
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Clear all auth data on any error
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
});
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!username || !password) {
|
||||
showError('Please fill in all fields');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
loginBtn.classList.add('loading');
|
||||
loginBtn.disabled = true;
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Save token
|
||||
localStorage.setItem('authToken', data.token);
|
||||
localStorage.setItem('username', data.username);
|
||||
localStorage.setItem('role', data.role);
|
||||
|
||||
// Redirect to dashboard
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
showError(data.error || 'Login failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
showError('Connection error. Please try again.');
|
||||
} finally {
|
||||
// Remove loading state
|
||||
loginBtn.classList.remove('loading');
|
||||
loginBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function showError(message) {
|
||||
errorText.textContent = message;
|
||||
errorMessage.classList.add('show');
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorMessage.classList.remove('show');
|
||||
}
|
||||
|
||||
// Clear error on input
|
||||
document.getElementById('username').addEventListener('input', hideError);
|
||||
document.getElementById('password').addEventListener('input', hideError);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||