Skip to content

Commit a52dde7

Browse files
authored
merge: cobalt 11 with local processing & better performance (#1287)
2 parents 4b9644e + 3142b49 commit a52dde7

File tree

178 files changed

+6243
-2518
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

178 files changed

+6243
-2518
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ jobs:
2424
node-version: 'lts/*'
2525
- uses: pnpm/action-setup@v4
2626
- run: .github/test.sh web
27+
env:
28+
WEB_DEFAULT_API: ${{ vars.WEB_DEFAULT_API }}
2729

2830
test-api:
2931
name: api sanity check

api/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ as long as you:
7171

7272
## open source acknowledgements
7373
### ffmpeg
74-
cobalt relies on ffmpeg for muxing and encoding media files. ffmpeg is absolutely spectacular and we're privileged to have an ability to use it for free, just like anyone else. we believe it should be way more recognized.
74+
cobalt relies on ffmpeg for muxing and encoding media files. ffmpeg is absolutely spectacular and we're privileged to have the ability to use it for free, just like anyone else. we believe it should be way more recognized.
7575

7676
you can [support ffmpeg here](https://ffmpeg.org/donations.html)!
7777

api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@imput/cobalt-api",
33
"description": "save what you love",
4-
"version": "10.9.4",
4+
"version": "11.0",
55
"author": "imput",
66
"exports": "./src/cobalt.js",
77
"type": "module",
@@ -34,6 +34,7 @@
3434
"ffmpeg-static": "^5.1.0",
3535
"hls-parser": "^0.10.7",
3636
"ipaddr.js": "2.2.0",
37+
"mime": "^4.0.4",
3738
"nanoid": "^5.0.9",
3839
"set-cookie-parser": "2.6.0",
3940
"undici": "^5.19.1",

api/src/config.js

Lines changed: 15 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,32 @@
1-
import { Constants } from "youtubei.js";
21
import { getVersion } from "@imput/version-info";
3-
import { services } from "./processing/service-config.js";
4-
import { supportsReusePort } from "./misc/cluster.js";
2+
import { loadEnvs, validateEnvs, setupEnvWatcher } from "./core/env.js";
3+
import * as cluster from "./misc/cluster.js";
54

65
const version = await getVersion();
76

8-
const disabledServices = process.env.DISABLED_SERVICES?.split(',') || [];
9-
const enabledServices = new Set(Object.keys(services).filter(e => {
10-
if (!disabledServices.includes(e)) {
11-
return e;
12-
}
13-
}));
14-
15-
const env = {
16-
apiURL: process.env.API_URL || '',
17-
apiPort: process.env.API_PORT || 9000,
18-
tunnelPort: process.env.API_PORT || 9000,
19-
20-
listenAddress: process.env.API_LISTEN_ADDRESS,
21-
freebindCIDR: process.platform === 'linux' && process.env.FREEBIND_CIDR,
22-
23-
corsWildcard: process.env.CORS_WILDCARD !== '0',
24-
corsURL: process.env.CORS_URL,
25-
26-
cookiePath: process.env.COOKIE_PATH,
27-
28-
rateLimitWindow: (process.env.RATELIMIT_WINDOW && parseInt(process.env.RATELIMIT_WINDOW)) || 60,
29-
rateLimitMax: (process.env.RATELIMIT_MAX && parseInt(process.env.RATELIMIT_MAX)) || 20,
30-
31-
sessionRateLimitWindow: (process.env.SESSION_RATELIMIT_WINDOW && parseInt(process.env.SESSION_RATELIMIT_WINDOW)) || 60,
32-
sessionRateLimit: (process.env.SESSION_RATELIMIT && parseInt(process.env.SESSION_RATELIMIT)) || 10,
33-
34-
durationLimit: (process.env.DURATION_LIMIT && parseInt(process.env.DURATION_LIMIT)) || 10800,
35-
streamLifespan: (process.env.TUNNEL_LIFESPAN && parseInt(process.env.TUNNEL_LIFESPAN)) || 90,
36-
37-
processingPriority: process.platform !== 'win32'
38-
&& process.env.PROCESSING_PRIORITY
39-
&& parseInt(process.env.PROCESSING_PRIORITY),
40-
41-
externalProxy: process.env.API_EXTERNAL_PROXY,
42-
43-
turnstileSitekey: process.env.TURNSTILE_SITEKEY,
44-
turnstileSecret: process.env.TURNSTILE_SECRET,
45-
jwtSecret: process.env.JWT_SECRET,
46-
jwtLifetime: process.env.JWT_EXPIRY || 120,
47-
48-
sessionEnabled: process.env.TURNSTILE_SITEKEY
49-
&& process.env.TURNSTILE_SECRET
50-
&& process.env.JWT_SECRET,
51-
52-
apiKeyURL: process.env.API_KEY_URL && new URL(process.env.API_KEY_URL),
53-
authRequired: process.env.API_AUTH_REQUIRED === '1',
54-
redisURL: process.env.API_REDIS_URL,
55-
instanceCount: (process.env.API_INSTANCE_COUNT && parseInt(process.env.API_INSTANCE_COUNT)) || 1,
56-
keyReloadInterval: 900,
57-
58-
enabledServices,
59-
60-
customInnertubeClient: process.env.CUSTOM_INNERTUBE_CLIENT,
61-
ytSessionServer: process.env.YOUTUBE_SESSION_SERVER,
62-
ytSessionReloadInterval: 300,
63-
ytSessionInnertubeClient: process.env.YOUTUBE_SESSION_INNERTUBE_CLIENT,
64-
}
7+
const env = loadEnvs();
658

669
const genericUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
6710
const cobaltUserAgent = `cobalt/${version} (+https://github.com/imputnet/cobalt)`;
6811

12+
export const canonicalEnv = Object.freeze(structuredClone(process.env));
6913
export const setTunnelPort = (port) => env.tunnelPort = port;
7014
export const isCluster = env.instanceCount > 1;
15+
export const updateEnv = (newEnv) => {
16+
// tunnelPort is special and needs to get carried over here
17+
newEnv.tunnelPort = env.tunnelPort;
7118

72-
if (env.sessionEnabled && env.jwtSecret.length < 16) {
73-
throw new Error("JWT_SECRET env is too short (must be at least 16 characters long)");
74-
}
19+
for (const key in env) {
20+
env[key] = newEnv[key];
21+
}
7522

76-
if (env.instanceCount > 1 && !env.redisURL) {
77-
throw new Error("API_REDIS_URL is required when API_INSTANCE_COUNT is >= 2");
78-
} else if (env.instanceCount > 1 && !await supportsReusePort()) {
79-
console.error('API_INSTANCE_COUNT is not supported in your environment. to use this env, your node.js');
80-
console.error('version must be >= 23.1.0, and you must be running a recent enough version of linux');
81-
console.error('(or other OS that supports it). for more info, see `reusePort` option on');
82-
console.error('https://nodejs.org/api/net.html#serverlistenoptions-callback');
83-
throw new Error('SO_REUSEPORT is not supported');
23+
cluster.broadcast({ env_update: newEnv });
8424
}
8525

86-
if (env.customInnertubeClient && !Constants.SUPPORTED_CLIENTS.includes(env.customInnertubeClient)) {
87-
console.error("CUSTOM_INNERTUBE_CLIENT is invalid. Provided client is not supported.");
88-
console.error(`Supported clients are: ${Constants.SUPPORTED_CLIENTS.join(', ')}\n`);
89-
throw new Error("Invalid CUSTOM_INNERTUBE_CLIENT");
26+
await validateEnvs(env);
27+
28+
if (env.envFile) {
29+
setupEnvWatcher();
9030
}
9131

9232
export {

api/src/core/api.js

Lines changed: 42 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ import jwt from "../security/jwt.js";
88
import stream from "../stream/stream.js";
99
import match from "../processing/match.js";
1010

11-
import { env, isCluster, setTunnelPort } from "../config.js";
11+
import { env } from "../config.js";
1212
import { extract } from "../processing/url.js";
13-
import { Green, Bright, Cyan } from "../misc/console-text.js";
13+
import { Bright, Cyan } from "../misc/console-text.js";
1414
import { hashHmac } from "../security/secrets.js";
1515
import { createStore } from "../store/redis-ratelimit.js";
1616
import { randomizeCiphers } from "../misc/randomize-ciphers.js";
1717
import { verifyTurnstileToken } from "../security/turnstile.js";
1818
import { friendlyServiceName } from "../processing/service-alias.js";
19-
import { verifyStream, getInternalStream } from "../stream/manage.js";
19+
import { verifyStream } from "../stream/manage.js";
2020
import { createResponse, normalizeRequest, getIP } from "../processing/request.js";
21+
import { setupTunnelHandler } from "./itunnel.js";
2122

2223
import * as APIKeys from "../security/api-keys.js";
2324
import * as Cookies from "../processing/cookie/manager.js";
@@ -47,28 +48,31 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
4748
const startTime = new Date();
4849
const startTimestamp = startTime.getTime();
4950

50-
const serverInfo = JSON.stringify({
51-
cobalt: {
52-
version: version,
53-
url: env.apiURL,
54-
startTime: `${startTimestamp}`,
55-
durationLimit: env.durationLimit,
56-
turnstileSitekey: env.sessionEnabled ? env.turnstileSitekey : undefined,
57-
services: [...env.enabledServices].map(e => {
58-
return friendlyServiceName(e);
59-
}),
60-
},
61-
git,
62-
})
51+
const getServerInfo = () => {
52+
return JSON.stringify({
53+
cobalt: {
54+
version: version,
55+
url: env.apiURL,
56+
startTime: `${startTimestamp}`,
57+
turnstileSitekey: env.sessionEnabled ? env.turnstileSitekey : undefined,
58+
services: [...env.enabledServices].map(e => {
59+
return friendlyServiceName(e);
60+
}),
61+
},
62+
git,
63+
});
64+
}
65+
66+
const serverInfo = getServerInfo();
6367

6468
const handleRateExceeded = (_, res) => {
65-
const { status, body } = createResponse("error", {
69+
const { body } = createResponse("error", {
6670
code: "error.api.rate_exceeded",
6771
context: {
6872
limit: env.rateLimitWindow
6973
}
7074
});
71-
return res.status(status).json(body);
75+
return res.status(429).json(body);
7276
};
7377

7478
const keyGenerator = (req) => hashHmac(getIP(req), 'rate').toString('base64url');
@@ -94,14 +98,14 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
9498
});
9599

96100
const apiTunnelLimiter = rateLimit({
97-
windowMs: env.rateLimitWindow * 1000,
98-
limit: (req) => req.rateLimitMax || env.rateLimitMax,
101+
windowMs: env.tunnelRateLimitWindow * 1000,
102+
limit: env.tunnelRateLimitMax,
99103
standardHeaders: 'draft-6',
100104
legacyHeaders: false,
101-
keyGenerator: req => req.rateLimitKey || keyGenerator(req),
105+
keyGenerator: req => keyGenerator(req),
102106
store: await createStore('tunnel'),
103107
handler: (_, res) => {
104-
return res.sendStatus(429)
108+
return res.sendStatus(429);
105109
}
106110
});
107111

@@ -180,6 +184,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
180184
}
181185

182186
req.rateLimitKey = hashHmac(token, 'rate');
187+
req.isSession = true;
183188
} catch {
184189
return fail(res, "error.api.generic");
185190
}
@@ -244,6 +249,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
244249
if (!parsed) {
245250
return fail(res, "error.api.link.invalid");
246251
}
252+
247253
if ("error" in parsed) {
248254
let context;
249255
if (parsed?.context) {
@@ -257,13 +263,23 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
257263
host: parsed.host,
258264
patternMatch: parsed.patternMatch,
259265
params: normalizedRequest,
266+
isSession: req.isSession ?? false,
260267
});
261268

262269
res.status(result.status).json(result.body);
263270
} catch {
264271
fail(res, "error.api.generic");
265272
}
266-
})
273+
});
274+
275+
app.use('/tunnel', cors({
276+
methods: ['GET'],
277+
exposedHeaders: [
278+
'Estimated-Content-Length',
279+
'Content-Disposition'
280+
],
281+
...corsConfig,
282+
}));
267283

