Spaces:
Paused
Paused
| import NodeCache from "node-cache"; | |
| import { nanoid } from "nanoid"; | |
| import { randomBytes } from "crypto"; | |
| import { strict as assert } from "assert"; | |
| import { setMaxListeners } from "node:events"; | |
| import { env } from "../config.js"; | |
| import { closeRequest } from "./shared.js"; | |
| import { decryptStream, encryptStream, generateHmac } from "../misc/crypto.js"; | |
| // optional dependency | |
| const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); | |
| const streamCache = new NodeCache({ | |
| stdTTL: env.streamLifespan, | |
| checkperiod: 10, | |
| deleteOnExpire: true | |
| }) | |
| streamCache.on("expired", (key) => { | |
| streamCache.del(key); | |
| }) | |
| const internalStreamCache = new Map(); | |
| const hmacSalt = randomBytes(64).toString('hex'); | |
| export function createStream(obj) { | |
| const streamID = nanoid(), | |
| iv = randomBytes(16).toString('base64url'), | |
| secret = randomBytes(32).toString('base64url'), | |
| exp = new Date().getTime() + env.streamLifespan * 1000, | |
| hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt), | |
| streamData = { | |
| exp: exp, | |
| type: obj.type, | |
| urls: obj.u, | |
| service: obj.service, | |
| filename: obj.filename, | |
| requestIP: obj.requestIP, | |
| headers: obj.headers, | |
| metadata: obj.fileMetadata || false, | |
| audioBitrate: obj.audioBitrate, | |
| audioCopy: !!obj.audioCopy, | |
| audioFormat: obj.audioFormat, | |
| }; | |
| streamCache.set( | |
| streamID, | |
| encryptStream(streamData, iv, secret) | |
| ) | |
| let streamLink = new URL('/tunnel', env.apiURL); | |
| const params = { | |
| 'id': streamID, | |
| 'exp': exp, | |
| 'sig': hmac, | |
| 'sec': secret, | |
| 'iv': iv | |
| } | |
| for (const [key, value] of Object.entries(params)) { | |
| streamLink.searchParams.append(key, value); | |
| } | |
| return streamLink.toString(); | |
| } | |
| export function getInternalStream(id) { | |
| return internalStreamCache.get(id); | |
| } | |
| export function createInternalStream(url, obj = {}) { | |
| assert(typeof url === 'string'); | |
| let dispatcher; | |
| if (obj.requestIP) { | |
| dispatcher = freebind?.dispatcherFromIP(obj.requestIP, { strict: false }) | |
| } | |
| const streamID = nanoid(); | |
| let controller = obj.controller; | |
| if (!controller) { | |
| controller = new AbortController(); | |
| setMaxListeners(Infinity, controller.signal); | |
| } | |
| let headers; | |
| if (obj.headers) { | |
| headers = new Map(Object.entries(obj.headers)); | |
| } | |
| internalStreamCache.set(streamID, { | |
| url, | |
| service: obj.service, | |
| headers, | |
| controller, | |
| dispatcher | |
| }); | |
| let streamLink = new URL('/itunnel', `http://127.0.0.1:${env.apiPort}`); | |
| streamLink.searchParams.set('id', streamID); | |
| const cleanup = () => { | |
| destroyInternalStream(streamLink); | |
| controller.signal.removeEventListener('abort', cleanup); | |
| } | |
| controller.signal.addEventListener('abort', cleanup); | |
| return streamLink.toString(); | |
| } | |
| export function destroyInternalStream(url) { | |
| url = new URL(url); | |
| if (url.hostname !== '127.0.0.1') { | |
| return; | |
| } | |
| const id = url.searchParams.get('id'); | |
| if (internalStreamCache.has(id)) { | |
| closeRequest(getInternalStream(id)?.controller); | |
| internalStreamCache.delete(id); | |
| } | |
| } | |
| function wrapStream(streamInfo) { | |
| const url = streamInfo.urls; | |
| if (typeof url === 'string') { | |
| streamInfo.urls = createInternalStream(url, streamInfo); | |
| } else if (Array.isArray(url)) { | |
| for (const idx in streamInfo.urls) { | |
| streamInfo.urls[idx] = createInternalStream( | |
| streamInfo.urls[idx], streamInfo | |
| ); | |
| } | |
| } else throw 'invalid urls'; | |
| return streamInfo; | |
| } | |
| export function verifyStream(id, hmac, exp, secret, iv) { | |
| try { | |
| const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); | |
| const cache = streamCache.get(id.toString()); | |
| if (ghmac !== String(hmac)) return { status: 401 }; | |
| if (!cache) return { status: 404 }; | |
| const streamInfo = JSON.parse(decryptStream(cache, iv, secret)); | |
| if (!streamInfo) return { status: 404 }; | |
| if (Number(exp) <= new Date().getTime()) | |
| return { status: 404 }; | |
| return wrapStream(streamInfo); | |
| } | |
| catch { | |
| return { status: 500 }; | |
| } | |
| } | |