mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 07:32:51 +08:00
144 lines
7.4 KiB
Lua
144 lines
7.4 KiB
Lua
-- http_proxy_test.lua — 代理地址校验单元测试
|
||
-- 用法:lua5.1 tests/http_proxy_test.lua
|
||
|
||
package.path = "./root/usr/share/?.lua;" .. package.path
|
||
|
||
local http = require("substore.http")
|
||
|
||
local passed, failed = 0, 0
|
||
local function check(name, cond)
|
||
if cond then passed = passed + 1; print("PASS " .. name)
|
||
else failed = failed + 1; print("FAIL " .. name) end
|
||
end
|
||
|
||
-- ---------- parse_proxy 合法输入 ----------
|
||
check("empty proxy", http.parse_proxy("") == "")
|
||
check("http proxy", http.parse_proxy("http://192.168.5.1:8080") == "http://192.168.5.1:8080")
|
||
check("socks5 proxy", http.parse_proxy("socks5://192.168.5.1:1080") == "socks5://192.168.5.1:1080")
|
||
check("socks5h proxy", http.parse_proxy("socks5h://10.0.0.1:1080") == "socks5h://10.0.0.1:1080")
|
||
check("https proxy", http.parse_proxy("https://proxy.example.com:8443") == "https://proxy.example.com:8443")
|
||
check("auth proxy", http.parse_proxy("http://user:pass@192.168.5.1:8080") == "http://user:pass@192.168.5.1:8080")
|
||
check("trim spaces", http.parse_proxy(" socks5://192.168.5.1:1080 ") == "socks5://192.168.5.1:1080")
|
||
check("strip path", http.parse_proxy("http://192.168.5.1:8080/") == "http://192.168.5.1:8080")
|
||
|
||
-- ---------- parse_proxy 非法输入 ----------
|
||
check("bad scheme", http.parse_proxy("ftp://x:80") == nil)
|
||
check("no scheme", http.parse_proxy("192.168.5.1:1080") == nil)
|
||
check("bad port", http.parse_proxy("http://x:99999") == nil)
|
||
check("port zero", http.parse_proxy("http://x:0") == nil)
|
||
check("injection host", http.parse_proxy("http://192.168.5.1:1080; rm -rf /") == nil)
|
||
check("injection userinfo", http.parse_proxy("http://user;ls@192.168.5.1:1080") == nil)
|
||
local _, e = http.parse_proxy("ftp://x:80")
|
||
check("err is string", type(e) == "string")
|
||
|
||
-- ---------- wget 后端代理能力(§12 禁止 silent fallback) ----------
|
||
-- busybox wget 只能通过 http_proxy/https_proxy 使用 http(s) 代理;
|
||
-- socks* 必须明确报错,而不是丢掉代理直连。
|
||
check("wget env empty proxy", http.wget_proxy_env("") == "")
|
||
check("wget env nil proxy", http.wget_proxy_env(nil) == "")
|
||
local env_http = http.wget_proxy_env("http://192.168.5.1:8080")
|
||
check("wget env http proxy", type(env_http) == "string" and env_http:find("http_proxy=", 1, true) ~= nil)
|
||
local env_https = http.wget_proxy_env("https://proxy.example.com:8443")
|
||
check("wget env https proxy", type(env_https) == "string" and env_https:find("http_proxy=", 1, true) ~= nil)
|
||
|
||
local e5, m5 = http.wget_proxy_env("socks5://192.168.5.1:1080")
|
||
check("wget rejects socks5 (no silent direct)", e5 == nil)
|
||
check("wget socks5 err mentions SOCKS5", type(m5) == "string" and m5:find("SOCKS5", 1, true) ~= nil)
|
||
local e4 = http.wget_proxy_env("socks4://192.168.5.1:1080")
|
||
check("wget rejects socks4", e4 == nil)
|
||
local eh = http.wget_proxy_env("socks5h://192.168.5.1:1080")
|
||
check("wget rejects socks5h", eh == nil)
|
||
|
||
-- ---------- 日志脱敏(§39) ----------
|
||
check("redact proxy strips credentials",
|
||
http.redact_proxy("http://user:pass@192.168.5.1:8080") == "http://192.168.5.1:8080")
|
||
check("redact proxy keeps credentials-free addr",
|
||
http.redact_proxy("socks5://192.168.5.1:1080") == "socks5://192.168.5.1:1080")
|
||
check("redact proxy handles garbage", http.redact_proxy("nonsense") == "***")
|
||
check("redact proxy handles nil", http.redact_proxy(nil) == "***")
|
||
|
||
local scrubbed = http.scrub_credentials("curl: (7) failed http://user:pw@example.com/sub?token=abc")
|
||
check("scrub removes url credentials", scrubbed:find("user:pw", 1, true) == nil)
|
||
check("scrub keeps the rest", scrubbed:find("example.com", 1, true) ~= nil)
|
||
check("scrub leaves clean text alone",
|
||
http.scrub_credentials("HTTP 错误 404") == "HTTP 错误 404")
|
||
|
||
-- ---------- 重定向链复检(§14/§16) ----------
|
||
-- 纯函数,不触网:日志文本按 wget -S 的真实输出格式构造
|
||
local function chain(...)
|
||
local lines = {}
|
||
for _, loc in ipairs({ ... }) do
|
||
lines[#lines + 1] = " HTTP/1.1 302 Found"
|
||
lines[#lines + 1] = " Location: " .. loc
|
||
end
|
||
lines[#lines + 1] = " HTTP/1.1 200 OK"
|
||
return table.concat(lines, "\n")
|
||
end
|
||
|
||
-- 主机一律用**公网 IP 字面量**,不用域名。
|
||
-- check_public 在 DNS 解析失败时是 fail-closed 的(解析不出来即拒绝),
|
||
-- 而 example.com 这类域名在离线/沙箱环境解析不出来 —— 用它会让「公网目标应放行」
|
||
-- 变成「取决于本机有没有 DNS」,测试不可复现。IP 字面量不经过 DNS,结论确定。
|
||
local BASE = "http://1.1.1.1/api/sub"
|
||
check("chain public -> public allowed",
|
||
http.validate_redirect_chain(BASE, chain("http://8.8.8.8/a")) == true)
|
||
check("chain no redirect allowed",
|
||
http.validate_redirect_chain(BASE, " HTTP/1.1 200 OK") == true)
|
||
check("chain relative location allowed",
|
||
http.validate_redirect_chain(BASE, chain("/other/path")) == true)
|
||
|
||
-- 公网 → 内网:必须拒绝
|
||
local ok1, r1 = http.validate_redirect_chain(BASE, chain("http://127.0.0.1:8080/evil"))
|
||
check("chain to 127.0.0.1 rejected", ok1 == false)
|
||
check("chain 127.0.0.1 reason set", type(r1) == "string" and #r1 > 0)
|
||
check("chain to localhost rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://localhost/evil")) == false)
|
||
check("chain to 192.168.1.1 rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://192.168.1.1/evil")) == false)
|
||
check("chain to 10.0.0.1 rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://10.0.0.1/evil")) == false)
|
||
check("chain to 172.16.0.1 rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://172.16.0.1/evil")) == false)
|
||
check("chain to 169.254.169.254 rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://169.254.169.254/latest/meta-data/")) == false)
|
||
check("chain to ipv6 loopback rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://[::1]/evil")) == false)
|
||
check("chain to ipv6 link-local rejected",
|
||
http.validate_redirect_chain(BASE, chain("http://[fe80::1]/evil")) == false)
|
||
-- 协议相对地址(//host/path)主机必须取自 Location,不能被当成同主机路径
|
||
check("chain protocol-relative to private rejected",
|
||
http.validate_redirect_chain(BASE, chain("//127.0.0.1/evil")) == false)
|
||
check("chain protocol-relative to public allowed",
|
||
http.validate_redirect_chain(BASE, chain("//8.8.8.8/a")) == true)
|
||
-- 多跳:只要有一跳不安全就整体拒绝
|
||
check("chain multi-hop second hop private rejected",
|
||
http.validate_redirect_chain(BASE,
|
||
chain("http://8.8.8.8/a", "http://192.168.1.1/b")) == false)
|
||
check("chain multi-hop all public allowed",
|
||
http.validate_redirect_chain(BASE,
|
||
chain("http://8.8.8.8/a", "http://9.9.9.9/b")) == true)
|
||
-- 非法 Location
|
||
check("chain invalid location rejected",
|
||
http.validate_redirect_chain(BASE, chain("ftp://example.com/x")) == false)
|
||
|
||
-- 用 busybox wget 1.37 `-S` 的真实输出(逐字节捕获)锁定日志格式,
|
||
-- 防止上游改变输出格式后校验静默失效
|
||
local REAL_LOG = [[Connecting to 127.0.0.1:18080 (127.0.0.1:18080)
|
||
HTTP/1.1 302 Found
|
||
Server: BaseHTTP/0.6 Python/3.14.4
|
||
Date: Tue, 29 Sep 2026 11:42:19 GMT
|
||
Location: http://127.0.0.1:18081/
|
||
Connecting to 127.0.0.1:18081 (127.0.0.1:18081)
|
||
HTTP/1.1 200 OK
|
||
Server: BaseHTTP/0.6 Python/3.14.4
|
||
Date: Tue, 29 Sep 2026 11:42:19 GMT
|
||
Content-Length: 11
|
||
|
||
saving to '/tmp/probe_out.bin'
|
||
probe_out.bin 100% |********************************| 11 0:00:00 ETA
|
||
]]
|
||
check("real wget -S log: private hop rejected",
|
||
http.validate_redirect_chain(BASE, REAL_LOG) == false)
|
||
|
||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||
os.exit(failed == 0 and 0 or 1) |