mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 23:33:56 +08:00
🚀 Sync 2026-09-28 12:35:46
This commit is contained in:
1 parent
8b304e4854
commit
86e1d0663a
155 files changed
-172498
No files matched your search
@@ -1,292 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* Test double for root/etc/homeproxy/scripts/homeproxy.uc.
|
||||
*
|
||||
* Only `validation()` is stubbed: the real one shells out to
|
||||
* /sbin/validate_data, which does not exist outside OpenWrt. That stand-in
|
||||
* reproduces the `libvalidate` decisions for the five datatypes the callers
|
||||
* actually use - see the comment on validation() below for the contract and
|
||||
* the known differences. Everything else (isEmpty, decodeBase64Str, parseURL,
|
||||
* shellQuote, redactUrl) is a verbatim copy so the parser tests exercise the
|
||||
* same string handling as production. Keep these in sync when homeproxy.uc
|
||||
* changes; tests/ucode/test_mock_sync.sh enforces that for everything except
|
||||
* validation().
|
||||
*/
|
||||
|
||||
import { urldecode_params } from 'luci.http';
|
||||
|
||||
/* Verbatim copy of homeproxy.uc:shellQuote - migrate_config.uc imports it to
|
||||
* build the crontab rewrite, so it has to exist here too. Compared against
|
||||
* production by tests/ucode/test_mock_sync.sh. */
|
||||
export function shellQuote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
};
|
||||
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export function decodeBase64Str(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
str = trim(str);
|
||||
str = replace(str, /_/g, '/');
|
||||
str = replace(str, /-/g, '+');
|
||||
|
||||
const padding = length(str) % 4;
|
||||
if (padding)
|
||||
str = str + substr('====', padding);
|
||||
|
||||
return b64dec(str);
|
||||
};
|
||||
|
||||
/* Verbatim copy of homeproxy.uc:redactUrl - decoder.uc imports it so the
|
||||
* subscription token stays out of the parse-failed log line. */
|
||||
export function redactUrl(url) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return '';
|
||||
|
||||
let u = url;
|
||||
|
||||
/* userinfo: scheme://user:pass@host -> scheme://***@host */
|
||||
const at = index(u, '@');
|
||||
const scheme = match(u, /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//);
|
||||
if (scheme && at !== -1 && at > length(scheme[0]))
|
||||
u = substr(u, 0, length(scheme[0])) + '***' + substr(u, at);
|
||||
|
||||
/* path and query: keep the authority, mark the rest. Splitting them
|
||||
* rather than masking from the first '/' keeps the structural markers
|
||||
* (`/***?***`) so a reader can still tell a path from a query, and it is
|
||||
* why the authority end is the *earlier* of the two separators. */
|
||||
const scheme_end = scheme ? length(scheme[0]) : 0;
|
||||
const rest = substr(u, scheme_end);
|
||||
const path_at = index(rest, '/');
|
||||
const query_at = index(rest, '?');
|
||||
const has_path = path_at !== -1 && (query_at === -1 || path_at < query_at);
|
||||
|
||||
let authority_end = length(rest);
|
||||
if (has_path)
|
||||
authority_end = path_at;
|
||||
else if (query_at !== -1)
|
||||
authority_end = query_at;
|
||||
|
||||
let tail = '';
|
||||
if (has_path)
|
||||
tail += '/***';
|
||||
if (query_at !== -1)
|
||||
tail += '?***';
|
||||
|
||||
u = substr(u, 0, scheme_end + authority_end) + tail;
|
||||
|
||||
return u;
|
||||
};
|
||||
|
||||
/* inet_pton(AF_INET, ...): four decimal octets, 0-255, no leading zeros.
|
||||
* A helper rather than a call through validation(), because ucode resolves
|
||||
* module names lexically and validation() is declared below these. */
|
||||
function isValidIp4(data) {
|
||||
if (match(data, /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) === null)
|
||||
return false;
|
||||
for (let octet in split(data, '.'))
|
||||
if (int(octet) > 255 || (length(octet) > 1 && substr(octet, 0, 1) === '0'))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
/* inet_pton(AF_INET6, ...) for the textual forms a share link can carry:
|
||||
* at most one "::", 1-4 hex digits per group, exactly eight groups unless
|
||||
* "::" fills the gap, and an optional dotted-quad tail (the last 32 bits)
|
||||
* that counts as two groups. A "%zone" suffix, a CIDR prefix and surrounding
|
||||
* whitespace are all rejected, exactly as inet_pton rejects them. */
|
||||
function isValidIp6(data) {
|
||||
if (match(data, /^[0-9A-Fa-f:.]+$/) == null)
|
||||
return false;
|
||||
|
||||
/* A dotted-quad must be the tail; rewrite it as the two hex groups it
|
||||
* stands for, so the rest of the parse only sees "[0-9A-Fa-f:]". */
|
||||
if (index(data, '.') !== -1) {
|
||||
const quad = match(data, /^(.*:)([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})$/);
|
||||
if (quad === null || !isValidIp4(quad[2]))
|
||||
return false;
|
||||
|
||||
const octets = split(quad[2], '.');
|
||||
data = quad[1] + sprintf('%02x%02x:%02x%02x',
|
||||
int(octets[0]), int(octets[1]), int(octets[2]), int(octets[3]));
|
||||
}
|
||||
|
||||
const dbl = index(data, '::');
|
||||
let left = data, right = '';
|
||||
|
||||
if (dbl !== -1) {
|
||||
if (index(substr(data, dbl + 2), '::') !== -1)
|
||||
return false;
|
||||
left = substr(data, 0, dbl);
|
||||
right = substr(data, dbl + 2);
|
||||
}
|
||||
|
||||
let explicit = 0;
|
||||
for (let part in [ left, right ]) {
|
||||
if (part === '')
|
||||
continue;
|
||||
const groups = split(part, ':');
|
||||
for (let group in groups)
|
||||
if (match(group, /^[0-9A-Fa-f]{1,4}$/) === null)
|
||||
return false;
|
||||
explicit += length(groups);
|
||||
}
|
||||
|
||||
/* "::" has to stand for at least one group, so with it at most seven
|
||||
* groups may be written out; without it there must be exactly eight. */
|
||||
return dbl === -1 ? explicit === 8 : explicit < 8;
|
||||
};
|
||||
|
||||
/* Pure-ucode stand-in for the datatypes homeproxy.uc asks /sbin/validate_data
|
||||
* to check. Production shells out to OpenWrt's `libvalidate` (ubox
|
||||
* src/validate.c); off-target this function is what the parser unit tests and
|
||||
* tests/toolchain/validate-data.sh exercise, so it answers the same way the
|
||||
* real binary does for those five types:
|
||||
*
|
||||
* ip4addr inet_pton(AF_INET) - four decimal octets, 0-255, no leading
|
||||
* zeros ("01.2.3.4" and "1.2.3.04" are invalid)
|
||||
* ip6addr inet_pton(AF_INET6) - see isValidIp6 above; "2001:db8::1",
|
||||
* "::ffff:1.2.3.4" and "1:2:3:4:5:6:7:8" are
|
||||
* valid, "12345::1" and "fe80::1%eth0" are not
|
||||
* hostname dt_type_hostname - dot-separated labels of [A-Za-z0-9_-], each
|
||||
* 1..63 chars, the final label 1..255 chars;
|
||||
* "-a", "a-" and "a_b" are valid, "a..b",
|
||||
* "a%b", "foo." and ".foo" are not
|
||||
* host hostname || ip4addr || ip6addr
|
||||
* port dt_type_port - strtoul(v, &e, 10) with the whole string consumed
|
||||
* and the value compared as a signed 32-bit
|
||||
* int, so "-1" and " 80" are accepted and
|
||||
* "65536"/"80 " are not
|
||||
*
|
||||
* Known differences, none reachable from the callers:
|
||||
* - only those five types are modelled. The real binary also validates
|
||||
* macaddr, cidr, netmask, file/directory, uci references, ...; an unknown
|
||||
* type exits non-zero there (falsy) and returns false here.
|
||||
* - strtoul()'s saturation above 2^64 is not reproduced: a digit string
|
||||
* that long would wrap instead of clamping, so a port > 2^64 might
|
||||
* disagree. No share link carries one.
|
||||
*
|
||||
* tests/ucode/test_mock_sync.sh deliberately excludes this function from its
|
||||
* production-vs-mock corpus (it is a stand-in, not a copy); it is pinned by
|
||||
* the parser tests and by tests/toolchain/validate-data.sh. */
|
||||
export function validation(datatype, data) {
|
||||
if (!datatype || !data)
|
||||
return null;
|
||||
|
||||
switch (datatype) {
|
||||
case 'port': {
|
||||
const parts = match(data, /^[ \t\n\v\f\r]*([+-]?)([0-9]+)$/);
|
||||
if (parts === null)
|
||||
return false;
|
||||
|
||||
let n = 0;
|
||||
for (let digit in split(parts[2], ''))
|
||||
n = (n * 10 + int(digit)) % 4294967296;
|
||||
|
||||
if (parts[1] === '-' && n !== 0)
|
||||
n = 4294967296 - n;
|
||||
if (n > 2147483647)
|
||||
n -= 4294967296;
|
||||
|
||||
return n <= 65535;
|
||||
}
|
||||
case 'ip4addr':
|
||||
return isValidIp4(data);
|
||||
case 'ip6addr':
|
||||
return isValidIp6(data);
|
||||
case 'hostname':
|
||||
/* dt_type_hostname: every dot-terminated label is 1..63 chars, the
|
||||
* last one 1..255, all from [A-Za-z0-9_-]. */
|
||||
return match(data, /^([A-Za-z0-9_-]{1,63}\.)*[A-Za-z0-9_-]{1,255}$/) !== null;
|
||||
case 'host':
|
||||
return validation('ip4addr', data) === true ||
|
||||
validation('ip6addr', data) === true ||
|
||||
validation('hostname', data) === true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export function parseURL(url) {
|
||||
if (type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
const services = {
|
||||
http: '80',
|
||||
https: '443'
|
||||
};
|
||||
|
||||
const objurl = {};
|
||||
|
||||
objurl.href = url;
|
||||
|
||||
url = replace(url, /#(.+)$/, (_, val) => {
|
||||
objurl.hash = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => {
|
||||
objurl.protocol = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /\?(.+)/, (_, val) => {
|
||||
objurl.search = val;
|
||||
objurl.searchParams = urldecode_params(val);
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^\/\/([^\/]+)/, (_, val) => {
|
||||
val = replace(val, /^([^@]+)@/, (_, val) => {
|
||||
objurl.userinfo = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
val = replace(val, /:(\d+)$/, (_, val) => {
|
||||
objurl.port = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (validation('ip4addr', val) ||
|
||||
validation('ip6addr', replace(val, /\[|\]/g, '')) ||
|
||||
validation('hostname', val))
|
||||
objurl.hostname = val;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
objurl.pathname = url || '/';
|
||||
|
||||
if (!objurl.protocol || !objurl.hostname)
|
||||
return null;
|
||||
|
||||
if (objurl.userinfo) {
|
||||
objurl.userinfo = replace(objurl.userinfo, /:(.+)$/, (_, val) => {
|
||||
objurl.password = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) {
|
||||
objurl.username = objurl.userinfo;
|
||||
delete objurl.userinfo;
|
||||
} else {
|
||||
delete objurl.userinfo;
|
||||
delete objurl.password;
|
||||
}
|
||||
};
|
||||
|
||||
if (!objurl.port)
|
||||
objurl.port = services[objurl.protocol];
|
||||
|
||||
objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : '');
|
||||
objurl.origin = `${objurl.protocol}://${objurl.host}`;
|
||||
|
||||
return objurl;
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* homeproxy module shim for subscription/fetcher unit tests.
|
||||
*
|
||||
* The real homeproxy.uc wraps wGETVerbose around `executeCommand(
|
||||
* /usr/bin/wget ...)`, which means a unit test that imports it
|
||||
* would shell out to the network. Tests stage this file as
|
||||
* `homeproxy` in their -L directory so the fetcher picks up the
|
||||
* shim's wGETVerbose instead.
|
||||
*
|
||||
* The shim reads HP_TEST_WGET_CONTENT / HP_TEST_WGET_ERROR globals
|
||||
* (set by the test before each fetch call) and returns them. The
|
||||
* two other names the fetcher imports (isEmpty, redactUrl) are
|
||||
* short enough to copy verbatim from homeproxy.uc so the shim is
|
||||
* fully self-contained - the real homeproxy.uc also imports
|
||||
* validate_data / popen / a long list of luci.* modules that the
|
||||
* testbed does not have on PATH, and pulling the full file would
|
||||
* pull those imports along.
|
||||
*
|
||||
* The `global` prefix is the only cross-module shared state ucode
|
||||
* exposes without a dependency-injection hook, and the test is the
|
||||
* only writer.
|
||||
*/
|
||||
|
||||
export function wGETVerbose(url, _ua) {
|
||||
const content = global.HP_TEST_WGET_CONTENT;
|
||||
const error = global.HP_TEST_WGET_ERROR;
|
||||
return { content: content, error: error };
|
||||
};
|
||||
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
/* Verbatim copy of homeproxyuc:redactUrl - keeps the security
|
||||
* behaviour covered in test_subscription_fetcher.uc. */
|
||||
export function redactUrl(url) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return '';
|
||||
|
||||
let u = url;
|
||||
|
||||
/* userinfo: scheme://user:pass@host -> scheme://***@host */
|
||||
const at = index(u, '@');
|
||||
const scheme = match(u, /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//);
|
||||
if (scheme && at !== -1 && at > length(scheme[0]))
|
||||
u = substr(u, 0, length(scheme[0])) + '***' + substr(u, at);
|
||||
|
||||
/* path and query: keep the authority, mark the rest. Splitting them
|
||||
* rather than masking from the first '/' keeps the structural markers
|
||||
* (`/***?***`) so a reader can still tell a path from a query, and it is
|
||||
* why the authority end is the *earlier* of the two separators. */
|
||||
const scheme_end = scheme ? length(scheme[0]) : 0;
|
||||
const rest = substr(u, scheme_end);
|
||||
const path_at = index(rest, '/');
|
||||
const query_at = index(rest, '?');
|
||||
const has_path = path_at !== -1 && (query_at === -1 || path_at < query_at);
|
||||
|
||||
let authority_end = length(rest);
|
||||
if (has_path)
|
||||
authority_end = path_at;
|
||||
else if (query_at !== -1)
|
||||
authority_end = query_at;
|
||||
|
||||
let tail = '';
|
||||
if (has_path)
|
||||
tail += '/***';
|
||||
if (query_at !== -1)
|
||||
tail += '?***';
|
||||
|
||||
u = substr(u, 0, scheme_end + authority_end) + tail;
|
||||
|
||||
return u;
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Test double for root/etc/homeproxy/scripts/homeproxy.uc, scoped to
|
||||
* what firewall_pre.uc imports: isEmpty, RUN_DIR, validation.
|
||||
*
|
||||
* RUN_DIR is read from the HP_FW_RUN_DIR global so the test can
|
||||
* point the script at a scratch dir; firewall_pre.uc writes
|
||||
* fw4_forward.nft / fw4_input.nft into RUN_DIR, and the test must
|
||||
* not clobber /var/run/homeproxy on a host that happens to have it.
|
||||
*
|
||||
* validation() is the same pure-ucode stand-in the parser tests use
|
||||
* (the production one shells out to /sbin/validate_data, which does
|
||||
* not exist off-target). Keep it in sync with mocks/homeproxy.uc.
|
||||
*/
|
||||
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export const RUN_DIR = global.HP_FW_RUN_DIR;
|
||||
|
||||
/* Pure-ucode stand-in for /sbin/validate_data (port / ip4 / ip6 / hostname). */
|
||||
export function validation(datatype, data) {
|
||||
if (!datatype || !data)
|
||||
return null;
|
||||
|
||||
switch (datatype) {
|
||||
case 'port':
|
||||
return match(data, /^\d+$/) != null && int(data) >= 0 && int(data) <= 65535;
|
||||
case 'ip4addr':
|
||||
if (match(data, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) == null)
|
||||
return false;
|
||||
for (let octet in split(data, '.'))
|
||||
if (int(octet) > 255)
|
||||
return false;
|
||||
return true;
|
||||
case 'ip6addr':
|
||||
return match(data, /^[0-9a-fA-F:]+$/) != null && match(data, /::.*::/) == null;
|
||||
case 'hostname':
|
||||
return match(data, /^[A-Za-z0-9_][A-Za-z0-9_%-.]*[A-Za-z0-9]$/) != null ||
|
||||
match(data, /^[A-Za-z0-9_]$/) != null;
|
||||
case 'host':
|
||||
return validation('ip4addr', data) === true ||
|
||||
validation('ip6addr', data) === true ||
|
||||
validation('hostname', data) === true;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user