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
This commit is contained in:
root
2025-09-18 10:36:36 +02:00
parent 69fb363286
commit 8d25033ea5
4 changed files with 479 additions and 15 deletions

221
README_rdp_client.md Normal file
View File

@@ -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.*

64
install_rdp_client.sh Executable file
View File

@@ -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

View File

@@ -66,6 +66,9 @@ class RDPClient:
self.credentials = self._load_credentials() self.credentials = self._load_credentials()
self.history = self._load_history() 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) # Add existing connections to history if history is empty (first run or migration)
if not self.history and self.connections: if not self.history and self.connections:
for conn_name in self.connections.keys(): for conn_name in self.connections.keys():
@@ -201,6 +204,81 @@ class RDPClient:
return entry.get('count', 0) return entry.get('count', 0)
return 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): def _setup_gui(self):
"""Setup the main GUI""" """Setup the main GUI"""
# Configure style # Configure style
@@ -459,7 +537,15 @@ Tips:
• Recent connections show usage count (e.g., "Server (5x)") • Recent connections show usage count (e.g., "Server (5x)")
• Double-click any connection to connect quickly • Double-click any connection to connect quickly
• Use Test Connection to verify server availability • 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) messagebox.showinfo("Keyboard Shortcuts", help_text)
@@ -535,7 +621,7 @@ Tips:
def _new_connection(self): def _new_connection(self):
"""Create a new connection""" """Create a new connection"""
dialog = ConnectionDialog(self.root, "New Connection") dialog = ConnectionDialog(self.root, "New Connection", rdp_client=self)
if dialog.result: if dialog.result:
name = dialog.result["name"] name = dialog.result["name"]
if name in self.connections: if name in self.connections:
@@ -594,7 +680,7 @@ Tips:
conn["name"] = name conn["name"] = name
conn["password"] = password 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: if dialog.result:
new_name = dialog.result["name"] new_name = dialog.result["name"]
@@ -752,7 +838,16 @@ Tips:
# Multiple monitors # Multiple monitors
multimon = conn.get("multimon", "No") 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") cmd.append("/multimon")
elif multimon == "Span": elif multimon == "Span":
cmd.append("/span") cmd.append("/span")
@@ -779,17 +874,53 @@ Tips:
# Performance options # Performance options
if conn.get("compression", "Yes") == "Yes": if conn.get("compression", "Yes") == "Yes":
cmd.append("/compression") 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": if conn.get("fonts", "Yes") == "No":
cmd.append("-fonts") 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": if conn.get("wallpaper", "No") == "No":
cmd.append("-wallpaper") cmd.append("-wallpaper")
# Themes
if conn.get("themes", "Yes") == "No": if conn.get("themes", "Yes") == "No":
cmd.append("-themes") 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": if conn.get("printer", "No") == "Yes":
cmd.append("/printer") cmd.append("/printer")
@@ -799,6 +930,10 @@ Tips:
if conn.get("usb", "No") == "Yes": if conn.get("usb", "No") == "Yes":
cmd.append("/usb") cmd.append("/usb")
# Network options
if conn.get("network_auto_detect", "Yes") == "No":
cmd.append("-network-auto-detect")
# Execute # Execute
cmd_str = ' '.join(cmd).replace(f"/p:{password}", "/p:***") # Hide password in logs cmd_str = ' '.join(cmd).replace(f"/p:{password}", "/p:***") # Hide password in logs
self.logger.info(f"Executing RDP command: {cmd_str}") self.logger.info(f"Executing RDP command: {cmd_str}")
@@ -1111,8 +1246,9 @@ Tips:
class ConnectionDialog: 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.result = None
self.rdp_client = rdp_client
# Create dialog # Create dialog
self.dialog = tk.Toplevel(parent) self.dialog = tk.Toplevel(parent)
@@ -1151,11 +1287,19 @@ class ConnectionDialog:
advanced_frame = ttk.Frame(notebook) advanced_frame = ttk.Frame(notebook)
notebook.add(advanced_frame, text="Advanced") 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 tab
performance_frame = ttk.Frame(notebook) performance_frame = ttk.Frame(notebook)
notebook.add(performance_frame, text="Performance") 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 = {} self.fields = {}
# Basic fields # Basic fields
@@ -1181,41 +1325,51 @@ class ConnectionDialog:
self.fields[field] = var self.fields[field] = var
# Advanced fields # 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 = [ advanced_fields = [
("Resolution:", "resolution", "combo", ["1920x1080", "2560x1440", "1366x768", "1280x1024", "1024x768", "Full Screen"]), ("Resolution:", "resolution", "combo", ["1920x1080", "2560x1440", "1366x768", "1280x1024", "1024x768", "Full Screen"]),
("Color Depth:", "color_depth", "combo", ["32", "24", "16", "15"]), ("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"]), ("Sound:", "sound", "combo", ["Yes", "No", "Remote"]),
("Microphone:", "microphone", "combo", ["No", "Yes"]), ("Microphone:", "microphone", "combo", ["No", "Yes"]),
("Clipboard:", "clipboard", "combo", ["Yes", "No"]), ("Clipboard:", "clipboard", "combo", ["Yes", "No"]),
("Share Home Drive:", "drives", "combo", ["No", "Yes"]), ("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): 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])) 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 = 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 self.fields[field] = var
# Performance fields # Performance fields
performance_fields = [ performance_fields = [
("Compression:", "compression", "combo", ["Yes", "No"]), ("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"]), ("Font Smoothing:", "fonts", "combo", ["Yes", "No"]),
("Desktop Composition:", "aero", "combo", ["No", "Yes"]),
("Wallpaper:", "wallpaper", "combo", ["No", "Yes"]), ("Wallpaper:", "wallpaper", "combo", ["No", "Yes"]),
("Themes:", "themes", "combo", ["Yes", "No"]), ("Themes:", "themes", "combo", ["Yes", "No"]),
("Printer Sharing:", "printer", "combo", ["No", "Yes"]), ("Menu Animations:", "menu_anims", "combo", ["No", "Yes"]),
("COM Ports:", "com_ports", "combo", ["No", "Yes"]),
("USB Devices:", "usb", "combo", ["No", "Yes"]),
] ]
for i, (label, field, widget_type, values) in enumerate(performance_fields): 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])) 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 = 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 self.fields[field] = var
# Buttons # Buttons

25
rdp_client.sh Executable file
View File

@@ -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" "$@"