/** * 私人剪贴板「限时直通URL」端到端黑盒验证 * 流程:/go 拿 gate → 登录 → 上传测试图片 → 创建直链 → 无鉴权访问 → 模拟过期 → 删除清理验证 * 用法:node dev_test_scripts/debug/debug_private_clipboard_share_link.js */ const http = require('http') const fs = require('fs') const os = require('os') const path = require('path') const BASE = { hostname: 'localhost', port: 8976 } // 1x1 透明 PNG const PNG_1PX = Buffer.from('89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da63fcffff3f0300050201aad0a95e0000000049454e44ae426082', 'hex') const loadEnv = () => { const out = {} try { const text = fs.readFileSync(path.join(os.homedir(), 'Toolbox_local_creds.env.local'), 'utf8') for (const line of text.split(/\r?\n/)) { const t = line.trim() if (!t || t.startsWith('#')) continue const eq = t.indexOf('=') if (eq <= 0) continue out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim() } } catch {} return out } const req = (urlPath, opts = {}) => new Promise((resolve, reject) => { const headers = Object.assign({}, opts.headers || {}) if (opts.cookie) headers.Cookie = opts.cookie if (opts.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json' if (opts.body) headers['Content-Length'] = Buffer.byteLength(opts.body) const r = http.request(Object.assign({}, BASE, { path: urlPath, method: opts.method || 'GET', headers }), (rp) => { const chunks = [] rp.on('data', c => chunks.push(c)) rp.on('end', () => resolve({ status: rp.statusCode, headers: rp.headers, body: Buffer.concat(chunks) })) }) r.on('error', reject) if (opts.body) r.write(opts.body) r.end() }) const mergeCookies = (prev, resp) => { const set = resp.headers['set-cookie'] || [] const map = {} String(prev || '').split(/;\s*/).filter(Boolean).forEach(kv => { const i = kv.indexOf('='); if (i > 0) map[kv.slice(0, i)] = kv.slice(i + 1) }) set.forEach(c => { const kv = c.split(';')[0]; const i = kv.indexOf('='); if (i > 0) map[kv.slice(0, i)] = kv.slice(i + 1) }) return Object.keys(map).map(k => `${k}=${map[k]}`).join('; ') } let passed = 0 let failed = 0 const check = (name, cond, detail) => { if (cond) { passed++; console.log(` [PASS] ${name}`) } else { failed++; console.log(` [FAIL] ${name} ${detail || ''}`) } } ;(async () => { const env = loadEnv() const user = env.PRIVATE_CLIPBOARD_USERNAME || '' const pass = env.PRIVATE_CLIPBOARD_PASSWORD || '' if (!user || !pass) { console.error('[FAIL] env 缺少 PRIVATE_CLIPBOARD_USERNAME / PASSWORD'); process.exit(1) } // 1. /go 拿 gate cookie let ck = '' let r = await req('/go?systemId=Tools-private_clipboard') while (r.status >= 300 && r.status < 400 && r.headers.location) { ck = mergeCookies(ck, r) r = await req(r.headers.location, { cookie: ck }) } ck = mergeCookies(ck, r) check('1 /go gate 链路', /private_clipboard_gate=1/.test(ck), `cookie=${ck}`) // 2. 登录拿业务 cookie const login = await req('/api/private_clipboard/auth/login', { method: 'POST', cookie: ck, body: JSON.stringify({ username: user, password: pass }) }) ck = mergeCookies(ck, login) const loginJson = JSON.parse(login.body.toString('utf8') || '{}') check('2 业务登录', login.status === 200 && loginJson.ok === true, `status=${login.status}`) // 3. 上传测试图片 const up = await req('/api/private_clipboard/upload?filename=share_test.png&mime=image/png', { method: 'POST', cookie: ck, body: PNG_1PX, headers: { 'Content-Type': 'image/png' } }) const upJson = JSON.parse(up.body.toString('utf8') || '{}') check('3 上传测试图片', up.status === 200 && upJson.ok === true && upJson.id, `status=${up.status} body=${up.body.toString().slice(0, 100)}`) const itemId = upJson.id // 4. 创建限时直链 const sh = await req('/api/private_clipboard/share/create', { method: 'POST', cookie: ck, body: JSON.stringify({ id: itemId }) }) const shJson = JSON.parse(sh.body.toString('utf8') || '{}') check('4 创建限时直链', sh.status === 200 && shJson.ok === true && /^https:\/\/traesite\.umersoft\.com:8975\/share\/clip\/[a-f0-9]{32}$/.test(String(shJson.url || '')), `body=${sh.body.toString().slice(0, 160)}`) const token = String(shJson.url || '').split('/').pop() console.log(` 直链过期时间(北京时间): ${shJson.expires_at_text}`) // 5. 无鉴权访问(模拟第三方浏览器,不带任何 cookie) const pub = await req(`/share/clip/${token}`) check('5 无鉴权访问直链', pub.status === 200 && String(pub.headers['content-type'] || '').includes('image/png'), `status=${pub.status} type=${pub.headers['content-type']}`) // 5b. 未登录调用 share/create 必须被拒 const shNoAuth = await req('/api/private_clipboard/share/create', { method: 'POST', body: JSON.stringify({ id: itemId }) }) check('5b 未授权创建直链被拒', shNoAuth.status === 401 || shNoAuth.status === 403, `status=${shNoAuth.status}`) // 6. 模拟过期:把 expires_at 改成过去 const Database = require(path.join(process.cwd(), 'node_modules', 'better-sqlite3')) const db = new Database(path.join(process.cwd(), 'data', 'private_clipboard.db')) db.prepare('UPDATE clipboard_share_links SET expires_at = ? WHERE token = ?').run(Date.now() - 1000, token) db.close() const expired = await req(`/share/clip/${token}`) const expiredHtml = expired.body.toString('utf8') check('6 过期链接返回 410 + 提示文案', expired.status === 410 && expiredHtml.includes('你的链接期限已经大于三天') && expiredHtml.includes('无法访问'), `status=${expired.status}`) // 7. 重新生成(覆盖旧 token),再删除条目,验证 share 链接被清理 const sh2 = await req('/api/private_clipboard/share/create', { method: 'POST', cookie: ck, body: JSON.stringify({ id: itemId }) }) const sh2Json = JSON.parse(sh2.body.toString('utf8') || '{}') const token2 = String(sh2Json.url || '').split('/').pop() const del = await req(`/api/private_clipboard/${itemId}`, { method: 'DELETE', cookie: ck }) const delJson = JSON.parse(del.body.toString('utf8') || '{}') check('7 删除条目', del.status === 200 && delJson.ok === true, `status=${del.status}`) const afterDel = await req(`/share/clip/${token2}`) check('7b 删除后直链失效', afterDel.status === 410, `status=${afterDel.status}`) const db2 = new Database(path.join(process.cwd(), 'data', 'private_clipboard.db')) const left = db2.prepare('SELECT COUNT(1) AS c FROM clipboard_share_links WHERE item_id = ?').get(itemId) db2.close() check('7c share_links 表已清理', Number(left && left.c) === 0, `remaining=${left && left.c}`) // 8. 伪造 token const fake = await req('/share/clip/00000000000000000000000000000000') check('8 伪造 token 返回失效页', fake.status === 410, `status=${fake.status}`) console.log(`\n结果: ${passed} 通过 / ${failed} 失败`) process.exit(failed ? 1 : 0) })().catch(e => { console.error('[ERR]', e); process.exit(1) })