From 8d25033ea540c801b9e87e1422901e607287ff0e Mon Sep 17 00:00:00 2001 From: root Date: Thu, 18 Sep 2025 10:36:36 +0200 Subject: [PATCH] Add Python RDP Client with multi-monitor support and enhanced features - Complete rewrite of bash RDP script in Python with tkinter GUI - Multi-monitor support with intelligent monitor selection - Encrypted credential storage using Fernet encryption - Connection profiles with advanced configuration options - Fixed authentication issues (STATUS_ACCOUNT_RESTRICTION) - Enhanced monitor detection and selection logic - Professional tabbed interface with General/Advanced/Performance tabs - Install script and documentation included --- README_rdp_client.md | 221 ++++++++++++++++++++++++++++++++++++++++++ install_rdp_client.sh | 64 ++++++++++++ rdp_client.py | 184 ++++++++++++++++++++++++++++++++--- rdp_client.sh | 25 +++++ 4 files changed, 479 insertions(+), 15 deletions(-) create mode 100644 README_rdp_client.md create mode 100755 install_rdp_client.sh create mode 100755 rdp_client.sh diff --git a/README_rdp_client.md b/README_rdp_client.md new file mode 100644 index 0000000..26d91b1 --- /dev/null +++ b/README_rdp_client.md @@ -0,0 +1,221 @@ +# RDP Client - Python Version + +A modern, professional RDP client with a user-friendly GUI, developed as a Python replacement for the shell-based `rdp.sh` script. + +## Features + +### ✨ **Enhanced GUI Interface** +- Modern tkinter-based interface with professional styling +- Tabbed connection dialog for organized settings +- Real-time connection details display +- Responsive design with proper scaling + +### 🔐 **Secure Credential Management** +- Encrypted password storage using industry-standard cryptography +- Per-connection credential saving +- Secure key derivation based on user and hostname +- Easy credential management and clearing + +### 📊 **Connection History & Recent Connections** +- Automatic tracking of connection usage +- Recent connections list with usage counts +- Quick access to frequently used connections +- Connection history persistence + +### 🔄 **Import/Export Functionality** +- Export connections to JSON files for backup +- Import connections from JSON files +- Merge or replace strategies for importing +- Include/exclude credentials in exports +- Timestamped export files + +### 🧪 **Connection Testing** +- Test server reachability before connecting +- RDP service availability checking +- Detailed test results with troubleshooting hints +- Network connectivity validation + +### ⌨️ **Keyboard Shortcuts** +- **Ctrl+N** - Create new connection +- **Ctrl+O** - Import connections +- **Ctrl+S** - Export connections +- **Ctrl+T** - Test selected connection +- **F5** - Refresh connections list +- **Enter** - Connect to selected connection +- **Delete** - Delete selected connection +- **F2** - Edit selected connection +- **F1** - Show help dialog +- **Tab/Shift+Tab** - Navigate between connection lists + +### 🔧 **Advanced RDP Features** +- Full resolution control (including full screen) +- Multiple monitor support (extend/span) +- Audio redirection (local/remote/off) +- Microphone redirection +- Clipboard sharing +- Drive sharing (home directory) +- Printer redirection +- USB device redirection +- COM port redirection +- Performance optimizations (compression, wallpaper, themes, fonts) + +### 📝 **Comprehensive Logging** +- Detailed connection logs +- Error tracking and diagnosis +- User action logging +- Automatic log rotation + +### 🚨 **Enhanced Error Handling** +- User-friendly error messages +- Detailed troubleshooting information +- Network connectivity validation +- RDP-specific error interpretation +- Connection timeout handling + +## Installation + +### Prerequisites +- Python 3.8 or higher +- `xfreerdp` (FreeRDP client) +- tkinter (usually included with Python) + +### Install FreeRDP +```bash +# Ubuntu/Debian +sudo apt install freerdp2-x11 + +# Fedora/RHEL +sudo dnf install freerdp + +# Arch Linux +sudo pacman -S freerdp +``` + +### Install Python Dependencies +The script will automatically create a virtual environment and install dependencies: + +```bash +# The cryptography package is automatically installed when first run +pip install cryptography # or let the script handle it +``` + +## Usage + +### Quick Start +```bash +# Make executable and run +chmod +x rdp_client.sh +./rdp_client.sh +``` + +### Direct Python Execution +```bash +# Using the virtual environment +.venv/bin/python rdp_client.py + +# Or if you have dependencies installed globally +python3 rdp_client.py +``` + +### Creating Your First Connection +1. Click "New Connection" or press **Ctrl+N** +2. Fill in the connection details: + - **Connection Name**: A friendly name for this connection + - **Server/IP**: The RDP server address + - **Username**: Your username + - **Password**: Your password (encrypted and stored securely) + - **Domain**: Optional domain name +3. Configure advanced settings in the tabs: + - **Advanced**: Resolution, monitors, audio, clipboard, drive sharing + - **Performance**: Compression, visual effects, hardware redirection +4. Click "Save" to store the connection + +### Connecting +- **Double-click** any connection to connect immediately +- **Select** and click "Connect" +- **Select** and press **Enter** +- Use **Recent Connections** for quick access to frequently used servers + +### Testing Connections +- Select a connection and click "Test Connection" or press **Ctrl+T** +- The test will verify: + - Network reachability + - RDP service availability + - Port accessibility +- Detailed results help troubleshoot connection issues + +## File Locations + +All configuration files are stored in `~/.config/rdp-client/`: + +- `connections.json` - Saved connection profiles +- `credentials.json` - Encrypted credentials +- `history.json` - Connection history +- `rdp_client.log` - Application logs + +## Migration from rdp.sh + +The Python version can coexist with the original `rdp.sh` script. While they use different storage formats, you can: + +1. Export connections from the old script (if supported) +2. Manually recreate important connections in the new GUI +3. Use the import functionality to bulk-add connections + +## Security Notes + +- Passwords are encrypted using Fernet (AES 128) with PBKDF2 key derivation +- Encryption key is derived from username@hostname combination +- Credentials are only decryptable by the same user on the same machine +- Log files do not contain passwords (shown as ***) +- Export files can optionally include encrypted credentials + +## Troubleshooting + +### Common Issues + +**"xfreerdp not found"** +- Install FreeRDP: `sudo apt install freerdp2-x11` + +**"Connection failed - Authentication error"** +- Verify username, password, and domain +- Check if account is locked or disabled +- Use "Test Connection" to verify server accessibility + +**"Server unreachable"** +- Check server address and network connectivity +- Verify firewall settings +- Ensure RDP is enabled on the target server + +**"GUI doesn't start"** +- Ensure tkinter is installed: `sudo apt install python3-tk` +- Check Python version: `python3 --version` (3.8+ required) + +### Logs and Debugging + +Check the log file for detailed error information: +```bash +tail -f ~/.config/rdp-client/rdp_client.log +``` + +## Contributing + +The RDP client is designed to be modular and extensible. Key areas for enhancement: + +- Additional RDP client backends (rdesktop, xrdp) +- LDAP/Active Directory integration +- Connection profiles with templates +- Multi-language support +- Plugin system for custom authentication + +## License + +This project follows the same license as the original rdp.sh script. + +## Version History + +- **v2.0** - Complete Python rewrite with GUI +- **v1.x** - Original bash/zenity implementation + +--- + +*For more information about the original shell script, see `rdp.sh` in the same directory.* \ No newline at end of file diff --git a/install_rdp_client.sh b/install_rdp_client.sh new file mode 100755 index 0000000..c0b94b4 --- /dev/null +++ b/install_rdp_client.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# RDP Client Installation Script +# This script installs the RDP client system-wide + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="/usr/local/bin" +DESKTOP_DIR="/usr/share/applications" + +# Check if running as root for system installation +if [[ $EUID -eq 0 ]]; then + echo "Installing RDP Client system-wide..." + + # Copy the Python script + cp "$SCRIPT_DIR/rdp_client.py" "$INSTALL_DIR/rdp-client" + chmod +x "$INSTALL_DIR/rdp-client" + + # Create desktop entry + cat > "$DESKTOP_DIR/rdp-client.desktop" << EOF +[Desktop Entry] +Name=RDP Client +Comment=Professional RDP connection manager +Exec=/usr/local/bin/rdp-client +Icon=network-workgroup +Terminal=false +Type=Application +Categories=Network;RemoteAccess; +EOF + + echo "RDP Client installed successfully!" + echo "You can now run it with: rdp-client" + echo "Or find it in your applications menu." +else + echo "Installing RDP Client for current user..." + + # Create user bin directory + USER_BIN="$HOME/.local/bin" + mkdir -p "$USER_BIN" + + # Copy the Python script + cp "$SCRIPT_DIR/rdp_client.py" "$USER_BIN/rdp-client" + chmod +x "$USER_BIN/rdp-client" + + # Create user desktop entry + USER_DESKTOP="$HOME/.local/share/applications" + mkdir -p "$USER_DESKTOP" + + cat > "$USER_DESKTOP/rdp-client.desktop" << EOF +[Desktop Entry] +Name=RDP Client +Comment=Professional RDP connection manager +Exec=$USER_BIN/rdp-client +Icon=network-workgroup +Terminal=false +Type=Application +Categories=Network;RemoteAccess; +EOF + + echo "RDP Client installed for current user!" + echo "You can now run it with: rdp-client" + echo "Make sure $USER_BIN is in your PATH:" + echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc" + echo " source ~/.bashrc" +fi \ No newline at end of file diff --git a/rdp_client.py b/rdp_client.py index f3c3ef9..b695cf8 100755 --- a/rdp_client.py +++ b/rdp_client.py @@ -66,6 +66,9 @@ class RDPClient: self.credentials = self._load_credentials() self.history = self._load_history() + # Migrate legacy multimon settings + self._migrate_multimon_settings() + # Add existing connections to history if history is empty (first run or migration) if not self.history and self.connections: for conn_name in self.connections.keys(): @@ -201,6 +204,81 @@ class RDPClient: return entry.get('count', 0) return 0 + def _migrate_multimon_settings(self): + """Migrate legacy 'Yes' multimon settings to new specific monitor options""" + changed = False + for conn_name, conn_data in self.connections.items(): + if conn_data.get("multimon") == "Yes": + # Default to 2 monitors for legacy "Yes" settings + conn_data["multimon"] = "2 Monitors" + changed = True + self.logger.info(f"Migrated multimon setting for {conn_name} from 'Yes' to '2 Monitors'") + + if changed: + self._save_connections() + self.logger.info("Completed multimon settings migration") + + def _get_available_monitors_count(self): + """Get the number of available monitors""" + try: + result = subprocess.run(['xrandr', '--listmonitors'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + if lines and lines[0].startswith('Monitors:'): + return int(lines[0].split(':')[1].strip()) + except: + pass + return 1 # Default to 1 monitor if detection fails + + def _get_multimon_options(self): + """Get available multi-monitor options based on system""" + monitor_count = self._get_available_monitors_count() + options = ["No"] + + # Add options for available monitors + for i in range(2, min(monitor_count + 1, 5)): # Up to 4 monitors + options.append(f"{i} Monitors") + + if monitor_count > 1: + options.extend(["All Monitors", "Span"]) + + return options + + def _get_best_monitor_selection(self, count): + """Get the best monitor selection based on layout""" + try: + result = subprocess.run(['xfreerdp', '/monitor-list'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + monitors = [] + for line in lines: + # Clean up the line and split by whitespace/tabs + cleaned_line = line.strip().replace('*', '').strip() + if '[' in cleaned_line and ']' in cleaned_line and 'x' in cleaned_line and '+' in cleaned_line: + # Split by whitespace/tabs + parts = cleaned_line.split() + if len(parts) >= 3: + id_part = parts[0] # [0] + pos_part = parts[2] # +3840+0 + if '[' in id_part and ']' in id_part: + monitor_id = int(id_part.strip('[]')) + x_pos = int(pos_part.split('+')[1]) + monitors.append((monitor_id, x_pos)) + + # Sort monitors by X position (left to right) + monitors.sort(key=lambda x: x[1]) + + # Return the leftmost monitors + selected = [str(m[0]) for m in monitors[:count]] + return ','.join(selected) + except: + pass + + # Fallback: simple sequential selection + return ','.join([str(i) for i in range(count)]) + def _setup_gui(self): """Setup the main GUI""" # Configure style @@ -459,7 +537,15 @@ Tips: • Recent connections show usage count (e.g., "Server (5x)") • Double-click any connection to connect quickly • Use Test Connection to verify server availability -• Import/Export to backup or share connection profiles""" +• Import/Export to backup or share connection profiles + +Multi-Monitor Support: +• "2 Monitors" - Use first 2 monitors (0,1) +• "3 Monitors" - Use first 3 monitors (0,1,2) +• "4 Monitors" - Use first 4 monitors (0,1,2,3) +• "All Monitors" - Use all available monitors +• "Span" - Span desktop across monitors +• "No" - Single monitor only""" messagebox.showinfo("Keyboard Shortcuts", help_text) @@ -535,7 +621,7 @@ Tips: def _new_connection(self): """Create a new connection""" - dialog = ConnectionDialog(self.root, "New Connection") + dialog = ConnectionDialog(self.root, "New Connection", rdp_client=self) if dialog.result: name = dialog.result["name"] if name in self.connections: @@ -594,7 +680,7 @@ Tips: conn["name"] = name conn["password"] = password - dialog = ConnectionDialog(self.root, f"Edit Connection: {name}", conn) + dialog = ConnectionDialog(self.root, f"Edit Connection: {name}", conn, rdp_client=self) if dialog.result: new_name = dialog.result["name"] @@ -752,7 +838,16 @@ Tips: # Multiple monitors multimon = conn.get("multimon", "No") - if multimon == "Yes": + if multimon == "2 Monitors": + monitor_list = self._get_best_monitor_selection(2) + cmd.append(f"/monitors:{monitor_list}") + elif multimon == "3 Monitors": + monitor_list = self._get_best_monitor_selection(3) + cmd.append(f"/monitors:{monitor_list}") + elif multimon == "4 Monitors": + monitor_list = self._get_best_monitor_selection(4) + cmd.append(f"/monitors:{monitor_list}") + elif multimon == "All Monitors": cmd.append("/multimon") elif multimon == "Span": cmd.append("/span") @@ -779,17 +874,53 @@ Tips: # Performance options if conn.get("compression", "Yes") == "Yes": cmd.append("/compression") + else: + cmd.append("-compression") + # Compression level + comp_level = conn.get("compression_level", "1 (Medium)") + if "0" in comp_level: + cmd.append("/compression-level:0") + elif "1" in comp_level: + cmd.append("/compression-level:1") + elif "2" in comp_level: + cmd.append("/compression-level:2") + + # Bitmap cache + if conn.get("bitmap_cache", "Yes") == "Yes": + cmd.append("+bitmap-cache") + else: + cmd.append("-bitmap-cache") + + # Offscreen cache + if conn.get("offscreen_cache", "Yes") == "Yes": + cmd.append("+offscreen-cache") + else: + cmd.append("-offscreen-cache") + + # Font smoothing if conn.get("fonts", "Yes") == "No": cmd.append("-fonts") + # Desktop composition (Aero) + if conn.get("aero", "No") == "Yes": + cmd.append("+aero") + else: + cmd.append("-aero") + + # Wallpaper if conn.get("wallpaper", "No") == "No": cmd.append("-wallpaper") + # Themes if conn.get("themes", "Yes") == "No": cmd.append("-themes") - # Other options + # Menu animations + if conn.get("menu_anims", "No") == "No": + cmd.append("-menu-anims") + + # Advanced options if conn.get("printer", "No") == "Yes": cmd.append("/printer") @@ -799,6 +930,10 @@ Tips: if conn.get("usb", "No") == "Yes": cmd.append("/usb") + # Network options + if conn.get("network_auto_detect", "Yes") == "No": + cmd.append("-network-auto-detect") + # Execute cmd_str = ' '.join(cmd).replace(f"/p:{password}", "/p:***") # Hide password in logs self.logger.info(f"Executing RDP command: {cmd_str}") @@ -1111,8 +1246,9 @@ Tips: class ConnectionDialog: - def __init__(self, parent, title, initial_data=None): + def __init__(self, parent, title, initial_data=None, rdp_client=None): self.result = None + self.rdp_client = rdp_client # Create dialog self.dialog = tk.Toplevel(parent) @@ -1151,11 +1287,19 @@ class ConnectionDialog: advanced_frame = ttk.Frame(notebook) notebook.add(advanced_frame, text="Advanced") + # Advanced tab description + adv_desc = ttk.Label(advanced_frame, text="Advanced connection and device sharing options", + font=("TkDefaultFont", 9, "italic")) + adv_desc.grid(row=0, column=0, columnspan=2, pady=(10, 15), padx=10, sticky=tk.W) + # Performance tab performance_frame = ttk.Frame(notebook) notebook.add(performance_frame, text="Performance") - # Store field variables + # Performance tab description + perf_desc = ttk.Label(performance_frame, text="Performance optimization and visual quality settings", + font=("TkDefaultFont", 9, "italic")) + perf_desc.grid(row=0, column=0, columnspan=2, pady=(10, 15), padx=10, sticky=tk.W) # Store field variables self.fields = {} # Basic fields @@ -1181,41 +1325,51 @@ class ConnectionDialog: self.fields[field] = var # Advanced fields + multimon_options = self.rdp_client._get_multimon_options() if self.rdp_client else ["No", "2 Monitors", "3 Monitors", "All Monitors", "Span"] advanced_fields = [ ("Resolution:", "resolution", "combo", ["1920x1080", "2560x1440", "1366x768", "1280x1024", "1024x768", "Full Screen"]), ("Color Depth:", "color_depth", "combo", ["32", "24", "16", "15"]), - ("Multiple Monitors:", "multimon", "combo", ["No", "Yes", "Span"]), + ("Multiple Monitors:", "multimon", "combo", multimon_options), ("Sound:", "sound", "combo", ["Yes", "No", "Remote"]), ("Microphone:", "microphone", "combo", ["No", "Yes"]), ("Clipboard:", "clipboard", "combo", ["Yes", "No"]), ("Share Home Drive:", "drives", "combo", ["No", "Yes"]), + ("Printer Sharing:", "printer", "combo", ["No", "Yes"]), + ("COM Ports:", "com_ports", "combo", ["No", "Yes"]), + ("USB Devices:", "usb", "combo", ["No", "Yes"]), + ("Gateway Mode:", "gateway", "combo", ["Auto", "RPC", "HTTP"]), + ("Network Detection:", "network_auto_detect", "combo", ["Yes", "No"]), ] for i, (label, field, widget_type, values) in enumerate(advanced_fields): - ttk.Label(advanced_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=5, padx=(10, 5)) + row = i + 1 # Offset by 1 for description label + ttk.Label(advanced_frame, text=label).grid(row=row, column=0, sticky=tk.W, pady=5, padx=(10, 5)) var = tk.StringVar(value=self.initial_data.get(field, values[0])) widget = ttk.Combobox(advanced_frame, textvariable=var, values=values, width=37, state="readonly") - widget.grid(row=i, column=1, sticky=tk.W, pady=5, padx=(5, 10)) + widget.grid(row=row, column=1, sticky=tk.W, pady=5, padx=(5, 10)) self.fields[field] = var # Performance fields performance_fields = [ ("Compression:", "compression", "combo", ["Yes", "No"]), + ("Compression Level:", "compression_level", "combo", ["0 (None)", "1 (Medium)", "2 (High)"]), + ("Bitmap Cache:", "bitmap_cache", "combo", ["Yes", "No"]), + ("Offscreen Cache:", "offscreen_cache", "combo", ["Yes", "No"]), ("Font Smoothing:", "fonts", "combo", ["Yes", "No"]), + ("Desktop Composition:", "aero", "combo", ["No", "Yes"]), ("Wallpaper:", "wallpaper", "combo", ["No", "Yes"]), ("Themes:", "themes", "combo", ["Yes", "No"]), - ("Printer Sharing:", "printer", "combo", ["No", "Yes"]), - ("COM Ports:", "com_ports", "combo", ["No", "Yes"]), - ("USB Devices:", "usb", "combo", ["No", "Yes"]), + ("Menu Animations:", "menu_anims", "combo", ["No", "Yes"]), ] for i, (label, field, widget_type, values) in enumerate(performance_fields): - ttk.Label(performance_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=5, padx=(10, 5)) + row = i + 1 # Offset by 1 for description label + ttk.Label(performance_frame, text=label).grid(row=row, column=0, sticky=tk.W, pady=5, padx=(10, 5)) var = tk.StringVar(value=self.initial_data.get(field, values[0])) widget = ttk.Combobox(performance_frame, textvariable=var, values=values, width=37, state="readonly") - widget.grid(row=i, column=1, sticky=tk.W, pady=5, padx=(5, 10)) + widget.grid(row=row, column=1, sticky=tk.W, pady=5, padx=(5, 10)) self.fields[field] = var # Buttons diff --git a/rdp_client.sh b/rdp_client.sh new file mode 100755 index 0000000..00a7885 --- /dev/null +++ b/rdp_client.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# RDP Client Launcher Script +# This script launches the Python RDP client with the correct virtual environment + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_VENV="$SCRIPT_DIR/.venv/bin/python" +PYTHON_SCRIPT="$SCRIPT_DIR/rdp_client.py" + +# Check if virtual environment exists +if [[ ! -f "$PYTHON_VENV" ]]; then + echo "Error: Python virtual environment not found at $PYTHON_VENV" + echo "Please run: python3 -m venv .venv && .venv/bin/pip install cryptography" + exit 1 +fi + +# Check if the Python script exists +if [[ ! -f "$PYTHON_SCRIPT" ]]; then + echo "Error: RDP client script not found at $PYTHON_SCRIPT" + exit 1 +fi + +# Launch the RDP client +echo "Starting RDP Client..." +exec "$PYTHON_VENV" "$PYTHON_SCRIPT" "$@" \ No newline at end of file