Skip to content

Commit a27815f

Browse files
xqvvuc121914yu
authored andcommitted
fix: type check
1 parent 24cb93a commit a27815f

10 files changed

Lines changed: 229 additions & 59 deletions

File tree

document/content/self-host/upgrading/4-15/4150.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ SYNC_INDEX=true
3636
5. 增加文件解析/HTML转Markdown/文本切块 worker pool,避免并发太高导致资源耗尽,可通过环境变量调整其 pool 数量。
3737
6. 模型思考配置。
3838

39+
## 环境变量更新
40+
41+
```dotenv
42+
TRUSTED_PROXY_ENABLE=false
43+
TRUSTED_PROXY_IPS=
44+
```
45+
3946
## ⚙️ 优化
4047

4148
1. 增加父子节点选中互斥功能,解决:同时选中父子节点时,移动节点会出现抖动。

packages/service/common/security/clientIp.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const MAX_FORWARDED_FOR_LENGTH = 2048;
2121
const MAX_FORWARDED_FOR_HOPS = 32;
2222

2323
let cachedTrustedProxyIpEnv: string | undefined | null = null;
24+
let cachedTrustedProxyEnableEnv: boolean | undefined | null = null;
2425
let cachedNodeEnv: string | undefined | null = null;
2526
let cachedTrustProxyFn: TrustProxyFn = proxyaddr.compile([]);
2627
let warnedInvalidTrustedProxyIpEnv: string | undefined;
@@ -107,22 +108,31 @@ const parseTrustedProxyIpEnv = (trustedProxyIpEnv?: string) => {
107108
return Array.from(validAddresses);
108109
};
109110

