first commit

This commit is contained in:
duffyduck 2025-12-22 14:50:34 +01:00
commit 8620ff31ac
16 changed files with 2392 additions and 0 deletions

349
README.md Normal file
View File

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

Binary file not shown.

View File

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

8
ansible/inventory.ini Normal file
View File

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

View File

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

649
ansible/playbook.yml Normal file
View File

@ -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: |
<?xml version="1.0" encoding="UTF-8"?>
<openbox_menu xmlns="http://openbox.org/3.4/menu">
<menu id="root-menu" label="Openbox">
<item label="RDP Profile Manager">
<action name="Execute">
<command>/usr/local/bin/rdp-profile-manager.py</command>
</action>
</item>
<item label="Bluetooth Manager">
<action name="Execute">
<command>blueman-manager</command>
</action>
</item>
<item label="Datei Manager">
<action name="Execute">
<command>pcmanfm</command>
</action>
</item>
<item label="Sound Einstellungen">
<action name="Execute">
<command>pavucontrol</command>
</action>
</item>
<menu id="display-menu" label="Anzeigeeinstellungen">
<item label="Anzeige konfigurieren">
<action name="Execute">
<command>arandr</command>
</action>
</item>
<item label="Aktuelles Profil speichern">
<action name="Execute">
<command>sh -c 'profile=$(zenity --entry --title="Display Profil speichern" --text="Profilnamen eingeben:"); [ -n "$profile" ] &amp;&amp; autorandr --save "$profile" --force &amp;&amp; echo "$profile" &gt; ~/.config/autorandr/last_profile &amp;&amp; zenitiy --info --text="Profil gespeichert: $profile"'</command>
</action>
</item>
<item label="Profil laden">
<action name="Execute">
<command>sh -c 'profile=$(zenity --list --title="Display Profil laden" --column="Profile" $(autorandr --list));[ -n "$profile" ] &amp;&amp; autorandr --load "$profile" &amp;&amp; echo "$profile" &gt; ~/.config/autorandr/last_profile &amp;&amp; zenity --info --text="Dieses Profil wird automatisch beim nächsten Start geladen"'</command>
</action>
</item>
<item label="Delete Profile">
<action name="Execute">
<command>sh -c 'profile=$(zenity --list --title="Display Profil löschen" --text="Profile auswählen zum löschen:" --column="Profiles" $(autorandr --list));[ -n "$profile" ] &amp;&amp; zenity --question --title="Löschen bestätigen" --text="Delete profile: $profile?" &amp;&amp; autorandr --remove "$profile" &amp;&amp; zenity --info --text="Profil gelöscht: $profile"'</command></action></item>
<menu id="gpu-menu" label="Grafiktreiber"><item label="Install Intel Graphics Driver">
<action name="Execute">
<command>lxterminal -e "bash -c 'echo Installing Intel graphics driver...; sudo apt update &amp;&amp; sudo apt install -y xserver-xorg-video-intel firmware-misc-nonfree &amp;&amp; echo Done! Please reboot for changes to take effect.; read -p Press Enter to close...'"</command>
</action>
</item>
<item label="Install AMD/Radeon Driver">
<action name="Execute">
<command>lxterminal -e "bash -c 'echo Installing AMD/Radeon graphics driver...; sudo apt update &amp;&amp; sudo apt install -y firmware-amd-graphics xserver-xorg-video-amdgpu xserver-xorg-video-radeon &amp;&amp; echo Done! Please reboot for changes to take effect.; read -p Press Enter to close...'"</command>
</action>
</item>
<item label="Install NVIDIA Driver">
<action name="Execute">
<command>lxterminal -e "bash -c 'echo Installing NVIDIA proprietary driver...; sudo apt update &amp;&amp; sudo apt install -y nvidia-driver firmware-misc-nonfree &amp;&amp; echo Done! Please reboot for changes to take effect.; read -p Press Enter to close...'"</command>
</action>
</item>
<separator />
<item label="Show Current GPU Info">
<action name="Execute">
<command>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...'"</command>
</action>
</item>
</menu>
</menu><item label="Terminal">
<action name="Execute">
<command>lxterminal</command>
</action>
</item>
<separator />
<item label="Neustart">
<action name="Execute">
<command>systemctl reboot</command>
</action>
</item>
<item label="Ausschalten">
<action name="Execute">
<command>systemctl poweroff</command>
</action>
</item>
</menu>
</openbox_menu>
# === 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

