mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 07:13:56 +08:00
🐶 Sync 2026-10-03 11:32:07
This commit is contained in:
1 parent
a5bb91e6ed
commit
66e3d835c4
72 files changed
+5107
-1799
No files matched your search
@@ -133,6 +133,45 @@ check('cert path ignores an empty value',
|
||||
check('cert path ignores a missing section',
|
||||
hp.validateCertificatePath(null, '/etc/passwd') === true);
|
||||
|
||||
/* validateRuleSetPath: the rule-set policy has to be the same list the backend
|
||||
* enforces (RULE_PATH_ROOTS in homeproxy-pro.uc; guard 50 compares them
|
||||
* textually). It is deliberately NARROWER than the certificate list and than
|
||||
* the backend's general HP_DIR gate - a rule-set belongs in the archive and
|
||||
* nowhere else - so the interesting assertions are the ones where a path is
|
||||
* inside /etc/homeproxy-pro but outside the archive, and where it is under the
|
||||
* /tmp/homeproxy_ prefix the certificate list also refuses. A UI built on the
|
||||
* general gate would offer both, and the generator would then refuse them. */
|
||||
check('rule-set path accepts the archive',
|
||||
hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/ruleset/example.srs') === true);
|
||||
check('rule-set path rejects a bare root',
|
||||
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/ruleset/')));
|
||||
check('rule-set path rejects /etc/passwd',
|
||||
isError(hp.validateRuleSetPath('sec1', '/etc/passwd')));
|
||||
check('rule-set path rejects another path under /etc/homeproxy-pro',
|
||||
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/resources/china_ip4.json')));
|
||||
check('rule-set path rejects the certs directory',
|
||||
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/certs/server_privatekey.pem')));
|
||||
check('rule-set path rejects /tmp/homeproxy_ upload staging',
|
||||
isError(hp.validateRuleSetPath('sec1', '/tmp/homeproxy_ruleset_upload.tmp')));
|
||||
check('rule-set path rejects a traversal out of the archive',
|
||||
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/ruleset/../../etc/shadow')));
|
||||
check('rule-set path rejects a relative path',
|
||||
isError(hp.validateRuleSetPath('sec1', 'etc/homeproxy-pro/ruleset/x.srs')));
|
||||
check('rule-set path ignores an empty value',
|
||||
hp.validateRuleSetPath('sec1', '') === true);
|
||||
check('rule-set path ignores a missing section',
|
||||
hp.validateRuleSetPath(null, '/etc/passwd') === true);
|
||||
|
||||
/* The placeholder and the datalist entry the form offers have to be paths this
|
||||
* validator accepts. A placeholder naming a directory the field then refuses is
|
||||
* the same class of bug the mirrored lists exist to prevent, and the
|
||||
* placeholder is what a user copies. */
|
||||
const defaultPath = hp.rule_path_default;
|
||||
check('the rule-set form offers a default path',
|
||||
typeof defaultPath === 'string' && defaultPath.length > 0, String(defaultPath));
|
||||
check('the offered default path passes its own validator',
|
||||
hp.validateRuleSetPath('sec1', defaultPath) === true, String(defaultPath));
|
||||
|
||||
/* --- dns_server: the legacy 'wan' value has to stay acceptable ---------- *
|
||||
* The preset-mode "Overseas DNS server" field used to offer a 'wan' entry
|
||||
* ("WAN DNS (read from interface)"). The entry is gone from the dropdown -
|
||||
@@ -228,5 +267,92 @@ check('dns_server still rejects an empty value', isError(validateDnsServer('')))
|
||||
check('dns_server still rejects a value that is neither a hostname nor an address',
|
||||
isError(validateDnsServer('not a dns server')));
|
||||
|
||||
/* --- subscription.js: the update_interval validator ---------------------- *
|
||||
* sing-box parses update_interval as a Go duration. A bare number used to
|
||||
* reach it unchanged and come back as a measured hard failure - `time:
|
||||
* missing unit in duration "3600"` - which rejects the WHOLE configuration,
|
||||
* so the reload is aborted and the user sees only "my change did not take".
|
||||
* The generator now normalises bare numbers (strToTime), and this validator
|
||||
* is what stops the shapes Go cannot read at all, on the page.
|
||||
*
|
||||
* The callback is registered inline in render(), so the module is loaded
|
||||
* against a form mock and the stored option is driven the way a widget calls
|
||||
* it. `1d` is the important rejection: it is what the placeholder used to
|
||||
* show, it is the value a user is most likely to copy, and Go's duration
|
||||
* units are ns/us/ms/s/m/h - there is no day.
|
||||
*/
|
||||
function updateIntervalValidate() {
|
||||
const classes = [ 'Value', 'ListValue', 'Flag', 'DynamicList', 'MultiValue', 'TextValue',
|
||||
'Button', 'TypedSection', 'NamedSection', 'GridSection', 'SectionValue' ];
|
||||
const formMock = {};
|
||||
for (const name of classes) {
|
||||
formMock[name] = class {};
|
||||
formMock[name].__name__ = name;
|
||||
}
|
||||
|
||||
let captured = null;
|
||||
const sectionMock = {
|
||||
tab() {},
|
||||
taboption(_tab, kind, name, title, description) { return this.option(kind, name, title, description); },
|
||||
option(kind, name, title, description) {
|
||||
const option = { kind, name, title, description, value() {}, depends() {} };
|
||||
if (kind && kind.__name__ === 'SectionValue')
|
||||
option.subsection = sectionMock;
|
||||
if (name === 'update_interval')
|
||||
captured = option;
|
||||
return option;
|
||||
}
|
||||
};
|
||||
|
||||
const sub = loadLuciModule(
|
||||
path.join(root, 'htdocs/luci-static/resources/view/homeproxy-pro/client/subscription.js'),
|
||||
{
|
||||
baseclass: { extend: (o) => o },
|
||||
form: formMock,
|
||||
uci: { sections: () => {} },
|
||||
homeproxy-pro: {
|
||||
renderSectionAdd: () => {},
|
||||
loadModalTitle: () => {},
|
||||
loadDefaultLabel: () => {},
|
||||
validateUniqueValue: () => true,
|
||||
validateRuleSetPath: () => true,
|
||||
rule_path_default: '/etc/homeproxy-pro/ruleset/example.srs'
|
||||
}
|
||||
},
|
||||
/* The module binds four of its labels through L.bind() at render
|
||||
* time, so the runtime has to carry one. A passthrough is enough -
|
||||
* nothing under test reads what those labels resolve to - but the
|
||||
* default runtime has no bind() at all, which is why this mock is
|
||||
* passed rather than left implicit. */
|
||||
{ L: { bind: (fn) => fn } }
|
||||
);
|
||||
|
||||
sub.render({ s: sectionMock });
|
||||
|
||||
const validator = captured && captured.validate;
|
||||
if (typeof validator !== 'function')
|
||||
throw new Error('subscription.js no longer registers an update_interval validator');
|
||||
|
||||
const ctx = { section: { formvalue: () => 'remote' } };
|
||||
return (value) => validator.call(ctx, 'ruleset0', value);
|
||||
}
|
||||
|
||||
const validateUpdateInterval = updateIntervalValidate();
|
||||
|
||||
/* Accepted: a bare number (the generator appends "s") and Go's own duration
|
||||
* grammar, including compounds like 1h30m. */
|
||||
for (const ok of [ '3600', '24h', '1h', '30m', '1h30m', '500ms', '1.5h', '90s' ])
|
||||
check(`update_interval accepts '${ok}'`, validateUpdateInterval(ok) === true, String(validateUpdateInterval(ok)));
|
||||
|
||||
/* Refused: a day/week/year unit, prose, a spaced value, and a negative. */
|
||||
for (const bad of [ '1d', '1w', '1y', 'daily', '24 h', '-1h', '1 h', 'h', '1x' ])
|
||||
check(`update_interval rejects '${bad}'`, isError(validateUpdateInterval(bad)));
|
||||
|
||||
/* Optional, not required: an empty value means "no update_interval", which
|
||||
* sing-box reads as its own default. Making the field required would refuse
|
||||
* to save an existing, working remote rule-set. */
|
||||
check('update_interval accepts an empty value', validateUpdateInterval('') === true);
|
||||
check('update_interval ignores a missing section', validateUpdateInterval(null, '1d') === true);
|
||||
|
||||
console.log(`frontend validators: ${checks} checks, ${failures} failures`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in new issue
Block a user