diff --git a/api/package.json b/api/package.json index 48f75d1..408be59 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.8.2", + "version": "10.8.4", "author": "imput", "exports": "./src/cobalt.js", "type": "module", @@ -38,7 +38,7 @@ "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", - "youtubei.js": "^13.2.0", + "youtubei.js": "^13.3.0", "zod": "^3.23.8" }, "optionalDependencies": { diff --git a/api/src/config.js b/api/src/config.js index 98da6fe..bb4994c 100644 --- a/api/src/config.js +++ b/api/src/config.js @@ -28,6 +28,9 @@ const env = { rateLimitWindow: (process.env.RATELIMIT_WINDOW && parseInt(process.env.RATELIMIT_WINDOW)) || 60, rateLimitMax: (process.env.RATELIMIT_MAX && parseInt(process.env.RATELIMIT_MAX)) || 20, + sessionRateLimitWindow: (process.env.SESSION_RATELIMIT_WINDOW && parseInt(process.env.SESSION_RATELIMIT_WINDOW)) || 60, + sessionRateLimit: (process.env.SESSION_RATELIMIT && parseInt(process.env.SESSION_RATELIMIT)) || 10, + durationLimit: (process.env.DURATION_LIMIT && parseInt(process.env.DURATION_LIMIT)) || 10800, streamLifespan: (process.env.TUNNEL_LIFESPAN && parseInt(process.env.TUNNEL_LIFESPAN)) || 90, diff --git a/api/src/core/api.js b/api/src/core/api.js index 868d44a..45c8f57 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -74,8 +74,8 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { const keyGenerator = (req) => hashHmac(getIP(req), 'rate').toString('base64url'); const sessionLimiter = rateLimit({ - windowMs: 60000, - limit: 10, + windowMs: env.sessionRateLimitWindow * 1000, + limit: env.sessionRateLimit, standardHeaders: 'draft-6', legacyHeaders: false, keyGenerator, @@ -91,7 +91,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { keyGenerator: req => req.rateLimitKey || keyGenerator(req), store: await createStore('api'), handler: handleRateExceeded - }) + }); const apiTunnelLimiter = rateLimit({ windowMs: env.rateLimitWindow * 1000, @@ -103,7 +103,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { handler: (_, res) => { return res.sendStatus(429) } - }) + }); app.set('trust proxy', ['loopback', 'uniquelocal']); @@ -175,7 +175,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { return fail(res, "error.api.auth.jwt.invalid"); } - if (!jwt.verify(token)) { + if (!jwt.verify(token, getIP(req, 32))) { return fail(res, "error.api.auth.jwt.invalid"); } @@ -221,7 +221,7 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { } try { - res.json(jwt.generate()); + res.json(jwt.generate(getIP(req, 32))); } catch { return fail(res, "error.api.generic"); } diff --git a/api/src/processing/helpers/youtube-session.js b/api/src/processing/helpers/youtube-session.js index 5235c42..85f1a6e 100644 --- a/api/src/processing/helpers/youtube-session.js +++ b/api/src/processing/helpers/youtube-session.js @@ -1,8 +1,11 @@ import * as cluster from "../../misc/cluster.js"; +import { Agent } from "undici"; import { env } from "../../config.js"; import { Green, Yellow } from "../../misc/console-text.js"; +const defaultAgent = new Agent(); + let session; const validateSession = (sessionResponse) => { @@ -32,7 +35,11 @@ const loadSession = async () => { const sessionServerUrl = new URL(env.ytSessionServer); sessionServerUrl.pathname = "/token"; - const newSession = await fetch(sessionServerUrl).then(a => a.json()); + const newSession = await fetch( + sessionServerUrl, + { dispatcher: defaultAgent } + ).then(a => a.json()); + validateSession(newSession); if (!session || session.updated < newSession?.updated) { diff --git a/api/src/processing/request.js b/api/src/processing/request.js index d512bfe..61bf027 100644 --- a/api/src/processing/request.js +++ b/api/src/processing/request.js @@ -82,14 +82,13 @@ export function normalizeRequest(request) { )); } -export function getIP(req) { +export function getIP(req, prefix = 56) { const strippedIP = req.ip.replace(/^::ffff:/, ''); const ip = ipaddr.parse(strippedIP); if (ip.kind() === 'ipv4') { return strippedIP; } - const prefix = 56; const v6Bytes = ip.toByteArray(); v6Bytes.fill(0, prefix / 8); diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 1dc8bf3..00fa4eb 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -136,12 +136,13 @@ export const services = { tiktok: { patterns: [ ":user/video/:postId", + "i18n/share/video/:postId", ":shortLink", "t/:shortLink", ":user/photo/:postId", "v/:postId.html" ], - subdomains: ["vt", "vm", "m"], + subdomains: ["vt", "vm", "m", "t"], }, tumblr: { patterns: [ diff --git a/api/src/processing/services/tiktok.js b/api/src/processing/services/tiktok.js index 6fec01d..93e07c5 100644 --- a/api/src/processing/services/tiktok.js +++ b/api/src/processing/services/tiktok.js @@ -1,6 +1,6 @@ import Cookie from "../cookie/cookie.js"; -import { extract } from "../url.js"; +import { extract, normalizeURL } from "../url.js"; import { genericUserAgent } from "../../config.js"; import { updateCookie } from "../cookie/manager.js"; import { createStream } from "../../stream/manage.js"; @@ -23,8 +23,8 @@ export default async function(obj) { if (html.startsWith(' Buffer.from(b).toString("base64url"); const fromBase64URL = (b) => Buffer.from(b, "base64url").toString(); -const makeHmac = (header, payload) => - createHmac("sha256", env.jwtSecret) - .update(`${header}.${payload}`) - .digest("base64url"); +const makeHmac = (data) => { + return createHmac("sha256", env.jwtSecret) + .update(data) + .digest("base64url"); +} + +const sign = (header, payload) => + makeHmac(`${header}.${payload}`); + +const getIPHash = (ip) => + makeHmac(ip).slice(0, 8); -const generate = () => { +const generate = (ip) => { const exp = Math.floor(new Date().getTime() / 1000) + env.jwtLifetime; const header = toBase64URL(JSON.stringify({ @@ -21,10 +28,11 @@ const generate = () => { const payload = toBase64URL(JSON.stringify({ jti: nanoid(8), + sub: getIPHash(ip), exp, })); - const signature = makeHmac(header, payload); + const signature = sign(header, payload); return { token: `${header}.${payload}.${signature}`, @@ -32,7 +40,7 @@ const generate = () => { }; } -const verify = (jwt) => { +const verify = (jwt, ip) => { const [header, payload, signature] = jwt.split(".", 3); const timestamp = Math.floor(new Date().getTime() / 1000); @@ -40,17 +48,16 @@ const verify = (jwt) => { return false; } - const verifySignature = makeHmac(header, payload); + const verifySignature = sign(header, payload); if (verifySignature !== signature) { return false; } - if (timestamp >= JSON.parse(fromBase64URL(payload)).exp) { - return false; - } + const data = JSON.parse(fromBase64URL(payload)); - return true; + return getIPHash(ip) === data.sub + && timestamp <= data.exp; } export default {