Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ ENABLE_SECURE_COOKIES=true
# - Fail Open 策略:Redis 不可用时自动降级,不影响服务可用性
ENABLE_RATE_LIMIT=true # 是否启用限流功能(默认:true)
REDIS_URL=redis://localhost:6379 # Redis 连接地址(Docker 部署使用 redis://redis:6379,支持 rediss:// TLS)
REDIS_TLS_REJECT_UNAUTHORIZED=true # 是否验证 Redis TLS 证书(默认:true)
# 设置为 false 可跳过证书验证,用于自签证书或共享证书场景
# 仅在 rediss:// 协议时生效

# Session 配置
SESSION_TTL=300 # Session 过期时间(秒,默认 300 = 5 分钟)
Expand Down
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ Docker Compose is the **preferred deployment method** — it automatically provi
| `DSN` | - | PostgreSQL connection string, e.g., `postgres://user:pass@host:5432/db`. |
| `AUTO_MIGRATE` | `true` | Executes Drizzle migrations on startup; consider disabling in production for manual control. |
| `REDIS_URL` | `redis://localhost:6379` | Redis endpoint, supports `rediss://` for TLS providers. |
| `REDIS_TLS_REJECT_UNAUTHORIZED` | `true` | Validate Redis TLS certificates; set `false` to skip (for self-signed/shared certs). |
| `ENABLE_RATE_LIMIT` | `true` | Toggles multi-dimensional rate limiting; Fail-Open handles Redis outages gracefully. |
| `SESSION_TTL` | `300` | Session cache window (seconds) that drives vendor reuse. |
| `ENABLE_SECURE_COOKIES` | `true` | Browsers require HTTPS for Secure cookies; set to `false` when serving plain HTTP outside localhost. |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Docker Compose 是**首选部署方式**,自动配置数据库、Redis 和应
| `DSN` | - | PostgreSQL 连接串,如 `postgres://user:pass@host:5432/db`. |
| `AUTO_MIGRATE` | `true` | 启动时自动执行 Drizzle 迁移;生产环境可关闭以人工控制。 |
| `REDIS_URL` | `redis://localhost:6379` | Redis 地址,支持 `rediss://` 用于 TLS。 |
| `REDIS_TLS_REJECT_UNAUTHORIZED` | `true` | 是否验证 Redis TLS 证书;设为 `false` 可跳过验证(用于自签/共享证书)。 |
| `ENABLE_RATE_LIMIT` | `true` | 控制多维限流开关;Fail-Open 策略在 Redis 不可用时自动降级。 |
| `SESSION_TTL` | `300` | Session 缓存时间(秒),影响供应商复用策略。 |
| `ENABLE_SECURE_COOKIES` | `true` | 仅 HTTPS 场景能设置 Secure Cookie;HTTP 访问(非 localhost)需改为 `false`。 |
Expand Down
9 changes: 7 additions & 2 deletions scripts/clear-session-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,16 @@ async function createRedisClient(redisUrl: string): Promise<Redis> {
};

