mirror of
https://github.com/pockethost/pockethost.git
synced 2025-11-23 22:15:49 +00:00
feat(pockethost): add IP and hostname rate limiters
This commit is contained in:
parent
f91d1cf001
commit
262da3e455
@ -53,6 +53,7 @@
|
|||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"node-os-utils": "^1.3.7",
|
"node-os-utils": "^1.3.7",
|
||||||
"pocketbase": "^0.21.3",
|
"pocketbase": "^0.21.3",
|
||||||
|
"rate-limiter-flexible": "^8.1.0",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"tail": "^2.2.6",
|
"tail": "^2.2.6",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
||||||
|
import { Logger } from 'src/common'
|
||||||
|
|
||||||
|
const getClientIp = (req: express.Request): string | undefined => {
|
||||||
|
const cf = req.headers['cf-connecting-ip'] || req.headers['true-client-ip']
|
||||||
|
if (cf) return Array.isArray(cf) ? cf[0] : cf
|
||||||
|
|
||||||
|
const xff = req.headers['x-forwarded-for']
|
||||||
|
const xffStr = Array.isArray(xff) ? xff.join(',') : xff
|
||||||
|
if (typeof xffStr === 'string') {
|
||||||
|
const ip = xffStr.split(',')?.[0]?.trim()
|
||||||
|
if (ip) return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
const xri = req.headers['x-real-ip']
|
||||||
|
if (xri) return Array.isArray(xri) ? xri[0] : xri
|
||||||
|
|
||||||
|
return req.ip || req.socket?.remoteAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware factory to create a rate limiting middleware
|
||||||
|
export const createRateLimiterMiddleware = (logger: Logger) => {
|
||||||
|
const { dbg, warn } = logger.create(`RateLimiter`)
|
||||||
|
dbg(`Creating`)
|
||||||
|
|
||||||
|
const ipRateLimiter = new RateLimiterMemory({
|
||||||
|
points: 1000,
|
||||||
|
duration: 60 * 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
const hostnameRateLimiter = new RateLimiterMemory({
|
||||||
|
points: 10000,
|
||||||
|
duration: 60 * 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
const ip = getClientIp(req)
|
||||||
|
if (!ip) {
|
||||||
|
warn(`Could not determine IP address`)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = req.hostname
|
||||||
|
// dbg(`Request from ${ip} for host ${hostname}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ipResult = await ipRateLimiter.consume(ip)
|
||||||
|
dbg(`IP points remaining for ${ip}: ${ipResult.remainingPoints}`)
|
||||||
|
} catch (rateLimiterRes: any) {
|
||||||
|
const retryAfter = Math.ceil(rateLimiterRes.msBeforeNext / 1000)
|
||||||
|
warn(`IP rate limit exceeded for ${ip} on host ${hostname}. Retry after ${retryAfter} seconds`)
|
||||||
|
res.set('Retry-After', String(retryAfter))
|
||||||
|
res.status(429).send(`Too Many Requests: retry after ${retryAfter} seconds`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hostnameResult = await hostnameRateLimiter.consume(hostname)
|
||||||
|
dbg(`Hostname points remaining for ${hostname}: ${hostnameResult.remainingPoints}`)
|
||||||
|
next()
|
||||||
|
} catch (rateLimiterRes: any) {
|
||||||
|
const retryAfter = Math.ceil(rateLimiterRes.msBeforeNext / 1000)
|
||||||
|
warn(`Hostname rate limit exceeded for ${hostname} by IP ${ip}. Retry after ${retryAfter} seconds`)
|
||||||
|
res.set('Retry-After', String(retryAfter))
|
||||||
|
res.status(429).send(`Too Many Requests: retry after ${retryAfter} seconds`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ import { createProxyMiddleware } from 'http-proxy-middleware'
|
|||||||
import https from 'https'
|
import https from 'https'
|
||||||
import { createIpWhitelistMiddleware } from './cidr'
|
import { createIpWhitelistMiddleware } from './cidr'
|
||||||
import { createVhostProxyMiddleware } from './createVhostProxyMiddleware'
|
import { createVhostProxyMiddleware } from './createVhostProxyMiddleware'
|
||||||
|
import { createRateLimiterMiddleware } from './rate-limiter'
|
||||||
|
|
||||||
export type FirewallOptions = {
|
export type FirewallOptions = {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
@ -82,6 +83,7 @@ export const firewall = async ({ logger }: FirewallOptions) => {
|
|||||||
|
|
||||||
// Use the IP blocker middleware
|
// Use the IP blocker middleware
|
||||||
app.use(createIpWhitelistMiddleware(IPCIDR_LIST()))
|
app.use(createIpWhitelistMiddleware(IPCIDR_LIST()))
|
||||||
|
app.use(createRateLimiterMiddleware(logger))
|
||||||
|
|
||||||
forEach(hostnameRoutes, (target, host) => {
|
forEach(hostnameRoutes, (target, host) => {
|
||||||
app.use(createVhostProxyMiddleware(host, target, IS_DEV(), logger))
|
app.use(createVhostProxyMiddleware(host, target, IS_DEV(), logger))
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -253,6 +253,9 @@ importers:
|
|||||||
pocketbase:
|
pocketbase:
|
||||||
specifier: ^0.21.3
|
specifier: ^0.21.3
|
||||||
version: 0.21.5
|
version: 0.21.5
|
||||||
|
rate-limiter-flexible:
|
||||||
|
specifier: ^8.1.0
|
||||||
|
version: 8.1.0
|
||||||
semver:
|
semver:
|
||||||
specifier: ^7.6.3
|
specifier: ^7.6.3
|
||||||
version: 7.6.3
|
version: 7.6.3
|
||||||
@ -3606,6 +3609,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
rate-limiter-flexible@8.1.0:
|
||||||
|
resolution: {integrity: sha512-J+4xBdVboibP1h0Imn4nFoCLT+UM9Os9vJaWaRWkLsQxS7jrhLJeLlmzP5hyCEsLwtgFIIY5KcWiJGyyVTMaKg==}
|
||||||
|
|
||||||
raw-body@2.5.2:
|
raw-body@2.5.2:
|
||||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -7550,6 +7556,8 @@ snapshots:
|
|||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
|
|
||||||
|
rate-limiter-flexible@8.1.0: {}
|
||||||
|
|
||||||
raw-body@2.5.2:
|
raw-body@2.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user