2023-05-19 12:13:38 +02:00
|
|
|
import cors from "cors";
|
|
|
|
import rateLimit from "express-rate-limit";
|
2024-07-24 17:49:47 +02:00
|
|
|
import { setGlobalDispatcher, ProxyAgent } from "undici";
|
2024-08-03 17:34:02 +02:00
|
|
|
import { getCommit, getBranch, getRemote, getVersion } from "@imput/version-info";
|
|
|
|
|
2024-08-22 19:04:11 +02:00
|
|
|
import jwt from "../security/jwt.js";
|
|
|
|
import stream from "../stream/stream.js";
|
|
|
|
import match from "../processing/match.js";
|
2024-05-16 10:20:40 +02:00
|
|
|
|
2024-08-22 19:04:11 +02:00
|
|
|
import { env } from "../config.js";
|
|
|
|
import { extract } from "../processing/url.js";
|
2024-08-03 10:47:13 +02:00
|
|
|
import { languageCode } from "../misc/utils.js";
|
2024-08-22 19:04:11 +02:00
|
|
|
import { Bright, Cyan } from "../misc/console-text.js";
|
|
|
|
import { generateHmac, generateSalt } from "../misc/crypto.js";
|
2024-08-15 20:10:17 +02:00
|
|
|
import { randomizeCiphers } from "../misc/randomize-ciphers.js";
|
2024-08-16 19:28:03 +02:00
|
|
|
import { verifyTurnstileToken } from "../security/turnstile.js";
|
2024-08-22 19:04:11 +02:00
|
|
|
import { verifyStream, getInternalStream } from "../stream/manage.js";
|
|
|
|
import { createResponse, normalizeRequest, getIP } from "../processing/request.js";
|
2024-05-16 10:20:40 +02:00
|
|
|
|
2024-08-03 17:34:02 +02:00
|
|
|
const git = {
|
|
|
|
branch: await getBranch(),
|
|
|
|
commit: await getCommit(),
|
|
|
|
remote: await getRemote(),
|
|
|
|
}
|
|
|
|
|
|
|
|
const version = await getVersion();
|
|
|
|
|
2024-05-16 10:20:40 +02:00
|
|
|
const acceptRegex = /^application\/json(; charset=utf-8)?$/;
|
|
|
|
|
|
|
|
const ipSalt = generateSalt();
|
|
|
|
const corsConfig = env.corsWildcard ? {} : {
|
|
|
|
origin: env.corsURL,
|
|
|
|
optionsSuccessStatus: 200
|
|
|
|
}
|
2023-05-19 12:13:38 +02:00
|
|
|
|
2024-08-24 12:13:42 +02:00
|
|
|
const fail = (res, code, context) => {
|
|
|
|
const { status, body } = createResponse("error", { code, context });
|
2024-08-16 20:55:26 +02:00
|
|
|
res.status(status).json(body);
|
|
|
|
}
|
|
|
|
|
2024-08-22 19:04:11 +02:00
|
|
|
export const runAPI = (express, app, __dirname) => {
|
2024-05-16 10:20:40 +02:00
|
|
|
const startTime = new Date();
|
|
|
|
const startTimestamp = startTime.getTime();
|
2024-07-24 17:27:26 +02:00
|
|
|
|
2024-08-22 20:33:52 +02:00
|
|
|
const serverInfo = JSON.stringify({
|
|
|
|
cobalt: {
|
|
|
|
version: version,
|
|
|
|
url: env.apiURL,
|
|
|
|
startTime: `${startTimestamp}`,
|
|
|
|
durationLimit: env.durationLimit,
|
|
|
|
services: [...env.enabledServices],
|
|
|
|
},
|
2024-08-03 17:34:02 +02:00
|
|
|
git,
|
2024-08-22 20:33:52 +02:00
|
|
|
})
|
2023-05-19 12:13:38 +02:00
|
|
|
|
|
|
|
const apiLimiter = rateLimit({
|
2024-05-16 09:54:11 +02:00
|
|
|
windowMs: env.rateLimitWindow * 1000,
|
2024-05-16 09:58:28 +02:00
|
|
|
max: env.rateLimitMax,
|
2023-08-04 20:43:12 +02:00
|
|
|
standardHeaders: true,
|
2023-05-19 12:13:38 +02:00
|
|
|
legacyHeaders: false,
|
2024-08-16 20:55:26 +02:00
|
|
|
keyGenerator: req => {
|
|
|
|
if (req.authorized) {
|
|
|
|
return generateHmac(req.header("Authorization"), ipSalt);
|
|
|
|
}
|
|
|
|
return generateHmac(getIP(req), ipSalt);
|
|
|
|
},
|
2024-05-14 09:08:36 +02:00
|
|
|
handler: (req, res) => {
|
2024-08-06 16:45:04 +02:00
|
|
|
const { status, body } = createResponse("error", {
|
2024-08-19 17:51:45 +02:00
|
|
|
code: "error.api.rate_exceeded",
|
2024-08-06 16:45:04 +02:00
|
|
|
context: {
|
|
|
|
limit: env.rateLimitWindow
|
2024-08-03 09:51:09 +02:00
|
|
|
}
|
2023-08-04 20:43:12 +02:00
|
|
|
});
|
2024-08-06 16:45:04 +02:00
|
|
|
return res.status(status).json(body);
|
2023-05-19 12:13:38 +02:00
|
|
|
}
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
|
|
|
|
2023-05-19 12:13:38 +02:00
|
|
|
const apiLimiterStream = rateLimit({
|
2024-05-16 09:54:11 +02:00
|
|
|
windowMs: env.rateLimitWindow * 1000,
|
2024-05-16 09:58:28 +02:00
|
|
|
max: env.rateLimitMax,
|
2023-08-04 20:43:12 +02:00
|
|
|
standardHeaders: true,
|
2023-05-19 12:13:38 +02:00
|
|
|
legacyHeaders: false,
|
2024-03-05 15:55:17 +01:00
|
|
|
keyGenerator: req => generateHmac(getIP(req), ipSalt),
|
2024-05-14 09:08:36 +02:00
|
|
|
handler: (req, res) => {
|
2024-05-15 18:28:09 +02:00
|
|
|
return res.sendStatus(429)
|
2023-05-19 12:13:38 +02:00
|
|
|
}
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
2023-05-19 12:13:38 +02:00
|
|
|
|
2023-07-25 21:46:25 +02:00
|
|
|
app.set('trust proxy', ['loopback', 'uniquelocal']);
|
|
|
|
|
2024-08-03 17:51:05 +02:00
|
|
|
app.use('/', cors({
|
2024-03-05 13:14:26 +01:00
|
|
|
methods: ['GET', 'POST'],
|
2024-05-16 09:59:53 +02:00
|
|
|
exposedHeaders: [
|
|
|
|
'Ratelimit-Limit',
|
|
|
|
'Ratelimit-Policy',
|
|
|
|
'Ratelimit-Remaining',
|
|
|
|
'Ratelimit-Reset'
|
|
|
|
],
|
|
|
|
...corsConfig,
|
2024-08-16 20:55:26 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
app.post('/', (req, res, next) => {
|
2024-08-16 20:59:59 +02:00
|
|
|
if (!env.turnstileSecret || !env.jwtSecret) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2024-08-16 20:55:26 +02:00
|
|
|
try {
|
2024-08-16 20:59:59 +02:00
|
|
|
const authorization = req.header("Authorization");
|
|
|
|
if (!authorization) {
|
|
|
|
return fail(res, "error.api.auth.jwt.missing");
|
|
|
|
}
|
2024-08-16 20:55:26 +02:00
|
|
|
|
2024-08-16 20:59:59 +02:00
|
|
|
if (!authorization.startsWith("Bearer ") || authorization.length > 256) {
|
|
|
|
return fail(res, "error.api.auth.jwt.invalid");
|
|
|
|
}
|
2024-08-16 20:55:26 +02:00
|
|
|
|
2024-08-16 20:59:59 +02:00
|
|
|
const verifyJwt = jwt.verify(
|
|
|
|
authorization.split("Bearer ", 2)[1]
|
|
|
|
);
|
2024-08-16 20:55:26 +02:00
|
|
|
|
2024-08-16 20:59:59 +02:00
|
|
|
if (!verifyJwt) {
|
|
|
|
return fail(res, "error.api.auth.jwt.invalid");
|
|
|
|
}
|
2024-08-16 20:55:26 +02:00
|
|
|
|
2024-08-16 20:59:59 +02:00
|
|
|
if (!acceptRegex.test(req.header('Accept'))) {
|
2024-08-19 17:51:45 +02:00
|
|
|
return fail(res, "error.api.header.accept");
|
2024-08-16 20:59:59 +02:00
|
|
|
}
|
2024-08-16 20:55:26 +02:00
|
|
|
|
2024-08-16 20:59:59 +02:00
|
|
|
if (!acceptRegex.test(req.header('Content-Type'))) {
|
2024-08-19 17:51:45 +02:00
|
|
|
return fail(res, "error.api.header.content_type");
|
2024-08-16 20:55:26 +02:00
|
|
|
}
|
2024-08-16 20:59:59 +02:00
|
|
|
|
|
|
|
req.authorized = true;
|
2024-08-16 20:55:26 +02:00
|
|
|
} catch {
|
|
|
|
return fail(res, "error.api.generic");
|
|
|
|
}
|
2024-08-16 20:59:59 +02:00
|
|
|
next();
|
2024-08-16 20:55:26 +02:00
|
|
|
});
|
2024-03-05 13:14:26 +01:00
|
|
|
|
2024-08-16 20:55:26 +02:00
|
|
|
app.post('/', apiLimiter);
|
2024-08-03 17:51:05 +02:00
|
|
|
app.use('/stream', apiLimiterStream);
|
2023-05-19 12:13:38 +02:00
|
|
|
|
2024-08-03 17:51:05 +02:00
|
|
|
app.use('/', express.json({ limit: 1024 }));
|
2024-08-16 19:41:20 +02:00
|
|
|
app.use('/', (err, _, res, next) => {
|
2024-07-24 17:27:26 +02:00
|
|
|
if (err) {
|
2024-08-06 16:45:04 +02:00
|
|
|
const { status, body } = createResponse("error", {
|
2024-08-19 17:51:45 +02:00
|
|
|
code: "error.api.invalid_body",
|
2023-08-04 20:43:12 +02:00
|
|
|
});
|
2024-08-06 16:45:04 +02:00
|
|
|
return res.status(status).json(body);
|
2023-08-04 20:43:12 +02:00
|
|
|
}
|
2024-07-24 17:27:26 +02:00
|
|
|
|
|
|
|
next();
|
|
|
|
});
|
2024-03-05 13:14:26 +01:00
|
|
|
|
2024-08-16 19:28:03 +02:00
|
|
|
app.post("/session", async (req, res) => {
|
|
|
|
if (!env.turnstileSecret || !env.jwtSecret) {
|
2024-08-16 20:55:26 +02:00
|
|
|
return fail(res, "error.api.auth.not_configured")
|
2024-08-16 19:28:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const turnstileResponse = req.header("cf-turnstile-response");
|
|
|
|
|
|
|
|
if (!turnstileResponse) {
|
2024-08-16 20:55:26 +02:00
|
|
|
return fail(res, "error.api.auth.turnstile.missing");
|
2024-08-16 19:28:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const turnstileResult = await verifyTurnstileToken(
|
|
|
|
turnstileResponse,
|
|
|
|
req.ip
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!turnstileResult) {
|
2024-08-16 20:55:26 +02:00
|
|
|
return fail(res, "error.api.auth.turnstile.invalid");
|
2024-08-16 19:28:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
res.json(jwt.generate());
|
|
|
|
} catch {
|
2024-08-16 20:55:26 +02:00
|
|
|
return fail(res, "error.api.generic");
|
2024-08-16 19:28:03 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-08-03 17:51:05 +02:00
|
|
|
app.post('/', async (req, res) => {
|
2024-05-15 15:29:18 +02:00
|
|
|
const request = req.body;
|
|
|
|
const lang = languageCode(req);
|
2024-05-15 18:28:09 +02:00
|
|
|
|
2024-05-15 15:29:18 +02:00
|
|
|
if (!request.url) {
|
2024-08-19 17:51:45 +02:00
|
|
|
return fail(res, "error.api.link.missing");
|
2024-05-15 15:29:18 +02:00
|
|
|
}
|
|
|
|
|
2024-08-03 19:06:32 +02:00
|
|
|
if (request.youtubeDubBrowserLang) {
|
|
|
|
request.youtubeDubLang = lang;
|
|
|
|
}
|
|
|
|
|
2024-08-08 18:34:54 +02:00
|
|
|
const { success, data: normalizedRequest } = await normalizeRequest(request);
|
|
|
|
if (!success) {
|
2024-08-19 17:51:45 +02:00
|
|
|
return fail(res, "error.api.invalid_body");
|
2024-05-15 15:29:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const parsed = extract(normalizedRequest.url);
|
2024-08-24 12:13:42 +02:00
|
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
return fail(res, "error.api.link.invalid");
|
|
|
|
}
|
2024-08-22 20:16:26 +02:00
|
|
|
if ("error" in parsed) {
|
2024-08-24 12:13:42 +02:00
|
|
|
let context;
|
|
|
|
if (parsed?.context) {
|
|
|
|
context = parsed.context;
|
|
|
|
}
|
|
|
|
return fail(res, `error.api.${parsed.error}`, context);
|
2024-05-15 15:29:18 +02:00
|
|
|
}
|
|
|
|
|
2023-05-19 12:13:38 +02:00
|
|
|
try {
|
2024-05-15 15:29:18 +02:00
|
|
|
const result = await match(
|
2024-08-03 19:06:32 +02:00
|
|
|
parsed.host, parsed.patternMatch, normalizedRequest
|
2024-05-15 15:29:18 +02:00
|
|
|
);
|
2024-05-15 14:45:23 +02:00
|
|
|
|
2024-05-15 15:29:18 +02:00
|
|
|
res.status(result.status).json(result.body);
|
|
|
|
} catch {
|
2024-08-19 17:51:45 +02:00
|
|
|
fail(res, "error.api.generic");
|
2023-05-19 12:13:38 +02:00
|
|
|
}
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
|
|
|
|
2024-08-03 17:51:05 +02:00
|
|
|
app.get('/stream', (req, res) => {
|
2024-05-15 18:28:09 +02:00
|
|
|
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);
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2024-05-22 18:45:32 +02:00
|
|
|
if (!checkQueries || !checkBaseLength || !checkSafeLength) {
|
2024-08-03 19:06:32 +02:00
|
|
|
return res.status(400).end();
|
2024-05-15 18:28:09 +02:00
|
|
|
}
|
2024-05-22 18:45:32 +02:00
|
|
|
|
|
|
|
if (req.query.p) {
|
2024-08-03 19:06:32 +02:00
|
|
|
return res.status(200).end();
|
2024-05-22 18:45:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const streamInfo = verifyStream(id, sig, exp, sec, iv);
|
|
|
|
if (!streamInfo?.service) {
|
2024-08-03 19:06:32 +02:00
|
|
|
return res.status(streamInfo.status).end();
|
2024-05-22 18:45:32 +02:00
|
|
|
}
|
|
|
|
return stream(res, streamInfo);
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
2023-05-19 12:13:38 +02:00
|
|
|
|
2024-08-03 17:51:05 +02:00
|
|
|
app.get('/istream', (req, res) => {
|
2024-05-22 18:45:32 +02:00
|
|
|
if (!req.ip.endsWith('127.0.0.1')) {
|
|
|
|
return res.sendStatus(403);
|
2024-05-15 18:28:09 +02:00
|
|
|
}
|
|
|
|
|
2024-05-22 18:45:32 +02:00
|
|
|
if (String(req.query.id).length !== 21) {
|
|
|
|
return res.sendStatus(400);
|
|
|
|
}
|
|
|
|
|
|
|
|
const streamInfo = getInternalStream(req.query.id);
|
|
|
|
if (!streamInfo) {
|
|
|
|
return res.sendStatus(404);
|
2023-05-19 12:13:38 +02:00
|
|
|
}
|
2024-05-22 18:45:32 +02:00
|
|
|
|
2024-07-06 13:36:25 +02:00
|
|
|
streamInfo.headers = new Map([
|
2024-07-06 13:40:41 +02:00
|
|
|
...(streamInfo.headers || []),
|
|
|
|
...Object.entries(req.headers)
|
2024-07-06 13:36:25 +02:00
|
|
|
]);
|
2024-05-22 18:45:32 +02:00
|
|
|
|
|
|
|
return stream(res, { type: 'internal', ...streamInfo });
|
|
|
|
})
|
|
|
|
|
2024-08-03 17:51:05 +02:00
|
|
|
app.get('/', (_, res) => {
|
2024-08-22 20:33:52 +02:00
|
|
|
res.type('json');
|
|
|
|
res.status(200).send(serverInfo);
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
2024-03-05 13:14:26 +01:00
|
|
|
|
2023-05-21 21:13:05 +02:00
|
|
|
app.get('/favicon.ico', (req, res) => {
|
2024-08-02 18:35:49 +02:00
|
|
|
res.status(404).end();
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
2024-03-05 13:14:26 +01:00
|
|
|
|
2023-05-21 21:13:05 +02:00
|
|
|
app.get('/*', (req, res) => {
|
2024-08-03 17:51:05 +02:00
|
|
|
res.redirect('/');
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
2023-05-19 12:13:38 +02:00
|
|
|
|
2024-08-30 10:25:46 +02:00
|
|
|
// handle all express errors
|
|
|
|
app.use((err, req, res, next) => {
|
|
|
|
return fail(res, "error.api.generic");
|
|
|
|
})
|
|
|
|
|
2024-06-15 18:20:33 +02:00
|
|
|
randomizeCiphers();
|
|
|
|
setInterval(randomizeCiphers, 1000 * 60 * 30); // shuffle ciphers every 30 minutes
|
|
|
|
|
2024-07-24 17:49:47 +02:00
|
|
|
if (env.externalProxy) {
|
|
|
|
if (env.freebindCIDR) {
|
|
|
|
throw new Error('Freebind is not available when external proxy is enabled')
|
|
|
|
}
|
|
|
|
|
|
|
|
setGlobalDispatcher(new ProxyAgent(env.externalProxy))
|
|
|
|
}
|
|
|
|
|
2024-05-13 20:20:10 +02:00
|
|
|
app.listen(env.apiPort, env.listenAddress, () => {
|
2023-08-04 20:43:12 +02:00
|
|
|
console.log(`\n` +
|
2024-08-03 17:34:02 +02:00
|
|
|
Bright(Cyan("cobalt ")) + Bright("API ^_^") + "\n" +
|
|
|
|
|
|
|
|
"~~~~~~\n" +
|
|
|
|
Bright("version: ") + version + "\n" +
|
|
|
|
Bright("commit: ") + git.commit + "\n" +
|
|
|
|
Bright("branch: ") + git.branch + "\n" +
|
|
|
|
Bright("remote: ") + git.remote + "\n" +
|
|
|
|
Bright("start time: ") + startTime.toUTCString() + "\n" +
|
|
|
|
"~~~~~~\n" +
|
|
|
|
|
|
|
|
Bright("url: ") + Bright(Cyan(env.apiURL)) + "\n" +
|
|
|
|
Bright("port: ") + env.apiPort + "\n"
|
2023-08-04 20:43:12 +02:00
|
|
|
)
|
2024-05-15 18:28:09 +02:00
|
|
|
})
|
2023-05-19 12:13:38 +02:00
|
|
|
}
|