From 214a01499acb03d7b0e9591cd05384680ddb7e39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Sep 2026 10:37:57 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=88=20Sync=202026-09-30=2010:37:56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firewall/Makefile | 17 ++- firewall/files/firewall.config | 14 +- firewall/files/firewall.init | 23 ---- ...00-fullconenat.patch => fullconenat.patch} | 0 ...lconenat.patch => z-bcm-fullconenat.patch} | 12 +- luci-app-homeproxy-pro/Makefile | 2 +- .../root/etc/config/homeproxy-pro | 9 ++ luci-app-homeproxy-pro/tests/arch-guard.sh | 28 ++++ luci-app-substore/Makefile | 2 +- .../lib/lua/luci/view/substore/local_form.htm | 12 +- .../lib/lua/luci/view/substore/node_edit.htm | 12 +- .../usr/lib/lua/luci/view/substore/nodes.htm | 8 +- .../root/usr/share/substore/core.lua | 61 ++++++++- .../root/usr/share/substore/node.lua | 44 +++++++ .../resources/substore/nodeform.js | 29 ++-- .../tests/core_merge_node_test.lua | 58 ++++++++ .../tests/view_injection_test.lua | 124 ++++++++++++++++++ 17 files changed, 386 insertions(+), 69 deletions(-) rename firewall/patches/{100-fullconenat.patch => fullconenat.patch} (100%) rename firewall/patches/{101-bcm-fullconenat.patch => z-bcm-fullconenat.patch} (85%) create mode 100644 luci-app-substore/tests/view_injection_test.lua diff --git a/firewall/Makefile b/firewall/Makefile index 45c524813..aefecd597 100644 --- a/firewall/Makefile +++ b/firewall/Makefile @@ -9,18 +9,20 @@ include $(TOPDIR)/rules.mk PKG_NAME:=firewall -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=$(PROJECT_GIT)/project/firewall3.git -PKG_SOURCE_DATE:=2022-02-17 -PKG_SOURCE_VERSION:=4cd7d4f36bea731bf901cb067456f1d460294926 +PKG_SOURCE_DATE:=2025-10-03 +PKG_SOURCE_VERSION:=3a65fde5a3e675fdf2a24cb3f5d12d0a426be36e PKG_MIRROR_HASH:=skip PKG_MAINTAINER:=Jo-Philipp Wich PKG_LICENSE:=ISC PKG_CONFIG_DEPENDS := CONFIG_IPV6 +PKG_BUILD_FLAGS:=gc-sections lto + include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk @@ -28,8 +30,11 @@ define Package/firewall SECTION:=net CATEGORY:=Base system TITLE:=OpenWrt C Firewall - DEPENDS:=+libubox +libubus +libuci +libip4tc +IPV6:libip6tc +libiptext +IPV6:libiptext6 +libxtables +kmod-ipt-core +kmod-ipt-conntrack +iptables-mod-conntrack-extra +IPV6:kmod-nf-conntrack6 +kmod-ipt-nat +iptables-mod-fullconenat \ - +ipset +iptables-mod-conntrack-extra +iptables-mod-iprange +iptables-mod-socket +iptables-mod-tproxy + DEPENDS:=+libubox +libubus +libuci +libip4tc +IPV6:libip6tc +libiptext +IPV6:libiptext6 \ + +libxtables +kmod-ipt-core +kmod-ipt-conntrack +IPV6:kmod-nf-conntrack6 +kmod-ipt-nat \ + +iptables-mod-fullconenat \ + +iptables-mod-fullconenat +ip6tables-mod-fullconenat +iptables-legacy +ip6tables-legacy +kmod-ipt-nat6 +kmod-ipt-offload \ + +ipset +iptables-mod-conntrack-extra +iptables-mod-iprange +iptables-mod-socket +iptables-mod-tproxy PROVIDES:=uci-firewall endef @@ -42,8 +47,6 @@ define Package/firewall/conffiles /etc/firewall.user endef -TARGET_CFLAGS += -ffunction-sections -fdata-sections -flto -TARGET_LDFLAGS += -Wl,--gc-sections -flto CMAKE_OPTIONS += $(if $(CONFIG_IPV6),,-DDISABLE_IPV6=1) define Package/firewall/install diff --git a/firewall/files/firewall.config b/firewall/files/firewall.config index a96030bf3..652cca090 100644 --- a/firewall/files/firewall.config +++ b/firewall/files/firewall.config @@ -1,9 +1,10 @@ config defaults option synflood_protect 1 - option input ACCEPT + option input REJECT option output ACCEPT option forward REJECT - option fullcone 0 + option flow_offloading 1 + option fullcone 1 # Uncomment this line to disable ipv6 rules # option disable_ipv6 1 @@ -55,13 +56,12 @@ config rule option target ACCEPT # Allow DHCPv6 replies -# see https://dev.openwrt.org/ticket/10381 +# see https://github.com/openwrt/openwrt/issues/5066 config rule option name Allow-DHCPv6 option src wan option proto udp - option src_ip fc00::/6 - option dest_ip fc00::/6 + option dest_ip fe80::/10 option dest_port 546 option family ipv6 option target ACCEPT @@ -82,7 +82,7 @@ config rule config rule option name Allow-ICMPv6-Input option src wan - option proto icmp + option proto icmp list icmp_type echo-request list icmp_type echo-reply list icmp_type destination-unreachable @@ -141,7 +141,7 @@ config rule option proto udp option family ipv4 option target REJECT - option enabled false + option enabled 0 # include a file with users custom iptables rules config include diff --git a/firewall/files/firewall.init b/firewall/files/firewall.init index 2d641d049..ee3ed1a28 100755 --- a/firewall/files/firewall.init +++ b/firewall/files/firewall.init @@ -4,17 +4,6 @@ START=19 USE_PROCD=1 QUIET="" -LOCK_FILE="/var/lock/procd_firewall.lock" -set_lock() { - exec 1000>"$LOCK_FILE" - flock -xn 1000 -} - -unset_lock() { - flock -u 1000 - rm -rf "$LOCK_FILE" -} - validate_firewall_redirect() { uci_validate_section firewall redirect "${1}" \ @@ -41,39 +30,27 @@ validate_firewall_rule() service_triggers() { procd_add_reload_trigger firewall - set_lock procd_open_validate validate_firewall_redirect validate_firewall_rule procd_close_validate - unset_lock } restart() { - unset_lock - set_lock fw3 restart - unset_lock } start_service() { - set_lock fw3 ${QUIET} start - unset_lock } stop_service() { fw3 flush - unset_lock } reload_service() { - unset_lock - set_lock - reload_config firewall fw3 reload - unset_lock } boot() { diff --git a/firewall/patches/100-fullconenat.patch b/firewall/patches/fullconenat.patch similarity index 100% rename from firewall/patches/100-fullconenat.patch rename to firewall/patches/fullconenat.patch diff --git a/firewall/patches/101-bcm-fullconenat.patch b/firewall/patches/z-bcm-fullconenat.patch similarity index 85% rename from firewall/patches/101-bcm-fullconenat.patch rename to firewall/patches/z-bcm-fullconenat.patch index 9cea4a57b..25871e23c 100644 --- a/firewall/patches/101-bcm-fullconenat.patch +++ b/firewall/patches/z-bcm-fullconenat.patch @@ -1,6 +1,6 @@ --- a/defaults.c +++ b/defaults.c -@@ -49,7 +49,7 @@ const struct fw3_option fw3_flag_opts[] +@@ -49,7 +49,7 @@ FW3_OPT("synflood_rate", limit, defaults, syn_flood_rate), FW3_OPT("synflood_burst", int, defaults, syn_flood_rate.burst), @@ -9,9 +9,10 @@ FW3_OPT("tcp_syncookies", bool, defaults, tcp_syncookies), FW3_OPT("tcp_ecn", int, defaults, tcp_ecn), + --- a/options.h +++ b/options.h -@@ -98,6 +98,13 @@ enum fw3_reject_code +@@ -98,6 +98,13 @@ __FW3_REJECT_CODE_MAX }; @@ -25,7 +26,7 @@ extern const char *fw3_flag_names[__FW3_FLAG_MAX]; -@@ -297,7 +304,7 @@ struct fw3_defaults +@@ -297,7 +304,7 @@ enum fw3_reject_code any_reject_code; bool syn_flood; @@ -34,9 +35,10 @@ struct fw3_limit syn_flood_rate; bool tcp_syncookies; + --- a/zones.c +++ b/zones.c -@@ -757,7 +757,7 @@ print_zone_rule(struct fw3_ipt_handle *h +@@ -756,7 +756,7 @@ r = fw3_ipt_rule_new(handle); fw3_ipt_rule_src_dest(r, msrc, mdest); /*FIXME: Workaround for FULLCONE-NAT*/ @@ -45,7 +47,7 @@ { warn("%s will enable FULLCONE-NAT", zone->name); fw3_ipt_rule_target(r, "FULLCONENAT"); -@@ -767,6 +767,12 @@ print_zone_rule(struct fw3_ipt_handle *h +@@ -766,6 +766,12 @@ fw3_ipt_rule_target(r, "FULLCONENAT"); fw3_ipt_rule_append(r, "zone_%s_prerouting", zone->name); } diff --git a/luci-app-homeproxy-pro/Makefile b/luci-app-homeproxy-pro/Makefile index 9d10c53a8..e63cf2409 100644 --- a/luci-app-homeproxy-pro/Makefile +++ b/luci-app-homeproxy-pro/Makefile @@ -23,7 +23,7 @@ LUCI_DEPENDS:= \ PKG_NAME:=luci-app-homeproxy-pro PKG_VERSION:=28.9.1.14 -PKG_RELEASE:=8 +PKG_RELEASE:=9 # Only files the package actually ships belong here. The previous list named # four paths that exist nowhere in the tree - certs/ and ruleset/ are created at diff --git a/luci-app-homeproxy-pro/root/etc/config/homeproxy-pro b/luci-app-homeproxy-pro/root/etc/config/homeproxy-pro index 04222645b..77f19d3f4 100644 --- a/luci-app-homeproxy-pro/root/etc/config/homeproxy-pro +++ b/luci-app-homeproxy-pro/root/etc/config/homeproxy-pro @@ -26,6 +26,15 @@ config homeproxy-pro 'config' option main_udp_node 'same' option dns_server '8.8.8.8' option china_dns_server '223.5.5.5' + # Bootstrap resolver for the DoH/DoT endpoint's own hostname. Without + # it `main-dns` borrows the WAN resolver, so the one lookup that decides + # where every proxied name goes is answered by the ISP: poisoning that + # single answer is enough to break the whole DNS split, and it is the + # only query that was still leaving through the ISP after the split + # itself was configured correctly. This is a conffile, so upgrading + # never rewrites an existing /etc/config/homeproxy-pro - the value below + # only reaches fresh installs. + list bootstrap_dns '223.5.5.5' option routing_mode 'bypass_mainland_china' option routing_port 'common' option proxy_mode 'redirect_tproxy' diff --git a/luci-app-homeproxy-pro/tests/arch-guard.sh b/luci-app-homeproxy-pro/tests/arch-guard.sh index a193d2ce6..e1563f87b 100755 --- a/luci-app-homeproxy-pro/tests/arch-guard.sh +++ b/luci-app-homeproxy-pro/tests/arch-guard.sh @@ -2064,6 +2064,34 @@ fi rm -rf "$SNAP_TMP" trap - EXIT INT TERM +echo +echo "== guard 45: a fresh install bootstraps main-dns, an untouched config still does not ==" + +# E3. With no bootstrap_dns, `main-dns` (the DoH/DoT endpoint) falls back to +# the WAN resolver, so the ISP answers the one lookup that decides where every +# proxied name goes. The package default below closes that for fresh +# installs; this guard exists so neither half drifts silently: +# (a) an explicitly empty list must still emit NO bootstrap-dns server, so +# a user who clears the field gets the old behaviour back rather than a +# resolver the generator invents for them; +# (b) the package-shipped default is what makes the fix reach a new install +# at all - the field is read from the 'config' section, and the file is +# a conffile, so this line only lands on fresh installs. +if grep -q "if (isEmpty(ctx.bootstrap_dns))" "$SCRIPTS/generator/dns.uc"; then + pass "an empty bootstrap_dns still emits no bootstrap-dns server" +else + fail "append_bootstrap_dns no longer short-circuits on an empty bootstrap_dns - a" + fail "user who cleared the field would get a resolver they did not ask for" +fi + +if grep -q "^ list bootstrap_dns '223.5.5.5'" "$ROOT/root/etc/config/homeproxy-pro"; then + pass "a fresh install ships a bootstrap_dns, so main-dns never asks the ISP" +else + fail "the package-shipped /etc/config/homeproxy-pro no longer ships a bootstrap_dns -" + fail "a new install would resolve its DoH endpoint's hostname through the WAN" + fail "resolver, which is the last query that still leaves via the ISP" +fi + echo printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')" if [ "$FAILED" != 0 ]; then diff --git a/luci-app-substore/Makefile b/luci-app-substore/Makefile index ba0b29286..7e32432dd 100644 --- a/luci-app-substore/Makefile +++ b/luci-app-substore/Makefile @@ -10,7 +10,7 @@ PKG_NAME:=luci-app-substore # 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9); # 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。 PKG_VERSION:=2.6.0 -PKG_RELEASE:=11 +PKG_RELEASE:=12 LUCI_DEPENDS:=+luci-lua-runtime +luci-compat diff --git a/luci-app-substore/root/usr/lib/lua/luci/view/substore/local_form.htm b/luci-app-substore/root/usr/lib/lua/luci/view/substore/local_form.htm index 0e0d1f298..8ee112908 100644 --- a/luci-app-substore/root/usr/lib/lua/luci/view/substore/local_form.htm +++ b/luci-app-substore/root/usr/lib/lua/luci/view/substore/local_form.htm @@ -18,6 +18,10 @@ -- 归一为白名单取值后用 json_encode 输出,保证是合法的 JS 字符串字面量 if local_mode ~= "form" then local_mode = "text" end local js_mode = util.json_encode(local_mode) or '"text"' + -- 协议与字段清单:与 core.merge_form_node 共用 substore/node.lua 的同一份数据 + local node_mod = require("substore.node") + local js_protos = (util.json_encode(node_mod.PROTOS) or "[]"):gsub("

