Initial commit.

This commit is contained in:
Storm Dragon
2026-04-16 19:46:49 -04:00
parent 00b5d6c8f7
commit 793e8829d7
11 changed files with 2178 additions and 199 deletions

454
.includes/copyparty.sh Normal file
View File

@@ -0,0 +1,454 @@
#!/usr/bin/env bash
copypartyConfigFile="/etc/copyparty/copyparty.conf"
copypartyPort="3923"
copyparty_installed() {
pacman -Q copyparty &> /dev/null
}
ufw_installed() {
pacman -Q ufw &> /dev/null
}
valid_ipv4_subnet() {
local subnetValue="$1"
[[ "$subnetValue" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1
awk -F '[./]' '
{
for (index = 1; index <= 4; index++) {
if ($index < 0 || $index > 255) {
exit 1
}
}
exit 0
}
' <<< "$subnetValue"
}
detect_private_subnets() {
ip -o -4 addr show up scope global | awk '
{
split($4, parts, "/")
split(parts[1], octets, ".")
if (octets[1] == 10) {
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
} else if (octets[1] == 192 && octets[2] == 168) {
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
} else if (octets[1] == 172 && octets[2] >= 16 && octets[2] <= 31) {
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
} else {
next
}
if (!seen[subnet]++) {
print subnet
}
}
'
}
choose_lan_subnet() {
local detectedSubnet=""
local subnetChoice=""
detectedSubnet="$(detect_private_subnets | head -n 1)"
subnetChoice="$(inputbox "Confirm the LAN subnet for Copyparty access." "${detectedSubnet:-192.168.1.0/24}")" || return 1
if valid_ipv4_subnet "$subnetChoice"; then
printf '%s\n' "$subnetChoice"
return 0
fi
msgbox "Enter a valid IPv4 subnet such as 192.168.1.0/24."
return 1
}
install_copyparty() {
local packageList=(
copyparty
ffmpeg
cfssl
python-mutagen
python-pillow
python-pyvips
libkeyfinder
python-pyopenssl
python-pyzmq
python-argon2-cffi
)
if copyparty_installed; then
return 0
fi
if ! install_package "${packageList[@]}"; then
msgbox "Failed to install Copyparty and its optional feature packages."
return 1
fi
msgbox "Copyparty and its optional feature packages are installed."
return 0
}
list_local_users() {
awk -F ':' '
$3 >= 1000 && $3 < 60000 && $7 !~ /(nologin|false)$/ {
print $1
}
' /etc/passwd | sort
}
choose_copyparty_user() {
local userEntries=()
local userName=""
local selectedUser=""
while IFS= read -r userName; do
[[ -n "$userName" ]] && userEntries+=("$userName")
done < <(list_local_users)
if [[ "${#userEntries[@]}" -eq 0 ]]; then
msgbox "No regular local users were found."
return 1
fi
selectedUser="$(menulist "${userEntries[@]}")" || return 1
printf '%s\n' "$selectedUser"
return 0
}
account_name_exists() {
local accountsFile="$1"
local username="$2"
awk -F ':' -v username="$username" '
/^[[:space:]]+[A-Za-z0-9._-]+:/ {
accountName = $1
gsub(/^[[:space:]]+/, "", accountName)
if (accountName == username) {
exit 0
}
}
END {
exit 1
}
' "$accountsFile"
}
collect_copyparty_accounts() {
local tempAccountsFile=""
local username=""
local password=""
local addAnother="No"
tempAccountsFile="$(mktemp)"
while true; do
username="$(inputbox "Enter a Copyparty username.")" || {
rm -f "$tempAccountsFile"
return 1
}
if [[ -z "$username" || "$username" =~ [[:space:]:] ]]; then
msgbox "Usernames cannot be blank and cannot contain spaces or colons."
continue
fi
if account_name_exists "$tempAccountsFile" "$username"; then
msgbox "That Copyparty username already exists in this configuration."
continue
fi
password="$(passwordbox "Enter the password for ${username}.")" || {
rm -f "$tempAccountsFile"
return 1
}
if [[ -z "$password" ]]; then
msgbox "Passwords cannot be blank."
continue
fi
printf ' %s: %s\n' "$username" "$password" >> "$tempAccountsFile"
addAnother="$(yesno "Add another Copyparty account?")"
if [[ "$addAnother" != "Yes" ]]; then
break
fi
done
printf '%s\n' "$tempAccountsFile"
return 0
}
choose_share_mode() {
local shareMode=""
shareMode="$(menulist "Private read-write" "Private read-only" "Public read-only")" || return 1
printf '%s\n' "$shareMode"
return 0
}
append_share_config() {
local outputFile="$1"
local sharePath="$2"
local urlPath="$3"
local shareMode="$4"
local usernamesCsv="$5"
{
printf '\n[%s]\n' "$urlPath"
printf ' %s\n' "$sharePath"
printf ' accs:\n'
case "$shareMode" in
"Private read-write")
printf ' rwmd: %s\n' "$usernamesCsv"
printf ' flags:\n'
printf ' daw\n'
;;
"Private read-only")
printf ' r: %s\n' "$usernamesCsv"
;;
"Public read-only")
printf ' r: *\n'
;;
esac
} >> "$outputFile"
}
share_url_exists() {
local sharesFile="$1"
local urlPath="$2"
awk -v urlPath="$urlPath" '
/^\[[^]]+\]$/ {
sectionName = $0
sub(/^\[/, "", sectionName)
sub(/\]$/, "", sectionName)
if (sectionName == urlPath) {
exit 0
}
}
END {
exit 1
}
' "$sharesFile"
}
service_user_can_read_share() {
local serviceUser="$1"
local sharePath="$2"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" -u "$serviceUser" test -r "$sharePath" &&
sudo "${sudoFlags[@]}" -u "$serviceUser" test -x "$sharePath"
}
service_user_can_write_share() {
local serviceUser="$1"
local sharePath="$2"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" -u "$serviceUser" test -w "$sharePath" &&
sudo "${sudoFlags[@]}" -u "$serviceUser" test -x "$sharePath"
}
collect_copyparty_shares() {
local usernamesCsv="$1"
local serviceUser="$2"
local tempSharesFile=""
local sharePath=""
local urlPath=""
local shareMode=""
local addAnother="No"
tempSharesFile="$(mktemp)"
while true; do
sharePath="$(inputbox "Enter the local path to share.")" || {
rm -f "$tempSharesFile"
return 1
}
if [[ -z "$sharePath" ]]; then
msgbox "A local path is required."
continue
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" test -d "$sharePath"; then
msgbox "The path ${sharePath} does not exist or is not a directory."
continue
fi
if ! service_user_can_read_share "$serviceUser" "$sharePath"; then
msgbox "The selected service user ${serviceUser} cannot read ${sharePath}. Adjust permissions or choose another path."
continue
fi
urlPath="$(inputbox "Enter the Copyparty URL path for this share, for example /files.")" "/files" || {
rm -f "$tempSharesFile"
return 1
}
if [[ ! "$urlPath" =~ ^/[^[:space:]]*$ ]]; then
msgbox "Enter a URL path that starts with / and contains no spaces."
continue
fi
if share_url_exists "$tempSharesFile" "$urlPath"; then
msgbox "That Copyparty URL path is already defined in this configuration."
continue
fi
shareMode="$(choose_share_mode)" || {
rm -f "$tempSharesFile"
return 1
}
if [[ "$shareMode" == "Private read-write" ]] &&
! service_user_can_write_share "$serviceUser" "$sharePath"; then
msgbox "The selected service user ${serviceUser} cannot write to ${sharePath}. Choose another path or adjust permissions."
continue
fi
append_share_config "$tempSharesFile" "$sharePath" "$urlPath" "$shareMode" "$usernamesCsv"
addAnother="$(yesno "Add another Copyparty share?")"
if [[ "$addAnother" != "Yes" ]]; then
break
fi
done
printf '%s\n' "$tempSharesFile"
return 0
}
join_account_names() {
local accountsFile="$1"
awk -F ':' '
BEGIN {
first = 1
}
/^[[:space:]]+[A-Za-z0-9._-]+:/ {
gsub(/^[[:space:]]+/, "", $1)
if (!first) {
printf ", "
}
printf "%s", $1
first = 0
}
END {
printf "\n"
}
' "$accountsFile"
}
write_copyparty_config() {
local serviceUser="$1"
local accountsFile="$2"
local sharesFile="$3"
local tempConfigFile=""
local serviceGroup=""
tempConfigFile="$(mktemp)"
serviceGroup="$(id -gn "$serviceUser")"
{
printf '[global]\n'
printf ' i: 0.0.0.0\n'
printf ' p: %s\n' "$copypartyPort"
printf ' e2dsa\n'
printf '\n[accounts]\n'
cat "$accountsFile"
cat "$sharesFile"
} > "$tempConfigFile"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -d -m 755 /etc/copyparty; then
rm -f "$tempConfigFile"
msgbox "Failed to create /etc/copyparty."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -o root -g "$serviceGroup" -m 640 "$tempConfigFile" "$copypartyConfigFile"; then
rm -f "$tempConfigFile"
msgbox "Failed to write ${copypartyConfigFile}."
return 1
fi
rm -f "$tempConfigFile"
return 0
}
enable_copyparty_service() {
local serviceUser="$1"
local unitName="copyparty@${serviceUser}"
local configuredUnit=""
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
while IFS= read -r configuredUnit; do
[[ -z "$configuredUnit" || "$configuredUnit" == "$unitName" ]] && continue
sudo "${sudoFlags[@]}" systemctl disable --now "$configuredUnit" &> /dev/null || true
done < <(
sudo "${sudoFlags[@]}" find /etc/systemd/system -type l -name 'copyparty@*.service' -printf '%f\n' 2> /dev/null | sort -u
)
if ! sudo "${sudoFlags[@]}" systemctl enable "$unitName"; then
msgbox "Copyparty was configured, but the service failed to enable."
return 1
fi
if sudo "${sudoFlags[@]}" systemctl is-active --quiet "$unitName"; then
if ! sudo "${sudoFlags[@]}" systemctl restart "$unitName"; then
msgbox "Copyparty was configured, but the running service failed to restart."
return 1
fi
return 0
fi
if ! sudo "${sudoFlags[@]}" systemctl start "$unitName"; then
msgbox "Copyparty was configured, but the service failed to start."
return 1
fi
return 0
}
configure_copyparty_firewall() {
local lanSubnet=""
if ! ufw_installed; then
msgbox "ufw is not installed, so Copyparty will listen on ${copypartyPort} without helper-managed LAN restrictions. Install and configure ufw if you need local-only enforcement."
return 0
fi
if [[ "$(yesno "ufw is installed. Allow LAN-only access to Copyparty on port ${copypartyPort}?")" != "Yes" ]]; then
return 0
fi
lanSubnet="$(choose_lan_subnet)" || return 1
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow from "$lanSubnet" to any port "$copypartyPort" proto tcp; then
msgbox "Failed to allow Copyparty access for ${lanSubnet}."
return 1
fi
msgbox "Copyparty is allowed on ${copypartyPort}/tcp for ${lanSubnet}."
return 0
}
serviceUser="$(choose_copyparty_user)" || return 1
install_copyparty || return 1
accountsFile="$(collect_copyparty_accounts)" || return 1
usernamesCsv="$(join_account_names "$accountsFile")"
sharesFile="$(collect_copyparty_shares "$usernamesCsv" "$serviceUser")" || {
rm -f "$accountsFile"
return 1
}
write_copyparty_config "$serviceUser" "$accountsFile" "$sharesFile" || {
rm -f "$accountsFile" "$sharesFile"
return 1
}
rm -f "$accountsFile" "$sharesFile"
enable_copyparty_service "$serviceUser" || return 1
configure_copyparty_firewall || return 1
msgbox "Copyparty is configured. For advanced tuning, edit ${copypartyConfigFile} as root."

