| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- // 仅供本地浏览器联调:模拟后台 API,与独立的真实 Reverb 实例通信。
- // 不读取项目 .env,不连接任何业务数据库,只监听 127.0.0.1。
- import http from 'node:http';
- import { createHash, createHmac } from 'node:crypto';
- import { readFile } from 'node:fs/promises';
- import { fileURLToPath } from 'node:url';
- const port = Number(process.env.PROBE_FIXTURE_PORT || 19272);
- const reverbPort = Number(process.env.PROBE_REVERB_PORT || 19273);
- const key = 'probe-fixture-key';
- const secret = 'probe-fixture-secret';
- const app = 'probe-fixture';
- const token = 'probe-test-token';
- const expiry = Math.floor(Date.now() / 1000) + 3600;
- const A = 'admin.orders.withdraw';
- const B = 'admin.users.online';
- const C = 'admin.stats.platform_count';
- const D = 'admin.stats.unprocessed';
- const CHANNELS = [A, B, C, D];
- const history = [];
- let mode = 'normal';
- const files = new Map([
- ['/realtime-test.html', ['../../public/realtime-test.html', 'text/html']],
- ['/static/css/realtime-test.css', ['../../public/static/css/realtime-test.css', 'text/css']],
- ['/static/js/realtime-test.js', ['../../public/static/js/realtime-test.js', 'application/javascript']],
- ['/static/js/realtime-probe.js', ['../../public/static/js/realtime-probe.js', 'application/javascript']],
- ['/static/vendor/realtime/pusher-8.4.0.min.js', ['../../public/static/vendor/realtime/pusher-8.4.0.min.js', 'application/javascript']],
- ['/static/vendor/realtime/laravel-echo-2.1.6.js', ['../../public/static/vendor/realtime/laravel-echo-2.1.6.js', 'application/javascript']],
- ]);
- const json = (res, data, status = 200) => { res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(data)); };
- const body = async (req) => {
- let text = '';
- for await (const chunk of req) { text += chunk; if (text.length > 65536) throw new Error('fixture request too large'); }
- return text;
- };
- const server = http.createServer(async (req, res) => {
- try {
- const url = new URL(req.url, `http://127.0.0.1:${port}`);
- if (req.method === 'GET' && files.has(url.pathname)) {
- const [path, mime] = files.get(url.pathname);
- res.writeHead(200, { 'Content-Type': `${mime}; charset=utf-8`, 'Cache-Control': 'no-store' });
- res.end(await readFile(fileURLToPath(new URL(path, import.meta.url)))); return;
- }
- if (req.method === 'POST' && url.pathname === '/__fixture/mode') {
- mode = (JSON.parse(await body(req))).mode;
- json(res, { mode }); return;
- }
- if (req.method === 'POST' && url.pathname === '/__fixture/publish') {
- const input = JSON.parse(await body(req));
- const channel = input.channel || A;
- if (!CHANNELS.includes(channel)) { json(res, { error: 'unknown fixture channel' }, 400); return; }
- const statistics = channel === C || channel === D;
- const event = { seq: history.length + 1, id: history.length + 1, channel,
- event: statistics ? 'stats.updated' : channel === A ? 'order.updated' : 'user.online', action: statistics || channel === A ? 'updated' : 'online',
- key: statistics ? { scope: 'all' } : { id: 88 }, data: channel === C
- ? { total: 3, apple: 1, android: 1, pc: 1, mobile: 0, wechat: 0, unknown: 0 }
- : channel === D ? { usdtRecharge: 1, rgRecharge: 2, rmbRecharge: 0, usdtWithdraw: 1, rgWithdraw: 0, rmbWithdraw: 0, shareClaim: 1 }
- : { id: 88, status: 1, note: input.note || 'fixture event', token: 'private-fixture-value' },
- source: 'browser-fixture', created_at: new Date().toISOString() };
- const payload = JSON.stringify({ name: event.event, channels: [`private-${channel}`], data: JSON.stringify(event) });
- const path = `/apps/${app}/events`;
- const values = { auth_key: key, auth_timestamp: String(Math.floor(Date.now() / 1000)), auth_version: '1.0', body_md5: createHash('md5').update(payload).digest('hex') };
- const query = new URLSearchParams(Object.entries(values).sort(([a], [b]) => a.localeCompare(b)));
- query.set('auth_signature', createHmac('sha256', secret).update(`POST\n${path}\n${query}`).digest('hex'));
- const reply = await fetch(`http://127.0.0.1:${reverbPort}${path}?${query}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
- if (!reply.ok) throw new Error(`Reverb publish returned HTTP ${reply.status}: ${await reply.text()}`);
- history.push(event); json(res, { seq: event.seq }); return;
- }
- if (req.headers.authorization !== `Bearer ${token}`) { json(res, { msg: 'fixture token invalid' }, 401); return; }
- if (url.pathname === '/admin/realtime/config') {
- json(res, { code: 0, data: { key, broadcaster: 'reverb', ws_host: '127.0.0.1', ws_port: reverbPort, force_tls: false,
- channels: CHANNELS, last_seq: history.length, session_expires_at: expiry, auth_endpoint: '/admin/broadcasting/auth' } });
- } else if (url.pathname === '/admin/broadcasting/auth' && req.method === 'POST') {
- const data = new URLSearchParams(await body(req));
- const channel = data.get('channel_name');
- if (mode === 'expired') { json(res, { msg: 'fixture expired' }, 401); return; }
- if (mode === 'denied' || !CHANNELS.map((name) => `private-${name}`).includes(channel)) { json(res, { msg: 'fixture denied' }, 403); return; }
- const channel_data = JSON.stringify({ user_id: `1:${expiry}` });
- const auth = `${key}:${createHmac('sha256', secret).update(`${data.get('socket_id')}:${channel}:${channel_data}`).digest('hex')}`;
- json(res, { auth, channel_data });
- } else if (url.pathname === '/admin/realtime/replay') {
- const after = Number(url.searchParams.get('after_seq'));
- const channels = (url.searchParams.get('channels') || '').split(',');
- json(res, { code: 0, data: { resync: mode === 'resync', ...(mode === 'resync' ? { reason: 'too_many_events' } : {}),
- last_seq: history.length, channels,
- events: mode === 'resync' ? [] : history.filter((event) => event.seq > after && channels.includes(event.channel)) } });
- } else json(res, { error: 'fixture route not found' }, 404);
- } catch (error) { json(res, { error: error.message }, 500); }
- });
- server.listen(port, '127.0.0.1', () => console.log(`Probe fixture: http://127.0.0.1:${port}/realtime-test.html`));
- process.on('SIGINT', () => server.close(() => process.exit(0)));
- process.on('SIGTERM', () => server.close(() => process.exit(0)));
|