mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 18:33:56 +08:00
⛄ Sync 2026-10-25 10:56:54
This commit is contained in:
1 parent
37e1475e87
commit
816dd89fe9
68 files changed
+55036
-377
No files matched your search
@@ -0,0 +1,97 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const http = require('node:http');
|
||||
const net = require('node:net');
|
||||
const { spawn, spawnSync } = require('node:child_process');
|
||||
const { config, geoDir } = require('./helpers/access-fixture.cjs');
|
||||
|
||||
const nginx = process.env.MINIGATE_TEST_NGINX;
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
function request(socketPath, source, { spoof = false, websocket = false } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = '';
|
||||
const socket = net.connect(socketPath, () => {
|
||||
const v6 = source.includes(':');
|
||||
socket.write(`PROXY ${v6 ? 'TCP6' : 'TCP4'} ${source} ${v6 ? '::1' : '127.0.0.1'} 12345 2000\r\n` +
|
||||
'GET / HTTP/1.1\r\nHost: app.example.com\r\n' +
|
||||
(spoof ? 'X-Forwarded-For: 14.196.0.1\r\nX-Real-IP: 14.196.0.1\r\n' : '') +
|
||||
(websocket ? 'Connection: Upgrade\r\nUpgrade: websocket\r\n' : 'Connection: close\r\n') + '\r\n');
|
||||
});
|
||||
socket.setTimeout(3000, () => socket.destroy(new Error('请求超时')));
|
||||
socket.on('data', chunk => { response += chunk; });
|
||||
socket.on('error', error => error.code === 'ECONNRESET' ? resolve(response) : reject(error));
|
||||
socket.on('close', () => resolve(response));
|
||||
});
|
||||
}
|
||||
|
||||
async function running(t, options = {}) {
|
||||
let reached = 0;
|
||||
const backend = http.createServer((req, res) => { reached++; res.end('backend-reached'); });
|
||||
backend.on('upgrade', (req, socket) => { reached++; socket.end('HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n'); });
|
||||
await new Promise(resolve => backend.listen(0, '127.0.0.1', resolve));
|
||||
t.after(() => new Promise(resolve => backend.close(resolve)));
|
||||
const port = backend.address().port;
|
||||
const fixture = config(`
|
||||
write_server "$SITES_DIR/site.conf" app.example.com 2000 127.0.0.1 ${port} 0 0 1 old 0 0
|
||||
write_redirect_server "$SITES_DIR/redirect.conf" app.example.com 2000
|
||||
generate_main_conf
|
||||
`, options);
|
||||
assert.equal(fixture.status, 0, fixture.stderr);
|
||||
const site = path.join(fixture.dir, 'sites/site.conf');
|
||||
const socketPath = path.join(fixture.dir, 'proxy.sock');
|
||||
fs.writeFileSync(site, fs.readFileSync(site, 'utf8').replace('listen 2000;', `listen unix:${socketPath} proxy_protocol;`));
|
||||
const main = path.join(fixture.dir, 'main.conf');
|
||||
fs.mkdirSync(path.join(fixture.dir, 'logs'));
|
||||
const tempPaths = ['client_body', 'proxy', 'fastcgi', 'uwsgi', 'scgi']
|
||||
.map(name => ` ${name}_temp_path ${fixture.dir}/${name}-temp;`).join('\n');
|
||||
fs.writeFileSync(main, fs.readFileSync(main, 'utf8').replace('http {', `http {\n${tempPaths}`));
|
||||
const check = spawnSync(nginx, ['-t', '-c', main, '-p', fixture.dir], { encoding: 'utf8' });
|
||||
assert.equal(check.status, 0, check.stderr);
|
||||
const child = spawn(nginx, ['-c', main, '-p', fixture.dir, '-g', 'daemon off;'], { stdio: 'ignore' });
|
||||
t.after(async () => {
|
||||
if (child.exitCode === null) {
|
||||
child.kill('SIGQUIT');
|
||||
await new Promise(resolve => child.once('exit', resolve));
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < 50 && !fs.existsSync(socketPath); i++) await pause(50);
|
||||
assert.ok(fs.existsSync(socketPath), fs.existsSync(path.join(fixture.dir, 'error.log')) ? fs.readFileSync(path.join(fixture.dir, 'error.log'), 'utf8') : 'Nginx 未启动');
|
||||
return { ...fixture, socketPath, redirectPath: path.join(fixture.dir, 'http_2000.sock'), reached: () => reached };
|
||||
}
|
||||
|
||||
test('real nginx enforces source policy before upstream, redirect and websocket', { skip: !nginx }, async t => {
|
||||
const fixture = await running(t);
|
||||
for (const ip of ['14.196.0.1', '::ffff:14.196.0.1', '2001:250:2000::1', '192.168.2.15', '10.0.0.2', 'fd00::15', 'fe80::15']) {
|
||||
const response = await request(fixture.socketPath, ip);
|
||||
assert.match(response, /200 OK/, ip);
|
||||
assert.match(response, /backend-reached/, ip);
|
||||
}
|
||||
const reached = fixture.reached();
|
||||
for (const ip of ['8.8.8.8', '::ffff:8.8.8.8', '1.0.1.1', '203.0.113.10', '2001:4860:4860::8888', '2001:db8::1']) {
|
||||
assert.equal(await request(fixture.socketPath, ip, { spoof: true }), '', ip);
|
||||
assert.equal(await request(fixture.redirectPath, ip), '', `redirect ${ip}`);
|
||||
assert.equal(await request(fixture.socketPath, ip, { websocket: true }), '', `websocket ${ip}`);
|
||||
}
|
||||
assert.equal(fixture.reached(), reached, '拒绝的请求不得到达后端');
|
||||
assert.match(await request(fixture.redirectPath, '14.196.0.1'), /308 Permanent Redirect/);
|
||||
assert.match(await request(fixture.socketPath, '14.196.0.1', { websocket: true }), /101 Switching Protocols/);
|
||||
});
|
||||
|
||||
test('real nginx permits all sources when disabled and fails closed if selected data is missing', { skip: !nginx }, async t => {
|
||||
const open = await running(t, { enabled: '0' });
|
||||
assert.match(await request(open.socketPath, '8.8.8.8'), /200 OK/);
|
||||
const missing = await running(t, { directory: '/missing-minigate-geo-data' });
|
||||
assert.equal(await request(missing.socketPath, '14.196.0.1'), '');
|
||||
assert.match(await request(missing.socketPath, '192.168.2.15'), /200 OK/);
|
||||
});
|
||||
|
||||
|
||||
test('real nginx accepts all bundled province includes together', { skip: !nginx }, async t => {
|
||||
const regions = fs.readFileSync(path.join(geoDir, 'regions.txt'), 'utf8').trim().split('\n').map(row => row.split(' ')[0]).join(' ');
|
||||
const fixture = await running(t, { regions });
|
||||
assert.match(await request(fixture.socketPath, '1.0.1.1'), /200 OK/);
|
||||
assert.equal(await request(fixture.socketPath, '8.8.8.8'), '');
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { root, geoDir } = require('./helpers/access-fixture.cjs');
|
||||
const path = require('node:path');
|
||||
|
||||
const lua = process.env.MINIGATE_TEST_LUA;
|
||||
|
||||
test('LuCI access settings offer all provinces, default off, validate multiple choices and reject invalid values', { skip: !lua }, () => {
|
||||
const result = spawnSync(lua, ['-'], { encoding: 'utf8', input: `
|
||||
local options = {}
|
||||
package.preload['luci.sys'] = function() return {} end
|
||||
package.preload['luci.model.uci'] = function()
|
||||
return {cursor=function() return {foreach=function() end, get=function() end} end}
|
||||
end
|
||||
Flag, Value, DynamicList, ListValue, DummyValue = 'Flag', 'Value', 'DynamicList', 'ListValue', 'DummyValue'
|
||||
NamedSection, TypedSection = 'NamedSection', 'TypedSection'
|
||||
function Map()
|
||||
return {section=function()
|
||||
return {option=function(self, kind, name)
|
||||
local option = {kind=kind, choices={}}
|
||||
function option:value(key, label) self.choices[key] = label end
|
||||
function option:depends(key, value) self.dependency={key,value} end
|
||||
options[name] = option
|
||||
return option
|
||||
end}
|
||||
end}
|
||||
end
|
||||
local real_open = io.open
|
||||
io.open = function(file, mode)
|
||||
if file == '/usr/lib/minigate/geo/regions.txt' then file = ${JSON.stringify(path.join(geoDir, 'regions.txt'))} end
|
||||
return real_open(file, mode)
|
||||
end
|
||||
assert(dofile(${JSON.stringify(path.join(root, 'luasrc/model/cbi/minigate/proxy.lua'))}))
|
||||
assert(options.access_enabled.default == '0')
|
||||
local regions = options.access_regions
|
||||
assert(regions.kind == 'DynamicList')
|
||||
assert(regions.default == '510000')
|
||||
assert(regions.dependency[1] == 'access_enabled' and regions.dependency[2] == '1')
|
||||
assert(regions.choices['510000'] == '四川省')
|
||||
local count = 0; for _ in pairs(regions.choices) do count = count + 1 end
|
||||
assert(count == 34)
|
||||
assert(regions:validate({'510000', '500000'}, 'global'))
|
||||
assert(regions:validate('510000', 'global'))
|
||||
assert(not regions:validate({}, 'global'))
|
||||
assert(not regions:validate({''}, 'global'))
|
||||
assert(not regions:validate({'990000'}, 'global'))
|
||||
assert(not regions:validate({'510000', '../500000'}, 'global'))
|
||||
print('LuCI access validation passed')
|
||||
` });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validation passed/);
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { config, geoDir, root } = require('./helpers/access-fixture.cjs');
|
||||
|
||||
test('disabled or absent access option preserves unrestricted generated sites', () => {
|
||||
for (const enabled of ['', '0']) {
|
||||
const result = config('write_geo_access; write_server "$conf" app.example.com 2000 127.0.0.1 80 0 0 1 old 1 0', { enabled });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.doesNotMatch(result.conf, /minigate_geo_allowed/);
|
||||
assert.match(result.conf, /proxy_pass http:\/\/127.0.0.1:80/);
|
||||
}
|
||||
});
|
||||
|
||||
test('province policy defaults to deny, uses connection IP and preserves private LAN', () => {
|
||||
const result = config('write_geo_access');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /geo \$minigate_client_addr \$minigate_geo_allowed/);
|
||||
assert.match(result.stdout, /default 0;/);
|
||||
assert.match(result.stdout, /192\.168\.0\.0\/16 1;/);
|
||||
assert.match(result.stdout, /fc00::\/7 1;/);
|
||||
assert.match(result.stdout, /fe80::\/10 1;/);
|
||||
assert.match(result.stdout, /510000\.conf;/);
|
||||
assert.doesNotMatch(result.stdout, /http_x_|100\.64|0\.0\.0\.0\/0/);
|
||||
});
|
||||
|
||||
test('multiple selected provinces are included once and other provinces stay absent', () => {
|
||||
const result = config('write_geo_access', { regions: '510000 500000 510000' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal((result.stdout.match(/510000\.conf/g) || []).length, 1);
|
||||
assert.match(result.stdout, /500000\.conf/);
|
||||
assert.doesNotMatch(result.stdout, /440000\.conf/);
|
||||
});
|
||||
|
||||
test('empty, invalid or missing region data never falls back to public access', () => {
|
||||
for (const options of [
|
||||
{ regions: '' }, { regions: '../510000; return 200;' }, { regions: '990000' },
|
||||
{ directory: '/missing-minigate-geo-data' }
|
||||
]) {
|
||||
const result = config('write_geo_access', options);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /default 0;/);
|
||||
assert.doesNotMatch(result.stdout, /include .*\.conf|return 200/);
|
||||
}
|
||||
});
|
||||
|
||||
test('both ordinary and multiplexed proxy sites reject before proxy_pass and websocket upgrade', () => {
|
||||
for (const mux of [0, 1]) {
|
||||
const result = config(`write_server "$conf" app.example.com 2000 127.0.0.1 80 0 0 1 old 1 ${mux}`);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.conf, /if \(\$minigate_geo_allowed = 0\) \{ return 444; \}/);
|
||||
assert.ok(result.conf.indexOf('return 444;') < result.conf.indexOf('proxy_pass'));
|
||||
assert.match(result.conf, /proxy_set_header Upgrade/);
|
||||
}
|
||||
});
|
||||
|
||||
test('HTTP redirect rejects unapproved source before sending a redirect', () => {
|
||||
const result = config('write_redirect_server "$conf" app.example.com 2000');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.ok(result.conf.indexOf('return 444;') < result.conf.indexOf('return 308'));
|
||||
});
|
||||
|
||||
test('the legacy scanner cannot add global firewall bans in request access mode', () => {
|
||||
const scanner = path.join(root, 'root/usr/lib/minigate/geofence.sh');
|
||||
const result = spawnSync('/bin/sh', ['-c', `
|
||||
export MINIGATE_GEOFENCE_LIBRARY_ONLY=1
|
||||
. '${scanner}'
|
||||
uci() { [ "$3" = minigate.global.access_enabled ] && echo 1; }
|
||||
init_block_set() { echo unexpected >&2; return 1; }
|
||||
scan_and_block
|
||||
`], { encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stderr, '');
|
||||
});
|
||||
|
||||
test('bundled regions all cover IPv4 and IPv6 and data follows every install path', () => {
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(geoDir, 'metadata.json')));
|
||||
const regions = fs.readFileSync(path.join(geoDir, 'regions.txt'), 'utf8').trim().split('\n');
|
||||
assert.equal(regions.length, 34);
|
||||
for (const row of regions) {
|
||||
const code = row.split(' ')[0];
|
||||
const data = fs.readFileSync(path.join(geoDir, `${code}.conf`), 'utf8');
|
||||
assert.match(data, /^\d+\.\d+\.\d+\.\d+\/\d+ 1;$/m, code);
|
||||
assert.match(data, /^[a-f0-9:]+\/\d+ 1;$/m, code);
|
||||
assert.equal(data.trim().split('\n').length - 1, metadata.networks[code]['4'] + metadata.networks[code]['6']);
|
||||
}
|
||||
for (const file of ['Makefile', 'install.sh', 'scripts/build-ipk.sh']) {
|
||||
assert.match(fs.readFileSync(path.join(root, file), 'utf8'), /root\/usr\/lib\/minigate\/geo/, file);
|
||||
}
|
||||
assert.match(fs.readFileSync(path.join(geoDir, 'LICENSE.ip2region'), 'utf8'), /Copyright.*Lionsoul/);
|
||||
});
|
||||
|
||||
|
||||
test('assigned LAN global IPv6 subnet is exempted without allowing a broad remote prefix', () => {
|
||||
const run = mask => config(`
|
||||
ubus() { echo fixture; }
|
||||
jsonfilter() {
|
||||
case "$2" in
|
||||
*'[0].address') echo 2001:db8:1234:: ;;
|
||||
*'[0].mask') echo ${mask} ;;
|
||||
esac
|
||||
}
|
||||
write_geo_access
|
||||
`);
|
||||
const assigned = run('64');
|
||||
assert.equal(assigned.status, 0, assigned.stderr);
|
||||
assert.match(assigned.stdout, /2001:db8:1234::\/64 1;/);
|
||||
assert.doesNotMatch(run('0').stdout, /2001:db8:1234/);
|
||||
assert.doesNotMatch(run('32').stdout, /2001:db8:1234/);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
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 geoDir = path.join(root, 'root/usr/lib/minigate/geo');
|
||||
|
||||
function config(body, { enabled = '1', regions = '510000', directory = geoDir } = {}) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mg-access-'));
|
||||
const file = path.join(dir, 'site.conf');
|
||||
fs.writeFileSync(file, '');
|
||||
fs.mkdirSync(path.join(dir, 'sites'));
|
||||
fs.mkdirSync(path.join(dir, 'streams'));
|
||||
fs.writeFileSync(path.join(dir, 'mime.types'), '');
|
||||
const quote = s => `'${s.replaceAll("'", "'\\''")}'`;
|
||||
const command = `
|
||||
export MINIGATE_PROXY_LIBRARY_ONLY=1
|
||||
export MINIGATE_NGINX_CONF=${quote(path.join(dir, 'main.conf'))}
|
||||
export MINIGATE_SITES_DIR=${quote(path.join(dir, 'sites'))}
|
||||
export MINIGATE_STREAMS_DIR=${quote(path.join(dir, 'streams'))}
|
||||
export MINIGATE_MIME_TYPES=${quote(path.join(dir, 'mime.types'))}
|
||||
export MINIGATE_PID_FILE=${quote(path.join(dir, 'nginx.pid'))}
|
||||
export MINIGATE_NGINX_ERROR_LOG=${quote(path.join(dir, 'error.log'))}
|
||||
export MINIGATE_ACCESS_LOG=${quote(path.join(dir, 'access.log'))}
|
||||
export MINIGATE_GEO_DIR=${quote(directory)}
|
||||
export MINIGATE_LOGFILE=${quote(path.join(dir, 'proxy.log'))}
|
||||
export MINIGATE_SOCKET_DIR=${quote(dir)}
|
||||
. ${quote(script)}
|
||||
uci() {
|
||||
case "$3" in
|
||||
minigate.global.access_enabled) printf '%s\\n' ${quote(enabled)} ;;
|
||||
minigate.global.access_regions) printf '%s\\n' ${quote(regions)} ;;
|
||||
esac
|
||||
}
|
||||
conf=${quote(file)}
|
||||
${body}
|
||||
`;
|
||||
const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf8' });
|
||||
return { ...result, conf: fs.readFileSync(file, 'utf8'), dir };
|
||||
}
|
||||
|
||||
module.exports = { config, geoDir, root };
|
||||
Reference in new issue
Block a user