mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
🎁 Sync 2026-10-05 11:45:22
This commit is contained in:
1 parent
41b75a01bd
commit
6f0e562fa5
7 files changed
+250
-24
No files matched your search
@@ -18,7 +18,7 @@ LUCI_DEPENDS:= \
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy-pro
|
||||
PKG_VERSION:=28.10.1.14
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
LUCI_BASENAME:=homeproxy-pro
|
||||
|
||||
|
||||
@@ -316,14 +316,31 @@ return view.extend({
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy-pro', _('Diagnostic report'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_debug_report');
|
||||
o.rawhtml = true;
|
||||
o.render = function() {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('p', {}, _('Collects system, dependency, routing, firewall and configuration state into one file. Credentials are masked by option name; public addresses and LAN topology are not. Read it before posting it anywhere.')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
/* The button belongs on the title line, not parked under the
|
||||
* description where it reads as part of the paragraph.
|
||||
*
|
||||
* It cannot simply be passed in as part of the title: form.js builds
|
||||
* the heading with `E('h3', {}, this.title)` behind a
|
||||
* `typeof(this.title) == 'string'` guard, so a node here is dropped
|
||||
* and the heading disappears entirely. The section's own render() is
|
||||
* the seam instead - it runs on every re-render and resolves with the
|
||||
* finished section element, so the button goes into the h3 that
|
||||
* render() has just built. Nothing of LuCI's own rendering is
|
||||
* reproduced, which is what keeps this from silently drifting when
|
||||
* form.js changes. */
|
||||
const reportButton = () => E('button', {
|
||||
/* The same class triple as the "Clean log" button above, on
|
||||
* purpose: `btn` is what carries the button's font-size, so
|
||||
* matching it keeps the two the same size without this view
|
||||
* hard-coding a size of its own that the theme can change
|
||||
* under it. */
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'margin-left: 8px;',
|
||||
/* createHandlerFn(ctx, fn) only uses ctx to resolve a string
|
||||
* method name and as the `this` for the call; fn is an arrow
|
||||
* function, so ctx is inert here. The section is passed anyway
|
||||
* so the binding reads like the one it replaced. */
|
||||
'click': ui.createHandlerFn(s, () => {
|
||||
return hp.rpcCall('debug_report', [], { expect: { '': {} } })
|
||||
.then((res) => {
|
||||
if (!res.result)
|
||||
@@ -341,7 +358,26 @@ return view.extend({
|
||||
window.URL.revokeObjectURL(url);
|
||||
});
|
||||
})
|
||||
}, [ _('Generate and download') ])
|
||||
}, [ _('Generate and download') ]);
|
||||
|
||||
const renderDiagnosticSection = s.render.bind(s);
|
||||
s.render = function() {
|
||||
return renderDiagnosticSection().then((el) => {
|
||||
/* The gap before the button is the stylesheet's
|
||||
* `margin-left`, not a text node - a spacer node would drag
|
||||
* `document` into the test environment for nothing. */
|
||||
const heading = el.querySelector('h3');
|
||||
if (heading)
|
||||
dom.append(heading, reportButton());
|
||||
return el;
|
||||
});
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_debug_report');
|
||||
o.rawhtml = true;
|
||||
o.render = function() {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('p', {}, _('Collects system, dependency, routing, firewall and configuration state into one file. Credentials are masked by option name; public addresses and LAN topology are not. Read it before posting it anywhere.'))
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -230,6 +230,38 @@ hp_prepare_ruleset_dir() {
|
||||
|| log "Warning: failed to create ${hp_dir}/ruleset."
|
||||
}
|
||||
|
||||
# hp_rule_set_current <source> <output>
|
||||
# 0 when <output> is already the up-to-date rendering of <source>, 1 when it
|
||||
# has to be generated.
|
||||
#
|
||||
# Generating china-domain.json measured 3540 ms on the x86-64 target
|
||||
# (111,353 suffixes, 2.2 MB), and the source lists are replaced at most daily
|
||||
# by update_resources.sh. Regenerating unconditionally therefore spent a
|
||||
# third of every reload - every Save & Apply, every node switch - rebuilding a
|
||||
# byte-identical file, which is the single largest cost in a reload.
|
||||
#
|
||||
# The three conditions below are exactly the cases the unconditional version
|
||||
# had to cover:
|
||||
#
|
||||
# [ -f "$out" ] a fresh install predates the file, or a previous run
|
||||
# produced nothing
|
||||
# [ -s "$out" ] the previous run was interrupted after truncating it; an
|
||||
# empty rule-set would make the route-side set match nothing,
|
||||
# which is the silent inversion this whole file exists to
|
||||
# prevent
|
||||
# [ "$out" -nt "$src" ] the list was replaced while the service was stopped
|
||||
#
|
||||
# A newer-but-corrupt output is deliberately NOT detected here. It does not
|
||||
# need to be: the generated config names the file, a rule-set sing-box cannot
|
||||
# parse fails the start, and the health gate rolls back to the known-good
|
||||
# configuration. The check is cheap (<1 ms) and cannot mask a real failure -
|
||||
# it can only skip work that would rebuild identical bytes.
|
||||
hp_rule_set_current() {
|
||||
local src="$1" out="$2"
|
||||
|
||||
[ -f "$out" ] && [ -s "$out" ] && [ "$out" -nt "$src" ]
|
||||
}
|
||||
|
||||
# hp_prepare_runtime_files <hp-dir> <run-dir> <routing-mode> <client> <server>
|
||||
# Create the mode-specific working files, truncate the instance logs and hand
|
||||
# every runtime file to the sing-box user. <client>/<server> are "1"/"0".
|
||||
@@ -249,7 +281,12 @@ hp_prepare_runtime_files() {
|
||||
# file and sing-box would refuse to start. A missing source list is not
|
||||
# an error - the generator then emits no route-side rule at all.
|
||||
if [ "$client_enabled" = "1" ] && [ -f "$hp_dir/resources/china_ip4.txt" ]; then
|
||||
if ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
|
||||
if hp_rule_set_current "$hp_dir/resources/china_ip4.txt" "$hp_dir/resources/china_ip4.json"; then
|
||||
# Unchanged source: keep the file, but re-assert ownership anyway -
|
||||
# chown costs nothing and survives an install that reset it.
|
||||
chown sing-box:sing-box "$hp_dir/resources/china_ip4.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip4.json to sing-box."
|
||||
elif ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
|
||||
"$hp_dir/resources/china_ip4.txt" "$hp_dir/resources/china_ip4.json" >>"$LOG_PATH" 2>&1; then
|
||||
chown sing-box:sing-box "$hp_dir/resources/china_ip4.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip4.json to sing-box."
|
||||
@@ -275,7 +312,10 @@ hp_prepare_runtime_files() {
|
||||
cn_ipv6="$(grep -cE '^[0-9a-fA-F:]+:[0-9a-fA-F:]*(/[0-9]{1,3})?[[:space:]]*$' \
|
||||
"$hp_dir/resources/china_ip6.txt" 2>"/dev/null")"
|
||||
[ -n "$cn_ipv6" ] || cn_ipv6=0
|
||||
if [ "$cn_ipv6" -gt 0 ] && ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
|
||||
if [ "$cn_ipv6" -gt 0 ] && hp_rule_set_current "$hp_dir/resources/china_ip6.txt" "$hp_dir/resources/china_ip6.json"; then
|
||||
chown sing-box:sing-box "$hp_dir/resources/china_ip6.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip6.json to sing-box."
|
||||
elif [ "$cn_ipv6" -gt 0 ] && ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
|
||||
"$hp_dir/resources/china_ip6.txt" "$hp_dir/resources/china_ip6.json" >>"$LOG_PATH" 2>&1; then
|
||||
chown sing-box:sing-box "$hp_dir/resources/china_ip6.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip6.json to sing-box."
|
||||
@@ -304,7 +344,10 @@ hp_prepare_runtime_files() {
|
||||
# DNS-side rule for it, and the split degrades to the resolver default
|
||||
# rather than failing the start.
|
||||
if [ "$client_enabled" = "1" ] && [ -f "$hp_dir/resources/china_list.txt" ]; then
|
||||
if ucode -S "$hp_dir/scripts/runtime/domain_ruleset.uc" \
|
||||
if hp_rule_set_current "$hp_dir/resources/china_list.txt" "$hp_dir/resources/china-domain.json"; then
|
||||
chown sing-box:sing-box "$hp_dir/resources/china-domain.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china-domain.json to sing-box."
|
||||
elif ucode -S "$hp_dir/scripts/runtime/domain_ruleset.uc" \
|
||||
"$hp_dir/resources/china_list.txt" "$hp_dir/resources/china-domain.json" >>"$LOG_PATH" 2>&1; then
|
||||
chown sing-box:sing-box "$hp_dir/resources/china-domain.json" 2>"/dev/null" \
|
||||
|| log "Warning: failed to hand ${hp_dir}/resources/china-domain.json to sing-box."
|
||||
|
||||
@@ -83,11 +83,23 @@ class MockOption {
|
||||
}
|
||||
|
||||
class MockSection {
|
||||
constructor(kind, name) {
|
||||
constructor(kind, name, title) {
|
||||
this.__kind = kind;
|
||||
this.__name = name;
|
||||
this.title = title;
|
||||
this.options = [];
|
||||
}
|
||||
/* form.js NamedSection.render() resolves with the finished section
|
||||
* element, and status.js appends the report button into the h3 that
|
||||
* render() built - the title itself cannot carry it, because form.js
|
||||
* guards the heading with `typeof(this.title) == 'string'`. The mock
|
||||
* reproduces exactly that much: a section element holding an h3. */
|
||||
render() {
|
||||
return Promise.resolve(E('div', {}, [
|
||||
this.title != null && this.title !== '' ? E('h3', {}, this.title) : '',
|
||||
...this.options.map((o) => E('div', { 'class': 'cbi-section-node' }, []))
|
||||
]));
|
||||
}
|
||||
tab() {}
|
||||
taboption(_tab, ...args) { return this.option(...args); }
|
||||
option(kind, name, title, description) {
|
||||
@@ -120,8 +132,8 @@ function makeForm() {
|
||||
|
||||
form.Map = class {
|
||||
constructor(title) { this.title = title; this.sections = []; }
|
||||
section(kind, name) {
|
||||
const section = new MockSection(kind && kind.__name__ ? kind.__name__ : String(kind), name);
|
||||
section(kind, name, title) {
|
||||
const section = new MockSection(kind && kind.__name__ ? kind.__name__ : String(kind), name, title);
|
||||
this.sections.push(section);
|
||||
return section;
|
||||
}
|
||||
@@ -268,18 +280,29 @@ function testAccessDomainLists() {
|
||||
* 3. status.js: each log view keeps its own basename
|
||||
* ====================================================================== */
|
||||
|
||||
function E(tag, attrs, children) {
|
||||
if (Array.isArray(tag) && attrs === undefined)
|
||||
return { tag: null, attrs: {}, children: tag, appendChild() {} };
|
||||
|
||||
/* A node carries the two DOM operations a view can perform on an element it
|
||||
* did not create itself: appendChild, and a tag lookup. The log views never
|
||||
* need them - they build their own tree - but status.js reaches into the
|
||||
* element the form layer returns in order to put the report button into the
|
||||
* heading, so the mock has to offer the same two. findTag is a hoisted
|
||||
* function declaration, so it is in scope here. */
|
||||
function mockNode(tag, attrs, children) {
|
||||
return {
|
||||
tag,
|
||||
attrs: attrs || {},
|
||||
children: children === undefined ? [] : (Array.isArray(children) ? children : [ children ]),
|
||||
appendChild() {}
|
||||
appendChild(child) { this.children.push(child); return child; },
|
||||
querySelector(t) { return findTag(this, t); }
|
||||
};
|
||||
}
|
||||
|
||||
function E(tag, attrs, children) {
|
||||
if (Array.isArray(tag) && attrs === undefined)
|
||||
return mockNode(null, {}, tag);
|
||||
|
||||
return mockNode(tag, attrs, children);
|
||||
}
|
||||
|
||||
function findClickable(node) {
|
||||
if (!node || typeof node !== 'object')
|
||||
return null;
|
||||
@@ -314,7 +337,13 @@ function testLogViewWiring() {
|
||||
path.join(root, 'htdocs/luci-static/resources/view/homeproxy-pro/status.js'),
|
||||
{
|
||||
baseclass: {},
|
||||
dom: { content: () => {} },
|
||||
dom: {
|
||||
content: () => {},
|
||||
append: (parent, ...children) => {
|
||||
for (const child of children)
|
||||
parent.appendChild(child);
|
||||
}
|
||||
},
|
||||
form,
|
||||
fs: { read_direct: (p) => { reads.push(p); return Promise.resolve('log line'); } },
|
||||
poll: { add: () => {} },
|
||||
@@ -380,8 +409,36 @@ function testLogViewWiring() {
|
||||
check('status.js: all three log views rendered', rendered === 3, String(rendered));
|
||||
check('status.js: the log poll was registered', pollHandlers !== null && typeof pollHandlers.read === 'function');
|
||||
|
||||
/* --- the report button lives on the title line -----------------------
|
||||
*
|
||||
* form.js builds the heading as `E('h3', {}, this.title)` behind a
|
||||
* `typeof(this.title) == 'string'` guard, so the button cannot be part of
|
||||
* the title. status.js overrides the section's render() and appends the
|
||||
* button into the h3 that render() already built. Assert on the h3's own
|
||||
* subtree: a check that walked the whole section would still pass if the
|
||||
* button fell back below the description, which is the exact regression
|
||||
* this replaced. */
|
||||
const report_section = map.sections.find((s) =>
|
||||
s.options.some((o) => o.__name === '_debug_report'));
|
||||
check('status.js: the report section exists', report_section !== null,
|
||||
String(map.sections.map((s) => s.__name).join(',')));
|
||||
|
||||
return Promise.resolve(report_section ? report_section.render() : null).then((el) => {
|
||||
const heading = el ? el.querySelector('h3') : null;
|
||||
check('status.js: the report section renders a heading', heading !== null, String(el));
|
||||
|
||||
const in_heading = heading ? findClickable(heading) : null;
|
||||
check('status.js: the download button sits inside the heading',
|
||||
in_heading !== null, 'no clickable node under the h3');
|
||||
|
||||
/* And it must be gone from the body, or the page shows two of them. */
|
||||
const body = findOption(report_section, '_debug_report');
|
||||
const body_tree = body.render(0, 'cfg01', false);
|
||||
check('status.js: the report body no longer holds the button',
|
||||
findClickable(body_tree) === null, 'a button is still in the description block');
|
||||
|
||||
if (!pollHandlers)
|
||||
return Promise.resolve();
|
||||
return;
|
||||
|
||||
/* One tick reads one file per registered target, keyed by basename. */
|
||||
return Promise.resolve(pollHandlers.read()).then(() => {
|
||||
@@ -393,6 +450,7 @@ function testLogViewWiring() {
|
||||
].sort()),
|
||||
JSON.stringify(reads));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testNodesValidator();
|
||||
|
||||
@@ -862,6 +862,63 @@ else
|
||||
echo "PASS: the dnsmasq snippets are gone after the client is switched off"
|
||||
fi
|
||||
|
||||
# --- the rule-set mtime gate -------------------------------------------
|
||||
#
|
||||
# Regenerating china-domain.json measured 3540 ms on the target and the
|
||||
# source lists are replaced at most daily, so hp_prepare_runtime_files skips
|
||||
# the rebuild when the output is already up to date. Asserted against the
|
||||
# function rather than through a scenario: every scenario in this file runs
|
||||
# against a sandbox that has no pre-existing rule-set, so the skip branch is
|
||||
# unreachable from a scenario and the regression it guards would be silent.
|
||||
#
|
||||
# The three conditions that must still rebuild are exactly the three the
|
||||
# unconditional version existed for - a fresh install, a truncated previous
|
||||
# run, and a list replaced while the service was stopped. All four cases are
|
||||
# checked, because a gate that skips too eagerly is worse than the 3.5s it
|
||||
# saves: it would keep a rule-set the source has moved past.
|
||||
# `.` is a POSIX special builtin: sourcing a missing file terminates a
|
||||
# non-interactive shell outright, and `||` does not get a chance to run. So
|
||||
# this names the real path and never guards it.
|
||||
. "$ROOT/root/etc/homeproxy-pro/scripts/runtime/service.sh"
|
||||
|
||||
hp_rs_dir="$(mktemp -d "${TMPDIR:-/tmp}/hp-rule-set.XXXXXX")"
|
||||
hp_rs_src="$hp_rs_dir/src.txt"
|
||||
hp_rs_out="$hp_rs_dir/out.json"
|
||||
printf 'a.cn\n' > "$hp_rs_src"
|
||||
|
||||
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
|
||||
echo "FAIL: a missing rule-set counted as current, so a fresh install would never get one"
|
||||
exit 1
|
||||
else
|
||||
echo "PASS: a missing rule-set is regenerated"
|
||||
fi
|
||||
|
||||
: > "$hp_rs_out"
|
||||
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
|
||||
echo "FAIL: an empty rule-set counted as current; it would match nothing and invert the split"
|
||||
exit 1
|
||||
else
|
||||
echo "PASS: an empty rule-set is regenerated"
|
||||
fi
|
||||
|
||||
printf '{"rules":[]}\n' > "$hp_rs_out"; sleep 1; touch "$hp_rs_src"
|
||||
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
|
||||
echo "FAIL: a rule-set older than its source counted as current"
|
||||
exit 1
|
||||
else
|
||||
echo "PASS: a rule-set older than its source is regenerated"
|
||||
fi
|
||||
|
||||
sleep 1; touch "$hp_rs_out"
|
||||
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
|
||||
echo "PASS: an up-to-date rule-set is kept (the reload skip still works)"
|
||||
else
|
||||
echo "FAIL: an up-to-date rule-set would be rebuilt; the reload-time skip regressed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$hp_rs_dir"
|
||||
|
||||
if cmp -s "$GOLDEN" "$TRACE.norm"; then
|
||||
echo "PASS: runtime orchestration matches $(basename "$GOLDEN")"
|
||||
# Counted from the golden rather than hardcoded: a scenario added without
|
||||
|
||||
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=uugamebooster
|
||||
PKG_VERSION:=14.6.24
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://uurouter.gdl.netease.com/uuplugin/openwrt-$(ARCH)/v$(PKG_VERSION)/uu.tar.gz?
|
||||
|
||||
@@ -9,6 +9,18 @@ PROG="/usr/bin/uugamebooster"
|
||||
|
||||
UU_CONF="/usr/share/uugamebooster/uu.conf"
|
||||
|
||||
firewall_rule() {
|
||||
json_add_object ""
|
||||
json_add_string type rule
|
||||
json_add_string src '*'
|
||||
[ "$1" = forward ] && json_add_string dest '*'
|
||||
json_add_string device tun163
|
||||
json_add_string direction "$2"
|
||||
json_add_string proto all
|
||||
json_add_string target ACCEPT
|
||||
json_close_object
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
local enabled
|
||||
@@ -24,9 +36,29 @@ start_service() {
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
# UU's own nftables ACCEPT rules cannot bypass fw4's later base chains.
|
||||
# Publish tunnel rules through procd so fw4 owns their full lifecycle.
|
||||
if [ -x /sbin/fw4 ]; then
|
||||
procd_open_data
|
||||
json_add_array firewall
|
||||
firewall_rule input in
|
||||
firewall_rule forward in
|
||||
firewall_rule forward out
|
||||
json_close_array
|
||||
procd_close_data
|
||||
fi
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_started() {
|
||||
[ ! -x /sbin/fw4 ] || procd_set_config_changed firewall
|
||||
}
|
||||
|
||||
service_stopped() {
|
||||
[ ! -x /sbin/fw4 ] || procd_set_config_changed firewall
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
|
||||
Reference in new issue
Block a user