298
.includes/firewall.sh Normal file
View File

@@ -0,0 +1,298 @@
#!/usr/bin/env bash
sshConfigDropIn="/etc/ssh/sshd_config.d/99-stormux-server.conf"
ufw_installed() {
pacman -Q ufw &> /dev/null
}
ufw_status_output() {
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" ufw status 2>&1
}
ensure_ufw() {
if ufw_installed; then
return 0
fi
if [[ "$(yesno "ufw is not installed. Install it now and continue?")" != "Yes" ]]; then
msgbox "Firewall action cancelled."
return 1
fi
if ! install_package ufw; then
msgbox "Failed to install ufw."
return 1
fi
msgbox "ufw installed."
return 0
}
valid_port() {
local portValue="$1"
[[ "$portValue" =~ ^[0-9]+$ ]] && (( portValue >= 1 && portValue <= 65535 ))
}
valid_port_rule() {
local ruleValue="$1"
if [[ "$ruleValue" =~ ^[0-9]+$ ]]; then
valid_port "$ruleValue"
return $?
fi
if [[ "$ruleValue" =~ ^([0-9]+)/(tcp|udp)$ ]]; then
valid_port "${BASH_REMATCH[1]}"
return $?
fi
return 1
}
resolve_ssh_port() {
local configuredPort=""
if [[ -r "$sshConfigDropIn" ]]; then
configuredPort="$(awk '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*Port[[:space:]]+[0-9]+([[:space:]]*#.*)?$/ {
port = $2
}
END {
if (port != "") {
print port
}
}
' "$sshConfigDropIn")"
fi
if valid_port "$configuredPort"; then
printf '%s\n' "$configuredPort"
return 0
fi
configuredPort="$(inputbox "Unable to confirm the SSH port automatically. Enter the SSH port to allow before changing the firewall.")" || return 1
if valid_port "$configuredPort"; then
printf '%s\n' "$configuredPort"
return 0
fi
msgbox "A valid SSH port is required before enabling firewall rules."
return 1
}
allow_rule() {
local ruleValue="$1"
local description="$2"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow "$ruleValue"; then
msgbox "Failed to allow ${description}."
return 1
fi
msgbox "${description} allowed."
return 0
}
delete_rule() {
local ruleValue="$1"
local description="$2"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw delete allow "$ruleValue"; then
msgbox "Failed to remove ${description}."
return 1
fi
msgbox "${description} removed."
return 0
}
allow_ssh_port() {
local sshPort=""
ensure_ufw || return 1
sshPort="$(resolve_ssh_port)" || {
msgbox "Firewall change cancelled because the SSH port could not be confirmed."
return 1
}
allow_rule "${sshPort}/tcp" "SSH port ${sshPort}/tcp"
}
enable_firewall() {
ensure_ufw || return 1
allow_ssh_port || return 1
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if sudo "${sudoFlags[@]}" systemctl enable --now ufw; then
msgbox "Firewall enabled."
return 0
fi
msgbox "Failed to enable ufw."
return 1
}
disable_firewall() {
ensure_ufw || return 1
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if sudo "${sudoFlags[@]}" systemctl disable --now ufw; then
msgbox "Firewall disabled."
return 0
fi
msgbox "Failed to disable ufw."
return 1
}
open_custom_port() {
local portNumber=""
local protocolChoice=""
ensure_ufw || return 1
portNumber="$(inputbox "Enter the port number to open.")" || return 1
if ! valid_port "$portNumber"; then
msgbox "Enter a valid port number from 1 to 65535."
return 1
fi
protocolChoice="$(menulist "TCP" "UDP" "Both")" || return 1
case "$protocolChoice" in
"TCP")
allow_rule "${portNumber}/tcp" "Port ${portNumber}/tcp"
;;
"UDP")
allow_rule "${portNumber}/udp" "Port ${portNumber}/udp"
;;
"Both")
allow_rule "${portNumber}/tcp" "Port ${portNumber}/tcp" || return 1
allow_rule "${portNumber}/udp" "Port ${portNumber}/udp"
;;
esac
}
list_simple_allow_rules() {
local sshPort=""
local statusLine=""
declare -A tcpPorts=()
declare -A udpPorts=()
sshPort="$(resolve_ssh_port 2> /dev/null || true)"
while IFS= read -r statusLine; do
if [[ "$statusLine" =~ ^[[:space:]]*([0-9]+)/(tcp|udp)[[:space:]]+ALLOW[[:space:]]+Anywhere[[:space:]]*$ ]]; then
if [[ "${BASH_REMATCH[1]}" == "$sshPort" ]]; then
continue
fi
case "${BASH_REMATCH[2]}" in
tcp)
tcpPorts["${BASH_REMATCH[1]}"]=1
;;
udp)
udpPorts["${BASH_REMATCH[1]}"]=1
;;
esac
fi
done < <(ufw_status_output)
for portNumber in "${!tcpPorts[@]}"; do
if [[ -n "${udpPorts[$portNumber]:-}" ]]; then
printf '%s/both\n' "$portNumber"
unset 'udpPorts[$portNumber]'
else
printf '%s/tcp\n' "$portNumber"
fi
done
for portNumber in "${!udpPorts[@]}"; do
printf '%s/udp\n' "$portNumber"
done | sort -n -t / -k 1,1
}
close_port() {
local removableRules=()
local selectedRule=""
local ruleValue=""
ensure_ufw || return 1
while IFS= read -r ruleValue; do
[[ -n "$ruleValue" ]] && removableRules+=("$ruleValue")
done < <(list_simple_allow_rules | sort -n -t / -k 1,1)
if [[ "${#removableRules[@]}" -eq 0 ]]; then
msgbox "No simple removable port rules were found."
return 0
fi
selectedRule="$(menulist "${removableRules[@]}")" || return 0
case "$selectedRule" in
*/tcp|*/udp)
delete_rule "$selectedRule" "Port ${selectedRule}"
;;
*/both)
ruleValue="${selectedRule%/both}"
delete_rule "${ruleValue}/tcp" "Port ${ruleValue}/tcp" || return 1
delete_rule "${ruleValue}/udp" "Port ${ruleValue}/udp"
;;
esac
}
view_firewall_status() {
local tempFile=""
local statusText=""
ensure_ufw || return 1
tempFile="$(mktemp)"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
statusText="$(sudo "${sudoFlags[@]}" ufw status verbose 2>&1)"
printf '%s\n' "$statusText" > "$tempFile"
textbox "$tempFile"
rm -f "$tempFile"
}
while true; do
firewallChoice="$(menulist \
"Enable firewall" \
"Disable firewall" \
"Allow SSH" \
"Open port" \
"Close port" \
"View status" \
"Back")" || break
case "$firewallChoice" in
"Enable firewall")
enable_firewall
;;
"Disable firewall")
disable_firewall
;;
"Allow SSH")
allow_ssh_port
;;
"Open port")
open_custom_port
;;
"Close port")
close_port
;;
"View status")
view_firewall_status
;;
"Back")
break
;;
esac
done

