Files
FastGPT/packages/service/common/system/utils.ts
T
Archer aaa7d17ef1 V4.14.9 dev (#6555)
* feat: encapsulate logger (#6535)

* feat: encapsulate logger

* update engines

---------

Co-authored-by: archer <545436317@qq.com>

* next config

* dev shell

* Agent sandbox (#6532)

* docs: switch to docs layout and apply black theme (#6533)

* feat: add Gemini 3.1 models

- Add gemini-3.1-pro-preview (released February 19, 2026)
- Add gemini-3.1-flash-lite-preview (released March 3, 2026)

Both models support:
- 1M context window
- 64k max response
- Vision
- Tool choice

* docs: switch to docs layout and apply black theme

- Change layout from notebook to docs
- Update logo to icon + text format
- Apply fumadocs black theme
- Simplify global.css (keep only navbar and TOC styles)
- Fix icon components to properly accept className props
- Add mobile text overflow handling
- Update Node engine requirement to >=20.x

* doc

* doc

* lock

* fix: ts

* doc

* doc

---------

Co-authored-by: archer <archer@archerdeMac-mini.local>
Co-authored-by: archer <545436317@qq.com>

* Doc (#6493)

* cloud doc

* doc refactor

* doc move

* seo

* remove doc

* yml

* doc

* fix: tsconfig

* fix: tsconfig

* sandbox version (#6497)

* sandbox version

* add sandbox log

* update lock

* fix

* fix: sandbox

* doc

* add console

* i18n

* sandbxo in agent

* feat: agent sandbox

* lock

* feat: sandbox ui

* sandbox check exists

* env tempalte

* doc

* lock

* sandbox in chat window

* sandbox entry

* fix: test

* rename var

* sandbox config tip

* update sandbox lifecircle

* update prompt

* rename provider test

* sandbox logger

* yml

---------

Co-authored-by: Archer <archer@fastgpt.io>
Co-authored-by: archer <archer@archerdeMac-mini.local>

* perf: sandbox error tip

* Add sandbox limit and fix some issue (#6550)

* sandbox in plan

* fix: some issue

* fix: test

* editor default path

* fix: comment

* perf: sandbox worksapce

* doc

* perf: del sandbox

* sandbox build

* fix: test

* fix: pr comment

---------

Co-authored-by: Ryo <whoeverimf5@gmail.com>
Co-authored-by: Archer <archer@fastgpt.io>
Co-authored-by: archer <archer@archerdeMac-mini.local>
2026-03-16 17:09:25 +08:00

194 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isIP, isIPv6 } from 'net';
import dns from 'dns/promises';
const isDevEnv = process.env.NODE_ENV === 'development';
const SERVICE_LOCAL_PORT = `${process.env.PORT || 3000}`;
const SERVICE_LOCAL_HOST =
process.env.HOSTNAME && isIPv6(process.env.HOSTNAME)
? `[${process.env.HOSTNAME}]:${SERVICE_LOCAL_PORT}`
: `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`;
export const isInternalAddress = async (url: string): Promise<boolean> => {
const isInternalIPv6 = (ip: string): boolean => {
// 移除 IPv6 地址中的方括号(如果有)
const cleanIp = ip.replace(/^\[|\]$/g, '');
// 检查 IPv4-mapped IPv6 地址(格式:::ffff:xxxx:xxxx
// Node.js URL 解析器会将 IPv4 部分转换为十六进制
const ipv4MappedPattern = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
const ipv4MappedMatch = cleanIp.match(ipv4MappedPattern);
if (ipv4MappedMatch) {
// 将十六进制转换回 IPv4 地址
const hex1 = parseInt(ipv4MappedMatch[1], 16);
const hex2 = parseInt(ipv4MappedMatch[2], 16);
// hex1 包含前两个字节,hex2 包含后两个字节
const byte1 = (hex1 >> 8) & 0xff;
const byte2 = hex1 & 0xff;
const byte3 = (hex2 >> 8) & 0xff;
const byte4 = hex2 & 0xff;
const ipv4 = `${byte1}.${byte2}.${byte3}.${byte4}`;
return isInternalIPv4(ipv4);
}
// IPv6 内部地址范围
const internalIPv6Patterns = [
/^::1$/, // Loopback
/^::$/, // Unspecified
/^fe80:/i, // Link-local
/^fc00:/i, // Unique local address (ULA)
/^fd00:/i, // Unique local address (ULA)
/^::ffff:0:0/i, // IPv4-mapped IPv6
/^::ffff:127\./i, // IPv4-mapped loopback
/^::ffff:10\./i, // IPv4-mapped private (10.0.0.0/8)
/^::ffff:172\.(1[6-9]|2[0-9]|3[0-1])\./i, // IPv4-mapped private (172.16.0.0/12)
/^::ffff:192\.168\./i, // IPv4-mapped private (192.168.0.0/16)
/^::ffff:169\.254\./i, // IPv4-mapped link-local
/^::ffff:100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./i // IPv4-mapped shared address space
];
return internalIPv6Patterns.some((pattern) => pattern.test(cleanIp));
};
const isInternalIPv4 = (ip: string): boolean => {
// 验证是否为有效的 IPv4 格式
const ipv4Pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const match = ip.match(ipv4Pattern);
if (!match) {
return false;
}
// 解析 IP 地址的各个部分
const parts = [
parseInt(match[1], 10),
parseInt(match[2], 10),
parseInt(match[3], 10),
parseInt(match[4], 10)
];
// 验证每个部分是否在有效范围内 (0-255)
if (parts.some((part) => part < 0 || part > 255)) {
return false;
}
// 检查是否为内部 IP 地址范围
return (
parts[0] === 0 || // 0.0.0.0/8 - Current network
parts[0] === 10 || // 10.0.0.0/8 - Private network
parts[0] === 127 || // 127.0.0.0/8 - Loopback
(parts[0] === 169 && parts[1] === 254) || // 169.254.0.0/16 - Link-local
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12 - Private network
(parts[0] === 192 && parts[1] === 168) || // 192.168.0.0/16 - Private network
(parts[0] >= 224 && parts[0] <= 239) || // 224.0.0.0/4 - Multicast
(parts[0] >= 240 && parts[0] <= 255) || // 240.0.0.0/4 - Reserved
(parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) || // 100.64.0.0/10 - Shared address space
(parts[0] === 192 && parts[1] === 0 && parts[2] === 0) || // 192.0.0.0/24 - IETF Protocol Assignments
(parts[0] === 192 && parts[1] === 0 && parts[2] === 2) || // 192.0.2.0/24 - Documentation (TEST-NET-1)
(parts[0] === 198 && parts[1] === 18) || // 198.18.0.0/15 - Benchmarking
(parts[0] === 198 && parts[1] === 19) || // 198.18.0.0/15 - Benchmarking
(parts[0] === 198 && parts[1] === 51 && parts[2] === 100) || // 198.51.100.0/24 - Documentation (TEST-NET-2)
(parts[0] === 203 && parts[1] === 0 && parts[2] === 113) // 203.0.113.0/24 - Documentation (TEST-NET-3)
);
};
try {
const parsedUrl = new URL(url);
// 移除 IPv6 地址的方括号(如果有)
const hostname = parsedUrl.hostname.replace(/^\[|\]$/g, '');
const fullUrl = parsedUrl.toString();
// 1. 检查 localhost 和常见的本地域名变体
const localhostVariants = ['localhost', '127.0.0.1', '::1', '0.0.0.0'];
const localHostname = SERVICE_LOCAL_HOST.split(':')[0];
if (localhostVariants.includes(hostname) || hostname === localHostname) {
return true;
}
// 2. 检查云服务商元数据端点(始终阻止,无论 CHECK_INTERNAL_IP 设置如何)
const metadataEndpoints = [
// AWS
'http://169.254.169.254/',
'http://[fd00:ec2::254]/',
// Azure
'http://169.254.169.254/',
// GCP
'http://metadata.google.internal/',
'http://metadata/',
// Alibaba Cloud
'http://100.100.100.200/',
// Tencent Cloud
'http://metadata.tencentyun.com/',
// Huawei Cloud
'http://169.254.169.254/',
// Oracle Cloud
'http://169.254.169.254/',
// DigitalOcean
'http://169.254.169.254/',
// Kubernetes
'http://kubernetes.default.svc/',
'https://kubernetes.default.svc/'
];
if (metadataEndpoints.some((endpoint) => fullUrl.startsWith(endpoint))) {
return true;
}
// 3. 只有显式设置 CHECK_INTERNAL_IP=true 时才启用私有 IP 检查
if (process.env.CHECK_INTERNAL_IP !== 'true') {
return false;
}
// 4. 使用 Node.js 的 isIP 函数检测 IP 版本
const ipVersion = isIP(hostname);
if (ipVersion === 4) {
// IPv4 地址检查
return isInternalIPv4(hostname);
} else if (ipVersion === 6) {
// IPv6 地址检查
return isInternalIPv6(hostname);
} else {
// 不是 IP 地址,是域名 - 需要解析
try {
// 解析所有 A 和 AAAA 记录
const [ipv4Addresses, ipv6Addresses] = await Promise.allSettled([
dns.resolve4(hostname),
dns.resolve6(hostname)
]);
// 检查所有解析的 IP 是否为内部地址
const allIPs = [
...(ipv4Addresses.status === 'fulfilled' ? ipv4Addresses.value : []),
...(ipv6Addresses.status === 'fulfilled' ? ipv6Addresses.value : [])
];
// 如果任何一个解析的 IP 是内部地址,则拒绝
for (const ip of allIPs) {
if (isInternalIPv4(ip) || isInternalIPv6(ip)) {
return true;
}
}
return false;
} catch (error) {
return false;
}
}
} catch (error) {
// URL 解析失败 - 宽松策略:允许访问
return false;
}
};
export const PRIVATE_URL_TEXT = 'Request to private network not allowed';