# [ Config editor ] =========================================================== # Normalizes an OpenLiteSpeed configuration file. # Collapses three or more consecutive blank lines into two. # [$1] (file): config file path (defaults to $openlitespeedConfigFile). function openlitespeedConfigNormalize() { local file="${1:-$openlitespeedConfigFile}" [[ -n "$file" ]] || { appError "Config file not specified"; return 1; } perl -0pi -e 's/\n{3,}/\n\n/g' "$file" || { appError "Failed normalize config file: $file" return 1 } } # Edits an OpenLiteSpeed configuration file via the OLS config editor script. # $1 (file): config file path. # $2 (mode): edit mode passed to ols-editor.pl. # $3+ (...): additional editor arguments. function openlitespeedConfigEditFile() { local file="$1" local mode="$2" shift 2 || return 1 [[ -n "$file" ]] || { appError "Config file not specified"; return 1; } [[ -n "$mode" ]] || { appError "Edit mode not specified"; return 1; } perl "$appPath/libs/modules/assets/ols-editor.pl" "$file" "$mode" "$@" || { appError "Failed edit config file: $file | mode=$mode" return 1 } } # Edits the main OLS config file. # $@ (...): edit mode and arguments. function openlitespeedConfigEdit() { openlitespeedConfigEditFile "$openlitespeedConfigFile" "$@" } # Edits the OLS WebAdmin config file. # $@ (...): edit mode and arguments. function openlitespeedAdminConfigEdit() { openlitespeedConfigEditFile "$openlitespeedAdminPath/admin_config.conf" "$@" } # Deletes a value from the main OLS config. function openlitespeedConfigEditDel() { openlitespeedConfigEdit del "$@" } # Deletes masked values from the main OLS config. function openlitespeedConfigEditDelMasked() { openlitespeedConfigEdit del_masked "$@" } # Adds a value to the main OLS config. function openlitespeedConfigEditAdd() { openlitespeedConfigEdit add "$@" } # Sets a value in the main OLS config. function openlitespeedConfigEditSet() { openlitespeedConfigEdit set "$@" } # Sets a masked value in the main OLS config. function openlitespeedConfigEditSetMasked() { openlitespeedConfigEdit set_masked "$@" } # Adds a virtual host entry to the main OLS config. # $1 (domain): virtual host name. function openlitespeedConfigVHostAdd() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } openlitespeedConfigEdit vhost_add "$domain" } # Deletes a virtual host entry from the main OLS config. # $1 (domain): virtual host name. function openlitespeedConfigVHostDel() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } openlitespeedConfigEdit vhost_del "$domain" } # Sets a value in the OLS WebAdmin config. function openlitespeedAdminConfigEditSet() { openlitespeedAdminConfigEdit set "$@" } # Lists OLS virtual hosts filtered by state. # [$1] (type): filter type: all, up, or down (defaults to all). function openlitespeedVhostList() { local type="${1:-all}" arrayContains "$type" "all" "up" "down" || { appError "Unknown type: $type"; return 1; } openlitespeedConfigEdit vhost_list "$type" } # Lists OLS virtual hosts with their map entries, filtered by state. # [$1] (type): filter type: all, up, or down (defaults to all). function openlitespeedVhostListMap() { local type="${1:-all}" arrayContains "$type" "all" "up" "down" || { appError "Unknown type: $type"; return 1; } openlitespeedConfigEdit vhost_list_map "$type" } # Lists configured OLS aliases. function openlitespeedAliasList() { openlitespeedConfigEdit alias_list } # Lists aliases assigned to a virtual host. # $1 (domain): site domain name. function openlitespeedVhostAlias() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } openlitespeedConfigEdit vhost_alias "$domain" } # Executes a command inside the OLS deployment container. # $@ (...): command and arguments to execute. function openlitespeedExec() { k3sRun exec deploy/"$openlitespeedKube" -c "$openlitespeedKube" -- "$@" } # Executes a command inside a specific OLS pod. # $1 (pod): pod name. # $2+ (...): command and arguments to execute. function openlitespeedPodExec() { local pod="$1" [[ -n "$pod" ]] || { appError "Pod not specified"; return 1; } shift k3sRun exec pod/"$pod" -c "$openlitespeedKube" -- "$@" } # Rebuilds filesystem layout, ownership, and permissions for a virtual host. # $1 (vhostPath): virtual host chroot path. # $2 (vhostDataPath): virtual host data path. # $3 (ug): system user/group name for the site. function openlitespeedVhostRebuildPath() { local vhostPath="$1" local vhostDataPath="$2" local ug="$3" [[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; } [[ -n "$vhostDataPath" ]] || { appError "vhostDataPath not specified"; return 1; } [[ -n "$ug" ]] || { appError "ug not specified"; return 1; } # Create site directories mkdir -p -- "$vhostPath"/{www,tmp,session} || { appError "Create vhost directories failed: $vhostPath"; return 1; } # Chroot directory: owned by root (required for OpenSSH) chown root:root -- "$vhostPath" || { appError "Change owner of chroot directory failed: $vhostPath"; return 1; } chmod 755 -- "$vhostPath" || { appError "Change permission of chroot directory failed: $vhostPath"; return 1; } # Site directories: owned by site user chown -R -- "$ug:$ug" "$vhostPath/www" "$vhostPath/tmp" "$vhostPath/session" || { appError "Change owner of site directories failed: $vhostPath"; return 1; } # Permissions: www find "$vhostPath/www" -type d -exec chmod 2750 -- {} + || { appError "Change permissions of www directories failed"; return 1; } find "$vhostPath/www" -type f -exec chmod 640 -- {} + || { appError "Change permissions of www files failed"; return 1; } # Permissions: tmp find "$vhostPath/tmp" -type d -exec chmod 700 -- {} + || { appError "Change permissions of tmp directories failed"; return 1; } find "$vhostPath/tmp" -type f -exec chmod 600 -- {} + || { appError "Change permissions of tmp files failed"; return 1; } # Permissions: session find "$vhostPath/session" -type d -exec chmod 700 -- {} + || { appError "Change permissions of session directories failed"; return 1; } find "$vhostPath/session" -type f -exec chmod 600 -- {} + || { appError "Change permissions of session files failed"; return 1; } # Vhost data directory and bootstrap.php mkdir -p -- "$vhostDataPath" || { appError "Create vhost data directory failed: $vhostDataPath"; return 1; } touch -- "$vhostDataPath/bootstrap.php" || { appError "Create bootstrap.php failed: $vhostDataPath/bootstrap.php"; return 1; } chown -R -- "$ug:$ug" "$vhostDataPath" || { appError "Change owner of vhost data directory failed: $vhostDataPath"; return 1; } find "$vhostDataPath" -type d -exec chmod 700 -- {} + || { appError "Change permissions of vhost data directories failed"; return 1; } find "$vhostDataPath" -type f -exec chmod 600 -- {} + || { appError "Change permissions of vhost data files failed"; return 1; } } # Rebuilds ACL rules for a virtual host. # Resets existing ACLs, then grants OLS nobody user read access to www/data and rw to tmp/session. # $1 (vhostPath): virtual host chroot path. # $2 (vhostDataPath): virtual host data path. function openlitespeedVhostRebuildACL() { local vhostPath="$1" local vhostDataPath="$2" [[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; } [[ -n "$vhostDataPath" ]] || { appError "vhostDataPath not specified"; return 1; } # ACL reset: vhostPath setfacl -R -b -- "$vhostPath" || { appError "Clean ACL rules for vhost path failed: $vhostPath"; return 1; } find "$vhostPath" -type d -exec setfacl -k -- {} + || { appError "Clean default ACL rules for vhost directories failed: $vhostPath"; return 1; } # ACL for OLS user nobody: www # Directories: read/traverse + inheritance | Files: read find "$vhostPath/www" -type d -exec setfacl -m u:nobody:rx,m:rx,d:u:nobody:rx,d:m:rx -- {} + || { appError "Set ACL for nobody on www directories failed"; return 1; } find "$vhostPath/www" -type f -exec setfacl -m u:nobody:r,m:r -- {} + || { appError "Set ACL for nobody on www files failed"; return 1; } # ACL for OLS user nobody: tmp/session # Directories: rwx + inheritance | Files: rw find "$vhostPath/tmp" "$vhostPath/session" -type d -exec setfacl -m u:nobody:rwx,m:rwx,d:u:nobody:rwx,d:m:rwx -- {} + || { appError "Set ACL for nobody on tmp/session directories failed"; return 1; } find "$vhostPath/tmp" "$vhostPath/session" -type f -exec setfacl -m u:nobody:rw,m:rw -- {} + || { appError "Set ACL for nobody on tmp/session files failed"; return 1; } # ACL reset: vhostDataPath setfacl -R -b -- "$vhostDataPath" || { appError "Clean ACL rules for vhost data path failed: $vhostDataPath"; return 1; } find "$vhostDataPath" -type d -exec setfacl -k -- {} + || { appError "Clean default ACL rules for vhost data directories failed: $vhostDataPath"; return 1; } # ACL for OLS user nobody: vhostDataPath find "$vhostDataPath" -type d -exec setfacl -m u:nobody:rx,m:rx,d:u:nobody:rx,d:m:rx -- {} + || { appError "Set ACL for nobody on vhost data directories failed"; return 1; } find "$vhostDataPath" -type f -exec setfacl -m u:nobody:r,m:r -- {} + || { appError "Set ACL for nobody on vhost data files failed"; return 1; } } # Sets user disk and inode quota for a virtual host. # Requires user quota to be already enabled and active on the target filesystem. # $1 (domain): site domain name. function openlitespeedVhostRebuildQuota() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local ug ug=$(domainToUser "$domain") local quotaMount quotaMount=$(findmnt -no TARGET --target "$vhostsPath") [[ -n "$quotaMount" ]] || { appError "Quota mount not found: $vhostsPath"; return 1; } local quotaBlockLimit quotaInodeLimit quotaBlockLimit=$(siteConfigGetOrSet "$domain" "quotaBlockLimit" "$openlitespeedQuotaBlockLimit") quotaInodeLimit=$(siteConfigGetOrSet "$domain" "quotaInodeLimit" "$openlitespeedQuotaInodeLimit") setquota -u "$ug" "$quotaBlockLimit" "$quotaBlockLimit" "$quotaInodeLimit" "$quotaInodeLimit" "$quotaMount" || { appError "Set quota failed: ug=$ug mount=$quotaMount" return 1 } } # Checks whether user quota support is ready on the virtual host filesystem. function openlitespeedQuotaCheck() { local quotaMount quotaMount=$(findmnt -no TARGET --target "$vhostsPath") [[ -n "$quotaMount" ]] || { appError "Quota mount not found: $vhostsPath"; return 1; } local quotaOptions quotaOptions=$(findmnt -no OPTIONS --target "$quotaMount") [[ "$quotaOptions" == *usrquota* || "$quotaOptions" == *uquota* ]] || { appError "User quota is not enabled: mount=$quotaMount options=$quotaOptions" return 1 } quotaon -p "$quotaMount" 2>/dev/null | grep -qi "user quota on" || { appError "User quota is not active: mount=$quotaMount" return 1 } command -v setquota >/dev/null 2>&1 || { appError "setquota command not found"; return 1; } } # Prints disk and inode quota hard limits for a user on the virtual host filesystem. # Output format: # $1 (user): username or numeric UID. function openlitespeedVhostQuota() { local user="$1" [[ -n "$user" ]] || { appError "User not specified"; return 1; } local quotaMount quotaMount=$(findmnt -no TARGET --target "$vhostsPath") [[ -n "$quotaMount" ]] || { appError "Quota mount not found: $vhostsPath"; return 1; } local quotaLimits error run quotaLimits error quota -u "$user" --filesystem "$quotaMount" || { appError "$error"; return 1; } quotaLimits=$(awk 'NR>2 && $1 != "" { print $4, $7; exit }' <<< "$quotaLimits") [[ -n "$quotaLimits" ]] || { appError "Parse quota limits failed: user=$user mount=$quotaMount"; return 1; } printf '%s\n' "$quotaLimits" } # Configures XFS project quota for a virtual host. # $1 (vhostPath): virtual host path to assign to an XFS project. # $2 (projectId): numeric XFS project ID. # $3 (projectName): XFS project name. function openlitespeedVhostRebuildXFS() { local vhostPath="$1" local projectId="$2" local projectName="$3" [[ -n "$vhostPath" ]] || { appError "vhostPath not specified"; return 1; } [[ -n "$projectId" ]] || { appError "projectId not specified"; return 1; } [[ -n "$projectName" ]] || { appError "projectName not specified"; return 1; } local quotaFs quotaFs=$(findmnt -no FSTYPE --target "$vhostPath") [[ "$quotaFs" == "xfs" ]] || { appError "XFS quota filesystem mismatch: vhostPath=$vhostPath fs=$quotaFs expected=xfs" return 1 } local quotaMount quotaMount=$(findmnt -no TARGET --target "$vhostPath") [[ -n "$quotaMount" ]] || { appError "Detect XFS quota mount failed: $vhostPath"; return 1; } [[ "$quotaMount" == '/' || "$vhostPath" == "$quotaMount"/* ]] || { appError "XFS quota mount mismatch: vhostPath=$vhostPath quotaMount=$quotaMount expected=$vhostsPath" return 1 } local quotaOptions quotaOptions=$(findmnt -no OPTIONS --target "$vhostPath") [[ "$quotaOptions" != *noquota* && ( "$quotaOptions" == *prjquota* || "$quotaOptions" == *pquota* ) ]] || { appError "XFS project quota is not enabled: vhostPath=$vhostPath mount=$quotaMount options=$quotaOptions" return 1 } touch /etc/projects /etc/projid || { appError "Create XFS quota registry files failed"; return 1; } # /etc/projects format: projectId:path sed -i "\#:$vhostPath\$#d" /etc/projects sed -i "\#^$projectId:#d" /etc/projects printf '%s:%s\n' "$projectId" "$vhostPath" >> /etc/projects # /etc/projid format: projectName:projectId sed -i "\#^$projectName:#d" /etc/projid sed -i "\#:$projectId\$#d" /etc/projid printf '%s:%s\n' "$projectName" "$projectId" >> /etc/projid xfs_quota -x -c "project -s $projectName" "$quotaMount" || { appError "Set XFS project quota project failed: $projectName $vhostPath" return 1 } xfs_quota -x -c "limit -p bhard=$openlitespeedVhostBlockHard ihard=$openlitespeedVhostInodeHard $projectName" "$quotaMount" || { appError "Set XFS project quota limits failed: $projectName" return 1 } } # Rebuilds a virtual host: user/group, filesystem layout, permissions, and ACLs. # $1 (domain): site domain name. function openlitespeedVhostRebuild() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local ug vhostPath vhostDataPath ug=$(domainToUser "$domain") vhostPath="$vhostsPath/$domain" vhostDataPath="$openlitespeedVhostDataPath/$domain" # User and group if ! getent group "$ug" >/dev/null 2>&1; then groupadd -- "$ug" || { appError "Create group failed: $ug"; return 1; } fi if ! id "$ug" >/dev/null 2>&1; then useradd -M -g "$ug" -d "$vhostPath" -s /usr/sbin/nologin -- "$ug" >/dev/null 2>&1 || { appError "Create user failed: $ug"; return 1; } else usermod -g "$ug" -d "$vhostPath" -s /usr/sbin/nologin -- "$ug" >/dev/null 2>&1 || { appError "Update user failed: $ug"; return 1; } fi openlitespeedVhostRebuildPath "$vhostPath" "$vhostDataPath" "$ug" || return 1 openlitespeedVhostRebuildACL "$vhostPath" "$vhostDataPath" || return 1 # Quota # openlitespeedVhostRebuildQuota "$domain" || return 1 # XFS [ NO USE ! | Only for XFS + prjquota ] # local uId # uId=$(id -u "$ug" 2>/dev/null) # [[ -n "$uId" ]] || { appError "Get user id failed: $ug"; return 1; } # openlitespeedVhostRebuildXFS "$vhostPath" "$uId" "$domain" || return 1 } # Creates or deletes OLS config entries for a virtual host. # On create, generates the vhost config file from template if it does not exist. # $1 (domain): site domain name. # [$2] (option): action: create or delete (defaults to create). function openlitespeedConfigRebuild() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local option="${2:-create}" case "$option" in create|delete) ;; *) appError "Unknown option: $option" return 1 ;; esac fileBackup "$openlitespeedConfigFile" || return 1 openlitespeedConfigVHostDel "$domain" || return 1 local vHostFile="$openlitespeedVhostsPath/$domain.conf" if [[ "$option" == "create" ]]; then openlitespeedConfigEditAdd 'listener\h+HTTP' map "$domain $domain" || return 1 openlitespeedConfigVHostAdd "$domain" || return 1 if [[ ! -f "$vHostFile" ]]; then local profileFile memory_limit max_execution_time post_max_size upload_max_filesize profileFile="$appAssetsPath/openlitespeed/profiles/memory-$kubeMemoryProfile.config" [[ -f "$profileFile" ]] || { appError "Profile not found: $profileFile"; return 1; } memory_limit=$(configGet "$profileFile" "memory_limit") max_execution_time=$(configGet "$profileFile" "max_execution_time") post_max_size=$(configGet "$profileFile" "post_max_size") upload_max_filesize=$(configGet "$profileFile" "upload_max_filesize") cp -f -- "$appAssetsPath/openlitespeed/vhost.conf" "$vHostFile" || { appError "Copy vhost template failed"; return 1; } sed -i \ -e "s|{{domain}}|$domain|g" \ -e "s|{{memory_limit}}|$memory_limit|g" \ -e "s|{{max_execution_time}}|$max_execution_time|g" \ -e "s|{{post_max_size}}|$post_max_size|g" \ -e "s|{{upload_max_filesize}}|$upload_max_filesize|g" \ -- "$vHostFile" || { appError "Template substitution failed: $vHostFile"; return 1; } fi else openlitespeedConfigEditDelMasked 'listener\h+HTTP' map "$domain *" || return 1 rm -f -- "$vHostFile" rm -f -- "$openlitespeedVhostsPath/$domain.conf0" rm -f -- "$openlitespeedVhostsPath/$domain.txt" openlitespeedConfigEdit vhost_up "$domain" fi openlitespeedConfigNormalize "$openlitespeedConfigFile" || return 1 } # Sets the domain aliases for a virtual host, updating both site config and OLS listener map. # $1 (domain): primary site domain name. # $@ (...): alias domain names to assign. function openlitespeedAliasSet() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 shift local -a aliasesRaw=("$@") local -a aliases=() run vhostList error openlitespeedVhostList || { appError "Failed get list of virtual hosts: $error"; return 1; } listContains "$domain" "$vhostList" || { appError "Domain is not exists in OpenLiteSpeed config"; return 1; } for aliasRaw in "${aliasesRaw[@]}"; do alias="$(domainPrepare "$aliasRaw")" [[ -n "$alias" ]] || continue [[ "$alias" == "$domain" ]] && continue arrayContains "$alias" "${aliases[@]}" || aliases+=("$alias") done for alias in "${aliases[@]}"; do runError error domainCheck "$alias" || { appError "$alias: $error"; return 1; } listContains "$alias" "$vhostList" && { appError "$alias: Is already exists as virtual host"; return 1; } done local aliasValue='' [[ ${#aliases[@]} -gt 0 ]] && aliasValue="$(IFS=','; printf '%s\n' "${aliases[*]}")" local domainConfigFile="$appDataPath/config/$domain.config" configSet "$domainConfigFile" alias "$aliasValue" || { appError "Failed set alias in $domainConfigFile"; return 1; } fileBackup "$openlitespeedConfigFile" || return 1 openlitespeedConfigEditDelMasked 'listener\h+HTTP' map "$domain *" || return 1 openlitespeedConfigEditAdd 'listener\h+HTTP' map "$domain $domain" || return 1 for alias in "${aliases[@]}"; do openlitespeedConfigEditAdd 'listener\h+HTTP' map "$domain $alias" || return 1 done printf '%s' "$aliasValue" return 0 } # Marks a virtual host as suspended. # $1 (domain): site domain name. function openlitespeedVHostDown() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local vhostList error run vhostList error openlitespeedVhostList || return 1 runSilent fileCheckLineLength "$openlitespeedConfigFile" 8000 || { appError "fileCheckLineLength 8000"; return 1; } if listContains "$domain" "$vhostList"; then openlitespeedConfigEdit vhost_down "$domain" || return 1 fi } # Marks a virtual host as active. # $1 (domain): site domain name. function openlitespeedVHostUp() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local vhostList error run vhostList error openlitespeedVhostList || return 1 if listContains "$domain" "$vhostList"; then openlitespeedConfigEdit vhost_up "$domain" || return 1 fi } # Updates the Traefik middleware IP whitelist for the OLS admin panel from $openlitespeedAdminWhiteList. function openlitespeedAdminWhiteList() { local ipList=() whiteList error for i in "${!openlitespeedAdminWhiteList[@]}"; do ipList[$i]="\"${openlitespeedAdminWhiteList[$i]}\"" done whiteList=$(IFS=','; printf '%s' "${ipList[*]}") runError error k3sRun patch middleware "$openlitespeedKube-admin-allowlist" --type=merge -p "{\"spec\":{\"ipWhiteList\":{\"sourceRange\":[${whiteList}]}}}" \ || { appError "$error"; return 1; } printf '%s' "$whiteList" } # Restricts OLS admin access to Traefik pod IPs only by updating the admin_config.conf ACL. function openlitespeedAdminAllowList() { local podList podList=$("$k3sCmd" kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' | awk 'NF') local -a ipList mapfile -t ipList < <(printf '%s\n' "$podList") [[ "${#ipList[@]}" -gt 0 ]] || { appError "Traefik pod IPs not found"; return 1; } local allowList allowList=$(IFS=','; printf '%s' "${ipList[*]}") fileBackup "$openlitespeedAdminPath/admin_config.conf" || return 1 openlitespeedAdminConfigEditSet 'accessControl' deny 'ALL' || return 1 openlitespeedAdminConfigEditSet 'accessControl' allow "$allowList" || return 1 printf '%s' "$allowList" }