9
.includes/functions.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
install_package() {
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
yay --sudoflags "${sudoFlags[@]}" --sudoloop --noconfirm -Syu
# shellcheck disable=SC2154
yay --sudoflags "${sudoFlags[@]}" --sudoloop --needed --noconfirm -Sy "$@"
}

174
.includes/minidlna.sh Normal file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env bash
minidlnaConfigFile="/etc/minidlna.conf"
minidlna_installed() {
pacman -Q minidlna &> /dev/null
}
ufw_installed() {
pacman -Q ufw &> /dev/null
}
valid_port() {
local portValue="$1"
[[ "$portValue" =~ ^[0-9]+$ ]] && (( portValue >= 1 && portValue <= 65535 ))
}
install_minidlna() {
if minidlna_installed; then
return 0
fi
if ! install_package minidlna; then
msgbox "Failed to install minidlna."
return 1
fi
return 0
}
enable_minidlna_service() {
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" systemctl enable minidlna; then
msgbox "Failed to enable minidlna."
return 1
fi
return 0
}
read_minidlna_port() {
local configuredPort=""
local fallbackPort=""
if [[ -r "$minidlnaConfigFile" ]]; then
configuredPort="$(awk -F '=' '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*port[[:space:]]*=/ {
value = $2
sub(/[[:space:]]*#.*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
port = value
}
END {
if (port != "") {
print port
}
}
' "$minidlnaConfigFile")"
fi
if valid_port "$configuredPort"; then
printf '%s\n' "$configuredPort"
return 0
fi
fallbackPort="$(inputbox "Unable to confirm the MiniDLNA port from /etc/minidlna.conf. Enter the TCP port to allow." "8200")" || return 1
if valid_port "$fallbackPort"; then
printf '%s\n' "$fallbackPort"
return 0
fi
msgbox "A valid MiniDLNA port is required before adding firewall rules."
return 1
}
detect_private_subnets() {
ip -o -4 addr show up scope global | awk '
{
split($4, parts, "/")
split(parts[1], octets, ".")
prefix = parts[2] + 0
if (octets[1] == 10) {
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
} else if (octets[1] == 192 && octets[2] == 168) {
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
} else if (octets[1] == 172 && octets[2] >= 16 && octets[2] <= 31) {
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
} else {
next
}
if (!seen[subnet]++) {
print subnet
}
}
'
}
valid_ipv4_subnet() {
local subnetValue="$1"
[[ "$subnetValue" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1
awk -F '[./]' '
{
for (index = 1; index <= 4; index++) {
if ($index < 0 || $index > 255) {
exit 1
}
}
exit 0
}
' <<< "$subnetValue"
}
choose_lan_subnet() {
local detectedSubnet=""
local subnetChoice=""
detectedSubnet="$(detect_private_subnets | head -n 1)"
subnetChoice="$(inputbox "Confirm the LAN subnet for MiniDLNA firewall access." "${detectedSubnet:-192.168.1.0/24}")" || return 1
if valid_ipv4_subnet "$subnetChoice"; then
printf '%s\n' "$subnetChoice"
return 0
fi
msgbox "Enter a valid IPv4 subnet such as 192.168.1.0/24."
return 1
}
configure_minidlna_firewall() {
local minidlnaPort=""
local lanSubnet=""
minidlnaPort="$(read_minidlna_port)" || return 1
lanSubnet="$(choose_lan_subnet)" || return 1
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow from "$lanSubnet" to any port "$minidlnaPort" proto tcp; then
msgbox "Failed to allow MiniDLNA TCP access for ${lanSubnet}."
return 1
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow from "$lanSubnet" to any port 1900 proto udp; then
msgbox "Failed to allow SSDP discovery for ${lanSubnet}."
return 1
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw reload; then
msgbox "MiniDLNA firewall rules were added, but ufw reload failed."
return 1
fi
msgbox "MiniDLNA firewall rules were added for ${lanSubnet}."
return 0
}
install_minidlna || return 1
enable_minidlna_service || return 1
msgbox "MiniDLNA is installed and enabled for future boots. Edit /etc/minidlna.conf to set your media paths before rebooting or before manually starting the service. It will start automatically on the next server reboot."
if ufw_installed; then
if [[ "$(yesno "ufw is installed. Configure LAN-only firewall rules for MiniDLNA now?")" == "Yes" ]]; then
configure_minidlna_firewall
fi
fi

242
.includes/mumble.sh Normal file
View File

@@ -0,0 +1,242 @@
#!/usr/bin/env bash
mumbleConfigFile="/etc/mumble/mumble-server.ini"
mumbleWelcomeFile="/etc/mumble/welcomefile.html"
mumbleDefaultPort="64738"
mumble_installed() {
pacman -Q mumble-server &> /dev/null
}
ufw_installed() {
pacman -Q ufw &> /dev/null
}
valid_port() {
local portValue="$1"
[[ "$portValue" =~ ^[0-9]+$ ]] && (( portValue >= 1 && portValue <= 65535 ))
}
install_mumble_server() {
if mumble_installed; then
return 0
fi
if ! install_package mumble-server; then
msgbox "Failed to install mumble-server."
return 1
fi
return 0
}
set_mumble_config_value() {
local keyName="$1"
local keyValue="$2"
local tempFile=""
tempFile="$(mktemp)"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2024,SC2154
if ! sudo "${sudoFlags[@]}" awk -v keyName="$keyName" -v keyValue="$keyValue" '
BEGIN {
updated = 0
}
$0 ~ "^[;#]?[[:space:]]*" keyName "[[:space:]]*=" {
print keyName "=" keyValue
updated = 1
next
}
{
print
}
END {
if (!updated) {
print keyName "=" keyValue
}
}
' "$mumbleConfigFile" > "$tempFile"; then
rm -f "$tempFile"
msgbox "Failed to update ${keyName} in ${mumbleConfigFile}."
return 1
fi
if ! sudo "${sudoFlags[@]}" install -m 640 "$tempFile" "$mumbleConfigFile"; then
rm -f "$tempFile"
msgbox "Failed to save ${mumbleConfigFile}."
return 1
fi
rm -f "$tempFile"
return 0
}
clear_mumble_config_value() {
local keyName="$1"
set_mumble_config_value "$keyName" ""
}
read_mumble_port() {
local configuredPort=""
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if sudo "${sudoFlags[@]}" test -r "$mumbleConfigFile"; then
configuredPort="$(sudo "${sudoFlags[@]}" awk -F '=' '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*port[[:space:]]*=/ {
value = $2
sub(/[[:space:]]*#.*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
port = value
}
END {
if (port != "") {
print port
}
}
' "$mumbleConfigFile")"
fi
if valid_port "$configuredPort"; then
printf '%s\n' "$configuredPort"
else
printf '%s\n' "$mumbleDefaultPort"
fi
}
capture_welcome_message() {
local tempFile=""
local messageSize=0
if [[ "$(yesno "Would you like to create a welcome message now?")" != "Yes" ]]; then
return 1
fi
tempFile="$(mktemp)"
clear
cat <<'EOF'
Enter the Mumble welcome message below.
HTML is supported.
Press Ctrl+D on a new line when you are finished.
Leave it blank and press Ctrl+D to skip it.
EOF
cat > "$tempFile"
messageSize="$(wc -c < "$tempFile")"
if [[ "$messageSize" -eq 0 ]]; then
rm -f "$tempFile"
return 1
fi
printf '%s\n' "$tempFile"
return 0
}
install_welcome_message() {
local messageFile="$1"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -m 644 "$messageFile" "$mumbleWelcomeFile"; then
msgbox "Failed to write ${mumbleWelcomeFile}."
return 1
fi
set_mumble_config_value "welcometextfile" "$mumbleWelcomeFile"
}
set_mumble_superuser_password() {
local superuserPassword="$1"
if [[ -z "$superuserPassword" ]]; then
msgbox "The SuperUser password cannot be blank."
return 1
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! printf '%s\n' "$superuserPassword" | sudo "${sudoFlags[@]}" mumble-server -ini "$mumbleConfigFile" -readsupw; then
msgbox "Failed to set the Mumble SuperUser password."
return 1
fi
return 0
}
enable_mumble_service() {
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" systemctl enable --now mumble-server; then
msgbox "Mumble was configured, but the service failed to enable or start."
return 1
fi
return 0
}
configure_mumble_firewall() {
local mumblePort=""
if ! ufw_installed; then
return 0
fi
if [[ "$(yesno "ufw is installed. Open the default Mumble port now?")" != "Yes" ]]; then
return 0
fi
mumblePort="$(read_mumble_port)"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow "${mumblePort}/tcp"; then
msgbox "Failed to allow ${mumblePort}/tcp for Mumble."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow "${mumblePort}/udp"; then
msgbox "Failed to allow ${mumblePort}/udp for Mumble."
return 1
fi
msgbox "The default Mumble firewall rules were added."
return 0
}
install_mumble_server || return 1
superuserPassword="$(passwordbox "Enter the Mumble SuperUser password.")" || return 1
serverPassword="$(passwordbox "Enter a server password, or leave blank for no server password.")" || return 1
allowRecordingChoice="$(yesno "Allow users to record audio on this server?")"
welcomeMessageFile=""
welcomeMessageFile="$(capture_welcome_message || true)"
reset
set_mumble_superuser_password "$superuserPassword" || return 1
if [[ -n "$serverPassword" ]]; then
set_mumble_config_value "serverpassword" "$serverPassword" || return 1
else
clear_mumble_config_value "serverpassword" || return 1
fi
if [[ "$allowRecordingChoice" == "Yes" ]]; then
set_mumble_config_value "allowRecording" "true" || return 1
else
set_mumble_config_value "allowRecording" "false" || return 1
fi
if [[ -n "$welcomeMessageFile" ]]; then
install_welcome_message "$welcomeMessageFile" || {
rm -f "$welcomeMessageFile"
return 1
}
rm -f "$welcomeMessageFile"
else
clear_mumble_config_value "welcometextfile" || return 1
fi
enable_mumble_service || return 1
configure_mumble_firewall || return 1
msgbox "Basic Mumble configuration is complete. To fine tune your setup, edit ${mumbleConfigFile} as root."

501
.includes/nginx.sh Normal file
View File

@@ -0,0 +1,501 @@
#!/usr/bin/env bash
nginxConfigFile="/etc/nginx/nginx.conf"
nginxBackupFile="/etc/nginx/nginx.conf.configure-server.bak"
nginxSitesAvailable="/etc/nginx/sites-available"
nginxSitesEnabled="/etc/nginx/sites-enabled"
nginxDefaultSite="${nginxSitesAvailable}/default.conf"
nginxDefaultSiteLink="${nginxSitesEnabled}/default.conf"
clacksInfoUrl="https://www.gnuterrypratchett.com/"
nginxManagedInclude="include /etc/nginx/sites-enabled/*.conf;"
nginx_installed() {
pacman -Q nginx &> /dev/null
}
ufw_installed() {
pacman -Q ufw &> /dev/null
}
ensure_ufw() {
if ufw_installed; then
return 0
fi
if [[ "$(yesno "ufw is not installed. Install it now and continue?")" != "Yes" ]]; then
msgbox "Firewall action cancelled."
return 1
fi
if ! install_package ufw; then
msgbox "Failed to install ufw."
return 1
fi
msgbox "ufw installed."
return 0
}
install_nginx() {
local clacksHeader=""
if ! nginx_installed; then
if ! install_package nginx; then
msgbox "Failed to install nginx."
return 1
fi
fi
clacksHeader="$(prompt_clacks_header || true)"
setup_nginx_layout "$clacksHeader" || return 1
if ! test_nginx_config; then
return 1
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" systemctl enable --now nginx; then
msgbox "nginx was configured, but the service failed to enable or start."
return 1
fi
msgbox "nginx is installed and running."
return 0
}
prompt_clacks_header() {
local rawNames=""
local formattedNames=()
local nameEntry=""
local clacksHeader=""
if [[ "$(yesno "Would you like to add an optional X-Clacks-Overhead header for all nginx sites?")" != "Yes" ]]; then
return 1
fi
msgbox "X-Clacks-Overhead is a tribute header inspired by Terry Pratchett's Clacks. For more information, visit ${clacksInfoUrl}"
rawNames="$(inputbox "Enter a comma-separated list of names for the X-Clacks-Overhead header, or leave blank to skip.")" || return 1
if [[ -z "${rawNames//[[:space:]]/}" ]]; then
return 1
fi
while IFS= read -r nameEntry; do
nameEntry="${nameEntry#"${nameEntry%%[![:space:]]*}"}"
nameEntry="${nameEntry%"${nameEntry##*[![:space:]]}"}"
if [[ -n "$nameEntry" ]]; then
formattedNames+=("GNU ${nameEntry}")
fi
done < <(printf '%s\n' "$rawNames" | tr ',' '\n')
if [[ "${#formattedNames[@]}" -eq 0 ]]; then
return 1
fi
clacksHeader="$(printf '%s, ' "${formattedNames[@]}")"
clacksHeader="${clacksHeader%, }"
printf '%s\n' "$clacksHeader"
return 0
}
setup_nginx_layout() {
local clacksHeader="$1"
local generatedConfig=""
local tempConfig=""
if ! sudo "${sudoFlags[@]}" test -r "$nginxConfigFile"; then
msgbox "Unable to read ${nginxConfigFile}. Install nginx first."
return 1
fi
generatedConfig="$(generate_nginx_config "$clacksHeader")" || {
msgbox "Failed to generate nginx.conf."
return 1
}
tempConfig="$(mktemp)"
printf '%s\n' "$generatedConfig" > "$tempConfig"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -d -m 755 "$nginxSitesAvailable" "$nginxSitesEnabled"; then
rm -f "$tempConfig"
msgbox "Failed to create the nginx site directories."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" test -e "$nginxBackupFile"; then
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" cp "$nginxConfigFile" "$nginxBackupFile" 2> /dev/null || true
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -m 644 "$tempConfig" "$nginxConfigFile"; then
rm -f "$tempConfig"
msgbox "Failed to write ${nginxConfigFile}."
return 1
fi
rm -f "$tempConfig"
ensure_default_site
}
generate_nginx_config() {
local clacksHeader="$1"
sudo "${sudoFlags[@]}" awk -v clacksHeader="$clacksHeader" -v managedInclude="$nginxManagedInclude" '
BEGIN {
skippingServer = 0
braceDepth = 0
insertedSitesInclude = 0
skipManagedComment = 0
}
{
if ($0 == "# Managed by configure-server") {
skipManagedComment = 1
next
}
if ($0 ~ /^[[:space:]]*add_header X-Clacks-Overhead /) {
next
}
if ($0 ~ /^[[:space:]]*include \/etc\/nginx\/sites-enabled\/\*\.conf;$/) {
next
}
if (skippingServer) {
opens = gsub(/\{/, "{")
closes = gsub(/\}/, "}")
braceDepth += opens - closes
if (braceDepth <= 0) {
skippingServer = 0
}
next
}
if ($0 ~ /^ server \{$/) {
skippingServer = 1
braceDepth = 1
next
}
if (skipManagedComment) {
skipManagedComment = 0
}
print
if (!insertedSitesInclude && $0 ~ /^[[:space:]]*#gzip[[:space:]]+on;/) {
if (clacksHeader != "") {
print ""
print " add_header X-Clacks-Overhead \"" clacksHeader "\" always;"
}
print ""
print " " managedInclude
insertedSitesInclude = 1
}
}
' "$nginxConfigFile"
}
ensure_default_site() {
local tempSiteFile=""
if [[ -e "$nginxDefaultSite" ]]; then
if [[ ! -L "$nginxDefaultSiteLink" ]]; then
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" ln -sf "$nginxDefaultSite" "$nginxDefaultSiteLink"
fi
return 0
fi
tempSiteFile="$(mktemp)"
cat > "$tempSiteFile" <<'EOF'
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
EOF
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -m 644 "$tempSiteFile" "$nginxDefaultSite"; then
rm -f "$tempSiteFile"
msgbox "Failed to create the default nginx site."
return 1
fi
rm -f "$tempSiteFile"
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ln -sf "$nginxDefaultSite" "$nginxDefaultSiteLink"; then
msgbox "Failed to enable the default nginx site."
return 1
fi
return 0
}
create_site() {
local siteName=""
local serverNames=""
local siteRoot=""
local tempSiteFile=""
local siteConfigFile=""
local defaultIndexFile=""
if ! nginx_installed; then
msgbox "Install nginx first."
return 1
fi
siteName="$(inputbox "Enter a short site name for the config file, for example example.com.")" || return 1
if [[ -z "$siteName" || ! "$siteName" =~ ^[A-Za-z0-9._-]+$ ]]; then
msgbox "Enter a valid site name using letters, numbers, dots, dashes, or underscores."
return 1
fi
serverNames="$(inputbox "Enter the server_name value, using spaces between names." "$siteName")" || return 1
siteRoot="$(inputbox "Enter the site document root." "/srv/http/${siteName}")" || return 1
if [[ -z "$siteRoot" ]]; then
msgbox "A document root is required."
return 1
fi
siteConfigFile="${nginxSitesAvailable}/${siteName}.conf"
tempSiteFile="$(mktemp)"
cat > "$tempSiteFile" <<EOF
server {
listen 80;
server_name ${serverNames};
root ${siteRoot};
index index.html index.htm;
location / {
try_files \$uri \$uri/ =404;
}
}
EOF
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -d -m 755 "$siteRoot"; then
rm -f "$tempSiteFile"
msgbox "Failed to create ${siteRoot}."
return 1
fi
defaultIndexFile="$(mktemp)"
cat > "$defaultIndexFile" <<EOF
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${siteName}</title>
</head>
<body>
<h1>${siteName}</h1>
<p>nginx is serving ${siteName} from ${siteRoot}.</p>
</body>
</html>
EOF
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -m 644 "$tempSiteFile" "$siteConfigFile"; then
rm -f "$tempSiteFile" "$defaultIndexFile"
msgbox "Failed to write ${siteConfigFile}."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" test -e "${siteRoot}/index.html"; then
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" install -m 644 "$defaultIndexFile" "${siteRoot}/index.html" || true
fi
rm -f "$tempSiteFile" "$defaultIndexFile"
msgbox "Site ${siteName} was created in ${nginxSitesAvailable}. Enable it from the nginx menu when you are ready."
return 0
}
select_site_file() {
local searchPath="$1"
local filePattern="$2"
local selectedSite=""
local siteEntries=()
local sitePath=""
while IFS= read -r sitePath; do
[[ -n "$sitePath" ]] && siteEntries+=("$(basename "$sitePath")")
done < <(find "$searchPath" -maxdepth 1 -type "$filePattern" -name '*.conf' | sort)
if [[ "${#siteEntries[@]}" -eq 0 ]]; then
return 1
fi
selectedSite="$(menulist "${siteEntries[@]}")" || return 1
printf '%s\n' "$selectedSite"
}
enable_site() {
local siteName=""
if ! nginx_installed; then
msgbox "Install nginx first."
return 1
fi
siteName="$(select_site_file "$nginxSitesAvailable" f)" || {
msgbox "No available site configs were found."
return 1
}
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ln -sf "${nginxSitesAvailable}/${siteName}" "${nginxSitesEnabled}/${siteName}"; then
msgbox "Failed to enable ${siteName}."
return 1
fi
msgbox "${siteName} is enabled. Test and reload nginx when you are ready."
return 0
}
disable_site() {
local siteName=""
if ! nginx_installed; then
msgbox "Install nginx first."
return 1
fi
siteName="$(select_site_file "$nginxSitesEnabled" l)" || {
msgbox "No enabled sites were found."
return 1
}
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" rm -f "${nginxSitesEnabled}/${siteName}"; then
msgbox "Failed to disable ${siteName}."
return 1
fi
msgbox "${siteName} is disabled. Test and reload nginx when you are ready."
return 0
}
test_nginx_config() {
local tempFile=""
local status=0
local statusText=""
if ! nginx_installed; then
msgbox "Install nginx first."
return 1
fi
tempFile="$(mktemp)"
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
statusText="$(sudo "${sudoFlags[@]}" nginx -t 2>&1)"
status=$?
printf '%s\n' "$statusText" > "$tempFile"
textbox "$tempFile"
rm -f "$tempFile"
return "$status"
}
reload_nginx() {
if ! nginx_installed; then
msgbox "Install nginx first."
return 1
fi
if ! test_nginx_config; then
msgbox "nginx was not reloaded because the config test failed."
return 1
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" systemctl reload nginx; then
msgbox "Failed to reload nginx."
return 1
fi
msgbox "nginx reloaded successfully."
return 0
}
open_web_ports() {
ensure_ufw || return 1
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow 80/tcp; then
msgbox "Failed to open 80/tcp."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow 443/tcp; then
msgbox "Failed to open 443/tcp."
return 1
fi
msgbox "Web ports 80/tcp and 443/tcp are allowed."
return 0
}
while true; do
nginxChoice="$(menulist \
"Install nginx" \
"Create site" \
"Enable site" \
"Disable site" \
"Test config" \
"Reload nginx" \
"Open web ports" \
"Back")" || break
case "$nginxChoice" in
"Install nginx")
install_nginx
;;
"Create site")
create_site
;;
"Enable site")
enable_site
;;
"Disable site")
disable_site
;;
"Test config")
test_nginx_config
;;
"Reload nginx")
reload_nginx
;;
"Open web ports")
open_web_ports
;;
"Back")
break
;;
esac
done

