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
}

68
configure-server.sh Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Configure Server
if [[ "$(whoami)" == "root" ]]; then
echo "Please run configure-server as your user, not as root."
exit 0
fi
logFile="/tmp/configure-server.log"
filter_ansi() {
sed -r -e 's/\x1B\[[0-9;]*[A-Za-z]//g' \
-e 's/\x1B[()#][0-9A-Za-z]//g' \
-e 's/\x1B[><=]//g' \
-e 's/\x0F//g' \
-e 's/\x1B\[?[0-9;]*[A-Za-z]//g' \
-e 's/\x1B\][0-9]*;[^\x07]*\x07//g' \
-e 's/\x1B\][0-9]*;[^\x1B]*\x1B\\//g' \
-e 's/\x08//g' \
-e 's/\r//g' \
-e 's/\x1B\[\?1049[hl]//g' \
-e 's/\x1B\[\?1[hl]//g' \
-e 's/\x1B\[\?1000[hl]//g' \
-e '/^[[:space:]]*$/d' \
-e ':a;N;$!ba;s/\n{2,}/\n/g'
}
exec > >(stdbuf -oL tee >(filter_ansi >> "$logFile")) 2> >(stdbuf -oL tee >(filter_ansi >> "$logFile") >&2)
unset sudoFlags
if [[ -x /etc/audibleprompt.sh ]]; then
export SUDO_ASKPASS=/etc/audibleprompt.sh
export sudoFlags=("-A")
fi
source .includes/functions.sh
source .includes/ui.sh
while true; do
choice="$(menulist "Firewall" "MiniDLNA" "Mumble Server" "Nginx" "Top Speed Server" "Top Speed Console" "Copyparty" "Exit")" || break
case "$choice" in
"Firewall")
source .includes/firewall.sh
;;
"MiniDLNA")
source .includes/minidlna.sh
;;
"Mumble Server")
source .includes/mumble.sh
;;
"Nginx")
source .includes/nginx.sh
;;
"Top Speed Server")
source .includes/topspeed.sh install
;;
"Top Speed Console")
source .includes/topspeed.sh console
;;
"Copyparty")
source .includes/copyparty.sh
;;
"Exit")
break
;;
esac
done
exit 0

View File

@@ -1,101 +0,0 @@
# Configure-Server Cleanup And Firewall Design
## Goal
Turn this repository from a copied `configure-stormux` launcher into a minimal server-focused entrypoint that starts where the conversion process leaves off. For the first pass, the top-level interface should expose only firewall management and exit.
## Current State
`configure-server.sh` is still structured like the workstation-oriented `configure-stormux` script. Most menu entries and helper includes target desktop, accessibility, gaming, or first-boot setup behavior that should already be complete before this tool is run. The only server-specific behavior is `.includes/convert-to-server.sh`, which is a destructive one-shot conversion script rather than an ongoing management interface.
## Scope
This change covers:
- Reworking `configure-server.sh` into a server-specific launcher.
- Adding a dedicated firewall submenu implemented in a new `.includes/firewall.sh`.
- Removing obsolete `.includes` scripts that are no longer used by the server launcher.
- Updating touched user-facing strings from `configure-stormux` or `Stormux` phrasing where it materially improves correctness for this repo.
This change does not cover:
- User management.
- Nginx or other service setup.
- Advanced firewall rule editing beyond basic allow/status/enable/disable flows.
- Reusing `.includes/convert-to-server.sh` from the main menu.
## Top-Level Interface
The top-level menu in `configure-server.sh` will contain exactly:
- `Firewall`
- `Exit`
Selecting `Firewall` will source `.includes/firewall.sh`. Selecting `Exit` or cancelling the menu will terminate the script cleanly.
## Firewall Submenu
The firewall submenu is intentionally narrow and dialog-driven. It will provide:
- `Install ufw`
- `Enable firewall`
- `Disable firewall`
- `Allow SSH`
- `Open custom port`
- `View status`
- `Back`
Behavior details:
- `Install ufw` installs the package only if it is not already present.
- `Enable firewall` first ensures the active SSH port is explicitly allowed before enabling `ufw`.
- `Disable firewall` runs `ufw disable`.
- `Allow SSH` allows the current SSH port based on the server conversion drop-in file if available. If the port cannot be determined confidently, the user is prompted to enter it manually. If the port still cannot be confirmed, the action is cancelled without changing firewall rules.
- `Open custom port` prompts for either a bare port such as `80` or an explicit `port/protocol` string such as `443/tcp` and validates the input before calling `ufw allow`.
- `View status` shows `ufw status verbose` in a dialog-friendly text view.
## SSH Port Safety
Preventing SSH lockout takes priority over convenience.
- The primary source of truth for the SSH port is `/etc/ssh/sshd_config.d/99-stormux-server.conf`, which is written by the earlier server conversion step.
- Firewall actions that could affect remote access must check that file first and extract the configured `Port` value when present.
- If the port file is missing, unreadable, malformed, or otherwise ambiguous, the script must not guess. It should prompt the user to enter the SSH port explicitly.
- If the user cancels the prompt or enters an invalid value, the script must cancel the firewall-enabling action rather than continue.
- `Enable firewall` should allow the resolved SSH port before running `systemctl enable --now ufw` or `ufw enable`.
- `Allow SSH` should reuse the same resolution logic so behavior is consistent.
## File Boundaries
- `configure-server.sh`
- Owns startup checks, logging, shared include loading, and the top-level menu loop.
- `.includes/firewall.sh`
- Owns the firewall submenu and firewall-specific helper functions.
- `.includes/functions.sh`
- Continues to own shared helpers still used by the launcher or firewall flow.
- `.includes/ui.sh`
- Continues to own dialog wrapper functions.
Obsolete include files that are no longer referenced by the top-level server launcher will be deleted as part of this cleanup.
## Cleanup Rules
- Remove menu options and code paths tied to desktop setup, screen readers, gaming, IRC help, GUI installs, EEPROM updates, timezone setup, first-user renaming, or the old conversion step.
- Delete the corresponding unused `.includes` scripts from the repository rather than leaving dead files behind.
- Keep changes scoped to this server cleanup and firewall addition; do not add placeholders for future subsystems.
## Error Handling
- Missing `ufw` will be reported clearly for actions that require it, with `Install ufw` available as the explicit fix path.
- If the SSH port cannot be confirmed, `Enable firewall` must abort with a clear message rather than risk locking the user out.
- Invalid custom port input will show a message and return to the firewall menu without applying a rule.
- Commands that require privilege will continue using the existing `sudoFlags` handling.
## Verification
Verification for this change will be limited to the narrowest relevant shell checks:
- `bash -n` on each edited shell script.
- `shellcheck` on each edited shell script.
No runtime firewall manipulation will be claimed as verified unless it is actually executed in this environment.

