fix: backport unraid/webgui#2269 rc.nginx update (#1436)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced remote access detection for nginx using updated configuration
and plugin integration.
- Introduced a comprehensive Nginx management script tailored for Unraid
OS with SSL handling, server lifecycle controls, and dynamic
configuration.
- **Bug Fixes**
- Improved script robustness by fixing shell loop syntax for proper
handling of array keys.
- **Chores**
  - Added compatibility patches for Unraid versions before 7.2.0.
  - Updated test suites to include new nginx script modifications.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Pujit Mehrotra
2025-06-25 12:58:28 -04:00
committed by GitHub
parent 5ba4479663
commit a7ef06ea25
12 changed files with 1989 additions and 8 deletions

View File

@@ -0,0 +1,891 @@
#!/bin/bash
#
# script: rc.nginx
#
# Nginx daemon control script.
# Written for Slackware Linux by Cherife Li <cherife-#-dotimes.com>.
# LimeTech - modified for Unraid OS
# Bergware - modified for Unraid OS, May 2025
# reference:
# LANNAME 'tower'
# LANMDNS 'tower.local'
# LANFQDN 'lan-ip.hash.myunraid.net' (wildcard cert)
# WANFQDN 'wan-ip.hash.myunraid.net' (wildcard cert)
# WG0FQDN 'wg0-ip.hash.myunraid.net' (wildcard cert)
DAEMON="Nginx server daemon"
CALLER="nginx"
NGINX="/usr/sbin/nginx"
TS="/usr/local/sbin/tailscale"
PID="/var/run/nginx.pid"
SSL="/boot/config/ssl"
CONF="/etc/nginx/nginx.conf"
IDENT="/boot/config/ident.cfg"
SERVERS="/etc/nginx/conf.d/servers.conf"
LOCATIONS="/etc/nginx/conf.d/locations.conf"
INI="/var/local/emhttp/nginx.ini.new"
CERTPATH="$SSL/certs/certificate_bundle.pem"
TSCERTPATH="$SSL/certs/ts_bundle.pem"
MYSERVERS="/boot/config/plugins/dynamix.my.servers/myservers.cfg"
DEFAULTS="/etc/default/nginx"
SYSTEM="/sys/class/net"
SYSLOG="/var/log/syslog"
# Load defaults
# Defines NGINX_CUSTOMFA for custom Content-Security-Policy frame-ancestors url
[[ -r $DEFAULTS ]] && . $DEFAULTS
# hold server names
SERVER_NAMES=()
# read Unraid settings
[[ -r $IDENT ]] && . <(fromdos <$IDENT)
# preset default values
[[ -z $START_PAGE ]] && START_PAGE=Main
[[ -z $PORT ]] && PORT=80
[[ -z $PORTSSL ]] && PORTSSL=443
[[ -z $USE_SSL ]] && USE_SSL=no
[[ $PORTSSL != 443 ]] && PORTSSL_URL=":$PORTSSL"
[[ $PORT != 80 ]] && PORT_URL=":$PORT"
# delete legacy unraid.net certificate
if [[ -f $CERTPATH ]]; then
TMPCERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
[[ $TMPCERTNAME == *\.unraid\.net ]] && rm $CERTPATH
fi
# if USE_SSL="auto" and no uploaded cert, treat like USE_SSL="no"
[[ $USE_SSL == auto && ! -f $CERTPATH ]] && USE_SSL=no
# override default page if no regkey
if ! find /boot/config/*.key &>/dev/null; then
START_PAGE="Tools/Registration"
fi
# run & log functions
. /etc/rc.d/rc.runlog
# library functions
. /etc/rc.d/rc.library.source
fqdn(){
echo ${CERTNAME/'*'/${1//[.:]/-}}
}
# create listening ports
listen(){
T=' '
if check && [[ $1 == lo ]]; then
if [[ $IPV4 == yes ]]; then
echo "${T}listen 127.0.0.1:$PORT; # lo"
echo "${T}listen 127.0.0.1:$PORTSSL; # lo"
fi
if [[ $IPV6 == yes ]]; then
echo "${T}listen [::1]:$PORT; # lo"
echo "${T}listen [::1]:$PORTSSL; # lo"
fi
elif [[ -n $BIND ]]; then
for ADDR in $BIND; do
[[ $(ipv $ADDR) == 4 ]] && echo "${T}listen $ADDR:$*; # $(show $ADDR)"
[[ $(ipv $ADDR) == 6 ]] && echo "${T}listen [$ADDR]:$*; # $(show $ADDR)"
done
else
# default listen on any interface with ipv4 protocol
echo "${T}listen $*;"
fi
}
# create redirect server blocks
redirect(){
T=' '
if check && [[ -n $BIND ]]; then
URL=$1
TAG=$2
shift 2
case $URL in
'host')
echo "server {"
for ADDR in $BIND; do
HOST=
[[ $(ipv $ADDR) == 4 ]] && HOST="$ADDR"
[[ $(ipv $ADDR) == 6 ]] && HOST="[$ADDR]"
[[ -n $HOST ]] && echo "${T}listen $HOST:$*; # $(show $ADDR)"
done
echo "${T}add_header Content-Security-Policy \"frame-ancestors 'self' $NGINX_CUSTOMFA\";"
echo "${T}return 302 https://\$host:$PORTSSL\$request_uri;"
echo "}"
;;
'fqdn')
for ADDR in $BIND; do
HOST=
[[ $TAG == 4 && $(ipv $ADDR) == 4 ]] && HOST="$ADDR"
[[ $TAG == 6 && $(ipv $ADDR) == 6 ]] && HOST="[$ADDR]"
if [[ -n $HOST ]]; then
echo "server {"
echo "${T}listen $HOST:$*; # $(show $ADDR)"
echo "${T}add_header Content-Security-Policy \"frame-ancestors 'self' $NGINX_CUSTOMFA\";"
echo "${T}return 302 https://$(fqdn $ADDR)$PORTSSL_URL\$request_uri;"
echo "}"
fi
done
;;
esac
fi
}
# build our servers
# pay attention to escaping
build_servers(){
cat <<- 'EOF' >$SERVERS
#
# Listen on local socket for nchan publishers
#
server {
listen unix:/var/run/nginx.socket default_server;
location ~ /pub/(.*)$ {
nchan_publisher;
nchan_channel_id "$1";
nchan_message_buffer_length $arg_buffer_length;
nchan_message_timeout 0;
}
location ~ /nchan_stub_status$ {
nchan_stub_status;
}
}
EOF
cat <<- EOF >>$SERVERS
#
# Always accept http requests from localhost
# ex: http://localhost
# ex: http://127.0.0.1
# ex: http://[::1]
#
server {
$(listen lo)
#
add_header Content-Security-Policy "frame-ancestors 'self' $NGINX_CUSTOMFA";
include /etc/nginx/conf.d/locations.conf;
}
EOF
if [[ $USE_SSL == no ]]; then
cat <<- EOF >>$SERVERS
#
# Port settings for http protocol
# ex: http://tower (IP address resolved via NetBIOS)
# ex: http://tower.local (IP address resolved via mDNS)
# ex: http://192.168.1.100
# ex: http://[::ffff:192.168.1.100]
#
server {
$(listen $PORT default_server)
#
add_header Content-Security-Policy "frame-ancestors 'self' $NGINX_CUSTOMFA";
location ~ /wsproxy/$PORT/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
EOF
elif [[ $USE_SSL == yes ]]; then
cat <<- EOF >>$SERVERS
#
# Port settings for https protocol (self-signed cert)
# ex: https://tower.local
#
server {
$(listen $PORTSSL ssl default_server)
http2 on;
add_header Content-Security-Policy "frame-ancestors 'self' $SELFCERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $SELFCERTPATH;
ssl_certificate_key $SELFCERTPATH;
ssl_trusted_certificate $SELFCERTPATH;
#
# OCSP stapling
ssl_stapling $SELFCERTSTAPLE;
ssl_stapling_verify $SELFCERTSTAPLE;
#
location ~ /wsproxy/$PORTSSL/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
#
# Redirect http requests to https
# ex: http://tower.local -> https://tower.local
#
$(redirect host 0 $PORT default_server)
EOF
elif [[ $USE_SSL == auto ]]; then
if [[ -n $LANFQDN ]]; then
cat <<- EOF >>$SERVERS
#
# Redirect http requests to https
# ex: http://tower.local -> https://lan-ip.hash.myunraid.net
# ex: http://192.168.1.100 -> https://lan-ip.hash.myunraid.net
#
$(redirect fqdn 4 $PORT default_server)
EOF
fi
if [[ -n $LANFQDN6 ]]; then
cat <<- EOF >>$SERVERS
#
# Redirect http requests to https
# ex: http://[::ffff:192.168.1.100] -> https://lan-ip.hash.myunraid.net
#
$(redirect fqdn 6 $PORT default_server)
EOF
fi
cat <<- EOF >>$SERVERS
#
# Return 404 (Not Found) as default ssl action, using self-signed cert
#
server {
$(listen $PORTSSL ssl default_server)
http2 on;
add_header Content-Security-Policy "frame-ancestors 'self' $SELFCERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $SELFCERTPATH;
ssl_certificate_key $SELFCERTPATH;
ssl_trusted_certificate $SELFCERTPATH;
#
# OCSP stapling
ssl_stapling $SELFCERTSTAPLE;
ssl_stapling_verify $SELFCERTSTAPLE;
return 404;
}
EOF
fi
if [[ -f $CERTPATH ]]; then
if [[ $USE_SSL == no ]]; then
cat <<- EOF >>$SERVERS
#
# Return 404 (Not Found) as default ssl action
#
server {
$(listen $PORTSSL ssl default_server)
http2 on;
add_header Content-Security-Policy "frame-ancestors 'self' $SELFCERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $SELFCERTPATH;
ssl_certificate_key $SELFCERTPATH;
ssl_trusted_certificate $SELFCERTPATH;
#
# OCSP stapling
ssl_stapling $SELFCERTSTAPLE;
ssl_stapling_verify $SELFCERTSTAPLE;
return 404;
}
EOF
fi
if [[ -n $LANFQDN || -n $LANFQDN6 ]]; then
cat <<- EOF >>$SERVERS
#
# Port settings for https using CA-signed cert
# ex: https://lan-ip.hash.myunraid.net
#
server {
$(listen $PORTSSL ssl)
http2 on;
server_name ${SERVER_NAMES[@]};
add_header Content-Security-Policy "frame-ancestors 'self' $CERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $CERTPATH;
ssl_certificate_key $CERTPATH;
ssl_trusted_certificate $CERTPATH;
#
# OCSP stapling
ssl_stapling $CERTSTAPLE;
ssl_stapling_verify $CERTSTAPLE;
#
location ~ /wsproxy/$PORTSSL/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
EOF
fi
fi
if [[ -n $TSFQDN ]]; then
cat <<- EOF >>$SERVERS
#
# Redirect Tailscale http requests to https
# ex: http://tower.magicDNS.ts.net -> https://tower.magicDNS.ts.net
#
server {
$(listen $PORT)
server_name $TSFQDN;
return 302 https://$TSFQDN$PORTSSL_URL$request_uri;
}
#
# Port settings for https using Tailscale cert
# ex: https://tower.magicDNS.ts.net
#
server {
$(listen $PORTSSL ssl http2)
server_name $TSFQDN;
add_header Content-Security-Policy "frame-ancestors 'self' $TSFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $TSCERTPATH;
ssl_certificate_key $TSCERTPATH;
ssl_trusted_certificate $TSCERTPATH;
#
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
#
location ~ /wsproxy/$PORTSSL/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
EOF
fi
}
# build our locations
# pay attention to escaping
build_locations(){
cat <<- EOF >$LOCATIONS
#
# Default start page
#
location = / {
return 302 \$scheme://\$http_host/$START_PAGE;
}
EOF
cat <<- 'EOF' >>$LOCATIONS
#
# Redirect to login page for authentication
#
location /login {
allow all;
limit_req zone=authlimit burst=20 nodelay;
try_files /login.php =404;
include fastcgi_params;
}
location /logout {
allow all;
try_files /login.php =404;
include fastcgi_params;
}
#
# Redirect to login page on failed authentication (401)
#
error_page 401 @401;
location @401 {
return 302 $scheme://$http_host/login;
}
#
# deny access to any hidden file (beginning with a .period)
#
location ~ /\. {
return 404;
}
#
# page files handled by template.php
#
location ~^/[A-Z].* {
try_files $uri /webGui/template.php$is_args$args;
}
#
# nchan subscriber endpoint
#
location ~ /sub/(.*)$ {
nchan_subscriber;
nchan_subscriber_timeout 0;
# nchan_authorize_request <url here>
nchan_channel_id "$1";
nchan_channel_id_split_delimiter ",";
}
location /nchan_stub_status {
nchan_stub_status;
}
#
# my servers proxy
#
location /graphql {
allow all;
error_log /dev/null crit;
proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
proxy_intercept_errors on;
error_page 502 = @graph502;
}
location @graph502 {
default_type application/json;
return 200 '{"errors":[{"error":{"name":"InternalError","message":"Graphql is offline."}}]}';
}
#
# websocket proxy
#
location ~ /wsproxy/(.*)$ {
proxy_read_timeout 3600;
proxy_pass http://127.0.0.1:$1;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
#
# add Cache-Control headers to novnc
#
location ~ /plugins\/dynamix.vm.manager\/novnc/(.*)$ {
gzip on;
gzip_disable "MSIE [1-6]\.";
gzip_types text/css application/javascript text/javascript application/x-javascript;
add_header Cache-Control no-cache;
}
#
# pass PHP scripts to FastCGI server listening on unix:/var/run/php-fpm.sock
#
location ~ ^(.+\.php)(.*)$ {
fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
}
#
# enable compression of JS/CSS/WOFF files
# if version tag on querystring, tell browser to cache indefinitely
#
location ~ \.(js|css|woff)$ {
gzip on;
gzip_disable "MSIE [1-6]\.";
gzip_types text/css application/javascript text/javascript application/x-javascript application/font-woff font-woff;
if ( $args ~ "v=" ) {
expires max;
}
}
#
# robots.txt available without authentication
#
location = /robots.txt {
add_header Access-Control-Allow-Origin *; #robots.txt any origin
allow all;
}
#
# redirect.htm available without authentication
#
location = /redirect {
rewrite ^ /redirect.htm break;
allow all;
}
#
# proxy update.htm and logging.htm scripts to emhttpd listening on local socket
#
location = /update.htm {
keepalive_timeout 0;
proxy_read_timeout 180; # 3 minutes
proxy_pass http://unix:/var/run/emhttpd.socket:/update.htm;
}
location = /logging.htm {
proxy_read_timeout 864000; # 10 days(!)
proxy_pass http://unix:/var/run/emhttpd.socket:/logging.htm;
}
#
# proxy webterminal to ttyd server listening on unix:/var/run/<tag>.sock
#
location ~ /webterminal/(.*)/(.*)$ {
proxy_read_timeout 864000; # 10 days(!)
proxy_pass http://unix:/var/run/$1.sock:/$2;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location = /webterminal/auth_token.js {
return 204;
}
#
# proxy logterminal to ttyd server listening on unix:/var/tmp/<tag>.sock
#
location ~ /logterminal/(.*)/(.*)$ {
proxy_read_timeout 864000; # 10 days(!)
proxy_pass http://unix:/var/tmp/$1.sock:/$2;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
EOF
}
# check if certificate common name or any alternative name matches LANMDNS
acceptable_selfcert(){
local CN
for CN in $(openssl x509 -noout -subject -nameopt multiline -in $SELFCERTPATH | sed -n 's/ *commonName *= //p' ;
openssl x509 -noout -ext subjectAltName -in $SELFCERTPATH | grep -Eo "DNS:[a-zA-Z 0-9.*-]*" | sed "s/DNS://g"); do
CN=${CN/\*/$LANNAME} # support wildcard custom certs
[[ ${CN,,} = ${LANMDNS,,} ]] && return 0
done
return 1
}
build_ssl(){
mkdir -p $SSL/certs
if [[ ! -f $SSL/dhparam.pem ]]; then
# regenerate dhparam file
# use -dsaparam per: https://security.stackexchange.com/questions/95178/diffie-hellman-parameters-still-calculating-after-24-hours
echo "Regenerating dhparam..."
openssl dhparam -dsaparam -out $SSL/dhparam.pem 2048 &>/dev/null
fi
ln -sf $SSL/dhparam.pem /etc/nginx/dhparam.pem
LANNAME=$(hostname)
LANMDNS=${LANNAME}${LOCAL_TLD:+.$LOCAL_TLD}
# fetch LAN IP address (read management interface eth0)
[[ -e $SYSTEM/bond0 ]] && DEV=bond0 || DEV=eth0
[[ -e $SYSTEM/br0 ]] && DEV=br0
LANIP=$(ip -4 -br addr show scope global primary dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
LANIP6=$(ip -6 -br addr show scope global primary -deprecated dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
# try wireless connection if no IP address on interface eth0
[[ -z $LANIP && -e $SYSTEM/wlan0 ]] && LANIP=$(ip -4 -br addr show scope global primary dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
[[ -z $LANIP6 && -e $SYSTEM/wlan0 ]] && LANIP6=$(ip -6 -br addr show scope global primary -deprecated dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
# regenerate self-signed cert if local TLD changes */
SELFCERTPATH=$SSL/certs/${LANNAME}_unraid_bundle.pem
[[ -f $SELFCERTPATH ]] && ! acceptable_selfcert && rm -f $SELFCERTPATH
if [[ ! -f $SELFCERTPATH ]]; then
# regenerate private key and certificate
echo "Regenerating private key and certificate..."
openssl_subject="/O=Self-signed/OU=Unraid/CN=$LANMDNS"
openssl_altname="DNS:$LANMDNS"
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -sha512 -keyout /tmp/key.pem -out /tmp/cert.pem -subj "$openssl_subject" -extensions SAN -config <(cat /etc/ssl/openssl.cnf; printf "[SAN]\nsubjectAltName=${openssl_altname}") &>/dev/null
cat /tmp/cert.pem /tmp/key.pem >$SELFCERTPATH
rm -f /tmp/cert.pem /tmp/key.pem
fi
# determine if OCSP stapling should be enabled for this cert
[[ -n $(openssl x509 -noout -ocsp_uri -in "$SELFCERTPATH") ]] && SELFCERTSTAPLE=on || SELFCERTSTAPLE=off
# define CSP frame-ancestors for the self-signed cert
[[ -n $LOCAL_TLD ]] && [[ "$LOCAL_TLD" != "local" ]] && SELFCERTFA="https://*.$LOCAL_TLD/"
# handle Certificate Authority signed cert if present
if [[ -f $CERTPATH ]]; then
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
CERTFA="https://*.${CERTNAME#*.}/"
# check if Remote Access is enabled and fetch WANIP
if [[ -L /usr/local/sbin/unraid-api ]] && grep -qs 'wanaccess="yes"' $MYSERVERS && ! grep -qs 'username=""' $MYSERVERS; then
WANACCESS=yes
WANIP=$(curl https://wanip4.unraid.net/ 2>/dev/null)
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
# wildcard LE certificate
# add Unraid Connect to CSP frame-ancestors for a myunraid.net cert
CERTFA+=" https://connect.myunraid.net/"
[[ -n $LANIP ]] && LANFQDN=$(fqdn $LANIP) SERVER_NAMES+=($LANFQDN)
[[ -n $LANIP6 ]] && LANFQDN6=$(fqdn $LANIP6) SERVER_NAMES+=($LANFQDN6)
# check if remote access enabled
if [[ -n $WANACCESS ]]; then
[[ -n $WANIP ]] && WANFQDN=$(fqdn $WANIP) SERVER_NAMES+=($WANFQDN)
[[ -n $WANIP6 ]] && WANFQDN6=$(fqdn $WANIP6) SERVER_NAMES+=($WANFQDN6)
fi
if check; then
# add included interfaces
declare -A NET_FQDN NET_FQDN6
for ADDR in $BIND; do
# convert IP to name
NET=$(show $ADDR)
# skip invalid interface, LAN interface and WG VPN tunneled interfaces
[[ -z $NET || $(show $LANIP) == $NET || (${NET:0:2} == wg && $(scan TYPE:1 $WIREGUARD/$NET.cfg) -ge 7) ]] && continue
[[ $(ipv $ADDR) == 4 ]] && NET_FQDN[$NET]=$(fqdn $ADDR) || NET_FQDN6[$NET]=$(fqdn $ADDR)
SERVER_NAMES+=($(fqdn $ADDR))
done
fi
else
# custom certificate, this would be better as SELFCERTPATH
LANFQDN=${CERTNAME/\*/$LANNAME} # support wildcard custom certs
SERVER_NAMES+=($LANFQDN)
fi
# determine if OCSP stapling should be enabled for this cert
[[ -n $(openssl x509 -noout -ocsp_uri -in "$CERTPATH") ]] && CERTSTAPLE=on || CERTSTAPLE=off
fi
# handle TS cert if present
if [[ -f "$TSCERTPATH" ]]; then
# confirm TS is intalled and running
if [[ -x $TS ]] && $TS status &>/dev/null; then
# extract common name from cert
TSFQDN1=$(openssl x509 -noout -subject -nameopt multiline -in "$TSCERTPATH" | sed -n 's/ *commonName *= //p')
# get tailscale domain
TSFQDN2=$($TS status -json | jq ' .Self.DNSName' | tr -d '"' | sed 's/.$//')
if [[ -n "$TSFQDN1" ]] && [[ "$TSFQDN1" == "$TSFQDN2" ]]; then
# common name and tailscale domain are equal and not empty, the cert is valid, use it
TSFQDN=$TSFQDN1
# define CSP frame-ancestors for TS cert
TSFA="https://*.${TSFQDN#*.}/"
fi
fi
fi
# build servers configuration file
build_servers
# build locations configuration file
build_locations
# define the default URL used to access the server
if [[ $USE_SSL == auto ]]; then
[[ -n $LANIP && $(ipv $LANIP) == 4 ]] && DEFAULTURL="https://$LANFQDN$PORTSSL_URL"
[[ -n $LANIP && $(ipv $LANIP) == 6 ]] && DEFAULTURL="https://[$LANFQDN6]$PORTSSL_URL"
elif [[ $USE_SSL == yes ]]; then
DEFAULTURL="https://$LANMDNS$PORTSSL_URL"
else
DEFAULTURL="http://$LANMDNS$PORT_URL"
fi
mkdir -p $(dirname "$INI")
# always defined:
echo "NGINX_LANIP=\"$LANIP\"" >$INI
echo "NGINX_LANIP6=\"$LANIP6\"" >>$INI
echo "NGINX_LANNAME=\"$LANNAME\"" >>$INI
echo "NGINX_LANMDNS=\"$LANMDNS\"" >>$INI
echo "NGINX_BIND=\"$BIND\"" >>$INI
echo "NGINX_CERTPATH=\"$CERTPATH\"" >>$INI
echo "NGINX_USESSL=\"$USE_SSL\"" >>$INI
echo "NGINX_PORT=\"$PORT\"" >>$INI
echo "NGINX_PORTSSL=\"$PORTSSL\"" >>$INI
echo "NGINX_DEFAULTURL=\"$DEFAULTURL\"" >>$INI
# defined if certificate_bundle.pem present:
echo "NGINX_CERTNAME=\"$CERTNAME\"" >>$INI
echo "NGINX_LANFQDN=\"$LANFQDN\"" >>$INI
echo "NGINX_LANFQDN6=\"$LANFQDN6\"" >>$INI
# defined if remote access enabled:
echo "NGINX_WANACCESS=\"$WANACCESS\"" >>$INI
echo "NGINX_WANIP=\"$WANIP\"" >>$INI
echo "NGINX_WANIP6=\"$WANIP6\"" >>$INI
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
echo "NGINX_TAILSCALEFQDN=\"$TSFQDN\"" >>$INI
# add included interfaces
for NET in ${!NET_FQDN[@]}; do
echo "NGINX_${NET^^}FQDN=\"${NET_FQDN[$NET]}\"" >>$INI
done
for NET in ${!NET_FQDN6[@]}; do
echo "NGINX_${NET^^}FQDN6=\"${NET_FQDN6[$NET]}\"" >>$INI
done
# atomically update file
mv $INI ${INI%.*}
}
unraid_api_control(){
# signal unraid-api script, if installed
if [[ -f /etc/rc.d/rc.unraid-api ]]; then
/etc/rc.d/rc.unraid-api $1
fi
}
nginx_running(){
sleep 0.1
[[ -s $PID && -n "$(cat $PID)" && -d "/proc/$(cat $PID)" ]] && return 0 || return 1
}
nginx_waitfor_shutdown(){
for i in {1..10}; do
if ! nginx_running; then break; fi
sleep 1
done
return 0
}
nginx_check(){
log "Checking configuration for correct syntax and then trying to open files referenced in configuration..."
run $NGINX -t -c $CONF
}
nginx_start(){
log "Starting $DAEMON..."
local REPLY
if nginx_running; then
REPLY="Already started"
elif [[ ! -r $CONF ]]; then
# sanity checks, no config file, exit
log "$CONF does not exist, aborting."
exit 1
else
# build ssl configuration file
build_ssl
# nginx does not unlink stale unix sockets before rebinding
# see: https://trac.nginx.org/nginx/ticket/753
rm -f /var/run/nginx.socket
[[ -x $NGINX ]] && $NGINX -c $CONF 2>/dev/null
# side-load unraid-api
unraid_api_control start
# resume nchan publishers
/usr/local/sbin/monitor_nchan start
rm -f /tmp/publishPaused
if nginx_running; then REPLY="Started"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."
}
nginx_stop(){
log "Stopping $DAEMON gracefully..."
local REPLY
if ! nginx_running; then
REPLY="Already stopped"
else
unraid_api_control stop
# pause nchan publishers
/usr/local/sbin/monitor_nchan stop
kill -QUIT $(cat $PID)
nginx_waitfor_shutdown
# safety hammer
pkill -f $NGINX
nginx_waitfor_shutdown
if ! nginx_running; then REPLY="Stopped"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."
}
nginx_stop_forced(){
log "Stopping $DAEMON forcibly..."
local REPLY
if ! nginx_running; then
REPLY="Already stopped"
else
unraid_api_control stop
# stop nchan publishers
/usr/local/sbin/monitor_nchan kill
kill -TERM $(cat $PID)
nginx_waitfor_shutdown
if ! nginx_running; then REPLY="Stopped"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."
}
nginx_restart(){
log "Restarting $DAEMON..."
# only stop working system if configuration is valid
if nginx_running; then
if nginx_check; then
nginx_stop
nginx_start
else
log "Invalid configuration, $DAEMON not restarted"
return 1
fi
else
log "$DAEMON... Not running."
fi
}
nginx_reload(){
log "Reloading $DAEMON..."
# only stop working system if configuration is valid
if nginx_running; then
build_ssl
if nginx_check; then
log "Reloading $DAEMON configuration..."
# pause nchan publishers
/usr/local/sbin/monitor_nchan stop
kill -HUP $(cat $PID)
sleep 1
if tail -10 $SYSLOG | grep -qm1 'Address already in use'; then
# unconditional restart when binding fails
sleep 2
log "Restarting $DAEMON..."
nginx_renew
fi
# resume nchan publishers
/usr/local/sbin/monitor_nchan start
rm -f /tmp/publishPaused
else
log "Invalid configuration, $DAEMON not reloaded"
return 1
fi
else
log "$DAEMON... Not running."
fi
}
nginx_renew(){
# stop unconditionally
pkill -f $NGINX
# rebuild configuration
build_ssl
# start unconditionally
$NGINX -c $CONF 2>/dev/null
}
nginx_update(){
if nginx_running && check && [[ "$(this)" != "$BIND" ]]; then
log "Updating $DAEMON..."
nginx_reload
fi
}
nginx_upgrade(){
if nginx_running; then
echo "Upgrading to the new Nginx binary."
echo "Make sure the Nginx binary has been replaced with new one"
echo "or Nginx server modules were added/removed."
kill -USR2 $(cat $PID)
sleep 3
kill -QUIT $(cat $PID.oldbin)
fi
}
nginx_rotate(){
if nginx_running; then
log "Rotating $DAEMON logs..."
kill -USR1 $(cat $PID)
fi
}
nginx_status(){
if nginx_running; then
echo "$DAEMON is currently running."
else
echo "$DAEMON is not running."
exit 1
fi
}
case "$1" in
'check')
nginx_check
;;
'start')
nginx_start
;;
'stop')
nginx_stop
;;
'term')
nginx_stop_forced
;;
'restart')
nginx_restart
;;
'reload')
nginx_reload
;;
'renew')
nginx_renew
;;
'update')
nginx_update
;;
'port')
echo $PORT
;;
'upgrade')
nginx_upgrade
;;
'rotate')
nginx_rotate
;;
'status')
nginx_status
;;
*)
echo "Usage: $BASENAME check|start|stop|term|restart|reload|renew|update|port|upgrade|rotate|status"
exit 1
esac
exit 0

