From 98e05368ed411275e444486f9b0d9f7fe52d2779 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:28:09 +0600 Subject: [PATCH] api: raw stream status responses, clean up core --- src/core/api.js | 165 ++++++++++++++++++----------------- src/modules/stream/manage.js | 38 +++----- src/modules/stream/stream.js | 2 +- 3 files changed, 98 insertions(+), 107 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 228ab6db..cdcf216b 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -16,10 +16,10 @@ import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; import { extract } from "../modules/processing/url.js"; export function runAPI(express, app, gitCommit, gitBranch, __dirname) { - const corsConfig = !env.corsWildcard ? { + const corsConfig = env.corsWildcard ? {} : { origin: env.corsURL, optionsSuccessStatus: 200 - } : {}; + }; const apiLimiter = rateLimit({ windowMs: 60000, @@ -33,7 +33,8 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { "text": loc(languageCode(req), 'ErrorRateLimit') }); } - }); + }) + const apiLimiterStream = rateLimit({ windowMs: 60000, max: 25, @@ -41,12 +42,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), handler: (req, res) => { - return res.status(429).json({ - "status": "rate-limit", - "text": loc(languageCode(req), 'ErrorRateLimit') - }); + return res.sendStatus(429) } - }); + }) const startTime = new Date(); const startTimestamp = startTime.getTime(); @@ -56,7 +54,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.use('/api/:type', cors({ methods: ['GET', 'POST'], ...corsConfig - })); + })) app.use('/api/json', apiLimiter); app.use('/api/stream', apiLimiterStream); @@ -65,27 +63,27 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.use((req, res, next) => { try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') } next(); - }); + }) app.use('/api/json', express.json({ verify: (req, res, buf) => { - let acceptCon = String(req.header('Accept')) === "application/json"; - if (acceptCon) { + const acceptHeader = String(req.header('Accept')) === "application/json"; + if (acceptHeader) { if (buf.length > 720) throw new Error(); JSON.parse(buf); } else { throw new Error(); } } - })); + })) // handle express.json errors properly (https://github.com/expressjs/express/issues/4065) app.use('/api/json', (err, req, res, next) => { - let errorText = "invalid json body"; - let acceptCon = String(req.header('Accept')) !== "application/json"; + const errorText = "invalid json body"; + const acceptHeader = String(req.header('Accept')) !== "application/json"; - if (err || acceptCon) { - if (acceptCon) errorText = "invalid accept header"; + if (err || acceptHeader) { + if (acceptHeader) errorText = "invalid accept header"; return res.status(400).json({ status: "error", text: errorText @@ -93,12 +91,14 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } else { next(); } - }); + }) const acceptRegex = /^application\/json(; charset=utf-8)?$/; + app.post('/api/json', async (req, res) => { const request = req.body; const lang = languageCode(req); + const fail = (t) => { const { status, body } = createResponse("error", { t: loc(lang, t) }); res.status(status).json(body); @@ -132,80 +132,83 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } catch { fail('ErrorSomethingWentWrong'); } - }); + }) - app.get('/api/:type', (req, res) => { - try { - let j; - switch (req.params.type) { - case 'stream': - const q = req.query; - const checkQueries = q.t && q.e && q.h && q.s && q.i; - const checkBaseLength = q.t.length === 21 && q.e.length === 13; - const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22; - if (checkQueries && checkBaseLength && checkSafeLength) { - if (q.p) { - return res.status(200).json({ - status: "continue" - }) - } - let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); - if (streamInfo.error) { - return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); - } - return stream(res, streamInfo); - } + app.get('/api/stream', (req, res) => { + const id = String(req.query.id); + const exp = String(req.query.exp); + const sig = String(req.query.sig); + const sec = String(req.query.sec); + const iv = String(req.query.iv); - j = apiJSON(0, { - t: "bad request. stream link may be incomplete or corrupted." - }) - return res.status(j.status).json(j.body); - case 'istream': - if (!req.ip.endsWith('127.0.0.1')) - return res.sendStatus(403); - if (('' + req.query.t).length !== 21) - return res.sendStatus(400); - - let streamInfo = getInternalStream(req.query.t); - if (!streamInfo) return res.sendStatus(404); - streamInfo.headers = req.headers; - - return stream(res, { type: 'internal', ...streamInfo }); - case 'serverInfo': - return res.status(200).json({ - version: version, - commit: gitCommit, - branch: gitBranch, - name: env.apiName, - url: env.apiURL, - cors: Number(env.corsWildcard), - startTime: `${startTimestamp}` - }); - default: - j = apiJSON(0, { - t: "unknown response type" - }) - return res.status(j.status).json(j.body); + const checkQueries = id && exp && sig && sec && iv; + const checkBaseLength = id.length === 21 && exp.length === 13; + const checkSafeLength = sig.length === 43 && sec.length === 43 && iv.length === 22; + + if (checkQueries && checkBaseLength && checkSafeLength) { + // rate limit probe, will not return json after 8.0 + if (req.query.p) { + return res.status(200).json({ + status: "continue" + }) + } + try { + const streamInfo = verifyStream(id, sig, exp, sec, iv); + if (!streamInfo?.service) { + return res.sendStatus(streamInfo.status); + } + return stream(res, streamInfo); + } catch { + return res.destroy(); } - } catch (e) { - return res.status(500).json({ - status: "error", - text: loc(languageCode(req), 'ErrorCantProcess') - }); } - }); + return res.sendStatus(400); + }) + + app.get('/api/istream', (req, res) => { + try { + if (!req.ip.endsWith('127.0.0.1')) + return res.sendStatus(403); + if (String(req.query.id).length !== 21) + return res.sendStatus(400); + + const streamInfo = getInternalStream(req.query.id); + if (!streamInfo) return res.sendStatus(404); + streamInfo.headers = req.headers; + + return stream(res, { type: 'internal', ...streamInfo }); + } catch { + return res.destroy(); + } + }) + + app.get('/api/serverInfo', (req, res) => { + try { + return res.status(200).json({ + version: version, + commit: gitCommit, + branch: gitBranch, + name: env.apiName, + url: env.apiURL, + cors: Number(env.corsWildcard), + startTime: `${startTimestamp}` + }); + } catch { + return res.destroy(); + } + }) app.get('/api/status', (req, res) => { res.status(200).end() - }); + }) app.get('/favicon.ico', (req, res) => { res.sendFile(`${__dirname}/src/front/icons/favicon.ico`) - }); + }) app.get('/*', (req, res) => { - res.redirect('/api/json') - }); + res.redirect('/api/serverInfo') + }) app.listen(env.apiPort, env.listenAddress, () => { console.log(`\n` + @@ -214,5 +217,5 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { `URL: ${Cyan(`${env.apiURL}`)}\n` + `Port: ${env.apiPort}\n` ) - }); + }) } diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 7d19354f..86334fa2 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -1,6 +1,6 @@ import NodeCache from "node-cache"; import { randomBytes } from "crypto"; -import { nanoid } from 'nanoid'; +import { nanoid } from "nanoid"; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; import { streamLifespan, env } from "../config.js"; @@ -11,15 +11,6 @@ const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube']; -const streamNoAccess = { - error: "i couldn't verify if you have access to this stream. go back and try again!", - status: 401 -} -const streamNoExist = { - error: "this download link has expired or doesn't exist. go back and try again!", - status: 400 -} - const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, @@ -61,11 +52,11 @@ export function createStream(obj) { let streamLink = new URL('/api/stream', env.apiURL); const params = { - 't': streamID, - 'e': exp, - 'h': hmac, - 's': secret, - 'i': iv + 'id': streamID, + 'exp': exp, + 'sig': hmac, + 'sec': secret, + 'iv': iv } for (const [key, value] of Object.entries(params)) { @@ -96,7 +87,7 @@ export function createInternalStream(url, obj = {}) { }; let streamLink = new URL('/api/istream', `http://127.0.0.1:${env.apiPort}`); - streamLink.searchParams.set('t', streamID); + streamLink.searchParams.set('id', streamID); return streamLink.toString(); } @@ -106,7 +97,7 @@ export function destroyInternalStream(url) { return; } - const id = url.searchParams.get('t'); + const id = url.searchParams.get('id'); if (internalStreamCache[id]) { internalStreamCache[id].controller.abort(); @@ -141,22 +132,19 @@ export function verifyStream(id, hmac, exp, secret, iv) { const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); const cache = streamCache.get(id.toString()); - if (ghmac !== String(hmac)) return streamNoAccess; - if (!cache) return streamNoExist; + if (ghmac !== String(hmac)) return { status: 401 }; + if (!cache) return { status: 404 }; const streamInfo = JSON.parse(decryptStream(cache, iv, secret)); - if (!streamInfo) return streamNoExist; + if (!streamInfo) return { status: 404 }; if (Number(exp) <= new Date().getTime()) - return streamNoExist; + return { status: 404 }; return wrapStream(streamInfo); } catch { - return { - error: "something went wrong and i couldn't verify this stream. go back and try again!", - status: 500 - } + return { status: 500 }; } } diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index 3de1cb3e..3b3494b6 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -25,6 +25,6 @@ export default async function(res, streamInfo) { break; } } catch { - res.status(500).json({ status: "error", text: "Internal Server Error" }); + res.sendStatus(500); } }