View File

@@ -1,98 +0,0 @@
# Top Speed Server Design
## Goal
Add a server-style Top Speed setup flow that installs the latest arm64 server release dynamically, runs it as an unprivileged `topspeed` user from a standard server layout, manages it with systemd, and provides a console attach option through `tmux`.
## Scope
This change covers:
- Adding `Top Speed Server` and `Top Speed Console` menu entries.
- Installing required packages for download, archive extraction, JSON parsing, and console management.
- Reusing the existing GitHub release metadata pattern from `linux-game-manager` to locate the latest arm64 server download without hard-coding a versioned URL.
- Creating a locked-down `topspeed` service account with no password and no sudo access.
- Installing Top Speed under `/var/lib/topspeed`.
- Writing and enabling a systemd service that launches the server in a named tmux session.
- Offering firewall setup for the documented default Top Speed ports when `ufw` is present.
This change does not cover:
- Custom Top Speed port configuration.
- Automatic editing of Top Speed configuration files.
- Disabling or changing the server's own automatic update behavior.
- Automatic upgrade workflows after initial install.
## Layout
- Top Speed service account:
- user: `topspeed`
- home: `/var/lib/topspeed`
- shell: non-login shell
- Installation root:
- `/var/lib/topspeed/server`
- Temporary downloads:
- handled in a temporary directory and removed after extraction
- Systemd unit:
- `/etc/systemd/system/topspeed.service`
This keeps the install server-like and predictable for experienced admins while staying simple enough for new users to inspect.
## Download Strategy
The installer will query the GitHub releases API and select the newest arm64 server asset using `jq`, following the same basic approach already used for Top Speed in `linux-game-manager`.
- Repository: `diamondStar35/top_speed`
- Release metadata endpoint: GitHub releases API
- Asset selection:
- match the arm64 server archive name pattern
- reject missing or ambiguous matches
If `jq` is missing, the script will install it first. If `curl` is missing, the script will install it first as well.
## Service Model
The Top Speed server runs in the foreground and accepts console input, so the systemd service should not run the binary directly. Instead:
- systemd starts a named `tmux` session as the `topspeed` user
- the Top Speed server runs inside that `tmux` session
- the service restarts on failure
This provides both automatic startup and an attachable live console.
## Console Access
The `Top Speed Console` menu entry will:
- verify that the `topspeed` tmux session exists
- temporarily leave the dialog UI
- attach to the tmux session
- return to the menu after the user detaches
If the service is not installed or the tmux session is not active, the script should show a clear message instead of failing noisily.
## Firewall Rules
If `ufw` is installed, the setup flow will offer to add rules for the documented default Top Speed ports:
- server port `28630`
- discovery port `28631`
For this helper, the firewall setup will assume defaults only. If a user later customizes Top Speed ports, they are responsible for matching firewall rules manually.
## Error Handling
- If the GitHub metadata query fails, show a clear error and stop.
- If no matching arm64 server asset is found, show a clear error and stop.
- If user creation, directory creation, extraction, or service installation fails, stop with a clear message.
- If firewall setup is declined or `ufw` is absent, continue without treating that as fatal.
- If console attach is requested while the session is not running, show a message and return to the menu.
## Verification
Verification for this change will be limited in this environment to:
- `bash -n` on edited shell scripts
- `shellcheck` on edited shell scripts
The Top Speed service itself, tmux session behavior, download flow, and firewall behavior will not be claimed as runtime-verified here because this machine is not the deployment target.