mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 02:13:56 +08:00
🛸 Sync 2026-10-29 02:04:12
This commit is contained in:
1 parent
b21929fceb
commit
bfd56813e0
37 files changed
+3918
-4346
No files matched your search
@@ -178,3 +178,38 @@ test('upgrades stop the old login guard tree before running the previous init sc
|
||||
assert.ok(treeStop > 0 && treeStop < initStop, 'installer must stop the login guard tree before the old init script');
|
||||
assert.match(installer, /stop_process_tree\(\) \{[\s\S]*pgrep -P "\$1"/);
|
||||
});
|
||||
|
||||
// 回归:v2026.10.28 中修复防火墙时读取 bans.txt 的循环变量覆盖了调用方的 ip,
|
||||
// 达到阈值后用空地址封禁,日志出现「封禁 失败」,攻击者再也不会被封。
|
||||
test('reaching the threshold bans the attacking address even when other bans are persisted', () => {
|
||||
for (const setsEmpty of [false, true]) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-threshold-'));
|
||||
const dataDir = path.join(dir, 'data');
|
||||
const calls = path.join(dir, 'nft.log');
|
||||
fs.mkdirSync(dataDir);
|
||||
fs.writeFileSync(path.join(dataDir, 'bans.txt'), '111.170.34.182 9999999999\n185.246.128.170 9999999999\n');
|
||||
const result = spawnSync('/bin/sh', ['-c', `
|
||||
logger() { :; }
|
||||
uci() { case "$3" in minigate.login_guard.threshold) echo 3 ;; *) return 1 ;; esac; }
|
||||
nft() {
|
||||
printf '%s\\n' "$*" >> ${JSON.stringify(calls)}
|
||||
case "$*" in
|
||||
'list set inet fw4 login_banned_v4'|'list set inet fw4 login_banned_v6') [ ${setsEmpty ? 1 : 0} = 1 ] || echo 'elements = { 111.170.34.182 }' ;;
|
||||
'list chain inet fw4 input') echo '@login_banned_v4 @login_banned_v6' ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
export MINIGATE_LG_LIBRARY_ONLY=1 MINIGATE_LG_DATA_DIR=${JSON.stringify(dataDir)}
|
||||
export MINIGATE_LG_RUN_DIR=${JSON.stringify(path.join(dir, 'run'))} MINIGATE_LG_LOG=${JSON.stringify(path.join(dir, 'lg.log'))}
|
||||
. ${JSON.stringify(script)}
|
||||
for i in 1 2 3; do handle_failure 139.162.161.162; done
|
||||
`], { encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const log = fs.readFileSync(path.join(dir, 'lg.log'), 'utf8');
|
||||
assert.match(fs.readFileSync(calls, 'utf8'), /add element inet fw4 login_banned_v4 \{ 139\.162\.161\.162 timeout 43200s \}/, `setsEmpty=${setsEmpty}`);
|
||||
assert.match(log, /BAN 139\.162\.161\.162 12小时(达到 3 次失败)/);
|
||||
assert.doesNotMatch(log, /封禁 +失败/);
|
||||
assert.match(fs.readFileSync(path.join(dataDir, 'bans.txt'), 'utf8'), /^139\.162\.161\.162 \d+$/m);
|
||||
assert.equal(fs.existsSync(path.join(dir, 'run/counters/139.162.161.162')), false, 'counter is cleared after a ban');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
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 lua = process.env.MINIGATE_TEST_LUA;
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const template = path.join(root, 'luasrc/view/minigate/region_select.htm');
|
||||
const proxyPage = path.join(root, 'luasrc/model/cbi/minigate/proxy.lua');
|
||||
const regions = path.join(root, 'root/usr/lib/minigate/geo/regions.txt');
|
||||
|
||||
// 最小的 LuCI 模板解释器:只支持本模板用到的 <% %>、<%= %> 和 <%+ %>(include 输出占位)
|
||||
const renderer = `
|
||||
local function compile(src)
|
||||
local code, pos = {"local _o = {}\\nlocal function _w(v) _o[#_o + 1] = tostring(v) end\\n"}, 1
|
||||
while true do
|
||||
local s, e = src:find("<%", pos, true)
|
||||
local text = src:sub(pos, (s or 0) - 1)
|
||||
if s == nil then text = src:sub(pos) end
|
||||
code[#code + 1] = "_w(" .. string.format("%q", text) .. ")\\n"
|
||||
if not s then break end
|
||||
local close = assert(src:find("%>", e + 1, true), "unclosed tag")
|
||||
local body = src:sub(e + 1, close - 1)
|
||||
local mode = body:sub(1, 1)
|
||||
if mode == "=" then
|
||||
code[#code + 1] = "_w(" .. body:sub(2) .. ")\\n"
|
||||
elseif mode == "+" then
|
||||
code[#code + 1] = "_w('<!--include " .. body:sub(2):gsub("%s", "") .. "-->')\\n"
|
||||
else
|
||||
code[#code + 1] = body .. "\\n"
|
||||
end
|
||||
pos = close + 2
|
||||
end
|
||||
code[#code + 1] = "return table.concat(_o)"
|
||||
return table.concat(code)
|
||||
end
|
||||
|
||||
function render_template(file, scope)
|
||||
local f = assert(io.open(file)); local src = f:read("*a"); f:close()
|
||||
local fn = assert(loadstring(compile(src), file))
|
||||
setfenv(fn, setmetatable(scope, {__index = _G}))
|
||||
return fn()
|
||||
end
|
||||
|
||||
function pcdata(v)
|
||||
return (tostring(v or ""):gsub("[&<>\\"']", {["&"]="&", ["<"]="<", [">"]=">", ['"']=""", ["'"]="'"}))
|
||||
end
|
||||
`;
|
||||
|
||||
// 用 proxy.lua 生成真实的 option(分组、选项、cfgvalue),再渲染模板
|
||||
const pageLoader = `
|
||||
local options, stored = {}, nil
|
||||
local function node()
|
||||
local n = {}
|
||||
function n:option(kind, name)
|
||||
local o = {kind=kind, name=name, keylist={}, vallist={}}
|
||||
function o:value(k, v) self.keylist[#self.keylist + 1] = k; self.vallist[#self.vallist + 1] = v or k end
|
||||
function o:depends() end
|
||||
options[name] = o
|
||||
return o
|
||||
end
|
||||
return n
|
||||
end
|
||||
function Map()
|
||||
local m = node()
|
||||
m.uci = {get=function() return stored end}
|
||||
function m:section() return node() end
|
||||
return m
|
||||
end
|
||||
for _, name in ipairs({'Flag','Value','DynamicList','ListValue','DummyValue','MultiValue','NamedSection','TypedSection','SimpleSection'}) do _G[name] = name end
|
||||
package.preload['luci.model.uci'] = function() return {cursor=function() return {foreach=function() end, get=function() return nil end} end} end
|
||||
package.preload['luci.controller.minigate'] = function() return {ddns_state=function() return {} end} end
|
||||
local real_open = io.open
|
||||
io.open = function(file, mode)
|
||||
if file == '/usr/lib/minigate/geo/regions.txt' then file = ${JSON.stringify(regions)} end
|
||||
return real_open(file, mode)
|
||||
end
|
||||
assert(dofile(${JSON.stringify(proxyPage)}))
|
||||
local o = options.access_regions
|
||||
function render_regions(value)
|
||||
stored = value
|
||||
return render_template(${JSON.stringify(template)}, {self=o, section='global', cbid='cbid.minigate.global.access_regions'})
|
||||
end
|
||||
`;
|
||||
|
||||
function run(body) {
|
||||
const result = spawnSync(lua, ['-'], { encoding: 'utf8', input: renderer + pageLoader + body });
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
test('region selector renders 34 native checkboxes grouped by region with saved choices checked', { skip: !lua }, () => {
|
||||
const html = run(`io.write(render_regions({'510000', '500000'}))`);
|
||||
const boxes = html.match(/<input type="checkbox" class="cbi-input-checkbox" name="cbid\.minigate\.global\.access_regions" value="\d{6}"[^>]*>/g) || [];
|
||||
assert.equal(boxes.length, 34);
|
||||
const checked = boxes.filter(box => box.includes('checked="checked"')).map(box => box.match(/value="(\d+)"/)[1]);
|
||||
assert.deepEqual(checked.sort(), ['500000', '510000']);
|
||||
const groups = [...html.matchAll(/data-group="([^"]+)"/g)].map(match => match[1]);
|
||||
assert.deepEqual(groups, ['华北', '东北', '华东', '华中', '华南', '西南', '西北', '港澳台']);
|
||||
const southwest = html.slice(html.indexOf('data-group="西南"'), html.indexOf('data-group="西北"'));
|
||||
for (const name of ['重庆市', '四川省', '贵州省', '云南省', '西藏']) assert.ok(southwest.includes(`data-name="${name}"`), name);
|
||||
assert.match(html, /^<!--include cbi\/valueheader-->/);
|
||||
assert.match(html, /<!--include cbi\/valuefooter-->\s*$/);
|
||||
assert.match(html, /id="cbid\.minigate\.global\.access_regions\.panel" hidden="hidden"/);
|
||||
});
|
||||
|
||||
test('region selector accepts the old list format and falls back to the default region', { skip: !lua }, () => {
|
||||
const out = run(`
|
||||
local function checked(html) local n = 0; for _ in html:gmatch('checked="checked"') do n = n + 1 end; return n end
|
||||
print(checked(render_regions('510000 500000 440000')), checked(render_regions(nil)), render_regions(nil):match('value="510000" checked') ~= nil)
|
||||
`);
|
||||
assert.equal(out.trim(), '3\t1\ttrue');
|
||||
});
|
||||
|
||||
test('region choices are saved as one string and a hidden field never blocks saving the page', { skip: !lua }, () => {
|
||||
const out = run(`
|
||||
local o = options.access_regions
|
||||
print(o.validate(o, {'510000', '500000'}, 'global'), o.validate(o, '510000', 'global'))
|
||||
print(o.validate(o, {'510000', '990000'}, 'global'), (select(2, o.validate(o, nil, 'global'))))
|
||||
local parsed = 0
|
||||
MultiValue = {parse=function() parsed = parsed + 1 end}
|
||||
local form = {}
|
||||
o.map = {formvalue=function(_, key) return form[key] end}
|
||||
o.parse(o, 'global')
|
||||
form['cbid.minigate.global.access_enabled'] = '1'
|
||||
o.parse(o, 'global')
|
||||
print(parsed)
|
||||
`);
|
||||
assert.equal(out.trim(), '510000 500000\t510000\nnil\t请选择至少一个允许的地区\n1');
|
||||
});
|
||||
|
||||
test('region selector script keeps the page form intact and styles follow the theme', () => {
|
||||
const html = fs.readFileSync(template, 'utf8');
|
||||
const style = fs.readFileSync(path.join(root, 'luasrc/view/minigate/style.htm'), 'utf8');
|
||||
// 芯片的移除按钮、清空按钮都不能提交表单;搜索框回车不提交
|
||||
assert.match(html, /remove\.type='button'/);
|
||||
assert.match(html, /<button type="button" class="cbi-button mgx-region-clear">/);
|
||||
assert.match(html, /search\.onkeydown=function\(e\)\{if\(e\.key==='Enter'\)e\.preventDefault\(\);\}/);
|
||||
assert.match(style, /\.mgx-region-items\{display:grid;grid-template-columns:repeat\(auto-fill,minmax\(9\.5em,1fr\)\)/);
|
||||
assert.match(style, /\.mgx-region \[hidden\]\{display:none!important\}/);
|
||||
});
|
||||
Reference in new issue
Block a user