#!/usr/bin/env bash ######################################################################################### #### xfreerdp-connect.sh — RDP connection launcher with Zenity GUI prompt #### #### Connects to Windows hosts via xfreerdp with optional RemoteApp support. #### #### Requires: bash, xfreerdp, zenity #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.2 #### #### #### #### Usage: #### #### ./xfreerdp-connect.sh #### #### ./xfreerdp-connect.sh --help #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Colors ──────────────────────────────────────────────────────────── if [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' RESET='\033[0m' else RED="" GREEN="" RESET="" fi log() { echo -e "${GREEN}[OK]${RESET} $*"; } err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } # ── Profile support ────────────────────────────────────────────────── PROFILE_DIR="$HOME/.config/xfreerdp-connect" PROFILE_FILE="$PROFILE_DIR/profiles.conf" profile_list() { if [[ ! -f "$PROFILE_FILE" ]]; then echo "No saved profiles." return fi local found=0 while IFS= read -r line; do if [[ "$line" =~ ^\[(.+)\]$ ]]; then local name="${BASH_REMATCH[1]}" local srv="" while IFS= read -r kv; do [[ -z "$kv" || "$kv" =~ ^\[ ]] && break if [[ "$kv" =~ ^server= ]]; then srv="${kv#server=}" fi done printf " %-20s %s\n" "$name" "$srv" found=1 fi done < "$PROFILE_FILE" if [[ "$found" -eq 0 ]]; then echo "No saved profiles." fi } profile_load() { local name="$1" if [[ ! -f "$PROFILE_FILE" ]]; then err "Profile '$name' not found (no profiles file)" exit 1 fi local in_section=0 found=0 while IFS= read -r line; do if [[ "$line" =~ ^\[(.+)\]$ ]]; then if [[ "${BASH_REMATCH[1]}" == "$name" ]]; then in_section=1 found=1 continue else [[ "$in_section" -eq 1 ]] && break fi fi if [[ "$in_section" -eq 1 && -n "$line" ]]; then case "$line" in server=*) server="${line#server=}" ;; port=*) port="${line#port=}" ;; domain=*) domain="${line#domain=}" ;; username=*) username="${line#username=}" ;; app=*) app="${line#app=}" ;; fullscreen=*) fullscreen="${line#fullscreen=}" ;; multimon=*) multimon="${line#multimon=}" ;; drive=*) drive="${line#drive=}" ;; sound=*) sound="${line#sound=}" ;; gateway=*) gateway="${line#gateway=}" ;; admin=*) admin="${line#admin=}" ;; no_nla=*) no_nla="${line#no_nla=}" ;; resolution=*) resolution="${line#resolution=}" ;; esac fi done < "$PROFILE_FILE" if [[ "$found" -eq 0 ]]; then err "Profile '$name' not found" exit 1 fi log "Loaded profile '$name'" } profile_save() { local name="$1" if [[ -f "$PROFILE_FILE" ]]; then while IFS= read -r line; do if [[ "$line" =~ ^\[(.+)\]$ && "${BASH_REMATCH[1]}" == "$name" ]]; then err "Profile '$name' already exists (use --delete first)" exit 1 fi done < "$PROFILE_FILE" fi mkdir -p "$PROFILE_DIR" { echo "[$name]" [[ -n "$server" ]] && echo "server=$server" [[ -n "$port" ]] && echo "port=$port" [[ -n "$domain" ]] && echo "domain=$domain" [[ -n "$username" ]] && echo "username=$username" [[ -n "$app" ]] && echo "app=$app" [[ "$fullscreen" -eq 1 ]] && echo "fullscreen=1" [[ "$multimon" -eq 1 ]] && echo "multimon=1" [[ -n "$drive" ]] && echo "drive=$drive" [[ "$sound" -eq 1 ]] && echo "sound=1" [[ -n "$gateway" ]] && echo "gateway=$gateway" [[ "$admin" -eq 1 ]] && echo "admin=1" [[ "$no_nla" -eq 1 ]] && echo "no_nla=1" [[ -n "$resolution" ]] && echo "resolution=$resolution" echo "" } >> "$PROFILE_FILE" log "Saved profile '$name'" } profile_delete() { local name="$1" if [[ ! -f "$PROFILE_FILE" ]]; then err "Profile '$name' not found (no profiles file)" exit 1 fi local found=0 in_section=0 local tmpfile tmpfile=$(mktemp) while IFS= read -r line; do if [[ "$line" =~ ^\[(.+)\]$ ]]; then if [[ "${BASH_REMATCH[1]}" == "$name" ]]; then in_section=1 found=1 continue else in_section=0 fi fi if [[ "$in_section" -eq 0 ]]; then echo "$line" >> "$tmpfile" fi done < "$PROFILE_FILE" if [[ "$found" -eq 0 ]]; then rm -f "$tmpfile" err "Profile '$name' not found" exit 1 fi mv "$tmpfile" "$PROFILE_FILE" log "Deleted profile '$name'" } # ── Defaults ────────────────────────────────────────────────────────── server="" port="3389" domain="" username="" password="" app="" fullscreen=0 multimon=0 drive="" sound=0 gateway="" admin=0 no_nla=0 resolution="" opt_profile="" opt_save="" opt_list=0 opt_delete="" # ── Usage ───────────────────────────────────────────────────────────── usage() { cat </dev/null; then err "xfreerdp not found. Install with:" err " Debian/Ubuntu: sudo apt install freerdp3-x11 (or freerdp2-x11)" err " Fedora/RHEL: sudo dnf install freerdp" exit 1 fi # Detect FreeRDP major version (2 or 3) FREERDP_VER=2 freerdp_ver_output=$(xfreerdp --version 2>&1 || true) if [[ "$freerdp_ver_output" =~ ([0-9]+)\.[0-9]+ ]]; then FREERDP_VER="${BASH_REMATCH[1]}" fi # ── Prompt if needed ─────────────────────────────────────────────────── if [[ -z "$server" || -z "$username" || -z "$password" ]]; then if ! command -v zenity &>/dev/null; then err "Missing required fields and zenity not found for GUI prompt" err "Provide --server, --user, and --pass on the command line" exit 1 fi # Profile picker — show if profiles exist and --profile was not given if [[ -z "$opt_profile" && -f "$PROFILE_FILE" ]]; then profiles=() while IFS= read -r line; do if [[ "$line" =~ ^\[(.+)\]$ ]]; then profiles+=("${BASH_REMATCH[1]}") fi done < "$PROFILE_FILE" if [[ ${#profiles[@]} -gt 0 ]]; then picker_args=() for p in "${profiles[@]}"; do picker_args+=("$p") done picker_args+=("New connection") picked=$(zenity --list --title="Select Profile" \ --text="Choose a saved profile or start a new connection" \ --column="Profile" "${picker_args[@]}") || { log "Cancelled" exit 0 } if [[ "$picked" != "New connection" && -n "$picked" ]]; then profile_load "$picked" fi fi fi output=$(zenity --forms --title="RDP Connection" \ --text="Enter connection details" \ --separator="," \ --add-entry="Server${server:+ [$server]}" \ --add-entry="Port${port:+ [$port]}" \ --add-entry="Domain${domain:+ [$domain]}" \ --add-entry="Username${username:+ [$username]}" \ --add-password="Password" \ --add-entry="Remote App${app:+ [$app]}") || { log "Cancelled" exit 0 } IFS=, read -r z_server z_port z_domain z_username z_password z_app <<< "$output" # Use Zenity values only if CLI values were not already set server="${z_server:-$server}" port="${z_port:-$port}" domain="${z_domain:-$domain}" username="${z_username:-$username}" password="${z_password:-$password}" app="${z_app:-$app}" fi if [[ -z "$server" ]]; then err "Server is required" exit 1 fi if [[ -z "$username" ]]; then err "Username is required" exit 1 fi # ── Build xfreerdp command ──────────────────────────────────────────── cmd=(xfreerdp /v:"${server}:${port}" /u:"${username}" +clipboard /from-stdin ) # Version-specific flags if [[ "$FREERDP_VER" -ge 3 ]]; then cmd+=(/cert:ignore) else cmd+=(/cert-ignore /rfx) fi # Display mode if [[ "$fullscreen" -eq 1 ]]; then cmd+=(/f) elif [[ -n "$resolution" ]]; then cmd+=("/size:${resolution}") else cmd+=(/workarea /dynamic-resolution) fi # Multi-monitor [[ "$multimon" -eq 1 ]] && cmd+=(/multimon) # Domain [[ -n "$domain" ]] && cmd+=(/d:"${domain}") # Drive redirection [[ -n "$drive" ]] && cmd+=("/drive:${drive}") # Sound [[ "$sound" -eq 1 ]] && cmd+=(/sound) # RD Gateway [[ -n "$gateway" ]] && cmd+=("/g:${gateway}") # Admin/console session [[ "$admin" -eq 1 ]] && cmd+=(/admin) # Disable NLA if [[ "$no_nla" -eq 1 ]]; then if [[ "$FREERDP_VER" -ge 3 ]]; then cmd+=(/sec:nla:off) else cmd+=(-sec-nla) fi fi # RemoteApp if [[ -n "$app" ]]; then cmd+=(/t:"${app} on ${server}" "/app:||${app}") else cmd+=(/t:"${server}") fi # ── Connect ─────────────────────────────────────────────────────────── log "Connecting to ${server}:${port} as ${username}..." # Password passed via stdin to keep it out of the process list printf '/p:%s\n' "$password" | "${cmd[@]}" password=""