Files
blackbird/fsys/public/index.html
T

191 lines
9.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>fsys 测试页</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f1419; color: #e6e6e6; padding: 32px 16px; }
.wrap { max-width: 860px; margin: 0 auto; }
h1 { font-size: 22px; margin-bottom: 4px; }
.sub { color: #8b98a5; font-size: 13px; margin-bottom: 24px; }
.card { background: #1a2027; border: 1px solid #2a3441; border-radius: 10px; padding: 16px; margin-bottom: 14px; }
.card h2 { font-size: 14px; color: #9fb3c8; margin-bottom: 10px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
td { padding: 7px 8px; border-bottom: 1px solid #232d38; vertical-align: top; }
td.name { width: 34%; color: #d7e2ec; }
td.st { width: 90px; font-weight: 600; }
.ok { color: #4ade80; } .fail { color: #f87171; } .pending { color: #8b98a5; }
td.detail { color: #8b98a5; font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
button { background: #2563eb; color: #fff; border: 0; border-radius: 8px; padding: 10px 22px;
font-size: 14px; cursor: pointer; margin-bottom: 18px; }
button:hover { background: #1d4ed8; }
.links a { color: #60a5fa; text-decoration: none; margin-right: 14px; font-size: 13px; }
.links a:hover { text-decoration: underline; }
#summary { font-size: 14px; margin-bottom: 14px; }
</style>
</head>
<body>
<div class="wrap">
<h1>fsys 测试页</h1>
<div class="sub">C++ 静态文件托管服务 · 路由/安全功能自检</div>
<button onclick="runAll()">▶ 运行全部测试</button>
<div id="summary"></div>
<div class="card">
<h2>API 自检</h2>
<table id="tbl">
<tr data-t="health"><td class="name">GET /health</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="filelist_root"><td class="name">GET /api/filelist(根目录)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="filelist_sub"><td class="name">GET /api/filelist?path=test%2F(编码子目录)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="filelist_traverse"><td class="name">GET /api/filelist?path=..%2F..(遍历应 403)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="static_file"><td class="name">GET /test/hello.txt(静态文件)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="download"><td class="name">GET /test/hello.txt?download=true(附件头)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="utf8_path"><td class="name">GET 中文文件名(URL 解码)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="hash"><td class="name">POST /api/hash(sha256("test123"))</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="hash_bad"><td class="name">POST /api/hash 非法输入(应 400)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="check_user"><td class="name">POST /api/check_user(结构校验)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="favicon_private"><td class="name">GET /api/favicon 内网地址(SSRF 应 403)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="favicon_public"><td class="name">GET /api/favicon 公网地址(需外网)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="headers"><td class="name">安全响应头(CSP/X-Frame-Options…)</td><td class="st pending">—</td><td class="detail"></td></tr>
<tr data-t="notfound"><td class="name">GET /不存在的路径(应 404)</td><td class="st pending">—</td><td class="detail"></td></tr>
</table>
</div>
<div class="card links">
<h2>手动验证链接</h2>
<a href="/api/filelist?path=" target="_blank">filelist 根</a>
<a href="/api/filelist?path=test" target="_blank">filelist test/</a>
<a href="/test/hello.txt" target="_blank">hello.txt</a>
<a href="/test/hello.txt?download=true" target="_blank">下载模式</a>
<a href="/health" target="_blank">health</a>
</div>
</div>
<script>
const EXPECT_HASH = 'ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae';
function setRow(key, ok, detail) {
const tr = document.querySelector(`tr[data-t="${key}"]`);
if (!tr) return;
const st = tr.children[1], dt = tr.children[2];
st.textContent = ok ? 'PASS' : 'FAIL';
st.className = 'st ' + (ok ? 'ok' : 'fail');
dt.textContent = detail || '';
}
async function j(url, opt) {
const res = await fetch(url, opt);
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch (e) {}
return { res, text, json };
}
const tests = {
async health() {
const { res, json } = await j('/health');
const ok = res.status === 200 && json && json.status === 'ok';
setRow('health', ok, `HTTP ${res.status} ${JSON.stringify(json)}`);
},
async filelist_root() {
const { res, json } = await j('/api/filelist?path=');
const ok = res.status === 200 && json && json.success === true && Array.isArray(json.items);
setRow('filelist_root', ok, `HTTP ${res.status}, ${json && json.items ? json.items.length + ' 项' : '无 items'}`);
},
async filelist_sub() {
const { res, json } = await j('/api/filelist?path=' + encodeURIComponent('test'));
const hasHello = json && Array.isArray(json.items) && json.items.some(i => i.name === 'hello.txt');
setRow('filelist_sub', res.status === 200 && hasHello, `HTTP ${res.status}, hello.txt ${hasHello ? '可见' : '缺失'}`);
},
async filelist_traverse() {
const { res } = await j('/api/filelist?path=' + encodeURIComponent('../../'));
setRow('filelist_traverse', res.status === 403, `HTTP ${res.status}(期望 403)`);
},
async static_file() {
const { res, text } = await j('/test/hello.txt');
const ok = res.status === 200 && text.includes('hello fsys');
setRow('static_file', ok, `HTTP ${res.status}, ${(res.headers.get('content-type') || '').split(';')[0]}`);
},
async download() {
const { res } = await j('/test/hello.txt?download=true');
const cd = res.headers.get('content-disposition') || '';
setRow('download', res.status === 200 && cd.includes('attachment'), `content-disposition: ${cd || '(无)'}`);
},
async utf8_path() {
const { res, text } = await j('/test/' + encodeURIComponent('中文文件.txt'));
const ok = res.status === 200 && text.includes('中文内容');
setRow('utf8_path', ok, `HTTP ${res.status}(URL 解码生效=${ok})`);
},
async hash() {
const { res, json } = await j('/api/hash', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: 'test123' })
});
const ok = res.status === 200 && json && json.hash === EXPECT_HASH;
setRow('hash', ok, `HTTP ${res.status}, hash ${ok ? '匹配' : '不匹配'}`);
},
async hash_bad() {
const { res } = await j('/api/hash', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: 12345 })
});
setRow('hash_bad', res.status === 400, `HTTP ${res.status}(期望 400)`);
},
async check_user() {
const { res, json } = await j('/api/check_user', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'fsys-test@example.com' })
});
// db 可能不可用:只要返回结构合法(success 字段存在)即 PASS
const ok = [200, 500].includes(res.status) && json && typeof json.success === 'boolean';
setRow('check_user', ok, `HTTP ${res.status} ${JSON.stringify(json)}`);
},
async favicon_private() {
const { res } = await j('/api/favicon?url=' + encodeURIComponent('http://127.0.0.1:8005/'));
setRow('favicon_private', res.status === 403, `HTTP ${res.status}(期望 403 forbidden host)`);
},
async favicon_public() {
try {
const { res } = await j('/api/favicon?url=' + encodeURIComponent('https://www.baidu.com'));
const ct = res.headers.get('content-type') || '';
setRow('favicon_public', res.status === 200 && ct.startsWith('image/'), `HTTP ${res.status} ${ct}`);
} catch (e) {
setRow('favicon_public', false, '网络不可达: ' + e.message);
}
},
async headers() {
const res = await fetch('/health');
const need = ['content-security-policy', 'x-frame-options', 'x-content-type-options',
'referrer-policy', 'permissions-policy'];
const missing = need.filter(h => !res.headers.get(h));
setRow('headers', missing.length === 0, missing.length ? '缺失: ' + missing.join(', ') : '5 项安全头齐全');
},
async notfound() {
const { res } = await j('/no/such/file-' + Date.now());
setRow('notfound', res.status === 404, `HTTP ${res.status}(期望 404)`);
}
};
async function runAll() {
const summary = document.getElementById('summary');
summary.textContent = '测试运行中…';
document.querySelectorAll('#tbl tr').forEach(tr => {
tr.children[1].textContent = '…'; tr.children[1].className = 'st pending';
tr.children[2].textContent = '';
});
let pass = 0, fail = 0;
for (const [name, fn] of Object.entries(tests)) {
try { await fn(); }
catch (e) { setRow(name, false, '异常: ' + e.message); }
const st = document.querySelector(`tr[data-t="${name}"] .st`);
if (st && st.classList.contains('ok')) pass++; else fail++;
}
summary.innerHTML = `完成:<span class="ok">${pass} 通过</span> / <span class="fail">${fail} 失败</span>(共 ${pass + fail})`;
}
</script>
</body>
</html>