Initial commit.
This commit is contained in:
454
.includes/copyparty.sh
Normal file
454
.includes/copyparty.sh
Normal 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
298
.includes/firewall.sh
Normal 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
9
.includes/functions.sh
Normal 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
174
.includes/minidlna.sh
Normal 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
242
.includes/mumble.sh
Normal 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
501
.includes/nginx.sh
Normal 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
373
.includes/topspeed.sh
Normal 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
59
.includes/ui.sh
Normal 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
68
configure-server.sh
Normal 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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user