20
check-rdp-health.sh Executable file
View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,412 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HackerSoft Boot Logo Generator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5em;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
}
.control-group {
display: flex;
flex-direction: column;
}
label {
font-weight: 600;
margin-bottom: 8px;
color: #555;
}
input, select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.canvas-container {
text-align: center;
margin: 30px 0;
padding: 20px;
background: #000;
border-radius: 10px;
}
canvas {
max-width: 100%;
border: 3px solid #667eea;
border-radius: 5px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.button-group {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
button {
padding: 15px 30px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #28a745;
color: white;
}
.btn-secondary:hover {
background: #218838;
transform: translateY(-2px);
}
.info-box {
background: #e3f2fd;
padding: 20px;
border-radius: 10px;
margin-top: 20px;
border-left: 5px solid #2196f3;
}
.info-box h3 {
color: #1976d2;
margin-bottom: 10px;
}
.info-box ul {
margin-left: 20px;
color: #555;
}
.info-box li {
margin: 5px 0;
}
.presets {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 10px;
}
.preset-btn {
padding: 8px 15px;
background: #f0f0f0;
border: 2px solid #ddd;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.preset-btn:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<h1>🎨 HackerSoft Boot Logo Generator</h1>
<div class="controls">
<div class="control-group">
<label for="mainText">Haupttext:</label>
<input type="text" id="mainText" value="HackerSoft">
</div>
<div class="control-group">
<label for="subText">Untertitel:</label>
<input type="text" id="subText" value="RDP Thin Client">
</div>
<div class="control-group">
<label for="companyText">Firma (unten):</label>
<input type="text" id="companyText" value="Hacker-Net Telekommunikation">
</div>
<div class="control-group">
<label for="mainColor">Hauptfarbe:</label>
<input type="color" id="mainColor" value="#00ff88">
</div>
<div class="control-group">
<label for="accentColor">Akzentfarbe:</label>
<input type="color" id="accentColor" value="#667eea">
</div>
<div class="control-group">
<label for="bgStyle">Hintergrund:</label>
<select id="bgStyle">
<option value="gradient">Gradient (Dunkel)</option>
<option value="solid-dark">Einfarbig Dunkel</option>
<option value="solid-black">Schwarz</option>
<option value="matrix">Matrix-Style</option>
</select>
</div>
</div>
<div class="presets">
<button class="preset-btn" onclick="loadPreset('hacker')">🟢 Hacker Green</button>
<button class="preset-btn" onclick="loadPreset('corporate')">🔵 Corporate Blue</button>
<button class="preset-btn" onclick="loadPreset('fire')">🔴 Fire Red</button>
<button class="preset-btn" onclick="loadPreset('modern')">⚪ Modern Minimal</button>
</div>
<div class="canvas-container">
<canvas id="logoCanvas" width="1920" height="1080"></canvas>
</div>
<div class="button-group">
<button class="btn-primary" onclick="generateLogo()">🎨 Logo Generieren</button>
<button class="btn-secondary" onclick="downloadLogo('boot-logo')">💾 Boot-Logo Download</button>
<button class="btn-secondary" onclick="downloadLogo('grub-background')">💾 GRUB Background Download</button>
<button class="btn-secondary" onclick="downloadLogo('login-background')">💾 Login Background Download</button>
</div>
<div class="info-box">
<h3>📋 Verwendung:</h3>
<ul>
<li><strong>1.</strong> Text und Farben anpassen</li>
<li><strong>2.</strong> "Logo Generieren" klicken</li>
<li><strong>3.</strong> Alle 3 Varianten downloaden</li>
<li><strong>4.</strong> Dateien nach <code>rdp-thin-client/files/branding/</code> kopieren</li>
<li><strong>5.</strong> Ansible-Playbook ausführen</li>
</ul>
<br>
<strong>💡 Tipp:</strong> Die "Presets" geben dir verschiedene Farb-Styles!
</div>
</div>
<script>
const canvas = document.getElementById('logoCanvas');
const ctx = canvas.getContext('2d');
// Presets
const presets = {
hacker: {
mainColor: '#00ff88',
accentColor: '#00cc66',
bgStyle: 'matrix'
},
corporate: {
mainColor: '#4a90e2',
accentColor: '#667eea',
bgStyle: 'gradient'
},
fire: {
mainColor: '#ff4444',
accentColor: '#ff8800',
bgStyle: 'gradient'
},
modern: {
mainColor: '#ffffff',
accentColor: '#888888',
bgStyle: 'solid-black'
}
};
function loadPreset(preset) {
const p = presets[preset];
document.getElementById('mainColor').value = p.mainColor;
document.getElementById('accentColor').value = p.accentColor;
document.getElementById('bgStyle').value = p.bgStyle;
generateLogo();
}
function generateLogo() {
const mainText = document.getElementById('mainText').value;
const subText = document.getElementById('subText').value;
const companyText = document.getElementById('companyText').value;
const mainColor = document.getElementById('mainColor').value;
const accentColor = document.getElementById('accentColor').value;
const bgStyle = document.getElementById('bgStyle').value;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Background
drawBackground(bgStyle, mainColor, accentColor);
// Main Text
ctx.font = 'bold 180px Arial';
ctx.fillStyle = mainColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Text with glow effect
ctx.shadowColor = mainColor;
ctx.shadowBlur = 30;
ctx.fillText(mainText, canvas.width / 2, canvas.height / 2 - 80);
// Subtitle
ctx.shadowBlur = 20;
ctx.font = 'bold 60px Arial';
ctx.fillStyle = accentColor;
ctx.fillText(subText, canvas.width / 2, canvas.height / 2 + 50);
// Company text at bottom
ctx.shadowBlur = 10;
ctx.font = '40px Arial';
ctx.fillStyle = '#888888';
ctx.fillText(companyText, canvas.width / 2, canvas.height - 100);
// Version/Build info
ctx.font = '30px Arial';
ctx.fillStyle = '#666666';
ctx.fillText('v1.0 | Debian 12/13', canvas.width / 2, canvas.height - 50);
// Reset shadow
ctx.shadowBlur = 0;
// Decorative elements
drawDecorations(mainColor, accentColor);
}
function drawBackground(style, mainColor, accentColor) {
switch(style) {
case 'gradient':
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(0.5, '#16213e');
gradient.addColorStop(1, '#0f0f1e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
break;
case 'solid-dark':
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
break;
case 'solid-black':
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
break;
case 'matrix':
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Matrix effect
for (let i = 0; i < 50; i++) {
ctx.fillStyle = mainColor + '20';
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 30 + 10;
ctx.fillRect(x, y, 2, size);
}
break;
}
}
function drawDecorations(mainColor, accentColor) {
// Corner accents
ctx.strokeStyle = mainColor;
ctx.lineWidth = 5;
// Top-left
ctx.beginPath();
ctx.moveTo(50, 150);
ctx.lineTo(50, 50);
ctx.lineTo(150, 50);
ctx.stroke();
// Top-right
ctx.beginPath();
ctx.moveTo(canvas.width - 50, 150);
ctx.lineTo(canvas.width - 50, 50);
ctx.lineTo(canvas.width - 150, 50);
ctx.stroke();
// Bottom-left
ctx.beginPath();
ctx.moveTo(50, canvas.height - 150);
ctx.lineTo(50, canvas.height - 50);
ctx.lineTo(150, canvas.height - 50);
ctx.stroke();
// Bottom-right
ctx.beginPath();
ctx.moveTo(canvas.width - 50, canvas.height - 150);
ctx.lineTo(canvas.width - 50, canvas.height - 50);
ctx.lineTo(canvas.width - 150, canvas.height - 50);
ctx.stroke();
}
function downloadLogo(filename) {
const link = document.createElement('a');
link.download = filename + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
// Generate initial logo
window.onload = function() {
generateLogo();
};
// Auto-update on input change
document.querySelectorAll('input, select').forEach(el => {
el.addEventListener('input', generateLogo);
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

238
files/rdp-launcher.sh Normal file
View File

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

446
files/rdp-profile-manager.py Executable file
View File

@ -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('<Double-1>', 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()

201
files/session-watcher.py Executable file
View File

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