From 41eaf923aaf4d24fb88e343be12ef637574c1e2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Aug 2026 10:27:26 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Sync=202026-08-07=2010:27:26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gecoosac/Makefile | 2 +- gecoosac/files/etc/init.d/gecoosac | 103 +++++--- gecoosac/files/etc/uci-defaults/gecoosac | 126 ++++++---- ghttpd/Makefile | 4 +- ghttpd/files/ghttpd.init | 18 ++ luci-app-daede/Makefile | 2 +- .../usr/share/luci-app-daede/update-pkg.sh | 14 +- luci-app-gecoosac/Makefile | 3 +- .../luci-static/resources/view/gecoosac.js | 75 ++++-- .../root/usr/libexec/rpcd/luci.gecoosac | 231 +++++++++++++----- 10 files changed, 402 insertions(+), 176 deletions(-) diff --git a/gecoosac/Makefile b/gecoosac/Makefile index 89eb090d..ea6c119b 100644 --- a/gecoosac/Makefile +++ b/gecoosac/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gecoosac PKG_VERSION:=2.2.20251015 -PKG_RELEASE:=15 +PKG_RELEASE:=16 PKG_MAINTAINER:=Roc Lai PKG_LICENSE:=AGPL-3.0-only diff --git a/gecoosac/files/etc/init.d/gecoosac b/gecoosac/files/etc/init.d/gecoosac index e91c30b9..35392876 100644 --- a/gecoosac/files/etc/init.d/gecoosac +++ b/gecoosac/files/etc/init.d/gecoosac @@ -332,8 +332,6 @@ generate_default_cert() { return 0 fi - mkdir -p /etc/gecoosac/tls - cert_cn="${cert_ip:-$cert_host}" cert_san="DNS:$cert_host" [ -n "$cert_ip" ] && cert_san="${cert_san},IP:${cert_ip}" @@ -371,6 +369,8 @@ generate_default_cert() { } normalize_conf() { + [ "$isonlyoneprot" = "0" ] || isonlyoneprot="1" + [ "$https" = "1" ] || https="0" if is_safe_upload_dir "$upload_dir"; then upload_dir="$(normalize_path "$upload_dir")" else @@ -383,14 +383,16 @@ normalize_conf() { logger -t gecoosac "refusing unsupported database directory: $db_dir" return 1 fi - is_abs_path "$crt_file" || { - logger -t gecoosac "refusing non-absolute certificate path: $crt_file" - return 1 - } - is_abs_path "$key_file" || { - logger -t gecoosac "refusing non-absolute key path: $key_file" - return 1 - } + if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then + is_abs_path "$crt_file" || { + logger -t gecoosac "refusing non-absolute certificate path: $crt_file" + return 1 + } + is_abs_path "$key_file" || { + logger -t gecoosac "refusing non-absolute key path: $key_file" + return 1 + } + fi if is_safe_pid_dir "$piddir" "$upload_dir"; then piddir="$(normalize_path "$piddir")" else @@ -401,14 +403,14 @@ normalize_conf() { logger -t gecoosac "refusing invalid interface port: $port" return 1 } - is_port "$m_port" || { - logger -t gecoosac "refusing invalid management port: $m_port" - return 1 - } + if [ "$isonlyoneprot" = "0" ]; then + is_port "$m_port" || { + logger -t gecoosac "refusing invalid management port: $m_port" + return 1 + } + fi [ "$enabled" = "1" ] || enabled="0" - [ "$isonlyoneprot" = "0" ] || isonlyoneprot="1" - [ "$https" = "1" ] || https="0" case "$lang" in zh|en) ;; *) lang="$DEFAULT_LANG" ;; @@ -419,22 +421,17 @@ normalize_conf() { } ensure_dirs() { - local path - - path="$(normalize_path "$upload_dir")" || return 1 - if ! ensure_dir_tree "$path" || ! mkdir -p "$db_dir" "$piddir" /etc/gecoosac/tls; then + if ! ensure_managed_dir upload "$upload_dir" || \ + ! ensure_managed_dir db "$db_dir" || \ + ! ensure_managed_dir pid "$piddir" || \ + ! ensure_managed_dir tls /etc/gecoosac/tls; then logger -t gecoosac "failed to create runtime directories" return 1 fi - chmod go-w "$path" || return 1 - is_secure_upload_dir "$path" || { - logger -t gecoosac "upload directory or its parent is not root-owned and private: $path" - return 1 - } } -ensure_dir_tree() { - local path current part rest +ensure_secure_dir_tree() { + local path current part rest mode path="$(normalize_path "$1")" || return 1 current="/" @@ -451,11 +448,53 @@ ensure_dir_tree() { if [ -e "$current" ]; then [ -d "$current" ] || return 1 else - mkdir "$current" || return 1 + mode=0755 + [ -n "$rest" ] || mode=0750 + mkdir -m "$mode" "$current" || return 1 fi + case "$current" in + /tmp) is_secure_dir "$current" 1 || return 1 ;; + *) is_secure_dir "$current" || return 1 ;; + esac done } +managed_dir_path() { + local role="$1" path="$2" anchor + + case "$role:$path" in + db:/var/lib/gecoosac|db:/var/lib/gecoosac/*) + anchor="$(readlink -f /var 2>/dev/null)" || return 1 + case "$anchor" in + /var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;; + *) return 1 ;; + esac + ;; + pid:/var/run|pid:/var/run/*) + anchor="$(readlink -f /var/run 2>/dev/null)" || return 1 + [ "$anchor" = "/tmp/run" ] || return 1 + printf '%s%s\n' "$anchor" "${path#/var/run}" + ;; + *) printf '%s\n' "$path" ;; + esac +} + +ensure_managed_dir() { + local role="$1" path physical + + path="$(normalize_path "$2")" || return 1 + case "$role" in + upload) is_safe_upload_dir "$path" || return 1 ;; + db) is_safe_db_dir "$path" "$upload_dir" || return 1 ;; + pid) is_safe_pid_dir "$path" "$upload_dir" || return 1 ;; + tls) [ "$path" = "/etc/gecoosac/tls" ] || return 1 ;; + *) return 1 ;; + esac + physical="$(managed_dir_path "$role" "$path")" || return 1 + ensure_secure_dir_tree "$physical" || return 1 + [ -d "$path" ] +} + prepare_service() { init_conf [ "$enabled" = "1" ] || return 0 @@ -528,16 +567,16 @@ wait_service_stopped() { local i if ! type service_running >/dev/null 2>&1; then - return 0 + return 1 fi i=0 - while [ "$i" -lt 5 ]; do - service_running gecoosac || return 0 + while service_running gecoosac; do + [ "$i" -ge 5 ] && return 1 sleep 1 i=$((i + 1)) done - return 1 + return 0 } reload_service() { diff --git a/gecoosac/files/etc/uci-defaults/gecoosac b/gecoosac/files/etc/uci-defaults/gecoosac index ec236b19..795cf0f6 100644 --- a/gecoosac/files/etc/uci-defaults/gecoosac +++ b/gecoosac/files/etc/uci-defaults/gecoosac @@ -125,15 +125,14 @@ normalize_upload_dir() { upload_dir="$(uci -q get gecoosac.config.upload_dir)" [ -n "$upload_dir" ] || return 0 - - if is_safe_upload_dir "$upload_dir"; then - normalized="$(normalize_path "$upload_dir")" - else - return 0 - fi + normalized="$(normalize_path "$upload_dir")" || return 0 + case "$normalized" in + /etc/gecoosac/upload) normalized="$DEFAULT_UPLOAD_DIR" ;; + *) is_safe_upload_dir "$normalized" || return 0 ;; + esac [ "$upload_dir" = "$normalized" ] && return 0 - uci -q set "gecoosac.config.upload_dir=${normalized}" + uci -q set "gecoosac.config.upload_dir=${normalized}" || return 1 changed=1 } @@ -158,73 +157,90 @@ normalize_dir_option() { changed=1 } +is_managed_cert_path() { + case "$1" in + ""|"$2"|"$3") return 0 ;; + esac + return 1 +} + +is_regular_file_or_absent() { + [ ! -e "$1" ] && [ ! -L "$1" ] && return 0 + [ -f "$1" ] && [ ! -L "$1" ] +} + migrate_legacy_cert_paths() { - local crt_file key_file migrate + local crt_file key_file old_crt old_key new_crt new_key path crt_file="$(uci -q get gecoosac.config.crt_file)" key_file="$(uci -q get gecoosac.config.key_file)" - migrate=0 + is_managed_cert_path "$crt_file" "$OLD_CRT_FILE" "$DEFAULT_CRT_FILE" || return 0 + is_managed_cert_path "$key_file" "$OLD_KEY_FILE" "$DEFAULT_KEY_FILE" || return 0 - if [ "$crt_file" = "$OLD_CRT_FILE" ] && [ -z "$key_file" ]; then - uci -q set "gecoosac.config.key_file=${OLD_KEY_FILE}" || return 1 - key_file="$OLD_KEY_FILE" - changed=1 - elif [ -z "$crt_file" ] && [ "$key_file" = "$OLD_KEY_FILE" ]; then - uci -q set "gecoosac.config.crt_file=${OLD_CRT_FILE}" || return 1 - crt_file="$OLD_CRT_FILE" + for path in "$OLD_CRT_FILE" "$OLD_KEY_FILE" "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; do + is_regular_file_or_absent "$path" || { + logger -t gecoosac "refusing unsafe certificate migration path: $path" + return 1 + } + done + + old_crt=0 + old_key=0 + new_crt=0 + new_key=0 + [ -f "$OLD_CRT_FILE" ] && old_crt=1 + [ -f "$OLD_KEY_FILE" ] && old_key=1 + [ -f "$DEFAULT_CRT_FILE" ] && new_crt=1 + [ -f "$DEFAULT_KEY_FILE" ] && new_key=1 + + if { [ "$old_crt" = "1" ] && [ "$new_crt" = "1" ]; } || \ + { [ "$old_key" = "1" ] && [ "$new_key" = "1" ]; }; then + logger -t gecoosac "refusing conflicting legacy and current certificate files" + return 1 + fi + if [ "$old_crt" = "1" ] && [ "$new_key" = "1" ]; then + logger -t gecoosac "refusing reverse-split certificate migration state" + return 1 + fi + if [ $((old_crt + new_crt)) -ne $((old_key + new_key)) ]; then + logger -t gecoosac "refusing incomplete certificate migration state" + return 1 + fi + + if [ "$old_crt" = "1" ]; then + mv "$OLD_CRT_FILE" "$DEFAULT_CRT_FILE" || return 1 + fi + if [ "$old_key" = "1" ]; then + mv "$OLD_KEY_FILE" "$DEFAULT_KEY_FILE" || return 1 + fi + if [ -f "$DEFAULT_CRT_FILE" ]; then + chmod 644 "$DEFAULT_CRT_FILE" || return 1 + chmod 600 "$DEFAULT_KEY_FILE" || return 1 + fi + + if [ "$crt_file" != "$DEFAULT_CRT_FILE" ]; then + uci -q set "gecoosac.config.crt_file=${DEFAULT_CRT_FILE}" || return 1 changed=1 fi - - if [ "$crt_file" = "$OLD_CRT_FILE" ] && [ "$key_file" = "$OLD_KEY_FILE" ]; then - migrate=1 - elif [ -z "$crt_file" ] && [ -z "$key_file" ]; then - [ -f "$OLD_CRT_FILE" ] && [ -f "$OLD_KEY_FILE" ] || return 0 - migrate=1 + if [ "$key_file" != "$DEFAULT_KEY_FILE" ]; then + uci -q set "gecoosac.config.key_file=${DEFAULT_KEY_FILE}" || return 1 + changed=1 fi - - [ "$migrate" = "1" ] || return 0 - [ -f "$OLD_CRT_FILE" ] && [ ! -L "$OLD_CRT_FILE" ] || return 0 - [ -f "$OLD_KEY_FILE" ] && [ ! -L "$OLD_KEY_FILE" ] || return 0 - [ ! -e "$DEFAULT_CRT_FILE" ] && [ ! -L "$DEFAULT_CRT_FILE" ] || return 0 - [ ! -e "$DEFAULT_KEY_FILE" ] && [ ! -L "$DEFAULT_KEY_FILE" ] || return 0 - - mkdir -p /etc/gecoosac/tls || { - logger -t gecoosac "unable to prepare TLS directory for legacy certificate migration" - return 1 - } - if ! mv "$OLD_CRT_FILE" "$DEFAULT_CRT_FILE"; then - logger -t gecoosac "unable to migrate legacy certificate" - return 1 - fi - if ! mv "$OLD_KEY_FILE" "$DEFAULT_KEY_FILE"; then - mv "$DEFAULT_CRT_FILE" "$OLD_CRT_FILE" 2>/dev/null - logger -t gecoosac "unable to migrate legacy private key" - return 1 - fi - if ! chmod 644 "$DEFAULT_CRT_FILE" || ! chmod 600 "$DEFAULT_KEY_FILE" || \ - ! uci -q set "gecoosac.config.crt_file=${DEFAULT_CRT_FILE}" || \ - ! uci -q set "gecoosac.config.key_file=${DEFAULT_KEY_FILE}"; then - mv "$DEFAULT_KEY_FILE" "$OLD_KEY_FILE" 2>/dev/null - mv "$DEFAULT_CRT_FILE" "$OLD_CRT_FILE" 2>/dev/null - logger -t gecoosac "unable to complete legacy certificate migration" - return 1 - fi - changed=1 } migrate_config() { local compat + migrate_legacy_cert_paths || return 1 compat="$(uci -q get gecoosac.config.config_compat)" [ "$compat" = "$CONFIG_COMPAT" ] && return 0 - migrate_legacy_cert_paths || return 1 uci -q set "gecoosac.config.config_compat=${CONFIG_COMPAT}" || return 1 changed=1 } ensure_section migrate_config || exit 1 -normalize_upload_dir +normalize_upload_dir || exit 1 normalize_dir_option db_dir "$DEFAULT_DB_DIR" is_safe_db_dir normalize_dir_option piddir "$DEFAULT_PID_DIR" is_safe_pid_dir set_default enabled 0 @@ -243,6 +259,8 @@ set_default debug 0 set_default showtip 0 set_default log 0 -[ "$changed" = "1" ] && uci -q commit gecoosac +if [ "$changed" = "1" ]; then + uci -q commit gecoosac || exit 1 +fi exit 0 diff --git a/ghttpd/Makefile b/ghttpd/Makefile index fa95d4ba..4f99aaf5 100644 --- a/ghttpd/Makefile +++ b/ghttpd/Makefile @@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk PKG_ARCH_ghttpd:=$(ARCH) PKG_NAME:=ghttpd -PKG_VERSION:=0.0.1 -PKG_RELEASE:=3 +PKG_VERSION:=0.0.2 +PKG_RELEASE:=4 PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://github.com/Carseason/openwrt-packages/releases/download/prebuilt/ PKG_HASH:=skip diff --git a/ghttpd/files/ghttpd.init b/ghttpd/files/ghttpd.init index d2a04174..d010b7ab 100755 --- a/ghttpd/files/ghttpd.init +++ b/ghttpd/files/ghttpd.init @@ -16,6 +16,13 @@ stop_default_httpd_services() { done } +start_default_httpd_services() { + for service in uhttpd nginx; do + [ -x "/etc/init.d/$service" ] || continue + "/etc/init.d/$service" enabled && "/etc/init.d/$service" start + done +} + start_service() { config_load ghttpd config_foreach get_config ghttpd @@ -31,6 +38,17 @@ start_service() { procd_close_instance } +stop_service() { + procd_kill "$NAME" + config_load ghttpd + config_foreach get_config ghttpd + start_default_httpd_services +} + service_triggers() { procd_add_reload_trigger "ghttpd" } + +reload_service() { + restart +} diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index bba0bb62..1ed00e19 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede PKG_VERSION:=1.14.7 -PKG_RELEASE:=37 +PKG_RELEASE:=38 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-daede/root/usr/share/luci-app-daede/update-pkg.sh b/luci-app-daede/root/usr/share/luci-app-daede/update-pkg.sh index 13d8ed19..d0f26c3c 100755 --- a/luci-app-daede/root/usr/share/luci-app-daede/update-pkg.sh +++ b/luci-app-daede/root/usr/share/luci-app-daede/update-pkg.sh @@ -56,17 +56,17 @@ fi if [ -n "$ver" ]; then constraint="$PKG=$ver" else - constraint="$PKG" + echo "result: 软件源中没有找到 $PKG,请检查网络或软件源配置" + exit 1 fi echo "--- apk add -s $constraint ---" - if apk add -s "$constraint" 2>&1; then - echo "--- apk add $constraint ---" - apk add "$constraint" 2>&1 - else - echo "result: apk cannot resolve $constraint; no packages were changed" - exit 1 + if ! apk add -s "$constraint" 2>&1; then + echo "note: apk 预检失败,通常是系统上其它软件包的问题,继续尝试升级" fi + echo "--- apk add $constraint ---" + apk add "$constraint" 2>&1 + exit 0 ) 9>/tmp/luci-app-daede.apk.lock rc=$? if [ "$rc" != 0 ]; then diff --git a/luci-app-gecoosac/Makefile b/luci-app-gecoosac/Makefile index a3be5869..24b3b6c4 100644 --- a/luci-app-gecoosac/Makefile +++ b/luci-app-gecoosac/Makefile @@ -7,10 +7,11 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-gecoosac PKG_VERSION:=2.2 -PKG_RELEASE:=15 +PKG_RELEASE:=16 LUCI_TITLE:=LuCI Support for gecoosac LUCI_DEPENDS:=+luci-base +gecoosac +LUCI_EXTRA_DEPENDS:=gecoosac (>=2.2.20251015-r5) LUCI_PKGARCH:=all PKG_LICENSE:=AGPL-3.0-only diff --git a/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js b/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js index 5dcf92ba..b8f82e04 100644 --- a/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js +++ b/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js @@ -42,11 +42,16 @@ function validPortValue(value) { return /^[0-9]+$/.test(text) && Number.isSafeInteger(port) && port >= 1 && port <= 65535; } -function validatePortValue(section_id, value, otherOption, singlePortOption) { + +function validatePortValue(section_id, value, otherOption, singlePortOption, activeInSinglePort) { + const singlePort = singlePortOption.formvalue(section_id); + + if (!activeInSinglePort && singlePort !== '0') + return true; + if (!validPortValue(value)) return _('Port must be an integer between 1 and 65535.'); - const singlePort = singlePortOption.formvalue(section_id); const otherValue = otherOption.formvalue(section_id); if (singlePort === '0' && validPortValue(otherValue) && Number(value) === Number(otherValue)) @@ -55,6 +60,25 @@ function validatePortValue(section_id, value, otherOption, singlePortOption) { return true; } +function validateCertificatePath(section_id, value, singlePortOption, httpsOption) { + if (singlePortOption.formvalue(section_id) !== '0' || httpsOption.formvalue(section_id) !== '1' || !value) + return true; + + return String(value).charAt(0) === '/' ? true : _('Expecting an absolute path'); +} + +function triggerActiveValidation(section_id, options) { + for (const option of options) { + if (!option.isActive(section_id)) + continue; + + const element = option.getUIElement(section_id); + + if (element) + element.triggerValidation(); + } +} + function normalizePath(value) { const path = String(value || ''); @@ -217,6 +241,7 @@ return view.extend({ render(data) { let m, s, o, uploadDirOption; let portOption, managementPortOption, singlePortOption; + let httpsOption, certificateOption, keyOption; m = new form.Map('gecoosac', _('Gecoos AC'), _('Only supports Gecoos AP firmware 7.6 and above.') + '
' + @@ -272,10 +297,10 @@ return view.extend({ o.depends('isonlyoneprot', '0'); portOption.validate = function(section_id, value) { - return validatePortValue(section_id, value, managementPortOption, singlePortOption); + return validatePortValue(section_id, value, managementPortOption, singlePortOption, true); }; managementPortOption.validate = function(section_id, value) { - return validatePortValue(section_id, value, portOption, singlePortOption); + return validatePortValue(section_id, value, portOption, singlePortOption, false); }; singlePortOption.validate = function(section_id, value) { if (value === '0') { @@ -288,34 +313,46 @@ return view.extend({ return true; }; - const revalidatePorts = function(_event, section_id) { - for (const option of [ portOption, managementPortOption, singlePortOption ]) { - const element = option.getUIElement(section_id); - - if (element) - element.triggerValidation(); - } - }; - portOption.onchange = revalidatePorts; - managementPortOption.onchange = revalidatePorts; - singlePortOption.onchange = revalidatePorts; - - o = s.option(form.Flag, 'https', _('Enable HTTPS service'), + httpsOption = s.option(form.Flag, 'https', _('Enable HTTPS service'), _('Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key.')); + o = httpsOption; o.default = '0'; o.depends('isonlyoneprot', '0'); - o = s.option(form.Value, 'crt_file', _('Specify crt certificate file')); + certificateOption = s.option(form.Value, 'crt_file', _('Specify crt certificate file')); + o = certificateOption; o.placeholder = DEFAULT_CRT_FILE; o.default = DEFAULT_CRT_FILE; o.datatype = 'file'; o.depends({ isonlyoneprot: '0', https: '1' }); + o.validate = function(section_id, value) { + return validateCertificatePath(section_id, value, singlePortOption, httpsOption); + }; - o = s.option(form.Value, 'key_file', _('Specify key certificate file')); + keyOption = s.option(form.Value, 'key_file', _('Specify key certificate file')); + o = keyOption; o.placeholder = DEFAULT_KEY_FILE; o.default = DEFAULT_KEY_FILE; o.datatype = 'file'; o.depends({ isonlyoneprot: '0', https: '1' }); + o.validate = function(section_id, value) { + return validateCertificatePath(section_id, value, singlePortOption, httpsOption); + }; + + const revalidateProtocolOptions = function(_event, section_id) { + triggerActiveValidation(section_id, [ + portOption, + managementPortOption, + singlePortOption, + httpsOption, + certificateOption, + keyOption + ]); + }; + portOption.onchange = revalidateProtocolOptions; + managementPortOption.onchange = revalidateProtocolOptions; + singlePortOption.onchange = revalidateProtocolOptions; + httpsOption.onchange = revalidateProtocolOptions; o = s.option(form.Value, 'upload_dir', _('Upload dir path'), _('Upload AP upgrade firmware here. Use an absolute path ending with /gecoosac/upload, for example /tmp/gecoosac/upload.
Do not place it under /etc/gecoosac because that directory is backed up during sysupgrade.')); diff --git a/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac b/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac index 0c9f1a6a..bf50d332 100755 --- a/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac +++ b/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac @@ -132,6 +132,7 @@ configured_path_in_upload() { path="$(normalize_path "$path")" || return 2 path_in_dir "$path" "$upload_path" && return 0 + [ -e "$path" ] || [ -L "$path" ] || return 1 real_path="$(readlink -f "$path" 2>/dev/null)" || return 2 [ -n "$real_path" ] || return 2 @@ -165,29 +166,74 @@ service_status() { } if json_select gecoosac 2>/dev/null; then - if ! json_select instances 2>/dev/null; then - json_cleanup - status_result 0 0 "Invalid service status response" - return + if json_select instances 2>/dev/null; then + json_get_keys instances + for instance in $instances; do + json_select "$instance" 2>/dev/null || continue + json_get_var state running + json_select .. + if [ "$state" = "1" ]; then + running=1 + break + fi + done fi - json_get_keys instances - for instance in $instances; do - json_select "$instance" 2>/dev/null || continue - json_get_var state running - json_select .. - if [ "$state" = "1" ]; then - running=1 - break - fi - done fi json_cleanup status_result 1 "$running" } +clear_upload_unlock() { + flock -u 9 >/dev/null 2>&1 + exec 9<&- +} + +clear_upload_locked_result() { + clear_upload_unlock + json_result "$1" "$2" "$3" +} + +is_safe_clear_stage() { + local stage="$1" + + [ -d "$stage" ] && [ ! -L "$stage" ] || return 1 + is_secure_dir "$stage" || return 1 + if [ -e "$stage/upload" ] || [ -L "$stage/upload" ]; then + [ -d "$stage/upload" ] && [ ! -L "$stage/upload" ] || return 1 + is_secure_upload_dir "$stage/upload" || return 1 + fi +} + +ensure_live_upload_dir() { + local path="$1" + + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + mkdir "$path" 2>/dev/null || { + [ -d "$path" ] && [ ! -L "$path" ] || return 1 + } + fi + [ -d "$path" ] && [ ! -L "$path" ] || return 1 + chmod 0750 "$path" || return 1 + is_secure_upload_dir "$path" +} + +validate_clear_protected_paths() { + local root="$1" option + + for option in db_dir piddir crt_file key_file; do + configured_path_in_upload "$option" "$root" + case "$?" in + 0) return 0 ;; + 1) ;; + *) return 2 ;; + esac + done + return 1 +} + clear_upload() { - local path real parent stage option + local path real parent stage option candidate stages live_exists path="$(uci -q get gecoosac.config.upload_dir)" [ -n "$path" ] || path="$DEFAULT_UPLOAD_DIR" @@ -198,70 +244,137 @@ clear_upload() { esac path="$(normalize_path "$path")" || { json_result 0 "Expecting an absolute path"; return; } - real="$(readlink -f "$path" 2>/dev/null)" + if ! safe_upload_path "$path"; then + json_result 0 "Only Gecoos upload directories can be cleared" "$path" + return + fi + + parent="${path%/*}" + if [ ! -e "$parent" ] && [ ! -L "$parent" ]; then + json_result 1 "" "$path" + return + fi + if ! is_secure_upload_dir "$parent"; then + json_result 0 "Upload directory or its parent is not root-owned and private" "$path" + return + fi + + if [ -e "$path" ] || [ -L "$path" ]; then + real="$(readlink -f "$path" 2>/dev/null)" + else + real="$path" + fi [ -n "$real" ] || { json_result 0 "Unable to resolve upload directory" "$path"; return; } real="$(normalize_path "$real")" || { json_result 0 "Expecting an absolute path"; return; } + [ "$real" = "$path" ] || { json_result 0 "Only Gecoos upload directories can be cleared" "$real"; return; } - if [ "$real" != "$path" ] || ! safe_upload_path "$real"; then - json_result 0 "Only Gecoos upload directories can be cleared" "$real" - return - fi - if [ ! -e "$real" ] && [ ! -L "$real" ]; then - json_result 1 "" "$real" - return - fi - if ! is_secure_upload_dir "$real"; then - json_result 0 "Upload directory or its parent is not root-owned and private" "$real" - return - fi - - for option in db_dir piddir crt_file key_file; do - configured_path_in_upload "$option" "$real" - case "$?" in - 0) json_result 0 "Upload directory contains a configured protected path" "$real"; return ;; - 1) ;; - *) json_result 0 "Unable to validate configured paths" "$real"; return ;; - esac - done - - parent="${real%/*}" - stage="$(mktemp -d "$parent/.gecoosac-clear.XXXXXX" 2>/dev/null)" || { + exec 9<"$parent" 2>/dev/null || { json_result 0 "Unable to prepare upload directory cleanup" "$real" return } - if ! chmod 0700 "$stage"; then - rm -rf "$stage" + if ! flock -n 9 >/dev/null 2>&1; then + clear_upload_unlock json_result 0 "Unable to prepare upload directory cleanup" "$real" return fi - if ! mv "$real" "$stage/upload"; then - rm -rf "$stage" - json_result 0 "Unable to prepare upload directory cleanup" "$real" + if ! is_secure_upload_dir "$parent"; then + clear_upload_locked_result 0 "Upload directory or its parent is not root-owned and private" "$real" return fi - - if ! rm -rf "$stage/upload"; then - if [ ! -e "$real" ] && [ ! -L "$real" ]; then - mkdir "$real" 2>/dev/null && chmod 0750 "$real" 2>/dev/null + parent="${real%/*}" + stages=0 + for candidate in "$parent"/.gecoosac-clear.??????; do + [ "$candidate" != "$parent/.gecoosac-clear.??????" ] || continue + stages=1 + is_safe_clear_stage "$candidate" || { + clear_upload_locked_result 0 "Unable to validate upload cleanup stage" "$candidate" + return + } + if [ -e "$candidate/upload" ] || [ -L "$candidate/upload" ]; then + validate_clear_protected_paths "$candidate/upload" + case "$?" in + 0) clear_upload_locked_result 0 "Upload cleanup stage contains a configured protected path" "$candidate"; return ;; + 1) ;; + *) clear_upload_locked_result 0 "Unable to validate configured paths" "$candidate"; return ;; + esac fi - logger -t gecoosac "upload cleanup retained staged data at $stage" - json_result 0 "Unable to remove upload directory contents" "$real" + done + + live_exists=0 + [ -e "$real" ] || [ -L "$real" ] && live_exists=1 + if [ "$live_exists" = "1" ] && ! is_secure_upload_dir "$real"; then + clear_upload_locked_result 0 "Upload directory or its parent is not root-owned and private" "$real" return fi + if [ "$live_exists" = "0" ] && [ "$stages" = "0" ]; then + clear_upload_locked_result 1 "" "$real" + return + fi + if [ "$live_exists" = "1" ]; then + validate_clear_protected_paths "$real" + case "$?" in + 0) clear_upload_locked_result 0 "Upload directory contains a configured protected path" "$real"; return ;; + 1) ;; + *) clear_upload_locked_result 0 "Unable to validate configured paths" "$real"; return ;; + esac + fi + if [ "$live_exists" = "0" ]; then + ensure_live_upload_dir "$real" || { + clear_upload_locked_result 0 "Unable to recreate upload directory" "$real" + return + } + fi - if [ ! -d "$real" ]; then - if ! mkdir "$real" && { [ ! -d "$real" ] || [ -L "$real" ]; }; then - json_result 0 "Unable to recreate upload directory" "$real" + for candidate in "$parent"/.gecoosac-clear.??????; do + [ "$candidate" != "$parent/.gecoosac-clear.??????" ] || continue + if [ -e "$candidate/upload" ] || [ -L "$candidate/upload" ]; then + rm -rf "$candidate/upload" || { + logger -t gecoosac "upload cleanup retained staged data at $candidate" + clear_upload_locked_result 0 "Unable to remove upload directory contents" "$real" + return + } + fi + rmdir "$candidate" || { + clear_upload_locked_result 0 "Unable to remove upload cleanup stage" "$candidate" + return + } + done + + if [ "$live_exists" = "1" ]; then + stage="$(mktemp -d "$parent/.gecoosac-clear.XXXXXX" 2>/dev/null)" || { + clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$real" + return + } + if ! chmod 0700 "$stage" || ! mv "$real" "$stage/upload"; then + rm -rf "$stage" + clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$real" return fi - fi - if [ -L "$real" ] || ! chmod 0750 "$real" || ! is_secure_upload_dir "$real" || ! rmdir "$stage"; then - json_result 0 "Unable to recreate upload directory" "$real" - return + if ! ensure_live_upload_dir "$real"; then + if [ ! -e "$real" ] && [ ! -L "$real" ]; then + mv "$stage/upload" "$real" 2>/dev/null + fi + clear_upload_locked_result 0 "Unable to recreate upload directory" "$real" + return + fi + else + stage= fi - json_result 1 "" "$real" + if [ "$live_exists" = "1" ]; then + if ! rm -rf "$stage/upload"; then + logger -t gecoosac "upload cleanup retained staged data at $stage" + clear_upload_locked_result 0 "Unable to remove upload directory contents" "$real" + return + fi + rmdir "$stage" || { + clear_upload_locked_result 0 "Unable to remove upload cleanup stage" "$stage" + return + } + fi + + clear_upload_locked_result 1 "" "$real" } case "$1" in