commit 8620ff31acab0f829f8e501f7d97935caf11a0f6 Author: duffyduck Date: Mon Dec 22 14:50:34 2025 +0100 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..2eab56e --- /dev/null +++ b/README.md @@ -0,0 +1,349 @@ +# RDP Thin Client System - Complete Setup + +Vollständiges Ansible-Deployment für dedizierte RDP-Thin-Clients auf Debian 13 (Trixie). + +## Features + +- Headless Auto-Login +- Profilbasierte RDP-Verbindungen +- Audio/Mikrofon-Weiterleitung (Bluetooth-Headsets!) +- USB/Smart-Card Redirection +- Python-GUI für Profilverwaltung +- Tastenkombination zum Session-Verlassen (Strg+Alt+Q) +- Bluetooth-Manager-Integration + +--- + +## 1. Verzeichnisstruktur + +rdp-thin-client/ +├── ansible/ +│ ├── playbook.yml # Hauptinstallation +│ ├── inventory.ini # Deine Clients +│ └── group_vars/all.yml # Globale Variablen +├── files/ +│ ├── rdp-profile-manager.py # GUI für Profile +│ ├── rdp-launcher.sh      # FreeRDP-Wrapper +│ ├── session-watcher.py   # Tastenkombination überwachen +│ └── branding/                      # FreeRDP-Wrapper + +│ ├── boot-logo.png + +│ ├── boot-logocreator.html #boot logo creator tool in webbrowser + +│ ├── grub-background.png + +│ └── login-background.png + +└── config/ + └── profiles.ini.example + +## 2. Ansible Inventory + +ansible/inventory.ini: + +```ini +[rdp_clients] +thin-client-01 ansible_host=192.168.1.101 +thin-client-02 ansible_host=192.168.1.102 +thin-client-03 ansible_host=192.168.1.103 + +[rdp_clients:vars] +ansible_user=root +ansible_ssh_private_key_file=~/.ssh/id_rsa +``` + +--- + +## 3. Ansible Variables + +**ansible/group_vars/all.yml:** + +```yaml +--- +# System User +thin_client_user: rdpuser +thin_client_password: "{{ 'rdpuser' | password_hash('sha512') }}" + +# Display Manager +display_manager: lightdm + +# RDP Client +rdp_client: freerdp + +# Audio System +audio_system: pipewire + +# Profile Directory +profile_dir: /home/{{ thin_client_user }}/.config/rdp-profiles +profile_file: "{{ profile_dir }}/profiles.ini" + +# Exit Hotkey +exit_hotkey: "Control+Alt+q" + +# Packages +base_packages: + - xorg + - openbox + - lightdm + - python3 + - python3-tk + - python3-configparser + - freerdp2-x11 + - pulseaudio + - pipewire + - pipewire-pulse + - pipewire-audio + - bluez + - blueman + - pcscd + - libpcsclite1 + - libccid + - xinput + - xdotool + - pcmanfm + - lxterminal +``` + +--- + +### 4. Playbook + +```yaml +--- +- name: Setup RDP Thin Client + hosts: rdp_clients + become: yes + + tasks: + # === BASE SYSTEM === + - name: Update APT cache + apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Install base packages + apt: + name: "{{ base_packages }}" + state: present + + - name: Create thin client user + user: + name: "{{ thin_client_user }}" + password: "{{ thin_client_password }}" + shell: /bin/bash + groups: audio,video,bluetooth,plugdev + append: yes + + - name: Configure auto-login for LightDM + copy: + dest: /etc/lightdm/lightdm.conf.d/50-autologin.conf + content: | + [Seat:*] + autologin-user={{ thin_client_user }} + autologin-user-timeout=0 + user-session=openbox + + # === OPENBOX CONFIGURATION === + - name: Create openbox config directory + file: + path: /home/{{ thin_client_user }}/.config/openbox + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Configure Openbox autostart + copy: + dest: /home/{{ thin_client_user }}/.config/openbox/autostart + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + content: | + #!/bin/bash + # Start PipeWire + pipewire & + pipewire-pulse & + + # Start Bluetooth + blueman-applet & + + # Start Session Watcher (monitors exit hotkey) + /usr/local/bin/session-watcher.py & + + # Start RDP Launcher + /usr/local/bin/rdp-launcher.sh & + + - name: Configure Openbox menu (right-click context menu) + copy: + dest: /home/{{ thin_client_user }}/.config/openbox/menu.xml + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + content: | + + + + + + /usr/local/bin/rdp-profile-manager.py + + + + + blueman-manager + + + + + pcmanfm + + + + + lxterminal + + + + + + systemctl reboot + + + + + systemctl poweroff + + + + + + # === AUDIO CONFIGURATION === + - name: Enable PipeWire services for user + systemd: + name: "{{ item }}" + enabled: yes + scope: user + daemon_reload: yes + loop: + - pipewire.service + - pipewire-pulse.service + become_user: "{{ thin_client_user }}" + + # === BLUETOOTH CONFIGURATION === + - name: Enable Bluetooth service + systemd: + name: bluetooth + enabled: yes + state: started + + # === SMART CARD CONFIGURATION === + - name: Enable pcscd service + systemd: + name: pcscd + enabled: yes + state: started + + # === PROFILE DIRECTORY === + - name: Create RDP profile directory + file: + path: "{{ profile_dir }}" + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Create empty profiles.ini if not exists + copy: + dest: "{{ profile_file }}" + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0644' + content: | + # RDP Profile Configuration + # Auto-generated by RDP Thin Client Setup + force: no + + # === COPY SCRIPTS === + - name: Copy RDP Profile Manager + copy: + src: ../src/rdp-profile-manager.py + dest: /usr/local/bin/rdp-profile-manager.py + mode: '0755' + + - name: Copy RDP Launcher + copy: + src: ../src/rdp-launcher.sh + dest: /usr/local/bin/rdp-launcher.sh + mode: '0755' + + - name: Copy Session Watcher + copy: + src: ../src/session-watcher.py + dest: /usr/local/bin/session-watcher.py + mode: '0755' + + # === CLEANUP === + - name: Remove unnecessary packages + apt: + name: + - gnome-* + - libreoffice-* + state: absent + autoremove: yes + + - name: Disable unnecessary services + systemd: + name: "{{ item }}" + enabled: no + state: stopped + loop: + - ModemManager + - cups + ignore_errors: yes + + handlers: + - name: Reboot system + reboot: + msg: "Rebooting to apply thin client configuration" + reboot_timeout: 300 + + +``` + +### 5. Ansible installieren und RDP-Client Debian Hosts vorbereiten + +bash + +```bash +# Ansible installieren +sudo apt update +sudo apt install ansible sshpass + +# SSH-Keys generieren (falls noch nicht vorhanden) +ssh-keygen -t rsa -b 4096 + +# SSH-Keys auf Thin Clients kopieren +ssh-copy-id root@192.168.1.101 +ssh-copy-id root@192.168.1.102 +ssh-copy-id root@192.168.1.1 + +``` + +```bash +cd rdp-thin-client/ansible + +# Syntax-Check +ansible-playbook -i inventory.ini playbook.yml --syntax-check + +# Dry-Run (Check Mode) +ansible-playbook -i inventory.ini playbook.yml --check + +# Deployment ausführen +ansible-playbook -i inventory.ini playbook.yml + +# Nur bestimmte Hosts +ansible-playbook -i inventory.ini playbook.yml --limit thin-client-01 + +# Mit Verbose-Output +ansible-playbook -i inventory.ini playbook.yml -vvv +``` diff --git a/ansible/.playbook.yml.kate-swp b/ansible/.playbook.yml.kate-swp new file mode 100644 index 0000000..c6385d8 Binary files /dev/null and b/ansible/.playbook.yml.kate-swp differ diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000..26c33d9 --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,50 @@ +--- +# System User +thin_client_user: rdpuser +thin_client_password: "{{ 'rdpuser' | password_hash('sha512') }}" + +# Display Manager +display_manager: lightdm + +# RDP Client +rdp_client: freerdp + +# Audio System +audio_system: pipewire + +# Profile Directory +profile_dir: /home/{{ thin_client_user }}/.config/rdp-profiles +profile_file: "{{ profile_dir }}/profiles.ini" + +# Exit Hotkey +exit_hotkey: "Control+Alt+q" + +# Packages +base_packages: + - xorg + - openbox + - lightdm + - python3 + - python3-tk + - python3-pip + - pipewire + - pipewire-pulse + - pipewire-audio + - pipewire-alsa + - wireplumber + - bluez + - blueman + - pcscd + - libpcsclite1 + - libccid + - xinput + - xdotool + - pcmanfm + - lxterminal + - zenity + - pavucontrol + - helvum + - udiskie + - arandr + - autorandr + - numlockx diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 0000000..bfebe11 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,8 @@ +[rdp_clients] +thin-client-01 ansible_host=192.168.0.13 +thin-client-02 ansible_host=192.168.0.29 +thin-client-03 ansible_host=192.168.0.23 + +[rdp_clients:vars] +ansible_user=root +#ansible_ssh_private_key_file=~/.ssh/id_rsa diff --git a/ansible/inventory.ini.example b/ansible/inventory.ini.example new file mode 100644 index 0000000..274c30b --- /dev/null +++ b/ansible/inventory.ini.example @@ -0,0 +1,8 @@ +[rdp_clients] +thin-client-01 ansible_host=192.168.1.101 +thin-client-02 ansible_host=192.168.1.102 +thin-client-03 ansible_host=192.168.1.103 + +[rdp_clients:vars] +ansible_user=root +ansible_ssh_private_key_file=~/.ssh/id_rsa diff --git a/ansible/playbook.yml b/ansible/playbook.yml new file mode 100644 index 0000000..ba8b08d --- /dev/null +++ b/ansible/playbook.yml @@ -0,0 +1,649 @@ +--- +# ansible/playbook.yml +# RDP Thin Client Setup - Main Playbook v14 + +- name: Setup RDP Thin Client + hosts: rdp_clients + become: yes + + tasks: + # Non-free Repos aktivieren + - name: Enable non-free and contrib repositories + lineinfile: + path: /etc/apt/sources.list + regexp: '^deb http://deb.debian.org/debian/ {{ ansible_distribution_release }} main$' + line: 'deb http://deb.debian.org/debian/ {{ ansible_distribution_release }} main contrib non-free non-free-firmware' + backrefs: yes + when: ansible_distribution == 'Debian' + ignore_errors: yes + + - name: Enable non-free and contrib for security updates + lineinfile: + path: /etc/apt/sources.list + regexp: '^deb http://security.debian.org/debian-security {{ ansible_distribution_release }}-security main$' + line: 'deb http://security.debian.org/debian-security {{ ansible_distribution_release }}-security main contrib non-free non-free-firmware' + backrefs: yes + when: ansible_distribution == 'Debian' + ignore_errors: yes + # === BASE SYSTEM === + - name: Update APT cache + apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Ensure sudo is installed + apt: + name: sudo + state: present + update_cache: yes + become: yes + + - name: Remove PulseAudio if present (conflicts with PipeWire) + apt: + name: + - pulseaudio + - pulseaudio-utils + state: absent + ignore_errors: yes + + - name: Install base packages + apt: + name: "{{ item }}" + state: present + loop: "{{ base_packages }}" + ignore_errors: yes + + - name: Ensure util-linux is installed (provides lsblk) + apt: + name: util-linux + state: present + + - name: Install udiskie for USB automount + apt: + name: udiskie + state: present + + + + - name: Update APT cache again for emoji fonts + apt: + update_cache: yes + + - name: Install Emoji fonts for GUI + apt: + name: fonts-noto-color-emoji + state: present + + - name: Install FreeRDP (try multiple package names for Debian 12/13 compatibility) + apt: + name: "{{ item }}" + state: present + loop: + - freerdp3-x11 + - freerdp2-x11 + ignore_errors: yes + + - name: Find FreeRDP binary location + shell: which xfreerdp 2>/dev/null || which xfreerdp3 2>/dev/null || echo "not_found" + register: freerdp_location + changed_when: false + + - name: Create xfreerdp symlink if only xfreerdp3 exists + file: + src: /usr/bin/xfreerdp3 + dest: /usr/bin/xfreerdp + state: link + when: "'xfreerdp3' in freerdp_location.stdout and 'not_found' not in freerdp_location.stdout" + ignore_errors: yes + + - name: Display FreeRDP installation status + debug: + msg: "FreeRDP found at: {{ freerdp_location.stdout }}" + + - name: Create thin client user + user: + name: "{{ thin_client_user }}" + password: "{{ thin_client_password }}" + shell: /bin/bash + groups: audio,video,bluetooth,plugdev,netdev,sudo + append: yes + + - name: Allow rdpuser to use sudo for package installation + lineinfile: + path: /etc/sudoers.d/rdpuser + line: '{{ thin_client_user }} ALL=(ALL) NOPASSWD: /usr/bin/apt, /usr/bin/apt-get, /sbin/reboot, /sbin/shutdown' + create: yes + mode: '0440' + validate: 'visudo -cf %s' + + - name: Create media directory for USB mounts + file: + path: /media/rdpuser + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Create udiskie config directory + file: + path: /home/{{ thin_client_user }}/.config/udiskie + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Configure udiskie to mount in /media/rdpuser + copy: + dest: /home/{{ thin_client_user }}/.config/udiskie/config.yml + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0644' + content: | + device_config: + - id_type: filesystem + mount_path: /media/rdpuser/{device_file} + + + - name: Create LightDM config directory + file: + path: /etc/lightdm/lightdm.conf.d + state: directory + mode: '0755' + + - name: Configure auto-login for LightDM + copy: + dest: /etc/lightdm/lightdm.conf.d/50-autologin.conf + content: | + [Seat:*] + autologin-user={{ thin_client_user }} + autologin-user-timeout=0 + user-session=openbox + + # === OPENBOX CONFIGURATION === + - name: Create openbox config directory + file: + path: /home/{{ thin_client_user }}/.config/openbox + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Configure Openbox autostart + copy: + dest: /home/{{ thin_client_user }}/.config/openbox/autostart + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + content: | + #!/bin/bash + #Nummernblock aktivieren + numlockx on & + # Auto-load display configuration + # Try to load the last saved profile, fallback to common, then auto-detect + if [ -f ~/.config/autorandr/last_profile ]; then + LAST_PROFILE=$(cat ~/.config/autorandr/last_profile) + autorandr --load "$LAST_PROFILE" 2>/dev/null || autorandr --change + else + autorandr --load common 2>/dev/null || autorandr --change + fi + # Deaktiviere Energiesparfunktionen + xset s off # Screensaver aus + xset -dpms # DPMS (Display Power Management) aus + xset s noblank # Kein Blank-Screen + + # Start PipeWire + pipewire & + pipewire-pulse & + + # Start Bluetooth + blueman-applet & + + # Start USB automount (udiskie) + udiskie --tray --automount --notify & + + # Session Watcher läuft als systemd-Service + + # Start RDP Launcher + /usr/local/bin/rdp-launcher.sh & + + - name: Configure Openbox menu (right-click context menu) + copy: + dest: /home/{{ thin_client_user }}/.config/openbox/menu.xml + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + content: | + + + + + + /usr/local/bin/rdp-profile-manager.py + + + + + blueman-manager + + + + + pcmanfm + + + + + pavucontrol + + + + + + arandr + + + + + sh -c 'profile=$(zenity --entry --title="Display Profil speichern" --text="Profilnamen eingeben:"); [ -n "$profile" ] && autorandr --save "$profile" --force && echo "$profile" > ~/.config/autorandr/last_profile && zenitiy --info --text="Profil gespeichert: $profile"' + + + + + sh -c 'profile=$(zenity --list --title="Display Profil laden" --column="Profile" $(autorandr --list));[ -n "$profile" ] && autorandr --load "$profile" && echo "$profile" > ~/.config/autorandr/last_profile && zenity --info --text="Dieses Profil wird automatisch beim nächsten Start geladen"' + + + + + sh -c 'profile=$(zenity --list --title="Display Profil löschen" --text="Profile auswählen zum löschen:" --column="Profiles" $(autorandr --list));[ -n "$profile" ] && zenity --question --title="Löschen bestätigen" --text="Delete profile: $profile?" && autorandr --remove "$profile" && zenity --info --text="Profil gelöscht: $profile"' + + + lxterminal -e "bash -c 'echo Installing Intel graphics driver...; sudo apt update && sudo apt install -y xserver-xorg-video-intel firmware-misc-nonfree && echo Done! Please reboot for changes to take effect.; read -p Press Enter to close...'" + + + + + lxterminal -e "bash -c 'echo Installing AMD/Radeon graphics driver...; sudo apt update && sudo apt install -y firmware-amd-graphics xserver-xorg-video-amdgpu xserver-xorg-video-radeon && echo Done! Please reboot for changes to take effect.; read -p Press Enter to close...'" + + + + + lxterminal -e "bash -c 'echo Installing NVIDIA proprietary driver...; sudo apt update && sudo apt install -y nvidia-driver firmware-misc-nonfree && echo Done! Please reboot for changes to take effect.; read -p Press Enter to close...'" + + + + + + lxterminal -e "bash -c 'echo === Graphics Card Info ===; lspci | grep -i vga; echo; echo === Loaded Driver ===; lsmod | grep -iE video|radeon|amdgpu|nouveau|nvidia|i915; echo; echo === Xorg Driver ===; grep -i driver /var/log/Xorg.0.log 2>/dev/null | tail -n 20; read -p Press Enter to close...'" + + + + + + lxterminal + + + + + + systemctl reboot + + + + + systemctl poweroff + + + + + + + # === AUDIO CONFIGURATION === + # PipeWire wird über Openbox autostart gestartet (siehe autostart config) + # Keine systemd user services nötig + + # === BLUETOOTH CONFIGURATION === + - name: Check if Bluetooth hardware is present + stat: + path: /sys/class/bluetooth + register: bluetooth_hw + + - name: Enable Bluetooth service (if hardware present) + systemd: + name: bluetooth + enabled: yes + state: started + when: bluetooth_hw.stat.exists + ignore_errors: yes + + # === SMART CARD CONFIGURATION === + - name: Check if pcscd is installed + command: which pcscd + register: pcscd_installed + ignore_errors: yes + changed_when: false + + - name: Enable pcscd service (socket-activated on demand) + systemd: + name: pcscd + enabled: yes + when: pcscd_installed.rc == 0 + ignore_errors: yes + + # === PROFILE DIRECTORY === + - name: Create RDP profile directory + file: + path: "{{ profile_dir }}" + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Create empty profiles.ini if not exists + copy: + dest: "{{ profile_file }}" + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0644' + content: | + # RDP Profile Configuration + # Auto-generated by RDP Thin Client Setup + force: no + + # === INSTALL PYTHON DEPENDENCIES === + - name: Install python-xlib via pip + shell: pip3 install python-xlib --break-system-packages + args: + creates: /usr/local/lib/python3.11/dist-packages/Xlib + ignore_errors: yes + + - name: Alternative - Install python-xlib from apt + apt: + name: python3-xlib + state: present + ignore_errors: yes + + # === COPY SCRIPTS === + - name: Copy RDP Profile Manager + copy: + src: ../files/rdp-profile-manager.py + dest: /usr/local/bin/rdp-profile-manager.py + mode: '0755' + + - name: Copy RDP Launcher + copy: + src: ../files/rdp-launcher.sh + dest: /usr/local/bin/rdp-launcher.sh + mode: '0755' + + - name: Copy Session Watcher + copy: + src: ../files/session-watcher.py + dest: /usr/local/bin/session-watcher.py + mode: '0755' + + # === SESSION WATCHER SYSTEMD SERVICE === + - name: Create systemd user service directory + file: + path: /home/{{ thin_client_user }}/.config/systemd/user + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Create Session Watcher systemd service + copy: + dest: /home/{{ thin_client_user }}/.config/systemd/user/session-watcher.service + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0644' + content: | + [Unit] + Description=RDP Session Watcher - Exit Hotkey Monitor + After=graphical.target + + [Service] + Type=simple + ExecStart=/usr/local/bin/session-watcher.py + Restart=always + RestartSec=3 + Environment=DISPLAY=:0 + + [Install] + WantedBy=default.target + + - name: Create default.target.wants directory + file: + path: /home/{{ thin_client_user }}/.config/systemd/user/default.target.wants + state: directory + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + mode: '0755' + + - name: Create symlink to enable Session Watcher service + file: + src: /home/{{ thin_client_user }}/.config/systemd/user/session-watcher.service + dest: /home/{{ thin_client_user }}/.config/systemd/user/default.target.wants/session-watcher.service + owner: "{{ thin_client_user }}" + group: "{{ thin_client_user }}" + state: link + force: yes + ignore_errors: yes + + # === BRANDING === + - name: Check if branding files exist + stat: + path: ../files/branding/boot-logo.png + register: branding_check + delegate_to: localhost + become: no + + - name: Create branding directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /usr/share/pixmaps/hackersoft + - /boot/grub + when: branding_check.stat.exists + + - name: Copy boot logo for Plymouth + copy: + src: ../files/branding/boot-logo.png + dest: /usr/share/pixmaps/hackersoft/boot-logo.png + mode: '0644' + when: branding_check.stat.exists + + - name: Copy GRUB background + copy: + src: ../files/branding/grub-background.png + dest: /boot/grub/hackersoft-bg.png + mode: '0644' + when: branding_check.stat.exists + + - name: Configure LightDM to use custom background + lineinfile: + path: /etc/default/grub + regexp: '^#?GRUB_BACKGROUND=' + line: 'GRUB_BACKGROUND="/boot/grub/hackersoft-bg.png"' + when: branding_check.stat.exists + notify: Update GRUB + + # === POWER MANAGEMENT DEAKTIVIEREN === + - name: Disable systemd suspend/hibernate + systemd: + name: "{{ item }}" + masked: yes + loop: + - sleep.target + - suspend.target + - hibernate.target + - hybrid-sleep.target + ignore_errors: yes + + - name: Create logind config directory + file: + path: /etc/systemd/logind.conf.d + state: directory + mode: '0755' + + - name: Create systemd logind config to disable power management + copy: + dest: /etc/systemd/logind.conf.d/no-suspend.conf + content: | + [Login] + HandlePowerKey=ignore + HandleSuspendKey=ignore + HandleHibernateKey=ignore + HandleLidSwitch=ignore + HandleLidSwitchExternalPower=ignore + IdleAction=ignore + mode: '0644' + notify: Restart systemd-logind + + - name: Enable NumLock in LightDM + lineinfile: + path: /etc/lightdm/lightdm.conf + regexp: '^#?greeter-setup-script=' + line: 'greeter-setup-script=/usr/bin/numlockx on' + create: yes + + - name: Install Plymouth for boot splash + apt: + name: + - plymouth + - plymouth-themes + state: present + when: branding_check.stat.exists + + - name: Create custom Plymouth theme directory + file: + path: /usr/share/plymouth/themes/hackersoft + state: directory + mode: '0755' + when: branding_check.stat.exists + + - name: Create Plymouth theme configuration + copy: + dest: /usr/share/plymouth/themes/hackersoft/hackersoft.plymouth + content: | + [Plymouth Theme] + Name=HackerSoft + Description=HackerSoft RDP Thin Client Boot Splash + ModuleName=script + + [script] + ImageDir=/usr/share/plymouth/themes/hackersoft + ScriptFile=/usr/share/plymouth/themes/hackersoft/hackersoft.script + mode: '0644' + when: branding_check.stat.exists + + - name: Create Plymouth script + copy: + dest: /usr/share/plymouth/themes/hackersoft/hackersoft.script + content: | + logo.image = Image("boot-logo.png"); + logo.sprite = Sprite(logo.image); + logo.opacity = 1.0; + + screen_width = Window.GetWidth(); + screen_height = Window.GetHeight(); + logo_width = logo.image.GetWidth(); + logo_height = logo.image.GetHeight(); + + logo.x = screen_width / 2 - logo_width / 2; + logo.y = screen_height / 2 - logo_height / 2; + logo.sprite.SetPosition(logo.x, logo.y, 0); + + fun refresh_callback() { + logo.sprite.SetOpacity(logo.opacity); + } + + Plymouth.SetRefreshFunction(refresh_callback); + mode: '0644' + when: branding_check.stat.exists + + - name: Copy logo to Plymouth theme + copy: + src: ../files/branding/boot-logo.png + dest: /usr/share/plymouth/themes/hackersoft/boot-logo.png + mode: '0644' + when: branding_check.stat.exists + + - name: Set Plymouth theme + command: plymouth-set-default-theme -R hackersoft + when: branding_check.stat.exists + ignore_errors: yes + + - name: Create backgrounds directory if needed + file: + path: /usr/share/backgrounds + state: directory + mode: '0755' + when: branding_check.stat.exists + + - name: Check if desktop-background.png exists + stat: + path: ../files/branding/desktop-background.png + register: desktop_bg_check + delegate_to: localhost + become: no + + - name: Copy desktop background (dedicated file) + copy: + src: ../files/branding/desktop-background.png + dest: /usr/share/backgrounds/hackersoft-wallpaper.png + mode: '0644' + when: branding_check.stat.exists and desktop_bg_check.stat.exists + + - name: Copy desktop background (fallback to login-background) + copy: + src: ../files/branding/login-background.png + dest: /usr/share/backgrounds/hackersoft-wallpaper.png + mode: '0644' + when: branding_check.stat.exists and not desktop_bg_check.stat.exists + + - name: Install feh for wallpaper management + apt: + name: feh + state: present + when: branding_check.stat.exists + + - name: Set Openbox wallpaper in autostart + lineinfile: + path: /home/{{ thin_client_user }}/.config/openbox/autostart + line: 'feh --bg-scale /usr/share/backgrounds/hackersoft-wallpaper.png &' + insertafter: '^fi' + when: branding_check.stat.exists + + # === CLEANUP === + - name: Remove unnecessary packages + apt: + name: + - gnome-* + - libreoffice-* + state: absent + autoremove: yes + ignore_errors: yes + + - name: Disable unnecessary services + systemd: + name: "{{ item }}" + enabled: no + state: stopped + loop: + - ModemManager + - cups + ignore_errors: yes + + - name: Final message + debug: + msg: "RDP Thin Client setup complete! Please reboot the system." + + handlers: + - name: Update GRUB + command: update-grub + + - name: Restart systemd-logind + systemd: + name: systemd-logind + state: restarted diff --git a/check-rdp-health.sh b/check-rdp-health.sh new file mode 100755 index 0000000..978e753 --- /dev/null +++ b/check-rdp-health.sh @@ -0,0 +1,20 @@ + #!/bin/bash + +# Check if X is running +if ! pgrep -x "Xorg" > /dev/null; then + echo "ERROR: X Server not running" + exit 1 +fi + +# Check if PipeWire is running +if ! pgrep -x "pipewire" > /dev/null; then + echo "WARNING: PipeWire not running" +fi + +# Check if Bluetooth is running +if ! systemctl is-active --quiet bluetooth; then + echo "WARNING: Bluetooth service not active" +fi + +echo "OK: System healthy" +exit 0 diff --git a/config/profiles.ini.example b/config/profiles.ini.example new file mode 100644 index 0000000..2148be5 --- /dev/null +++ b/config/profiles.ini.example @@ -0,0 +1,11 @@ +[wawi] +server = 172.0.2.5 +username = admin +domain = +resolution = client +redirect_audio = True +redirect_microphone = True +redirect_usb = True +redirect_smartcard = True +redirect_printers = False +exit_hotkey = Control+Alt+q diff --git a/files/branding/boot-logo.png b/files/branding/boot-logo.png new file mode 100644 index 0000000..9ff599f Binary files /dev/null and b/files/branding/boot-logo.png differ diff --git a/files/branding/bootlogo-creator.html b/files/branding/bootlogo-creator.html new file mode 100644 index 0000000..b9a15a8 --- /dev/null +++ b/files/branding/bootlogo-creator.html @@ -0,0 +1,412 @@ + + + + + + HackerSoft Boot Logo Generator + + + +
+

