600 lines
18 KiB
Bash
Executable File
600 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
|
# barnard-ui
|
|
# Description: Make managing servers with barnard easy.
|
|
#
|
|
# Copyright 2019, F123 Consulting, <information@f123.org>
|
|
# Copyright 2019, Stormux, <storm_dragon@linux-a11y.org>
|
|
# Copyright 2019, Storm Dragon, <storm_dragon@linux-a11y.org>
|
|
#
|
|
# 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
|