mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 17:53:56 +08:00
🤞 Sync 2026-10-17 10:45:37
This commit is contained in:
1 parent
1849cc6156
commit
315f273687
107 files changed
+3810
-1215
No files matched your search
@@ -0,0 +1,81 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const script = path.join(root, 'root/usr/lib/minigate/geofence.sh');
|
||||
|
||||
function runLibrary(body, accessLog = '') {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-geofence-test-'));
|
||||
const log = path.join(dir, 'access.log');
|
||||
fs.writeFileSync(log, accessLog);
|
||||
const command = `
|
||||
set -u
|
||||
export MINIGATE_GEOFENCE_LIBRARY_ONLY=1
|
||||
export MINIGATE_ACCESS_LOG=${JSON.stringify(log)}
|
||||
export MINIGATE_GEOFENCE_LOG=${JSON.stringify(path.join(dir, 'geofence.log'))}
|
||||
export MINIGATE_GEO_CACHE_DIR=${JSON.stringify(path.join(dir, 'cache'))}
|
||||
. ${JSON.stringify(script)}
|
||||
${body}
|
||||
`;
|
||||
return spawnSync('/bin/sh', ['-c', command], { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
const entry = client => JSON.stringify({ time: '2026-10-17T07:22:40+08:00', domain: 'box.example.com', client, status: 200 });
|
||||
const ACCESS_LOG = [
|
||||
entry('192.168.2.1'),
|
||||
entry('198.51.100.7'),
|
||||
entry('172.20.0.5'),
|
||||
entry('203.0.113.9'),
|
||||
entry('198.51.100.7'),
|
||||
entry('2001:db8::1'),
|
||||
entry('100.100.1.1'),
|
||||
entry('192.168.2.15')
|
||||
].join('\n') + '\n';
|
||||
|
||||
test('private, loopback, link-local and carrier NAT addresses are recognised', () => {
|
||||
const privateIps = ['0.0.0.0', '10.1.2.3', '127.0.0.1', '169.254.1.1', '172.16.0.1', '172.31.255.255',
|
||||
'192.168.2.15', '100.64.0.1', '100.99.1.1', '100.127.255.255'];
|
||||
const publicIps = ['172.15.0.1', '172.32.0.1', '100.63.255.255', '100.128.0.1', '198.51.100.7', '1.10.0.1', '192.169.0.1'];
|
||||
const check = ips => ips.map(ip => `is_private_ip ${ip} && echo "${ip} private" || echo "${ip} public"`).join('\n');
|
||||
const result = runLibrary(check([...privateIps, ...publicIps]));
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.stdout.trim().split('\n'), [
|
||||
...privateIps.map(ip => `${ip} private`),
|
||||
...publicIps.map(ip => `${ip} public`)
|
||||
]);
|
||||
});
|
||||
|
||||
test('client extraction works without grep -P and keeps only unique public IPv4 visitors', () => {
|
||||
const result = runLibrary(`
|
||||
grep() { [ "\${1:-}" = "-oP" ] && { echo "grep: unrecognized option: P" >&2; return 2; }; command grep "$@"; }
|
||||
extract_public_ips
|
||||
`, ACCESS_LOG);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.stdout.trim().split('\n'), ['198.51.100.7', '203.0.113.9']);
|
||||
});
|
||||
|
||||
test('scan never looks up or blocks LAN visitors', () => {
|
||||
const result = runLibrary(`
|
||||
uci() {
|
||||
[ "\${1:-}" = "-q" ] && shift
|
||||
case "\${2:-}" in
|
||||
minigate.global.geo_enabled) echo 1 ;;
|
||||
minigate.global.geo_allowed) echo 四川 ;;
|
||||
esac
|
||||
}
|
||||
init_block_set() { :; }
|
||||
nft() { :; }
|
||||
sleep() { :; }
|
||||
lookup_geo() { echo "lookup $1" >&2; echo 广东; }
|
||||
block_ip() { echo "block $1"; }
|
||||
mkdir -p "$CACHE_DIR"
|
||||
scan_and_block
|
||||
`, ACCESS_LOG);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.stderr.trim().split('\n'), ['lookup 198.51.100.7', 'lookup 203.0.113.9']);
|
||||
assert.deepEqual(result.stdout.trim().split('\n'), ['block 198.51.100.7', 'block 203.0.113.9']);
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const script = path.join(root, 'root/usr/lib/minigate/proxy.sh');
|
||||
const HEADER = '# MiniGate 局域网直连解析,由 proxy.sh 自动生成';
|
||||
|
||||
const SITES = {
|
||||
'000_default_2000_1_1_.conf': 'server {\n listen unix:/var/run/minigate/https_2000.sock default_server ssl proxy_protocol;\n server_name _;\n return 444;\n}\n',
|
||||
'site_1.conf': 'server {\n listen unix:/var/run/minigate/https_2000.sock ssl proxy_protocol;\n server_name ly.example.com;\n}\nserver {\n listen unix:/var/run/minigate/http_2000.sock proxy_protocol;\n server_name ly.example.com;\n return 308 https://ly.example.com:2000$request_uri;\n}\n',
|
||||
'site_2.conf': 'server {\n listen 2000 ssl;\n server_name box.example.com;\n}\n',
|
||||
'site_3.conf': 'server {\n listen 443 ssl;\n server_name *.wild.example.com;\n}\n'
|
||||
};
|
||||
|
||||
function sandbox({ sites = SITES, hosts } = {}) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lan-dns-test-'));
|
||||
const sitesDir = path.join(dir, 'sites');
|
||||
fs.mkdirSync(sitesDir);
|
||||
for (const [name, content] of Object.entries(sites)) fs.writeFileSync(path.join(sitesDir, name), content);
|
||||
const hostsFile = path.join(dir, 'hosts', 'minigate');
|
||||
if (hosts !== undefined) {
|
||||
fs.mkdirSync(path.dirname(hostsFile));
|
||||
fs.writeFileSync(hostsFile, hosts);
|
||||
}
|
||||
return { dir, sitesDir, hostsFile, hupLog: path.join(dir, 'hup.log'), logFile: path.join(dir, 'proxy.log') };
|
||||
}
|
||||
|
||||
// 以库模式加载 proxy.sh,替换 uci/ubus/pidof/send_hup 后执行一段命令
|
||||
function runLibrary(box, body, { lanDns = '1', lanIp = '', ifaceIp = '192.168.2.2', networkIp = '' } = {}) {
|
||||
const command = `
|
||||
set -u
|
||||
export MINIGATE_PROXY_LIBRARY_ONLY=1
|
||||
export MINIGATE_SITES_DIR=${JSON.stringify(box.sitesDir)}
|
||||
export MINIGATE_LAN_HOSTS_FILE=${JSON.stringify(box.hostsFile)}
|
||||
export MINIGATE_LOGFILE=${JSON.stringify(box.logFile)}
|
||||
. ${JSON.stringify(script)}
|
||||
uci() {
|
||||
[ "\${1:-}" = "-q" ] && shift
|
||||
case "\${1:-}:\${2:-}" in
|
||||
get:minigate.global.lan_dns) printf '%s\\n' ${JSON.stringify(lanDns)} ;;
|
||||
get:minigate.global.lan_ip) [ -n ${JSON.stringify(lanIp)} ] && printf '%s\\n' ${JSON.stringify(lanIp)} ;;
|
||||
get:network.lan.ipaddr) [ -n ${JSON.stringify(networkIp)} ] && printf '%s\\n' ${JSON.stringify(networkIp)} ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
ubus() { [ -n ${JSON.stringify(ifaceIp)} ] && printf '%s\\n' ${JSON.stringify(ifaceIp)}; }
|
||||
jsonfilter() { cat; }
|
||||
pidof() { printf '101 102\\n'; }
|
||||
send_hup() { printf '%s\\n' "$*" >> ${JSON.stringify(box.hupLog)}; }
|
||||
${body}
|
||||
`;
|
||||
return spawnSync('/bin/sh', ['-c', command], { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
const read = file => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null);
|
||||
const hups = box => (read(box.hupLog) || '').split('\n').filter(Boolean);
|
||||
|
||||
test('enabled sync maps every concrete site domain to the LAN address and reloads dnsmasq once', () => {
|
||||
const box = sandbox();
|
||||
let result = runLibrary(box, 'sync_lan_hosts');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(read(box.hostsFile), `${HEADER}\n192.168.2.2 box.example.com\n192.168.2.2 ly.example.com\n`);
|
||||
assert.deepEqual(hups(box), ['101 102']);
|
||||
assert.match(read(box.logFile), /局域网直连解析 -> 192\.168\.2\.2: box\.example\.com ly\.example\.com/);
|
||||
|
||||
// 内容不变时不再重载,避免每次保存都清空 dnsmasq 缓存
|
||||
result = runLibrary(box, 'sync_lan_hosts');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(hups(box), ['101 102']);
|
||||
});
|
||||
|
||||
test('a changed address rewrites the file and reloads again', () => {
|
||||
const box = sandbox({ hosts: `${HEADER}\n192.168.2.9 box.example.com\n192.168.2.9 ly.example.com\n` });
|
||||
const result = runLibrary(box, 'sync_lan_hosts');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(read(box.hostsFile), /^192\.168\.2\.2 box\.example\.com$/m);
|
||||
assert.deepEqual(hups(box), ['101 102']);
|
||||
});
|
||||
|
||||
test('a manual LAN address takes priority over the interface address', () => {
|
||||
const box = sandbox();
|
||||
const result = runLibrary(box, 'sync_lan_hosts', { lanIp: '10.0.0.5' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(read(box.hostsFile), `${HEADER}\n10.0.0.5 box.example.com\n10.0.0.5 ly.example.com\n`);
|
||||
});
|
||||
|
||||
test('without interface status the first network.lan.ipaddr entry is used without its prefix', () => {
|
||||
const box = sandbox();
|
||||
const result = runLibrary(box, 'get_lan_ip', { ifaceIp: '', networkIp: '192.168.9.1/24 192.168.10.1/24' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout.trim(), '192.168.9.1');
|
||||
});
|
||||
|
||||
test('an invalid LAN address writes nothing and removes a stale file', () => {
|
||||
const box = sandbox({ hosts: `${HEADER}\n192.168.2.2 box.example.com\n` });
|
||||
const result = runLibrary(box, 'sync_lan_hosts', { lanIp: '192.168.1.300' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(read(box.hostsFile), null);
|
||||
assert.deepEqual(hups(box), ['101 102']);
|
||||
assert.match(read(box.logFile), /未取得有效的 LAN IPv4 地址/);
|
||||
});
|
||||
|
||||
test('disabling removes the file once and does not reload when nothing was written', () => {
|
||||
const box = sandbox({ hosts: `${HEADER}\n192.168.2.2 box.example.com\n` });
|
||||
let result = runLibrary(box, 'sync_lan_hosts', { lanDns: '0' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(read(box.hostsFile), null);
|
||||
assert.deepEqual(hups(box), ['101 102']);
|
||||
|
||||
result = runLibrary(box, 'sync_lan_hosts', { lanDns: '0' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(hups(box), ['101 102']);
|
||||
});
|
||||
|
||||
test('no generated sites means no hosts file', () => {
|
||||
const box = sandbox({ sites: { '000_default_443_1_0_.conf': 'server {\n server_name _;\n}\n' } });
|
||||
const result = runLibrary(box, 'sync_lan_hosts');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(read(box.hostsFile), null);
|
||||
assert.deepEqual(hups(box), []);
|
||||
});
|
||||
|
||||
test('reload syncs LAN hosts only after nginx accepted the new configuration', () => {
|
||||
const box = sandbox();
|
||||
const body = ok => `
|
||||
PID_FILE=${JSON.stringify(path.join(box.dir, 'missing.pid'))}
|
||||
prepare_config() { return 0; }
|
||||
start_prepared() { return ${ok ? 0 : 1}; }
|
||||
sync_lan_hosts() { echo synced; }
|
||||
do_reload
|
||||
`;
|
||||
let result = runLibrary(box, body(true));
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout.trim(), 'synced');
|
||||
|
||||
result = runLibrary(box, body(false));
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.stdout.trim(), '');
|
||||
});
|
||||
|
||||
test('proxy.sh stop removes the LAN hosts file', () => {
|
||||
const box = sandbox({ hosts: `${HEADER}\n192.168.2.2 box.example.com\n` });
|
||||
const bin = path.join(box.dir, 'bin');
|
||||
fs.mkdirSync(bin);
|
||||
fs.writeFileSync(path.join(bin, 'pidof'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
|
||||
const result = spawnSync('/bin/sh', [script, 'stop'], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${bin}:${process.env.PATH}`,
|
||||
MINIGATE_PID_FILE: path.join(box.dir, 'missing.pid'),
|
||||
MINIGATE_LAN_HOSTS_FILE: box.hostsFile,
|
||||
MINIGATE_LOGFILE: box.logFile
|
||||
}
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(read(box.hostsFile), null);
|
||||
assert.match(read(box.logFile), /局域网直连解析已移除/);
|
||||
});
|
||||
|
||||
test('LuCI exposes the toggle and an optional IPv4 address, disabled by default', () => {
|
||||
const page = fs.readFileSync(path.join(root, 'luasrc/model/cbi/minigate/proxy.lua'), 'utf8');
|
||||
const config = fs.readFileSync(path.join(root, 'root/etc/config/minigate'), 'utf8');
|
||||
assert.match(page, /s:option\(Flag, "lan_dns", "启用局域网直连解析"\)\no\.default = "0"/);
|
||||
assert.match(page, /s:option\(Value, "lan_ip",[^\n]*\)\no\.datatype = "ip4addr"/);
|
||||
assert.match(page, /o:depends\("lan_dns", "1"\)/);
|
||||
assert.match(config, /option lan_dns '0'/);
|
||||
});
|
||||
@@ -36,6 +36,8 @@ function fixture() {
|
||||
write(dir, 'tmp/minigate-update-state.json', '{}');
|
||||
write(dir, 'tmp/minigate_proxy_443.tmp');
|
||||
write(dir, 'tmp/luci-indexcache');
|
||||
write(dir, 'tmp/hosts/minigate', '192.168.1.1 app.example.com\n');
|
||||
write(dir, 'tmp/hosts/dhcp.cfg01411c', '192.168.1.1 router\n');
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -67,9 +69,11 @@ test('default uninstall removes the app but preserves configuration, certificate
|
||||
'var/run/minigate-nginx.pid',
|
||||
'tmp/minigate-update-state.json',
|
||||
'tmp/minigate_proxy_443.tmp',
|
||||
'tmp/luci-indexcache'
|
||||
'tmp/luci-indexcache',
|
||||
'tmp/hosts/minigate'
|
||||
]) assert.equal(fs.existsSync(path.join(dir, relative)), false, relative);
|
||||
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'tmp/hosts/dhcp.cfg01411c'), 'utf8'), '192.168.1.1 router\n');
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'etc/config/minigate'), 'utf8'), 'config-data');
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'etc/minigate/certs/site.pem'), 'utf8'), 'certificate-data');
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'etc/minigate/login-guard/bans.txt'), 'utf8'), '198.51.100.4 9999999999');
|
||||
|
||||
Reference in new issue
Block a user