diff --git a/gecoosac/Makefile b/gecoosac/Makefile index 4d73d277..b087be77 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:=18 +PKG_RELEASE:=19 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 718a2cfa..7daa7d9c 100644 --- a/gecoosac/files/etc/init.d/gecoosac +++ b/gecoosac/files/etc/init.d/gecoosac @@ -86,6 +86,29 @@ normalize_path() { printf '%s\n' "$normalized" } +path_uses_clear_stage() { + local path="$1" resolved + + path="$(normalize_path "$path")" || return 1 + case "$path" in + /.gecoosac-clear.*|/.gecoosac-clear.*/*|*/.gecoosac-clear.*|*/.gecoosac-clear.*/*) + return 0 + ;; + esac + + if [ -e "$1" ] || [ -L "$1" ]; then + resolved="$(readlink -f "$1" 2>/dev/null)" || return 1 + resolved="$(normalize_path "$resolved")" || return 1 + case "$resolved" in + /.gecoosac-clear.*|/.gecoosac-clear.*/*|*/.gecoosac-clear.*|*/.gecoosac-clear.*/*) + return 0 + ;; + esac + fi + + return 1 +} + is_safe_upload_dir() { local path physical @@ -468,12 +491,20 @@ normalize_conf() { logger -t gecoosac "refusing unsupported upload directory: $upload_dir" return 1 fi + if path_uses_clear_stage "$upload_dir"; then + logger -t gecoosac "refusing reserved upload cleanup stage path: $upload_dir" + return 1 + fi if is_safe_db_dir "$db_dir" "$upload_dir"; then db_dir="$(normalize_path "$db_dir")" else logger -t gecoosac "refusing unsupported database directory: $db_dir" return 1 fi + if path_uses_clear_stage "$db_dir"; then + logger -t gecoosac "refusing reserved upload cleanup stage path: $db_dir" + return 1 + fi if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then is_abs_path "$crt_file" || { logger -t gecoosac "refusing non-absolute certificate path: $crt_file" @@ -483,6 +514,10 @@ normalize_conf() { logger -t gecoosac "refusing non-absolute key path: $key_file" return 1 } + if path_uses_clear_stage "$crt_file" || path_uses_clear_stage "$key_file"; then + logger -t gecoosac "refusing reserved upload cleanup stage certificate path" + return 1 + fi fi if is_safe_pid_dir "$piddir" "$upload_dir"; then piddir="$(normalize_path "$piddir")" @@ -490,6 +525,10 @@ normalize_conf() { logger -t gecoosac "refusing unsupported PID directory: $piddir" return 1 fi + if path_uses_clear_stage "$piddir"; then + logger -t gecoosac "refusing reserved upload cleanup stage path: $piddir" + return 1 + fi is_port "$port" || { logger -t gecoosac "refusing invalid interface port: $port" return 1 @@ -659,8 +698,16 @@ start_prepared_service() { } start_service() { - prepare_service || exit 1 - start_prepared_service + service_prepare_failed=0 + if ! prepare_service; then + service_prepare_failed=1 + return 0 + fi + start_prepared_service || service_prepare_failed=1 +} + +service_started() { + [ "${service_prepare_failed:-0}" = "0" ] } service_running_state() { @@ -709,7 +756,7 @@ wait_service_stopped() { state="$?" case "$state" in 0) return 0 ;; - 1) + 1|2) [ "$i" -ge 5 ] && return 1 sleep 1 i=$((i + 1)) diff --git a/luci-app-gecoosac/Makefile b/luci-app-gecoosac/Makefile index 41a47bf8..98d23a30 100644 --- a/luci-app-gecoosac/Makefile +++ b/luci-app-gecoosac/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-gecoosac PKG_VERSION:=2.2 -PKG_RELEASE:=18 +PKG_RELEASE:=19 LUCI_TITLE:=LuCI Support for gecoosac LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files 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 bdb77fd5..dcadbec7 100644 --- a/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js +++ b/luci-app-gecoosac/htdocs/luci-static/resources/view/gecoosac.js @@ -14,6 +14,7 @@ const DEFAULT_PID_DIR = '/var/run'; const CONFIG_BACKUP_DIR = '/etc/gecoosac'; const DB_DIR_PREFIXES = [ '/etc/gecoosac', '/tmp/gecoosac', '/var/lib/gecoosac' ]; const PID_DIR_PREFIXES = [ '/var/run', '/tmp/gecoosac' ]; +const CLEAR_STAGE_PATH_ERROR = _('Paths under .gecoosac-clear.* are reserved for upload cleanup.'); let statusPollRegistered = false; @@ -92,7 +93,12 @@ function validateCertificatePath(section_id, value, singlePortOption, httpsOptio if (singlePortOption.formvalue(section_id) !== '0' || httpsOption.formvalue(section_id) !== '1' || !value) return true; - return String(value).charAt(0) === '/' ? true : _('Expecting an absolute path'); + if (String(value).charAt(0) !== '/') + return _('Expecting an absolute path'); + + return usesClearStagePath(value) + ? CLEAR_STAGE_PATH_ERROR + : true; } function triggerActiveValidation(section_id, options) { @@ -134,6 +140,17 @@ function normalizePath(value) { return '/' + parts.join('/'); } +function usesClearStagePath(value) { + const path = normalizePath(value); + const segments = path === null ? [] : path.split('/'); + + for (const segment of segments) + if (segment.indexOf('.gecoosac-clear.') === 0) + return true; + + return false; +} + function managedPath(value, policy) { const path = normalizePath(value); @@ -247,14 +264,14 @@ function clientHost() { return host; } -function clientUrl() { - const singlePort = uci.get('gecoosac', 'config', 'isonlyoneprot') !== '0'; - const https = uci.get('gecoosac', 'config', 'https') === '1'; - const port = singlePort - ? validPort(uci.get('gecoosac', 'config', 'port'), '60650') - : validPort(uci.get('gecoosac', 'config', 'm_port'), '8080'); +function clientUrl(status) { + const protocol = status && status.protocol; + const port = validPort(status && status.port, null); - return (singlePort || !https ? 'http://' : 'https://') + clientHost() + ':' + port; + if ((protocol === 'http' || protocol === 'https') && port !== null) + return protocol + '://' + clientHost() + ':' + port; + + return null; } function renderStatusContent(status) { @@ -263,12 +280,13 @@ function renderStatusContent(status) { (RPC_ERROR_MESSAGES[status.error] || _('Unable to query service status'))); const running = serviceRunning(status); + const url = running ? clientUrl(status) : null; const text = running ? _('The GecoosAC service is running.') : _('The GecoosAC service is not running.'); const state = E('span', { 'class': running ? 'gecoosac-running' : 'gecoosac-stopped' }, text); - if (!running) + if (!running || !url) return E('p', {}, state); return E('p', {}, [ @@ -276,7 +294,7 @@ function renderStatusContent(status) { E('button', { 'class': 'cbi-button cbi-button-reload', 'click': function() { - const client = window.open(clientUrl(), '_blank', 'noopener'); + const client = window.open(url, '_blank', 'noopener'); if (client) client.opener = null; } @@ -442,6 +460,8 @@ return view.extend({ o.validate = function(section_id, value) { if (usesManagedPath(value) && !pathPolicy) return _('Unable to validate /var paths on this system.'); + if (usesClearStagePath(value)) + return CLEAR_STAGE_PATH_ERROR; return validUploadDir(value, pathPolicy) ? true @@ -458,6 +478,8 @@ return view.extend({ const uploadDir = uploadDirOption.formvalue(section_id) || DEFAULT_UPLOAD_DIR; if ((usesManagedPath(value) || usesManagedPath(uploadDir)) && !pathPolicy) return _('Unable to validate /var paths on this system.'); + if (usesClearStagePath(value)) + return CLEAR_STAGE_PATH_ERROR; if (!validPathPrefix(value, DB_DIR_PREFIXES)) return _('Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac.'); @@ -477,6 +499,8 @@ return view.extend({ const uploadDir = uploadDirOption.formvalue(section_id) || DEFAULT_UPLOAD_DIR; if ((usesManagedPath(value) || usesManagedPath(uploadDir)) && !pathPolicy) return _('Unable to validate /var paths on this system.'); + if (usesClearStagePath(value)) + return CLEAR_STAGE_PATH_ERROR; if (!validPathPrefix(value, PID_DIR_PREFIXES)) return _('PID directory must be under /var/run or /tmp/gecoosac.'); diff --git a/luci-app-gecoosac/po/zh_Hans/gecoosac.po b/luci-app-gecoosac/po/zh_Hans/gecoosac.po index a5a51eb6..1efbc5db 100644 --- a/luci-app-gecoosac/po/zh_Hans/gecoosac.po +++ b/luci-app-gecoosac/po/zh_Hans/gecoosac.po @@ -193,6 +193,9 @@ msgstr "无法验证上传目录清理暂存区" msgid "Upload cleanup stage contains a configured protected path" msgstr "上传目录清理暂存区包含已配置的受保护路径" +msgid "Paths under .gecoosac-clear.* are reserved for upload cleanup." +msgstr ".gecoosac-clear.* 下的路径保留用于上传目录清理。" + msgid "Unable to recreate upload directory" msgstr "无法重新创建上传目录" diff --git a/luci-app-gecoosac/po/zh_Hant/gecoosac.po b/luci-app-gecoosac/po/zh_Hant/gecoosac.po index 6c1f6d9d..ca2dd4a8 100644 --- a/luci-app-gecoosac/po/zh_Hant/gecoosac.po +++ b/luci-app-gecoosac/po/zh_Hant/gecoosac.po @@ -193,6 +193,9 @@ msgstr "無法驗證上傳目錄清理暫存區" msgid "Upload cleanup stage contains a configured protected path" msgstr "上傳目錄清理暫存區包含已設定的受保護路徑" +msgid "Paths under .gecoosac-clear.* are reserved for upload cleanup." +msgstr ".gecoosac-clear.* 下的路徑保留供上傳目錄清理使用。" + msgid "Unable to recreate upload directory" msgstr "無法重新建立上傳目錄" diff --git a/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac b/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac index 27cbc35b..9884e9f2 100755 --- a/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac +++ b/luci-app-gecoosac/root/usr/libexec/rpcd/luci.gecoosac @@ -243,20 +243,78 @@ configured_path_in_upload() { } status_result() { - local ok="$1" running="$2" message="$3" + local ok="$1" running="$2" message="$3" protocol="$4" port="$5" json_init json_add_boolean ok "$ok" json_add_boolean running "$running" [ -n "$message" ] && json_add_string error "$message" + [ -n "$protocol" ] && json_add_string protocol "$protocol" + [ -n "$port" ] && json_add_string port "$port" json_dump json_cleanup } +runtime_endpoint() { + local keys key argument pending interface_port management_port + local single_port https_enabled + + status_protocol=http + status_port= + pending= + interface_port= + management_port= + single_port=1 + https_enabled=0 + + json_select command 2>/dev/null || return 1 + json_get_keys keys + for key in $keys; do + json_get_var argument "$key" + if [ -n "$pending" ]; then + case "$pending" in + interface) interface_port="$argument" ;; + management) management_port="$argument" ;; + single) single_port="$argument" ;; + https) https_enabled="$argument" ;; + esac + pending= + continue + fi + + case "$argument" in + -p) pending=interface ;; + -mp) pending=management ;; + -isonlyoneprot) pending=single ;; + -https) pending=https ;; + esac + done + json_select .. + + case "$single_port" in + 0) + [ -n "$management_port" ] || return 1 + status_port="$management_port" + [ "$https_enabled" = "1" ] && status_protocol=https + ;; + 1) + status_port="$interface_port" + ;; + *) return 1 ;; + esac + + case "$status_port" in + ""|*[!0-9]*) return 1 ;; + esac + [ "$status_port" -ge 1 ] 2>/dev/null && [ "$status_port" -le 65535 ] +} + service_status() { - local data instances instance running state + local data instances instance running state status_protocol status_port running=0 + status_protocol= + status_port= data="$(ubus call service list '{"name":"gecoosac"}' 2>/dev/null)" || { status_result 0 0 "Unable to query service status" return @@ -272,17 +330,18 @@ service_status() { 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 + runtime_endpoint break fi + json_select .. done fi fi json_cleanup - status_result 1 "$running" + status_result 1 "$running" "" "$status_protocol" "$status_port" } path_policy() { diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 1609a016..ee4030a9 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.8.7 -PKG_RELEASE:=73 +PKG_RELEASE:=74 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua index 0bcba726..2ba194c7 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua @@ -24,24 +24,16 @@ for k, e in ipairs(api.get_valid_nodes()) do end local dynamicList_write = function(self, section, value) - local t = {} - local t2 = {} + local new_t = {} if type(value) == "table" then - local x - for _, x in ipairs(value) do - if x and #x > 0 then - if not t2[x] then - t2[x] = x - t[#t+1] = x - end - end - end + new_t = api.table_remove_duplicates(value) else - t = { value } + new_t = { value } end - t = table.concat(t, " ") - return DynamicList.write(self, section, t) + local new_val = table.concat(new_t, " ") + return DynamicList.write(self, section, new_val) end + local doh_validate = function(self, value, t) if value ~= "" then local flag = 0 diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua index bb46df0b..f751a04a 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua @@ -168,23 +168,14 @@ source.validate = function(self, value, t) end local dynamicList_write = function(self, section, value) - local t = {} - local t2 = {} + local new_t = {} if type(value) == "table" then - local x - for _, x in ipairs(value) do - if x and #x > 0 then - if not t2[x] then - t2[x] = x - t[#t+1] = x - end - end - end + new_t = api.table_remove_duplicates(value) else - t = { value } + new_t = { value } end - t = table.concat(t, " ") - return DynamicList.write(self, section, t) + local new_val = table.concat(new_t, " ") + return DynamicList.write(self, section, new_val) end source.write = dynamicList_write diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua index fdffb931..3b1198ec 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -534,22 +534,13 @@ o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") o:depends({ [_n("tls")] = true, [_n("reality")] = false }) function o.custom_write(self, section, value) - local t = {} - local t2 = {} + local new_t if type(value) == "table" then - local x - for _, x in ipairs(value) do - if x and #x > 0 then - if not t2[x] then - t2[x] = x - t[#t+1] = x - end - end - end + new_t = api.table_remove_duplicates(value) else - t = { value } + new_t = { value } end - m:set(section, self.option:sub(1 + #option_prefix), t) + m:set(section, self.option:sub(1 + #option_prefix), new_t) end o = s:option(TextValue, _n("reality_mldsa65Verify"), "ML-DSA-65 " .. translate("Public key")) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua index 9c3e3947..bdbff970 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua @@ -590,22 +590,13 @@ o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") o:depends({ [_n("tls")] = true }) function o.custom_write(self, section, value) - local t = {} - local t2 = {} + local new_t if type(value) == "table" then - local x - for _, x in ipairs(value) do - if x and #x > 0 then - if not t2[x] then - t2[x] = x - t[#t+1] = x - end - end - end + new_t = api.table_remove_duplicates(value) else - t = { value } + new_t = { value } end - m:set(section, self.option:sub(1 + #option_prefix), t) + m:set(section, self.option:sub(1 + #option_prefix), new_t) end o = s:option(Flag, _n("ech"), translate("ECH")) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua index 11bfc669..c07ec992 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua @@ -218,23 +218,14 @@ o:depends({ [_n("reality")] = true }) o = s:option(DynamicList, _n("reality_serverNames"), translate("serverNames")) o:depends({ [_n("reality")] = true }) -function o.write(self, section, value) - local t = {} - local t2 = {} +function o.custom_write(self, section, value) + local new_t = {} if type(value) == "table" then - local x - for _, x in ipairs(value) do - if x and #x > 0 then - if not t2[x] then - t2[x] = x - t[#t+1] = x - end - end - end + new_t = api.table_remove_duplicates(value) else - t = { value } + new_t = { value } end - return DynamicList.write(self, section, t) + m:set(section, self.option:sub(1 + #option_prefix), new_t) end o = s:option(ListValue, _n("alpn"), translate("alpn")) diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index 5437f4f7..c1d3dee7 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -1863,3 +1863,19 @@ function get_network_devices() table.sort(_devices, function(a, b) return a.sort < b.sort end) return _devices end + +function table_remove_duplicates(t) + if not t or #t == 0 then return nil end + local t_lookup = {} + local new_t = {} + local x + for _, x in ipairs(t) do + if x and #x > 0 then + if not t_lookup[x] then + t_lookup[x] = x + new_t[#new_t+1] = x + end + end + end + return new_t +end