mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 07:03:56 +08:00
293 lines
9.3 KiB
Ucode
293 lines
9.3 KiB
Ucode
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Copyright (C) 2025 ImmortalWrt.org
|
|
*
|
|
* Test double for root/etc/homeproxy-pro/scripts/homeproxy-pro.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-pro.uc
|
|
* changes; tests/ucode/test_mock_sync.sh enforces that for everything except
|
|
* validation().
|
|
*/
|
|
|
|
import { urldecode_params } from 'luci.http';
|
|
|
|
/* Verbatim copy of homeproxy-pro.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-pro.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-pro.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;
|
|
};
|