373
.includes/topspeed.sh Normal file
View File

@@ -0,0 +1,373 @@
#!/usr/bin/env bash
topspeedUser="topspeed"
topspeedHome="/var/lib/topspeed"
topspeedInstallDir="${topspeedHome}/server"
topspeedServiceFile="/etc/systemd/system/topspeed.service"
topspeedServiceScript="${topspeedInstallDir}/topspeed-service.sh"
topspeedStopScript="${topspeedInstallDir}/topspeed-stop.sh"
topspeedSessionName="topspeed"
topspeedRepoApi="https://api.github.com/repos/diamondStar35/top_speed/releases"
topspeedAssetPattern='^TopSpeed\.Server-linux-arm64-Release-v-.*\.zip$'
topspeedServerPort="28630"
topspeedDiscoveryPort="28631"
package_installed() {
local packageName="$1"
pacman -Q "$packageName" &> /dev/null
}
ufw_installed() {
pacman -Q ufw &> /dev/null
}
ensure_topspeed_dependencies() {
local packagesToInstall=()
local packageName=""
for packageName in curl jq tmux unzip; do
if ! package_installed "$packageName"; then
packagesToInstall+=("$packageName")
fi
done
if [[ "${#packagesToInstall[@]}" -gt 0 ]]; then
if ! install_package "${packagesToInstall[@]}"; then
msgbox "Failed to install the required Top Speed dependencies."
return 1
fi
fi
return 0
}
fetch_topspeed_asset() {
local tempDir=""
local metadataFile=""
local assetName=""
local assetUrl=""
tempDir="$(mktemp -d)"
metadataFile="${tempDir}/topspeed-release.json"
if ! curl -fsSL -H "Accept: application/vnd.github+json" "$topspeedRepoApi" -o "$metadataFile"; then
rm -rf "$tempDir"
msgbox "Failed to fetch Top Speed release metadata from GitHub."
return 1
fi
assetName="$(jq -r --arg assetPattern "$topspeedAssetPattern" '
([.[] | .assets[] | select(.name | test($assetPattern))] | .[0].name) // empty
' "$metadataFile")"
assetUrl="$(jq -r --arg assetPattern "$topspeedAssetPattern" '
([.[] | .assets[] | select(.name | test($assetPattern))] | .[0].browser_download_url) // empty
' "$metadataFile")"
if [[ -z "$assetName" || -z "$assetUrl" || "$assetName" == "null" || "$assetUrl" == "null" ]]; then
rm -rf "$tempDir"
msgbox "Unable to find the latest Top Speed arm64 server download in GitHub release metadata."
return 1
fi
if ! curl -fL --retry 5 -o "${tempDir}/${assetName}" "$assetUrl"; then
rm -rf "$tempDir"
msgbox "Failed to download the Top Speed server archive."
return 1
fi
printf '%s\n' "${tempDir}/${assetName}"
}
ensure_topspeed_user() {
if id -u "$topspeedUser" &> /dev/null; then
return 0
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" useradd \
--system \
--home-dir "$topspeedHome" \
--create-home \
--shell /usr/bin/nologin \
"$topspeedUser"; then
msgbox "Failed to create the topspeed service user."
return 1
fi
return 0
}
stop_topspeed_service_if_present() {
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if sudo "${sudoFlags[@]}" systemctl cat topspeed.service &> /dev/null; then
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" systemctl stop topspeed.service &> /dev/null || true
fi
}
install_topspeed_server() {
local archivePath=""
local archiveDir=""
archivePath="$(fetch_topspeed_asset)" || return 1
archiveDir="$(dirname "$archivePath")"
stop_topspeed_service_if_present
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -d -o "$topspeedUser" -g "$topspeedUser" -m 755 "$topspeedInstallDir"; then
rm -rf "$archiveDir"
msgbox "Failed to create the Top Speed install directory."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" unzip -o -d "$topspeedInstallDir" "$archivePath"; then
rm -rf "$archiveDir"
msgbox "Failed to extract the Top Speed server archive."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" chown -R "${topspeedUser}:${topspeedUser}" "$topspeedHome"; then
rm -rf "$archiveDir"
msgbox "Failed to set ownership on the Top Speed files."
return 1
fi
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" find "$topspeedInstallDir" -type f -name 'TopSpeed.Server*' -exec chmod 755 {} + || true
rm -rf "$archiveDir"
return 0
}
write_topspeed_runtime_scripts() {
local tempServiceScript=""
local tempStopScript=""
tempServiceScript="$(mktemp)"
tempStopScript="$(mktemp)"
cat > "$tempServiceScript" <<EOF
#!/usr/bin/env bash
sessionName="${topspeedSessionName}"
installDir="${topspeedInstallDir}"
stopFlagPath="${topspeedHome}/.stopping"
binaryPath=""
find_topspeed_binary() {
local exactBinary=""
local fallbackBinary=""
exactBinary="\$(find "\$installDir" -type f -name 'TopSpeed.Server' | sort | head -n 1)"
if [[ -n "\$exactBinary" ]]; then
printf '%s\n' "\$exactBinary"
return 0
fi
fallbackBinary="\$(find "\$installDir" -type f -name 'TopSpeed.Server*' | sort | head -n 1)"
if [[ -n "\$fallbackBinary" ]]; then
printf '%s\n' "\$fallbackBinary"
return 0
fi
return 1
}
rm -f "\$stopFlagPath"
if tmux has-session -t "\$sessionName" &> /dev/null; then
tmux kill-session -t "\$sessionName"
fi
binaryPath="\$(find_topspeed_binary)" || exit 1
tmux new-session -d -s "\$sessionName" "\$binaryPath" || exit 1
while tmux has-session -t "\$sessionName" &> /dev/null; do
sleep 5
done
if [[ -f "\$stopFlagPath" ]]; then
rm -f "\$stopFlagPath"
exit 0
fi
exit 1
EOF
cat > "$tempStopScript" <<EOF
#!/usr/bin/env bash
sessionName="${topspeedSessionName}"
stopFlagPath="${topspeedHome}/.stopping"
touch "\$stopFlagPath"
if tmux has-session -t "\$sessionName" &> /dev/null; then
tmux send-keys -t "\$sessionName" C-c
sleep 2
if tmux has-session -t "\$sessionName" &> /dev/null; then
tmux kill-session -t "\$sessionName"
fi
fi
EOF
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -o "$topspeedUser" -g "$topspeedUser" -m 755 "$tempServiceScript" "$topspeedServiceScript"; then
rm -f "$tempServiceScript" "$tempStopScript"
msgbox "Failed to install the Top Speed runtime script."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -o "$topspeedUser" -g "$topspeedUser" -m 755 "$tempStopScript" "$topspeedStopScript"; then
rm -f "$tempServiceScript" "$tempStopScript"
msgbox "Failed to install the Top Speed stop script."
return 1
fi
rm -f "$tempServiceScript" "$tempStopScript"
return 0
}
write_topspeed_service() {
local tempServiceFile=""
tempServiceFile="$(mktemp)"
cat > "$tempServiceFile" <<EOF
[Unit]
Description=Top Speed Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${topspeedUser}
Group=${topspeedUser}
WorkingDirectory=${topspeedInstallDir}
ExecStart=${topspeedServiceScript}
ExecStop=${topspeedStopScript}
Restart=on-failure
RestartSec=5
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
EOF
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" install -m 644 "$tempServiceFile" "$topspeedServiceFile"; then
rm -f "$tempServiceFile"
msgbox "Failed to write the Top Speed systemd service file."
return 1
fi
rm -f "$tempServiceFile"
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" systemctl daemon-reload; then
msgbox "Failed to reload systemd after writing the Top Speed service."
return 1
fi
return 0
}
configure_topspeed_firewall() {
if ! ufw_installed; then
return 0
fi
if [[ "$(yesno "ufw is installed. Open the default Top Speed ports ${topspeedServerPort} and ${topspeedDiscoveryPort} now?")" != "Yes" ]]; then
return 0
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow "${topspeedServerPort}/tcp"; then
msgbox "Failed to allow the Top Speed server port ${topspeedServerPort}/tcp."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow "${topspeedServerPort}/udp"; then
msgbox "Failed to allow the Top Speed server port ${topspeedServerPort}/udp."
return 1
fi
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" ufw allow "${topspeedDiscoveryPort}/udp"; then
msgbox "Failed to allow the Top Speed discovery port ${topspeedDiscoveryPort}/udp."
return 1
fi
msgbox "The default Top Speed firewall rules were added."
return 0
}
enable_topspeed_service() {
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
if ! sudo "${sudoFlags[@]}" systemctl enable --now topspeed.service; then
msgbox "Top Speed was installed, but the service failed to enable or start."
return 1
fi
return 0
}
install_topspeed_flow() {
ensure_topspeed_dependencies || return 1
ensure_topspeed_user || return 1
install_topspeed_server || return 1
write_topspeed_runtime_scripts || return 1
write_topspeed_service || return 1
enable_topspeed_service || return 1
configure_topspeed_firewall || return 1
msgbox "Top Speed Server is installed. The service is enabled and started, and the console is available through the Top Speed Console menu entry."
return 0
}
topspeed_session_running() {
if ! id -u "$topspeedUser" &> /dev/null; then
return 1
fi
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" -u "$topspeedUser" tmux has-session -t "$topspeedSessionName" &> /dev/null
}
attach_topspeed_console() {
local attachStatus=0
if ! topspeed_session_running; then
msgbox "Top Speed is not running."
return 1
fi
clear
echo "Attaching to the Top Speed console. Detach with Ctrl-b d."
# `sudoFlags` is initialized by the main launcher before sourcing this file.
# shellcheck disable=SC2154
sudo "${sudoFlags[@]}" -u "$topspeedUser" tmux attach-session -t "$topspeedSessionName"
attachStatus=$?
reset
return "$attachStatus"
}
case "${1:-install}" in
install)
install_topspeed_flow
;;
console)
attach_topspeed_console
;;
*)
msgbox "Unknown Top Speed action: ${1}"
return 1
;;
esac

59
.includes/ui.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
export DIALOGOPTS='--insecure --no-lines --visit-items'
inputbox() {
local promptText="$1"
local initialValue="${2:-}"
dialog --backtitle "Configure Server" \
--clear \
--inputbox "$promptText" 0 0 "$initialValue" --stdout
}
passwordbox() {
local promptText="$1"
local initialValue="${2:-}"
dialog --backtitle "Configure Server" \
--clear \
--passwordbox "$promptText" 0 0 "$initialValue" --stdout
}
msgbox() {
dialog --backtitle "Configure Server" \
--clear \
--msgbox "$*" 10 72
}
yesno() {
if dialog --backtitle "Configure Server" \
--clear \
--yesno "$*" 10 80 --stdout; then
echo "Yes"
else
echo "No"
fi
}
menulist() {
local menuList=()
local menuItem=""
for menuItem in "$@"; do
menuList+=("$menuItem" "$menuItem")
done
dialog --backtitle "Configure Server" \
--clear \
--no-tags \
--menu "Please select an option" 0 0 0 "${menuList[@]}" --stdout
}
textbox() {
local filePath="$1"
dialog --backtitle "Configure Server" \
--clear \
--textbox "$filePath" 0 0
}