diff --git a/api/src/core/api.js b/api/src/core/api.js index c8ff6fbf..0487ad7a 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -245,7 +245,10 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { return fail(res, "error.api.invalid_body"); } - const parsed = extract(normalizedRequest.url); + const parsed = extract( + normalizedRequest.url, + APIKeys.getAllowedServices(req.rateLimitKey), + ); if (!parsed) { return fail(res, "error.api.link.invalid"); diff --git a/api/src/core/env.js b/api/src/core/env.js index a6623ca0..f7600f65 100644 --- a/api/src/core/env.js +++ b/api/src/core/env.js @@ -11,6 +11,7 @@ const forceLocalProcessingOptions = ["never", "session", "always"]; const youtubeHlsOptions = ["never", "key", "always"]; export const loadEnvs = (env = process.env) => { + const allServices = new Set(Object.keys(services)); const disabledServices = env.DISABLED_SERVICES?.split(',') || []; const enabledServices = new Set(Object.keys(services).filter(e => { if (!disabledServices.includes(e)) { @@ -64,6 +65,7 @@ export const loadEnvs = (env = process.env) => { instanceCount: (env.API_INSTANCE_COUNT && parseInt(env.API_INSTANCE_COUNT)) || 1, keyReloadInterval: 900, + allServices, enabledServices, customInnertubeClient: env.CUSTOM_INNERTUBE_CLIENT, diff --git a/api/src/processing/url.js b/api/src/processing/url.js index 5bb690e6..dbbda1cd 100644 --- a/api/src/processing/url.js +++ b/api/src/processing/url.js @@ -199,7 +199,7 @@ export function normalizeURL(url) { ); } -export function extract(url) { +export function extract(url, enabledServices = env.enabledServices) { if (!(url instanceof URL)) { url = new URL(url); } @@ -210,7 +210,7 @@ export function extract(url) { return { error: "link.invalid" }; } - if (!env.enabledServices.has(host)) { + if (!enabledServices.has(host)) { // show a different message when youtube is disabled on official instances // as it only happens when shit hits the fan if (new URL(env.apiURL).hostname.endsWith(".imput.net") && host === "youtube") { diff --git a/api/src/security/api-keys.js b/api/src/security/api-keys.js index 37ec66fb..c0ef7770 100644 --- a/api/src/security/api-keys.js +++ b/api/src/security/api-keys.js @@ -15,7 +15,7 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12 let keys = {}, reader = null; -const ALLOWED_KEYS = new Set(['name', 'ips', 'userAgents', 'limit']); +const ALLOWED_KEYS = new Set(['name', 'ips', 'userAgents', 'limit', 'allowedServices']); /* Expected format pseudotype: ** type KeyFileContents = Record< @@ -24,7 +24,8 @@ const ALLOWED_KEYS = new Set(['name', 'ips', 'userAgents', 'limit']); ** name?: string, ** limit?: number | "unlimited", ** ips?: CIDRString[], -** userAgents?: string[] +** userAgents?: string[], +** allowedServices?: "all" | string[], ** } ** >; */ @@ -77,6 +78,19 @@ const validateKeys = (input) => { throw "`userAgents` in details contains an invalid user agent: " + invalid_ua; } } + + if (details.allowedServices) { + const isArray = Array.isArray(details.allowedServices); + + if (isArray) { + const invalid_services = details.allowedServices.find(service => !env.allServices.has(service)); + if (invalid_services) { + throw "`allowedServices` in details contains an invalid service"; + } + } else if (details.allowedServices !== "all") { + throw "details object contains value for `allowedServices` which is not an array or `all`"; + } + } }); } @@ -112,6 +126,14 @@ const formatKeys = (keyData) => { if (data.userAgents) { formatted[key].userAgents = data.userAgents.map(generateWildcardRegex); } + + if (data.allowedServices) { + if (Array.isArray(data.allowedServices)) { + formatted[key].allowedServices = new Set(data.allowedServices); + } else { + formatted[key].allowedServices = data.allowedServices; + } + } } return formatted; @@ -230,3 +252,15 @@ export const setup = (url) => { }); } } + +export const getAllowedServices = (key) => { + if (typeof key !== "string") return; + + const allowedServices = keys[key.toLowerCase()]?.allowedServices; + if (!allowedServices) return; + + if (allowedServices === "all") { + return env.allServices; + } + return allowedServices; +}