短信发送失败增加冷却防连点限流并配置trust proxy恢复真实客户端IP

This commit is contained in:
yml2213
2026-08-22 19:10:06 +08:00
parent 7e1c879c41
commit 90ff15b627
10 changed files with 59 additions and 0 deletions
+5
View File
@@ -27,6 +27,11 @@ type CreateAppOptions = {
export function createApp({ startupState, isShutdownStarted, config }: CreateAppOptions) {
const app = express()
// 部署在反代(Caddy)之后时让 req.ip 取 X-Forwarded-For 的真实客户端地址;限流、访问日志与登录设备记录都依赖它。
if (config.server?.trustProxy !== undefined && config.server?.trustProxy !== null) {
app.set('trust proxy', config.server.trustProxy)
}
app.use(accessLogMiddleware)
app.use(createCorsMiddleware(config))
app.use(
+2
View File
@@ -6,6 +6,8 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
return {
server: {
port: 3000,
// 默认信任一层反代(Caddy),req.ip 才能取到真实客户端地址;直连暴露端口时设为 false。
trustProxy: 1,
},
data: {
+26
View File
@@ -28,6 +28,7 @@ type EnvOverride = {
export const ENV_OVERRIDES: readonly EnvOverride[] = [
integerEnv('PORT', ['server', 'port']),
trustProxyEnv('HTTP_TRUST_PROXY', ['server', 'trustProxy']),
stringEnv('DATA_ROOT', ['data', 'root'], path.resolve),
stringEnv('LOG_LEVEL', ['logging', 'level']),
stringEnv('LOG_INTEGRATION_LEVEL', ['logging', 'integrationLevel']),
@@ -258,6 +259,31 @@ function booleanEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
}
}
function trustProxyEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
return {
env,
path: configPath,
read: parseTrustProxy,
}
}
/** HTTP_TRUST_PROXY 支持 true/false、反代层数(数字)与 loopback 等预定义子网,其余值回落到默认。 */
function parseTrustProxy(rawValue: unknown): RuntimeConfigValue | null {
const value = parseString(rawValue)
if (value === null) return null
const normalized = value.trim().toLowerCase()
if (normalized === 'true' || normalized === 'yes') return true
if (normalized === 'false' || normalized === 'no') return false
if (normalized === 'loopback' || normalized === 'uniquelocal' || normalized === 'linklocal') {
return normalized
}
if (/^\d+$/.test(normalized)) {
const hops = Number(normalized)
return hops >= 0 ? hops : null
}
return null
}
function adminUsersJsonEnv(env: string, configPath: RuntimeConfigPath): EnvOverride {
return {
env,
@@ -44,6 +44,13 @@ export function validateRuntimeConfig(
const productionLike = isProductionLike(env)
requireInteger(issues, 'server.port', config.server?.port, { min: 1, max: 65535 })
const trustProxy = config.server?.trustProxy
if (trustProxy !== undefined && trustProxy !== null && !isValidTrustProxy(trustProxy)) {
issues.push({
path: 'server.trustProxy',
message: 'server.trustProxy 仅支持 true/false、非负整数或 loopback/uniquelocal/linklocal',
})
}
requireInteger(issues, 'database.maxConnections', config.database?.maxConnections, { min: 1 })
requireOptionalInteger(issues, 'database.idleTimeoutMs', config.database?.idleTimeoutMs, {
min: 1,
@@ -288,6 +295,12 @@ function requireOptionalInteger(
requireInteger(issues, configPath, value, options)
}
function isValidTrustProxy(value: unknown): boolean {
if (typeof value === 'boolean') return true
if (typeof value === 'number') return Number.isInteger(value) && value >= 0
return ['loopback', 'uniquelocal', 'linklocal'].includes(String(value))
}
function isHttpUrl(value: string): boolean {
try {
const url = new URL(value)
+2
View File
@@ -20,6 +20,8 @@ export type AffiliateDashSkuMapping = Record<string, string>
export type RuntimeConfig = {
server: {
port: number
/** Express trust proxytrue/false、反代层数或 loopback 等预定义子网;默认 1(单层反代)。 */
trustProxy?: boolean | number | string
}
data: {
root: string