View File

@@ -10,6 +10,7 @@ import AuthRequestModification from '@app/unraid-api/unraid-file-modifier/modifi
import DefaultPageLayoutModification from '@app/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.js';
import LogRotateModification from '@app/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.js';
import NotificationsPageModification from '@app/unraid-api/unraid-file-modifier/modifications/notifications-page.modification.js';
import RcNginxModification from '@app/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.js';
import SSOFileModification from '@app/unraid-api/unraid-file-modifier/modifications/sso.modification.js';
interface ModificationTestCase {
@@ -47,6 +48,11 @@ const patchTestCases: ModificationTestCase[] = [
'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/emhttp/auth-request.php',
fileName: 'auth-request.php',
},
{
ModificationClass: RcNginxModification,
fileUrl: 'https://raw.githubusercontent.com/unraid/webgui/refs/heads/7.1/etc/rc.d/rc.nginx',
fileName: 'rc.nginx',
},
];
/** Modifications that simply add a new file & remove it on rollback. */
@@ -103,9 +109,11 @@ async function testModification(testCase: ModificationTestCase) {
// Apply patch and verify modified file
await patcher.apply();
await expect(await readFile(filePath, 'utf-8')).toMatchFileSnapshot(
`snapshots/${fileName}.modified.snapshot.php`
);
let snapshotFile = `snapshots/${fileName}.modified.snapshot`;
if (fileName.endsWith('.php') || fileName.endsWith('.page')) {
snapshotFile += '.php';
}
await expect(await readFile(filePath, 'utf-8')).toMatchFileSnapshot(snapshotFile);
// Rollback and verify original state
await patcher.rollback();

View File

@@ -0,0 +1,909 @@
#!/bin/bash
#
# script: rc.nginx
#
# Nginx daemon control script.
# Written for Slackware Linux by Cherife Li <cherife-#-dotimes.com>.
# LimeTech - modified for Unraid OS
# Bergware - modified for Unraid OS, May 2025
# reference:
# LANNAME 'tower'
# LANMDNS 'tower.local'
# LANFQDN 'lan-ip.hash.myunraid.net' (wildcard cert)
# WANFQDN 'wan-ip.hash.myunraid.net' (wildcard cert)
# WG0FQDN 'wg0-ip.hash.myunraid.net' (wildcard cert)
DAEMON="Nginx server daemon"
CALLER="nginx"
NGINX="/usr/sbin/nginx"
TS="/usr/local/sbin/tailscale"
PID="/var/run/nginx.pid"
SSL="/boot/config/ssl"
CONF="/etc/nginx/nginx.conf"
IDENT="/boot/config/ident.cfg"
SERVERS="/etc/nginx/conf.d/servers.conf"
LOCATIONS="/etc/nginx/conf.d/locations.conf"
INI="/var/local/emhttp/nginx.ini.new"
CERTPATH="$SSL/certs/certificate_bundle.pem"
TSCERTPATH="$SSL/certs/ts_bundle.pem"
CONNECT_CONFIG="/boot/config/plugins/dynamix.my.servers/configs/connect.json"
API_UTILS="/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh"
DEFAULTS="/etc/default/nginx"
SYSTEM="/sys/class/net"
SYSLOG="/var/log/syslog"
# Load defaults
# Defines NGINX_CUSTOMFA for custom Content-Security-Policy frame-ancestors url
[[ -r $DEFAULTS ]] && . $DEFAULTS
# hold server names
SERVER_NAMES=()
# read Unraid settings
[[ -r $IDENT ]] && . <(fromdos <$IDENT)
# preset default values
[[ -z $START_PAGE ]] && START_PAGE=Main
[[ -z $PORT ]] && PORT=80
[[ -z $PORTSSL ]] && PORTSSL=443
[[ -z $USE_SSL ]] && USE_SSL=no
[[ $PORTSSL != 443 ]] && PORTSSL_URL=":$PORTSSL"
[[ $PORT != 80 ]] && PORT_URL=":$PORT"
# delete legacy unraid.net certificate
if [[ -f $CERTPATH ]]; then
TMPCERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
[[ $TMPCERTNAME == *\.unraid\.net ]] && rm $CERTPATH
fi
# if USE_SSL="auto" and no uploaded cert, treat like USE_SSL="no"
[[ $USE_SSL == auto && ! -f $CERTPATH ]] && USE_SSL=no
# override default page if no regkey
if ! find /boot/config/*.key &>/dev/null; then
START_PAGE="Tools/Registration"
fi
# run & log functions
. /etc/rc.d/rc.runlog
# library functions
. /etc/rc.d/rc.library.source
fqdn(){
echo ${CERTNAME/'*'/${1//[.:]/-}}
}
# check if remote access should be enabled
check_remote_access(){
# Check if connect plugin is enabled using api_utils.sh
if [[ -f $API_UTILS ]] && $API_UTILS is_api_plugin_enabled "unraid-api-plugin-connect"; then
# Plugin is enabled, check connect.json configuration
if [[ -f $CONNECT_CONFIG ]] && command -v jq >/dev/null 2>&1; then
local wanaccess=$(jq -r '.wanaccess' "$CONNECT_CONFIG" 2>/dev/null)
local username=$(jq -r '.username' "$CONNECT_CONFIG" 2>/dev/null)
# Enable remote access if wanaccess is true and username is not empty
if [[ $wanaccess == "true" && -n $username && $username != "null" ]]; then
return 0
fi
fi
fi
return 1
}
# create listening ports
listen(){
T=' '
if check && [[ $1 == lo ]]; then
if [[ $IPV4 == yes ]]; then
echo "${T}listen 127.0.0.1:$PORT; # lo"
echo "${T}listen 127.0.0.1:$PORTSSL; # lo"
fi
if [[ $IPV6 == yes ]]; then
echo "${T}listen [::1]:$PORT; # lo"
echo "${T}listen [::1]:$PORTSSL; # lo"
fi
elif [[ -n $BIND ]]; then
for ADDR in $BIND; do
[[ $(ipv $ADDR) == 4 ]] && echo "${T}listen $ADDR:$*; # $(show $ADDR)"
[[ $(ipv $ADDR) == 6 ]] && echo "${T}listen [$ADDR]:$*; # $(show $ADDR)"
done
else
# default listen on any interface with ipv4 protocol
echo "${T}listen $*;"
fi
}
# create redirect server blocks
redirect(){
T=' '
if check && [[ -n $BIND ]]; then
URL=$1
TAG=$2
shift 2
case $URL in
'host')
echo "server {"
for ADDR in $BIND; do
HOST=
[[ $(ipv $ADDR) == 4 ]] && HOST="$ADDR"
[[ $(ipv $ADDR) == 6 ]] && HOST="[$ADDR]"
[[ -n $HOST ]] && echo "${T}listen $HOST:$*; # $(show $ADDR)"
done
echo "${T}add_header Content-Security-Policy \"frame-ancestors 'self' $NGINX_CUSTOMFA\";"
echo "${T}return 302 https://\$host:$PORTSSL\$request_uri;"
echo "}"
;;
'fqdn')
for ADDR in $BIND; do
HOST=
[[ $TAG == 4 && $(ipv $ADDR) == 4 ]] && HOST="$ADDR"
[[ $TAG == 6 && $(ipv $ADDR) == 6 ]] && HOST="[$ADDR]"
if [[ -n $HOST ]]; then
echo "server {"
echo "${T}listen $HOST:$*; # $(show $ADDR)"
echo "${T}add_header Content-Security-Policy \"frame-ancestors 'self' $NGINX_CUSTOMFA\";"
echo "${T}return 302 https://$(fqdn $ADDR)$PORTSSL_URL\$request_uri;"
echo "}"
fi
done
;;
esac
fi
}
# build our servers
# pay attention to escaping
build_servers(){
cat <<- 'EOF' >$SERVERS
#
# Listen on local socket for nchan publishers
#
server {
listen unix:/var/run/nginx.socket default_server;
location ~ /pub/(.*)$ {
nchan_publisher;
nchan_channel_id "$1";
nchan_message_buffer_length $arg_buffer_length;
nchan_message_timeout 0;
}
location ~ /nchan_stub_status$ {
nchan_stub_status;
}
}
EOF
cat <<- EOF >>$SERVERS
#
# Always accept http requests from localhost
# ex: http://localhost
# ex: http://127.0.0.1
# ex: http://[::1]
#
server {
$(listen lo)
#
add_header Content-Security-Policy "frame-ancestors 'self' $NGINX_CUSTOMFA";
include /etc/nginx/conf.d/locations.conf;
}
EOF
if [[ $USE_SSL == no ]]; then
cat <<- EOF >>$SERVERS
#
# Port settings for http protocol
# ex: http://tower (IP address resolved via NetBIOS)
# ex: http://tower.local (IP address resolved via mDNS)
# ex: http://192.168.1.100
# ex: http://[::ffff:192.168.1.100]
#
server {
$(listen $PORT default_server)
#
add_header Content-Security-Policy "frame-ancestors 'self' $NGINX_CUSTOMFA";
location ~ /wsproxy/$PORT/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
EOF
elif [[ $USE_SSL == yes ]]; then
cat <<- EOF >>$SERVERS
#
# Port settings for https protocol (self-signed cert)
# ex: https://tower.local
#
server {
$(listen $PORTSSL ssl default_server)
http2 on;
add_header Content-Security-Policy "frame-ancestors 'self' $SELFCERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $SELFCERTPATH;
ssl_certificate_key $SELFCERTPATH;
ssl_trusted_certificate $SELFCERTPATH;
#
# OCSP stapling
ssl_stapling $SELFCERTSTAPLE;
ssl_stapling_verify $SELFCERTSTAPLE;
#
location ~ /wsproxy/$PORTSSL/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
#
# Redirect http requests to https
# ex: http://tower.local -> https://tower.local
#
$(redirect host 0 $PORT default_server)
EOF
elif [[ $USE_SSL == auto ]]; then
if [[ -n $LANFQDN ]]; then
cat <<- EOF >>$SERVERS
#
# Redirect http requests to https
# ex: http://tower.local -> https://lan-ip.hash.myunraid.net
# ex: http://192.168.1.100 -> https://lan-ip.hash.myunraid.net
#
$(redirect fqdn 4 $PORT default_server)
EOF
fi
if [[ -n $LANFQDN6 ]]; then
cat <<- EOF >>$SERVERS
#
# Redirect http requests to https
# ex: http://[::ffff:192.168.1.100] -> https://lan-ip.hash.myunraid.net
#
$(redirect fqdn 6 $PORT default_server)
EOF
fi
cat <<- EOF >>$SERVERS
#
# Return 404 (Not Found) as default ssl action, using self-signed cert
#
server {
$(listen $PORTSSL ssl default_server)
http2 on;
add_header Content-Security-Policy "frame-ancestors 'self' $SELFCERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $SELFCERTPATH;
ssl_certificate_key $SELFCERTPATH;
ssl_trusted_certificate $SELFCERTPATH;
#
# OCSP stapling
ssl_stapling $SELFCERTSTAPLE;
ssl_stapling_verify $SELFCERTSTAPLE;
return 404;
}
EOF
fi
if [[ -f $CERTPATH ]]; then
if [[ $USE_SSL == no ]]; then
cat <<- EOF >>$SERVERS
#
# Return 404 (Not Found) as default ssl action
#
server {
$(listen $PORTSSL ssl default_server)
http2 on;
add_header Content-Security-Policy "frame-ancestors 'self' $SELFCERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $SELFCERTPATH;
ssl_certificate_key $SELFCERTPATH;
ssl_trusted_certificate $SELFCERTPATH;
#
# OCSP stapling
ssl_stapling $SELFCERTSTAPLE;
ssl_stapling_verify $SELFCERTSTAPLE;
return 404;
}
EOF
fi
if [[ -n $LANFQDN || -n $LANFQDN6 ]]; then
cat <<- EOF >>$SERVERS
#
# Port settings for https using CA-signed cert
# ex: https://lan-ip.hash.myunraid.net
#
server {
$(listen $PORTSSL ssl)
http2 on;
server_name ${SERVER_NAMES[@]};
add_header Content-Security-Policy "frame-ancestors 'self' $CERTFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $CERTPATH;
ssl_certificate_key $CERTPATH;
ssl_trusted_certificate $CERTPATH;
#
# OCSP stapling
ssl_stapling $CERTSTAPLE;
ssl_stapling_verify $CERTSTAPLE;
#
location ~ /wsproxy/$PORTSSL/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
EOF
fi
fi
if [[ -n $TSFQDN ]]; then
cat <<- EOF >>$SERVERS
#
# Redirect Tailscale http requests to https
# ex: http://tower.magicDNS.ts.net -> https://tower.magicDNS.ts.net
#
server {
$(listen $PORT)
server_name $TSFQDN;
return 302 https://$TSFQDN$PORTSSL_URL$request_uri;
}
#
# Port settings for https using Tailscale cert
# ex: https://tower.magicDNS.ts.net
#
server {
$(listen $PORTSSL ssl http2)
server_name $TSFQDN;
add_header Content-Security-Policy "frame-ancestors 'self' $TSFA $NGINX_CUSTOMFA";
# Ok to use concatenated pem files; nginx will do the right thing.
ssl_certificate $TSCERTPATH;
ssl_certificate_key $TSCERTPATH;
ssl_trusted_certificate $TSCERTPATH;
#
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
#
location ~ /wsproxy/$PORTSSL/ { return 403; }
include /etc/nginx/conf.d/locations.conf;
}
EOF
fi
}
# build our locations
# pay attention to escaping
build_locations(){
cat <<- EOF >$LOCATIONS
#
# Default start page
#
location = / {
return 302 \$scheme://\$http_host/$START_PAGE;
}
EOF
cat <<- 'EOF' >>$LOCATIONS
#
# Redirect to login page for authentication
#
location /login {
allow all;
limit_req zone=authlimit burst=20 nodelay;
try_files /login.php =404;
include fastcgi_params;
}
location /logout {
allow all;
try_files /login.php =404;
include fastcgi_params;
}
#
# Redirect to login page on failed authentication (401)
#
error_page 401 @401;
location @401 {
return 302 $scheme://$http_host/login;
}
#
# deny access to any hidden file (beginning with a .period)
#
location ~ /\. {
return 404;
}
#
# page files handled by template.php
#
location ~^/[A-Z].* {
try_files $uri /webGui/template.php$is_args$args;
}
#
# nchan subscriber endpoint
#
location ~ /sub/(.*)$ {
nchan_subscriber;
nchan_subscriber_timeout 0;
# nchan_authorize_request <url here>
nchan_channel_id "$1";
nchan_channel_id_split_delimiter ",";
}
location /nchan_stub_status {
nchan_stub_status;
}
#
# my servers proxy
#
location /graphql {
allow all;
error_log /dev/null crit;
proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
proxy_intercept_errors on;
error_page 502 = @graph502;
}
location @graph502 {
default_type application/json;
return 200 '{"errors":[{"error":{"name":"InternalError","message":"Graphql is offline."}}]}';
}
#
# websocket proxy
#
location ~ /wsproxy/(.*)$ {
proxy_read_timeout 3600;
proxy_pass http://127.0.0.1:$1;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
#
# add Cache-Control headers to novnc
#
location ~ /plugins\/dynamix.vm.manager\/novnc/(.*)$ {
gzip on;
gzip_disable "MSIE [1-6]\.";
gzip_types text/css application/javascript text/javascript application/x-javascript;
add_header Cache-Control no-cache;
}
#
# pass PHP scripts to FastCGI server listening on unix:/var/run/php-fpm.sock
#
location ~ ^(.+\.php)(.*)$ {
fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
}
#
# enable compression of JS/CSS/WOFF files
# if version tag on querystring, tell browser to cache indefinitely
#
location ~ \.(js|css|woff)$ {
gzip on;
gzip_disable "MSIE [1-6]\.";
gzip_types text/css application/javascript text/javascript application/x-javascript application/font-woff font-woff;
if ( $args ~ "v=" ) {
expires max;
}
}
#
# robots.txt available without authentication
#
location = /robots.txt {
add_header Access-Control-Allow-Origin *; #robots.txt any origin
allow all;
}
#
# redirect.htm available without authentication
#
location = /redirect {
rewrite ^ /redirect.htm break;
allow all;
}
#
# proxy update.htm and logging.htm scripts to emhttpd listening on local socket
#
location = /update.htm {
keepalive_timeout 0;
proxy_read_timeout 180; # 3 minutes
proxy_pass http://unix:/var/run/emhttpd.socket:/update.htm;
}
location = /logging.htm {
proxy_read_timeout 864000; # 10 days(!)
proxy_pass http://unix:/var/run/emhttpd.socket:/logging.htm;
}
#
# proxy webterminal to ttyd server listening on unix:/var/run/<tag>.sock
#
location ~ /webterminal/(.*)/(.*)$ {
proxy_read_timeout 864000; # 10 days(!)
proxy_pass http://unix:/var/run/$1.sock:/$2;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location = /webterminal/auth_token.js {
return 204;
}
#
# proxy logterminal to ttyd server listening on unix:/var/tmp/<tag>.sock
#
location ~ /logterminal/(.*)/(.*)$ {
proxy_read_timeout 864000; # 10 days(!)
proxy_pass http://unix:/var/tmp/$1.sock:/$2;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
EOF
}
# check if certificate common name or any alternative name matches LANMDNS
acceptable_selfcert(){
local CN
for CN in $(openssl x509 -noout -subject -nameopt multiline -in $SELFCERTPATH | sed -n 's/ *commonName *= //p' ;
openssl x509 -noout -ext subjectAltName -in $SELFCERTPATH | grep -Eo "DNS:[a-zA-Z 0-9.*-]*" | sed "s/DNS://g"); do
CN=${CN/\*/$LANNAME} # support wildcard custom certs
[[ ${CN,,} = ${LANMDNS,,} ]] && return 0
done
return 1
}
build_ssl(){
mkdir -p $SSL/certs
if [[ ! -f $SSL/dhparam.pem ]]; then
# regenerate dhparam file
# use -dsaparam per: https://security.stackexchange.com/questions/95178/diffie-hellman-parameters-still-calculating-after-24-hours
echo "Regenerating dhparam..."
openssl dhparam -dsaparam -out $SSL/dhparam.pem 2048 &>/dev/null
fi
ln -sf $SSL/dhparam.pem /etc/nginx/dhparam.pem
LANNAME=$(hostname)
LANMDNS=${LANNAME}${LOCAL_TLD:+.$LOCAL_TLD}
# fetch LAN IP address (read management interface eth0)
[[ -e $SYSTEM/bond0 ]] && DEV=bond0 || DEV=eth0
[[ -e $SYSTEM/br0 ]] && DEV=br0
LANIP=$(ip -4 -br addr show scope global primary dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
LANIP6=$(ip -6 -br addr show scope global primary -deprecated dev $DEV | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
# try wireless connection if no IP address on interface eth0
[[ -z $LANIP && -e $SYSTEM/wlan0 ]] && LANIP=$(ip -4 -br addr show scope global primary dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
[[ -z $LANIP6 && -e $SYSTEM/wlan0 ]] && LANIP6=$(ip -6 -br addr show scope global primary -deprecated dev wlan0 | awk '{print $3;exit}' | sed -r 's/\/[0-9]+//')
# regenerate self-signed cert if local TLD changes */
SELFCERTPATH=$SSL/certs/${LANNAME}_unraid_bundle.pem
[[ -f $SELFCERTPATH ]] && ! acceptable_selfcert && rm -f $SELFCERTPATH
if [[ ! -f $SELFCERTPATH ]]; then
# regenerate private key and certificate
echo "Regenerating private key and certificate..."
openssl_subject="/O=Self-signed/OU=Unraid/CN=$LANMDNS"
openssl_altname="DNS:$LANMDNS"
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -sha512 -keyout /tmp/key.pem -out /tmp/cert.pem -subj "$openssl_subject" -extensions SAN -config <(cat /etc/ssl/openssl.cnf; printf "[SAN]\nsubjectAltName=${openssl_altname}") &>/dev/null
cat /tmp/cert.pem /tmp/key.pem >$SELFCERTPATH
rm -f /tmp/cert.pem /tmp/key.pem
fi
# determine if OCSP stapling should be enabled for this cert
[[ -n $(openssl x509 -noout -ocsp_uri -in "$SELFCERTPATH") ]] && SELFCERTSTAPLE=on || SELFCERTSTAPLE=off
# define CSP frame-ancestors for the self-signed cert
[[ -n $LOCAL_TLD ]] && [[ "$LOCAL_TLD" != "local" ]] && SELFCERTFA="https://*.$LOCAL_TLD/"
# handle Certificate Authority signed cert if present
if [[ -f $CERTPATH ]]; then
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
CERTFA="https://*.${CERTNAME#*.}/"
# check if Remote Access is enabled and fetch WANIP
if [[ -L /usr/local/sbin/unraid-api ]] && check_remote_access; then
WANACCESS=yes
WANIP=$(curl https://wanip4.unraid.net/ 2>/dev/null)
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
# wildcard LE certificate
# add Unraid Connect to CSP frame-ancestors for a myunraid.net cert
CERTFA+=" https://connect.myunraid.net/"
[[ -n $LANIP ]] && LANFQDN=$(fqdn $LANIP) SERVER_NAMES+=($LANFQDN)
[[ -n $LANIP6 ]] && LANFQDN6=$(fqdn $LANIP6) SERVER_NAMES+=($LANFQDN6)
# check if remote access enabled
if [[ -n $WANACCESS ]]; then
[[ -n $WANIP ]] && WANFQDN=$(fqdn $WANIP) SERVER_NAMES+=($WANFQDN)
[[ -n $WANIP6 ]] && WANFQDN6=$(fqdn $WANIP6) SERVER_NAMES+=($WANFQDN6)
fi
if check; then
# add included interfaces
declare -A NET_FQDN NET_FQDN6
for ADDR in $BIND; do
# convert IP to name
NET=$(show $ADDR)
# skip invalid interface, LAN interface and WG VPN tunneled interfaces
[[ -z $NET || $(show $LANIP) == $NET || (${NET:0:2} == wg && $(scan TYPE:1 $WIREGUARD/$NET.cfg) -ge 7) ]] && continue
[[ $(ipv $ADDR) == 4 ]] && NET_FQDN[$NET]=$(fqdn $ADDR) || NET_FQDN6[$NET]=$(fqdn $ADDR)
SERVER_NAMES+=($(fqdn $ADDR))
done
fi
else
# custom certificate, this would be better as SELFCERTPATH
LANFQDN=${CERTNAME/\*/$LANNAME} # support wildcard custom certs
SERVER_NAMES+=($LANFQDN)
fi
# determine if OCSP stapling should be enabled for this cert
[[ -n $(openssl x509 -noout -ocsp_uri -in "$CERTPATH") ]] && CERTSTAPLE=on || CERTSTAPLE=off
fi
# handle TS cert if present
if [[ -f "$TSCERTPATH" ]]; then
# confirm TS is intalled and running
if [[ -x $TS ]] && $TS status &>/dev/null; then
# extract common name from cert
TSFQDN1=$(openssl x509 -noout -subject -nameopt multiline -in "$TSCERTPATH" | sed -n 's/ *commonName *= //p')
# get tailscale domain
TSFQDN2=$($TS status -json | jq ' .Self.DNSName' | tr -d '"' | sed 's/.$//')
if [[ -n "$TSFQDN1" ]] && [[ "$TSFQDN1" == "$TSFQDN2" ]]; then
# common name and tailscale domain are equal and not empty, the cert is valid, use it
TSFQDN=$TSFQDN1
# define CSP frame-ancestors for TS cert
TSFA="https://*.${TSFQDN#*.}/"
fi
fi
fi
# build servers configuration file
build_servers
# build locations configuration file
build_locations
# define the default URL used to access the server
if [[ $USE_SSL == auto ]]; then
[[ -n $LANIP && $(ipv $LANIP) == 4 ]] && DEFAULTURL="https://$LANFQDN$PORTSSL_URL"
[[ -n $LANIP && $(ipv $LANIP) == 6 ]] && DEFAULTURL="https://[$LANFQDN6]$PORTSSL_URL"
elif [[ $USE_SSL == yes ]]; then
DEFAULTURL="https://$LANMDNS$PORTSSL_URL"
else
DEFAULTURL="http://$LANMDNS$PORT_URL"
fi
mkdir -p $(dirname "$INI")
# always defined:
echo "NGINX_LANIP=\"$LANIP\"" >$INI
echo "NGINX_LANIP6=\"$LANIP6\"" >>$INI
echo "NGINX_LANNAME=\"$LANNAME\"" >>$INI
echo "NGINX_LANMDNS=\"$LANMDNS\"" >>$INI
echo "NGINX_BIND=\"$BIND\"" >>$INI
echo "NGINX_CERTPATH=\"$CERTPATH\"" >>$INI
echo "NGINX_USESSL=\"$USE_SSL\"" >>$INI
echo "NGINX_PORT=\"$PORT\"" >>$INI
echo "NGINX_PORTSSL=\"$PORTSSL\"" >>$INI
echo "NGINX_DEFAULTURL=\"$DEFAULTURL\"" >>$INI
# defined if certificate_bundle.pem present:
echo "NGINX_CERTNAME=\"$CERTNAME\"" >>$INI
echo "NGINX_LANFQDN=\"$LANFQDN\"" >>$INI
echo "NGINX_LANFQDN6=\"$LANFQDN6\"" >>$INI
# defined if remote access enabled:
echo "NGINX_WANACCESS=\"$WANACCESS\"" >>$INI
echo "NGINX_WANIP=\"$WANIP\"" >>$INI
echo "NGINX_WANIP6=\"$WANIP6\"" >>$INI
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
echo "NGINX_TAILSCALEFQDN=\"$TSFQDN\"" >>$INI
# add included interfaces
for NET in "${!NET_FQDN[@]}"; do
echo "NGINX_${NET^^}FQDN=\"${NET_FQDN[$NET]}\"" >>$INI
done
for NET in "${!NET_FQDN6[@]}"; do
echo "NGINX_${NET^^}FQDN6=\"${NET_FQDN6[$NET]}\"" >>$INI
done
# atomically update file
mv $INI ${INI%.*}
}
unraid_api_control(){
# signal unraid-api script, if installed
if [[ -f /etc/rc.d/rc.unraid-api ]]; then
/etc/rc.d/rc.unraid-api $1
fi
}
nginx_running(){
sleep 0.1
[[ -s $PID && -n "$(cat $PID)" && -d "/proc/$(cat $PID)" ]] && return 0 || return 1
}
nginx_waitfor_shutdown(){
for i in {1..10}; do
if ! nginx_running; then break; fi
sleep 1
done
return 0
}
nginx_check(){
log "Checking configuration for correct syntax and then trying to open files referenced in configuration..."
run $NGINX -t -c $CONF
}
nginx_start(){
log "Starting $DAEMON..."
local REPLY
if nginx_running; then
REPLY="Already started"
elif [[ ! -r $CONF ]]; then
# sanity checks, no config file, exit
log "$CONF does not exist, aborting."
exit 1
else
# build ssl configuration file
build_ssl
# nginx does not unlink stale unix sockets before rebinding
# see: https://trac.nginx.org/nginx/ticket/753
rm -f /var/run/nginx.socket
[[ -x $NGINX ]] && $NGINX -c $CONF 2>/dev/null
# side-load unraid-api
unraid_api_control start
# resume nchan publishers
/usr/local/sbin/monitor_nchan start
rm -f /tmp/publishPaused
if nginx_running; then REPLY="Started"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."
}
nginx_stop(){
log "Stopping $DAEMON gracefully..."
local REPLY
if ! nginx_running; then
REPLY="Already stopped"
else
unraid_api_control stop
# pause nchan publishers
/usr/local/sbin/monitor_nchan stop
kill -QUIT $(cat $PID)
nginx_waitfor_shutdown
# safety hammer
pkill -f $NGINX
nginx_waitfor_shutdown
if ! nginx_running; then REPLY="Stopped"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."
}
nginx_stop_forced(){
log "Stopping $DAEMON forcibly..."
local REPLY
if ! nginx_running; then
REPLY="Already stopped"
else
unraid_api_control stop
# stop nchan publishers
/usr/local/sbin/monitor_nchan kill
kill -TERM $(cat $PID)
nginx_waitfor_shutdown
if ! nginx_running; then REPLY="Stopped"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."
}
nginx_restart(){
log "Restarting $DAEMON..."
# only stop working system if configuration is valid
if nginx_running; then
if nginx_check; then
nginx_stop
nginx_start
else
log "Invalid configuration, $DAEMON not restarted"
return 1
fi
else
log "$DAEMON... Not running."
fi
}
nginx_reload(){
log "Reloading $DAEMON..."
# only stop working system if configuration is valid
if nginx_running; then
build_ssl
if nginx_check; then
log "Reloading $DAEMON configuration..."
# pause nchan publishers
/usr/local/sbin/monitor_nchan stop
kill -HUP $(cat $PID)
sleep 1
if tail -10 $SYSLOG | grep -qm1 'Address already in use'; then
# unconditional restart when binding fails
sleep 2
log "Restarting $DAEMON..."
nginx_renew
fi
# resume nchan publishers
/usr/local/sbin/monitor_nchan start
rm -f /tmp/publishPaused
else
log "Invalid configuration, $DAEMON not reloaded"
return 1
fi
else
log "$DAEMON... Not running."
fi
}
nginx_renew(){
# stop unconditionally
pkill -f $NGINX
# rebuild configuration
build_ssl
# start unconditionally
$NGINX -c $CONF 2>/dev/null
}
nginx_update(){
if nginx_running && check && [[ "$(this)" != "$BIND" ]]; then
log "Updating $DAEMON..."
nginx_reload
fi
}
nginx_upgrade(){
if nginx_running; then
echo "Upgrading to the new Nginx binary."
echo "Make sure the Nginx binary has been replaced with new one"
echo "or Nginx server modules were added/removed."
kill -USR2 $(cat $PID)
sleep 3
kill -QUIT $(cat $PID.oldbin)
fi
}
nginx_rotate(){
if nginx_running; then
log "Rotating $DAEMON logs..."
kill -USR1 $(cat $PID)
fi
}
nginx_status(){
if nginx_running; then
echo "$DAEMON is currently running."
else
echo "$DAEMON is not running."
exit 1
fi
}
case "$1" in
'check')
nginx_check
;;
'start')
nginx_start
;;
'stop')
nginx_stop
;;
'term')
nginx_stop_forced
;;
'restart')
nginx_restart
;;
'reload')
nginx_reload
;;
'renew')
nginx_renew
;;
'update')
nginx_update
;;
'port')
echo $PORT
;;
'upgrade')
nginx_upgrade
;;
'rotate')
nginx_rotate
;;
'status')
nginx_status
;;
*)
echo "Usage: $BASENAME check|start|stop|term|restart|reload|renew|update|port|upgrade|rotate|status"
exit 1
esac
exit 0

