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.
This commit is contained in:
UNITRONIX
2026-01-15 20:36:24 +01:00
parent 19b409b48a
commit d810384cd5
84 changed files with 7370 additions and 10166 deletions
+2 -2
View File
@@ -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
+135 -85
View File
@@ -1,12 +1,13 @@
# 🚀 BetterDesk Console
# 🚀 BetterDesk Console
<div align="center">
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![RustDesk](https://img.shields.io/badge/RustDesk-1.1.14-green.svg)
![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)
![Version](https://img.shields.io/badge/version-1.3.0--secure-brightgreen.svg)
![Security](https://img.shields.io/badge/API-localhost--only-orange.svg)
![Version](https://img.shields.io/badge/version-1.5.0-brightgreen.svg)
![Security](https://img.shields.io/badge/API-X--API--Key--Auth-green.svg)
![Access](https://img.shields.io/badge/LAN-Accessible-blue.svg)
**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 @@
![Details](screenshots/device-details.png)
*Detailed device information modal*
### Mobile Responsive
![Mobile](screenshots/mobile-view.png)
*Fully responsive design for mobile devices*
### Public Key Management
![Public Key](screenshots/public_key_page.png)
*Secure public key access with password protection*
### Settings
![Settings](screenshots/settings_page.png)
*User settings and password management*
### User Management
![User Management](screenshots/user_mgmt.png)
*Multi-user administration panel*
### About
![About](screenshots/about.png)
*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
---
+12 -8
View File
@@ -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
-262
View File
@@ -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! 🎉
-161
View File
@@ -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*
-219
View File
@@ -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! 🔐
-427
View File
@@ -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 "$@"
-19
View File
@@ -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."
-399
View File
@@ -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!"
+162
View File
@@ -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
-98
View File
@@ -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)
-869
View File
@@ -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?
-214
View File
@@ -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
-339
View File
@@ -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! 🎉**
+354
View File
@@ -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*
-306
View File
@@ -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
-295
View File
@@ -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
-351
View File
@@ -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)
-220
View File
@@ -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+
-184
View File
@@ -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.
-304
View File
@@ -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/
-317
View File
@@ -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
-262
View File
@@ -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! 🎉
-161
View File
@@ -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*
-219
View File
@@ -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! 🔐
-323
View File
@@ -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
-227
View File
@@ -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
-149
View File
@@ -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
+59 -13
View File
@@ -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
---
+73 -2
View File
@@ -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
+23 -115
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-81
View File
@@ -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.
-141
View File
@@ -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
Binary file not shown.
Binary file not shown.
-328
View File
@@ -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 ""
-287
View File
@@ -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 ""
-501
View File
@@ -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 ""
+5
View File
@@ -0,0 +1,5 @@
# HTTP API dependencies
axum = { version = "0.6", features = ["tokio"] }
tower = "0.4"
serde_json = "1.0"
-45
View File
@@ -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)
}
-103
View File
@@ -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
-53
View File
@@ -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"
-101
View File
@@ -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'"
-60
View File
@@ -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
Binary file not shown.
Binary file not shown.
-15
View File
@@ -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
}
-52
View File
@@ -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 ...
Binary file not shown.
@@ -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
+95 -20
View File
@@ -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())
+16 -2
View File
@@ -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(())
}
+6 -2
View File
@@ -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 {
-177
View File
@@ -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
+8 -3
View File
@@ -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
+89 -11
View File
@@ -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
-427
View File
@@ -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 "$@"
+246
View File
@@ -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()
-456
View File
@@ -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 "$@"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

+460
View File
@@ -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 "$@"
-441
View File
@@ -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)
-70
View File
@@ -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)
+812
View File
@@ -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)
+421
View File
@@ -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
+3
View File
@@ -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
+4
View File
@@ -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
+4 -4
View File
@@ -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>
+924
View File
@@ -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');
}
}
+844
View File
@@ -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);
}
+412
View File
@@ -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;
}
+210
View File
@@ -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
};
+170
View File
@@ -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;
}
-236
View File
@@ -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()">&times;</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()">&times;</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()">&times;</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()">&times;</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>
+536
View File
@@ -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()">&times;</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()">&times;</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()">&times;</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()">&times;</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()">&times;</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()">&times;</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()">&times;</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>
+829
View File
@@ -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()">&times;</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()">&times;</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()">&times;</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()">&times;</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>
+378
View File
@@ -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>