#!/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://xclacksoverhead.org/home/about" 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 } enable_nginx_service() { # `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 return 0 } web_ports_open() { local statusLine="" local hasHttp="No" local hasHttps="No" if ! ufw_installed; then return 1 fi while IFS= read -r statusLine; do if [[ "$statusLine" =~ ^[[:space:]]*80/tcp([[:space:]]+\(v6\))?[[:space:]]+ALLOW([[:space:]]+IN)?[[:space:]]+Anywhere([[:space:]]+\(v6\))?[[:space:]]*$ ]]; then hasHttp="Yes" elif [[ "$statusLine" =~ ^[[:space:]]*443/tcp([[:space:]]+\(v6\))?[[:space:]]+ALLOW([[:space:]]+IN)?[[:space:]]+Anywhere([[:space:]]+\(v6\))?[[:space:]]*$ ]]; then hasHttps="Yes" fi done < <( # `sudoFlags` is initialized by the main launcher before sourcing this file. # shellcheck disable=SC2154 sudo "${sudoFlags[@]}" ufw status 2>&1 ) [[ "$hasHttp" == "Yes" && "$hasHttps" == "Yes" ]] } offer_open_web_ports() { if ! ufw_installed; then return 0 fi if web_ports_open; then return 0 fi if [[ "$(yesno "ufw is installed. Open 80/tcp and 443/tcp for nginx now?")" != "Yes" ]]; then return 0 fi open_web_ports } ensure_nginx() { local clacksHeader="" if nginx_installed; then return 0 fi if [[ "$(yesno "nginx is not installed. Install it now and continue?")" != "Yes" ]]; then msgbox "nginx action cancelled." return 1 fi if ! install_package nginx; then msgbox "Failed to install nginx." return 1 fi clacksHeader="$(prompt_clacks_header || true)" setup_nginx_layout "$clacksHeader" || return 1 if ! test_nginx_config; then return 1 fi if ! enable_nginx_service; then return 1 fi offer_open_web_ports || return 1 msgbox "nginx is installed and running." return 0 } prompt_clacks_header() { local rawNames="" local formattedNames=() local nameEntry="" local clacksHeader="" msgbox "X-Clacks-Overhead is an optional tribute header inspired by Terry Pratchett's Clacks. Websites use it to remember names with values such as GNU Terry Pratchett. For more information, please see: ${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="" ensure_nginx || return 1 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" < "$defaultIndexFile" < ${siteName}

${siteName}

nginx is serving ${siteName} from ${siteRoot}.

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="" ensure_nginx || return 1 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="" ensure_nginx || return 1 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="" ensure_nginx || return 1 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() { ensure_nginx || return 1 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 } close_web_ports() { ensure_ufw || return 1 # `sudoFlags` is initialized by the main launcher before sourcing this file. # shellcheck disable=SC2154 if ! sudo "${sudoFlags[@]}" ufw delete allow 80/tcp; then msgbox "Failed to close 80/tcp." return 1 fi # shellcheck disable=SC2154 if ! sudo "${sudoFlags[@]}" ufw delete allow 443/tcp; then msgbox "Failed to close 443/tcp." return 1 fi msgbox "Web ports 80/tcp and 443/tcp were removed." return 0 } web_ports_menu_label() { if web_ports_open; then printf '%s\n' "Close web ports" else printf '%s\n' "Open web ports" fi } while true; do webPortsChoice="$(web_ports_menu_label)" nginxChoice="$(menulist \ "Create site" \ "Enable site" \ "Disable site" \ "Test config" \ "Reload nginx" \ "$webPortsChoice" \ "Back")" || break case "$nginxChoice" in "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 ;; "Close web ports") close_web_ports ;; "Back") break ;; esac done