if (redisUrl.startsWith("rediss://")) {
const rejectUnauthorized = process.env.REDIS_TLS_REJECT_UNAUTHORIZED !== "false";
try {
const url = new URL(redisUrl);
options.tls = { host: url.hostname };
options.tls = {
host: url.hostname,
servername: url.hostname, // SNI support for cloud Redis providers
rejectUnauthorized,
};
} catch {
options.tls = {};
options.tls = { rejectUnauthorized };
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/config/env.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const EnvSchema = z.object({
AUTO_MIGRATE: z.string().default("true").transform(booleanTransform),
PORT: z.coerce.number().default(23000),
REDIS_URL: z.string().optional(),
REDIS_TLS_REJECT_UNAUTHORIZED: z.string().default("true").transform(booleanTransform),
ENABLE_RATE_LIMIT: z.string().default("true").transform(booleanTransform),
ENABLE_SECURE_COOKIES: z.string().default("true").transform(booleanTransform),
SESSION_TTL: z.coerce.number().default(300),
Expand Down
7 changes: 5 additions & 2 deletions src/lib/log-cleanup/cleanup-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ function getCleanupQueue(): Queue.Queue {
redisQueueOptions.username = url.username; // 传递用户名

if (useTls) {
logger.info("[CleanupQueue] Using TLS connection (rediss://)");
const rejectUnauthorized = process.env.REDIS_TLS_REJECT_UNAUTHORIZED !== "false";
logger.info("[CleanupQueue] Using TLS connection (rediss://)", { rejectUnauthorized });
redisQueueOptions.tls = {
host: url.hostname, // 显式 SNI 修复
host: url.hostname,
servername: url.hostname, // SNI support for cloud Redis providers
rejectUnauthorized,
};
}
} catch (e) {
Expand Down
7 changes: 5 additions & 2 deletions src/lib/notification/notification-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,12 @@ function getNotificationQueue(): Queue.Queue<NotificationJobData> {
redisQueueOptions.username = url.username; // 传递用户名

if (useTls) {
logger.info("[NotificationQueue] Using TLS connection (rediss://)");
const rejectUnauthorized = process.env.REDIS_TLS_REJECT_UNAUTHORIZED !== "false";
logger.info("[NotificationQueue] Using TLS connection (rediss://)", { rejectUnauthorized });
redisQueueOptions.tls = {
host: url.hostname, // 显式 SNI 修复
host: url.hostname,
servername: url.hostname, // SNI support for cloud Redis providers
rejectUnauthorized,
};
}
} catch (e) {
Expand Down
43 changes: 29 additions & 14 deletions src/lib/redis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,31 @@ function maskRedisUrl(redisUrl: string) {
}
}

/**
* Build TLS configuration for Redis connection.
* Supports skipping certificate verification via REDIS_TLS_REJECT_UNAUTHORIZED env.
* Includes servername for SNI (Server Name Indication) support.
*/
function buildTlsConfig(redisUrl: string): Record<string, unknown> {
const rejectUnauthorized = process.env.REDIS_TLS_REJECT_UNAUTHORIZED !== "false";

try {
const url = new URL(redisUrl);
return {
host: url.hostname,
servername: url.hostname, // SNI support for cloud Redis providers
rejectUnauthorized,
};
} catch {
return { rejectUnauthorized };
}
}

/**
* Build ioredis connection options with protocol-based TLS detection.
* - When `rediss://` is used, explicitly enable TLS via `tls: {}`
* - When `redis://` is used, keep plaintext TCP (no TLS option)
* - Supports REDIS_TLS_REJECT_UNAUTHORIZED env to skip certificate verification
*/
export function buildRedisOptionsForUrl(redisUrl: string) {
const isTLS = (() => {
Expand Down Expand Up @@ -46,7 +67,7 @@ export function buildRedisOptionsForUrl(redisUrl: string) {
} as const;

// Explicit TLS config for Upstash and other managed Redis providers
const tlsOptions = isTLS ? { tls: {} as Record<string, unknown> } : {};
const tlsOptions = isTLS ? { tls: buildTlsConfig(redisUrl) } : {};

return { isTLS, options: { ...baseOptions, ...tlsOptions } };
}
Expand Down Expand Up @@ -89,20 +110,14 @@ export function getRedisClient(): Redis | null {
},
};

// 2. 如果使用 rediss://,则添加显式的 TLS 和 SNI (host) 配置
// 2. 如果使用 rediss://,则添加显式的 TLS 配置(支持跳过证书验证)
if (useTls) {
logger.info("[Redis] Using TLS connection (rediss://)", { redisUrl: safeRedisUrl });
try {
// 从 URL 中解析 hostname,用于 SNI
const url = new URL(redisUrl);
redisOptions.tls = {
host: url.hostname,
};
} catch (e) {
logger.error("[Redis] Failed to parse REDIS_URL for TLS host:", e);
// 如果 URL 解析失败,回退
redisOptions.tls = {};
}
const rejectUnauthorized = process.env.REDIS_TLS_REJECT_UNAUTHORIZED !== "false";
logger.info("[Redis] Using TLS connection (rediss://)", {
redisUrl: safeRedisUrl,
rejectUnauthorized,
});
redisOptions.tls = buildTlsConfig(redisUrl);
}

// 3. 使用组合后的配置创建客户端
Expand Down
Loading