2022-07-08 19:17:56 +01:00
|
|
|
import NodeCache from "node-cache";
|
2023-04-29 16:30:59 +01:00
|
|
|
import { randomBytes } from "crypto";
|
2024-05-15 17:28:09 +01:00
|
|
|
import { nanoid } from "nanoid";
|
2022-07-08 19:17:56 +01:00
|
|
|
|
2024-03-05 14:55:17 +00:00
|
|
|
import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
|
2024-04-29 12:56:05 +01:00
|
|
|
import { streamLifespan, env } from "../config.js";
|
2024-04-26 12:53:50 +01:00
|
|
|
import { strict as assert } from "assert";
|
2022-07-08 19:17:56 +01:00
|
|
|
|
2024-05-12 17:35:52 +01:00
|
|
|
// optional dependency
|
|
|
|
const freebind = env.freebindCIDR && await import('freebind').catch(() => {});
|
|
|
|
|
2024-04-27 16:36:17 +01:00
|
|
|
const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube'];
|
|
|
|
|
2023-12-02 14:44:19 +00:00
|
|
|
const streamCache = new NodeCache({
|
|
|
|
stdTTL: streamLifespan/1000,
|
|
|
|
checkperiod: 10,
|
|
|
|
deleteOnExpire: true
|
|
|
|
})
|
2022-07-08 19:17:56 +01:00
|
|
|
|
2023-01-13 18:34:48 +00:00
|
|
|
streamCache.on("expired", (key) => {
|
|
|
|
streamCache.del(key);
|
2023-12-02 14:44:19 +00:00
|
|
|
})
|
|
|
|
|
2024-04-26 12:53:50 +01:00
|
|
|
const internalStreamCache = {};
|
2024-03-05 12:14:26 +00:00
|
|
|
const hmacSalt = randomBytes(64).toString('hex');
|
2023-01-13 18:34:48 +00:00
|
|
|
|
2022-07-08 19:17:56 +01:00
|
|
|
export function createStream(obj) {
|
2024-03-05 12:14:26 +00:00
|
|
|
const streamID = nanoid(),
|
2024-03-05 15:15:13 +00:00
|
|
|
iv = randomBytes(16).toString('base64url'),
|
2024-03-05 16:49:00 +00:00
|
|
|
secret = randomBytes(32).toString('base64url'),
|
2024-03-05 14:41:08 +00:00
|
|
|
exp = new Date().getTime() + streamLifespan,
|
2024-03-05 14:55:17 +00:00
|
|
|
hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt),
|
2024-03-05 12:14:26 +00:00
|
|
|
streamData = {
|
2024-03-05 14:45:54 +00:00
|
|
|
exp: exp,
|
2023-01-13 18:34:48 +00:00
|
|
|
type: obj.type,
|
|
|
|
urls: obj.u,
|
2024-03-05 14:45:54 +00:00
|
|
|
service: obj.service,
|
2023-01-13 18:34:48 +00:00
|
|
|
filename: obj.filename,
|
|
|
|
audioFormat: obj.audioFormat,
|
2024-03-05 12:14:26 +00:00
|
|
|
isAudioOnly: !!obj.isAudioOnly,
|
2023-01-13 18:34:48 +00:00
|
|
|
copy: !!obj.copy,
|
|
|
|
mute: !!obj.mute,
|
2024-05-12 17:35:52 +01:00
|
|
|
metadata: obj.fileMetadata || false,
|
|
|
|
requestIP: obj.requestIP
|
2024-03-05 12:14:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
streamCache.set(
|
|
|
|
streamID,
|
|
|
|
encryptStream(streamData, iv, secret)
|
|
|
|
)
|
|
|
|
|
2024-04-29 12:56:05 +01:00
|
|
|
let streamLink = new URL('/api/stream', env.apiURL);
|
2024-03-05 12:14:26 +00:00
|
|
|
|
|
|
|
const params = {
|
2024-05-15 17:28:09 +01:00
|
|
|
'id': streamID,
|
|
|
|
'exp': exp,
|
|
|
|
'sig': hmac,
|
|
|
|
'sec': secret,
|
|
|
|
'iv': iv
|
2023-01-13 18:34:48 +00:00
|
|
|
}
|
2024-03-05 12:14:26 +00:00
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(params)) {
|
|
|
|
streamLink.searchParams.append(key, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return streamLink.toString();
|
2022-07-08 19:17:56 +01:00
|
|
|
}
|
|
|
|
|
2024-04-26 12:53:50 +01:00
|
|
|
export function getInternalStream(id) {
|
|
|
|
return internalStreamCache[id];
|
|
|
|
}
|
|
|
|
|
2024-04-27 16:36:17 +01:00
|
|
|
export function createInternalStream(url, obj = {}) {
|
|
|
|
assert(typeof url === 'string');
|
2024-04-26 12:53:50 +01:00
|
|
|
|
2024-05-12 17:35:52 +01:00
|
|
|
let dispatcher;
|
|
|
|
if (obj.requestIP) {
|
|
|
|
dispatcher = freebind?.dispatcherFromIP(obj.requestIP, { strict: false })
|
|
|
|
}
|
|
|
|
|
2024-04-26 12:53:50 +01:00
|
|
|
const streamID = nanoid();
|
|
|
|
internalStreamCache[streamID] = {
|
2024-04-27 16:36:17 +01:00
|
|
|
url,
|
2024-04-27 11:48:22 +01:00
|
|
|
service: obj.service,
|
2024-05-12 17:35:52 +01:00
|
|
|
controller: new AbortController(),
|
|
|
|
dispatcher
|
2024-04-26 12:53:50 +01:00
|
|
|
};
|
|
|
|
|
2024-04-29 12:56:05 +01:00
|
|
|
let streamLink = new URL('/api/istream', `http://127.0.0.1:${env.apiPort}`);
|
2024-05-15 17:28:09 +01:00
|
|
|
streamLink.searchParams.set('id', streamID);
|
2024-04-26 12:53:50 +01:00
|
|
|
return streamLink.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function destroyInternalStream(url) {
|
2024-04-27 16:36:17 +01:00
|
|
|
url = new URL(url);
|
|
|
|
if (url.hostname !== '127.0.0.1') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-15 17:28:09 +01:00
|
|
|
const id = url.searchParams.get('id');
|
2024-04-26 12:53:50 +01:00
|
|
|
|
|
|
|
if (internalStreamCache[id]) {
|
|
|
|
internalStreamCache[id].controller.abort();
|
|
|
|
delete internalStreamCache[id];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-27 16:36:17 +01:00
|
|
|
function wrapStream(streamInfo) {
|
|
|
|
/* m3u8 links are currently not supported
|
|
|
|
* for internal streams, skip them */
|
|
|
|
if (M3U_SERVICES.includes(streamInfo.service)) {
|
|
|
|
return 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;
|
|
|
|
}
|
|
|
|
|
2024-03-05 12:14:26 +00:00
|
|
|
export function verifyStream(id, hmac, exp, secret, iv) {
|
2022-07-08 19:17:56 +01:00
|
|
|
try {
|
2024-03-05 14:55:17 +00:00
|
|
|
const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt);
|
2024-03-22 16:43:56 +00:00
|
|
|
const cache = streamCache.get(id.toString());
|
2024-03-05 12:14:26 +00:00
|
|
|
|
2024-05-15 17:28:09 +01:00
|
|
|
if (ghmac !== String(hmac)) return { status: 401 };
|
|
|
|
if (!cache) return { status: 404 };
|
2024-03-05 12:14:26 +00:00
|
|
|
|
2024-03-22 16:43:56 +00:00
|
|
|
const streamInfo = JSON.parse(decryptStream(cache, iv, secret));
|
2023-06-27 14:56:15 +01:00
|
|
|
|
2024-05-15 17:28:09 +01:00
|
|
|
if (!streamInfo) return { status: 404 };
|
2024-03-22 16:43:56 +00:00
|
|
|
|
|
|
|
if (Number(exp) <= new Date().getTime())
|
2024-05-15 17:28:09 +01:00
|
|
|
return { status: 404 };
|
2024-03-22 16:43:56 +00:00
|
|
|
|
2024-04-27 16:36:17 +01:00
|
|
|
return wrapStream(streamInfo);
|
2024-03-22 16:43:56 +00:00
|
|
|
}
|
2024-04-27 13:58:03 +01:00
|
|
|
catch {
|
2024-05-15 17:28:09 +01:00
|
|
|
return { status: 500 };
|
2022-07-08 19:17:56 +01:00
|
|
|
}
|
2022-08-01 16:48:37 +01:00
|
|
|
}
|