🎨 HackerSoft Boot Logo Generator

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + + +
+ +
+ +
+ +
+ + + + +
+ +
+

📋 Verwendung:

+
    +
  • 1. Text und Farben anpassen
  • +
  • 2. "Logo Generieren" klicken
  • +
  • 3. Alle 3 Varianten downloaden
  • +
  • 4. Dateien nach rdp-thin-client/files/branding/ kopieren
  • +
  • 5. Ansible-Playbook ausführen
  • +
+
+ 💡 Tipp: Die "Presets" geben dir verschiedene Farb-Styles! +
+
+ + + + diff --git a/files/branding/desktop-background.png b/files/branding/desktop-background.png new file mode 100644 index 0000000..1582fcf Binary files /dev/null and b/files/branding/desktop-background.png differ diff --git a/files/branding/grub-background.png b/files/branding/grub-background.png new file mode 100644 index 0000000..9ff599f Binary files /dev/null and b/files/branding/grub-background.png differ diff --git a/files/branding/login-background.png b/files/branding/login-background.png new file mode 100644 index 0000000..9ff599f Binary files /dev/null and b/files/branding/login-background.png differ diff --git a/files/rdp-launcher.sh b/files/rdp-launcher.sh new file mode 100644 index 0000000..c025f53 --- /dev/null +++ b/files/rdp-launcher.sh @@ -0,0 +1,238 @@ +#!/bin/bash +# RDP Launcher - Startet RDP-Verbindungen aus Profilen +# Speichere als: files/rdp-launcher.sh + +CONFIG_FILE="$HOME/.config/rdp-profiles/profiles.ini" +PROFILE_NAME="$1" + +# Funktion zum sicheren Starten des Profile Managers (ohne Doppelstart) +start_profile_manager() { + # Prüfe ob Manager bereits läuft + if pgrep -f "rdp-profile-manager.py" > /dev/null; then + echo "Profile Manager is already running" + return + fi + + /usr/local/bin/rdp-profile-manager.py & +} + +# Finde FreeRDP binary (Debian 12 = xfreerdp, Debian 13 = xfreerdp3) +# Prüfe ZUERST auf xfreerdp3, da es auch einen xfreerdp-Symlink geben könnte +if command -v xfreerdp3 &> /dev/null; then + FREERDP_CMD="xfreerdp3" + FREERDP_VERSION=3 +elif command -v xfreerdp &> /dev/null; then + FREERDP_CMD="xfreerdp" + FREERDP_VERSION=2 +else + zenity --error --text="FreeRDP nicht gefunden! Bitte installieren." 2>/dev/null + echo "ERROR: FreeRDP not found" + exit 1 +fi + +echo "Using $FREERDP_CMD (Version $FREERDP_VERSION)" + +# Funktion zum Auslesen von INI-Werten +get_ini_value() { + local section="$1" + local key="$2" + local default="$3" + + value=$(awk -F ' *= *' -v section="$section" -v key="$key" ' + /^\[.*\]$/ { + gsub(/^\[|\]$/, "", $0) + current_section = $0 + next + } + current_section == section && $1 == key { + # Remove leading/trailing whitespace from value + gsub(/^[ \t]+|[ \t]+$/, "", $2) + print $2 + exit + } + ' "$CONFIG_FILE") + + echo "${value:-$default}" +} + +# Wenn kein Profil angegeben, starte Profile Manager +if [ -z "$PROFILE_NAME" ]; then + # Check if profiles exist + if [ ! -s "$CONFIG_FILE" ] || ! grep -q '^\[' "$CONFIG_FILE"; then + # No profiles, start manager + start_profile_manager + exit 0 + fi + + # Suche nach Autostart-Profil + PROFILE_NAME=$(awk -F'[][]' ' + /^\[/ { section=$2 } + /^autostart[[:space:]]*=[[:space:]]*[Tt]rue/ && section { print section; exit } + ' "$CONFIG_FILE") + + # Wenn kein Autostart-Profil gefunden, starte Manager + if [ -z "$PROFILE_NAME" ]; then + echo "No autostart profile found, starting Profile Manager..." + start_profile_manager + exit 0 + fi + + echo "Starting autostart profile: $PROFILE_NAME" +fi + +# Funktion zum sicheren Starten des Profile Managers (ohne Doppelstart) +start_profile_manager() { + # Prüfe ob Manager bereits läuft + if pgrep -f "rdp-profile-manager.py" > /dev/null; then + echo "Profile Manager is already running" + return + fi + + /usr/local/bin/rdp-profile-manager.py & +} + +# Lese Profil-Konfiguration +SERVER=$(get_ini_value "$PROFILE_NAME" "server") +USERNAME=$(get_ini_value "$PROFILE_NAME" "username") +DOMAIN=$(get_ini_value "$PROFILE_NAME" "domain") +RESOLUTION=$(get_ini_value "$PROFILE_NAME" "resolution" "client") +MULTIMON=$(get_ini_value "$PROFILE_NAME" "multimon" "False") +REDIRECT_AUDIO=$(get_ini_value "$PROFILE_NAME" "redirect_audio" "True") +REDIRECT_MIC=$(get_ini_value "$PROFILE_NAME" "redirect_microphone" "True") +REDIRECT_USB=$(get_ini_value "$PROFILE_NAME" "redirect_usb" "True") +REDIRECT_SMARTCARD=$(get_ini_value "$PROFILE_NAME" "redirect_smartcard" "True") +REDIRECT_PRINTERS=$(get_ini_value "$PROFILE_NAME" "redirect_printers" "False") + +if [ -z "$SERVER" ]; then + zenity --error --text="Profil '$PROFILE_NAME' nicht gefunden!" 2>/dev/null || \ + echo "ERROR: Profile '$PROFILE_NAME' not found!" + exit 1 +fi + +# Wenn kein Username hinterlegt, frage danach +if [ -z "$USERNAME" ]; then + USERNAME=$(zenity --entry --title="Benutzername" --text="Benutzername für $SERVER:" 2>/dev/null) + if [ -z "$USERNAME" ]; then + echo "Aborted: No username provided" + start_profile_manager + exit 0 + fi +fi + +# Baue FreeRDP-Befehl +CMD="$FREERDP_CMD" + +# Server und Benutzer +CMD="$CMD /v:$SERVER" +CMD="$CMD /u:$USERNAME" + +if [ -n "$DOMAIN" ]; then + CMD="$CMD /d:$DOMAIN" +fi + +# Auflösung +if [ "$RESOLUTION" = "client" ]; then + CMD="$CMD /dynamic-resolution" + # smart-sizing nur bei FreeRDP2 + if [ "$FREERDP_VERSION" -eq 2 ]; then + CMD="$CMD /smart-sizing" + fi +else + CMD="$CMD /size:$RESOLUTION" +fi + +# Multi-Monitor +if [ "$MULTIMON" = "True" ] || [ "$MULTIMON" = "true" ]; then + CMD="$CMD /multimon" + echo "Multi-Monitor mode enabled" +fi + +# Audio +if [ "$REDIRECT_AUDIO" = "True" ]; then + CMD="$CMD /audio-mode:0" + CMD="$CMD /sound:sys:pulse" +else + CMD="$CMD /audio-mode:2" +fi + +# Mikrofon +if [ "$REDIRECT_MIC" = "True" ]; then + CMD="$CMD /microphone:sys:pulse" +fi + +# USB-Geräte +if [ "$REDIRECT_USB" = "True" ]; then + if [ "$FREERDP_VERSION" -eq 3 ]; then + # FreeRDP3 - USB-Geräte (Dongles, Smart Cards) + CMD="$CMD /usb:auto" + + # USB-Sticks als Laufwerk durchreichen - FESTES Verzeichnis für Hotswap! + echo "Redirecting USB media directory for hotswap support..." + MEDIA_DIR="/media/rdpuser" + + # Erstelle Verzeichnis falls nicht vorhanden + mkdir -p "$MEDIA_DIR" + + # Reiche das komplette media-Verzeichnis durch + CMD="$CMD /drive:hotplug,*" + echo "USB Hotswap enabled: $MEDIA_DIR" + else + # FreeRDP2 + CMD="$CMD /usb:id,dev:*" + MEDIA_DIR="/media/rdpuser" + mkdir -p "$MEDIA_DIR" + CMD="$CMD /drive:USB-Media,$MEDIA_DIR" + fi +fi + +# Smart Cards +if [ "$REDIRECT_SMARTCARD" = "True" ]; then + CMD="$CMD /smartcard" +fi + +# Drucker +if [ "$REDIRECT_PRINTERS" = "True" ]; then + CMD="$CMD /printer" +fi + +# Zusätzliche Optionen +CMD="$CMD /cert:ignore" +CMD="$CMD /compression" +CMD="$CMD /clipboard" +CMD="$CMD /gfx:AVC444" +CMD="$CMD /network:auto" + +# Zusätzliche Parameter nur für FreeRDP2 +if [ "$FREERDP_VERSION" -eq 2 ]; then + CMD="$CMD +fonts" + CMD="$CMD +aero" + CMD="$CMD +glyph-cache" +fi + +# Vollbild +CMD="$CMD /f" + +# Log +echo "Connecting to: $SERVER as $USERNAME" +echo "Command: $CMD" + +# Frage nach Passwort +PASSWORD=$(zenity --password --title="RDP Verbindung zu $SERVER" 2>/dev/null) + +if [ -z "$PASSWORD" ]; then + echo "Aborted by user" + start_profile_manager + exit 0 +fi + +# Starte RDP-Verbindung +# FreeRDP3 hat Probleme mit /from-stdin, nutze /p: stattdessen +if [ "$FREERDP_VERSION" -eq 3 ]; then + $CMD /p:"$PASSWORD" +else + # FreeRDP2 mit stdin + echo "$PASSWORD" | $CMD /from-stdin +fi + +# Nach Beenden der Verbindung - zeige Profile Manager wieder +/usr/local/bin/rdp-profile-manager.py & diff --git a/files/rdp-profile-manager.py b/files/rdp-profile-manager.py new file mode 100755 index 0000000..31f61e7 --- /dev/null +++ b/files/rdp-profile-manager.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +""" +HackerSoft RDP Profile Manager - GUI für RDP Thin Client +Speichere diese Datei als: files/rdp-profile-manager.py +Version: 1.1 +""" + +import tkinter as tk +from tkinter import ttk, messagebox +import configparser +import subprocess +from pathlib import Path +import os + +CONFIG_DIR = Path.home() / ".config" / "rdp-profiles" +CONFIG_FILE = CONFIG_DIR / "profiles.ini" +VERSION = "1.1" + +# Firmeninformationen +COMPANY_NAME = "Hacker-Net Telekommunikation" +COMPANY_ADDRESS = "Am Wunderburgpark 5b\n26135 Oldenburg" +COMPANY_PHONE = "Tel.: +49 441 35065316" +COMPANY_EMAIL = "E-Mail: info@hacker-net.de" + +# Prüfe ob Emoji-Fonts verfügbar sind +def check_emoji_support(): + """Prüft ob Emoji-Fonts installiert sind""" + emoji_fonts = [ + '/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf', + '/usr/share/fonts/opentype/noto/NotoColorEmoji.ttf', + '/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf' + ] + return any(Path(font).exists() for font in emoji_fonts) + +# Emoji-Support erkennen +USE_EMOJIS = check_emoji_support() + +# Button-Texte (mit oder ohne Emojis) +if USE_EMOJIS: + BTN_NEW = "➕ Neues Profil" + BTN_EDIT = "✏️ Bearbeiten" + BTN_DELETE = "🗑️ Löschen" + BTN_CONNECT = "🔌 Verbinden" + BTN_BLUETOOTH = "🎧 Bluetooth" + BTN_INFO = "ℹ️ Info" + BTN_AUTOSTART = "⭐ Autostart" + BTN_SAVE = "💾 Speichern" + BTN_CANCEL = "❌ Abbrechen" + LBL_AUDIO = "🔊 Audio umleiten" + LBL_MIC = "🎤 Mikrofon umleiten" + LBL_USB = "💾 USB-Geräte umleiten" + LBL_SMARTCARD = "💳 Smart Cards umleiten" + LBL_PRINTER = "🖨️ Drucker umleiten" +else: + BTN_NEW = "+ Neues Profil" + BTN_EDIT = "Bearbeiten" + BTN_DELETE = "Löschen" + BTN_CONNECT = "Verbinden" + BTN_BLUETOOTH = "Bluetooth" + BTN_INFO = "Info" + BTN_AUTOSTART = "* Autostart" + BTN_SAVE = "Speichern" + BTN_CANCEL = "Abbrechen" + LBL_AUDIO = "Audio umleiten" + LBL_MIC = "Mikrofon umleiten" + LBL_USB = "USB-Geräte umleiten" + LBL_SMARTCARD = "Smart Cards umleiten" + LBL_PRINTER = "Drucker umleiten" + +class RDPProfileManager: + def __init__(self, root): + self.root = root + self.root.title("HackerSoft - RDP Profile Manager") + self.root.geometry("900x600") + + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + if not CONFIG_FILE.exists(): + CONFIG_FILE.touch() + + self.config = configparser.ConfigParser() + self.load_profiles() + self.create_widgets() + self.refresh_profile_list() + + def load_profiles(self): + self.config.read(CONFIG_FILE) + + def save_profiles(self): + with open(CONFIG_FILE, 'w') as f: + self.config.write(f) + + def create_widgets(self): + # Top Frame - Buttons + top_frame = ttk.Frame(self.root, padding="10") + top_frame.pack(fill=tk.X) + + ttk.Button(top_frame, text=BTN_NEW, command=self.new_profile).pack(side=tk.LEFT, padx=5) + ttk.Button(top_frame, text=BTN_EDIT, command=self.edit_profile).pack(side=tk.LEFT, padx=5) + ttk.Button(top_frame, text=BTN_DELETE, command=self.delete_profile).pack(side=tk.LEFT, padx=5) + ttk.Button(top_frame, text=BTN_CONNECT, command=self.connect_profile).pack(side=tk.LEFT, padx=5) + ttk.Button(top_frame, text=BTN_AUTOSTART, command=self.toggle_autostart).pack(side=tk.LEFT, padx=5) + + # Rechte Seite + ttk.Button(top_frame, text=BTN_INFO, command=self.show_info).pack(side=tk.RIGHT, padx=5) + ttk.Button(top_frame, text=BTN_BLUETOOTH, command=self.open_bluetooth).pack(side=tk.RIGHT, padx=5) + + # Profile List + list_frame = ttk.Frame(self.root, padding="10") + list_frame.pack(fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + columns = ('Autostart', 'Server', 'Benutzer', 'Auflösung', 'Audio', 'USB') + self.tree = ttk.Treeview(list_frame, columns=columns, show='tree headings', yscrollcommand=scrollbar.set) + scrollbar.config(command=self.tree.yview) + + self.tree.heading('#0', text='Profil') + self.tree.heading('Autostart', text='Auto') + self.tree.heading('Server', text='Server') + self.tree.heading('Benutzer', text='Benutzer') + self.tree.heading('Auflösung', text='Auflösung') + self.tree.heading('Audio', text='Audio') + self.tree.heading('USB', text='USB') + + self.tree.column('#0', width=150) + self.tree.column('Autostart', width=50) + self.tree.column('Server', width=180) + self.tree.column('Benutzer', width=120) + self.tree.column('Auflösung', width=120) + self.tree.column('Audio', width=80) + self.tree.column('USB', width=80) + + self.tree.pack(fill=tk.BOTH, expand=True) + self.tree.bind('', lambda e: self.connect_profile()) + + def show_info(self): + """Zeigt Info-Dialog mit Firmendaten""" + info_window = tk.Toplevel(self.root) + info_window.title("Info") + info_window.geometry("450x350") + info_window.transient(self.root) + info_window.grab_set() + + # Logo/Header + header_frame = ttk.Frame(info_window, padding="20") + header_frame.pack(fill=tk.X) + + ttk.Label(header_frame, text="HackerSoft", font=('Arial', 24, 'bold')).pack() + ttk.Label(header_frame, text="RDP Profile Manager", font=('Arial', 12)).pack() + ttk.Label(header_frame, text=f"Version {VERSION}", font=('Arial', 10), foreground='gray').pack() + + # Separator + ttk.Separator(info_window, orient=tk.HORIZONTAL).pack(fill=tk.X, padx=20, pady=10) + + # Firmeninfo + info_frame = ttk.Frame(info_window, padding="20") + info_frame.pack(fill=tk.BOTH, expand=True) + + ttk.Label(info_frame, text=COMPANY_NAME, font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0,5)) + ttk.Label(info_frame, text=COMPANY_ADDRESS, font=('Arial', 10)).pack(anchor=tk.W, pady=2) + ttk.Label(info_frame, text=COMPANY_PHONE, font=('Arial', 10)).pack(anchor=tk.W, pady=2) + ttk.Label(info_frame, text=COMPANY_EMAIL, font=('Arial', 10), foreground='blue').pack(anchor=tk.W, pady=2) + + # Button + button_frame = ttk.Frame(info_window, padding="10") + button_frame.pack(fill=tk.X, side=tk.BOTTOM) + ttk.Button(button_frame, text="OK", command=info_window.destroy).pack() + + def toggle_autostart(self): + """Setzt/entfernt Autostart für ausgewähltes Profil""" + selected = self.tree.selection() + if not selected: + messagebox.showwarning("Warnung", "Bitte wählen Sie ein Profil aus!") + return + + profile_name = self.tree.item(selected[0])['text'] + profile = self.config[profile_name] + + # Aktueller Autostart-Status + current_autostart = profile.getboolean('autostart', False) + + if current_autostart: + # Deaktivieren + self.config[profile_name]['autostart'] = 'False' + messagebox.showinfo("Autostart", f"Autostart für '{profile_name}' wurde deaktiviert!") + else: + # Aktivieren - alle anderen deaktivieren + for section in self.config.sections(): + self.config[section]['autostart'] = 'False' + self.config[profile_name]['autostart'] = 'True' + messagebox.showinfo("Autostart", f"'{profile_name}' wird jetzt automatisch gestartet!") + + self.save_profiles() + self.refresh_profile_list() + + def refresh_profile_list(self): + self.tree.delete(*self.tree.get_children()) + for profile_name in self.config.sections(): + profile = self.config[profile_name] + autostart_icon = '⭐' if profile.getboolean('autostart', False) else '' + username_display = profile.get('username', '-') or '-' + self.tree.insert('', tk.END, text=profile_name, values=( + autostart_icon, + profile.get('server', ''), + username_display, + profile.get('resolution', 'Client'), + '✓' if profile.getboolean('redirect_audio', True) else '✗', + '✓' if profile.getboolean('redirect_usb', True) else '✗' + )) + + def new_profile(self): + dialog = ProfileDialog(self.root, "Neues Profil") + self.root.wait_window(dialog.top) + + if dialog.result: + profile_name = dialog.result['name'] + if profile_name in self.config: + messagebox.showerror("Fehler", f"Profil '{profile_name}' existiert bereits!") + return + + self.config[profile_name] = { + 'server': dialog.result['server'], + 'username': dialog.result['username'], + 'domain': dialog.result['domain'], + 'resolution': dialog.result['resolution'], + 'multimon': str(dialog.result['multimon']), + 'redirect_audio': str(dialog.result['redirect_audio']), + 'redirect_microphone': str(dialog.result['redirect_microphone']), + 'redirect_usb': str(dialog.result['redirect_usb']), + 'redirect_smartcard': str(dialog.result['redirect_smartcard']), + 'redirect_printers': str(dialog.result['redirect_printers']), + 'exit_hotkey': dialog.result['exit_hotkey'], + 'autostart': 'False' + } + + self.save_profiles() + self.refresh_profile_list() + messagebox.showinfo("Erfolg", f"Profil '{profile_name}' wurde erstellt!") + + def edit_profile(self): + selected = self.tree.selection() + if not selected: + messagebox.showwarning("Warnung", "Bitte wählen Sie ein Profil aus!") + return + + profile_name = self.tree.item(selected[0])['text'] + profile = self.config[profile_name] + + dialog = ProfileDialog(self.root, f"Profil bearbeiten: {profile_name}", profile_name, profile) + self.root.wait_window(dialog.top) + + if dialog.result: + new_name = dialog.result['name'] + if new_name != profile_name: + self.config.remove_section(profile_name) + + self.config[new_name] = { + 'server': dialog.result['server'], + 'username': dialog.result['username'], + 'domain': dialog.result['domain'], + 'resolution': dialog.result['resolution'], + 'multimon': str(dialog.result['multimon']), + 'redirect_audio': str(dialog.result['redirect_audio']), + 'redirect_microphone': str(dialog.result['redirect_microphone']), + 'redirect_usb': str(dialog.result['redirect_usb']), + 'redirect_smartcard': str(dialog.result['redirect_smartcard']), + 'redirect_printers': str(dialog.result['redirect_printers']), + 'exit_hotkey': dialog.result['exit_hotkey'], + 'autostart': profile.get('autostart', 'False') + } + + self.save_profiles() + self.refresh_profile_list() + messagebox.showinfo("Erfolg", f"Profil '{new_name}' wurde aktualisiert!") + + def delete_profile(self): + selected = self.tree.selection() + if not selected: + messagebox.showwarning("Warnung", "Bitte wählen Sie ein Profil aus!") + return + + profile_name = self.tree.item(selected[0])['text'] + + if messagebox.askyesno("Bestätigung", f"Profil '{profile_name}' wirklich löschen?"): + self.config.remove_section(profile_name) + self.save_profiles() + self.refresh_profile_list() + messagebox.showinfo("Erfolg", f"Profil '{profile_name}' wurde gelöscht!") + + def connect_profile(self): + selected = self.tree.selection() + if not selected: + messagebox.showwarning("Warnung", "Bitte wählen Sie ein Profil aus!") + return + + profile_name = self.tree.item(selected[0])['text'] + + try: + subprocess.Popen(['/usr/local/bin/rdp-launcher.sh', profile_name]) + self.root.withdraw() + except Exception as e: + messagebox.showerror("Fehler", f"Verbindung fehlgeschlagen: {str(e)}") + + def open_bluetooth(self): + try: + subprocess.Popen(['blueman-manager']) + except Exception as e: + messagebox.showerror("Fehler", f"Bluetooth Manager konnte nicht gestartet werden: {str(e)}") + + +class ProfileDialog: + def __init__(self, parent, title, profile_name=None, profile_data=None): + self.result = None + + self.top = tk.Toplevel(parent) + self.top.title(title) + self.top.geometry("550x700") + self.top.transient(parent) + self.top.grab_set() + + main_frame = ttk.Frame(self.top, padding="20") + main_frame.pack(fill=tk.BOTH, expand=True) + + row = 0 + + # Profile Name + ttk.Label(main_frame, text="Profilname:", font=('', 10, 'bold')).grid(row=row, column=0, sticky=tk.W, pady=5) + self.name_var = tk.StringVar(value=profile_name or "") + ttk.Entry(main_frame, textvariable=self.name_var, width=40).grid(row=row, column=1, pady=5) + row += 1 + + ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=2, sticky='ew', pady=10) + row += 1 + + # Server + ttk.Label(main_frame, text="Server:").grid(row=row, column=0, sticky=tk.W, pady=5) + self.server_var = tk.StringVar(value=profile_data.get('server', '') if profile_data else '') + ttk.Entry(main_frame, textvariable=self.server_var, width=40).grid(row=row, column=1, pady=5) + row += 1 + + # Username + ttk.Label(main_frame, text="Benutzername (optional):").grid(row=row, column=0, sticky=tk.W, pady=5) + self.username_var = tk.StringVar(value=profile_data.get('username', '') if profile_data else '') + ttk.Entry(main_frame, textvariable=self.username_var, width=40).grid(row=row, column=1, pady=5) + row += 1 + + # Domain + ttk.Label(main_frame, text="Domäne (optional):").grid(row=row, column=0, sticky=tk.W, pady=5) + self.domain_var = tk.StringVar(value=profile_data.get('domain', '') if profile_data else '') + ttk.Entry(main_frame, textvariable=self.domain_var, width=40).grid(row=row, column=1, pady=5) + row += 1 + + # Resolution + ttk.Label(main_frame, text="Auflösung:").grid(row=row, column=0, sticky=tk.W, pady=5) + self.resolution_var = tk.StringVar(value=profile_data.get('resolution', 'client') if profile_data else 'client') + resolution_combo = ttk.Combobox(main_frame, textvariable=self.resolution_var, width=37, state='readonly') + resolution_combo['values'] = ('client', '1920x1080', '1680x1050', '1600x900', '1440x900', '1366x768', '1280x1024', '1024x768') + resolution_combo.grid(row=row, column=1, pady=5) + row += 1 + + # Multi-Monitor + self.multimon_var = tk.BooleanVar(value=profile_data.getboolean('multimon', False) if profile_data else False) + ttk.Checkbutton(main_frame, text="Alle Monitore nutzen (Multi-Monitor)", variable=self.multimon_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=5) + row += 1 + + ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=2, sticky='ew', pady=15) + row += 1 + + ttk.Label(main_frame, text="Umleitungen:", font=('', 10, 'bold')).grid(row=row, column=0, sticky=tk.W, pady=5) + row += 1 + + # Redirects + self.audio_var = tk.BooleanVar(value=profile_data.getboolean('redirect_audio', True) if profile_data else True) + ttk.Checkbutton(main_frame, text=LBL_AUDIO, variable=self.audio_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=3) + row += 1 + + self.microphone_var = tk.BooleanVar(value=profile_data.getboolean('redirect_microphone', True) if profile_data else True) + ttk.Checkbutton(main_frame, text=LBL_MIC, variable=self.microphone_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=3) + row += 1 + + self.usb_var = tk.BooleanVar(value=profile_data.getboolean('redirect_usb', True) if profile_data else True) + ttk.Checkbutton(main_frame, text=LBL_USB, variable=self.usb_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=3) + row += 1 + + self.smartcard_var = tk.BooleanVar(value=profile_data.getboolean('redirect_smartcard', True) if profile_data else True) + ttk.Checkbutton(main_frame, text=LBL_SMARTCARD, variable=self.smartcard_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=3) + row += 1 + + self.printers_var = tk.BooleanVar(value=profile_data.getboolean('redirect_printers', False) if profile_data else False) + ttk.Checkbutton(main_frame, text=LBL_PRINTER, variable=self.printers_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=3) + row += 1 + + ttk.Separator(main_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=2, sticky='ew', pady=15) + row += 1 + + # Exit Hotkey + ttk.Label(main_frame, text="Tastenkombination:").grid(row=row, column=0, sticky=tk.W, pady=5) + self.hotkey_var = tk.StringVar(value=profile_data.get('exit_hotkey', 'Control+Alt+q') if profile_data else 'Control+Alt+q') + ttk.Entry(main_frame, textvariable=self.hotkey_var, width=40).grid(row=row, column=1, pady=5) + row += 1 + + ttk.Label(main_frame, text="Format: Control+Alt+q", font=('', 8), foreground='gray').grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=2) + row += 1 + + # Buttons + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=row, column=0, columnspan=2, pady=20) + + ttk.Button(button_frame, text=BTN_SAVE, command=self.ok).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text=BTN_CANCEL, command=self.cancel).pack(side=tk.LEFT, padx=5) + + def ok(self): + name = self.name_var.get().strip() + server = self.server_var.get().strip() + username = self.username_var.get().strip() + + if not name or not server: + messagebox.showerror("Fehler", "Bitte füllen Sie mindestens Profilname und Server aus!") + return + + self.result = { + 'name': name, + 'server': server, + 'username': username, + 'domain': self.domain_var.get().strip(), + 'resolution': self.resolution_var.get(), + 'multimon': self.multimon_var.get(), + 'redirect_audio': self.audio_var.get(), + 'redirect_microphone': self.microphone_var.get(), + 'redirect_usb': self.usb_var.get(), + 'redirect_smartcard': self.smartcard_var.get(), + 'redirect_printers': self.printers_var.get(), + 'exit_hotkey': self.hotkey_var.get().strip() + } + + self.top.destroy() + + def cancel(self): + self.top.destroy() + + +if __name__ == '__main__': + root = tk.Tk() + app = RDPProfileManager(root) + root.mainloop() diff --git a/files/session-watcher.py b/files/session-watcher.py new file mode 100755 index 0000000..bd9d520 --- /dev/null +++ b/files/session-watcher.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Session Watcher - Überwacht Tastenkombination zum Verlassen der RDP-Sitzung +Speichere als: files/session-watcher.py +""" + +import subprocess +import sys +import time +import configparser +from pathlib import Path +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.protocol import rq + +CONFIG_FILE = Path.home() / ".config" / "rdp-profiles" / "profiles.ini" + +class SessionWatcher: + def __init__(self): + self.display = display.Display() + self.root = self.display.screen().root + self.ctx = None + self.pressed_keys = set() + + # Default hotkey + self.exit_hotkey = "Control+Alt+q" + + # Parse hotkey from active profile (if any) + self.load_hotkey() + + def load_hotkey(self): + """Lädt Hotkey aus dem ersten aktiven Profil""" + if not CONFIG_FILE.exists(): + return + + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + + # Nutze ersten Profil-Hotkey als Standard + for section in config.sections(): + hotkey = config[section].get('exit_hotkey', '').strip() + if hotkey: + self.exit_hotkey = hotkey + break + + print(f"Exit hotkey: {self.exit_hotkey}") + + def parse_hotkey(self): + """Wandelt Hotkey-String in Keycodes um""" + parts = self.exit_hotkey.lower().split('+') + + modifiers = [] + key = None + + for part in parts: + part = part.strip() + if part in ['control', 'ctrl']: + modifiers.append('Control_L') + modifiers.append('Control_R') + elif part in ['alt']: + modifiers.append('Alt_L') + modifiers.append('Alt_R') + elif part in ['shift']: + modifiers.append('Shift_L') + modifiers.append('Shift_R') + elif part in ['super', 'win', 'meta']: + modifiers.append('Super_L') + modifiers.append('Super_R') + else: + key = part + + return modifiers, key + + def check_rdp_running(self): + """Prüft ob FreeRDP läuft""" + try: + result = subprocess.run(['pgrep', '-x', 'xfreerdp'], + capture_output=True, text=True) + return result.returncode == 0 + except: + return False + + def kill_rdp(self): + """Beendet alle RDP-Verbindungen - AGGRESSIV!""" + print("Killing RDP sessions...") + try: + # Erst SIGTERM versuchen (sauber) + result = subprocess.run(['pkill', '-15', 'xfreerdp'], check=False) + + # Warte kurz + time.sleep(0.5) + + # Prüfe ob noch läuft + check = subprocess.run(['pgrep', '-x', 'xfreerdp'], + capture_output=True, check=False) + + if check.returncode == 0: + # Immer noch da? KILL IT WITH FIRE! (SIGKILL) + print("RDP process still running, using SIGKILL...") + subprocess.run(['pkill', '-9', 'xfreerdp'], check=False) + subprocess.run(['pkill', '-9', 'xfreerdp3'], check=False) + time.sleep(0.3) + + # Auch xfreerdp3 killen (falls vorhanden) + subprocess.run(['pkill', '-9', 'xfreerdp3'], check=False) + + time.sleep(0.5) + + # Starte Profile Manager + subprocess.Popen(['/usr/local/bin/rdp-profile-manager.py']) + except Exception as e: + print(f"Error killing RDP: {e}") + + def event_handler(self, reply): + """Behandelt Tastatur-Events""" + if reply.category != record.FromServer: + return + if reply.client_swapped: + return + if not len(reply.data) or reply.data[0] < 2: + return + + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value( + data, self.display.display, None, None) + + if event.type == X.KeyPress: + keysym = self.display.keycode_to_keysym(event.detail, 0) + key_name = XK.keysym_to_string(keysym) + + if key_name: + self.pressed_keys.add(key_name) + self.check_hotkey() + + elif event.type == X.KeyRelease: + keysym = self.display.keycode_to_keysym(event.detail, 0) + key_name = XK.keysym_to_string(keysym) + + if key_name and key_name in self.pressed_keys: + self.pressed_keys.discard(key_name) + + def check_hotkey(self): + """Prüft ob Exit-Hotkey gedrückt wurde""" + modifiers, key = self.parse_hotkey() + + # Check if any modifier variant is pressed + modifier_pressed = False + for mod in modifiers: + if mod in self.pressed_keys: + modifier_pressed = True + break + + if not modifier_pressed: + return + + # Check key + if key and key.lower() in [k.lower() for k in self.pressed_keys]: + # Hotkey matched! + if self.check_rdp_running(): + print("Exit hotkey detected - killing RDP session") + self.pressed_keys.clear() # Prevent multiple triggers + self.kill_rdp() + + def run(self): + """Startet Event-Loop""" + # Setup record extension + self.ctx = self.display.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': (X.KeyPress, X.KeyRelease), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }] + ) + + self.display.record_enable_context(self.ctx, self.event_handler) + self.display.record_free_context(self.ctx) + + print("Session watcher started - monitoring for exit hotkey") + + while True: + event = self.display.next_event() + +if __name__ == '__main__': + try: + watcher = SessionWatcher() + watcher.run() + except KeyboardInterrupt: + print("\nSession watcher stopped") + sys.exit(0) + except Exception as e: + print(f"Error: {e}") + sys.exit(1)