"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const express_1 = __importDefault(require("express"));
const http = __importStar(require("http"));
const jwt = __importStar(require("jsonwebtoken"));
const browser_proxy_1 = __importStar(require("../routes/browser-proxy"));
const config_1 = require("../config");
// Mint a valid browser-proxy session token using the real jwt secret —
// the test machine has a config.json so getConfig() works.
function mintProxyToken() {
    const config = (0, config_1.getConfig)();
    return jwt.sign({ username: 'test', typ: 'browser-proxy' }, config.jwtSecret, { expiresIn: '1h' });
}
function tok() {
    return `_bp_tok=${encodeURIComponent(mintProxyToken())}`;
}
(0, vitest_1.describe)('parseHostport', () => {
    (0, vitest_1.it)('接受 host:port (http only)', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:8080')).toEqual({ host: '127.0.0.1', port: 8080 });
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('localhost:3000')).toEqual({ host: 'localhost', port: 3000 });
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('192.168.50.247:5173')).toEqual({ host: '192.168.50.247', port: 5173 });
    });
    (0, vitest_1.it)('拒绝 s: 前缀 (v0 仅 http)', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('s:127.0.0.1:8443')).toBeNull();
    });
    (0, vitest_1.it)('拒绝缺端口 / 非法端口 / 黑名单端口', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:0')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:99999')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:22')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:3389')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('127.0.0.1:6379')).toBeNull();
    });
    (0, vitest_1.it)('拒绝非法字符 (IPv6 brackets / 空格 / @ )', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('[::1]:8080')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('user@host:8080')).toBeNull();
        (0, vitest_1.expect)((0, browser_proxy_1.parseHostport)('host with space:8080')).toBeNull();
    });
});
(0, vitest_1.describe)('isAllowedProxyIp', () => {
    (0, vitest_1.it)('IPv4 字面量 RFC1918 / loopback 放行', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('127.0.0.1')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('127.5.5.5')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('0.0.0.0')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('10.0.0.5')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('192.168.1.1')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('172.16.5.5')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('172.31.255.255')).toBe(true);
    });
    (0, vitest_1.it)('IPv4 公网 IP 拒绝', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('1.1.1.1')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('8.8.8.8')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('172.32.0.1')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('172.15.0.1')).toBe(false);
    });
    (0, vitest_1.it)('Cloud metadata IP 拒绝（即便是 link-local 段）', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('169.254.169.254')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('169.254.170.2')).toBe(false);
    });
    (0, vitest_1.it)('IPv4-mapped IPv6 不能绕过', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('::ffff:8.8.8.8')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('::ffff:127.0.0.1')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('::ffff:169.254.169.254')).toBe(false);
    });
    (0, vitest_1.it)('IPv6 ULA / loopback 放行', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('::1')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('fc00::1')).toBe(true);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('fd00::1')).toBe(true);
    });
    (0, vitest_1.it)('Hostname 字符串不当 IP 判断（防 10.evil.com 绕过）', () => {
        // Pure-string hostnames must be rejected here; allowance only happens
        // after DNS resolution in resolveAllowedTarget.
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('10.evil.com')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('192.168.evil.com')).toBe(false);
        (0, vitest_1.expect)((0, browser_proxy_1.isAllowedProxyIp)('fc.evil.com')).toBe(false);
    });
});
(0, vitest_1.describe)('resolveAllowedTarget', () => {
    (0, vitest_1.it)('字面量 IP 放行返回原 IP', async () => {
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('127.0.0.1')).toBe('127.0.0.1');
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('192.168.1.1')).toBe('192.168.1.1');
    });
    (0, vitest_1.it)('字面量公网 IP 返回 null', async () => {
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('1.1.1.1')).toBeNull();
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('8.8.8.8')).toBeNull();
    });
    (0, vitest_1.it)('localhost 直接映射 127.0.0.1', async () => {
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('localhost')).toBe('127.0.0.1');
    });
    (0, vitest_1.it)('cloud metadata hostname 字面拒绝', async () => {
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('metadata.google.internal')).toBeNull();
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('metadata')).toBeNull();
    });
    (0, vitest_1.it)('cloud metadata IP 拒绝', async () => {
        (0, vitest_1.expect)(await (0, browser_proxy_1.resolveAllowedTarget)('169.254.169.254')).toBeNull();
    });
});
(0, vitest_1.describe)('rewriteHtml', () => {
    const prefix = '/api/browser-proxy/127.0.0.1:8080';
    const T = 'TESTTOK';
    (0, vitest_1.it)('重写 src/href/action 的 root-absolute path 并带上 token', () => {
        const input = '<script src="/main.js"></script><a href="/about">x</a><form action="/login">';
        const out = (0, browser_proxy_1.rewriteHtml)(input, prefix, T);
        (0, vitest_1.expect)(out).toContain('src="/api/browser-proxy/127.0.0.1:8080/main.js?_bp_tok=TESTTOK"');
        (0, vitest_1.expect)(out).toContain('href="/api/browser-proxy/127.0.0.1:8080/about?_bp_tok=TESTTOK"');
        (0, vitest_1.expect)(out).toContain('action="/api/browser-proxy/127.0.0.1:8080/login?_bp_tok=TESTTOK"');
    });
    (0, vitest_1.it)('已有 query string 用 & 拼 token', () => {
        const input = '<a href="/foo?x=1">';
        const out = (0, browser_proxy_1.rewriteHtml)(input, prefix, T);
        (0, vitest_1.expect)(out).toContain('href="/api/browser-proxy/127.0.0.1:8080/foo?x=1&_bp_tok=TESTTOK"');
    });
    (0, vitest_1.it)('strip 上游 <base href> 避免覆盖代理路径', () => {
        const input = '<head><base href="/"><base href="https://other.example/"></head><body></body>';
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe('<head></head><body></body>');
    });
    (0, vitest_1.it)('protocol-relative (//) 不改', () => {
        const input = '<img src="//cdn.example.com/x.png">';
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe(input);
    });
    (0, vitest_1.it)('相对路径不改', () => {
        const input = '<img src="foo/bar.png"><a href="../other">x</a>';
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe(input);
    });
    (0, vitest_1.it)('绝对 URL 不改', () => {
        const input = '<a href="https://example.com/path">x</a>';
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe(input);
    });
    (0, vitest_1.it)('单引号 attribute 也支持', () => {
        const input = "<a href='/foo'>";
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toContain("href='/api/browser-proxy/127.0.0.1:8080/foo?_bp_tok=TESTTOK'");
    });
});
(0, vitest_1.describe)('rewriteLocationHeader', () => {
    const parsed = { host: '127.0.0.1', port: 8080 };
    const prefix = '/api/browser-proxy/127.0.0.1:8080';
    const T = 'TESTTOK';
    (0, vitest_1.it)('absolute redirect 同 scheme/host/port → 重写为 proxy URL 并带 token', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('http://127.0.0.1:8080/new-path?x=1', parsed, prefix, T))
            .toBe('/api/browser-proxy/127.0.0.1:8080/new-path?x=1&_bp_tok=TESTTOK');
    });
    (0, vitest_1.it)('absolute redirect 不同 scheme → 原样保留（防协议混淆）', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('https://127.0.0.1:8080/foo', parsed, prefix, T))
            .toBe('https://127.0.0.1:8080/foo');
    });
    (0, vitest_1.it)('absolute redirect 不同 host → 原样保留', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('http://other.example/foo', parsed, prefix, T))
            .toBe('http://other.example/foo');
    });
    (0, vitest_1.it)('root-relative redirect → 加前缀 + token', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('/foo?x=1', parsed, prefix, T))
            .toBe('/api/browser-proxy/127.0.0.1:8080/foo?x=1&_bp_tok=TESTTOK');
    });
    (0, vitest_1.it)('protocol-relative (//) → 不改', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('//cdn.example/x', parsed, prefix, T))
            .toBe('//cdn.example/x');
    });
    (0, vitest_1.it)('纯相对路径 → 不改', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('foo/bar', parsed, prefix, T))
            .toBe('foo/bar');
    });
});
(0, vitest_1.describe)('stripTokenFromSubPath', () => {
    (0, vitest_1.it)('移除 _bp_tok 不留空 query', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?_bp_tok=abc')).toBe('/foo');
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?_bp_tok=abc#hash')).toBe('/foo#hash');
    });
    (0, vitest_1.it)('保留其它 query', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?x=1&_bp_tok=abc&y=2')).toBe('/foo?x=1&y=2');
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?_bp_tok=abc&x=1')).toBe('/foo?x=1');
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?x=1&_bp_tok=abc')).toBe('/foo?x=1');
    });
    (0, vitest_1.it)('没 token 不动', () => {
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo')).toBe('/foo');
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?x=1')).toBe('/foo?x=1');
        (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/')).toBe('/');
    });
});
(0, vitest_1.describe)('e2e: browser-proxy router 跑通本地 dummy server', () => {
    let upstream;
    let upstreamPort = 0;
    let proxy;
    let proxyPort = 0;
    const upstreamSeenUrls = [];
    (0, vitest_1.beforeAll)(async () => {
        upstream = http.createServer((req, res) => {
            upstreamSeenUrls.push(req.url || '');
            if (req.url === '/index.html') {
                res.writeHead(200, {
                    'Content-Type': 'text/html; charset=utf-8',
                    'X-Frame-Options': 'DENY',
                    'Content-Security-Policy': "frame-ancestors 'none'",
                    'Clear-Site-Data': '"cookies"',
                });
                res.end('<!doctype html><base href="/"><body><a href="/about">x</a><script src="/m.js"></script></body>');
            }
            else if (req.url === '/m.js') {
                res.writeHead(200, { 'Content-Type': 'application/javascript' });
                res.end('console.log(1);');
            }
            else if (req.url === '/redir') {
                res.writeHead(302, { 'Location': '/landed' });
                res.end();
            }
            else {
                res.writeHead(404);
                res.end('nope');
            }
        });
        await new Promise((resolve) => upstream.listen(0, '127.0.0.1', resolve));
        upstreamPort = upstream.address().port;
        const app = (0, express_1.default)();
        // Router handles its own auth (cookie-only); no middleware needed.
        app.use('/api/browser-proxy', browser_proxy_1.default);
        proxy = http.createServer(app);
        await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve));
        proxyPort = proxy.address().port;
    });
    (0, vitest_1.afterAll)(async () => {
        await new Promise((resolve) => upstream.close(() => resolve()));
        await new Promise((resolve) => proxy.close(() => resolve()));
    });
    const withTok = (path, init) => {
        const sep = path.includes('?') ? '&' : '?';
        return fetch(`http://127.0.0.1:${proxyPort}${path}${sep}${tok()}`, init);
    };
    (0, vitest_1.it)('无 token → 403 (session required)', async () => {
        const r = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
        (0, vitest_1.expect)(r.status).toBe(403);
    });
    (0, vitest_1.it)('伪造的 token (错 typ 或错 secret) → 403', async () => {
        const config = (0, config_1.getConfig)();
        const wrongTyp = jwt.sign({ username: 'x', typ: 'user' }, config.jwtSecret, { expiresIn: '1h' });
        const r1 = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html?_bp_tok=${encodeURIComponent(wrongTyp)}`);
        (0, vitest_1.expect)(r1.status).toBe(403);
        const wrongSecret = jwt.sign({ username: 'x', typ: 'browser-proxy' }, 'not-the-secret', { expiresIn: '1h' });
        const r2 = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html?_bp_tok=${encodeURIComponent(wrongSecret)}`);
        (0, vitest_1.expect)(r2.status).toBe(403);
    });
    (0, vitest_1.it)('代理 HTML：strip X-Frame / CSP / Clear-Site-Data + rewrite path + 注入 CSP sandbox + 带 token', async () => {
        const r = await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
        (0, vitest_1.expect)(r.status).toBe(200);
        (0, vitest_1.expect)(r.headers.get('x-frame-options')).toBeNull();
        (0, vitest_1.expect)(r.headers.get('content-security-policy')).toContain('sandbox');
        (0, vitest_1.expect)(r.headers.get('content-security-policy')).not.toContain('allow-same-origin');
        (0, vitest_1.expect)(r.headers.get('content-security-policy')).not.toContain('allow-popups-to-escape-sandbox');
        (0, vitest_1.expect)(r.headers.get('clear-site-data')).toBeNull();
        const body = await r.text();
        (0, vitest_1.expect)(body).toMatch(new RegExp(`href="/api/browser-proxy/127\\.0\\.0\\.1:${upstreamPort}/about\\?_bp_tok=`));
        (0, vitest_1.expect)(body).toMatch(new RegExp(`src="/api/browser-proxy/127\\.0\\.0\\.1:${upstreamPort}/m\\.js\\?_bp_tok=`));
        (0, vitest_1.expect)(body).not.toContain('<base');
    });
    (0, vitest_1.it)('代理 JS：原样透传不重写', async () => {
        const r = await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/m.js`);
        (0, vitest_1.expect)(r.status).toBe(200);
        (0, vitest_1.expect)(await r.text()).toBe('console.log(1);');
    });
    (0, vitest_1.it)('代理 redirect：root-relative Location 加前缀 + token', async () => {
        const r = await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/redir`, { redirect: 'manual' });
        (0, vitest_1.expect)(r.status).toBe(302);
        (0, vitest_1.expect)(r.headers.get('location')).toMatch(new RegExp(`^/api/browser-proxy/127\\.0\\.0\\.1:${upstreamPort}/landed\\?_bp_tok=`));
    });
    (0, vitest_1.it)('拒绝公网字面量 IP (403)', async () => {
        const r = await withTok('/api/browser-proxy/1.1.1.1:80/');
        (0, vitest_1.expect)(r.status).toBe(403);
    });
    (0, vitest_1.it)('拒绝 cloud metadata IP (403)', async () => {
        const r = await withTok('/api/browser-proxy/169.254.169.254:80/');
        (0, vitest_1.expect)(r.status).toBe(403);
    });
    (0, vitest_1.it)('拒绝非法 hostport (400)', async () => {
        const r = await withTok('/api/browser-proxy/not-a-host');
        (0, vitest_1.expect)(r.status).toBe(400);
    });
    (0, vitest_1.it)('拒绝黑名单端口 (400)', async () => {
        const r = await withTok('/api/browser-proxy/127.0.0.1:22/');
        (0, vitest_1.expect)(r.status).toBe(400);
    });
    (0, vitest_1.it)('session token 不泄露给上游', async () => {
        upstreamSeenUrls.length = 0;
        await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
        await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/m.js`);
        (0, vitest_1.expect)(upstreamSeenUrls.length).toBeGreaterThan(0);
        for (const u of upstreamSeenUrls) {
            (0, vitest_1.expect)(u).not.toContain('_bp_tok');
        }
    });
    (0, vitest_1.it)('_session 无 Bearer token → 401', async () => {
        const r = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/_session`, { method: 'POST' });
        (0, vitest_1.expect)(r.status).toBe(401);
    });
    (0, vitest_1.it)('_session 非 admin Bearer → 403', async () => {
        const config = (0, config_1.getConfig)();
        const userToken = jwt.sign({ username: 'definitely-not-admin', typ: 'user' }, config.jwtSecret, { expiresIn: '1h' });
        const r = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/_session`, {
            method: 'POST',
            headers: { Authorization: `Bearer ${userToken}` },
        });
        (0, vitest_1.expect)(r.status).toBe(403);
    });
});
//# sourceMappingURL=browser-proxy.test.js.map