#!/bin/bash # barnard-ui # Description: Make managing servers with barnard easy. # # Copyright 2019, F123 Consulting, # Copyright 2019, Stormux, # Copyright 2019, Storm Dragon, # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free # Software Foundation; either version 3, or (at your option) any later # version. # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this package; see the file COPYING. If not, write to the Free # Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # #--code-- # the gettext essentials export TEXTDOMAIN=barnard-ui export TEXTDOMAINDIR=/usr/share/locale # shellcheck disable=SC1091 if ! source gettext.sh 2> /dev/null; then gettext() { printf '%s\n' "$1" } fi cacheDir="${XDG_CACHE_HOME:-$HOME/.cache}" configDir="$HOME/.config/barnard" serverFile="$configDir/servers.conf" certFile="$configDir/barnard.pem" logFile="$cacheDir/${0##*/}.log" if ! mkdir -p "$cacheDir" "$configDir"; then printf 'Could not create Barnard configuration directories.\n' >&2 exit 1 fi if ! : > "$logFile"; then printf 'Could not write log file: %s\n' "$logFile" >&2 exit 1 fi # Settings to improve accessibility of dialog. export DIALOGOPTS='--insecure --no-lines --visit-items' declare -Ag mumbleServerList=() declare -Ag serverAddresses=() declare -Ag serverPorts=() declare -Ag serverPasswords=() declare -Ag serverInsecure=() # Log writing function log() { # Usage: command | log for just stdout. # Or command |& log for stderr and stdout. local line while IFS= read -r line ; do printf '%s\n' "$line" >> "$logFile" done } fatal() { local message="$*" printf '%s\n' "$message" | log if command -v dialog > /dev/null 2>&1; then dialog --clear --msgbox "$message" 10 72 else printf '%s\n' "$message" >&2 fi exit 1 } require_command() { local commandName="$1" local displayName="${2:-$1}" if ! command -v "$commandName" > /dev/null 2>&1; then fatal "$(gettext "Required command not found:") $displayName" fi } inputbox() { # Returns: text entered by the user # Args 1, Instructions for box. # args: 2 initial text (optional) dialog --clear --backtitle "$(gettext "Enter text and press enter.")" \ --inputbox "$1" 0 0 "$2" --stdout } passwordbox() { # Returns: text entered by the user # Args 1, Instructions for box. # args: 2 initial text (optional) dialog --clear --backtitle "$(gettext "Enter text and press enter.")" \ --passwordbox "$1" 0 0 "$2" --stdout } msgbox() { # Returns: None # Shows the provided message on the screen with an ok button. dialog --clear --msgbox "$*" 10 72 } yesno() { # Returns: Yes or No # Args: Question to user. # Called in if $(yesno) == "Yes" # Or variable=$(yesno) if dialog --clear --backtitle "$(gettext "Press 'Enter' for \"yes\" or 'Escape' for \"no\".")" --yesno "$*" 10 80 --stdout; then echo "Yes" else echo "No" fi } menulist() { # Args: menu options. # returns: selected tag local i local -a menuList=() for i in "$@" ; do menuList+=("$i" "$i") done dialog --backtitle "$(gettext "Use the up and down arrow keys to find the option you want, then press enter to select it.")" \ --clear \ --no-tags \ --menu "$(gettext "Please select one")" 0 0 0 "${menuList[@]}" --stdout } trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } field_is_valid() { local value="$1" [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] } port_is_valid() { local port="$1" [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) } parse_host_port() { local hostPort hostPort="$(trim "$1")" parsedAddress="" parsedPort="64738" if [[ -z "$hostPort" ]]; then return 1 fi if [[ "$hostPort" =~ ^\[([^]]+)\](:([0-9]+))?$ ]]; then parsedAddress="${BASH_REMATCH[1]}" parsedPort="${BASH_REMATCH[3]:-64738}" elif [[ "$hostPort" =~ ^(.+):([0-9]+)$ ]]; then parsedAddress="${BASH_REMATCH[1]}" parsedPort="${BASH_REMATCH[2]}" elif [[ "$hostPort" =~ ^(.+):([^:]+)$ ]]; then return 1 else parsedAddress="$hostPort" fi parsedAddress="$(trim "$parsedAddress")" if [[ -z "$parsedAddress" ]] || ! port_is_valid "$parsedPort"; then return 1 fi field_is_valid "$parsedAddress" } parse_server_input() { local raw="$1" local hostPort raw="$(trim "$raw")" parsedPassword="" if [[ -z "$raw" ]]; then return 1 fi if [[ "$raw" == *@* ]]; then parsedPassword="${raw%%@*}" hostPort="${raw#*@}" else hostPort="$raw" fi field_is_valid "$parsedPassword" && parse_host_port "$hostPort" } add_server_record() { local serverName local serverAddress="$2" local serverPort="$3" local serverPassword="$4" local insecure="${5:-0}" serverName="$(trim "$1")" if [[ -z "$serverName" ]]; then return 1 fi if ! field_is_valid "$serverName" || ! field_is_valid "$serverAddress" || ! field_is_valid "$serverPassword"; then return 1 fi if ! port_is_valid "$serverPort"; then return 1 fi insecure="${insecure,,}" if [[ "$insecure" == "true" || "$insecure" == "yes" || "$insecure" == "on" ]]; then insecure="1" fi if [[ "$insecure" != "1" ]]; then insecure="0" fi serverAddresses["$serverName"]="$serverAddress" serverPorts["$serverName"]="$serverPort" serverPasswords["$serverName"]="$serverPassword" serverInsecure["$serverName"]="$insecure" mumbleServerList["$serverName"]="$serverAddress:$serverPort" } server_names() { printf '%s\n' "${!mumbleServerList[@]}" | LC_ALL=C sort } server_list_is_empty() { (( ${#mumbleServerList[@]} == 0 )) } save_servers() { local tmpFile="$serverFile.tmp" local name local insecure if ! { printf '# barnard-ui server list\n' printf '# Passwords are stored only when provided; this file is written with mode 0600.\n\n' while IFS= read -r name; do [[ -z "$name" ]] && continue if [[ "${serverInsecure[$name]}" == "1" ]]; then insecure="true" else insecure="false" fi printf '[server]\n' printf 'name = %s\n' "$name" printf 'address = %s\n' "${serverAddresses[$name]}" printf 'port = %s\n' "${serverPorts[$name]}" printf 'password = %s\n' "${serverPasswords[$name]}" printf 'insecure = %s\n\n' "$insecure" done < <(server_names) } > "$tmpFile"; then rm -f "$tmpFile" msgbox "$(gettext "Could not save server list.")" return 1 fi chmod 600 "$tmpFile" 2> /dev/null || true if ! mv "$tmpFile" "$serverFile"; then rm -f "$tmpFile" msgbox "$(gettext "Could not save server list.")" return 1 fi } load_servers() { local line local name local address local port local password local insecure local key local value local inServerSection=0 local needsRewrite=0 [[ -r "$serverFile" ]] || return 0 flush_server_section() { if (( inServerSection )); then if [[ -n "$name" || -n "$address" || -n "$password" ]]; then if ! add_server_record "$name" "$address" "$port" "$password" "$insecure"; then printf 'Ignored invalid server entry from %s\n' "$serverFile" | log needsRewrite=1 fi fi fi name="" address="" port="64738" password="" insecure="0" inServerSection=0 } flush_server_section while IFS= read -r line || [[ -n "$line" ]]; do line="$(trim "$line")" [[ -z "$line" || "$line" == \#* ]] && continue if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then flush_server_section if [[ "${BASH_REMATCH[1],,}" == "server" ]]; then inServerSection=1 else needsRewrite=1 fi continue fi if (( ! inServerSection )); then needsRewrite=1 continue fi if [[ "$line" == *=* ]]; then key="${line%%=*}" value="${line#*=}" key="$(trim "$key")" key="${key,,}" value="$(trim "$value")" case "$key" in name) name="$value" ;; address|host) address="$value" ;; port) port="$value" ;; password) password="$value" ;; insecure) insecure="$value" ;; *) needsRewrite=1 ;; esac else needsRewrite=1 fi done < "$serverFile" flush_server_section if (( needsRewrite )); then save_servers fi } config_has_nonempty_value() { local key="$1" local configFile="${2:-$HOME/.barnard.toml}" local line local currentKey local value key="${key,,}" [[ -r "$configFile" ]] || return 1 while IFS= read -r line || [[ -n "$line" ]]; do line="$(trim "$line")" [[ -z "$line" || "$line" == \#* || "$line" != *=* ]] && continue currentKey="${line%%=*}" currentKey="$(trim "$currentKey")" currentKey="${currentKey,,}" [[ "$currentKey" == "$key" ]] || continue value="${line#*=}" value="$(trim "$value")" [[ -z "$value" || "$value" == '""' || "$value" == "''" ]] && return 1 return 0 done < "$configFile" return 1 } add-server() { local serverName local serverAddress local serverPassword local insecure="0" serverName="$(inputbox "$(gettext "Enter a name for the new server:")")" || return serverName="$(trim "$serverName")" if [[ -z "$serverName" ]]; then msgbox "$(gettext "Server name cannot be empty.")" return fi if ! field_is_valid "$serverName"; then msgbox "$(gettext "Server name cannot contain line breaks.")" return fi serverAddress="$(inputbox "$(gettext "Enter the address of the server. If the port is not standard, add it after a colon, like address:port.")")" || return if ! parse_server_input "$serverAddress"; then msgbox "$(gettext "Invalid server address or port.")" return fi serverPassword="$(passwordbox "$(gettext "Enter the server password, or leave it blank if there is no password:")")" || return if [[ -n "$serverPassword" ]]; then if ! field_is_valid "$serverPassword"; then msgbox "$(gettext "Server password cannot contain line breaks.")" return fi parsedPassword="$serverPassword" fi if [[ "$(yesno "$(gettext "Skip server certificate verification for this server?")")" == "Yes" ]]; then insecure="1" fi if ! add_server_record "$serverName" "$parsedAddress" "$parsedPort" "$parsedPassword" "$insecure"; then msgbox "$(gettext "Could not add server. Check the server name, address, and password.")" return fi save_servers || return printf 'Added server %s %s:%s\n' "$serverName" "$parsedAddress" "$parsedPort" | log msgbox "$(gettext "Added server") $serverName" } connect() { local serverName local barnardStatus local -a names=() local -a barnardArgs=() if server_list_is_empty; then msgbox "$(gettext "No saved servers. Add a server first.")" return fi mapfile -t names < <(server_names) serverName="$(menulist "${names[@]}" "$(gettext "Go Back")")" || return if [[ -z "$serverName" || "$serverName" == "$(gettext "Go Back")" ]]; then return fi require_command barnard barnard barnardArgs=(-server "${serverAddresses[$serverName]}:${serverPorts[$serverName]}") if [[ -n "${serverPasswords[$serverName]}" ]]; then barnardArgs+=(-password "${serverPasswords[$serverName]}") fi if [[ "${serverInsecure[$serverName]}" == "1" ]]; then barnardArgs+=(-insecure) fi if ! config_has_nonempty_value username; then barnardArgs+=(-username "${USER}-${HOSTNAME}") fi if [[ -f "$certFile" ]] && ! config_has_nonempty_value certificate; then barnardArgs+=(-certificate "$certFile") fi command barnard "${barnardArgs[@]}" --fifo "$configDir/cmd" --buffers 16 |& log barnardStatus=${PIPESTATUS[0]} if (( barnardStatus != 0 )); then msgbox "$(gettext "Barnard exited with status") $barnardStatus. $(gettext "See log:") $logFile" fi } remove-server() { local serverName local -a names=() if server_list_is_empty; then msgbox "$(gettext "No saved servers to remove.")" return fi mapfile -t names < <(server_names) serverName="$(menulist "${names[@]}" "$(gettext "Go Back")")" || return if [[ -z "$serverName" || "$serverName" == "$(gettext "Go Back")" ]]; then return fi unset "mumbleServerList[$serverName]" unset "serverAddresses[$serverName]" unset "serverPorts[$serverName]" unset "serverPasswords[$serverName]" unset "serverInsecure[$serverName]" save_servers || return printf 'Removed server %s\n' "$serverName" | log msgbox "$(gettext "Removed server") $serverName" } generate-certificate() { local commonName require_command openssl openssl if [[ -f "$certFile" ]]; then if [[ "$(yesno "$(gettext "A certificate already exists. Do you want to replace it? This may affect your registered identity on servers.")")" != "Yes" ]]; then return fi fi commonName="$(inputbox "$(gettext "Enter a name for your certificate (e.g., your username):")" "barnard")" || return [[ -z "$commonName" ]] && commonName="barnard" if openssl req -x509 -newkey rsa:2048 -keyout "$certFile" -out "$certFile" -days 3650 -nodes -subj "/CN=$commonName" 2> /dev/null; then chmod 600 "$certFile" msgbox "$(gettext "Certificate generated successfully.")" else msgbox "$(gettext "Failed to generate certificate. Make sure openssl is installed.")" fi } view-certificate() { local certInfo require_command openssl openssl if [[ ! -f "$certFile" ]]; then msgbox "$(gettext "No certificate found.") $certFile" return fi certInfo=$(openssl x509 -in "$certFile" -noout -subject -dates -fingerprint 2> /dev/null) if [[ -n "$certInfo" ]]; then msgbox "$certInfo" else msgbox "$(gettext "Could not read certificate information.")" fi } import-certificate() { local importPath require_command openssl openssl importPath="$(inputbox "$(gettext "Enter the full path to your certificate file (PEM format with certificate and private key):")")" || return [[ -z "$importPath" ]] && return # Expand ~ if present importPath="${importPath/#\~/$HOME}" if [[ ! -f "$importPath" ]]; then msgbox "$(gettext "File not found:") $importPath" return fi # Verify it's a valid certificate if ! openssl x509 -in "$importPath" -noout 2> /dev/null; then msgbox "$(gettext "The file does not appear to be a valid PEM certificate.")" return fi # Verify it contains a private key if ! openssl rsa -in "$importPath" -check -noout 2> /dev/null && ! openssl ec -in "$importPath" -check -noout 2> /dev/null; then msgbox "$(gettext "The file does not appear to contain a valid private key. The certificate file must contain both the certificate and private key.")" return fi if [[ -f "$certFile" ]]; then if [[ "$(yesno "$(gettext "A certificate already exists. Do you want to replace it?")")" != "Yes" ]]; then return fi fi if cp "$importPath" "$certFile" && chmod 600 "$certFile"; then msgbox "$(gettext "Certificate imported successfully.")" else msgbox "$(gettext "Failed to import certificate.")" fi } manage-certificate() { local certAction while : ; do certAction="$(menulist "$(gettext "Generate")" "$(gettext "View")" "$(gettext "Import")" "$(gettext "Go Back")")" || return case "$certAction" in "$(gettext "Generate")") generate-certificate ;; "$(gettext "View")") view-certificate ;; "$(gettext "Import")") import-certificate ;; "$(gettext "Go Back")"|"") return ;; esac done } main() { local action require_command dialog dialog load_servers while : ; do action="$(menulist "$(gettext "Connect")" "$(gettext "Add server")" "$(gettext "Remove server")" "$(gettext "Manage Certificate")" "$(gettext "Exit")")" || exit 0 case "$action" in "$(gettext "Connect")") connect ;; "$(gettext "Add server")") add-server ;; "$(gettext "Remove server")") remove-server ;; "$(gettext "Manage Certificate")") manage-certificate ;; "$(gettext "Exit")"|"") exit 0 ;; esac done } if [[ "${BARNARD_UI_TESTING:-0}" != "1" ]]; then main "$@" fi