mirror of
https://github.com/labring/FastGPT.git
synced 2026-04-26 02:07:28 +08:00
dbc443a770
* fix: http tool * fix: http tool * fix: test * fix: test * fix: test * fix: test
456 lines
19 KiB
TypeScript
456 lines
19 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
||
import { isInternalAddress } from '@fastgpt/service/common/system/utils';
|
||
|
||
// Mock dns module
|
||
vi.mock('node:dns/promises', () => ({
|
||
default: {
|
||
resolve4: vi.fn(),
|
||
resolve6: vi.fn()
|
||
},
|
||
resolve4: vi.fn(),
|
||
resolve6: vi.fn()
|
||
}));
|
||
|
||
// Import mocked dns after mock setup
|
||
import * as dns from 'node:dns/promises';
|
||
|
||
describe('SSRF Protection - isInternalAddress', () => {
|
||
const originalEnv = process.env.CHECK_INTERNAL_IP;
|
||
|
||
beforeEach(() => {
|
||
process.env.CHECK_INTERNAL_IP = 'true';
|
||
// 清除所有 mock
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
afterEach(() => {
|
||
// 恢复原始环境变量
|
||
if (originalEnv !== undefined) {
|
||
process.env.CHECK_INTERNAL_IP = originalEnv;
|
||
} else {
|
||
delete process.env.CHECK_INTERNAL_IP;
|
||
}
|
||
});
|
||
|
||
describe('Localhost 检查(始终阻止)', () => {
|
||
test('应该阻止 localhost', async () => {
|
||
expect(await isInternalAddress('http://localhost/')).toBe(true);
|
||
expect(await isInternalAddress('http://localhost:8080/')).toBe(true);
|
||
expect(await isInternalAddress('https://localhost/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 127.0.0.1', async () => {
|
||
expect(await isInternalAddress('http://127.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://127.0.0.1:8080/')).toBe(true);
|
||
expect(await isInternalAddress('https://127.0.0.1/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv6 loopback', async () => {
|
||
expect(await isInternalAddress('http://[::1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[::1]:8080/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 0.0.0.0', async () => {
|
||
expect(await isInternalAddress('http://0.0.0.0/')).toBe(true);
|
||
expect(await isInternalAddress('http://0.0.0.0:8080/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('云元数据端点检查(始终阻止)', () => {
|
||
test('应该阻止 AWS 元数据端点', async () => {
|
||
expect(await isInternalAddress('http://169.254.169.254/latest/meta-data/')).toBe(true);
|
||
expect(await isInternalAddress('http://169.254.169.254/latest/user-data/')).toBe(true);
|
||
expect(await isInternalAddress('http://169.254.169.254/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 GCP 元数据端点', async () => {
|
||
expect(await isInternalAddress('http://metadata.google.internal/')).toBe(true);
|
||
expect(await isInternalAddress('http://metadata.google.internal/computeMetadata/v1/')).toBe(
|
||
true
|
||
);
|
||
expect(await isInternalAddress('http://metadata/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 Alibaba Cloud 元数据端点', async () => {
|
||
expect(await isInternalAddress('http://100.100.100.200/')).toBe(true);
|
||
expect(await isInternalAddress('http://100.100.100.200/latest/meta-data/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 Kubernetes 服务端点', async () => {
|
||
expect(await isInternalAddress('http://kubernetes.default.svc/')).toBe(true);
|
||
expect(await isInternalAddress('https://kubernetes.default.svc/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('CHECK_INTERNAL_IP 未设置时(默认行为 - 安全优先)', () => {
|
||
beforeEach(() => {
|
||
delete process.env.CHECK_INTERNAL_IP;
|
||
});
|
||
|
||
test('应该允许公共 IP 地址', async () => {
|
||
expect(await isInternalAddress('http://8.8.8.8/')).toBe(false);
|
||
expect(await isInternalAddress('http://1.1.1.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://93.184.216.34/')).toBe(false);
|
||
});
|
||
|
||
test('应该阻止私有 IP 地址(默认启用安全检查)', async () => {
|
||
expect(await isInternalAddress('http://10.0.0.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://172.16.0.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://192.168.1.1/')).toBe(false);
|
||
});
|
||
|
||
test('应该阻止解析到私有 IP 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['10.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://internal.example.com/')).toBe(false);
|
||
});
|
||
|
||
test('应该允许解析到公共 IP 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://example.com/')).toBe(false);
|
||
});
|
||
|
||
test('应该阻止 localhost 和元数据端点', async () => {
|
||
expect(await isInternalAddress('http://localhost/')).toBe(true);
|
||
expect(await isInternalAddress('http://127.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://169.254.169.254/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('CHECK_INTERNAL_IP=false 时(向后兼容模式)', () => {
|
||
beforeEach(() => {
|
||
process.env.CHECK_INTERNAL_IP = 'false';
|
||
});
|
||
|
||
test('应该允许公共 IP 地址', async () => {
|
||
expect(await isInternalAddress('http://8.8.8.8/')).toBe(false);
|
||
expect(await isInternalAddress('http://1.1.1.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://93.184.216.34/')).toBe(false);
|
||
});
|
||
|
||
test('应该允许私有 IP 地址(向后兼容)', async () => {
|
||
expect(await isInternalAddress('http://10.0.0.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://172.16.0.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://192.168.1.1/')).toBe(false);
|
||
});
|
||
|
||
test('应该允许域名(不进行 DNS 解析)', async () => {
|
||
expect(await isInternalAddress('http://example.com/')).toBe(false);
|
||
expect(await isInternalAddress('https://www.google.com/')).toBe(false);
|
||
});
|
||
|
||
test('但仍然阻止 localhost 和元数据端点', async () => {
|
||
expect(await isInternalAddress('http://localhost/')).toBe(true);
|
||
expect(await isInternalAddress('http://127.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://169.254.169.254/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('CHECK_INTERNAL_IP=true 时(启用完整检查)', () => {
|
||
test('应该允许公共 IP 地址', async () => {
|
||
expect(await isInternalAddress('http://8.8.8.8/')).toBe(false);
|
||
expect(await isInternalAddress('http://1.1.1.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://93.184.216.34/')).toBe(false);
|
||
});
|
||
|
||
test('应该阻止私有 IPv4 地址 - 10.0.0.0/8', async () => {
|
||
expect(await isInternalAddress('http://10.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://10.255.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止私有 IPv4 地址 - 172.16.0.0/12', async () => {
|
||
expect(await isInternalAddress('http://172.16.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://172.31.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止私有 IPv4 地址 - 192.168.0.0/16', async () => {
|
||
expect(await isInternalAddress('http://192.168.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://192.168.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 link-local 地址 - 169.254.0.0/16', async () => {
|
||
expect(await isInternalAddress('http://169.254.1.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://169.254.169.254/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 shared address space - 100.64.0.0/10', async () => {
|
||
expect(await isInternalAddress('http://100.64.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://100.127.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 multicast 地址 - 224.0.0.0/4', async () => {
|
||
expect(await isInternalAddress('http://224.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://239.255.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 reserved 地址 - 240.0.0.0/4', async () => {
|
||
expect(await isInternalAddress('http://240.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://255.255.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 documentation 地址', async () => {
|
||
expect(await isInternalAddress('http://192.0.2.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://198.51.100.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://203.0.113.1/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 benchmarking 地址 - 198.18.0.0/15', async () => {
|
||
expect(await isInternalAddress('http://198.18.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://198.19.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv6 link-local 地址', async () => {
|
||
expect(await isInternalAddress('http://[fe80::1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[fe80::abcd:1234]/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv6 unique local 地址', async () => {
|
||
expect(await isInternalAddress('http://[fc00::1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[fd00::1]/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv6 unspecified 地址', async () => {
|
||
expect(await isInternalAddress('http://[::]/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv4-mapped IPv6 私有地址', async () => {
|
||
expect(await isInternalAddress('http://[::ffff:10.0.0.1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[::ffff:192.168.1.1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[::ffff:127.0.0.1]/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('DNS 解析功能测试(CHECK_INTERNAL_IP=true)', () => {
|
||
test('应该阻止解析到私有 IPv4 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['10.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://internal.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止解析到 localhost 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['127.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://localhost.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止解析到 link-local 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['169.254.169.254']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://metadata.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止解析到私有 IPv6 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockRejectedValue(new Error('No A records'));
|
||
vi.mocked(dns.resolve6).mockResolvedValue(['fc00::1']);
|
||
|
||
expect(await isInternalAddress('http://internal-v6.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止解析到多个 IP 且包含私有 IP 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8', '10.0.0.1', '1.1.1.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://mixed.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该允许解析到公共 IP 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8', '1.1.1.1']);
|
||
vi.mocked(dns.resolve6).mockResolvedValue(['2001:4860:4860::8888']);
|
||
|
||
expect(await isInternalAddress('http://public.example.com/')).toBe(false);
|
||
});
|
||
|
||
test('应该允许 DNS 解析失败的域名(宽松策略)', async () => {
|
||
vi.mocked(dns.resolve4).mockRejectedValue(new Error('DNS resolution failed'));
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('DNS resolution failed'));
|
||
|
||
// 修改:DNS 解析失败时返回 false(允许访问)
|
||
expect(await isInternalAddress('http://nonexistent.example.com/')).toBe(false);
|
||
});
|
||
|
||
test('应该阻止 DNS 重绑定攻击尝试', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['127.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://127.0.0.1.nip.io/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 xip.io 类型的域名(解析到私有 IP)', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['10.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://10.0.0.1.xip.io/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 nip.io 类型的域名(解析到私有 IP)', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['192.168.1.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://192.168.1.1.nip.io/')).toBe(true);
|
||
});
|
||
|
||
test('应该允许 xip.io 类型的域名(解析到公共 IP)', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://8.8.8.8.xip.io/')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理只有 IPv6 解析的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockRejectedValue(new Error('No A records'));
|
||
vi.mocked(dns.resolve6).mockResolvedValue(['2001:4860:4860::8888']);
|
||
|
||
expect(await isInternalAddress('http://ipv6-only.example.com/')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理 IPv6 link-local 解析', async () => {
|
||
vi.mocked(dns.resolve4).mockRejectedValue(new Error('No A records'));
|
||
vi.mocked(dns.resolve6).mockResolvedValue(['fe80::1']);
|
||
|
||
expect(await isInternalAddress('http://link-local.example.com/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('边界情况和安全测试', () => {
|
||
test('应该正确处理带端口的 URL', async () => {
|
||
expect(await isInternalAddress('http://10.0.0.1:8080/')).toBe(true);
|
||
expect(await isInternalAddress('http://8.8.8.8:8080/')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理带路径的 URL', async () => {
|
||
expect(await isInternalAddress('http://10.0.0.1/api/v1/users')).toBe(true);
|
||
expect(await isInternalAddress('http://8.8.8.8/api/v1/users')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理带查询参数的 URL', async () => {
|
||
expect(await isInternalAddress('http://10.0.0.1/?param=value')).toBe(true);
|
||
expect(await isInternalAddress('http://8.8.8.8/?param=value')).toBe(false);
|
||
});
|
||
|
||
test('应该允许无效的 URL(宽松策略)', async () => {
|
||
// 修改:URL 解析失败时返回 false(允许访问)
|
||
expect(await isInternalAddress('not-a-url')).toBe(false);
|
||
expect(await isInternalAddress('http://')).toBe(false);
|
||
expect(await isInternalAddress('')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理 IPv4 边界值', async () => {
|
||
expect(await isInternalAddress('http://0.0.0.0/')).toBe(true);
|
||
expect(await isInternalAddress('http://255.255.255.255/')).toBe(true);
|
||
});
|
||
|
||
test('应该允许无效的 IPv4 地址(宽松策略)', async () => {
|
||
// 这些会被 URL 解析器处理为域名,DNS 解析失败时允许访问
|
||
vi.mocked(dns.resolve4).mockRejectedValue(new Error('Invalid hostname'));
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('Invalid hostname'));
|
||
|
||
expect(await isInternalAddress('http://256.1.1.1/')).toBe(false);
|
||
expect(await isInternalAddress('http://1.1.1.256/')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理特殊字符的 URL', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://example.com/path?query=value#fragment')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理 HTTPS URL', async () => {
|
||
expect(await isInternalAddress('https://10.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('https://8.8.8.8/')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('已知绕过尝试(应该被阻止)', () => {
|
||
test('应该阻止 localhost 变体', async () => {
|
||
expect(await isInternalAddress('http://localhost/')).toBe(true);
|
||
expect(await isInternalAddress('http://127.0.0.1/')).toBe(true);
|
||
expect(await isInternalAddress('http://[::1]/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止通过 DNS 解析的域名绕过尝试', async () => {
|
||
// Mock DNS 解析返回内部 IP
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['127.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
expect(await isInternalAddress('http://127.0.0.1.nip.io/')).toBe(true);
|
||
expect(await isInternalAddress('http://localhost.example.com/')).toBe(true);
|
||
|
||
// Mock DNS 解析返回私有 IP
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['10.0.0.1']);
|
||
expect(await isInternalAddress('http://10.0.0.1.xip.io/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv6 绕过尝试', async () => {
|
||
expect(await isInternalAddress('http://[::1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[fe80::1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[fc00::1]/')).toBe(true);
|
||
});
|
||
|
||
test('应该阻止 IPv4-mapped IPv6 绕过尝试', async () => {
|
||
expect(await isInternalAddress('http://[::ffff:127.0.0.1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[::ffff:10.0.0.1]/')).toBe(true);
|
||
expect(await isInternalAddress('http://[::ffff:192.168.1.1]/')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('性能和稳定性测试', () => {
|
||
test('应该正确处理异常输入', async () => {
|
||
await expect(isInternalAddress('http://localhost/')).resolves.not.toThrow();
|
||
await expect(isInternalAddress('invalid')).resolves.not.toThrow();
|
||
await expect(isInternalAddress('')).resolves.not.toThrow();
|
||
await expect(isInternalAddress('http://')).resolves.not.toThrow();
|
||
});
|
||
|
||
test('应该正确处理 DNS 超时', async () => {
|
||
vi.mocked(dns.resolve4).mockImplementation(
|
||
() => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100))
|
||
);
|
||
vi.mocked(dns.resolve6).mockImplementation(
|
||
() => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100))
|
||
);
|
||
|
||
// 修改:DNS 超时时返回 false(允许访问)
|
||
expect(await isInternalAddress('http://slow.example.com/')).toBe(false);
|
||
});
|
||
|
||
test('应该正确处理空的 DNS 响应', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue([]);
|
||
vi.mocked(dns.resolve6).mockResolvedValue([]);
|
||
|
||
expect(await isInternalAddress('http://empty-dns.example.com/')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('混合场景测试', () => {
|
||
test('应该正确处理同时有公共和私有 IP 的域名', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8', '10.0.0.1']);
|
||
vi.mocked(dns.resolve6).mockRejectedValue(new Error('No AAAA records'));
|
||
|
||
// 只要有一个私有 IP 就应该阻止
|
||
expect(await isInternalAddress('http://mixed-ips.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该正确处理 IPv4 和 IPv6 混合解析', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8']);
|
||
vi.mocked(dns.resolve6).mockResolvedValue(['fc00::1']);
|
||
|
||
// IPv6 是私有地址,应该阻止
|
||
expect(await isInternalAddress('http://dual-stack-private.example.com/')).toBe(true);
|
||
});
|
||
|
||
test('应该正确处理 IPv4 和 IPv6 都是公共 IP', async () => {
|
||
vi.mocked(dns.resolve4).mockResolvedValue(['8.8.8.8']);
|
||
vi.mocked(dns.resolve6).mockResolvedValue(['2001:4860:4860::8888']);
|
||
|
||
expect(await isInternalAddress('http://dual-stack-public.example.com/')).toBe(false);
|
||
});
|
||
});
|
||
});
|