268284
app.get('/tunnel', apiTunnelLimiter, async (req, res) => {
269285
const id = String(req.query.id);
@@ -294,35 +310,11 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
294310
}
295311

296312
return stream(res, streamInfo);
297-
})
298-
299-
const itunnelHandler = (req, res) => {
300-
if (!req.ip.endsWith('127.0.0.1')) {
301-
return res.sendStatus(403);
302-
}
303-
304-
if (String(req.query.id).length !== 21) {
305-
return res.sendStatus(400);
306-
}
307-
308-
const streamInfo = getInternalStream(req.query.id);
309-
if (!streamInfo) {
310-
return res.sendStatus(404);
311-
}
312-
313-
streamInfo.headers = new Map([
314-
...(streamInfo.headers || []),
315-
...Object.entries(req.headers)
316-
]);
317-
318-
return stream(res, { type: 'internal', data: streamInfo });
319-
};
320-
321-
app.get('/itunnel', itunnelHandler);
313+
});
322314

323315
app.get('/', (_, res) => {
324316
res.type('json');
325-
res.status(200).send(serverInfo);
317+
res.status(200).send(env.envFile ? getServerInfo() : serverInfo);
326318
})
327319

328320
app.get('/favicon.ico', (req, res) => {
@@ -342,10 +334,6 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
342334
setInterval(randomizeCiphers, 1000 * 60 * 30); // shuffle ciphers every 30 minutes
343335

344336
if (env.externalProxy) {
345-
if (env.freebindCIDR) {
346-
throw new Error('Freebind is not available when external proxy is enabled')
347-
}
348-
349337
setGlobalDispatcher(new ProxyAgent(env.externalProxy))
350338
}
351339

@@ -384,17 +372,5 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => {
384372
}
385373
});
386374

387-
if (isCluster) {
388-
const istreamer = express();
389-
istreamer.get('/itunnel', itunnelHandler);
390-
const server = istreamer.listen({
391-
port: 0,
392-
host: '127.0.0.1',
393-
exclusive: true
394-
}, () => {
395-
const { port } = server.address();
396-
console.log(`${Green('[✓]')} cobalt sub-instance running on 127.0.0.1:${port}`);
397-
setTunnelPort(port);
398-
});
399-
}
375+
setupTunnelHandler();
400376
}

0 commit comments

Comments
 (0)