From: Guilherme Cardoso Date: Sun, 4 Jan 2026 12:32:33 +0000 (+0000) Subject: luci-app-rustdesk-server: add new application X-Git-Url: http://git.cdn.openwrt.org/?a=commitdiff_plain;h=37248679198bc57f97ddb210c3d5f18945cb3830;p=project%2Fluci.git luci-app-rustdesk-server: add new application Add LuCI web interface for managing RustDesk Server on OpenWrt. RustDesk is an open-source remote desktop application, providing a self-hosted alternative to proprietary solutions like TeamViewer and AnyDesk. It enables secure remote access without relying on third-party servers. This package provides configuration for the RustDesk server components: - hbbs: ID/Rendezvous server for connection brokering - hbbr: Relay server for NAT traversal Features: - Modern JavaScript UI with live status polling - Service management (start/stop/restart/enable/disable) - Public key display with copy button and regeneration - Firewall rule auto-management based on port settings - Tabbed configuration for ID Server (hbbs) and Relay Server (hbbr) - Log viewer with auto-refresh - Input validation (paths, ports, CIDR notation) - i18n ready with POT template Signed-off-by: Guilherme Cardoso --- diff --git a/applications/luci-app-rustdesk-server/Makefile b/applications/luci-app-rustdesk-server/Makefile new file mode 100644 index 0000000000..222e0c9434 --- /dev/null +++ b/applications/luci-app-rustdesk-server/Makefile @@ -0,0 +1,20 @@ +include $(TOPDIR)/rules.mk + +PKG_VERSION:=20250610 +PKG_RELEASE:=4 +PKG_NAME:=luci-app-rustdesk-server +PKG_MAINTAINER:=Guilherme Cardoso + +LUCI_TITLE:=LuCI support for RustDesk Server +LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-ucode +LUCI_PKGARCH:=all +PKG_LICENSE:=Apache-2.0 + +define Package/$(PKG_NAME)/conffiles +/etc/config/rustdesk-server +endef + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/applications/luci-app-rustdesk-server/README.md b/applications/luci-app-rustdesk-server/README.md new file mode 100644 index 0000000000..6283aac4fe --- /dev/null +++ b/applications/luci-app-rustdesk-server/README.md @@ -0,0 +1,299 @@ +# luci-app-rustdesk-server + +LuCI web interface for managing [RustDesk Server](https://github.com/rustdesk/rustdesk-server) on OpenWrt. + +RustDesk is a full-featured open source remote control alternative to TeamViewer and AnyDesk. This LuCI application provides a web-based interface to configure and manage the self-hosted RustDesk server components (hbbs and hbbr) on OpenWrt routers. + +## Features + +- **Service Management** - Start/Stop/Restart services directly from the UI +- **Boot Enable/Disable** - Toggle service startup at boot +- **Status Monitoring** - Real-time status of HBBS and HBBR services with live polling +- **Public Key Display** - View and copy the generated public key for client configuration +- **Key Regeneration** - Regenerate encryption keys when needed +- **Log Viewer** - View service logs with auto-refresh and auto-scroll features +- **Firewall Hints** - Displays required ports for manual firewall configuration +- **Tabbed Configuration** - Organized settings for ID Server (hbbs) and Relay Server (hbbr) +- **Input Validation** - Validates paths, ports, and configuration values +- **i18n Ready** - Full translation support with POT template + +## Architecture + +``` +luci-app-rustdesk-server/ +├── Makefile # OpenWrt package build file +├── htdocs/luci-static/resources/view/rustdesk-server/ +│ └── general.js # Main UI view (JavaScript) +├── po/templates/ +│ └── rustdesk-server.pot # Translation template +└── root/ + ├── etc/ + │ ├── config/rustdesk-server # UCI configuration + │ ├── init.d/rustdesk-server # procd init script + │ └── uci-defaults/50-luci-rustdesk-server # First-run setup + └── usr/share/ + ├── luci/menu.d/luci-app-rustdesk-server.json # Menu entry + └── rpcd/ + ├── acl.d/luci-app-rustdesk-server.json # ACL permissions + └── ucode/rustdesk-server.uc # RPC backend +``` + +## Requirements + +### OpenWrt Dependencies +- OpenWrt 23.05 or later with LuCI installed +- `luci-base` - LuCI core framework +- `rpcd` - RPC daemon +- `rpcd-mod-ucode` - ucode support for rpcd + +### RustDesk Server Binaries +The RustDesk server binaries (`hbbs`, `hbbr`) must be installed separately. They are **not included** in this package. + +#### Installing RustDesk Server Binaries + +1. **Download from GitHub Releases:** + ```bash + # Check your architecture + uname -m + + # Download appropriate binaries from: + # https://github.com/rustdesk/rustdesk-server/releases + + # Example for aarch64: + wget https://github.com/rustdesk/rustdesk-server/releases/download/1.1.11/rustdesk-server-linux-arm64v8.zip + unzip rustdesk-server-linux-arm64v8.zip + cp amd64/hbbs amd64/hbbr /usr/bin/ + chmod +x /usr/bin/hbbs /usr/bin/hbbr + ``` + +2. **Or build from source:** + ```bash + # See https://github.com/rustdesk/rustdesk-server for build instructions + ``` + +3. **Verify installation:** + ```bash + /usr/bin/hbbs --version + /usr/bin/hbbr --version + ``` + +## Installation + +### From OpenWrt Package Repository +```bash +opkg update +opkg install luci-app-rustdesk-server +``` + +### From Source (Development) +```bash +# Clone the LuCI repository +git clone https://github.com/openwrt/luci.git +cd luci + +# Build the package +make package/luci-app-rustdesk-server/compile +``` + +### Manual Installation +1. Copy the application files to your OpenWrt device: + ```bash + # Copy htdocs to /www + cp -r htdocs/luci-static /www/luci-static/ + + # Copy root files + cp -r root/* / + + # Set permissions + chmod +x /etc/init.d/rustdesk-server + ``` + +2. Reload rpcd to register the new RPC methods: + ```bash + /etc/init.d/rpcd reload + ``` + +3. Clear LuCI cache: + ```bash + rm -rf /tmp/luci-* + ``` + +4. Access the interface at: **Services → RustDesk Server** + +## Configuration + +### Binary Location +The application expects `hbbs` and `hbbr` binaries to be installed in `/usr/bin`. + +### Firewall Configuration +Firewall rules must be configured manually in **Network → Firewall → Traffic Rules**. The application displays the required ports in the Service Status section. + +The standard RustDesk port layout is: + +| Port | Protocol | Service | Calculation | +|------|----------|---------|-------------| +| HBBS-1 | TCP | NAT type test | server_port - 1 | +| HBBS | TCP/UDP | ID server / Hole punching | server_port | +| HBBS+2 | TCP | Web client support | server_port + 2 | +| HBBR | TCP | Relay server | relay_port | +| HBBR+2 | TCP | Web client support | relay_port + 2 | + +**Example:** With default ports (`server_port=21116` and `relay_port=21117`): +- TCP ports: 21115, 21116, 21117, 21118, 21119 +- UDP port: 21116 + +### Logging +Enable logging in General settings to write service output to `/var/log/rustdesk-server.log`. View logs in real-time using the Logs tab. + +### Database Location +The database is stored in `/tmp/rustdesk_db_v2.sqlite3`. This is a non-persistent location and will be cleared on reboot. This is intentional for embedded systems like OpenWrt where persistent storage may be limited. + +## Client Configuration + +After starting the service: + +1. Go to the LuCI interface and note your router's IP address +2. Copy the **Public Key** from the Service Status section +3. In RustDesk client settings, configure: + - **ID Server**: Your router's IP:21116 (or custom port if configured) + - **Relay Server**: Your router's IP:21117 (or custom port if configured) + - **Key**: The public key from step 2 + +## UCI Configuration Reference + +The configuration is stored in `/etc/config/rustdesk-server`: + +```uci +config rustdesk-server + option enabled '1' # Enable ID server (hbbs) + option enabled_relay '1' # Enable Relay server (hbbr) + + # HBBS options + option server_port '21116' # ID server port + option server_key '' # Custom key (optional) + + # HBBR options + option relay_port '21117' # Relay server port + + # Environment variables + option server_env_rust_log 'info' +``` + +## Files + +| Path | Description | +|------|-------------| +| `/etc/config/rustdesk-server` | UCI configuration file | +| `/etc/init.d/rustdesk-server` | procd init script | +| `/etc/rustdesk/` | Key storage directory | +| `/etc/rustdesk/id_ed25519.pub` | Public key (auto-generated) | +| `/var/log/rustdesk-server.log` | Service log file (when enabled) | +| `/usr/share/rpcd/ucode/rustdesk-server.uc` | RPC backend | +| `/usr/share/luci/menu.d/luci-app-rustdesk-server.json` | Menu entry | +| `/usr/share/rpcd/acl.d/luci-app-rustdesk-server.json` | ACL permissions | + +## Troubleshooting + +### Service won't start +1. **Check binaries exist:** + ```bash + ls -la /usr/bin/hbbs /usr/bin/hbbr + ``` +2. **Verify binaries are executable:** + ```bash + chmod +x /usr/bin/hbbs /usr/bin/hbbr + ``` +3. **Check system log:** + ```bash + logread | grep rustdesk-server + ``` +4. **Verify at least one server is enabled** in the configuration + +### Key not generated +The public key (`id_ed25519.pub`) is generated automatically when HBBS starts for the first time. If missing: +1. Ensure the key directory exists: `mkdir -p /etc/rustdesk` +2. Start the service and wait a few seconds +3. Check if key was created: `cat /etc/rustdesk/id_ed25519.pub` + +### Firewall / Connection issues +1. Verify firewall rules are configured in **Network → Firewall → Traffic Rules** +2. Check that required ports are open (TCP: 21115-21119, UDP: 21116) +3. Reload firewall: + ```bash + /etc/init.d/firewall reload + ``` +4. Verify the service is running: + ```bash + pidof hbbs hbbr + ``` +5. Check if ports are listening: + ```bash + netstat -tlnp | grep -E '2111[5-9]' + ``` +6. Test connectivity from client: + ```bash + nc -zv 21116 + ``` + +### RPC errors in browser console +1. Reload rpcd: + ```bash + /etc/init.d/rpcd reload + ``` +2. Clear LuCI cache: + ```bash + rm -rf /tmp/luci-* + ``` + +## Development + +### Building Translations +```bash +# Scan for translatable strings +./build/i18n-scan.pl applications/luci-app-rustdesk-server > applications/luci-app-rustdesk-server/po/templates/rustdesk-server.pot + +# Update existing translations +./build/i18n-update.pl applications/luci-app-rustdesk-server +``` + +### Testing Changes +1. Make changes to files +2. Copy to device and reload rpcd +3. Clear browser cache and LuCI cache +4. Refresh the page + +## Security Considerations + +This application implements multiple layers of input validation and sanitization to prevent shell injection attacks: + +### Frontend Validation (JavaScript) +All user inputs are validated before being saved to UCI configuration: + +| Field Type | Validation | +|------------|------------| +| Ports | Numeric only, range 1-65535, supports ranges and comma-separated lists | +| CIDR masks | Strict IP/prefix format validation | +| Keys | Alphanumeric and base64 characters only (`A-Za-z0-9+/=`) | +| URLs | Must start with `http://` or `https://`, no shell metacharacters | +| Paths | Must start with `/`, no shell metacharacters (`;|&$\`(){}[]<>'"\\!`) | +| Server lists | Alphanumeric, dots, colons, commas, hyphens, underscores only | +| Numeric fields | Use LuCI's built-in `uinteger` datatype | + +### Backend Validation (Init Script) +The init script (`/etc/init.d/rustdesk-server`) includes comprehensive validation functions that re-validate all configuration values before using them in shell commands: + +- `validate_numeric()` - Ensures values contain only digits +- `validate_port()` - Validates port range (1-65535) +- `validate_path()` - Checks for shell metacharacters and requires leading `/` +- `validate_url()` - Validates URL format and rejects dangerous characters +- `validate_key()` - Allows only base64-safe characters +- `validate_server_list()` - Allows only hostname/IP-safe characters +- `validate_cidr()` - Allows only digits, dots, and slash +- `validate_log_level()` - Whitelist of valid log levels + +Invalid values are rejected and logged with warnings to syslog. + +### RPC Backend Validation (ucode) +The RPC backend (`rustdesk-server.uc`) validates: +- `service_action`: Whitelist of allowed actions (`start`, `stop`, `restart`, `reload`, `enable`, `disable`) +- `get_log` lines parameter: Clamped to range 10-1000 diff --git a/applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js b/applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js new file mode 100644 index 0000000000..a78fee7a92 --- /dev/null +++ b/applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js @@ -0,0 +1,604 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require ui'; +'require uci'; +'require rpc'; +'require poll'; + +/* + * Constants - only frontend-relevant values + */ +const CONSTANTS = { + // Default ports (used in placeholders) + HBBS_DEFAULT_PORT: '21116', + HBBR_DEFAULT_PORT: '21117', + + // Polling interval (seconds) + POLL_INTERVAL: 3, + + // Colors for status display + COLORS: { + SUCCESS: 'green', + ERROR: 'red', + MUTED: 'gray', + INFO: '#888' + } +}; + +/* + * RPC declarations + */ +const callGetStatus = rpc.declare({ + object: 'luci.rustdesk-server', + method: 'get_status' +}); + +const callGetPublicKey = rpc.declare({ + object: 'luci.rustdesk-server', + method: 'get_public_key' +}); + +const callGetVersion = rpc.declare({ + object: 'luci.rustdesk-server', + method: 'get_version' +}); + +const callRegenerateKey = rpc.declare({ + object: 'luci.rustdesk-server', + method: 'regenerate_key' +}); + +const callServiceAction = rpc.declare({ + object: 'luci.rustdesk-server', + method: 'service_action', + params: ['action'] +}); + +/* + * Helper functions + */ +function handleAction(action) { + return fs.exec_direct('/etc/init.d/rustdesk-server', [action]); +} + + +/** + * Shell metacharacters regex - prevents command injection + */ +const SHELL_METACHARS = /[;&|$`(){}[\]<>'"\\!]/; + +/** + * Validates a safe string value (no shell metacharacters) + * Used for URL and path validation + * @param {string} value - The value to check + * @returns {boolean|string} True if safe, error message if not + */ +function containsShellMetachars(value) { + if (SHELL_METACHARS.test(value)) + return _('Invalid characters detected'); + return true; +} + +/** + * Validates a key string (alphanumeric and base64 characters only) + * @param {string} section_id - The section ID + * @param {string} value - The key value + * @returns {boolean|string} True if valid, error message if invalid + */ +function validateKey(section_id, value) { + if (!value || value.length === 0) + return true; + if (/[^A-Za-z0-9+/=]/.test(value)) + return _('Invalid characters.') + ' ' + _('Only alphanumeric and base64 characters (+/=) allowed.'); + return true; +} + +/** + * Validates a URL (must start with http:// or https://) + * @param {string} section_id - The section ID + * @param {string} value - The URL value + * @returns {boolean|string} True if valid, error message if invalid + */ +function validateURL(section_id, value) { + if (!value || value.length === 0) + return true; + const shellCheck = containsShellMetachars(value); + if (shellCheck !== true) + return shellCheck; + if (!/^https?:\/\//.test(value)) + return _('URL must start with http:// or https://'); + return true; +} + + +/** + * Creates a status indicator HTML string + * @param {boolean} isActive - Whether the status is active/good + * @param {string} activeText - Text to show when active + * @param {string} inactiveText - Text to show when inactive + * @param {string} [suffix] - Optional suffix to append + * @returns {string} HTML string + */ +function createStatusIndicator(isActive, activeText, inactiveText, suffix) { + const color = isActive ? CONSTANTS.COLORS.SUCCESS : CONSTANTS.COLORS.ERROR; + const symbol = isActive ? '●' : '○'; + const text = isActive ? activeText : inactiveText; + let html = '' + symbol + ' ' + text + ''; + + if (suffix) { + html += ' ' + suffix + ''; + } + + return html; +} + +/** + * Creates a checkmark status indicator + * @param {boolean} isActive - Whether the status is active/good + * @param {string} activeText - Text to show when active + * @param {string} inactiveText - Text to show when inactive + * @returns {string} HTML string + */ +function createCheckIndicator(isActive, activeText, inactiveText) { + const color = isActive ? CONSTANTS.COLORS.SUCCESS : CONSTANTS.COLORS.MUTED; + const symbol = isActive ? '✓' : '✗'; + const text = isActive ? activeText : inactiveText; + return '' + symbol + ' ' + text + ''; +} + +// Track if key exists globally for button state +let keyExistsGlobal = false; +// Track if any server is enabled in config +let anyServerEnabledGlobal = false; +// Track boot enabled state +let bootEnabledGlobal = false; + +return view.extend({ + render() { + let m, s, o; + + m = new form.Map('rustdesk-server', _('RustDesk Server'), + _('Remote Desktop Software Server configuration.') + + ' ' + _('Server') + ' | ' + + '' + _('Client') + ''); + + /* + Firewall Notice + */ + s = m.section(form.NamedSection, 'firewall_info'); + s.render = () => E('div', { 'class': 'alert-message notice' }, [ + E('h4', {}, _('Firewall Configuration Required')), + E('p', {}, _('Required ports (when using default settings): TCP 21115-21119, UDP 21116.')), + E('p', {}, _('Configure in Network → Firewall → Traffic Rules.')) + ]); + + /* + Status Section (custom render) + */ + s = m.section(form.NamedSection, 'global'); + s.render = L.bind((view, section_id) => { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', _('Service Status')), + + // Status Table for HBBS and HBBR + E('table', { 'class': 'table cbi-section-table', 'id': 'status_table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Component')), + E('th', { 'class': 'th' }, _('Service Status')), + E('th', { 'class': 'th' }, _('Binary')), + E('th', { 'class': 'th' }, _('Enabled')) + ]), + E('tr', { 'class': 'tr', 'id': 'hbbs_row' }, [ + E('td', { 'class': 'td' }, _('HBBS (ID Server)')), + E('td', { 'class': 'td', 'id': 'hbbs_status' }, '-'), + E('td', { 'class': 'td', 'id': 'hbbs_binary' }, '-'), + E('td', { 'class': 'td', 'id': 'hbbs_enabled' }, '-') + ]), + E('tr', { 'class': 'tr', 'id': 'hbbr_row' }, [ + E('td', { 'class': 'td' }, _('HBBR (Relay Server)')), + E('td', { 'class': 'td', 'id': 'hbbr_status' }, '-'), + E('td', { 'class': 'td', 'id': 'hbbr_binary' }, '-'), + E('td', { 'class': 'td', 'id': 'hbbr_enabled' }, '-') + ]) + ]), + + // Public Key with Regenerate button inline + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Public Key')), + E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; align-items: center; flex-wrap: wrap; gap: 8px;' }, [ + E('span', { 'id': 'public_key', 'style': 'word-break: break-all; flex: 1; min-width: 200px;' }, '-'), + E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'id': 'regenerate_key_btn', + 'disabled': true, + 'title': _('Regenerate the key pair (requires existing key)'), + 'click': (ev) => { + if (!keyExistsGlobal) { + ui.addTimeLimitedNotification(null, E('p', _('Cannot regenerate: No public key exists yet.') + ' ' + _('Start the service first to generate the initial key.')), 5000, 'warning'); + return; + } + if (!confirm(_('This will regenerate the key pair and restart the service.') + ' ' + _('All existing clients will need to be reconfigured.') + ' ' + _('Continue?'))) { + return; + } + ev.target.disabled = true; + ev.target.textContent = _('Regenerating...'); + + L.resolveDefault(callRegenerateKey(), {}).then((res) => { + if (res && res.success) { + ui.addTimeLimitedNotification(null, E('p', _('Keys deleted. Starting service to generate new keys...')), 5000, 'notice'); + // Use RPC to start service for reliable execution + // Add small delay to ensure service has fully stopped + return new Promise((resolve) => { + setTimeout(resolve, 1000); + }).then(() => L.resolveDefault(callServiceAction('start'), {})); + } else { + ui.addTimeLimitedNotification(null, E('p', _('Key regeneration failed: ') + (res.message || 'Could not delete keys')), 5000, 'error'); + throw new Error('Regeneration failed'); + } + }).then((startRes) => { + if (startRes && startRes.success) { + ui.addTimeLimitedNotification(null, E('p', _('Service started with new key')), 5000, 'notice'); + } else if (startRes) { + ui.addTimeLimitedNotification(null, E('p', _('Service start may have failed. Check status above.')), 5000, 'warning'); + } + }).catch((err) => { + if (err.message !== 'Regeneration failed') { + ui.addTimeLimitedNotification(null, E('p', _('Error: ') + err.message), 5000, 'error'); + } + }).finally(() => { + const btn = document.getElementById('regenerate_key_btn'); + if (btn) { + btn.disabled = !keyExistsGlobal; + btn.textContent = _('Regenerate Key'); + } + }); + } + }, _('Regenerate Key')) + ]) + ]), + + // Boot at startup toggle + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Start at Boot')), + E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; align-items: center; gap: 12px;' }, [ + E('span', { 'id': 'boot_status' }, '-'), + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'id': 'enable_boot_btn', + 'click': (ev) => { + const action = bootEnabledGlobal ? 'disable' : 'enable'; + ev.target.disabled = true; + ev.target.textContent = _('Processing...'); + + L.resolveDefault(callServiceAction(action), {}).then((res) => { + if (res && res.success) { + bootEnabledGlobal = !bootEnabledGlobal; + ui.addTimeLimitedNotification(null, E('p', + bootEnabledGlobal ? _('Service enabled at boot') : _('Service disabled at boot') + ), 5000, 'notice'); + } else { + const errMsg = res.error || res.message || (res.exit_code !== undefined ? 'Exit code: ' + res.exit_code : JSON.stringify(res)); + ui.addTimeLimitedNotification(null, E('p', _('Failed: ') + errMsg), 5000, 'error'); + } + }).catch((err) => { + ui.addTimeLimitedNotification(null, E('p', _('Error: ') + err.message), 5000, 'error'); + }).finally(() => { + const btn = document.getElementById('enable_boot_btn'); + const statusEl = document.getElementById('boot_status'); + if (btn) { + btn.disabled = false; + btn.textContent = bootEnabledGlobal ? _('Disable') : _('Enable'); + } + if (statusEl) { + statusEl.innerHTML = createCheckIndicator(bootEnabledGlobal, _('Enabled'), _('Disabled')); + } + }); + } + }, _('Loading...')) + ]) + ]), + + // Service Control section + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Service Control')), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', { 'style': 'margin-bottom: 8px;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'id': 'start_btn', + 'disabled': true, + 'title': _('Enable ID Server or Relay Server first'), + 'click': (ev) => { + if (!anyServerEnabledGlobal) { + ui.addTimeLimitedNotification(null, E('p', _('Cannot start service: Enable the ID Server or Relay Server in the configuration first.') + ' ' + _('Check "Enable ID Server" or "Enable Relay Server" below and click "Save & Apply".')), 5000, 'error'); + return; + } + + ev.target.disabled = true; + handleAction('start').then(() => { + ev.target.disabled = !anyServerEnabledGlobal; + }).catch((err) => { + ui.addTimeLimitedNotification(null, E('p', _('Failed to start service: ') + err.message), 5000, 'error'); + ev.target.disabled = !anyServerEnabledGlobal; + }); + } + }, _('Start')), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-remove', + 'click': (ev) => { + ev.target.disabled = true; + handleAction('stop').then(() => { + ev.target.disabled = false; + }).catch((err) => { + ui.addTimeLimitedNotification(null, E('p', _('Failed to stop service: ') + err.message), 5000, 'error'); + ev.target.disabled = false; + }); + } + }, _('Stop')), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': (ev) => { + ev.target.disabled = true; + handleAction('restart').then(() => { + ev.target.disabled = false; + }).catch((err) => { + ui.addTimeLimitedNotification(null, E('p', _('Failed to restart service: ') + err.message), 5000, 'error'); + ev.target.disabled = false; + }); + } + }, _('Restart')) + ]), + // Info message about Start requirement + E('div', { + 'class': 'cbi-value-description', + 'style': 'color: #666; font-size: 0.9em; margin-top: 4px;' + }, [ + E('em', {}, [ + _('Start will only work if at least "Enable ID Server" or "Enable Relay Server" is checked in the Configuration section below.') + ]) + ]) + ]) + ]) + ]); + }, o, this); + + /* + Polling for status updates + */ + poll.add(() => { + return Promise.all([ + L.resolveDefault(callGetStatus(), {}), + L.resolveDefault(callGetPublicKey(), {}), + L.resolveDefault(callGetVersion(), {}), + uci.load('rustdesk-server') + ]).then(([status = {}, keyInfo = {}, verInfo = {}]) => { + + // Get enabled status from UCI + const sections = uci.sections('rustdesk-server', 'rustdesk-server'); + let hbbsEnabled = false; + let hbbrEnabled = false; + if (sections && sections.length > 0) { + hbbsEnabled = (sections[0].enabled == '1'); + hbbrEnabled = (sections[0].enabled_relay == '1'); + } + + // HBBS Status (Service Status column) + const hbbsStatusEl = document.getElementById('hbbs_status'); + if (hbbsStatusEl) { + let suffix = ''; + if (status.hbbs_pid) { + suffix = '(PID: ' + status.hbbs_pid + ')'; + if (verInfo.hbbs_version) suffix += ' [' + verInfo.hbbs_version + ']'; + } + hbbsStatusEl.innerHTML = createStatusIndicator( + !!status.hbbs_pid, _('Running'), _('Stopped'), suffix + ); + } + + // HBBS Binary column + const hbbsBinaryEl = document.getElementById('hbbs_binary'); + if (hbbsBinaryEl) { + hbbsBinaryEl.innerHTML = createCheckIndicator(status.hbbs_exists, _('Found'), _('Not Found')); + } + + // HBBS Enabled column + const hbbsEnabledEl = document.getElementById('hbbs_enabled'); + if (hbbsEnabledEl) { + hbbsEnabledEl.innerHTML = createCheckIndicator(hbbsEnabled, _('Yes'), _('No')); + } + + // HBBR Status (Service Status column) + const hbbrStatusEl = document.getElementById('hbbr_status'); + if (hbbrStatusEl) { + let suffix = ''; + if (status.hbbr_pid) { + suffix = '(PID: ' + status.hbbr_pid + ')'; + if (verInfo.hbbr_version) suffix += ' [' + verInfo.hbbr_version + ']'; + } + hbbrStatusEl.innerHTML = createStatusIndicator( + !!status.hbbr_pid, _('Running'), _('Stopped'), suffix + ); + } + + // HBBR Binary column + const hbbrBinaryEl = document.getElementById('hbbr_binary'); + if (hbbrBinaryEl) { + hbbrBinaryEl.innerHTML = createCheckIndicator(status.hbbr_exists, _('Found'), _('Not Found')); + } + + // HBBR Enabled column + const hbbrEnabledEl = document.getElementById('hbbr_enabled'); + if (hbbrEnabledEl) { + hbbrEnabledEl.innerHTML = createCheckIndicator(hbbrEnabled, _('Yes'), _('No')); + } + + // Public Key - update global state + keyExistsGlobal = !!(keyInfo.key_exists && keyInfo.public_key); + + const keyEl = document.getElementById('public_key'); + const regenBtn = document.getElementById('regenerate_key_btn'); + + if (keyEl) { + if (keyInfo.key_exists && keyInfo.public_key) { + keyEl.innerHTML = '' + keyInfo.public_key + '' + + ' '; + } else { + keyEl.innerHTML = '' + _('Not generated yet - start the service') + ''; + } + } + + // Update regenerate button state + if (regenBtn) { + regenBtn.disabled = !keyExistsGlobal; + if (!keyExistsGlobal) { + regenBtn.title = _('Start the service first to generate the initial key'); + } else { + regenBtn.title = _('Regenerate the key pair (will restart service)'); + } + } + + // Update Start button state based on config + anyServerEnabledGlobal = hbbsEnabled || hbbrEnabled; + const startBtn = document.getElementById('start_btn'); + if (startBtn) { + startBtn.disabled = !anyServerEnabledGlobal; + if (!anyServerEnabledGlobal) { + startBtn.title = _('Enable ID Server or Relay Server in Configuration first'); + } else { + startBtn.title = _('Start the service'); + } + } + + // Update boot enabled status + bootEnabledGlobal = status.boot_enabled || false; + const bootStatusEl = document.getElementById('boot_status'); + const bootBtn = document.getElementById('enable_boot_btn'); + if (bootStatusEl) { + bootStatusEl.innerHTML = createCheckIndicator(bootEnabledGlobal, _('Enabled'), _('Disabled')); + } + if (bootBtn) { + bootBtn.textContent = bootEnabledGlobal ? _('Disable') : _('Enable'); + } + }); + }, CONSTANTS.POLL_INTERVAL); + + /* + Configuration Section + */ + s = m.section(form.TypedSection, 'rustdesk-server', _('Configuration')); + s.anonymous = true; + s.addremove = false; + + s.tab('hbbs', _('ID Server (hbbs)')); + s.tab('hbbr', _('Relay Server (hbbr)')); + + /* HBBS Settings */ + o = s.taboption('hbbs', form.Flag, 'enabled', _('Enable')); + o.rmempty = false; + + o = s.taboption('hbbs', form.Value, 'server_port', _('Port (-p, --port)')); + o.datatype = 'port'; + o.placeholder = CONSTANTS.HBBS_DEFAULT_PORT; + o.description = _('Sets the listening port for the ID/Rendezvous server'); + + o = s.taboption('hbbs', form.Value, 'server_key', _('Key (-k, --key)')); + o.description = _('Only allow clients with the same key. If empty, uses auto-generated key'); + o.validate = validateKey; + + o = s.taboption('hbbs', form.DynamicList, 'server_relay_servers', _('Relay Servers (-r, --relay-servers)')); + o.description = _('Default relay servers. Add one server per entry (hostname or hostname:port)'); + o.datatype = 'or(host,hostport)'; + + o = s.taboption('hbbs', form.DynamicList, 'server_rendezvous_servers', _('Rendezvous Servers (-R, --rendezvous-servers)')); + o.description = _('Additional rendezvous servers. Add one server per entry (hostname or hostname:port)'); + o.datatype = 'or(host,hostport)'; + + o = s.taboption('hbbs', form.Value, 'server_mask', _('LAN Mask (--mask)')); + o.description = _('Determine if the connection comes from LAN. Use CIDR notation.'); + o.placeholder = '192.168.0.0/16'; + o.datatype = 'cidr4'; + + o = s.taboption('hbbs', form.Value, 'server_rmem', _('UDP Recv Buffer (-M, --rmem)')); + o.datatype = 'uinteger'; + o.placeholder = '0'; + o.description = _('Sets UDP receive buffer size (0 = system default)'); + + o = s.taboption('hbbs', form.Value, 'server_serial', _('Serial Number (-s, --serial)')); + o.datatype = 'uinteger'; + o.placeholder = '0'; + o.description = _('Sets configure update serial number'); + + o = s.taboption('hbbs', form.Value, 'server_software_url', _('Software Download URL (-u, --software-url)')); + o.description = _('Sets the download URL of RustDesk software for clients'); + o.validate = validateURL; + + /* HBBS Settings - Environment Variables */ + o = s.taboption('hbbs', form.Flag, 'server_env_always_use_relay', _('ALWAYS_USE_RELAY')); + o.description = _('Force all connections to use relay servers'); + o.default = o.disabled; + + o = s.taboption('hbbs', form.ListValue, 'server_env_rust_log', _('RUST_LOG')); + o.description = _('Logging level for the ID server'); + o.value('', _('Default')); + o.value('error', _('Error')); + o.value('warn', _('Warning')); + o.value('info', _('Info')); + o.value('debug', _('Debug')); + o.value('trace', _('Trace')); + o.default = ''; + + /* HBBR Settings */ + o = s.taboption('hbbr', form.Flag, 'enabled_relay', _('Enable')); + o.rmempty = false; + + o = s.taboption('hbbr', form.Value, 'relay_port', _('Port (-p, --port)')); + o.datatype = 'port'; + o.placeholder = CONSTANTS.HBBR_DEFAULT_PORT; + o.description = _('Sets the listening port for the relay server'); + + o = s.taboption('hbbr', form.Value, 'relay_key', _('Key (-k, --key)')); + o.description = _('Only allow clients with the same key. If empty, uses auto-generated key'); + o.validate = validateKey; + + /* HBBR Settings - Environment Variables */ + o = s.taboption('hbbr', form.ListValue, 'relay_env_rust_log', _('RUST_LOG')); + o.description = _('Logging level for the relay server'); + o.value('', _('Default')); + o.value('error', _('Error')); + o.value('warn', _('Warning')); + o.value('info', _('Info')); + o.value('debug', _('Debug')); + o.value('trace', _('Trace')); + o.default = ''; + + o = s.taboption('hbbr', form.Value, 'relay_env_limit_speed', _('LIMIT_SPEED')); + o.datatype = 'uinteger'; + o.description = _('Speed limit per connection in Mb/s (0 = default)'); + o.placeholder = '0'; + + o = s.taboption('hbbr', form.Value, 'relay_env_single_bandwidth', _('SINGLE_BANDWIDTH')); + o.datatype = 'uinteger'; + o.description = _('Bandwidth limit per single connection in MB/s (0 = default)'); + o.placeholder = '0'; + + o = s.taboption('hbbr', form.Value, 'relay_env_total_bandwidth', _('TOTAL_BANDWIDTH')); + o.datatype = 'uinteger'; + o.description = _('Total bandwidth limit in MB/s (0 = default)'); + o.placeholder = '0'; + + o = s.taboption('hbbr', form.Value, 'relay_env_downgrade_threshold', _('DOWNGRADE_THRESHOLD')); + o.datatype = 'uinteger'; + o.description = _('Threshold for connection downgrade'); + + o = s.taboption('hbbr', form.Value, 'relay_env_downgrade_start_check', _('DOWNGRADE_START_CHECK')); + o.datatype = 'uinteger'; + o.description = _('Start check time for connection downgrade'); + + return m.render(); + } +}); diff --git a/applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/logs.js b/applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/logs.js new file mode 100644 index 0000000000..f7664bb920 --- /dev/null +++ b/applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/logs.js @@ -0,0 +1,4 @@ +'use strict'; +'require tools.views as views'; + +return views.LogreadBox('hbb', _('RustDesk Server Log')); diff --git a/applications/luci-app-rustdesk-server/po/templates/rustdesk-server.pot b/applications/luci-app-rustdesk-server/po/templates/rustdesk-server.pot new file mode 100644 index 0000000000..82e1c58bf0 --- /dev/null +++ b/applications/luci-app-rustdesk-server/po/templates/rustdesk-server.pot @@ -0,0 +1,434 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "RustDesk Server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Remote Desktop Software Server configuration." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Client" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Service Status" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Component" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Binary" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Enabled" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Disabled" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "HBBS (ID Server)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "HBBR (Relay Server)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Running" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Stopped" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Found" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Not Found" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Yes" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "No" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Firewall Ports" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Required firewall ports (configure in Network → Firewall → Traffic Rules):" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "(NAT test)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "(ID server)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "(Relay)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "(Web clients)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "(Hole punching)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Ports are relative to configured ID Server port. If using non-default ports, adjust accordingly." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Copy" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Public Key" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Regenerate Key" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Regenerate the key pair (requires existing key)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Cannot regenerate: No public key exists yet. Start the service first to generate the initial key." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "This will regenerate the key pair and restart the service. All existing clients will need to be reconfigured. Continue?" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Regenerating..." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Keys deleted. Starting service to generate new keys..." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Key regeneration failed: " +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Service started with new key" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Service start may have failed. Check status above." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Start the service first to generate the initial key" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Regenerate the key pair (will restart service)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Not generated yet - start the service" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Start at Boot" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Processing..." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Service enabled at boot" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Service disabled at boot" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Failed: " +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Error: " +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Disable" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Enable" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Loading..." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Service Control" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Enable ID Server or Relay Server first" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Cannot start service: The ID Server or Relay Server must be enabled in the configuration first. Please check \"Enable ID Server\" or \"Enable Relay Server\" below and click \"Save & Apply\"." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Failed to start service: " +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Failed to stop service: " +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Failed to restart service: " +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Start" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Stop" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Restart" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Start will only work if at least \"Enable ID Server\" or \"Enable Relay Server\" is checked in the Configuration section below." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Enable ID Server or Relay Server in Configuration first" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Start the service" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Configuration" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "General" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "ID Server (hbbs)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Relay Server (hbbr)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Enable ID Server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Enable Relay Server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Port (-p, --port)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Sets the listening port for the ID/Rendezvous server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Key (-k, --key)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Only allow clients with the same key. If empty, uses auto-generated key" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Invalid characters. Only alphanumeric and base64 characters (+/=) allowed" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Relay Servers (-r, --relay-servers)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Default relay servers. Add one server per entry (hostname or hostname:port)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Rendezvous Servers (-R, --rendezvous-servers)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Additional rendezvous servers. Add one server per entry (hostname or hostname:port)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "LAN Mask (--mask)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Determine if the connection comes from LAN. Use CIDR notation." +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "UDP Recv Buffer (-M, --rmem)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Sets UDP receive buffer size (0 = system default)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Serial Number (-s, --serial)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Sets configure update serial number" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Software Download URL (-u, --software-url)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Sets the download URL of RustDesk software for clients" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Invalid characters detected" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "URL must start with http:// or https://" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "ALWAYS_USE_RELAY" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Force all connections to use relay servers" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "RUST_LOG" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Logging level for the ID server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Logging level for the relay server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Default" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Error" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Warning" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Info" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Debug" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Trace" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Sets the listening port for the relay server" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "LIMIT_SPEED" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Speed limit per connection in Mb/s (0 = default)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "SINGLE_BANDWIDTH" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Bandwidth limit per single connection in MB/s (0 = default)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "TOTAL_BANDWIDTH" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Total bandwidth limit in MB/s (0 = default)" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "DOWNGRADE_THRESHOLD" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Threshold for connection downgrade" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "DOWNGRADE_START_CHECK" +msgstr "" + +#: applications/luci-app-rustdesk-server/htdocs/luci-static/resources/view/rustdesk-server/general.js +msgid "Start check time for connection downgrade" +msgstr "" + +#: applications/luci-app-rustdesk-server/root/usr/share/rpcd/acl.d/luci-app-rustdesk-server.json +msgid "Grant access to RustDesk Server configuration" +msgstr "" diff --git a/applications/luci-app-rustdesk-server/root/etc/config/rustdesk-server b/applications/luci-app-rustdesk-server/root/etc/config/rustdesk-server new file mode 100644 index 0000000000..c9b8e279d9 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/etc/config/rustdesk-server @@ -0,0 +1,3 @@ +config rustdesk-server + option enabled '0' + option enabled_relay '0' diff --git a/applications/luci-app-rustdesk-server/root/etc/init.d/rustdesk-server b/applications/luci-app-rustdesk-server/root/etc/init.d/rustdesk-server new file mode 100755 index 0000000000..eaaf9cfe84 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/etc/init.d/rustdesk-server @@ -0,0 +1,129 @@ +#!/bin/sh /etc/rc.common +# shellcheck disable=SC2034,SC2154 + +START=98 +STOP=10 +USE_PROCD=1 + +NAME=rustdesk-server +# Use wrapper scripts that set working directory for key generation +# See /usr/libexec/rustdesk-hbbs and /usr/libexec/rustdesk-hbbr for details +PROG_HBBS=/usr/libexec/rustdesk-hbbs +PROG_HBBR=/usr/libexec/rustdesk-hbbr +BIN_HBBS=/usr/bin/hbbs +BIN_HBBR=/usr/bin/hbbr +KEY_DIR=/etc/rustdesk + +get_config() { + config_get_bool enabled "$1" enabled 0 + config_get_bool enabled_relay "$1" enabled_relay 0 + + # HBBS settings + config_get server_port "$1" server_port + config_get server_key "$1" server_key + config_get server_relay_servers "$1" server_relay_servers + config_get server_rendezvous_servers "$1" server_rendezvous_servers + config_get server_mask "$1" server_mask + config_get server_rmem "$1" server_rmem + config_get server_serial "$1" server_serial + config_get server_software_url "$1" server_software_url + config_get_bool server_env_always_use_relay "$1" server_env_always_use_relay 0 + config_get server_env_rust_log "$1" server_env_rust_log + + # HBBR settings + config_get relay_port "$1" relay_port + config_get relay_key "$1" relay_key + config_get relay_env_rust_log "$1" relay_env_rust_log + config_get relay_env_limit_speed "$1" relay_env_limit_speed + config_get relay_env_single_bandwidth "$1" relay_env_single_bandwidth + config_get relay_env_total_bandwidth "$1" relay_env_total_bandwidth + config_get relay_env_downgrade_threshold "$1" relay_env_downgrade_threshold + config_get relay_env_downgrade_start_check "$1" relay_env_downgrade_start_check +} + +start_hbbs() { + [ "$enabled" = "1" ] || return 0 + [ -x "$BIN_HBBS" ] || { + logger -t "$NAME" "Error: $BIN_HBBS not found or not executable" + return 1 + } + + # Convert UCI lists (space-separated) to comma-separated for CLI + local relay_servers_csv="${server_relay_servers// /,}" + local rendezvous_servers_csv="${server_rendezvous_servers// /,}" + + # Build command arguments + local cmd_args="${server_port:+ -p $server_port}${relay_servers_csv:+ -r $relay_servers_csv}${server_key:+ -k $server_key}${rendezvous_servers_csv:+ -R $rendezvous_servers_csv}${server_mask:+ --mask $server_mask}${server_rmem:+ -M $server_rmem}${server_serial:+ -s $server_serial}${server_software_url:+ -u $server_software_url}" + + procd_open_instance hbbs + procd_set_param command "$PROG_HBBS" $cmd_args + + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/hbbs.pid + + # Environment variables + procd_append_param env "DB_URL=/tmp/rustdesk_db_v2.sqlite3" + [ "$server_env_always_use_relay" = "1" ] && procd_append_param env "ALWAYS_USE_RELAY=Y" + [ -n "$server_env_rust_log" ] && procd_append_param env "RUST_LOG=$server_env_rust_log" + + procd_close_instance +} + +start_hbbr() { + [ "$enabled_relay" = "1" ] || return 0 + [ -x "$BIN_HBBR" ] || { + logger -t "$NAME" "Error: $BIN_HBBR not found or not executable" + return 1 + } + + # Build command arguments + local cmd_args="${relay_port:+ -p $relay_port}${relay_key:+ -k $relay_key}" + + procd_open_instance hbbr + procd_set_param command "$PROG_HBBR" $cmd_args + + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/hbbr.pid + + # Environment variables + [ -n "$relay_env_rust_log" ] && procd_append_param env "RUST_LOG=$relay_env_rust_log" + [ "${relay_env_limit_speed:-0}" != "0" ] && procd_append_param env "LIMIT_SPEED=$relay_env_limit_speed" + [ "${relay_env_single_bandwidth:-0}" != "0" ] && procd_append_param env "SINGLE_BANDWIDTH=$relay_env_single_bandwidth" + [ "${relay_env_total_bandwidth:-0}" != "0" ] && procd_append_param env "TOTAL_BANDWIDTH=$relay_env_total_bandwidth" + [ -n "$relay_env_downgrade_threshold" ] && procd_append_param env "DOWNGRADE_THRESHOLD=$relay_env_downgrade_threshold" + [ -n "$relay_env_downgrade_start_check" ] && procd_append_param env "DOWNGRADE_START_CHECK=$relay_env_downgrade_start_check" + + procd_close_instance +} + +start_service() { + mkdir -p "$KEY_DIR" + + config_load "$NAME" + config_foreach get_config "$NAME" + + [ "$enabled" != "1" ] && [ "$enabled_relay" != "1" ] && { + logger -t "$NAME" "Services disabled. Not starting." + return 0 + } + + start_hbbs + start_hbbr +} + +stop_service() { + logger -t "$NAME" "Service stopping" +} + +service_triggers() { + procd_add_reload_trigger "$NAME" +} + +reload_service() { + stop + start +} diff --git a/applications/luci-app-rustdesk-server/root/etc/uci-defaults/50-luci-rustdesk-server b/applications/luci-app-rustdesk-server/root/etc/uci-defaults/50-luci-rustdesk-server new file mode 100644 index 0000000000..c91c840c37 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/etc/uci-defaults/50-luci-rustdesk-server @@ -0,0 +1,13 @@ +#!/bin/sh + +# luci-app-rustdesk-server UCI defaults +# This script runs on first install to set up sensible defaults + +uci -q batch <<-EOF >/dev/null + set rustdesk-server.@rustdesk-server[0]=rustdesk-server + set rustdesk-server.@rustdesk-server[0].enabled='0' + set rustdesk-server.@rustdesk-server[0].enabled_relay='0' + commit rustdesk-server +EOF + +exit 0 diff --git a/applications/luci-app-rustdesk-server/root/usr/libexec/rustdesk-hbbr b/applications/luci-app-rustdesk-server/root/usr/libexec/rustdesk-hbbr new file mode 100755 index 0000000000..5a14f6017c --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/usr/libexec/rustdesk-hbbr @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +# Wrapper script for hbbr (RustDesk Relay server) +# +# Purpose: This wrapper changes to /etc/rustdesk before exec'ing hbbr. +# This is for consistency with hbbs (which needs the working directory +# for key generation). Using exec ensures the process name remains +# "hbbr" (not "sh") so syslog filtering works correctly. + +cd /etc/rustdesk +exec /usr/bin/hbbr "$@" diff --git a/applications/luci-app-rustdesk-server/root/usr/libexec/rustdesk-hbbs b/applications/luci-app-rustdesk-server/root/usr/libexec/rustdesk-hbbs new file mode 100755 index 0000000000..ca61a47073 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/usr/libexec/rustdesk-hbbs @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +# Wrapper script for hbbs (RustDesk ID/Rendezvous server) +# +# Purpose: This wrapper changes to /etc/rustdesk before exec'ing hbbs. +# hbbs generates its keypair (id_ed25519, id_ed25519.pub) in its +# working directory. Using exec ensures the process name remains +# "hbbs" (not "sh") so syslog filtering works correctly. + +cd /etc/rustdesk +exec /usr/bin/hbbs "$@" diff --git a/applications/luci-app-rustdesk-server/root/usr/share/luci/menu.d/luci-app-rustdesk-server.json b/applications/luci-app-rustdesk-server/root/usr/share/luci/menu.d/luci-app-rustdesk-server.json new file mode 100644 index 0000000000..4d989f8ff9 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/usr/share/luci/menu.d/luci-app-rustdesk-server.json @@ -0,0 +1,33 @@ +{ + "admin/services/rustdesk-server": { + "title": "RustDesk Server", + "order": 60, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ + "luci-app-rustdesk-server" + ], + "uci": { + "rustdesk-server": true + } + } + }, + "admin/services/rustdesk-server/general": { + "title": "General", + "order": 10, + "action": { + "type": "view", + "path": "rustdesk-server/general" + } + }, + "admin/services/rustdesk-server/logs": { + "title": "Log", + "order": 20, + "action": { + "type": "view", + "path": "rustdesk-server/logs" + } + } +} diff --git a/applications/luci-app-rustdesk-server/root/usr/share/rpcd/acl.d/luci-app-rustdesk-server.json b/applications/luci-app-rustdesk-server/root/usr/share/rpcd/acl.d/luci-app-rustdesk-server.json new file mode 100644 index 0000000000..9066cf1279 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/usr/share/rpcd/acl.d/luci-app-rustdesk-server.json @@ -0,0 +1,75 @@ +{ + "luci-app-rustdesk-server": { + "description": "Grant access to RustDesk Server configuration", + "read": { + "uci": [ + "rustdesk-server" + ], + "ubus": { + "luci.rustdesk-server": [ + "get_status", + "get_public_key", + "get_version" + ], + "log": [ + "read" + ], + "file": [ + "stat", + "exec" + ] + }, + "file": { + "/usr/bin/hbbs": [ + "read" + ], + "/usr/bin/hbbr": [ + "read" + ], + "/etc/rustdesk/id_ed25519.pub": [ + "read" + ], + "/etc/rustdesk/*": [ + "list" + ], + "/etc/rc.d/S98rustdesk-server": [ + "read" + ] + } + }, + "write": { + "uci": [ + "rustdesk-server" + ], + "ubus": { + "luci.rustdesk-server": [ + "service_action", + "regenerate_key" + ] + }, + "cgi-io": [ + "exec" + ], + "file": { + "/etc/init.d/rustdesk-server start": [ + "exec" + ], + "/etc/init.d/rustdesk-server stop": [ + "exec" + ], + "/etc/init.d/rustdesk-server restart": [ + "exec" + ], + "/etc/init.d/rustdesk-server reload": [ + "exec" + ], + "/etc/init.d/rustdesk-server enable": [ + "exec" + ], + "/etc/init.d/rustdesk-server disable": [ + "exec" + ] + } + } + } +} diff --git a/applications/luci-app-rustdesk-server/root/usr/share/rpcd/ucode/rustdesk-server.uc b/applications/luci-app-rustdesk-server/root/usr/share/rpcd/ucode/rustdesk-server.uc new file mode 100644 index 0000000000..184bb2f0b4 --- /dev/null +++ b/applications/luci-app-rustdesk-server/root/usr/share/rpcd/ucode/rustdesk-server.uc @@ -0,0 +1,163 @@ +#!/usr/bin/env ucode +'use strict'; + +import { popen, access, readfile, unlink } from 'fs'; +import { process_list, init_enabled, init_action } from 'luci.sys'; + +const BIN_DIR = '/usr/bin'; +const KEY_DIR = '/etc/rustdesk'; + +/* + * Helper functions to reduce code duplication + */ + +// Shell escape a string to prevent command injection +function shellquote(s) { + return `'${replace(s, "'", "'\\''")}'`; +} + +// Get PID of a process by name using luci.sys.process_list() +function getProcessPid(process_name) { + for (let proc in process_list()) { + if (index(proc.COMMAND, process_name) >= 0) { + return proc.PID; + } + } + return null; +} + +// Execute a command and return trimmed output (for version queries only) +function execCommand(bin, args) { + let result = null; + let cmd = shellquote(bin) + ' ' + args; + let pp = popen(cmd, 'r'); + if (pp) { + let output = pp.read('all'); + pp.close(); + if (output) { + result = trim(output); + } + } + return result; +} + +// Check if a file exists +function fileExists(path) { + return !!access(path); +} + +// Read file content and trim +function readFileContent(path) { + let content = readfile(path); + return content ? trim(content) : null; +} + +// Safe file deletion +function safeUnlink(path) { + return unlink(path) || false; +} + +const methods = { + get_status: { + call: function() { + // Check if service is enabled for boot using luci.sys.init_enabled() + let boot_enabled = init_enabled('rustdesk-server'); + + return { + hbbs_pid: getProcessPid('hbbs'), + hbbr_pid: getProcessPid('hbbr'), + hbbs_exists: fileExists(BIN_DIR + '/hbbs'), + hbbr_exists: fileExists(BIN_DIR + '/hbbr'), + boot_enabled: boot_enabled + }; + } + }, + + get_public_key: { + call: function() { + let key_path = KEY_DIR + '/id_ed25519.pub'; + let key_exists = fileExists(key_path); + let public_key = null; + + if (key_exists) { + public_key = readFileContent(key_path); + } + + return { + key_exists: key_exists, + public_key: public_key, + key_path: key_path + }; + } + }, + + service_action: { + args: { action: 'action' }, + call: function(req) { + let action = ''; + + if (req && req.args && req.args.action) { + action = req.args.action; + } + + // Validate action - whitelist approach + const valid_actions = ['start', 'stop', 'restart', 'reload', 'enable', 'disable']; + if (index(valid_actions, action) < 0) { + return { + success: false, + error: 'Invalid action. Allowed: ' + join(', ', valid_actions) + }; + } + + // Use luci.sys.init_action() for service control + let result = init_action('rustdesk-server', action); + + return { + success: (result === 0), + action: action, + exit_code: result + }; + } + }, + + get_version: { + call: function() { + return { + hbbs_version: fileExists(BIN_DIR + '/hbbs') ? execCommand(BIN_DIR + '/hbbs', '--version 2>&1') : null, + hbbr_version: fileExists(BIN_DIR + '/hbbr') ? execCommand(BIN_DIR + '/hbbr', '--version 2>&1') : null + }; + } + }, + + regenerate_key: { + call: function() { + let key_priv = KEY_DIR + '/id_ed25519'; + let key_pub = KEY_DIR + '/id_ed25519.pub'; + + // Step 1: Stop the service first so keys are not in use + // init_action is synchronous - waits for service to fully stop + init_action('rustdesk-server', 'stop'); + + // Step 2: Remove existing keys + let priv_deleted = safeUnlink(key_priv); + let pub_deleted = safeUnlink(key_pub); + + // Verify keys are deleted + let keys_deleted = !fileExists(key_priv) && !fileExists(key_pub); + + // The UI will call restart to regenerate the keys + // hbbs automatically generates new keys on startup if they don't exist + + return { + success: keys_deleted, + keys_deleted: keys_deleted, + priv_deleted: priv_deleted, + pub_deleted: pub_deleted, + key_path: key_pub, + message: keys_deleted ? 'Keys deleted. Restart service to generate new keys.' : 'Failed to delete keys' + }; + } + } +}; + +return { 'luci.rustdesk-server': methods };