refactor(pockethost): improve logging and key management in rate limiter middleware

This commit is contained in:
Ben Allfree 2025-10-20 22:40:20 +00:00
parent 4a8d90e836
commit d157b77745

View File

@ -28,7 +28,8 @@ export const createRateLimiterMiddleware = (
userProxyIps: string[] = [], userProxyIps: string[] = [],
userProxyWhitelistIps: string[] = [] userProxyWhitelistIps: string[] = []
) => { ) => {
const { dbg, warn } = logger.create(`RateLimiter`) const rateLimiterLogger = logger.create(`RateLimiter`)
const { dbg, warn } = rateLimiterLogger
dbg(`Creating`) dbg(`Creating`)
if (userProxyIps.length > 0) { if (userProxyIps.length > 0) {
dbg(`User proxy IPs: ${userProxyIps.join(', ')}`) dbg(`User proxy IPs: ${userProxyIps.join(', ')}`)
@ -77,45 +78,53 @@ export const createRateLimiterMiddleware = (
return async (req: express.Request, res: express.Response, next: express.NextFunction) => { return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const connectingIp = getConnectingIp(req) const connectingIp = getConnectingIp(req)
const endClientIp = getClientIp(req)
const hostname = req.hostname
// Check if connecting IP is whitelisted - bypass all rate limiting const { dbg, warn } = rateLimiterLogger
if (connectingIp && userProxyWhitelistIps.includes(connectingIp)) { .create(hostname)
dbg(`Whitelisted user proxy IP detected: ${connectingIp} - bypassing rate limiting`) .breadcrumb(connectingIp || `unknown`)
return next() .breadcrumb(endClientIp || `unknown`)
}
if (isUserProxy(connectingIp)) { dbg(`\n`)
dbg(`User Proxy IP detected: ${connectingIp}`, req.headers) dbg(`--------------------------------`)
} dbg(`Incoming request`)
dbg(`Hostname: ${hostname}`)
dbg(`Connecting IP: ${connectingIp || `unknown`}`)
dbg(`End Client IP: ${endClientIp || `unknown`}`)
dbg(`\n`)
const ip = getClientIp(req) if (!hostname) {
if (!ip) { warn(`Could not determine hostname`)
warn(`Could not determine IP address`) res.status(429).send(`Hostname not found`)
res.status(429).send(`IP address not found`)
return return
} }
const hostname = req.hostname if (isUserProxy(connectingIp)) {
// dbg(`Request from ${ip} for host ${hostname}`) dbg(`User Proxy IP detected`, req.headers)
}
// Check rate limits first (requests per hour) // Check rate limits first (requests per hour per IP per hostname)
try { try {
const ipResult = await ipRateLimiter.consume(ip) const key = `${endClientIp}:${hostname}`
dbg(`IP points remaining for ${ip}: ${ipResult.remainingPoints}`) const ipResult = await ipRateLimiter.consume(key)
dbg(`IP request accepted. Key: ${key}. Points remaining: ${ipResult.remainingPoints}`)
} catch (rateLimiterRes: any) { } catch (rateLimiterRes: any) {
const retryAfter = Math.ceil(rateLimiterRes.msBeforeNext / 1000) const retryAfter = Math.ceil(rateLimiterRes.msBeforeNext / 1000)
warn(`IP rate limit exceeded for ${ip} on host ${hostname}. Retry after ${retryAfter} seconds`) warn(`IP rate limit exceeded. Retry after ${retryAfter} seconds`)
res.set('Retry-After', String(retryAfter)) res.set('Retry-After', String(retryAfter))
res.status(429).send(`Too Many Requests: retry after ${retryAfter} seconds [1]`) res.status(429).send(`Too Many Requests: retry after ${retryAfter} seconds [1]`)
return return
} }
// Check hostname rate limit (requests per hour per hostname)
try { try {
const hostnameResult = await hostnameRateLimiter.consume(hostname) const key = hostname
dbg(`Hostname points remaining for ${hostname}: ${hostnameResult.remainingPoints}`) const hostnameResult = await hostnameRateLimiter.consume(key)
dbg(`Hostname request accepted. Key: ${key}. Points remaining: ${hostnameResult.remainingPoints}`)
} catch (rateLimiterRes: any) { } catch (rateLimiterRes: any) {
const retryAfter = Math.ceil(rateLimiterRes.msBeforeNext / 1000) const retryAfter = Math.ceil(rateLimiterRes.msBeforeNext / 1000)
warn(`Hostname rate limit exceeded for ${hostname} by IP ${ip}. Retry after ${retryAfter} seconds`) warn(`Hostname rate limit exceeded. Retry after ${retryAfter} seconds`)
res.set('Retry-After', String(retryAfter)) res.set('Retry-After', String(retryAfter))
res.status(429).send(`Too Many Requests: retry after ${retryAfter} seconds [2]`) res.status(429).send(`Too Many Requests: retry after ${retryAfter} seconds [2]`)
return return
@ -127,37 +136,42 @@ export const createRateLimiterMiddleware = (
// Helper to release concurrent points // Helper to release concurrent points
const releaseConcurrentPoints = async () => { const releaseConcurrentPoints = async () => {
if (ipConcurrentConsumed) { if (ipConcurrentConsumed) {
const ipConcurrentResult = await ipConcurrentLimiter.reward(ip, 1) const key = `${endClientIp}:${hostname}`
dbg(`Released concurrent point for IP ${ip}. Points remaining: ${ipConcurrentResult.remainingPoints}`) const ipConcurrentResult = await ipConcurrentLimiter.reward(key, 1)
dbg(`Released IP concurrent point. Key: ${key}. Points remaining: ${ipConcurrentResult.remainingPoints}`)
} }
if (hostnameConcurrentConsumed) { if (hostnameConcurrentConsumed) {
const hostnameConcurrentResult = await hostnameConcurrentLimiter.reward(hostname, 1) const key = hostname
const hostnameConcurrentResult = await hostnameConcurrentLimiter.reward(key, 1)
dbg( dbg(
`Released concurrent point for hostname ${hostname}. Points remaining: ${hostnameConcurrentResult.remainingPoints}` `Released hostname concurrent point. Key: ${key}. Points remaining: ${hostnameConcurrentResult.remainingPoints}`
) )
} }
} }
// Check concurrent limits // Check concurrent limits per IP per hostname
try { try {
const ipConcurrentResult = await ipConcurrentLimiter.consume(ip) const key = `${endClientIp}:${hostname}`
const ipConcurrentResult = await ipConcurrentLimiter.consume(key)
dbg(`IP concurrent request accepted. Key: ${key}. Points remaining: ${ipConcurrentResult.remainingPoints}`)
ipConcurrentConsumed = true ipConcurrentConsumed = true
dbg(`IP concurrent request accepted for ${ip}. Points remaining: ${ipConcurrentResult.remainingPoints}`)
} catch (rateLimiterRes: any) { } catch (rateLimiterRes: any) {
warn(`IP concurrent limit exceeded for ${ip} on host ${hostname}`) warn(`IP concurrent limit exceeded.`)
res.status(429).send(`Too Many Requests: concurrent request limit exceeded [3]`) res.status(429).send(`Too Many Requests: concurrent request limit exceeded [3]`)
return return
} }
// Check overall concurrent limits per host
try { try {
const hostnameConcurrentResult = await hostnameConcurrentLimiter.consume(hostname) const key = hostname
hostnameConcurrentConsumed = true const hostnameConcurrentResult = await hostnameConcurrentLimiter.consume(key)
dbg( dbg(
`Hostname concurrent request accepted for ${hostname} on IP ${ip}. Points remaining: ${hostnameConcurrentResult.remainingPoints}` `Hostname concurrent request accepted. Key: ${key}. Points remaining: ${hostnameConcurrentResult.remainingPoints}`
) )
hostnameConcurrentConsumed = true
} catch (rateLimiterRes: any) { } catch (rateLimiterRes: any) {
await releaseConcurrentPoints() await releaseConcurrentPoints()
warn(`Hostname concurrent limit exceeded for ${hostname} by IP ${ip}`) warn(`Hostname concurrent limit exceeded.`)
res.status(429).send(`Too Many Requests: concurrent request limit exceeded [4]`) res.status(429).send(`Too Many Requests: concurrent request limit exceeded [4]`)
return return
} }