akborana4 commited on
Commit
b99de78
·
verified ·
1 Parent(s): 0bddd43

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +132 -41
server/server.js CHANGED
@@ -6,6 +6,7 @@ import { Server as SocketIOServer } from 'socket.io';
6
  import path from 'path';
7
  import { fileURLToPath } from 'url';
8
  import axios from 'axios';
 
9
  import {
10
  searchUniversal,
11
  getSong,
@@ -21,7 +22,6 @@ const app = express();
21
  app.use(cors());
22
  app.use(express.json());
23
 
24
- // --- small utils ---
25
  function short(v, n = 400) {
26
  try {
27
  const s = typeof v === 'string' ? v : JSON.stringify(v);
@@ -34,7 +34,7 @@ function log(...args) {
34
  console.log(new Date().toISOString(), ...args);
35
  }
36
 
37
- // ---------- Room state ----------
38
  const rooms = new Map();
39
  function ensureRoom(roomId) {
40
  if (!rooms.has(roomId)) {
@@ -70,6 +70,8 @@ function membersPayload(room) {
70
  isHost: id === room.hostId
71
  }));
72
  }
 
 
73
  function broadcastMembers(roomId) {
74
  const room = rooms.get(roomId);
75
  if (!room) return;
@@ -110,7 +112,7 @@ function findUserByName(room, targetNameRaw) {
110
  return null;
111
  }
112
 
113
- // Health check
114
  app.get('/healthz', (_req, res) => res.send('OK'));
115
 
116
  // ---------- JioSaavn proxies ----------
@@ -153,7 +155,7 @@ app.get('/api/lyrics', async (req, res) => {
153
  } catch (e) { log('/api/lyrics error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
154
  });
155
 
156
- // ---------- YouTube search (optional) ----------
157
  app.get('/api/ytsearch', async (req, res) => {
158
  try {
159
  const key = process.env.YT_API_KEY;
@@ -227,18 +229,19 @@ app.get('/api/yt/source', async (req, res) => {
227
  });
228
 
229
  /*
230
- NEW: /api/yt/proxy
231
- - Proxies the raw GoogleVideo (redirector) URL so browser won't hit CORS issues.
232
- - Supports Range header (seeking).
233
- - Validates host by default (only googlevideo.com).
234
- - Usage: <video src="/api/yt/proxy?url=<ENCODED_REDIRECTOR_URL>">
 
235
  */
236
  function isAllowedProxyHost(urlStr) {
237
  try {
238
  const u = new URL(urlStr);
239
  const host = u.hostname.toLowerCase();
240
- // default allow only googlevideo redirector hosts; expand list if you use others
241
- return host.endsWith('googlevideo.com') || host === 'redirector.googlevideo.com';
242
  } catch {
243
  return false;
244
  }
@@ -250,53 +253,145 @@ app.get('/api/yt/proxy', async (req, res) => {
250
  if (!raw) return res.status(400).json({ error: 'Missing url' });
251
 
252
  if (!isAllowedProxyHost(raw)) {
253
- // If you need to proxy other hosts, update isAllowedProxyHost accordingly.
254
  return res.status(400).json({ error: 'Proxy to this host is not allowed by server policy' });
255
  }
256
 
257
- // Preserve Range header for partial content requests (seeking)
258
  const rangeHeader = req.headers.range || null;
259
- const headers = {};
260
- if (rangeHeader) headers.Range = rangeHeader;
 
 
 
 
 
 
261
 
262
  log('/api/yt/proxy fetching remote', short(raw, 200), 'Range:', rangeHeader ? rangeHeader.slice(0,120) : 'none');
263
 
264
- // Use axios to stream remote response; allow non-2xx so we can forward status
265
  const resp = await axios.get(raw, {
266
  responseType: 'stream',
267
  timeout: 20000,
268
- headers,
 
269
  validateStatus: () => true
270
  });
271
 
272
- // Forward key headers from remote to client (if present)
273
- const hopByHop = new Set([
274
- 'transfer-encoding', 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
275
- 'te', 'trailer', 'upgrade'
276
- ]);
277
- const forwardable = ['content-type','content-length','content-range','accept-ranges','cache-control','last-modified','etag'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
 
 
 
 
 
279
  forwardable.forEach(h => {
280
  const v = resp.headers[h];
281
  if (v !== undefined) res.setHeader(h, v);
282
  });
283
 
284
- // Ensure CORS header (app.use(cors()) covers most routes but for streamed responses set explicitly)
285
  res.setHeader('Access-Control-Allow-Origin', '*');
286
- // allow range requests from browsers
287
  res.setHeader('Access-Control-Allow-Headers', 'Range,Accept,Content-Type');
288
- // reflect remote status (206 if partial, 200 if full)
289
  res.status(resp.status);
290
 
291
- // Pipe the remote stream to the response
292
- resp.data.pipe(res);
 
293
 
294
- // error handling on the stream
295
- resp.data.on('error', (err) => {
296
  log('/api/yt/proxy remote stream error:', err?.message || String(err));
297
- // if response not ended yet, try to send an error
298
- try { if (!res.headersSent) res.status(502).json({ error: 'Remote stream error', detail: err?.message || String(err) }); } catch {}
 
 
 
 
 
 
 
 
299
  });
 
300
  } catch (e) {
301
  log('/api/yt/proxy unexpected error:', e?.response?.status ?? e?.message ?? String(e));
302
  const detail = e?.response?.data ?? e?.message ?? String(e);
@@ -304,7 +399,10 @@ app.get('/api/yt/proxy', async (req, res) => {
304
  }
305
  });
306
 
307
- // ---------- Lobby ----------
 
 
 
308
  app.get('/api/rooms', (_req, res) => {
309
  const data = [...rooms.values()]
310
  .filter(r => r.users.size > 0)
@@ -318,17 +416,16 @@ app.get('/api/rooms', (_req, res) => {
318
  });
319
  app.get('/api/ping', (_req, res) => res.json({ ok: true }));
320
 
321
- // ---------- Static client ----------
322
  const clientDir = path.resolve(__dirname, '../client/dist');
323
  app.use(express.static(clientDir));
324
  app.get('*', (_req, res) => res.sendFile(path.join(clientDir, 'index.html')));
325
 
326
- // ---------- Socket.IO ----------
327
  const server = http.createServer(app);
328
  const io = new SocketIOServer(server, {
329
  cors: { origin: '*', methods: ['GET', 'POST'] }
330
  });
331
 
 
332
  io.on('connection', (socket) => {
333
  let joinedRoom = null;
334
 
@@ -372,11 +469,6 @@ io.on('connection', (socket) => {
372
  socket.to(roomId).emit('chat_message', { name, text, at: Date.now() });
373
  });
374
 
375
- // song_request, song_request_action, set_track, play/pause/seek/ended, admin_command, sync_request...
376
- // (The rest of your socket handlers are unchanged — keep them exactly as before.)
377
- // For brevity in this snippet, the same socket handlers from your previous file are used:
378
- // copy/paste the rest of the socket handlers here (or keep this file from my previous response).
379
- // ---------- START socket handlers ----------
380
  socket.on('song_request', ({ roomId, requester, query }) => {
381
  const room = rooms.get(roomId);
382
  if (!room) return;
@@ -531,7 +623,6 @@ io.on('connection', (socket) => {
531
  });
532
  });
533
 
534
- // start server
535
  const PORT = process.env.PORT || 3000;
536
  server.listen(PORT, () => {
537
  log(`Server running on port ${PORT}`);
 
6
  import path from 'path';
7
  import { fileURLToPath } from 'url';
8
  import axios from 'axios';
9
+ import { PassThrough } from 'stream';
10
  import {
11
  searchUniversal,
12
  getSong,
 
22
  app.use(cors());
23
  app.use(express.json());
24
 
 
25
  function short(v, n = 400) {
26
  try {
27
  const s = typeof v === 'string' ? v : JSON.stringify(v);
 
34
  console.log(new Date().toISOString(), ...args);
35
  }
36
 
37
+ // ---------- Room state (same as before) ----------
38
  const rooms = new Map();
39
  function ensureRoom(roomId) {
40
  if (!rooms.has(roomId)) {
 
70
  isHost: id === room.hostId
71
  }));
72
  }
73
+
74
+ // helpers used by socket handlers later
75
  function broadcastMembers(roomId) {
76
  const room = rooms.get(roomId);
77
  if (!room) return;
 
112
  return null;
113
  }
114
 
115
+ // Health
116
  app.get('/healthz', (_req, res) => res.send('OK'));
117
 
118
  // ---------- JioSaavn proxies ----------
 
155
  } catch (e) { log('/api/lyrics error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
156
  });
157
 
158
+ // ---------- YouTube search ----------
159
  app.get('/api/ytsearch', async (req, res) => {
160
  try {
161
  const key = process.env.YT_API_KEY;
 
229
  });
230
 
231
  /*
232
+ Robust /api/yt/proxy implementation
233
+ - Proxies only allowed hosts (default: googlevideo.com)
234
+ - Forwards Range header
235
+ - Sets a user-agent and referer to reduce upstream blocking
236
+ - Peeks first chunk and rejects HTML/text responses (common when link expired)
237
+ - Forwards content-type, content-length, content-range, accept-ranges, cache-control, etag, last-modified
238
  */
239
  function isAllowedProxyHost(urlStr) {
240
  try {
241
  const u = new URL(urlStr);
242
  const host = u.hostname.toLowerCase();
243
+ // allow googlevideo redirector hosts (expand as needed)
244
+ return host.endsWith('googlevideo.com') || host.endsWith('redirector.googlevideo.com');
245
  } catch {
246
  return false;
247
  }
 
253
  if (!raw) return res.status(400).json({ error: 'Missing url' });
254
 
255
  if (!isAllowedProxyHost(raw)) {
 
256
  return res.status(400).json({ error: 'Proxy to this host is not allowed by server policy' });
257
  }
258
 
259
+ // Forward client's Range if present
260
  const rangeHeader = req.headers.range || null;
261
+ const requestHeaders = {
262
+ // make upstream think this is a real browser request
263
+ 'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
264
+ 'Accept': 'video/*,*/*;q=0.9',
265
+ 'Referer': 'https://www.youtube.com/',
266
+ // pass Range if client requested it (seek)
267
+ ...(rangeHeader ? { Range: rangeHeader } : {})
268
+ };
269
 
270
  log('/api/yt/proxy fetching remote', short(raw, 200), 'Range:', rangeHeader ? rangeHeader.slice(0,120) : 'none');
271
 
 
272
  const resp = await axios.get(raw, {
273
  responseType: 'stream',
274
  timeout: 20000,
275
+ headers: requestHeaders,
276
+ maxRedirects: 5,
277
  validateStatus: () => true
278
  });
279
 
280
+ // If remote responded with an error status, try to capture a small snippet and return 502
281
+ if (resp.status >= 400) {
282
+ let snippet = '';
283
+ try {
284
+ // try to read a short snippet from the stream (if any)
285
+ const streamReader = resp.data;
286
+ const chunk = await new Promise((resolve) => {
287
+ let done = false;
288
+ const onData = (c) => { if (!done) { done = true; cleanup(); resolve(c); } };
289
+ const onEnd = () => { if (!done) { done = true; cleanup(); resolve(null); } };
290
+ const onErr = () => { if (!done) { done = true; cleanup(); resolve(null); } };
291
+ const cleanup = () => { streamReader.removeListener('data', onData); streamReader.removeListener('end', onEnd); streamReader.removeListener('error', onErr); };
292
+ streamReader.once('data', onData);
293
+ streamReader.once('end', onEnd);
294
+ streamReader.once('error', onErr);
295
+ });
296
+ if (chunk) snippet = chunk.toString('utf8', 0, 800);
297
+ } catch (xx) { /* ignore */ }
298
+ log('/api/yt/proxy remote returned error status', resp.status, 'snippet:', short(snippet, 400));
299
+ return res.status(502).json({ error: 'Remote returned error', status: resp.status, snippet: short(snippet, 800) });
300
+ }
301
+
302
+ // If content-type looks like html/text/json, the signed URL probably expired or upstream blocked — reject.
303
+ const ct = String(resp.headers['content-type'] || '').toLowerCase();
304
+ if (ct.includes('text/html') || ct.includes('application/json') || ct.includes('text/plain')) {
305
+ // read a small snippet from stream to give debug info then abort
306
+ let snippet = '';
307
+ try {
308
+ const streamReader = resp.data;
309
+ const chunk = await new Promise((resolve) => {
310
+ let done = false;
311
+ const onData = (c) => { if (!done) { done = true; cleanup(); resolve(c); } };
312
+ const onEnd = () => { if (!done) { done = true; cleanup(); resolve(null); } };
313
+ const onErr = () => { if (!done) { done = true; cleanup(); resolve(null); } };
314
+ const cleanup = () => { streamReader.removeListener('data', onData); streamReader.removeListener('end', onEnd); streamReader.removeListener('error', onErr); };
315
+ streamReader.once('data', onData);
316
+ streamReader.once('end', onEnd);
317
+ streamReader.once('error', onErr);
318
+ });
319
+ if (chunk) snippet = chunk.toString('utf8', 0, 1600);
320
+ } catch (xx) {}
321
+ log('/api/yt/proxy remote returned text/html or json; rejecting. ct=', ct, 'snippet=', short(snippet, 800));
322
+ return res.status(502).json({ error: 'Remote returned unexpected content-type', contentType: ct, snippet: short(snippet, 800) });
323
+ }
324
+
325
+ // Now we will peek first data chunk to ensure we didn't get HTML disguised as video,
326
+ // then pass the full stream to client via PassThrough.
327
+ const remoteStream = resp.data; // Readable
328
+ const pass = new PassThrough();
329
+
330
+ let firstChunkChecked = false;
331
+ let firstChunkBuffer = null;
332
+ const onRemoteData = (chunk) => {
333
+ if (!firstChunkChecked) {
334
+ firstChunkChecked = true;
335
+ firstChunkBuffer = chunk;
336
+ const s = chunk.toString('utf8', 0, Math.min(chunk.length, 1024)).trim();
337
+ // if snippet looks like HTML or JSON, abort (remote probably returned an error page)
338
+ if (s.startsWith('<!doctype') || s.startsWith('<html') || s.startsWith('{') || s.includes('<html') || s.includes('Sign in to')) {
339
+ // drain remote and return 502
340
+ try { remoteStream.pause(); } catch {}
341
+ const snippet = s.slice(0, 1600);
342
+ log('/api/yt/proxy detected HTML/JSON in first chunk; aborting. snippet:', short(snippet, 800));
343
+ // cleanup listeners
344
+ remoteStream.removeListener('data', onRemoteData);
345
+ remoteStream.removeAllListeners();
346
+ pass.end(); // end passthrough
347
+ // respond with 502 and snippet
348
+ return res.status(502).json({ error: 'Remote returned non-media content (HTML/JSON)', snippet: short(snippet, 1000) });
349
+ } else {
350
+ // write the first chunk into pass and then pipe remainder
351
+ pass.write(chunk);
352
+ // allow rest of remote stream to be piped into pass
353
+ remoteStream.pipe(pass, { end: true });
354
+ // remove this listener (since we piped)
355
+ remoteStream.removeListener('data', onRemoteData);
356
+ }
357
+ }
358
+ };
359
 
360
+ // attach the onRemoteData listener
361
+ remoteStream.on('data', onRemoteData);
362
+
363
+ // Forward key headers from remote to client
364
+ const forwardable = ['content-type','content-length','content-range','accept-ranges','cache-control','last-modified','etag'];
365
  forwardable.forEach(h => {
366
  const v = resp.headers[h];
367
  if (v !== undefined) res.setHeader(h, v);
368
  });
369
 
370
+ // Set explicit CORS
371
  res.setHeader('Access-Control-Allow-Origin', '*');
 
372
  res.setHeader('Access-Control-Allow-Headers', 'Range,Accept,Content-Type');
373
+ // set status (206 if partial content)
374
  res.status(resp.status);
375
 
376
+ // If remote hasn't emitted any data yet (rare), pipe anyway (the onRemoteData handles first chunk)
377
+ // finally pipe the pass-through to the client response
378
+ pass.pipe(res);
379
 
380
+ // handle remote stream errors
381
+ remoteStream.on('error', (err) => {
382
  log('/api/yt/proxy remote stream error:', err?.message || String(err));
383
+ try {
384
+ if (!res.headersSent) res.status(502).json({ error: 'Remote stream error', detail: err?.message || String(err) });
385
+ else res.destroy(err);
386
+ } catch {}
387
+ });
388
+
389
+ // handle pass errors
390
+ pass.on('error', (err) => {
391
+ log('/api/yt/proxy pass error:', err?.message || String(err));
392
+ try { if (!res.headersSent) res.status(502).json({ error: 'PassThrough error', detail: err?.message || String(err) }); } catch {}
393
  });
394
+
395
  } catch (e) {
396
  log('/api/yt/proxy unexpected error:', e?.response?.status ?? e?.message ?? String(e));
397
  const detail = e?.response?.data ?? e?.message ?? String(e);
 
399
  }
400
  });
401
 
402
+ // ---------- Lobby / static / Socket.IO ----------
403
+ // (The rest of your server remains the same — socket handlers, static client, etc.)
404
+ // For completeness, include the socket setup and handlers (copied from your previous file).
405
+
406
  app.get('/api/rooms', (_req, res) => {
407
  const data = [...rooms.values()]
408
  .filter(r => r.users.size > 0)
 
416
  });
417
  app.get('/api/ping', (_req, res) => res.json({ ok: true }));
418
 
 
419
  const clientDir = path.resolve(__dirname, '../client/dist');
420
  app.use(express.static(clientDir));
421
  app.get('*', (_req, res) => res.sendFile(path.join(clientDir, 'index.html')));
422
 
 
423
  const server = http.createServer(app);
424
  const io = new SocketIOServer(server, {
425
  cors: { origin: '*', methods: ['GET', 'POST'] }
426
  });
427
 
428
+ // Socket handlers (unchanged logic) — simplified copy from your previous code:
429
  io.on('connection', (socket) => {
430
  let joinedRoom = null;
431
 
 
469
  socket.to(roomId).emit('chat_message', { name, text, at: Date.now() });
470
  });
471
 
 
 
 
 
 
472
  socket.on('song_request', ({ roomId, requester, query }) => {
473
  const room = rooms.get(roomId);
474
  if (!room) return;
 
623
  });
624
  });
625
 
 
626
  const PORT = process.env.PORT || 3000;
627
  server.listen(PORT, () => {
628
  log(`Server running on port ${PORT}`);