const http = require('http'); const https = require('https'); const zlib = require('zlib'); const express = require('express'); const WsLib = require('ws'); const app = express(); // CORS: allow any local origin (Expo web dev server) app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Buvid3, X-Sessdata'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); function makeProxy(targetHost) { return (req, res) => { const buvid3 = req.headers['x-buvid3'] || ''; const sessdata = req.headers['x-sessdata'] || ''; const cookies = [ buvid3 && `buvid3=${buvid3}`, sessdata && `SESSDATA=${sessdata}`, ].filter(Boolean).join('; '); const options = { hostname: targetHost, path: req.url, method: req.method, headers: { 'Cookie': cookies, 'Referer': 'https://www.JKVideo.com', 'Origin': 'https://www.JKVideo.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Encoding': 'identity', }, }; const proxy = https.request(options, (proxyRes) => { // On successful QR login, extract SESSDATA from set-cookie and relay via custom header const setCookies = proxyRes.headers['set-cookie'] || []; const match = setCookies.find(c => c.includes('SESSDATA=')); if (match) { const val = match.split(';')[0].replace('SESSDATA=', ''); res.setHeader('X-Sessdata', val); } res.writeHead(proxyRes.statusCode, { 'Content-Type': proxyRes.headers['content-type'] || 'application/json', }); proxyRes.pipe(res); }); proxy.on('error', (err) => res.status(502).json({ error: err.message })); req.pipe(proxy); }; } app.use('/video-api', makeProxy('api.JKVideo.com')); app.use('/passport-api', makeProxy('passport.JKVideo.com')); app.use('/live-api', makeProxy('api.live.JKVideo.com')); // Dedicated comment proxy: buffer response and decompress by magic bytes (not Content-Encoding header) app.use('/comment-api', (req, res) => { const options = { hostname: 'comment.JKVideo.com', path: req.url, method: req.method, headers: { 'Referer': 'https://www.JKVideo.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9', }, }; const proxy = https.request(options, (proxyRes) => { res.setHeader('Content-Type', proxyRes.headers['content-type'] || 'text/xml; charset=utf-8'); const chunks = []; proxyRes.on('data', chunk => chunks.push(chunk)); proxyRes.on('end', () => { const buf = Buffer.concat(chunks); if (buf[0] === 0x1f && buf[1] === 0x8b) { // actual gzip data — decompress regardless of Content-Encoding header zlib.gunzip(buf, (err, result) => { if (err) res.status(502).end('gunzip error: ' + err.message); else res.end(result); }); } else { res.end(buf); } }); proxyRes.on('error', (err) => res.status(502).json({ error: err.message })); }); proxy.on('error', (err) => res.status(502).json({ error: err.message })); req.pipe(proxy); }); // Image CDN proxy — strips the host segment and forwards to the real CDN with Referer app.use('/img-proxy', (req, res) => { const parts = req.url.split('/').filter(Boolean); const host = parts[0]; if (!host || !host.endsWith('.hdslb.com')) return res.status(403).end(); req.url = '/' + parts.slice(1).join('/'); makeProxy(host)(req, res); }); const PORT = process.env.PROXY_PORT || 3001; const server = http.createServer(app); // WebSocket relay — Android Expo Go often can't reach live chat servers directly. // Device connects here; proxy opens the upstream WSS connection and relays all frames. const wss = new WsLib.Server({ server, path: '/danmaku-ws' }); wss.on('connection', (clientWs, req) => { const url = new URL(req.url, `http://localhost:${PORT}`); const target = url.searchParams.get('host'); if (!target || !target.includes('JKVideo.com')) { clientWs.close(4001, 'invalid target'); return; } console.log('[ws-relay] →', target); const upstream = new WsLib(target, { headers: { Origin: 'https://live.JKVideo.com' }, perMessageDeflate: false }); upstream.on('open', () => console.log('[ws-relay] upstream open')); upstream.on('message', data => { if (clientWs.readyState === 1) clientWs.send(data, { binary: true }); }); upstream.on('error', err => { console.error('[ws-relay] upstream error:', err.message); clientWs.close(); }); upstream.on('close', () => clientWs.close()); clientWs.on('message', data => { if (upstream.readyState === 1) upstream.send(data); }); clientWs.on('close', () => upstream.close()); clientWs.on('error', () => upstream.close()); }); server.listen(PORT, '0.0.0.0', () => console.log(`[Proxy] http://localhost:${PORT} ws://:${PORT}/danmaku-ws`) );