diff --git a/api/package.json b/api/package.json index 3a104158..da4d7c4c 100644 --- a/api/package.json +++ b/api/package.json @@ -46,6 +46,7 @@ }, "optionalDependencies": { "freebind": "^0.2.2", + "rate-limit-redis": "^4.2.0", "redis": "^4.7.0" } } diff --git a/api/src/core/api.js b/api/src/core/api.js index 1d4370e9..5ea75c37 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -12,6 +12,7 @@ import { env, setTunnelPort } from "../config.js"; import { extract } from "../processing/url.js"; import { Green, Bright, Cyan } from "../misc/console-text.js"; import { hashHmac } from "../security/secrets.js"; +import { createStore } from "../store/redis-ratelimit.js"; import { randomizeCiphers } from "../misc/randomize-ciphers.js"; import { verifyTurnstileToken } from "../security/turnstile.js"; import { friendlyServiceName } from "../processing/service-alias.js"; @@ -40,7 +41,7 @@ const fail = (res, code, context) => { res.status(status).json(body); } -export const runAPI = (express, app, __dirname, isPrimary = true) => { +export const runAPI = async (express, app, __dirname, isPrimary = true) => { const startTime = new Date(); const startTimestamp = startTime.getTime(); @@ -76,6 +77,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { standardHeaders: 'draft-6', legacyHeaders: false, keyGenerator, + store: await createStore('session'), handler: handleRateExceeded }); @@ -85,6 +87,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { standardHeaders: 'draft-6', legacyHeaders: false, keyGenerator: req => req.rateLimitKey || keyGenerator(req), + store: await createStore('api'), handler: handleRateExceeded }) @@ -94,6 +97,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { standardHeaders: 'draft-6', legacyHeaders: false, keyGenerator: req => req.rateLimitKey || keyGenerator(req), + store: await createStore('tunnel'), handler: (_, res) => { return res.sendStatus(429) } diff --git a/api/src/store/redis-ratelimit.js b/api/src/store/redis-ratelimit.js new file mode 100644 index 00000000..64d11e5e --- /dev/null +++ b/api/src/store/redis-ratelimit.js @@ -0,0 +1,19 @@ +import { env } from "../config.js"; + +let client, redis, redisLimiter; + +export const createStore = async (name) => { + if (!env.redisURL) return; + + if (!client) { + redis = await import('redis'); + redisLimiter = await import('rate-limit-redis'); + client = redis.createClient({ url: env.redisURL }); + await client.connect(); + } + + return new redisLimiter.default({ + prefix: `RL${name}_`, + sendCommand: (...args) => client.sendCommand(args), + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9283338a..b36f1ff5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: freebind: specifier: ^0.2.2 version: 0.2.2 + rate-limit-redis: + specifier: ^4.2.0 + version: 4.2.0(express-rate-limit@7.4.1(express@4.21.0)) redis: specifier: ^4.7.0 version: 4.7.0 @@ -1877,6 +1880,12 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + rate-limit-redis@4.2.0: + resolution: {integrity: sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA==} + engines: {node: '>= 16'} + peerDependencies: + express-rate-limit: '>= 6' + raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -3868,6 +3877,11 @@ snapshots: range-parser@1.2.1: {} + rate-limit-redis@4.2.0(express-rate-limit@7.4.1(express@4.21.0)): + dependencies: + express-rate-limit: 7.4.1(express@4.21.0) + optional: true + raw-body@2.5.2: dependencies: bytes: 3.1.2