akborana4 commited on
Commit
7e2652b
·
verified ·
1 Parent(s): 47281e6

Update server/server.js

Browse files
Files changed (1) hide show
  1. server/server.js +109 -146
server/server.js CHANGED
@@ -31,7 +31,7 @@ function short(v, n = 400) {
31
  } catch { return String(v).slice(0, n) + '…'; }
32
  }
33
 
34
- // ---------- Room state ----------
35
  const rooms = new Map();
36
  function ensureRoom(roomId) {
37
  if (!rooms.has(roomId)) {
@@ -110,7 +110,7 @@ function findUserByName(room, targetNameRaw) {
110
  // Health
111
  app.get('/healthz', (_req, res) => res.send('OK'));
112
 
113
- // ---------- JioSaavn proxies ----------
114
  app.get('/api/result', async (req, res) => {
115
  try {
116
  const q = req.query.q || '';
@@ -121,34 +121,10 @@ app.get('/api/result', async (req, res) => {
121
  res.status(500).json({ error: e.message });
122
  }
123
  });
124
- app.get('/api/song', async (req, res) => {
125
- try {
126
- const q = req.query.q || '';
127
- const data = await getSong(q);
128
- res.json(data);
129
- } catch (e) { log('/api/song error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
130
- });
131
- app.get('/api/album', async (req, res) => {
132
- try {
133
- const q = req.query.q || '';
134
- const data = await getAlbum(q);
135
- res.json(data);
136
- } catch (e) { log('/api/album error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
137
- });
138
- app.get('/api/playlist', async (req, res) => {
139
- try {
140
- const q = req.query.q || '';
141
- const data = await getPlaylist(q);
142
- res.json(data);
143
- } catch (e) { log('/api/playlist error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
144
- });
145
- app.get('/api/lyrics', async (req, res) => {
146
- try {
147
- const q = req.query.q || '';
148
- const data = await getLyrics(q);
149
- res.json(data);
150
- } catch (e) { log('/api/lyrics error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); }
151
- });
152
 
153
  // ---------- YouTube search (optional) ----------
154
  app.get('/api/ytsearch', async (req, res) => {
@@ -173,7 +149,7 @@ app.get('/api/ytsearch', async (req, res) => {
173
  }
174
  });
175
 
176
- // ---------- VidFly config & debug ----------
177
  const VIDFLY_API = process.env.VIDFLY_API || 'https://api.vidfly.ai/api/media/youtube/download';
178
 
179
  app.get('/api/vidfly/debug', async (req, res) => {
@@ -238,7 +214,14 @@ app.get('/api/yt/source', async (req, res) => {
238
  }
239
  });
240
 
241
- // ---------- Proxy & debug (googlevideo) ----------
 
 
 
 
 
 
 
242
  function isAllowedProxyHost(urlStr) {
243
  try {
244
  const u = new URL(urlStr);
@@ -249,147 +232,122 @@ function isAllowedProxyHost(urlStr) {
249
  }
250
  }
251
 
252
- // Debug endpoint for the signed googlevideo URL (small ranged GET, returns headers + snippet)
253
- app.get('/api/yt/proxy/debug', async (req, res) => {
 
254
  try {
255
- let raw = (req.query.url || '').trim();
256
- if (!raw) return res.status(400).json({ error: 'Missing url' });
257
- if (/^[a-zA-Z0-9_-]{11}$/.test(raw)) raw = `https://www.youtube.com/watch?v=${raw}`;
258
- try {
259
- const u = new URL(raw);
260
- const host = u.hostname.toLowerCase();
261
- if (!(host.endsWith('googlevideo.com') || host.endsWith('redirector.googlevideo.com'))) {
262
- return res.status(400).json({ error: 'Debug only allows googlevideo redirector URLs' });
263
- }
264
- } catch (e) {
265
- return res.status(400).json({ error: 'Invalid URL' });
266
- }
267
- const rangeHeader = 'bytes=0-16383';
268
- const requestHeaders = {
269
- 'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
270
- 'Accept': '*/*',
271
- 'Referer': 'https://www.youtube.com/',
272
- Range: rangeHeader
273
- };
274
- const resp = await axios.get(raw, {
275
- responseType: 'arraybuffer',
276
- headers: requestHeaders,
277
- timeout: 15000,
278
  maxRedirects: 5,
279
  validateStatus: () => true
280
  });
281
- const buf = Buffer.from(resp.data || new Uint8Array());
282
- const b64 = buf.length ? buf.slice(0, 4096).toString('base64') : null;
283
- const ct = String(resp.headers['content-type'] || '').toLowerCase();
284
- let textSnippet = null;
285
- if (ct.includes('text') || ct.includes('html') || ct.includes('json')) {
286
- try { textSnippet = buf.toString('utf8', 0, Math.min(buf.length, 2000)); } catch {}
287
- }
288
- return res.json({
289
- upstreamStatus: resp.status,
290
- upstreamHeaders: resp.headers,
291
- snippetBase64: b64,
292
- textSnippet
293
  });
294
- } catch (err) {
295
- const detail = err?.response?.data ?? err?.message ?? String(err);
296
- return res.status(500).json({ error: 'Debug fetch failed', detail: String(detail).slice(0, 1000) });
297
  }
298
- });
299
 
300
  app.get('/api/yt/proxy', async (req, res) => {
301
  try {
302
  const raw = (req.query.url || '').trim();
303
  if (!raw) return res.status(400).json({ error: 'Missing url' });
304
 
 
305
  if (!isAllowedProxyHost(raw)) {
306
  return res.status(400).json({ error: 'Proxy to this host is not allowed by server policy' });
307
  }
308
 
309
- const rangeHeader = req.headers.range || null;
 
 
 
310
  const requestHeaders = {
311
  'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
312
  'Accept': '*/*',
313
  'Referer': 'https://www.youtube.com/',
314
- ...(rangeHeader ? { Range: rangeHeader } : {})
 
315
  };
316
 
317
- log('/api/yt/proxy fetching', short(raw, 200), 'Range:', rangeHeader ? rangeHeader.slice(0,120) : 'none');
318
-
319
- const resp = await axios.get(raw, {
320
- responseType: 'stream',
321
- timeout: 20000,
322
- headers: requestHeaders,
323
- maxRedirects: 5,
324
- validateStatus: () => true
325
- });
326
 
327
- if (resp.status >= 400) {
328
- let snippet = '';
329
- try {
330
- const reader = resp.data;
331
- const chunk = await new Promise((resolve) => {
332
- let done = false;
333
- const onData = (c) => { if (!done) { done = true; cleanup(); resolve(c); } };
334
- const onEnd = () => { if (!done) { done = true; cleanup(); resolve(null); } };
335
- const onErr = () => { if (!done) { done = true; cleanup(); resolve(null); } };
336
- const cleanup = () => { reader.removeListener('data', onData); reader.removeListener('end', onEnd); reader.removeListener('error', onErr); };
337
- reader.once('data', onData);
338
- reader.once('end', onEnd);
339
- reader.once('error', onErr);
340
- });
341
- if (chunk) snippet = chunk.toString('utf8', 0, 800);
342
- } catch (xx) { /* ignore */ }
343
-
344
- log('/api/yt/proxy remote returned error status', resp.status, 'snippet:', short(snippet, 400));
345
- return res.status(502).json({ error: 'Remote returned error', status: resp.status, snippet: short(snippet, 800) });
346
  }
347
 
348
- const ct = String(resp.headers['content-type'] || '').toLowerCase();
349
- if (ct.includes('text/html') || ct.includes('application/json') || ct.includes('text/plain')) {
350
- let snippet = '';
351
- try {
352
- const reader = resp.data;
353
- const chunk = await new Promise((resolve) => {
354
- let done = false;
355
- const onData = (c) => { if (!done) { done = true; cleanup(); resolve(c); } };
356
- const onEnd = () => { if (!done) { done = true; cleanup(); resolve(null); } };
357
- const onErr = () => { if (!done) { done = true; cleanup(); resolve(null); } };
358
- const cleanup = () => { reader.removeListener('data', onData); reader.removeListener('end', onEnd); reader.removeListener('error', onErr); };
359
- reader.once('data', onData);
360
- reader.once('end', onEnd);
361
- reader.once('error', onErr);
362
- });
363
- if (chunk) snippet = chunk.toString('utf8', 0, 1600);
364
- } catch (xx) {}
365
-
366
- log('/api/yt/proxy remote returned non-media content-type', ct, short(snippet, 400));
367
- return res.status(502).json({ error: 'Remote returned non-media content-type', contentType: ct, snippet: short(snippet, 800) });
368
- }
 
 
369
 
370
- const forwardable = ['content-type','content-length','content-range','accept-ranges','cache-control','last-modified','etag'];
371
- forwardable.forEach(h => {
372
- const v = resp.headers[h];
373
- if (v !== undefined) res.setHeader(h, v);
374
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
 
 
376
  res.setHeader('Access-Control-Allow-Origin', '*');
377
- res.setHeader('Access-Control-Allow-Headers', 'Range,Accept,Content-Type');
378
-
379
- res.status(resp.status);
380
-
381
- const upstream = resp.data;
382
- upstream.on('error', (err) => {
383
- log('/api/yt/proxy upstream stream error:', err?.message || String(err));
384
- try {
385
- if (!res.headersSent) res.status(502).json({ error: 'Upstream stream error', detail: err?.message || String(err) });
386
- else res.destroy(err);
387
- } catch {}
388
- });
389
- upstream.pipe(res).on('error', (err) => {
390
- log('/api/yt/proxy pipe error:', err?.message || String(err));
391
- try { if (!res.headersSent) res.status(502).json({ error: 'Pipe error', detail: err?.message || String(err) }); } catch {}
392
- });
393
 
394
  } catch (e) {
395
  log('/api/yt/proxy unexpected error:', e?.response?.status ?? e?.message ?? String(e));
@@ -417,7 +375,7 @@ const clientDir = path.resolve(__dirname, '../client/dist');
417
  app.use(express.static(clientDir));
418
  app.get('*', (_req, res) => res.sendFile(path.join(clientDir, 'index.html')));
419
 
420
- // ---------- Socket.IO ----------
421
  const server = http.createServer(app);
422
  const io = new SocketIOServer(server, {
423
  cors: { origin: '*', methods: ['GET', 'POST'] }
@@ -428,19 +386,24 @@ io.on('connection', (socket) => {
428
 
429
  socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => {
430
  const room = ensureRoom(roomId);
 
431
  if (asHost || !room.hostId) room.hostId = socket.id;
432
  if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null;
 
433
  const role = socket.id === room.hostId ? 'host' : 'member';
434
  const cleanName = String(name || 'Guest').slice(0, 40);
435
  room.users.set(socket.id, { name: cleanName, role });
 
436
  socket.join(roomId);
437
  joinedRoom = roomId;
 
438
  ack?.({
439
  roomId,
440
  isHost: socket.id === room.hostId,
441
  state: currentState(room),
442
  roomName: room.name || room.id
443
  });
 
444
  io.to(roomId).emit('system', { text: `System: ${cleanName} has joined the chat` });
445
  broadcastMembers(roomId);
446
  });
 
31
  } catch { return String(v).slice(0, n) + '…'; }
32
  }
33
 
34
+ // ---------- Room state (your original code) ----------
35
  const rooms = new Map();
36
  function ensureRoom(roomId) {
37
  if (!rooms.has(roomId)) {
 
110
  // Health
111
  app.get('/healthz', (_req, res) => res.send('OK'));
112
 
113
+ // ---------- JioSaavn proxies (unchanged) ----------
114
  app.get('/api/result', async (req, res) => {
115
  try {
116
  const q = req.query.q || '';
 
121
  res.status(500).json({ error: e.message });
122
  }
123
  });
124
+ app.get('/api/song', async (req, res) => { try { const q = req.query.q || ''; const data = await getSong(q); res.json(data); } catch (e) { log('/api/song error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); } });
125
+ app.get('/api/album', async (req, res) => { try { const q = req.query.q || ''; const data = await getAlbum(q); res.json(data); } catch (e) { log('/api/album error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); } });
126
+ app.get('/api/playlist', async (req, res) => { try { const q = req.query.q || ''; const data = await getPlaylist(q); res.json(data); } catch (e) { log('/api/playlist error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); } });
127
+ app.get('/api/lyrics', async (req, res) => { try { const q = req.query.q || ''; const data = await getLyrics(q); res.json(data); } catch (e) { log('/api/lyrics error', e?.message || short(e?.response?.data)); res.status(500).json({ error: e.message }); } });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  // ---------- YouTube search (optional) ----------
130
  app.get('/api/ytsearch', async (req, res) => {
 
149
  }
150
  });
151
 
152
+ // ---------- VidFly resolver (unchanged) ----------
153
  const VIDFLY_API = process.env.VIDFLY_API || 'https://api.vidfly.ai/api/media/youtube/download';
154
 
155
  app.get('/api/vidfly/debug', async (req, res) => {
 
214
  }
215
  });
216
 
217
+ // ---------- Proxy with probe + redirect fallback ----------
218
+ function isProbablyMediaContentType(ct) {
219
+ if (!ct) return false;
220
+ const t = String(ct).toLowerCase();
221
+ return t.startsWith('video/') || t.startsWith('audio/') || t.includes('mpeg') || t.includes('mp4') || t.includes('ogg') || t.includes('webm');
222
+ }
223
+
224
+ // Allowed hosts for proxy to avoid open SSRF — keep limited to googlevideo/redirector
225
  function isAllowedProxyHost(urlStr) {
226
  try {
227
  const u = new URL(urlStr);
 
232
  }
233
  }
234
 
235
+ // probe function: try HEAD, else small ranged GET
236
+ async function probeUrl(url, headers = {}) {
237
+ // Attempt HEAD first
238
  try {
239
+ const hresp = await axios.head(url, {
240
+ headers,
241
+ timeout: 8000,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  maxRedirects: 5,
243
  validateStatus: () => true
244
  });
245
+ return { status: hresp.status, headers: hresp.headers, probeMethod: 'HEAD' };
246
+ } catch (e) {
247
+ // HEAD might be blocked; try tiny GET
248
+ }
249
+ try {
250
+ const gresp = await axios.get(url, {
251
+ headers: { ...headers, Range: 'bytes=0-1023' },
252
+ timeout: 10000,
253
+ responseType: 'arraybuffer',
254
+ maxRedirects: 5,
255
+ validateStatus: () => true
 
256
  });
257
+ return { status: gresp.status, headers: gresp.headers, probeMethod: 'GET', snippet: Buffer.from(gresp.data || new Uint8Array()).slice(0, 2048).toString('base64') };
258
+ } catch (e) {
259
+ return { error: e, probeMethod: 'ERR' };
260
  }
261
+ }
262
 
263
  app.get('/api/yt/proxy', async (req, res) => {
264
  try {
265
  const raw = (req.query.url || '').trim();
266
  if (!raw) return res.status(400).json({ error: 'Missing url' });
267
 
268
+ // Only allow googlevideo redirector (adjust if you want other hosts)
269
  if (!isAllowedProxyHost(raw)) {
270
  return res.status(400).json({ error: 'Proxy to this host is not allowed by server policy' });
271
  }
272
 
273
+ // Forward range header from client (if any)
274
+ const clientRange = req.headers.range || null;
275
+
276
+ // Common browser-like headers to increase chance of upstream acceptance
277
  const requestHeaders = {
278
  'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
279
  'Accept': '*/*',
280
  'Referer': 'https://www.youtube.com/',
281
+ 'Origin': 'https://www.youtube.com/',
282
+ ...(clientRange ? { Range: clientRange } : {})
283
  };
284
 
285
+ log('/api/yt/proxy probe', short(raw, 200), 'clientRange=', clientRange ? clientRange.slice(0,120) : 'none');
286
+ const probe = await probeUrl(raw, requestHeaders);
 
 
 
 
 
 
 
287
 
288
+ if (probe.error) {
289
+ log('/api/yt/proxy probe error', probe.error?.message || String(probe.error));
290
+ // Inconclusive probe — safest option: redirect the browser to the signed URL to let client fetch it
291
+ res.setHeader('Access-Control-Allow-Origin', '*');
292
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
293
+ return res.redirect(307, raw);
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
+ const upstreamStatus = probe.status;
297
+ const upstreamCT = String((probe.headers && probe.headers['content-type']) || '').toLowerCase();
298
+
299
+ // If upstream says it's media (video/audio) and status is 200/206 -> stream via proxy
300
+ if ((upstreamStatus === 200 || upstreamStatus === 206) && isProbablyMediaContentType(upstreamCT)) {
301
+ log('/api/yt/proxy streaming via server; content-type=', upstreamCT);
302
+
303
+ // Make streaming request (forward Range if client provided)
304
+ const streamResp = await axios.get(raw, {
305
+ responseType: 'stream',
306
+ headers: requestHeaders,
307
+ timeout: 20000,
308
+ maxRedirects: 5,
309
+ validateStatus: () => true
310
+ });
311
+
312
+ // If upstream returned non-success now, fall back to redirect
313
+ if (streamResp.status >= 400) {
314
+ log('/api/yt/proxy stream request failed, status=', streamResp.status, 'falling back to redirect');
315
+ res.setHeader('Access-Control-Allow-Origin', '*');
316
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
317
+ return res.redirect(307, raw);
318
+ }
319
 
320
+ // Forward useful headers
321
+ const forwardable = ['content-type','content-length','content-range','accept-ranges','cache-control','last-modified','etag'];
322
+ forwardable.forEach(h => {
323
+ const v = streamResp.headers[h];
324
+ if (v !== undefined) res.setHeader(h, v);
325
+ });
326
+
327
+ // CORS & expose headers so browser JS can see ranges
328
+ res.setHeader('Access-Control-Allow-Origin', '*');
329
+ res.setHeader('Access-Control-Allow-Headers', 'Range,Accept,Content-Type');
330
+ // Expose these headers to client
331
+ res.setHeader('Access-Control-Expose-Headers', 'Content-Length,Content-Range,Accept-Ranges');
332
+
333
+ res.status(streamResp.status);
334
+ // Pipe upstream stream to client
335
+ streamResp.data.on('error', (err) => {
336
+ log('/api/yt/proxy upstream stream error', err?.message || err);
337
+ try { if (!res.headersSent) res.status(502).json({ error: 'Upstream stream error', detail: String(err) }); else res.destroy(err); } catch {}
338
+ });
339
+ streamResp.data.pipe(res).on('error', (err) => {
340
+ log('/api/yt/proxy pipe error', err?.message || err);
341
+ try { if (!res.headersSent) res.status(502).json({ error: 'Pipe error', detail: String(err) }); } catch {}
342
+ });
343
+ return;
344
+ }
345
 
346
+ // For anything else (403/404 or non-media content-type), redirect browser to the signed URL
347
+ log('/api/yt/proxy falling back to redirect; upstreamStatus=', upstreamStatus, 'ct=', upstreamCT);
348
  res.setHeader('Access-Control-Allow-Origin', '*');
349
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
350
+ return res.redirect(307, raw);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
  } catch (e) {
353
  log('/api/yt/proxy unexpected error:', e?.response?.status ?? e?.message ?? String(e));
 
375
  app.use(express.static(clientDir));
376
  app.get('*', (_req, res) => res.sendFile(path.join(clientDir, 'index.html')));
377
 
378
+ // ---------- Socket.IO (your previous handlers) ----------
379
  const server = http.createServer(app);
380
  const io = new SocketIOServer(server, {
381
  cors: { origin: '*', methods: ['GET', 'POST'] }
 
386
 
387
  socket.on('join_room', ({ roomId, name, asHost = false, roomName = null }, ack) => {
388
  const room = ensureRoom(roomId);
389
+
390
  if (asHost || !room.hostId) room.hostId = socket.id;
391
  if (!room.name && roomName) room.name = String(roomName).trim().slice(0, 60) || null;
392
+
393
  const role = socket.id === room.hostId ? 'host' : 'member';
394
  const cleanName = String(name || 'Guest').slice(0, 40);
395
  room.users.set(socket.id, { name: cleanName, role });
396
+
397
  socket.join(roomId);
398
  joinedRoom = roomId;
399
+
400
  ack?.({
401
  roomId,
402
  isHost: socket.id === room.hostId,
403
  state: currentState(room),
404
  roomName: room.name || room.id
405
  });
406
+
407
  io.to(roomId).emit('system', { text: `System: ${cleanName} has joined the chat` });
408
  broadcastMembers(roomId);
409
  });