/** * 分享功能边界条件深度测试(兼容 Cookie + CSRF) * * 测试场景: * 1. 已过期的分享 * 2. 分享文件不存在 * 3. 被封禁用户的分享 * 4. 路径遍历攻击 * 5. 特殊字符路径 * 6. 并发密码尝试 * 7. 分享统计 * 8. 分享码唯一性 * 9. 过期时间格式 */ const http = require('http'); const https = require('https'); const bcrypt = require('bcryptjs'); const { db, ShareDB, UserDB } = require('./database'); const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001'; const results = { passed: 0, failed: 0, errors: [] }; const adminSession = { cookies: {}, csrfToken: '' }; let adminUserId = 1; function makeCookieHeader(cookies) { return Object.entries(cookies || {}) .map(([k, v]) => `${k}=${v}`) .join('; '); } function storeSetCookies(session, setCookieHeader) { if (!session || !setCookieHeader) return; const list = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader]; for (const raw of list) { const first = String(raw || '').split(';')[0]; const idx = first.indexOf('='); if (idx <= 0) continue; const key = first.slice(0, idx).trim(); const value = first.slice(idx + 1).trim(); session.cookies[key] = value; if (key === 'csrf_token') { session.csrfToken = value; } } } function isSafeMethod(method) { const upper = String(method || '').toUpperCase(); return upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS'; } function request(method, path, options = {}) { const { data = null, session = null, headers = {}, requireCsrf = true } = options; return new Promise((resolve, reject) => { const url = new URL(path, BASE_URL); const transport = url.protocol === 'https:' ? https : http; const requestHeaders = { ...headers }; if (session) { const cookieHeader = makeCookieHeader(session.cookies); if (cookieHeader) { requestHeaders.Cookie = cookieHeader; } if (requireCsrf && !isSafeMethod(method)) { const csrfToken = session.csrfToken || session.cookies.csrf_token; if (csrfToken) { requestHeaders['X-CSRF-Token'] = csrfToken; } } } let payload = null; if (data !== null && data !== undefined) { payload = JSON.stringify(data); requestHeaders['Content-Type'] = 'application/json'; requestHeaders['Content-Length'] = Buffer.byteLength(payload); } const req = transport.request({ protocol: url.protocol, hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method, headers: requestHeaders }, (res) => { let body = ''; res.on('data', chunk => { body += chunk; }); res.on('end', () => { if (session) { storeSetCookies(session, res.headers['set-cookie']); } let parsed = body; try { parsed = body ? JSON.parse(body) : {}; } catch (e) { // keep raw text } resolve({ status: res.statusCode, data: parsed, headers: res.headers }); }); }); req.on('error', reject); if (payload) { req.write(payload); } req.end(); }); } async function initCsrf(session) { const res = await request('GET', '/api/csrf-token', { session, requireCsrf: false }); if (res.status === 200 && res.data && res.data.csrfToken) { session.csrfToken = res.data.csrfToken; } return res; } function assert(condition, message) { if (condition) { results.passed++; console.log(` [PASS] ${message}`); } else { results.failed++; results.errors.push(message); console.log(` [FAIL] ${message}`); } } function createValidShareCode(prefix = '') { for (let i = 0; i < 20; i++) { const generated = ShareDB.generateShareCode(); const code = (prefix + generated).replace(/[^A-Za-z0-9]/g, '').slice(0, 16); if (!code || code.length < 6) continue; const exists = db.prepare('SELECT 1 FROM shares WHERE share_code = ?').get(code); if (!exists) return code; } return ShareDB.generateShareCode(); } async function testExpiredShare() { console.log('\n[测试] 已过期的分享...'); const expiredShareCode = createValidShareCode('E'); try { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const expiresAt = yesterday.toISOString().replace('T', ' ').substring(0, 19); db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, expires_at) VALUES (?, ?, ?, ?, ?, ?) `).run(adminUserId, expiredShareCode, '/expired-test.txt', 'file', 'local', expiresAt); console.log(` 创建过期分享: ${expiredShareCode}, 过期时间: ${expiresAt}`); const res = await request('POST', `/api/share/${expiredShareCode}/verify`, { data: {}, requireCsrf: false }); assert(res.status === 404, `过期分享应返回 404, 实际: ${res.status}`); assert(res.data && res.data.message === '分享不存在', '应提示分享不存在'); db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); results.failed++; db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode); return false; } } async function testShareWithDeletedFile() { console.log('\n[测试] 分享的文件不存在...'); const shareCode = createValidShareCode('N'); try { db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type) VALUES (?, ?, ?, ?, ?) `).run(adminUserId, shareCode, '/non_existent_file_xyz.txt', 'file', 'local'); console.log(` 创建分享: ${shareCode}, 路径: /non_existent_file_xyz.txt`); const res = await request('POST', `/api/share/${shareCode}/verify`, { data: {}, requireCsrf: false }); assert(res.status === 500 || res.status === 200, `应返回500或200,实际: ${res.status}`); if (res.status === 500) { assert(!!res.data?.message, '500时应返回错误消息'); } db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testShareByBannedUser() { console.log('\n[测试] 被封禁用户的分享...'); let testUserId = null; const shareCode = createValidShareCode('B'); try { testUserId = UserDB.create({ username: `test_banned_${Date.now()}`, email: `test_banned_${Date.now()}@test.com`, password: 'test123', is_verified: 1 }); db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type) VALUES (?, ?, ?, ?, ?) `).run(testUserId, shareCode, '/test.txt', 'file', 'local'); console.log(` 创建测试用户 ID: ${testUserId}`); console.log(` 创建分享: ${shareCode}`); UserDB.setBanStatus(testUserId, true); console.log(` 封禁用户: ${testUserId}`); const res = await request('POST', `/api/share/${shareCode}/verify`, { data: {}, requireCsrf: false }); console.log(` 被封禁用户分享访问状态码: ${res.status}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); UserDB.delete(testUserId); assert(true, '被封禁用户分享测试完成'); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); if (shareCode) db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); if (testUserId) UserDB.delete(testUserId); return false; } } async function testPathTraversalAttacks() { console.log('\n[测试] 路径遍历攻击防护...'); const shareCode = createValidShareCode('T'); try { db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type) VALUES (?, ?, ?, ?, ?) `).run(adminUserId, shareCode, '/allowed-folder', 'directory', 'local'); const attackPaths = [ '../../../etc/passwd', '..\\..\\..\\etc\\passwd', '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd', '/allowed-folder/../../../etc/passwd', '/allowed-folder/./../../etc/passwd', '....//....//....//etc/passwd', '/allowed-folder%00.txt/../../../etc/passwd' ]; let blocked = 0; for (const attackPath of attackPaths) { const res = await request('POST', `/api/share/${shareCode}/download-url`, { data: { path: attackPath }, requireCsrf: false }); if (res.status === 403 || res.status === 400) { blocked++; console.log(` [BLOCKED] ${attackPath.substring(0, 40)}...`); } else { console.log(` [WARN] 可能未阻止: ${attackPath}, 状态: ${res.status}`); } } assert(blocked >= attackPaths.length - 1, `路径遍历攻击应被阻止 (${blocked}/${attackPaths.length})`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testSpecialCharactersInPath() { console.log('\n[测试] 特殊字符路径处理...'); if (!adminSession.cookies.token) { console.log(' [WARN] 未登录,跳过特殊字符路径测试'); assert(true, '未登录时跳过特殊字符路径测试'); return true; } const specialPaths = [ '/文件夹/中文文件.txt', '/folder with spaces/file.txt', '/folder-with-dashes/file_underscore.txt', '/folder.with.dots/file.name.ext.txt', "/folder'with'quotes/file.txt" ]; let handled = 0; for (const virtualPath of specialPaths) { try { const createRes = await request('POST', '/api/share/create', { data: { share_type: 'file', file_path: virtualPath }, session: adminSession }); if (createRes.status === 200 || createRes.status === 400) { handled++; console.log(` [OK] ${virtualPath.substring(0, 30)}... - 状态: ${createRes.status}`); if (createRes.status === 200 && createRes.data?.share_code) { const mySharesRes = await request('GET', '/api/share/my', { session: adminSession }); const share = mySharesRes.data?.shares?.find(s => s.share_code === createRes.data.share_code); if (share) { await request('DELETE', `/api/share/${share.id}`, { session: adminSession }); } } } } catch (error) { console.log(` [ERROR] ${virtualPath}: ${error.message}`); } } assert(handled === specialPaths.length, '特殊字符路径处理完成'); return true; } async function testConcurrentPasswordAttempts() { console.log('\n[测试] 并发密码尝试限流...'); const shareCode = createValidShareCode('C'); try { const hashedPassword = bcrypt.hashSync('correct123', 10); db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, storage_type) VALUES (?, ?, ?, ?, ?, ?) `).run(adminUserId, shareCode, '/test.txt', 'file', hashedPassword, 'local'); const promises = []; for (let i = 0; i < 20; i++) { promises.push( request('POST', `/api/share/${shareCode}/verify`, { data: { password: `wrong${i}` }, requireCsrf: false }) ); } const responses = await Promise.all(promises); const rateLimited = responses.filter(r => r.status === 429).length; const unauthorized = responses.filter(r => r.status === 401).length; console.log(` 并发请求: 20, 限流: ${rateLimited}, 401错误: ${unauthorized}`); if (rateLimited > 0) { assert(true, '限流机制生效'); } else { console.log(' [INFO] 限流未触发(可能配置较宽松)'); assert(true, '并发测试完成'); } db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testShareStatistics() { console.log('\n[测试] 分享统计功能...'); const shareCode = createValidShareCode('S'); try { db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, view_count, download_count) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(adminUserId, shareCode, '/test.txt', 'file', 'local', 0, 0); for (let i = 0; i < 3; i++) { await request('POST', `/api/share/${shareCode}/verify`, { data: {}, requireCsrf: false }); } for (let i = 0; i < 2; i++) { await request('POST', `/api/share/${shareCode}/download`, { data: {}, requireCsrf: false }); } const share = db.prepare('SELECT view_count, download_count FROM shares WHERE share_code = ?').get(shareCode); assert(share.view_count === 3, `查看次数应为 3, 实际: ${share.view_count}`); assert(share.download_count === 2, `下载次数应为 2, 实际: ${share.download_count}`); console.log(` 查看次数: ${share.view_count}, 下载次数: ${share.download_count}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testShareCodeUniqueness() { console.log('\n[测试] 分享码唯一性...'); try { const codes = new Set(); for (let i = 0; i < 10; i++) { const code = ShareDB.generateShareCode(); if (codes.has(code)) { console.log(` [WARN] 发现重复分享码: ${code}`); } codes.add(code); } assert(codes.size === 10, `应生成 10 个唯一分享码, 实际: ${codes.size}`); console.log(` 生成了 ${codes.size} 个唯一分享码`); const sampleCode = ShareDB.generateShareCode(); assert(sampleCode.length === 8, `分享码长度应为 8, 实际: ${sampleCode.length}`); assert(/^[a-zA-Z0-9]+$/.test(sampleCode), '分享码应只包含字母数字'); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); return false; } } async function testExpiryTimeFormat() { console.log('\n[测试] 过期时间格式...'); try { const testDays = [1, 7, 30, 365]; for (const days of testDays) { const result = ShareDB.create(adminUserId, { share_type: 'file', file_path: `/test_${days}_days.txt`, expiry_days: days }); const share = db.prepare('SELECT expires_at FROM shares WHERE share_code = ?').get(result.share_code); const expiresAt = new Date(share.expires_at); const now = new Date(); const diffDays = Math.round((expiresAt - now) / (1000 * 60 * 60 * 24)); assert(Math.abs(diffDays - days) <= 1, `${days}天过期应正确设置, 实际差异: ${diffDays}天`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(result.share_code); } return true; } catch (error) { console.log(` [ERROR] ${error.message}`); return false; } } async function login() { console.log('\n[准备] 登录获取认证...'); try { await initCsrf(adminSession); const res = await request('POST', '/api/login', { data: { username: 'admin', password: 'admin123' }, session: adminSession, requireCsrf: false }); if (res.status === 200 && res.data?.success && adminSession.cookies.token) { await initCsrf(adminSession); const profileRes = await request('GET', '/api/user/profile', { session: adminSession }); if (profileRes.status === 200 && profileRes.data?.user?.id) { adminUserId = profileRes.data.user.id; } console.log(' 认证成功'); return true; } console.log(` 认证失败: status=${res.status}, message=${res.data?.message || 'unknown'}`); return false; } catch (error) { console.log(` [ERROR] ${error.message}`); return false; } } async function runTests() { console.log('========================================'); console.log(' 分享功能边界条件深度测试(Cookie + CSRF)'); console.log('========================================'); const loggedIn = await login(); if (!loggedIn) { console.log('\n[WARN] 登录失败,部分测试可能无法执行'); } await testExpiredShare(); await testShareWithDeletedFile(); await testShareByBannedUser(); await testPathTraversalAttacks(); await testSpecialCharactersInPath(); await testConcurrentPasswordAttempts(); await testShareStatistics(); await testShareCodeUniqueness(); await testExpiryTimeFormat(); console.log('\n========================================'); console.log(' 测试结果统计'); console.log('========================================'); console.log(`通过: ${results.passed}`); console.log(`失败: ${results.failed}`); if (results.errors.length > 0) { console.log('\n失败的测试:'); results.errors.forEach((err, i) => { console.log(` ${i + 1}. ${err}`); }); } console.log('\n========================================'); process.exit(results.failed > 0 ? 1 : 0); } runTests().catch(error => { console.error('测试执行失败:', error); process.exit(1); });