mirror of
https://github.com/wukko/cobalt.git
synced 2024-11-06 00:10:02 +00:00
stream: encrypt cached stream data (#379)
This commit is contained in:
commit
4d1ac4a00a
4 changed files with 92 additions and 41 deletions
|
@ -10,7 +10,7 @@ import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/util
|
|||
import { Bright, Cyan } from "../modules/sub/consoleText.js";
|
||||
import stream from "../modules/stream/stream.js";
|
||||
import loc from "../localization/manager.js";
|
||||
import { sha256 } from "../modules/sub/crypto.js";
|
||||
import { generateHmac } from "../modules/sub/crypto.js";
|
||||
import { verifyStream } from "../modules/stream/manage.js";
|
||||
|
||||
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
||||
|
@ -24,7 +24,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
max: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: req => sha256(getIP(req), ipSalt),
|
||||
keyGenerator: req => generateHmac(getIP(req), ipSalt),
|
||||
handler: (req, res, next, opt) => {
|
||||
return res.status(429).json({
|
||||
"status": "rate-limit",
|
||||
|
@ -37,7 +37,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
max: 25,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: req => sha256(getIP(req), ipSalt),
|
||||
keyGenerator: req => generateHmac(getIP(req), ipSalt),
|
||||
handler: (req, res, next, opt) => {
|
||||
return res.status(429).json({
|
||||
"status": "rate-limit",
|
||||
|
@ -47,11 +47,15 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
});
|
||||
|
||||
const startTime = new Date();
|
||||
const startTimestamp = Math.floor(startTime.getTime());
|
||||
const startTimestamp = startTime.getTime();
|
||||
|
||||
app.set('trust proxy', ['loopback', 'uniquelocal']);
|
||||
|
||||
app.use('/api/:type', cors(corsConfig));
|
||||
app.use('/api/:type', cors({
|
||||
methods: ['GET', 'POST'],
|
||||
...corsConfig
|
||||
}));
|
||||
|
||||
app.use('/api/json', apiLimiter);
|
||||
app.use('/api/stream', apiLimiterStream);
|
||||
app.use('/api/onDemand', apiLimiter);
|
||||
|
@ -60,6 +64,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
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";
|
||||
|
@ -71,6 +76,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// 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";
|
||||
|
@ -86,6 +92,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/json', async (req, res) => {
|
||||
try {
|
||||
let lang = languageCode(req);
|
||||
|
@ -118,13 +125,17 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
try {
|
||||
switch (req.params.type) {
|
||||
case 'stream':
|
||||
if (req.query.t && req.query.h && req.query.e && req.query.t.toString().length === 21
|
||||
&& req.query.h.toString().length === 64 && req.query.e.toString().length === 13) {
|
||||
let streamInfo = verifyStream(req.query.t, req.query.h, req.query.e);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
if (req.query.p) {
|
||||
if (q.p) {
|
||||
return res.status(200).json({
|
||||
status: "continue"
|
||||
});
|
||||
|
@ -132,7 +143,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
return stream(res, streamInfo);
|
||||
} else {
|
||||
let j = apiJSON(0, {
|
||||
t: "stream token, hmac, or expiry timestamp is missing"
|
||||
t: "bad request. stream link may be incomplete or corrupted."
|
||||
})
|
||||
return res.status(j.status).json(j.body);
|
||||
}
|
||||
|
@ -159,12 +170,15 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
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')
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import NodeCache from "node-cache";
|
|||
import { randomBytes } from "crypto";
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { sha256 } from "../sub/crypto.js";
|
||||
import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
|
||||
import { streamLifespan } from "../config.js";
|
||||
|
||||
const streamCache = new NodeCache({
|
||||
|
@ -15,48 +15,68 @@ streamCache.on("expired", (key) => {
|
|||
streamCache.del(key);
|
||||
})
|
||||
|
||||
const streamSalt = randomBytes(64).toString('hex');
|
||||
const hmacSalt = randomBytes(64).toString('hex');
|
||||
|
||||
export function createStream(obj) {
|
||||
let streamID = nanoid(),
|
||||
exp = Math.floor(new Date().getTime()) + streamLifespan,
|
||||
ghmac = sha256(`${streamID},${obj.service},${exp}`, streamSalt);
|
||||
|
||||
if (!streamCache.has(streamID)) {
|
||||
streamCache.set(streamID, {
|
||||
id: streamID,
|
||||
service: obj.service,
|
||||
const streamID = nanoid(),
|
||||
iv = randomBytes(16).toString('base64url'),
|
||||
secret = randomBytes(32).toString('base64url'),
|
||||
exp = new Date().getTime() + streamLifespan,
|
||||
hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt),
|
||||
streamData = {
|
||||
exp: exp,
|
||||
type: obj.type,
|
||||
urls: obj.u,
|
||||
service: obj.service,
|
||||
filename: obj.filename,
|
||||
hmac: ghmac,
|
||||
exp: exp,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
audioFormat: obj.audioFormat,
|
||||
time: obj.time ? obj.time : false,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
copy: !!obj.copy,
|
||||
mute: !!obj.mute,
|
||||
metadata: obj.fileMetadata ? obj.fileMetadata : false
|
||||
});
|
||||
} else {
|
||||
let streamInfo = streamCache.get(streamID);
|
||||
exp = streamInfo.exp;
|
||||
ghmac = streamInfo.hmac;
|
||||
metadata: obj.fileMetadata || false
|
||||
};
|
||||
|
||||
streamCache.set(
|
||||
streamID,
|
||||
encryptStream(streamData, iv, secret)
|
||||
)
|
||||
|
||||
let streamLink = new URL('/api/stream', process.env.API_URL);
|
||||
|
||||
const params = {
|
||||
't': streamID,
|
||||
'e': exp,
|
||||
'h': hmac,
|
||||
's': secret,
|
||||
'i': iv
|
||||
}
|
||||
return `${process.env.API_URL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
streamLink.searchParams.append(key, value);
|
||||
}
|
||||
|
||||
return streamLink.toString();
|
||||
}
|
||||
|
||||
export function verifyStream(id, hmac, exp) {
|
||||
export function verifyStream(id, hmac, exp, secret, iv) {
|
||||
try {
|
||||
let streamInfo = streamCache.get(id.toString());
|
||||
const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt);
|
||||
|
||||
if (ghmac !== String(hmac)) {
|
||||
return {
|
||||
error: "i couldn't verify if you have access to this stream. go back and try again!",
|
||||
status: 401
|
||||
}
|
||||
}
|
||||
|
||||
const streamInfo = JSON.parse(decryptStream(streamCache.get(id.toString()), iv, secret));
|
||||
|
||||
if (!streamInfo) return {
|
||||
error: "this download link has expired or doesn't exist. go back and try again!",
|
||||
status: 400
|
||||
}
|
||||
|
||||
let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt);
|
||||
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
|
||||
&& Number(exp) > Math.floor(new Date().getTime())) {
|
||||
if (String(exp) === String(streamInfo.exp) && Number(exp) > new Date().getTime()) {
|
||||
return streamInfo;
|
||||
}
|
||||
return {
|
||||
|
@ -64,6 +84,6 @@ export function verifyStream(id, hmac, exp) {
|
|||
status: 401
|
||||
}
|
||||
} catch (e) {
|
||||
return { status: 500, body: { status: "error", text: "Internal Server Error" } };
|
||||
return { status: 500, body: { status: "error", text: "couldn't verify this stream. request a new one!" } };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
import { createHmac } from "crypto";
|
||||
import { createHmac, createCipheriv, createDecipheriv, scryptSync } from "crypto";
|
||||
|
||||
export function sha256(str, salt) {
|
||||
return createHmac("sha256", salt).update(str).digest("hex");
|
||||
const algorithm = "aes256"
|
||||
|
||||
export function generateHmac(str, salt) {
|
||||
return createHmac("sha256", salt).update(str).digest("base64url");
|
||||
}
|
||||
|
||||
export function encryptStream(plaintext, iv, secret) {
|
||||
const buff = Buffer.from(JSON.stringify(plaintext));
|
||||
const key = Buffer.from(secret, "base64url");
|
||||
const cipher = createCipheriv(algorithm, key, Buffer.from(iv, "base64url"));
|
||||
|
||||
return Buffer.concat([ cipher.update(buff), cipher.final() ])
|
||||
}
|
||||
|
||||
export function decryptStream(ciphertext, iv, secret) {
|
||||
const buff = Buffer.from(ciphertext);
|
||||
const key = Buffer.from(secret, "base64url");
|
||||
const decipher = createDecipheriv(algorithm, key, Buffer.from(iv, "base64url"));
|
||||
|
||||
return Buffer.concat([ decipher.update(buff), decipher.final() ])
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ const apiVar = {
|
|||
},
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
|
||||
}
|
||||
const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
|
||||
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
||||
|
||||
export function apiJSON(type, obj) {
|
||||
|
|
Loading…
Reference in a new issue