mirror of
https://github.com/tiajinsha/JKVideo.git
synced 2026-04-04 22:49:02 +08:00
135 lines
5.3 KiB
JavaScript
135 lines
5.3 KiB
JavaScript
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://<LAN-IP>:${PORT}/danmaku-ws`)
|
|
);
|