Commit f252bc73 by Archer Committed by GitHub

perf: ip check (#6850)

* perf: ip check

* fix: restore IP limit proxy handling

* test: check xff and xri

* fix: type check

* doc

* update template

* fix: workspace

* fix: import

* fix: align IP limit env usage

* init env

* init env

* fix: mongo log url

* fix: mongo log url

* submodule

* update piagent import

---------

Co-authored-by: xqvvu <whoeverimf5@gmail.com>
parent 89b80f75
...@@ -14,17 +14,32 @@ description: 'FastGPT V4.15.0 更新说明' ...@@ -14,17 +14,32 @@ description: 'FastGPT V4.15.0 更新说明'
`fastgpt-app`, `fastgpt-pro` 可增加文件解析并发线程数 `fastgpt-app`, `fastgpt-pro` 可增加文件解析并发线程数
```bash ```dotenv
# 文件解析 worker 并发数 # 文件解析 worker 并发数(可选)
PARSE_FILE_WORKERS=10 PARSE_FILE_WORKERS=10
# 文件解析超时时间(秒) # 文件解析超时时间(秒)(可选)
PARSE_FILE_TIMEOUT_SECONDS=600 PARSE_FILE_TIMEOUT_SECONDS=600
# HTML 转 Markdown worker 并发数 # HTML 转 Markdown worker 并发数(可选)
HTML_TO_MARKDOWN_WORKERS=10 HTML_TO_MARKDOWN_WORKERS=10
# 文本切块 worker 并发数 # 文本切块 worker 并发数(可选)
TEXT_TO_CHUNKS_WORKERS=10 TEXT_TO_CHUNKS_WORKERS=10
# 自动同步 mongo 数据库索引, 改成 boolean 字符串值,而不是 0 和 1 # 自动同步 mongo 数据库索引, 改成 boolean 字符串值,而不是 0 和 1(可选)
SYNC_INDEX=true SYNC_INDEX=true
# 是否启用可信反向代理客户端 IP 校验(可选)
TRUSTED_PROXY_ENABLE=false
# 可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析(可选)
TRUSTED_PROXY_IPS=
```
### 确认是否遗漏环境变量
本次升级,增加了对于环境变量的检测,避免漏填必须的环境变量,需重点检查`fastgpt-app`和`fastgpt-pro`是否包含:
```dotenv
# 密钥加密密钥,两个服务需一致
AES256_SECRET_KEY=
# 文件 token 密钥,两个服务需一致
FILE_TOKEN_KEY=
``` ```
## 🚀 新增内容 ## 🚀 新增内容
...@@ -45,6 +60,7 @@ SYNC_INDEX=true ...@@ -45,6 +60,7 @@ SYNC_INDEX=true
5. 加强第三方知识库请求的 SSRF 防护。 5. 加强第三方知识库请求的 SSRF 防护。
6. codex-sandbox 加强 AST 检查,防止绕过安全检查。 6. codex-sandbox 加强 AST 检查,防止绕过安全检查。
7. 站点同步限流错误提示,重复提示。 7. 站点同步限流错误提示,重复提示。
8. 加强 IP 检测,避免伪造绕过。
## 🐛 修复 ## 🐛 修复
......
...@@ -145,8 +145,8 @@ ...@@ -145,8 +145,8 @@
"content/openapi/intro.mdx": "2026-04-26T21:08:47+08:00", "content/openapi/intro.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.en.mdx": "2026-04-26T21:08:47+08:00", "content/openapi/share.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.mdx": "2026-04-26T21:08:47+08:00", "content/openapi/share.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/env.en.mdx": "2026-04-30T00:20:04+08:00", "content/self-host/config/env.en.mdx": "2026-05-06T18:25:24+08:00",
"content/self-host/config/env.mdx": "2026-04-30T00:20:04+08:00", "content/self-host/config/env.mdx": "2026-05-06T18:25:24+08:00",
"content/self-host/config/json.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/config/json.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/json.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/config/json.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/model/intro.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/config/model/intro.en.mdx": "2026-04-26T21:08:47+08:00",
...@@ -257,7 +257,7 @@ ...@@ -257,7 +257,7 @@
"content/self-host/upgrading/4-14/41481.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/41481.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-15/4150.mdx": "2026-05-06T14:03:59+08:00", "content/self-host/upgrading/4-15/4150.mdx": "2026-05-06T18:31:58+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
...@@ -398,8 +398,8 @@ ...@@ -398,8 +398,8 @@
"content/self-host/upgrading/outdated/499.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/499.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/upgrade-intruction.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/upgrade-intruction.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00",
"content/toc.en.mdx": "2026-04-29T23:22:43+08:00", "content/toc.en.mdx": "2026-05-06T18:25:24+08:00",
"content/toc.mdx": "2026-04-29T23:22:43+08:00", "content/toc.mdx": "2026-05-06T18:25:24+08:00",
"content/use-cases/app-cases/dalle3.en.mdx": "2026-04-26T21:08:47+08:00", "content/use-cases/app-cases/dalle3.en.mdx": "2026-04-26T21:08:47+08:00",
"content/use-cases/app-cases/dalle3.mdx": "2026-04-26T21:08:47+08:00", "content/use-cases/app-cases/dalle3.mdx": "2026-04-26T21:08:47+08:00",
"content/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-04-26T21:08:47+08:00", "content/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-04-26T21:08:47+08:00",
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8", "@bany/curl-to-json": "^1.2.8",
"axios": "catalog:", "axios": "catalog:",
"ipaddr.js": "catalog:",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
"dayjs": "catalog:", "dayjs": "catalog:",
"encoding": "^0.1.13", "encoding": "^0.1.13",
...@@ -24,7 +25,6 @@ ...@@ -24,7 +25,6 @@
"next": "catalog:", "next": "catalog:",
"openai": "6.34.0", "openai": "6.34.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"ipaddr.js": "^2.3.0",
"timezones-list": "^3.0.2", "timezones-list": "^3.0.2",
"lodash": "catalog:", "lodash": "catalog:",
"zod": "catalog:", "zod": "catalog:",
......
...@@ -6,10 +6,10 @@ import type { LocationName } from './type'; ...@@ -6,10 +6,10 @@ import type { LocationName } from './type';
import { extractLocationData } from './utils'; import { extractLocationData } from './utils';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
export type { NextApiRequest } from 'next'; export type { NextApiRequest } from 'next';
import { getClientIp } from 'request-ip';
import { getLogger } from '../logger'; import { getLogger } from '../logger';
import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { localeType } from '@fastgpt/global/common/i18n/type';
import { formatI18nLocationToZhEn } from '@fastgpt/global/common/i18n/utils'; import { formatI18nLocationToZhEn } from '@fastgpt/global/common/i18n/utils';
import { getClientIpFromRequest } from '../security/clientIp';
const logger = getLogger(['GEO']); const logger = getLogger(['GEO']);
...@@ -109,7 +109,7 @@ export function initGeo() { ...@@ -109,7 +109,7 @@ export function initGeo() {
} }
export function getIpFromRequest(request: NextApiRequest): string { export function getIpFromRequest(request: NextApiRequest): string {
const ip = getClientIp(request); const ip = getClientIpFromRequest(request);
if (!ip || ip === '::1') { if (!ip || ip === '::1') {
return '127.0.0.1'; return '127.0.0.1';
} }
......
...@@ -7,6 +7,7 @@ import { getLogger, LogCategories, withContext } from '../logger'; ...@@ -7,6 +7,7 @@ import { getLogger, LogCategories, withContext } from '../logger';
import { setSpanError, withActiveSpan } from '../tracing'; import { setSpanError, withActiveSpan } from '../tracing';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { getClientIpFromRequest } from '../security/clientIp';
export type NextApiHandler<T = any> = ( export type NextApiHandler<T = any> = (
req: ApiRequestProps, req: ApiRequestProps,
...@@ -61,7 +62,7 @@ export const NextEntry = ({ ...@@ -61,7 +62,7 @@ export const NextEntry = ({
const url = req.url || ''; const url = req.url || '';
const route = getRequestRoute(url); const route = getRequestRoute(url);
const method = req.method?.toUpperCase() || ''; const method = req.method?.toUpperCase() || '';
const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress; const ip = getClientIpFromRequest(req);
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
const contentLength = req.headers['content-length']; const contentLength = req.headers['content-length'];
const requestBodySize = parseHeaderNumber(contentLength); const requestBodySize = parseHeaderNumber(contentLength);
......
import { type ApiRequestProps } from '../../type/next'; import { type ApiRequestProps } from '../../type/next';
import requestIp from 'request-ip';
import { authFrequencyLimit } from '../system/frequencyLimit/utils'; import { authFrequencyLimit } from '../system/frequencyLimit/utils';
import { addSeconds } from 'date-fns'; import { addSeconds } from 'date-fns';
import { type NextApiResponse } from 'next'; import { type NextApiResponse } from 'next';
import { jsonRes } from '../response'; import { jsonRes } from '../response';
import { serviceEnv } from '../../env'; import { serviceEnv } from '../../env';
import { getClientIpFromRequest } from '../security/clientIp';
// unit: times/s // unit: times/s
// how to use? // how to use?
...@@ -21,10 +21,11 @@ export function useIPFrequencyLimit({ ...@@ -21,10 +21,11 @@ export function useIPFrequencyLimit({
force?: boolean; force?: boolean;
}) { }) {
return async (req: ApiRequestProps, res: NextApiResponse) => { return async (req: ApiRequestProps, res: NextApiResponse) => {
const ip = requestIp.getClientIp(req); if (!serviceEnv.USE_IP_LIMIT && !force) {
if (!ip || (!serviceEnv.USE_IP_LIMIT && !force)) {
return; return;
} }
const ip = getClientIpFromRequest(req) ?? 'unknown';
try { try {
await authFrequencyLimit({ await authFrequencyLimit({
eventId: `ip-qps-limit-${id}-` + ip, eventId: `ip-qps-limit-${id}-` + ip,
......
import type { IncomingHttpHeaders, IncomingMessage } from 'http';
import ipaddr from 'ipaddr.js';
import proxyaddr from 'proxy-addr';
import { serviceEnv } from '../../env';
type IPAddress = ipaddr.IPv4 | ipaddr.IPv6;
type RequestWithClientIp = {
headers?: IncomingHttpHeaders;
socket?: {
remoteAddress?: string | null;
};
connection?: {
remoteAddress?: string | null;
};
};
type TrustProxyFn = (addr: string, i: number) => boolean;
const MAX_FORWARDED_FOR_LENGTH = 2048;
const MAX_FORWARDED_FOR_HOPS = 32;
let cachedTrustedProxyIpEnv: string | undefined | null = null;
let cachedTrustedProxyEnableEnv: boolean | undefined | null = null;
let cachedNodeEnv: string | undefined | null = null;
let cachedTrustProxyFn: TrustProxyFn = proxyaddr.compile([]);
let warnedInvalidTrustedProxyIpEnv: string | undefined;
// 不区分大小写读取 header 值;数组类型(如 set-cookie 风格)合并为逗号分隔字符串。
const getHeaderValue = (headers: IncomingHttpHeaders | undefined, key: string) => {
const value =
headers?.[key] ??
Object.entries(headers ?? {}).find(([headerKey]) => headerKey.toLowerCase() === key)?.[1];
if (Array.isArray(value)) return value.join(',');
return value;
};
// 剥离 IP 字符串外层的引号、IPv6 方括号以及 IPv4/IPv6 末尾的端口,返回纯地址。
const stripIpWrapper = (rawIp: string) => {
const ip = rawIp.trim().replace(/^"(.+)"$/, '$1');
const bracketedIpv6 = ip.match(/^\[([^\]]+)](?::\d+)?$/);
if (bracketedIpv6?.[1]) return bracketedIpv6[1];
const ipv4WithPort = ip.match(/^(\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/);
if (ipv4WithPort?.[1]) return ipv4WithPort[1];
return ip;
};
// 将原始字符串解析为 ipaddr.js 的地址对象;非法或为空时返回 null,内部走 ipaddr.process 以归一 IPv4-mapped IPv6。
const parseIpAddress = (rawIp?: string | null): IPAddress | null => {
if (!rawIp) return null;
const ip = stripIpWrapper(rawIp);
if (!ipaddr.isValid(ip)) return null;
try {
return ipaddr.process(ip);
} catch {
return null;
}
};
// 校验单条 TRUSTED_PROXY_IPS 配置项是否为合法的 IP 或 CIDR(校验掩码长度与地址族匹配)。
const isValidTrustedProxyAddress = (rawValue: string) => {
const addressParts = rawValue.trim().split('/');
if (addressParts.length > 2) return false;
const [rawAddress, rawPrefixLength] = addressParts;
const address = parseIpAddress(rawAddress);
if (!address) return false;
if (rawPrefixLength === undefined) return true;
const prefixLength = Number(rawPrefixLength);
const maxLength = address.kind() === 'ipv4' ? 32 : 128;
return Number.isInteger(prefixLength) && prefixLength > 0 && prefixLength <= maxLength;
};
// 按逗号/空白拆分 TRUSTED_PROXY_IPS,过滤非法项并去重打印一次警告;非 test 环境下提示运维。
const parseTrustedProxyIpEnv = (trustedProxyIpEnv?: string) => {
const validAddresses = new Set<string>();
const invalidAddresses = new Set<string>();
(trustedProxyIpEnv ?? '')
.split(/[,\s]+/)
.filter(Boolean)
.forEach((item) => {
if (isValidTrustedProxyAddress(item)) {
validAddresses.add(item);
} else {
invalidAddresses.add(item);
}
});
if (
invalidAddresses.size > 0 &&
process.env.NODE_ENV !== 'test' &&
warnedInvalidTrustedProxyIpEnv !== trustedProxyIpEnv
) {
warnedInvalidTrustedProxyIpEnv = trustedProxyIpEnv;
console.warn(
`[security:client-ip] Ignored invalid TRUSTED_PROXY_IPS entries: ${Array.from(
invalidAddresses
).join(', ')}`
);
}
return Array.from(validAddresses);
};
// 构建并缓存 proxy-addr 的信任判定函数;用于 TRUSTED_PROXY_ENABLE=true 的可信代理校验模式。
// 可信代理校验模式下,非生产环境默认信任 loopback,并叠加 TRUSTED_PROXY_IPS 配置。
// 仅当环境变量或 NODE_ENV 变化时才重新编译,避免每次请求都重复解析。
const getTrustProxyFn = () => {
const trustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const trustedProxyIpEnv = serviceEnv.TRUSTED_PROXY_IPS;
const nodeEnv = process.env.NODE_ENV;
if (
trustedProxyEnable === cachedTrustedProxyEnableEnv &&
trustedProxyIpEnv === cachedTrustedProxyIpEnv &&
nodeEnv === cachedNodeEnv
) {
return cachedTrustProxyFn;
}
cachedTrustedProxyEnableEnv = trustedProxyEnable;
cachedTrustedProxyIpEnv = trustedProxyIpEnv;
cachedNodeEnv = nodeEnv;
const trustedProxyAddresses = trustedProxyEnable
? [
...(nodeEnv === 'production' ? [] : (['loopback'] satisfies proxyaddr.Address[])),
...parseTrustedProxyIpEnv(trustedProxyIpEnv)
]
: [];
cachedTrustProxyFn = proxyaddr.compile(trustedProxyAddresses);
return cachedTrustProxyFn;
};
// 将地址对象转为小写字符串,统一 IPv6 大小写写法以便比较。
const normalizeIpAddress = (address: IPAddress) => address.toString().toLowerCase();
// 对外:把任意来源的 IP 字符串解析并归一化(去端口/方括号、小写),非法返回 undefined。
export const normalizeClientIp = (rawIp?: string | null) => {
const address = parseIpAddress(rawIp);
if (!address) return;
return normalizeIpAddress(address);
};
// 对外:判断给定 IP 是否在受信代理白名单内,供上游中间件决定是否采纳转发头。
export const isTrustedProxyIp = (rawIp?: string | null) => {
const ip = normalizeClientIp(rawIp);
if (!ip) return false;
return getTrustProxyFn()(ip, 0);
};
// 取 TCP 连接对端地址(socket / 旧版 connection 兜底),作为最可信的回退来源。
const getRemoteIp = (req: RequestWithClientIp) => {
return normalizeClientIp(req.socket?.remoteAddress ?? req.connection?.remoteAddress);
};
// 读取并归一化 X-Real-IP 头;通常由 Nginx 等单层代理设置为最初客户端 IP。
const getClientIpFromRealIp = (req: RequestWithClientIp) => {
const xRealIp = getHeaderValue(req.headers, 'x-real-ip');
return normalizeClientIp(xRealIp);
};
// 读取原始 X-Forwarded-For 头(不解析、不归一),后续校验和 proxy-addr 解析使用。
const getForwardedFor = (req: RequestWithClientIp) => {
return getHeaderValue(req.headers, 'x-forwarded-for');
};
// 关闭可信代理校验时的兼容模式:直接相信转发头。
// X-Forwarded-For 按行业约定取最左侧 IP;如果不存在或非法,再尝试 X-Real-IP。
const getClientIpFromForwardingHeaders = (req: RequestWithClientIp) => {
const forwardedFor = getForwardedFor(req);
if (forwardedFor && forwardedFor.length <= MAX_FORWARDED_FOR_LENGTH) {
const firstForwardedIp = forwardedFor.split(',')[0]?.trim();
const ip = normalizeClientIp(firstForwardedIp);
if (ip) return ip;
}
return getClientIpFromRealIp(req);
};
// 在调用 proxy-addr 前对 XFF 做尺寸/跳数/格式预检,防止超长或畸形头造成解析放大攻击。
const isForwardedForSafeToParse = (forwardedFor: string) => {
if (forwardedFor.length > MAX_FORWARDED_FOR_LENGTH) return false;
const hops = forwardedFor.split(',').map((hop) => hop.trim());
return (
hops.length > 0 &&
hops.length <= MAX_FORWARDED_FOR_HOPS &&
hops.every((hop) => Boolean(normalizeClientIp(hop)))
);
};
// 构造一个最小化的 IncomingMessage 形状对象供 proxy-addr 使用:
// 仅保留经过调用方校验的 XFF 头与指定 remoteAddress,避免外部 header 干扰判定。
const createProxyAddrRequest = (remoteAddress: string, forwardedFor: string) => {
return {
headers: {
'x-forwarded-for': forwardedFor
},
socket: {
remoteAddress
}
} as unknown as IncomingMessage;
};
// 对外:从请求中解析出最终客户端 IP。
// 策略:
// 1. TRUSTED_PROXY_ENABLE=false -> 兼容模式,直接相信 X-Forwarded-For / X-Real-IP,再回退远端 IP。
// 2. TRUSTED_PROXY_ENABLE=true -> 可信代理校验模式,先取 socket 远端 IP 作为底线;若不可解析直接返回 undefined。
// 3. 远端不在受信代理列表 -> 直接返回远端 IP,忽略一切转发头(防伪造)。
// 4. 远端可信 -> 优先用 X-Forwarded-For(经安全校验后交给 proxy-addr 沿信任链回溯),
// 否则回退 X-Real-IP;校验失败或转发头本身仍是受信代理时退回远端 IP。
export const getClientIpFromRequest = (req: RequestWithClientIp) => {
if (!serviceEnv.TRUSTED_PROXY_ENABLE) {
return getClientIpFromForwardingHeaders(req) ?? getRemoteIp(req);
}
const remoteIp = getRemoteIp(req);
if (!remoteIp) return;
const trustProxy = getTrustProxyFn();
if (trustProxy(remoteIp, 0)) {
const forwardedFor = getForwardedFor(req);
if (forwardedFor) {
if (!isForwardedForSafeToParse(forwardedFor)) {
return remoteIp;
}
const forwardedIp = normalizeClientIp(
proxyaddr(createProxyAddrRequest(remoteIp, forwardedFor), trustProxy)
);
if (forwardedIp && forwardedIp !== remoteIp && !trustProxy(forwardedIp, 0)) {
return forwardedIp;
}
return remoteIp;
}
const realIp = getClientIpFromRealIp(req);
return realIp && realIp !== remoteIp && !trustProxy(realIp, 0) ? realIp : remoteIp;
}
return remoteIp;
};
...@@ -30,9 +30,13 @@ export const authFrequencyLimit = async ({ ...@@ -30,9 +30,13 @@ export const authFrequencyLimit = async ({
).lean(); ).lean();
// 因为始终会返回+1的结果,所以这里不能直接等,需要多一个。 // 因为始终会返回+1的结果,所以这里不能直接等,需要多一个。
if (result.amount > maxAmount) { if (result.amount > maxAmount) {
return Promise.reject(ERROR_ENUM.uploadFileIntervalLimit); throw ERROR_ENUM.tooManyRequest;
} }
} catch (error) { } catch (error) {
if (error === ERROR_ENUM.tooManyRequest) {
throw error;
}
logger.error('Failed to update auth frequency limit', { eventId, error }); logger.error('Failed to update auth frequency limit', { eventId, error });
} }
}; };
...@@ -25,6 +25,7 @@ import { buildAgentTools } from './toolAdapter'; ...@@ -25,6 +25,7 @@ import { buildAgentTools } from './toolAdapter';
import { getLogger, LogCategories } from '../../../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../../common/logger';
import { serviceEnv } from '../../../../../../env'; import { serviceEnv } from '../../../../../../env';
import type { DispatchAgentModuleProps } from '..'; import type { DispatchAgentModuleProps } from '..';
import { Agent, type AgentEvent } from '@mariozechner/pi-agent-core';
type Response = DispatchNodeResultType<{ type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.answerText]: string;
...@@ -191,8 +192,6 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise< ...@@ -191,8 +192,6 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise<
: []; : [];
/* ===== Create & run Agent ===== */ /* ===== Create & run Agent ===== */
const { Agent } = await import('@mariozechner/pi-agent-core');
type AgentEvent = import('@mariozechner/pi-agent-core').AgentEvent;
const agent = new Agent({ const agent = new Agent({
initialState: { initialState: {
......
...@@ -103,12 +103,7 @@ export const serviceEnv = createEnv({ ...@@ -103,12 +103,7 @@ export const serviceEnv = createEnv({
.default( .default(
'mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true' 'mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true'
), ),
MONGODB_LOG_URI: z MONGODB_LOG_URI: z.string().optional(),
.string()
.default(
'mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true'
)
.optional(),
// VectorDB // VectorDB
VECTOR_VQ_LEVEL: IntSchema.default(32).meta({ VECTOR_VQ_LEVEL: IntSchema.default(32).meta({
...@@ -175,6 +170,14 @@ export const serviceEnv = createEnv({ ...@@ -175,6 +170,14 @@ export const serviceEnv = createEnv({
//==================== 安全配置 ==================== //==================== 安全配置 ====================
USE_IP_LIMIT: BoolSchema.default(false).meta({ description: '是否启用 IP 限流' }), USE_IP_LIMIT: BoolSchema.default(false).meta({ description: '是否启用 IP 限流' }),
CHECK_INTERNAL_IP: BoolSchema.default(false).meta({ description: '是否启用内网 IP 检查' }), CHECK_INTERNAL_IP: BoolSchema.default(false).meta({ description: '是否启用内网 IP 检查' }),
TRUSTED_PROXY_ENABLE: BoolSchema.default(false).meta({
description:
'是否启用可信反向代理客户端 IP 校验;关闭时兼容旧逻辑,直接信任 X-Forwarded-For/X-Real-IP'
}),
TRUSTED_PROXY_IPS: z.string().optional().meta({
description:
'可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析'
}),
PASSWORD_LOGIN_LOCK_SECONDS: defaultableIntSchema(120).meta({ PASSWORD_LOGIN_LOCK_SECONDS: defaultableIntSchema(120).meta({
description: '密码错误锁定时长(秒)' description: '密码错误锁定时长(秒)'
}), }),
...@@ -187,7 +190,6 @@ export const serviceEnv = createEnv({ ...@@ -187,7 +190,6 @@ export const serviceEnv = createEnv({
description: '是否强制将图片转成 base64 传递给模型' description: '是否强制将图片转成 base64 传递给模型'
}), }),
// ==================== 功能开关与特殊配置 ====================
//==================== Beta features ==================== //==================== Beta features ====================
SHOW_SKILL: BoolSchema.default(false).meta({ description: '是否展示 Skill 功能入口' }), SHOW_SKILL: BoolSchema.default(false).meta({ description: '是否展示 Skill 功能入口' }),
AGENT_ENGINE: z AGENT_ENGINE: z
......
...@@ -3,18 +3,11 @@ import type { ...@@ -3,18 +3,11 @@ import type {
LicenseDataType, LicenseDataType,
SystemEnvType SystemEnvType
} from '@fastgpt/global/common/system/types'; } from '@fastgpt/global/common/system/types';
import {
TTSModelType,
RerankModelItemType,
STTModelType,
EmbeddingModelItemType,
LLMModelItemType
} from '@fastgpt/global/core/ai/model.schema';
import type { SubPlanType } from '@fastgpt/global/support/wallet/sub/type'; import type { SubPlanType } from '@fastgpt/global/support/wallet/sub/type';
import type { WorkerNameEnum, WorkerPool } from './worker/utils'; import type { WorkerNameEnum, WorkerPool } from './worker/utils';
import { Worker } from 'worker_threads';
declare global { declare global {
var countTrackQueue: Map<string, { event: string; count: number; data: Record<string, any> }>;
var systemInitBufferId: string | undefined; var systemInitBufferId: string | undefined;
var systemVersion: string; var systemVersion: string;
...@@ -25,3 +18,5 @@ declare global { ...@@ -25,3 +18,5 @@ declare global {
var workerPoll: Record<WorkerNameEnum, WorkerPool>; var workerPoll: Record<WorkerNameEnum, WorkerPool>;
} }
export {};
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"ioredis": "^5.6.0", "ioredis": "^5.6.0",
"joplin-turndown-plugin-gfm": "^1.0.12", "joplin-turndown-plugin-gfm": "^1.0.12",
"ipaddr.js": "catalog:",
"json5": "catalog:", "json5": "catalog:",
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",
"jsonrepair": "^3.0.0", "jsonrepair": "^3.0.0",
...@@ -67,9 +68,9 @@ ...@@ -67,9 +68,9 @@
"pg": "^8.10.0", "pg": "^8.10.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-opentelemetry-transport": "^1.0.1", "pino-opentelemetry-transport": "^1.0.1",
"proxy-addr": "catalog:",
"proxy-agent": "catalog:", "proxy-agent": "catalog:",
"proxy-from-env": "^1.1.0", "proxy-from-env": "^1.1.0",
"request-ip": "catalog:",
"tiktoken": "1.0.17", "tiktoken": "1.0.17",
"turndown": "^7.1.2", "turndown": "^7.1.2",
"undici": "^7.18.2", "undici": "^7.18.2",
...@@ -87,8 +88,8 @@ ...@@ -87,8 +88,8 @@
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/papaparse": "5.3.7", "@types/papaparse": "5.3.7",
"@types/pg": "^8.6.6", "@types/pg": "^8.6.6",
"@types/proxy-addr": "catalog:",
"@types/proxy-from-env": "^1.0.4", "@types/proxy-from-env": "^1.0.4",
"@types/request-ip": "catalog:",
"@types/tunnel": "^0.0.4", "@types/tunnel": "^0.0.4",
"@types/turndown": "^5.0.4" "@types/turndown": "^5.0.4"
} }
......
...@@ -8,6 +8,17 @@ import { ...@@ -8,6 +8,17 @@ import {
type NextApiRequest type NextApiRequest
} from '@fastgpt/service/common/geo'; } from '@fastgpt/service/common/geo';
import { cleanupIntervalMs } from '@fastgpt/service/common/geo/constants'; import { cleanupIntervalMs } from '@fastgpt/service/common/geo/constants';
import { serviceEnv } from '@fastgpt/service/env';
const originalTrustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const setTrustedProxyEnable = (value: boolean) => {
serviceEnv.TRUSTED_PROXY_ENABLE = value;
};
afterEach(() => {
setTrustedProxyEnable(originalTrustedProxyEnable);
});
describe('getGeoReader', () => { describe('getGeoReader', () => {
it('should return a reader instance', () => { it('should return a reader instance', () => {
...@@ -174,10 +185,12 @@ describe('getIpFromRequest', () => { ...@@ -174,10 +185,12 @@ describe('getIpFromRequest', () => {
}); });
it('should return the IP from x-forwarded-for header', () => { it('should return the IP from x-forwarded-for header', () => {
setTrustedProxyEnable(true);
const req = { const req = {
headers: { 'x-forwarded-for': '203.0.113.50' }, headers: { 'x-forwarded-for': '203.0.113.50' },
connection: {}, connection: {},
socket: {} socket: { remoteAddress: '127.0.0.1' }
} as unknown as NextApiRequest; } as unknown as NextApiRequest;
const ip = getIpFromRequest(req); const ip = getIpFromRequest(req);
...@@ -185,13 +198,28 @@ describe('getIpFromRequest', () => { ...@@ -185,13 +198,28 @@ describe('getIpFromRequest', () => {
}); });
it('should return the IP from x-real-ip header', () => { it('should return the IP from x-real-ip header', () => {
setTrustedProxyEnable(true);
const req = { const req = {
headers: { 'x-real-ip': '198.51.100.10' }, headers: { 'x-real-ip': '198.51.100.10' },
connection: {}, connection: {},
socket: {} socket: { remoteAddress: '127.0.0.1' }
} as unknown as NextApiRequest; } as unknown as NextApiRequest;
const ip = getIpFromRequest(req); const ip = getIpFromRequest(req);
expect(ip).toBe('198.51.100.10'); expect(ip).toBe('198.51.100.10');
}); });
it('should ignore spoofed IP headers from untrusted direct clients', () => {
setTrustedProxyEnable(true);
const req = {
headers: { 'x-forwarded-for': '203.0.113.50', 'x-real-ip': '198.51.100.10' },
connection: {},
socket: { remoteAddress: '192.0.2.20' }
} as unknown as NextApiRequest;
const ip = getIpFromRequest(req);
expect(ip).toBe('192.0.2.20');
});
}); });
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { MongoFrequencyLimit } from '@fastgpt/service/common/system/frequencyLimit/schema';
import { jsonRes } from '@fastgpt/service/common/response';
import { serviceEnv } from '@fastgpt/service/env';
const originalUseIpLimit = serviceEnv.USE_IP_LIMIT;
const originalTrustedProxyEnable = serviceEnv.TRUSTED_PROXY_ENABLE;
const setUseIpLimit = (value: boolean) => {
serviceEnv.USE_IP_LIMIT = value;
};
const setTrustedProxyEnable = (value: boolean) => {
serviceEnv.TRUSTED_PROXY_ENABLE = value;
};
const createRes = () =>
({
setHeader: vi.fn(),
status: vi.fn().mockReturnThis(),
json: vi.fn(),
end: vi.fn()
}) as any;
const createReq = ({
headers = {},
remoteAddress
}: {
headers?: Record<string, string>;
remoteAddress?: string;
}) =>
({
headers,
socket: {
remoteAddress
}
}) as any;
describe('useIPFrequencyLimit', () => {
beforeEach(async () => {
vi.clearAllMocks();
await MongoFrequencyLimit.deleteMany({
eventId: /^ip-qps-limit-ip-spoof-test-/
});
});
afterEach(() => {
setUseIpLimit(originalUseIpLimit);
setTrustedProxyEnable(originalTrustedProxyEnable);
});
it('should enforce IP limit when USE_IP_LIMIT is enabled without force', async () => {
setUseIpLimit(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-toggle-enabled',
seconds: 60,
limit: 10
});
await middleware(
createReq({
remoteAddress: '198.51.100.40'
}),
createRes()
);
const record = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-toggle-enabled-198.51.100.40'
}).lean();
expect(record?.amount).toBe(1);
});
it('should skip IP limit when USE_IP_LIMIT is disabled without force', async () => {
setUseIpLimit(false);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-toggle-disabled',
seconds: 60,
limit: 10
});
await middleware(
createReq({
remoteAddress: '198.51.100.41'
}),
createRes()
);
const record = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-toggle-disabled-198.51.100.41'
}).lean();
expect(record).toBeNull();
});
it('should enforce IP limit when force is true even if USE_IP_LIMIT is disabled', async () => {
setUseIpLimit(false);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-toggle-forced',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '198.51.100.42'
}),
createRes()
);
const record = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-toggle-forced-198.51.100.42'
}).lean();
expect(record?.amount).toBe(1);
});
it('should ignore spoofed forwarding headers from untrusted direct clients', async () => {
setTrustedProxyEnable(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-direct',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '198.51.100.20',
headers: {
'x-forwarded-for': '203.0.113.50',
'x-real-ip': '203.0.113.51'
}
}),
createRes()
);
const realIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-direct-198.51.100.20'
}).lean();
const spoofedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-direct-203.0.113.50'
}).lean();
expect(realIpRecord?.amount).toBe(1);
expect(spoofedIpRecord).toBeNull();
});
it('should use X-Forwarded-For as the limit key when trusted proxy parsing is disabled', async () => {
setTrustedProxyEnable(false);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-compat',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '172.16.0.119',
headers: {
'x-forwarded-for': '60.186.209.23',
'x-real-ip': '60.186.209.23'
}
}),
createRes()
);
const forwardedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-compat-60.186.209.23'
}).lean();
const remoteIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-compat-172.16.0.119'
}).lean();
expect(forwardedIpRecord?.amount).toBe(1);
expect(remoteIpRecord).toBeNull();
});
it('should use proxy-addr result for trusted proxy forwarding chains', async () => {
setTrustedProxyEnable(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-proxy',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
remoteAddress: '127.0.0.1',
headers: {
'x-forwarded-for': '6.6.6.6, 203.0.113.50'
}
}),
createRes()
);
const clientIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-proxy-203.0.113.50'
}).lean();
const spoofedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-proxy-6.6.6.6'
}).lean();
expect(clientIpRecord?.amount).toBe(1);
expect(spoofedIpRecord).toBeNull();
});
it('should use a shared fail-closed key when client IP cannot be resolved', async () => {
setTrustedProxyEnable(true);
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-unknown',
seconds: 60,
limit: 10,
force: true
});
await middleware(
createReq({
headers: {
'x-forwarded-for': '203.0.113.50'
}
}),
createRes()
);
const unknownRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-unknown-unknown'
}).lean();
const spoofedIpRecord = await MongoFrequencyLimit.findOne({
eventId: 'ip-qps-limit-ip-spoof-test-unknown-203.0.113.50'
}).lean();
expect(unknownRecord?.amount).toBe(1);
expect(spoofedIpRecord).toBeNull();
});
it('should block requests after the IP limit is exceeded', async () => {
const middleware = useIPFrequencyLimit({
id: 'ip-spoof-test-block',
seconds: 60,
limit: 1,
force: true
});
const firstRes = createRes();
const secondRes = createRes();
const req = createReq({
remoteAddress: '198.51.100.30'
});
await middleware(req, firstRes);
await middleware(req, secondRes);
expect(jsonRes).toHaveBeenCalledTimes(1);
expect(jsonRes).toHaveBeenCalledWith(
secondRes,
expect.objectContaining({
code: 429
})
);
});
});
declare global {
var countTrackQueue: Map<string, { event: string; count: number; data: Record<string, any> }>;
}
export {};
...@@ -28,6 +28,7 @@ catalog: ...@@ -28,6 +28,7 @@ catalog:
'@types/react': ^18 '@types/react': ^18
'@types/react-dom': ^18 '@types/react-dom': ^18
'@types/request-ip': ^0.0.38 '@types/request-ip': ^0.0.38
'@types/proxy-addr': 2.0.3
'@typescript-eslint/eslint-plugin': ^6.21.0 '@typescript-eslint/eslint-plugin': ^6.21.0
'@typescript-eslint/parser': ^6.21.0 '@typescript-eslint/parser': ^6.21.0
'@vitest/coverage-v8': ^4.1.5 '@vitest/coverage-v8': ^4.1.5
...@@ -40,7 +41,8 @@ catalog: ...@@ -40,7 +41,8 @@ catalog:
'@chakra-ui/system': ^2 '@chakra-ui/system': ^2
'@emotion/react': ^11 '@emotion/react': ^11
'@emotion/styled': ^11 '@emotion/styled': ^11
axios: 1.15.2 axios: ^1.16.0
ipaddr.js: ^2.4.0
chalk: ^5.6.2 chalk: ^5.6.2
date-fns: ^3.6.0 date-fns: ^3.6.0
dayjs: 1.11.19 dayjs: 1.11.19
...@@ -61,6 +63,7 @@ catalog: ...@@ -61,6 +63,7 @@ catalog:
nanoid: ^5.1.3 nanoid: ^5.1.3
next: 16.2.4 next: 16.2.4
next-i18next: 15.4.2 next-i18next: 15.4.2
proxy-addr: 2.0.7
proxy-agent: ^6 proxy-agent: ^6
react: ^18 react: ^18
react-dom: ^18 react-dom: ^18
......
Subproject commit f269963dae71655c7eb82d8a5e20a9ecaf9d3cdd Subproject commit a10974c0a73ca45f4e041738e5adb227923e289c
...@@ -10,7 +10,7 @@ SYNC_INDEX=true ...@@ -10,7 +10,7 @@ SYNC_INDEX=true
# ==================== 密钥 ==================== # ==================== 密钥 ====================
# 用户账号密钥 # 用户账号密钥
TOKEN_KEY=fastgpt TOKEN_KEY=fastgpt
# 文件阅读时的密钥,必须是 32 位以上的随机字符串 # 文件阅读时的密钥
FILE_TOKEN_KEY= FILE_TOKEN_KEY=
# 密钥加密 key # 密钥加密 key
AES256_SECRET_KEY=fastgptsecret AES256_SECRET_KEY=fastgptsecret
...@@ -88,9 +88,9 @@ REDIS_URL=redis://default:mypassword@localhost:6379 ...@@ -88,9 +88,9 @@ REDIS_URL=redis://default:mypassword@localhost:6379
# STREAM_RESUME_REDIS_MEMORY_CHECK_INTERVAL_MS=5000 # STREAM_RESUME_REDIS_MEMORY_CHECK_INTERVAL_MS=5000
# MongoDB 连接参数;本地开发连接远程数据库时,可能需要添加 directConnection=true 才能连接 # MongoDB 连接参数;本地开发连接远程数据库时,可能需要添加 directConnection=true 才能连接
MONGODB_URI="mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true" MONGODB_URI=mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true
# 日志库 # 日志库
MONGODB_LOG_URI="mongodb://myusername:mypassword@localhost:27017/fastgpt?authSource=admin&directConnection=true" MONGODB_LOG_URI=
# 向量库优先级: pg > oceanbase > milvus > opengauss # 向量库优先级: pg > oceanbase > milvus > opengauss
# 向量量化等级: PG 支持 32/16,OceanBase 支持 32/8/1 # 向量量化等级: PG 支持 32/16,OceanBase 支持 32/8/1
...@@ -140,6 +140,10 @@ FILE_DOMAIN=http://localhost:3000 ...@@ -140,6 +140,10 @@ FILE_DOMAIN=http://localhost:3000
USE_IP_LIMIT=false USE_IP_LIMIT=false
# 启用内网 IP 检查 # 启用内网 IP 检查
CHECK_INTERNAL_IP=false CHECK_INTERNAL_IP=false
# 是否启用可信反向代理客户端 IP 校验
TRUSTED_PROXY_ENABLE=false
# 可信反向代理 IP/CIDR 列表,逗号或空白分隔。仅 TRUSTED_PROXY_ENABLE=true 时生效;仅显式可信代理传入的 X-Forwarded-For/X-Real-IP 会用于客户端 IP 解析
TRUSTED_PROXY_IPS=
# 密码错误锁定时长(秒) # 密码错误锁定时长(秒)
PASSWORD_LOGIN_LOCK_SECONDS= PASSWORD_LOGIN_LOCK_SECONDS=
# 密码过期月份(不设置则不过期) # 密码过期月份(不设置则不过期)
......
...@@ -86,7 +86,6 @@ ...@@ -86,7 +86,6 @@
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-gfm": "catalog:", "remark-gfm": "catalog:",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"request-ip": "catalog:",
"sass": "^1.58.3", "sass": "^1.58.3",
"undici": "^7.18.2", "undici": "^7.18.2",
"use-context-selector": "^1.4.4", "use-context-selector": "^1.4.4",
...@@ -106,7 +105,6 @@ ...@@ -106,7 +105,6 @@
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "catalog:",
"@typescript-eslint/eslint-plugin": "catalog:", "@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:", "@typescript-eslint/parser": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
......
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { getCodeServerPasswordFromSandbox } from '@/service/core/sandbox/proxy'; import { getCodeServerPasswordFromSandbox } from '@/service/core/sandbox/proxy';
import { normalizeClientIp } from '@fastgpt/service/common/security/clientIp';
// Internal-only endpoint: read the code-server password from the container config.yaml. // Internal-only endpoint: read the code-server password from the container config.yaml.
// Called by server.ts (running in the same process) to avoid importing service packages directly. // Called by server.ts (running in the same process) to avoid importing service packages directly.
// Only requests from 127.0.0.1 are accepted. // Only requests from 127.0.0.1 are accepted.
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
const clientIp = req.socket.remoteAddress; const clientIp = normalizeClientIp(req.socket.remoteAddress);
if (clientIp !== '127.0.0.1' && clientIp !== '::1' && clientIp !== '::ffff:127.0.0.1') { if (clientIp !== '127.0.0.1' && clientIp !== '::1') {
return res.status(403).json({ error: 'Internal only' }); return res.status(403).json({ error: 'Internal only' });
} }
......
...@@ -11,7 +11,6 @@ import { serviceEnv } from '@fastgpt/service/env'; ...@@ -11,7 +11,6 @@ import { serviceEnv } from '@fastgpt/service/env';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { authCode } from '@fastgpt/service/support/user/auth/controller'; import { authCode } from '@fastgpt/service/support/user/auth/controller';
import { createUserSession } from '@fastgpt/service/support/user/session'; import { createUserSession } from '@fastgpt/service/support/user/session';
import requestIp from 'request-ip';
import { setCookie } from '@fastgpt/service/support/permission/auth/common'; import { setCookie } from '@fastgpt/service/support/permission/auth/common';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
import { import {
...@@ -20,6 +19,7 @@ import { ...@@ -20,6 +19,7 @@ import {
type LoginSuccessResponseType type LoginSuccessResponseType
} from '@fastgpt/global/openapi/support/user/account/login/api'; } from '@fastgpt/global/openapi/support/user/account/login/api';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { getClientIpFromRequest } from '@fastgpt/service/common/security/clientIp';
async function handler( async function handler(
req: ApiRequestProps<LoginByPasswordBodyType>, req: ApiRequestProps<LoginByPasswordBodyType>,
...@@ -66,7 +66,7 @@ async function handler( ...@@ -66,7 +66,7 @@ async function handler(
teamId: userDetail.team.teamId, teamId: userDetail.team.teamId,
tmbId: userDetail.team.tmbId, tmbId: userDetail.team.tmbId,
isRoot: username === 'root', isRoot: username === 'root',
ip: requestIp.getClientIp(req) ip: getClientIpFromRequest(req)
}); });
setCookie(res, token); setCookie(res, token);
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
export async function register() { export async function register() {
try { try {
if (process.env.NEXT_RUNTIME === 'nodejs') { if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('@/env');
const { configureLogger, getLogger, LogCategories } = await import('@/service/logger'); const { configureLogger, getLogger, LogCategories } = await import('@/service/logger');
await runInitializationStep({ await runInitializationStep({
step: 'configure-logger', step: 'configure-logger',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment