mirror of
https://github.com/pockethost/pockethost.git
synced 2025-11-23 22:15:49 +00:00
enh(pockethost): add user proxy whitelisting
This commit is contained in:
parent
d558c1e527
commit
b03fdc41fe
@ -39,6 +39,24 @@ If you're making numerous requests from the client side, we recommend using the
|
|||||||
|
|
||||||
In general, exceeding the rate limit often indicates a coding issue. Another option is to write custom routes using [JS Hooks](/docs/programming) to perform bulk fetching and filtering server-side, which can be difficult to manage effectively on the client side.
|
In general, exceeding the rate limit often indicates a coding issue. Another option is to write custom routes using [JS Hooks](/docs/programming) to perform bulk fetching and filtering server-side, which can be difficult to manage effectively on the client side.
|
||||||
|
|
||||||
|
### Server-Side Rendering (SSR) and Proxy Servers
|
||||||
|
|
||||||
|
If you're using a proxy server for Server-Side Rendering (SSR) purposes, all requests to PocketHost will appear to come from your server's IP address rather than your end users' IPs. This means your server will quickly hit the per-IP rate limits (1,000 requests/hour and 5 concurrent requests), affecting all your users.
|
||||||
|
|
||||||
|
**Our recommended solutions:**
|
||||||
|
|
||||||
|
1. **Switch to Client-Side Rendering (CSR)** - Make API calls directly from the browser instead of through your server
|
||||||
|
2. **Use [PocketPages.dev](https://pocketpages.dev)** - A lightweight SSR solution that runs directly within PocketBase
|
||||||
|
|
||||||
|
**If you must use a proxy server:**
|
||||||
|
|
||||||
|
If neither of the above solutions work for your use case, you can configure your proxy to forward the real client IP addresses:
|
||||||
|
|
||||||
|
1. Configure your proxy server to send the `X-PocketHost-Client-IP` header with each request, containing the real client's IP address
|
||||||
|
2. Contact [PocketHost Support](/support) to whitelist your proxy server's IP address
|
||||||
|
|
||||||
|
Once whitelisted, PocketHost will use the IP from the `X-PocketHost-Client-IP` header for rate limiting instead of your proxy server's IP, ensuring each end user gets their own rate limit allocation.
|
||||||
|
|
||||||
### Special Cases
|
### Special Cases
|
||||||
|
|
||||||
In special cases, such as during conferences or events where a large amount of traffic originates from a single IP, we have ways to expand or bypass these rate limits. If this applies to you, please contact [PocketHost Support](/support).
|
In special cases, such as during conferences or events where a large amount of traffic originates from a single IP, we have ways to expand or bypass these rate limits. If this applies to you, please contact [PocketHost Support](/support).
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import express from 'express'
|
|||||||
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
||||||
import { Logger } from 'src/common'
|
import { Logger } from 'src/common'
|
||||||
|
|
||||||
const getClientIp = (req: express.Request): string | undefined => {
|
const getConnectingIp = (req: express.Request): string | undefined => {
|
||||||
|
// Check Cloudflare headers
|
||||||
const cf = req.headers['cf-connecting-ip'] || req.headers['true-client-ip']
|
const cf = req.headers['cf-connecting-ip'] || req.headers['true-client-ip']
|
||||||
if (cf) return Array.isArray(cf) ? cf[0] : cf
|
if (cf) return Array.isArray(cf) ? cf[0] : cf
|
||||||
|
|
||||||
|
// Check X-Forwarded-For
|
||||||
const xff = req.headers['x-forwarded-for']
|
const xff = req.headers['x-forwarded-for']
|
||||||
const xffStr = Array.isArray(xff) ? xff.join(',') : xff
|
const xffStr = Array.isArray(xff) ? xff.join(',') : xff
|
||||||
if (typeof xffStr === 'string') {
|
if (typeof xffStr === 'string') {
|
||||||
@ -13,6 +15,7 @@ const getClientIp = (req: express.Request): string | undefined => {
|
|||||||
if (ip) return ip
|
if (ip) return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check X-Real-IP
|
||||||
const xri = req.headers['x-real-ip']
|
const xri = req.headers['x-real-ip']
|
||||||
if (xri) return Array.isArray(xri) ? xri[0] : xri
|
if (xri) return Array.isArray(xri) ? xri[0] : xri
|
||||||
|
|
||||||
@ -20,9 +23,29 @@ const getClientIp = (req: express.Request): string | undefined => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Middleware factory to create a rate limiting middleware
|
// Middleware factory to create a rate limiting middleware
|
||||||
export const createRateLimiterMiddleware = (logger: Logger) => {
|
export const createRateLimiterMiddleware = (logger: Logger, userProxyIps: string[] = []) => {
|
||||||
const { dbg, warn } = logger.create(`RateLimiter`)
|
const { dbg, warn } = logger.create(`RateLimiter`)
|
||||||
dbg(`Creating`)
|
dbg(`Creating`)
|
||||||
|
if (userProxyIps.length > 0) {
|
||||||
|
dbg(`User proxy IPs: ${userProxyIps.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserProxy = (connectingIp: string | undefined): boolean => {
|
||||||
|
if (!connectingIp) return false
|
||||||
|
return userProxyIps.includes(connectingIp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getClientIp = (req: express.Request): string | undefined => {
|
||||||
|
const connectingIp = getConnectingIp(req)
|
||||||
|
|
||||||
|
// If from user proxy, check custom header first
|
||||||
|
if (isUserProxy(connectingIp)) {
|
||||||
|
const customIp = req.headers['x-pockethost-client-ip']
|
||||||
|
if (customIp) return Array.isArray(customIp) ? customIp[0] : customIp
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectingIp
|
||||||
|
}
|
||||||
|
|
||||||
const ipRateLimiter = new RateLimiterMemory({
|
const ipRateLimiter = new RateLimiterMemory({
|
||||||
points: 1000,
|
points: 1000,
|
||||||
@ -47,6 +70,10 @@ export const createRateLimiterMiddleware = (logger: Logger) => {
|
|||||||
|
|
||||||
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
const ip = getClientIp(req)
|
const ip = getClientIp(req)
|
||||||
|
if (isUserProxy(ip)) {
|
||||||
|
dbg(`User Proxy IP detected: ${ip}`, req.headers)
|
||||||
|
}
|
||||||
|
|
||||||
if (!ip) {
|
if (!ip) {
|
||||||
warn(`Could not determine IP address`)
|
warn(`Could not determine IP address`)
|
||||||
res.status(429).send(`IP address not found`)
|
res.status(429).send(`IP address not found`)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
MOTHERSHIP_NAME,
|
MOTHERSHIP_NAME,
|
||||||
MOTHERSHIP_PORT,
|
MOTHERSHIP_PORT,
|
||||||
|
PH_USER_PROXY_IPS,
|
||||||
SSL_CERT,
|
SSL_CERT,
|
||||||
SSL_KEY,
|
SSL_KEY,
|
||||||
} from '@'
|
} from '@'
|
||||||
@ -83,7 +84,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))
|
app.use(createRateLimiterMiddleware(logger, PH_USER_PROXY_IPS()))
|
||||||
|
|
||||||
forEach(hostnameRoutes, (target, host) => {
|
forEach(hostnameRoutes, (target, host) => {
|
||||||
app.use(createVhostProxyMiddleware(host, target, IS_DEV(), logger))
|
app.use(createVhostProxyMiddleware(host, target, IS_DEV(), logger))
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const createSettings = () => ({
|
|||||||
APEX_DOMAIN: mkString(_APEX_DOMAIN),
|
APEX_DOMAIN: mkString(_APEX_DOMAIN),
|
||||||
|
|
||||||
IPCIDR_LIST: mkCsvString([]),
|
IPCIDR_LIST: mkCsvString([]),
|
||||||
|
PH_USER_PROXY_IPS: mkCsvString([]),
|
||||||
DAEMON_PORT: mkNumber(3000),
|
DAEMON_PORT: mkNumber(3000),
|
||||||
DAEMON_PB_IDLE_TTL: mkNumber(1000 * 5), // 5 seconds
|
DAEMON_PB_IDLE_TTL: mkNumber(1000 * 5), // 5 seconds
|
||||||
PH_CONTAINER_LAUNCH_WARN_MS: mkNumber(200),
|
PH_CONTAINER_LAUNCH_WARN_MS: mkNumber(200),
|
||||||
@ -164,6 +165,7 @@ export const APP_URL = (...path: string[]) => [settings().APP_URL, path.join(`/`
|
|||||||
export const APEX_DOMAIN = () => settings().APEX_DOMAIN
|
export const APEX_DOMAIN = () => settings().APEX_DOMAIN
|
||||||
|
|
||||||
export const IPCIDR_LIST = () => settings().IPCIDR_LIST
|
export const IPCIDR_LIST = () => settings().IPCIDR_LIST
|
||||||
|
export const PH_USER_PROXY_IPS = () => settings().PH_USER_PROXY_IPS
|
||||||
export const DAEMON_PORT = () => settings().DAEMON_PORT
|
export const DAEMON_PORT = () => settings().DAEMON_PORT
|
||||||
export const DAEMON_PB_IDLE_TTL = () => settings().DAEMON_PB_IDLE_TTL
|
export const DAEMON_PB_IDLE_TTL = () => settings().DAEMON_PB_IDLE_TTL
|
||||||
export const PH_CONTAINER_LAUNCH_WARN_MS = () => settings().PH_CONTAINER_LAUNCH_WARN_MS
|
export const PH_CONTAINER_LAUNCH_WARN_MS = () => settings().PH_CONTAINER_LAUNCH_WARN_MS
|
||||||
@ -268,6 +270,7 @@ export const logConstants = () => {
|
|||||||
APP_URL,
|
APP_URL,
|
||||||
APEX_DOMAIN,
|
APEX_DOMAIN,
|
||||||
IPCIDR_LIST,
|
IPCIDR_LIST,
|
||||||
|
PH_USER_PROXY_IPS,
|
||||||
DAEMON_PORT,
|
DAEMON_PORT,
|
||||||
DAEMON_PB_IDLE_TTL,
|
DAEMON_PB_IDLE_TTL,
|
||||||
PH_CONTAINER_LAUNCH_WARN_MS,
|
PH_CONTAINER_LAUNCH_WARN_MS,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user