<%=is_edit and luci.i18n.translate("Edit local subscription") or luci.i18n.translate("Add local subscription")%>

@@ -118,6 +122,7 @@ var FIELD_LABELS = { "obfs": "Obfs", "obfs-param": "Obfs Param", "obfs-password": "Obfs Password", + "protocol": "Protocol", "protocol-param": "Protocol Param", "udp": "UDP", "skip-cert-verify": "Skip Cert Verify", @@ -140,8 +145,13 @@ var NODEFORM_I18N = { "node": "<%:Node%>", "defaultOpt": "<%:Default%>" }; +// 协议与字段清单由服务端注入(唯一来源:substore/node.lua 的 M.PROTOS / +// M.PROTO_FIELDS)。core.merge_form_node 用同一份清单决定哪些字段可以被表单 +// 覆盖,两边必须严格一致,因此不在前端另抄一份。 +var SUBSTORE_PROTOS = <%=js_protos%>; +var SUBSTORE_PROTO_FIELDS = <%=js_proto_fields%>; - + - + " 的值会在 HTML 词法阶段就闭合 + -- script 元素(与 JS 字符串上下文无关),剩下的部分被当作 HTML 执行。 + -- 转义 " 渲染) * var NODEFORM_I18N = { node: "...", defaultOpt: "..." }; // 界面文本(同上) + * var SUBSTORE_PROTOS = [...]; // 协议下拉框 + * var SUBSTORE_PROTO_FIELDS = { proto: [...] }; // 各协议渲染的字段 * 提供:SUBSTORE_PROTOS / PROTO_FIELDS / renderFields / collectNodes / addNodeRow + * + * 协议与字段清单**不在本文件里**,由页面从 substore/node.lua 的 M.PROTOS / + * M.PROTO_FIELDS 渲染后注入。这不是洁癖:服务端的 core.merge_form_node 用同一份 + * 清单决定「哪些字段可以被表单覆盖(含清空)」,两边各维护一份必然会漂移 —— + * 表单没渲染的字段提交不上来,而合并时一并清空就等于用空值覆盖原值(vmess 与 + * hysteria2/tuic 的 TLS 层字段就是这样被静默抹掉的)。详见 node.PROTO_FIELDS 的注释。 */ +var SUBSTORE_PROTOS = window.SUBSTORE_PROTOS || []; -// 必须与 substore/node.lua 的 M.PROTOS 一致(规格 §25「本地表单协议能力」的协议集合)。 -// 之前这里少了 hysteria / socks:node.lua、节点页筛选、规则 proto_filter 都认这两个协议, -// 唯独表单下拉框选不到,用户无法用「表单导入」录入它们。 -var SUBSTORE_PROTOS = ["vmess","vless","trojan","shadowsocks","ssr","hysteria2","tuic","hysteria","wireguard","socks"]; - -var PROTO_FIELDS = { - ssr: ["server","port","password","cipher","protocol","obfs","obfs-param","protocol-param","udp"], - vmess: ["server","port","uuid","alterId","cipher","net","headerType","path","host","sni","tls","udp","skip-cert-verify"], - vless: ["server","port","uuid","security","flow","net","headerType","path","host","sni","udp","skip-cert-verify"], - trojan: ["server","port","password","sni","net","headerType","path","host","udp","skip-cert-verify"], - shadowsocks: ["server","port","password","method","headerType","udp"], - hysteria2: ["server","port","password","sni","obfs","obfs-password","skip-cert-verify"], - // hysteria(v1):obfs 是普通字符串(不是 hysteria2 的 salamander),因此没有 obfs-password。 - // 字段集合以各输出模块实际消费的键为准(output_clash_meta / output_singbox / output_uri)。 - hysteria: ["server","port","password","sni","obfs","skip-cert-verify"], - tuic: ["server","port","uuid","password","sni","udp","skip-cert-verify"], - wireguard: ["server","port","private-key","public-key","pre-shared-key","ip","ipv6","allowed-ips","reserved","persistent-keepalive","listen-port","mtu","amnezia-wg-option"], - socks: ["server","port","username","password","udp"] -}; +var PROTO_FIELDS = window.SUBSTORE_PROTO_FIELDS || {}; // 枚举字段用下拉框;键为 "协议.字段" 优先,退化为通用 "字段" // 选项集合以模型/输出模块实际支持的值为准(见 output_singbox.build_transport / output_uri) diff --git a/luci-app-substore/tests/core_merge_node_test.lua b/luci-app-substore/tests/core_merge_node_test.lua index a66615ce6..1790b262b 100644 --- a/luci-app-substore/tests/core_merge_node_test.lua +++ b/luci-app-substore/tests/core_merge_node_test.lua @@ -53,5 +53,63 @@ local m5 = core.merge_form_node(orig, nil) check("nil form keeps non-form fields", m5.raw == "vmess://xxx") check("nil form clears form fields", m5.name == nil) +-- ---------- 表单没渲染的字段不得被清空 ---------- +-- +-- 表单按 node.PROTO_FIELDS[proto] 渲染,没渲染的字段提交不上来。原先 +-- merge_form_node 一律清空全部表单字段,等于用空值覆盖原值:vmess 与 +-- hysteria2/tuic 的 TLS 层字段(security)就是这样被静默抹掉的 —— 前者 +-- 变成明文,后者让 sing-box 因 C.ErrTLSRequired 拒绝启动。 +local parser = require("substore.parser") +local util = require("substore.util") +local node = require("substore.node") + +-- 模拟一次真实的表单提交:只提交本协议渲染出来的字段,再过 parse_local +local function submit(proto, vals) + local t = {} + for _, k in ipairs(node.PROTO_FIELDS[proto] or {}) do + if vals[k] ~= nil and vals[k] ~= "" then t[k] = tostring(vals[k]) end + end + t.type = proto + return parser.parse_local(util.json_encode({ t }), "form").nodes[1] +end + +local vless_node = { proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "tls" } +local kept = core.merge_form_node(vless_node, submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "tls" })) +check("vmess security kept through form", kept.security == "tls") + +-- 关掉 TLS 时,别名 tls 必须一起清掉,否则 build_tls / surge_line 会退回旧值 +local off = core.merge_form_node( + { proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "tls", tls = true }, + submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u", security = "none" })) +check("vmess security off honoured", off.security == "none") +check("vmess tls alias cleared", off.tls == nil) + +-- hysteria2 / tuic / hysteria 是 TLS-only,表单不渲染 security,不得被清掉 +for _, p in ipairs({ "hysteria2", "hysteria", "tuic" }) do + local o = { proto = p, name = "n", server = "1.1.1.1", port = 443, password = "pw", security = "tls" } + local r = core.merge_form_node(o, submit(p, { name = "n", server = "1.1.1.1", port = 443, password = "pw" })) + check(p .. " security kept through form", r.security == "tls") +end + +-- 未渲染的普通字段同样保留(WireGuard 的 dns、vmess 的 flow) +local wg = { proto = "wireguard", name = "w", server = "1.1.1.1", port = 51820, ["private-key"] = "pk", dns = "1.1.1.1" } +local wg2 = core.merge_form_node(wg, submit("wireguard", { name = "w", server = "1.1.1.1", port = 51820, ["private-key"] = "pk" })) +check("wireguard dns preserved", wg2.dns == "1.1.1.1") +local vm = { proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", flow = "xtls-rprx-vision" } +local vm2 = core.merge_form_node(vm, submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u" })) +check("vmess flow preserved", vm2.flow == "xtls-rprx-vision") + +-- 别名跟随规范名一起清空(解析器产出的是下划线写法) +local alias = core.merge_form_node( + { proto = "vmess", name = "n", server = "1.1.1.1", port = 443, uuid = "u", ["skip-cert-verify"] = true, skip_cert_verify = true }, + submit("vmess", { name = "n", server = "1.1.1.1", port = 443, uuid = "u" })) +check("skip-cert-verify cleared", alias["skip-cert-verify"] == nil) +check("skip_cert_verify alias cleared", alias.skip_cert_verify == nil) + +-- 协议未知时退回旧行为(清空全部表单字段) +local unk = core.merge_form_node({ proto = "snell", name = "s", server = "1.1.1.1", port = 443, password = "p" }, + { proto = "snell", name = "s", server = "1.1.1.1", port = 443 }) +check("unknown proto falls back to full wipe", unk.password == nil) + print(string.format("%d passed, %d failed", passed, failed)) os.exit(failed == 0 and 0 or 1) diff --git a/luci-app-substore/tests/view_injection_test.lua b/luci-app-substore/tests/view_injection_test.lua new file mode 100644 index 000000000..e81e0ff47 --- /dev/null +++ b/luci-app-substore/tests/view_injection_test.lua @@ -0,0 +1,124 @@ +-- view_injection_test.lua — 视图层的注入与字段清单一致性检查 +-- 用法:lua5.1 tests/view_injection_test.lua +-- +-- 这些是静态源码检查(.htm 需要 LuCI 运行时才能渲染,lua5.1 跑不了模板), +-- 但检查的正是那类「只在浏览器里才暴露」的缺陷: +-- H2 注入到 "。 +-- 字段清单必须由服务端注入:core.merge_form_node 用 node.PROTO_FIELDS 决定 +-- 哪些字段可以被表单覆盖,前端若另抄一份就会漂移(vmess 的 TLS 字段 +-- 丢失正是这么来的)。 + +package.path = "./root/usr/share/?.lua;" .. package.path + +local util = require("substore.util") +local node = require("substore.node") + +local passed, failed = 0, 0 + +local function check(name, cond) + if cond then + passed = passed + 1 + print("PASS " .. name) + else + failed = failed + 1 + print("FAIL " .. name) + end +end + +local VIEWS = { + "root/usr/lib/lua/luci/view/substore/nodes.htm", + "root/usr/lib/lua/luci/view/substore/node_edit.htm", + "root/usr/lib/lua/luci/view/substore/local_form.htm", + "root/usr/lib/lua/luci/view/substore/subscriptions.htm", + "root/usr/lib/lua/luci/view/substore/form.htm", + "root/usr/lib/lua/luci/view/substore/combo.htm", + "root/usr/lib/lua/luci/view/substore/output.htm", +} + +-- ---------- H2:JSON 编码注入到 JS 的位置必须同时转义 "" +check("json_encode does not escape <", util.json_encode(payload):find("", 1, true) ~= nil) + check(name .. " injects SUBSTORE_PROTO_FIELDS", src:find("var SUBSTORE_PROTO_FIELDS = <%=js_proto_fields%>", 1, true) ~= nil) + check(name .. " loads nodeform.js", src:find("nodeform.js", 1, true) ~= nil) +end + +-- 每个协议都要有字段清单,否则表单渲染不出任何输入框 +for _, p in ipairs(node.PROTOS) do + local f = node.PROTO_FIELDS[p] + check("PROTO_FIELDS has " .. p, type(f) == "table" and #f > 0) +end + +-- 每个渲染出来的字段都要有标签,否则界面上直接显示英文键名 +local label_src = (util.read_file("root/usr/lib/lua/luci/view/substore/node_edit.htm") or "") +local labels = {} +for k in label_src:gmatch('"([%w%-]+)"%s*:') do labels[k] = true end +local missing = {} +for p, fields in pairs(node.PROTO_FIELDS) do + for _, k in ipairs(fields) do + if not labels[k] then missing[#missing + 1] = p .. "." .. k end + end +end +table.sort(missing) +check("every form field has a label" .. (#missing > 0 and (" (missing: " .. table.concat(missing, ",") .. ")") or ""), + #missing == 0) + +-- TLS 层字段必须出现在表单里,否则编辑一次就丢 TLS(vmess 的历史缺陷) +for _, p in ipairs({ "vmess", "vless" }) do + local has = false + for _, k in ipairs(node.PROTO_FIELDS[p]) do if k == "security" then has = true end end + check(p .. " form renders security", has) +end + +-- TLS-only 协议必须由 normalize 保证 security,而不是靠 URI 解析器各自补 +for _, p in ipairs({ "hysteria2", "hysteria", "tuic", "trojan" }) do + check(p .. " normalize forces security=tls", + node.normalize({ proto = p, server = "h", port = 1 }).security == "tls") +end + +print(string.format("\n%d passed, %d failed", passed, failed)) +os.exit(failed == 0 and 0 or 1)