View File

@@ -0,0 +1,76 @@
Index: /etc/rc.d/rc.nginx
===================================================================
--- /etc/rc.d/rc.nginx original
+++ /etc/rc.d/rc.nginx modified
@@ -26,11 +26,12 @@
SERVERS="/etc/nginx/conf.d/servers.conf"
LOCATIONS="/etc/nginx/conf.d/locations.conf"
INI="/var/local/emhttp/nginx.ini.new"
CERTPATH="$SSL/certs/certificate_bundle.pem"
TSCERTPATH="$SSL/certs/ts_bundle.pem"
-MYSERVERS="/boot/config/plugins/dynamix.my.servers/myservers.cfg"
+CONNECT_CONFIG="/boot/config/plugins/dynamix.my.servers/configs/connect.json"
+API_UTILS="/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh"
DEFAULTS="/etc/default/nginx"
SYSTEM="/sys/class/net"
SYSLOG="/var/log/syslog"
# Load defaults
@@ -73,10 +74,27 @@
fqdn(){
echo ${CERTNAME/'*'/${1//[.:]/-}}
}
+# check if remote access should be enabled
+check_remote_access(){
+ # Check if connect plugin is enabled using api_utils.sh
+ if [[ -f $API_UTILS ]] && $API_UTILS is_api_plugin_enabled "unraid-api-plugin-connect"; then
+ # Plugin is enabled, check connect.json configuration
+ if [[ -f $CONNECT_CONFIG ]] && command -v jq >/dev/null 2>&1; then
+ local wanaccess=$(jq -r '.wanaccess' "$CONNECT_CONFIG" 2>/dev/null)
+ local username=$(jq -r '.username' "$CONNECT_CONFIG" 2>/dev/null)
+ # Enable remote access if wanaccess is true and username is not empty
+ if [[ $wanaccess == "true" && -n $username && $username != "null" ]]; then
+ return 0
+ fi
+ fi
+ fi
+ return 1
+}
+
# create listening ports
listen(){
T=' '
if check && [[ $1 == lo ]]; then
if [[ $IPV4 == yes ]]; then
@@ -566,11 +584,11 @@
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
CERTFA="https://*.${CERTNAME#*.}/"
# check if Remote Access is enabled and fetch WANIP
- if [[ -L /usr/local/sbin/unraid-api ]] && grep -qs 'wanaccess="yes"' $MYSERVERS && ! grep -qs 'username=""' $MYSERVERS; then
+ if [[ -L /usr/local/sbin/unraid-api ]] && check_remote_access; then
WANACCESS=yes
WANIP=$(curl https://wanip4.unraid.net/ 2>/dev/null)
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
@@ -660,14 +678,14 @@
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
echo "NGINX_TAILSCALEFQDN=\"$TSFQDN\"" >>$INI
# add included interfaces
- for NET in ${!NET_FQDN[@]}; do
+ for NET in "${!NET_FQDN[@]}"; do
echo "NGINX_${NET^^}FQDN=\"${NET_FQDN[$NET]}\"" >>$INI
done
- for NET in ${!NET_FQDN6[@]}; do
+ for NET in "${!NET_FQDN6[@]}"; do
echo "NGINX_${NET^^}FQDN6=\"${NET_FQDN6[$NET]}\"" >>$INI
done
# atomically update file
mv $INI ${INI%.*}
}

View File

@@ -0,0 +1,96 @@
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import {
FileModification,
ShouldApplyWithReason,
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
/**
* Patch rc.nginx on < Unraid 7.2.0 to read the updated connect & api config files
*
* Backport of https://github.com/unraid/webgui/pull/2269
*/
export default class RcNginxModification extends FileModification {
public filePath: string = '/etc/rc.d/rc.nginx' as const;
id: string = 'rc-nginx';
/**
* Generate a patch for the rc.nginx file
*
* Should result in the same patch as
* https://patch-diff.githubusercontent.com/raw/unraid/webgui/pull/2269.patch
*
* @param overridePath - The path to override the default file path
* @returns The patch for the rc.nginx file
*/
protected async generatePatch(overridePath?: string): Promise<string> {
if (!existsSync(this.filePath)) {
throw new Error(`File ${this.filePath} not found.`);
}
const fileContent = await readFile(this.filePath, 'utf8');
if (!fileContent.includes('MYSERVERS=')) {
throw new Error(`MYSERVERS not found in the file; incorrect target?`);
}
let newContent = fileContent.replace(
'MYSERVERS="/boot/config/plugins/dynamix.my.servers/myservers.cfg"',
`CONNECT_CONFIG="/boot/config/plugins/dynamix.my.servers/configs/connect.json"
API_UTILS="/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh"`
);
if (!newContent.includes('check_remote_access()')) {
newContent = newContent.replace(
'# create listening ports',
`# check if remote access should be enabled
check_remote_access(){
# Check if connect plugin is enabled using api_utils.sh
if [[ -f $API_UTILS ]] && $API_UTILS is_api_plugin_enabled "unraid-api-plugin-connect"; then
# Plugin is enabled, check connect.json configuration
if [[ -f $CONNECT_CONFIG ]] && command -v jq >/dev/null 2>&1; then
local wanaccess=$(jq -r '.wanaccess' "$CONNECT_CONFIG" 2>/dev/null)
local username=$(jq -r '.username' "$CONNECT_CONFIG" 2>/dev/null)
# Enable remote access if wanaccess is true and username is not empty
if [[ $wanaccess == "true" && -n $username && $username != "null" ]]; then
return 0
fi
fi
fi
return 1
}
# create listening ports`
);
}
newContent = newContent.replace(
`if [[ -L /usr/local/sbin/unraid-api ]] && grep -qs 'wanaccess="yes"' $MYSERVERS && ! grep -qs 'username=""' $MYSERVERS; then`,
`if [[ -L /usr/local/sbin/unraid-api ]] && check_remote_access; then`
);
newContent = newContent.replace(
'for NET in ${!NET_FQDN6[@]}; do',
'for NET in "${!NET_FQDN6[@]}"; do'
);
newContent = newContent.replace(
'for NET in ${!NET_FQDN[@]}; do',
'for NET in "${!NET_FQDN[@]}"; do'
);
return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
}
async shouldApply(): Promise<ShouldApplyWithReason> {
if (await this.isUnraidVersionGreaterThanOrEqualTo('7.2.0')) {
return {
shouldApply: false,
reason: 'Patch unnecessary for Unraid 7.2 or later because the Unraid API is integrated.',
};
}
const { shouldApply, reason } = await super.shouldApply();
return {
shouldApply: shouldApply,
reason: shouldApply ? 'Unraid version is less than 7.2.0, applying the patch.' : reason,
};
}
}

View File

@@ -1,5 +1,5 @@
declare global {
// eslint-disable-next-line no-var
var csrf_token: string;
}