-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Expand file tree
/
Copy pathdev-proxy.js
More file actions
134 lines (123 loc) · 5.3 KB
/
dev-proxy.js
File metadata and controls
134 lines (123 loc) · 5.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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`)
);