110-
// 构建并缓存 proxy-addr 的信任判定函数;非生产环境默认信任 loopback,叠加 TRUSTED_PROXY_IPS 配置。
111+
// 构建并缓存 proxy-addr 的信任判定函数;用于 TRUSTED_PROXY_ENABLE=true 的可信代理校验模式。
112+
// 可信代理校验模式下,非生产环境默认信任 loopback,并叠加 TRUSTED_PROXY_IPS 配置。
111113
// 仅当环境变量或 NODE_ENV 变化时才重新编译,避免每次请求都重复解析。
112114
const getTrustProxyFn = () => {
115+
const trustedProxyEnable = env.TRUSTED_PROXY_ENABLE;
113116
const trustedProxyIpEnv = env.TRUSTED_PROXY_IPS;
114117
const nodeEnv = process.env.NODE_ENV;
115-
if (trustedProxyIpEnv === cachedTrustedProxyIpEnv && nodeEnv === cachedNodeEnv) {
118+
if (
119+
trustedProxyEnable === cachedTrustedProxyEnableEnv &&
120+
trustedProxyIpEnv === cachedTrustedProxyIpEnv &&
121+
nodeEnv === cachedNodeEnv
122+
) {
116123
return cachedTrustProxyFn;
117124
}
118125

126+
cachedTrustedProxyEnableEnv = trustedProxyEnable;
119127
cachedTrustedProxyIpEnv = trustedProxyIpEnv;
120128
cachedNodeEnv = nodeEnv;
121129

122-
const trustedProxyAddresses = [
123-
...(nodeEnv === 'production' ? [] : (['loopback'] satisfies proxyaddr.Address[])),
124-
...parseTrustedProxyIpEnv(trustedProxyIpEnv)
125-
];
130+
const trustedProxyAddresses = trustedProxyEnable
131+
? [
132+
...(nodeEnv === 'production' ? [] : (['loopback'] satisfies proxyaddr.Address[])),
133+
...parseTrustedProxyIpEnv(trustedProxyIpEnv)
134+
]
135+
: [];
126136
cachedTrustProxyFn = proxyaddr.compile(trustedProxyAddresses);
127137

128138
return cachedTrustProxyFn;
@@ -149,11 +159,6 @@ export const isTrustedProxyIp = (rawIp?: string | null) => {
149159

150160
// 取 TCP 连接对端地址(socket / 旧版 connection 兜底),作为最可信的回退来源。
151161
const getRemoteIp = (req: RequestWithClientIp) => {
152-
console.log({
153-
remoteAddress: req.socket?.remoteAddress,
154-
xForwardedFor: req.headers?.['x-forwarded-for'],
155-
xRealIp: req.headers?.['x-real-ip']
156-
});
157162
return normalizeClientIp(req.socket?.remoteAddress ?? req.connection?.remoteAddress);
158163
};
159164

@@ -168,6 +173,19 @@ const getForwardedFor = (req: RequestWithClientIp) => {
168173
return getHeaderValue(req.headers, 'x-forwarded-for');
169174
};
170175

176+
// 关闭可信代理校验时的兼容模式:直接相信转发头。
177+
// X-Forwarded-For 按行业约定取最左侧 IP;如果不存在或非法,再尝试 X-Real-IP。
178+
const getClientIpFromForwardingHeaders = (req: RequestWithClientIp) => {
179+
const forwardedFor = getForwardedFor(req);
180+
if (forwardedFor && forwardedFor.length <= MAX_FORWARDED_FOR_LENGTH) {
181+
const firstForwardedIp = forwardedFor.split(',')[0]?.trim();
182+
const ip = normalizeClientIp(firstForwardedIp);
183+
if (ip) return ip;
184+
}
185+
186+
return getClientIpFromRealIp(req);
187+
};
188+
171189
// 在调用 proxy-addr 前对 XFF 做尺寸/跳数/格式预检,防止超长或畸形头造成解析放大攻击。
172190
const isForwardedForSafeToParse = (forwardedFor: string) => {
173191
if (forwardedFor.length > MAX_FORWARDED_FOR_LENGTH) return false;
@@ -195,11 +213,16 @@ const createProxyAddrRequest = (remoteAddress: string, forwardedFor: string) =>
195213

196214
// 对外:从请求中解析出最终客户端 IP。
197215
// 策略:
198-
// 1. 取 socket 远端 IP 作为底线;若不可解析直接返回 undefined。
199-
// 2. 远端不在受信代理列表 -> 直接返回远端 IP,忽略一切转发头(防伪造)。
200-
// 3. 远端可信 -> 优先用 X-Forwarded-For(经安全校验后交给 proxy-addr 沿信任链回溯),
216+
// 1. TRUSTED_PROXY_ENABLE=false -> 兼容模式,直接相信 X-Forwarded-For / X-Real-IP,再回退远端 IP。
217+
// 2. TRUSTED_PROXY_ENABLE=true -> 可信代理校验模式,先取 socket 远端 IP 作为底线;若不可解析直接返回 undefined。
218+
// 3. 远端不在受信代理列表 -> 直接返回远端 IP,忽略一切转发头(防伪造)。
219+
// 4. 远端可信 -> 优先用 X-Forwarded-For(经安全校验后交给 proxy-addr 沿信任链回溯),
201220
// 否则回退 X-Real-IP;校验失败或转发头本身仍是受信代理时退回远端 IP。
202221
export const getClientIpFromRequest = (req: RequestWithClientIp) => {
222+
if (!env.TRUSTED_PROXY_ENABLE) {
223+
return getClientIpFromForwardingHeaders(req) ?? getRemoteIp(req);
224+
}
225+
203226
const remoteIp = getRemoteIp(req);
204227
if (!remoteIp) return;
205228

packages/service/env.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ export const serviceEnv = createEnv({
175175
//==================== 安全配置 ====================
176176
USE_IP_LIMIT: BoolSchema.default(false).meta({ description: '是否启用 IP 限流' }),
177177
CHECK_INTERNAL_IP: BoolSchema.default(false).meta({ description: '是否启用内网 IP 检查' }),
178+
TRUSTED_PROXY_ENABLE: BoolSchema.default(false).meta({
179+
description:
180+
'是否启用可信反向代理客户端 IP 校验;关闭时兼容旧逻辑,直接信任 X-Forwarded-For/X-Real-IP'
181+
}),
182+
TRUSTED_PROXY_IPS: z.string().optional().meta({
183+
description:
184+
'可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析'
185+
}),
178186
PASSWORD_LOGIN_LOCK_SECONDS: defaultableIntSchema(120).meta({
179187
description: '密码错误锁定时长(秒)'
180188
}),
Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,11 @@ import type {
33
LicenseDataType,
44
SystemEnvType
55
} from '@fastgpt/global/common/system/types';
6-
import {
7-
TTSModelType,
8-
RerankModelItemType,
9-
STTModelType,
10-
EmbeddingModelItemType,
11-
LLMModelItemType
12-
} from '@fastgpt/global/core/ai/model.schema';
136
import type { SubPlanType } from '@fastgpt/global/support/wallet/sub/type';
147
import type { WorkerNameEnum, WorkerPool } from './worker/utils';
15-
import { Worker } from 'worker_threads';
168

179
declare global {
10+
var countTrackQueue: Map<string, { event: string; count: number; data: Record<string, any> }>;
1811
var systemInitBufferId: string | undefined;
1912

2013
var systemVersion: string;
@@ -25,3 +18,5 @@ declare global {
2518

2619
var workerPoll: Record<WorkerNameEnum, WorkerPool>;
2720
}
21+
22+
export {};

packages/service/test/common/geo/index.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import {
88
type NextApiRequest
99
} from '@fastgpt/service/common/geo';
1010
import { cleanupIntervalMs } from '@fastgpt/service/common/geo/constants';
11+
import { env } from '@fastgpt/service/env';
12+
13+
const originalTrustedProxyEnable = env.TRUSTED_PROXY_ENABLE;
14+
15+
const setTrustedProxyEnable = (value: boolean) => {
16+
(env as { TRUSTED_PROXY_ENABLE: boolean }).TRUSTED_PROXY_ENABLE = value;
17+
};
18+
19+
afterEach(() => {
20+
setTrustedProxyEnable(originalTrustedProxyEnable);
21+
});
1122

1223
describe('getGeoReader', () => {
1324
it('should return a reader instance', () => {
@@ -174,6 +185,8 @@ describe('getIpFromRequest', () => {
174185
});
175186

176187
it('should return the IP from x-forwarded-for header', () => {
188+
setTrustedProxyEnable(true);
189+
177190
const req = {
178191
headers: { 'x-forwarded-for': '203.0.113.50' },
179192
connection: {},
@@ -185,6 +198,8 @@ describe('getIpFromRequest', () => {
185198
});
186199

187200
it('should return the IP from x-real-ip header', () => {
201+
setTrustedProxyEnable(true);
202+
188203
const req = {
189204
headers: { 'x-real-ip': '198.51.100.10' },
190205
connection: {},
@@ -196,6 +211,8 @@ describe('getIpFromRequest', () => {
196211
});
197212

198213
it('should ignore spoofed IP headers from untrusted direct clients', () => {
214+
setTrustedProxyEnable(true);
215+
199216
const req = {
200217
headers: { 'x-forwarded-for': '203.0.113.50', 'x-real-ip': '198.51.100.10' },
201218
connection: {},

packages/service/test/common/middle/reqFrequencyLimit.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import { jsonRes } from '@fastgpt/service/common/response';
55
import { env } from '@fastgpt/service/env';
66

77
const originalUseIpLimit = env.USE_IP_LIMIT;
8+
const originalTrustedProxyEnable = env.TRUSTED_PROXY_ENABLE;
89

910
const setUseIpLimit = (value: boolean) => {
1011
(env as { USE_IP_LIMIT: boolean }).USE_IP_LIMIT = value;
1112
};
1213

14+
const setTrustedProxyEnable = (value: boolean) => {
15+
(env as { TRUSTED_PROXY_ENABLE: boolean }).TRUSTED_PROXY_ENABLE = value;
16+
};
17+
1318
const createRes = () =>
1419
({
1520
setHeader: vi.fn(),
@@ -42,6 +47,7 @@ describe('useIPFrequencyLimit', () => {
4247

4348
afterEach(() => {
4449
setUseIpLimit(originalUseIpLimit);
50+
setTrustedProxyEnable(originalTrustedProxyEnable);
4551
});
4652

4753
it('should enforce IP limit when USE_IP_LIMIT is enabled without force', async () => {
@@ -112,6 +118,8 @@ describe('useIPFrequencyLimit', () => {
112118
});
113119

114120
it('should ignore spoofed forwarding headers from untrusted direct clients', async () => {
121+
setTrustedProxyEnable(true);
122+
115123
const middleware = useIPFrequencyLimit({
116124
id: 'ip-spoof-test-direct',
117125
seconds: 60,
@@ -141,7 +149,41 @@ describe('useIPFrequencyLimit', () => {
141149
expect(spoofedIpRecord).toBeNull();
142150
});
143151

152+
it('should use X-Forwarded-For as the limit key when trusted proxy parsing is disabled', async () => {
153+
setTrustedProxyEnable(false);
154+
155+
const middleware = useIPFrequencyLimit({
156+
id: 'ip-spoof-test-compat',
157+
seconds: 60,
158+
limit: 10,
159+
force: true
160+
});
161+
162+
await middleware(
163+
createReq({
164+
remoteAddress: '172.16.0.119',
165+
headers: {
166+
'x-forwarded-for': '60.186.209.23',
167+
'x-real-ip': '60.186.209.23'
168+
}
169+
}),
170+
createRes()
171+
);
172+
173+
const forwardedIpRecord = await MongoFrequencyLimit.findOne({
174+
eventId: 'ip-qps-limit-ip-spoof-test-compat-60.186.209.23'
175+
}).lean();
176+
const remoteIpRecord = await MongoFrequencyLimit.findOne({
177+
eventId: 'ip-qps-limit-ip-spoof-test-compat-172.16.0.119'
178+
}).lean();
179+
180+
expect(forwardedIpRecord?.amount).toBe(1);
181+
expect(remoteIpRecord).toBeNull();
182+
});
183+
144184
it('should use proxy-addr result for trusted proxy forwarding chains', async () => {
185+
setTrustedProxyEnable(true);
186+
145187
const middleware = useIPFrequencyLimit({
146188
id: 'ip-spoof-test-proxy',
147189
seconds: 60,
@@ -171,6 +213,8 @@ describe('useIPFrequencyLimit', () => {
171213
});
172214

173215
it('should use a shared fail-closed key when client IP cannot be resolved', async () => {
216+
setTrustedProxyEnable(true);
217+
174218
const middleware = useIPFrequencyLimit({
175219
id: 'ip-spoof-test-unknown',
176220
seconds: 60,

packages/service/test/common/security/clientIp.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterEach, describe, expect, it, vi } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import ipaddr from 'ipaddr.js';
33
import {
44
getClientIpFromRequest,
@@ -8,8 +8,13 @@ import {
88
import { env } from '@fastgpt/service/env';
99

1010
const originalTrustedProxyIps = env.TRUSTED_PROXY_IPS;
11+
const originalTrustedProxyEnable = env.TRUSTED_PROXY_ENABLE;
1112
const originalNodeEnv = process.env.NODE_ENV;
1213

14+
const setTrustedProxyEnable = (value: boolean) => {
15+
(env as { TRUSTED_PROXY_ENABLE: boolean }).TRUSTED_PROXY_ENABLE = value;
16+
};
17+
1318
const setTrustedProxyIps = (value?: string) => {
1419
(env as { TRUSTED_PROXY_IPS?: string }).TRUSTED_PROXY_IPS = value;
1520
};
@@ -39,6 +44,7 @@ const createReq = ({
3944

4045
describe('clientIp', () => {
4146
afterEach(() => {
47+
setTrustedProxyEnable(originalTrustedProxyEnable);
4248
setTrustedProxyIps(originalTrustedProxyIps);
4349
setNodeEnv(originalNodeEnv);
4450
});
@@ -68,6 +74,18 @@ describe('clientIp', () => {
6874
});
6975

7076
describe('isTrustedProxyIp', () => {
77+
beforeEach(() => {
78+
setTrustedProxyEnable(true);
79+
});
80+
81+
it('should not trust proxies when trusted proxy parsing is disabled', () => {
82+
setTrustedProxyEnable(false);
83+
setTrustedProxyIps('127.0.0.1/8, 10.0.0.0/8');
84+
85+
expect(isTrustedProxyIp('127.0.0.1')).toBe(false);
86+
expect(isTrustedProxyIp('10.0.0.10')).toBe(false);
87+
});
88+
7189
it('should trust loopback proxies by default', () => {
7290
setTrustedProxyIps(undefined);
7391

@@ -131,6 +149,58 @@ describe('clientIp', () => {
131149
});
132150

133151
describe('getClientIpFromRequest', () => {
152+
beforeEach(() => {
153+
setTrustedProxyEnable(true);
154+
});
155+
156+
it('should trust forwarding headers when trusted proxy parsing is disabled', () => {
157+
setTrustedProxyEnable(false);
158+
setTrustedProxyIps('127.0.0.1/8, 10.0.0.0/8');
159+
160+
const ip = getClientIpFromRequest(
161+
createReq({
162+
remoteAddress: '127.0.0.1',
163+
headers: {
164+
'x-forwarded-for': '203.0.113.50',
165+
'x-real-ip': '203.0.113.51'
166+
}
167+
})
168+
);
169+
170+
expect(ip).toBe('203.0.113.50');
171+
});
172+
173+
it('should fall back to X-Real-IP in compatibility mode when X-Forwarded-For is invalid', () => {
174+
setTrustedProxyEnable(false);
175+
176+
const ip = getClientIpFromRequest(
177+
createReq({
178+
remoteAddress: '127.0.0.1',
179+
headers: {
180+
'x-forwarded-for': 'not-an-ip',
181+
'x-real-ip': '203.0.113.51'
182+
}
183+
})
184+
);
185+
186+
expect(ip).toBe('203.0.113.51');
187+
});
188+
189+
it('should use the left-most X-Forwarded-For IP in compatibility mode', () => {
190+
setTrustedProxyEnable(false);
191+
192+
const ip = getClientIpFromRequest(
193+
createReq({
194+
remoteAddress: '127.0.0.1',
195+
headers: {
196+
'x-forwarded-for': '203.0.113.50, 10.0.0.20'
197+
}
198+
})
199+
);
200+
201+
expect(ip).toBe('203.0.113.50');
202+
});
203+
134204
it('should ignore spoofed forwarding headers for direct requests', () => {
135205
setTrustedProxyIps(undefined);
136206

packages/service/type/env.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)