@zhin.js/plugin-nettools 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/commands/dns/[name]/[[type]]/index.js +5 -5
  3. package/commands/dns/[name]/[[type]]/index.ts +5 -5
  4. package/commands/domain-price/[suffix]/[[order]]/index.js +5 -5
  5. package/commands/domain-price/[suffix]/[[order]]/index.ts +5 -5
  6. package/commands/ping/[host]/index.js +4 -4
  7. package/commands/ping/[host]/index.ts +4 -4
  8. package/commands/whois/[domain]/index.js +4 -4
  9. package/commands/whois/[domain]/index.ts +4 -4
  10. package/lib/address-policy.d.ts +2 -0
  11. package/lib/address-policy.js +55 -54
  12. package/lib/address-policy.js.map +1 -1
  13. package/lib/cooldown.js +2 -1
  14. package/lib/cooldown.js.map +1 -1
  15. package/lib/dns.d.ts +2 -1
  16. package/lib/dns.js +14 -12
  17. package/lib/dns.js.map +1 -1
  18. package/lib/domain-price.d.ts +2 -1
  19. package/lib/domain-price.js +16 -19
  20. package/lib/domain-price.js.map +1 -1
  21. package/lib/errors.d.ts +5 -2
  22. package/lib/errors.js +8 -4
  23. package/lib/errors.js.map +1 -1
  24. package/lib/http.js +12 -11
  25. package/lib/http.js.map +1 -1
  26. package/lib/messages.d.ts +329 -0
  27. package/lib/messages.js +335 -0
  28. package/lib/messages.js.map +1 -0
  29. package/lib/ping.d.ts +2 -1
  30. package/lib/ping.js +13 -11
  31. package/lib/ping.js.map +1 -1
  32. package/lib/presentation.d.ts +1 -0
  33. package/lib/presentation.js +36 -0
  34. package/lib/presentation.js.map +1 -0
  35. package/lib/service.d.ts +6 -5
  36. package/lib/service.js +16 -15
  37. package/lib/service.js.map +1 -1
  38. package/lib/target-guard.js +6 -4
  39. package/lib/target-guard.js.map +1 -1
  40. package/lib/whois.d.ts +2 -1
  41. package/lib/whois.js +18 -16
  42. package/lib/whois.js.map +1 -1
  43. package/package.json +7 -7
  44. package/skills/nettools/tools/dns_lookup/index.js +1 -1
  45. package/skills/nettools/tools/domain_price/index.js +1 -1
  46. package/skills/nettools/tools/net_ping/index.js +1 -1
  47. package/skills/nettools/tools/whois_lookup/index.js +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @zhin.js/plugin-nettools
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 53bc6a0: 官方工具插件与游戏支持中英文反馈、帮助和配置标签;提醒增加与中文对应的有限英文时间语法。聊天后台任务记录创建者当时语言,管理员任务记录项目语言,旧任务保持中文;多轮选择与游戏使用开始时语言,保留第三方内容和已生成报告。
8
+ - Updated dependencies [53bc6a0]
9
+ - Updated dependencies [7595136]
10
+ - Updated dependencies [17d1301]
11
+ - @zhin.js/tool@1.1.10
12
+ - @zhin.js/skill@1.1.10
13
+
3
14
  ## 0.1.2
4
15
 
5
16
  ### Patch Changes
@@ -3,16 +3,16 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY',
6
+ description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY', descriptionI18n: { en: "DNS lookup over HTTPS; supports A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY" },
7
7
  params: {
8
- name: { type: 'string', description: '域名;PTR 查询时可填 IP' },
9
- type: { type: 'string', description: '记录类型,默认 A' },
8
+ name: { type: 'string', description: '域名;PTR 查询时可填 IP', descriptionI18n: { en: "Domain name, or an IP address for PTR queries" } },
9
+ type: { type: 'string', description: '记录类型,默认 A', descriptionI18n: { en: "Record type (default: A)" } },
10
10
  },
11
- async execute({ params, input, use, signal }) {
11
+ async execute({ params, input, use, signal, locale }) {
12
12
  const service = use(nettoolsServiceToken);
13
13
  const name = String(params.name ?? '');
14
14
  const type = params.type == null ? undefined : String(params.type);
15
15
  const callerKey = commandCallerKey(input);
16
- return service.run(() => service.dns(name, type, callerKey, signal));
16
+ return service.run(() => service.dns(name, type, callerKey, signal, locale), locale);
17
17
  },
18
18
  });
@@ -3,16 +3,16 @@ import { commandCallerKey } from '../../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY',
6
+ description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY', descriptionI18n: { en: "DNS lookup over HTTPS; supports A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY" },
7
7
  params: {
8
- name: { type: 'string', description: '域名;PTR 查询时可填 IP' },
9
- type: { type: 'string', description: '记录类型,默认 A' },
8
+ name: { type: 'string', description: '域名;PTR 查询时可填 IP', descriptionI18n: { en: "Domain name, or an IP address for PTR queries" } },
9
+ type: { type: 'string', description: '记录类型,默认 A', descriptionI18n: { en: "Record type (default: A)" } },
10
10
  },
11
- async execute({ params, input, use, signal }) {
11
+ async execute({ params, input, use, signal, locale }) {
12
12
  const service = use(nettoolsServiceToken);
13
13
  const name = String(params.name ?? '');
14
14
  const type = params.type == null ? undefined : String(params.type);
15
15
  const callerKey = commandCallerKey(input);
16
- return service.run(() => service.dns(name, type, callerKey, signal));
16
+ return service.run(() => service.dns(name, type, callerKey, signal, locale), locale);
17
17
  },
18
18
  });
@@ -3,17 +3,17 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: '域名注册/续费/转入价格比价(nazhumi.com)',
6
+ description: '域名注册/续费/转入价格比价(nazhumi.com)', descriptionI18n: { en: "Compare domain registration, renewal and transfer prices (nazhumi.com)" },
7
7
  alias: ['域名比价'],
8
8
  params: {
9
- suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名' },
10
- order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册' },
9
+ suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名', descriptionI18n: { en: "Domain suffix (com, .cn, .co.uk) or a full domain" } },
10
+ order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册', descriptionI18n: { en: "register (default), renew, or transfer" } },
11
11
  },
12
- async execute({ params, input, use, signal }) {
12
+ async execute({ params, input, use, signal, locale }) {
13
13
  const service = use(nettoolsServiceToken);
14
14
  const suffix = String(params.suffix ?? '');
15
15
  const order = params.order == null ? undefined : String(params.order);
16
16
  const callerKey = commandCallerKey(input);
17
- return service.run(() => service.domainPrice(suffix, order, callerKey, signal));
17
+ return service.run(() => service.domainPrice(suffix, order, callerKey, signal, locale), locale);
18
18
  },
19
19
  });
@@ -3,17 +3,17 @@ import { commandCallerKey } from '../../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: '域名注册/续费/转入价格比价(nazhumi.com)',
6
+ description: '域名注册/续费/转入价格比价(nazhumi.com)', descriptionI18n: { en: "Compare domain registration, renewal and transfer prices (nazhumi.com)" },
7
7
  alias: ['域名比价'],
8
8
  params: {
9
- suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名' },
10
- order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册' },
9
+ suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名', descriptionI18n: { en: "Domain suffix (com, .cn, .co.uk) or a full domain" } },
10
+ order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册', descriptionI18n: { en: "register (default), renew, or transfer" } },
11
11
  },
12
- async execute({ params, input, use, signal }) {
12
+ async execute({ params, input, use, signal, locale }) {
13
13
  const service = use(nettoolsServiceToken);
14
14
  const suffix = String(params.suffix ?? '');
15
15
  const order = params.order == null ? undefined : String(params.order);
16
16
  const callerKey = commandCallerKey(input);
17
- return service.run(() => service.domainPrice(suffix, order, callerKey, signal));
17
+ return service.run(() => service.domainPrice(suffix, order, callerKey, signal, locale), locale);
18
18
  },
19
19
  });
@@ -3,12 +3,12 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: '从多个地区 Ping 一个公网主机(check-host.net)',
7
- params: { host: { type: 'string', description: '域名或公网 IP' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '从多个地区 Ping 一个公网主机(check-host.net)', descriptionI18n: { en: "Ping a public host from multiple regions (check-host.net)" },
7
+ params: { host: { type: 'string', description: '域名或公网 IP', descriptionI18n: { en: "Domain name or public IP address" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const host = String(params.host ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.ping(host, callerKey, signal));
12
+ return service.run(() => service.ping(host, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -3,12 +3,12 @@ import { commandCallerKey } from '../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: '从多个地区 Ping 一个公网主机(check-host.net)',
7
- params: { host: { type: 'string', description: '域名或公网 IP' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '从多个地区 Ping 一个公网主机(check-host.net)', descriptionI18n: { en: "Ping a public host from multiple regions (check-host.net)" },
7
+ params: { host: { type: 'string', description: '域名或公网 IP', descriptionI18n: { en: "Domain name or public IP address" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const host = String(params.host ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.ping(host, callerKey, signal));
12
+ return service.run(() => service.ping(host, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -3,12 +3,12 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: '查询域名注册信息(RDAP / WHOIS)',
7
- params: { domain: { type: 'string', description: '要查询的域名' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '查询域名注册信息(RDAP / WHOIS)', descriptionI18n: { en: "Look up domain registration information using RDAP / WHOIS" },
7
+ params: { domain: { type: 'string', description: '要查询的域名', descriptionI18n: { en: "Domain to look up" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const domain = String(params.domain ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.whois(domain, callerKey, signal));
12
+ return service.run(() => service.whois(domain, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -3,12 +3,12 @@ import { commandCallerKey } from '../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: '查询域名注册信息(RDAP / WHOIS)',
7
- params: { domain: { type: 'string', description: '要查询的域名' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '查询域名注册信息(RDAP / WHOIS)', descriptionI18n: { en: "Look up domain registration information using RDAP / WHOIS" },
7
+ params: { domain: { type: 'string', description: '要查询的域名', descriptionI18n: { en: "Domain to look up" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const domain = String(params.domain ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.whois(domain, callerKey, signal));
12
+ return service.run(() => service.whois(domain, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -1,3 +1,4 @@
1
+ import { type NettoolsMessage } from './messages.js';
1
2
  export type ParsedHost = {
2
3
  readonly kind: 'ipv4';
3
4
  readonly address: string;
@@ -15,6 +16,7 @@ export type AddressVerdict = {
15
16
  } | {
16
17
  readonly blocked: true;
17
18
  readonly reason: string;
19
+ readonly detail: NettoolsMessage;
18
20
  };
19
21
  export interface ParseHostOptions {
20
22
  /** Allow `_` in labels (needed for DNS names such as `_dmarc.example.com`). */
@@ -1,3 +1,4 @@
1
+ import { message, formatMessage } from './messages.js';
1
2
  /**
2
3
  * Target address policy (SSRF guard).
3
4
  *
@@ -26,10 +27,10 @@
26
27
  import { isIPv4, isIPv6 } from 'node:net';
27
28
  import { NettoolsError } from './errors.js';
28
29
  const ALLOWED = Object.freeze({ blocked: false });
29
- function blocked(reason) {
30
- return Object.freeze({ blocked: true, reason });
30
+ function blocked(detail) {
31
+ return Object.freeze({ blocked: true, reason: formatMessage(detail), detail });
31
32
  }
32
- const CLOUD_METADATA_REASON = '云服务元数据地址';
33
+ const CLOUD_METADATA_REASON = message('cloudMetadata');
33
34
  /** Exact IPv4 metadata endpoints, checked before the range table for a clearer message. */
34
35
  const CLOUD_METADATA_IPV4 = new Set([
35
36
  '169.254.169.254', // AWS / GCP / Azure / Oracle / OpenStack instance metadata
@@ -41,29 +42,29 @@ const CLOUD_METADATA_IPV4 = new Set([
41
42
  /** Exact IPv6 metadata endpoints. */
42
43
  const CLOUD_METADATA_IPV6 = ['fd00:ec2::254', 'fd00:ec2::23'];
43
44
  const BLOCKED_IPV4_RANGES = [
44
- ipv4Rule('0.0.0.0', 8, '未指定 / “本网络”地址'),
45
- ipv4Rule('10.0.0.0', 8, '私有地址 (RFC 1918)'),
46
- ipv4Rule('100.64.0.0', 10, '运营商级 NAT 地址 (CGNAT, RFC 6598)'),
47
- ipv4Rule('127.0.0.0', 8, '回环地址'),
48
- ipv4Rule('169.254.0.0', 16, '链路本地地址'),
49
- ipv4Rule('172.16.0.0', 12, '私有地址 (RFC 1918)'),
50
- ipv4Rule('192.0.0.0', 24, 'IETF 协议保留地址'),
51
- ipv4Rule('192.0.2.0', 24, '文档示例地址'),
52
- ipv4Rule('192.88.99.0', 24, '已废弃的 6to4 中继地址'),
53
- ipv4Rule('192.168.0.0', 16, '私有地址 (RFC 1918)'),
54
- ipv4Rule('198.18.0.0', 15, '基准测试保留地址'),
55
- ipv4Rule('198.51.100.0', 24, '文档示例地址'),
56
- ipv4Rule('203.0.113.0', 24, '文档示例地址'),
57
- ipv4Rule('224.0.0.0', 4, '组播地址'),
58
- ipv4Rule('240.0.0.0', 4, '保留地址(含广播地址)'),
45
+ ipv4Rule('0.0.0.0', 8, message('unspecifiedV4')),
46
+ ipv4Rule('10.0.0.0', 8, message('privateV4')),
47
+ ipv4Rule('100.64.0.0', 10, message('cgnat')),
48
+ ipv4Rule('127.0.0.0', 8, message('loopback')),
49
+ ipv4Rule('169.254.0.0', 16, message('linkLocal')),
50
+ ipv4Rule('172.16.0.0', 12, message('privateV4')),
51
+ ipv4Rule('192.0.0.0', 24, message('ietfReserved')),
52
+ ipv4Rule('192.0.2.0', 24, message('documentation')),
53
+ ipv4Rule('192.88.99.0', 24, message('relay6to4')),
54
+ ipv4Rule('192.168.0.0', 16, message('privateV4')),
55
+ ipv4Rule('198.18.0.0', 15, message('benchmark')),
56
+ ipv4Rule('198.51.100.0', 24, message('documentation')),
57
+ ipv4Rule('203.0.113.0', 24, message('documentation')),
58
+ ipv4Rule('224.0.0.0', 4, message('multicast')),
59
+ ipv4Rule('240.0.0.0', 4, message('reservedBroadcast')),
59
60
  ];
60
61
  /** Special IPv6 ranges inside 2000::/3 that are still refused. */
61
62
  const BLOCKED_GLOBAL_IPV6_RANGES = [
62
- ipv6Rule('2001::', 32, 'Teredo 隧道地址'),
63
- ipv6Rule('2001:2::', 48, '基准测试保留地址'),
64
- ipv6Rule('2001:10::', 28, 'ORCHID 保留地址'),
65
- ipv6Rule('2001:db8::', 32, '文档示例地址'),
66
- ipv6Rule('3fff::', 20, '文档示例地址'),
63
+ ipv6Rule('2001::', 32, message('teredo')),
64
+ ipv6Rule('2001:2::', 48, message('benchmark')),
65
+ ipv6Rule('2001:10::', 28, message('orchid')),
66
+ ipv6Rule('2001:db8::', 32, message('documentation')),
67
+ ipv6Rule('3fff::', 20, message('documentation')),
67
68
  ];
68
69
  const BLOCKED_HOSTNAMES = new Set([
69
70
  'localhost',
@@ -105,19 +106,19 @@ const MAX_LABEL_LENGTH = 63;
105
106
  export function parseHostInput(input, options = {}) {
106
107
  const trimmed = input.trim();
107
108
  if (trimmed.length === 0) {
108
- throw invalid('请提供主机名或 IP 地址');
109
+ throw invalid(message('hostRequired'));
109
110
  }
110
111
  if (trimmed.length > MAX_HOSTNAME_LENGTH + 2) {
111
- throw invalid('主机名过长');
112
+ throw invalid(message('hostTooLong'));
112
113
  }
113
114
  if (/[\s/\\@?#%]/u.test(trimmed)) {
114
- throw invalid('只接受裸主机名或 IP,不要带协议、端口、路径或特殊字符');
115
+ throw invalid(message('bareHost'));
115
116
  }
116
117
  const bracketed = trimmed.startsWith('[') && trimmed.endsWith(']');
117
118
  const ipv6Candidate = bracketed ? trimmed.slice(1, -1) : trimmed;
118
119
  if (bracketed || trimmed.includes(':')) {
119
120
  if (!isIPv6(ipv6Candidate)) {
120
- throw invalid('不是有效的 IPv6 地址(如需端口请去掉端口)');
121
+ throw invalid(message('ipv6NoPort'));
121
122
  }
122
123
  const canonical = canonicalizeWithUrlParser(`[${ipv6Candidate}]`);
123
124
  const address = canonical.slice(1, -1);
@@ -142,33 +143,33 @@ function canonicalizeWithUrlParser(host) {
142
143
  const result = probe.hostname;
143
144
  const unchanged = result === placeholder && host.toLowerCase() !== placeholder;
144
145
  if (unchanged || result.length === 0) {
145
- throw invalid('不是有效的主机名或 IP 地址');
146
+ throw invalid(message('invalidHost'));
146
147
  }
147
148
  return result;
148
149
  }
149
150
  function validateDomainSyntax(name, options) {
150
151
  if (name.length === 0 || name.length > MAX_HOSTNAME_LENGTH) {
151
- throw invalid('主机名长度不合法');
152
+ throw invalid(message('hostLength'));
152
153
  }
153
154
  const labels = name.split('.');
154
155
  if (labels.length < (options.minLabels ?? 2)) {
155
- throw invalid('请提供完整域名(例如 example.com)');
156
+ throw invalid(message('fullDomain'));
156
157
  }
157
158
  const labelPattern = options.allowUnderscore
158
159
  ? /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/u
159
160
  : /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
160
161
  for (const label of labels) {
161
162
  if (label.length === 0 || label.length > MAX_LABEL_LENGTH || !labelPattern.test(label)) {
162
- throw invalid(`域名标签不合法:${label || '(空)'}`);
163
+ throw invalid(message('invalidLabel', { label: label || message('empty') }));
163
164
  }
164
165
  }
165
166
  const topLevel = labels[labels.length - 1] ?? '';
166
167
  if (/^\d+$/u.test(topLevel)) {
167
- throw invalid('顶级域不能是纯数字');
168
+ throw invalid(message('numericTld'));
168
169
  }
169
170
  }
170
- function invalid(message) {
171
- return new NettoolsError('invalid_input', message);
171
+ function invalid(detail) {
172
+ return new NettoolsError('invalid_input', detail);
172
173
  }
173
174
  /* ------------------------------------------------------------------ */
174
175
  /* Classification */
@@ -187,7 +188,7 @@ export function classifyParsedHost(host) {
187
188
  export function classifyAddress(address) {
188
189
  const parsed = parseHostInput(address);
189
190
  if (parsed.kind === 'domain') {
190
- throw invalid(`不是 IP 地址:${address}`);
191
+ throw invalid(message('notIp', { address }));
191
192
  }
192
193
  return classifyParsedHost(parsed);
193
194
  }
@@ -195,12 +196,12 @@ export function classifyHostname(name) {
195
196
  const normalized = name.toLowerCase().replace(/\.$/u, '');
196
197
  if (BLOCKED_HOSTNAMES.has(normalized)) {
197
198
  return normalized.startsWith('localhost')
198
- ? blocked('本机主机名')
199
+ ? blocked(message('localhost'))
199
200
  : blocked(CLOUD_METADATA_REASON);
200
201
  }
201
202
  for (const suffix of BLOCKED_SUFFIXES) {
202
203
  if (normalized === suffix || normalized.endsWith(`.${suffix}`)) {
203
- return blocked(`内网或保留域名后缀 .${suffix}`);
204
+ return blocked(message('reservedSuffix', { suffix }));
204
205
  }
205
206
  }
206
207
  return ALLOWED;
@@ -229,11 +230,11 @@ const MULTICAST_PREFIX = ipv6ToBytes('ff00::');
229
230
  const CLOUD_METADATA_IPV6_BYTES = CLOUD_METADATA_IPV6.map((address) => ipv6ToBytes(address));
230
231
  function classifyIpv6Bytes(bytes) {
231
232
  if (bytes.every((byte) => byte === 0)) {
232
- return blocked('未指定地址 (::)');
233
+ return blocked(message('unspecifiedV6'));
233
234
  }
234
235
  const isLoopback = bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;
235
236
  if (isLoopback) {
236
- return blocked('回环地址 (::1)');
237
+ return blocked(message('loopbackV6'));
237
238
  }
238
239
  for (const metadata of CLOUD_METADATA_IPV6_BYTES) {
239
240
  if (matchesPrefix(bytes, metadata, 128)) {
@@ -241,31 +242,31 @@ function classifyIpv6Bytes(bytes) {
241
242
  }
242
243
  }
243
244
  if (matchesPrefix(bytes, IPV4_MAPPED_PREFIX, 96)) {
244
- return classifyEmbeddedIpv4(bytes.slice(12, 16), 'IPv4 映射地址');
245
+ return classifyEmbeddedIpv4(bytes.slice(12, 16), message('mappedV4'));
245
246
  }
246
247
  if (matchesPrefix(bytes, NAT64_WELL_KNOWN_PREFIX, 96)) {
247
- return classifyEmbeddedIpv4(bytes.slice(12, 16), 'NAT64 地址');
248
+ return classifyEmbeddedIpv4(bytes.slice(12, 16), message('nat64'));
248
249
  }
249
250
  if (matchesPrefix(bytes, NAT64_LOCAL_PREFIX, 48)) {
250
- return blocked('本地 NAT64 地址');
251
+ return blocked(message('localNat64'));
251
252
  }
252
253
  if (matchesPrefix(bytes, SIX_TO_FOUR_PREFIX, 16)) {
253
- return classifyEmbeddedIpv4(bytes.slice(2, 6), '6to4 地址');
254
+ return classifyEmbeddedIpv4(bytes.slice(2, 6), message('sixToFour'));
254
255
  }
255
256
  if (matchesPrefix(bytes, UNIQUE_LOCAL_PREFIX, 7)) {
256
- return blocked('唯一本地地址 (ULA, fc00::/7)');
257
+ return blocked(message('ula'));
257
258
  }
258
259
  if (matchesPrefix(bytes, LINK_LOCAL_PREFIX, 10)) {
259
- return blocked('链路本地地址 (fe80::/10)');
260
+ return blocked(message('linkLocalV6'));
260
261
  }
261
262
  if (matchesPrefix(bytes, SITE_LOCAL_PREFIX, 10)) {
262
- return blocked('已废弃的站点本地地址 (fec0::/10)');
263
+ return blocked(message('siteLocalV6'));
263
264
  }
264
265
  if (matchesPrefix(bytes, MULTICAST_PREFIX, 8)) {
265
- return blocked('组播地址 (ff00::/8)');
266
+ return blocked(message('multicastV6'));
266
267
  }
267
268
  if (!matchesPrefix(bytes, GLOBAL_UNICAST_PREFIX, 3)) {
268
- return blocked('非全球单播 IPv6 地址');
269
+ return blocked(message('notGlobalV6'));
269
270
  }
270
271
  for (const rule of BLOCKED_GLOBAL_IPV6_RANGES) {
271
272
  if (matchesPrefix(bytes, rule.prefix, rule.bits)) {
@@ -280,7 +281,7 @@ function classifyEmbeddedIpv4(embedded, wrapper) {
280
281
  return verdict;
281
282
  }
282
283
  const dotted = Array.from(embedded).join('.');
283
- return blocked(`${wrapper}指向 ${dotted}(${verdict.reason})`);
284
+ return blocked(message('embeddedV4', { wrapper, address: dotted, reason: verdict.detail }));
284
285
  }
285
286
  /* ------------------------------------------------------------------ */
286
287
  /* Byte helpers */
@@ -307,7 +308,7 @@ export function ipv4ToBytes(address) {
307
308
  const parts = address.split('.').map((part) => Number(part));
308
309
  const valid = parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);
309
310
  if (!valid) {
310
- throw invalid(`不是有效的 IPv4 地址:${address}`);
311
+ throw invalid(message('invalidV4', { address }));
311
312
  }
312
313
  return Uint8Array.from(parts);
313
314
  }
@@ -326,13 +327,13 @@ export function ipv6ToBytes(address) {
326
327
  }
327
328
  const halves = text.split('::');
328
329
  if (halves.length > 2) {
329
- throw invalid(`不是有效的 IPv6 地址:${address}`);
330
+ throw invalid(message('invalidV6', { address }));
330
331
  }
331
332
  const head = parseGroups(halves[0] ?? '');
332
333
  const rest = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];
333
334
  const missing = 8 - head.length - rest.length;
334
335
  if (halves.length === 2 ? missing < 1 : missing !== 0) {
335
- throw invalid(`不是有效的 IPv6 地址:${address}`);
336
+ throw invalid(message('invalidV6', { address }));
336
337
  }
337
338
  groups.push(...head);
338
339
  for (let index = 0; index < missing && halves.length === 2; index += 1) {
@@ -355,7 +356,7 @@ function parseGroups(text) {
355
356
  }
356
357
  return text.split(':').map((group) => {
357
358
  if (!/^[0-9a-f]{1,4}$/u.test(group)) {
358
- throw invalid(`IPv6 分组不合法:${group}`);
359
+ throw invalid(message('invalidV6Group', { group }));
359
360
  }
360
361
  return Number.parseInt(group, 16);
361
362
  });
@@ -379,7 +380,7 @@ export function reverseLookupName(host) {
379
380
  }
380
381
  return `${nibbles.reverse().join('.')}.ip6.arpa`;
381
382
  }
382
- throw invalid('反向解析需要 IP 地址');
383
+ throw invalid(message('reverseNeedsIp'));
383
384
  }
384
385
  /** Human label for a parsed host. */
385
386
  export function displayHost(host) {
@@ -1 +1 @@
1
- {"version":3,"file":"address-policy.js","sourceRoot":"","sources":["../src/address-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target address policy (SSRF guard).\n *\n * Network tools probe hosts that users type into chat. Even though the probes\n * themselves run on third-party services (check-host.net, public DoH, RDAP),\n * we refuse anything that points at private, loopback, link-local, reserved or\n * cloud-metadata space so the bot can never be used to map internal networks\n * or poke metadata endpoints through a relay.\n *\n * Design:\n * 1. Canonicalise first. The input is fed through the WHATWG URL host parser,\n * the same parser browsers and Node's fetch use. It turns odd IPv4 spellings\n * (`2130706433`, `0x7f.1`, `017700000001`, `127.1`) into dotted quads,\n * normalises IPv6 and converts IDN to punycode. We then classify the\n * canonical form, so \"what we checked\" is exactly \"what we send\".\n * 2. Classify by bytes. IPv4 and IPv6 are compared as raw bytes against\n * prefix tables. Embedded IPv4 (IPv4-mapped, NAT64, 6to4) is unwrapped and\n * classified as IPv4.\n * 3. Allow-list IPv6. Only global unicast (2000::/3) is accepted, minus the\n * documentation / Teredo / special ranges listed below.\n * 4. Names. Single-label names, internal suffixes (`.local`, `.internal`, ...)\n * and well-known metadata hostnames are refused before any lookup.\n * Hostnames are additionally resolved (see target-guard.ts) and refused\n * when any A/AAAA record is blocked.\n */\nimport { isIPv4, isIPv6 } from 'node:net';\nimport { NettoolsError } from './errors.js';\n\nexport type ParsedHost =\n | { readonly kind: 'ipv4'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'ipv6'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'domain'; readonly name: string };\n\nexport type AddressVerdict =\n | { readonly blocked: false }\n | { readonly blocked: true; readonly reason: string };\n\nexport interface ParseHostOptions {\n /** Allow `_` in labels (needed for DNS names such as `_dmarc.example.com`). */\n readonly allowUnderscore?: boolean;\n /** Minimum number of labels for a domain (default 2, i.e. a full domain). */\n readonly minLabels?: number;\n}\n\nconst ALLOWED: AddressVerdict = Object.freeze({ blocked: false });\n\nfunction blocked(reason: string): AddressVerdict {\n return Object.freeze({ blocked: true, reason });\n}\n\n/* ------------------------------------------------------------------ */\n/* Tables */\n/* ------------------------------------------------------------------ */\n\ninterface PrefixRule {\n readonly prefix: Uint8Array;\n readonly bits: number;\n readonly reason: string;\n}\n\nconst CLOUD_METADATA_REASON = '云服务元数据地址';\n\n/** Exact IPv4 metadata endpoints, checked before the range table for a clearer message. */\nconst CLOUD_METADATA_IPV4 = new Set([\n '169.254.169.254', // AWS / GCP / Azure / Oracle / OpenStack instance metadata\n '169.254.170.2', // AWS ECS task metadata\n '169.254.169.123', // AWS Time Sync\n '100.100.100.200', // Alibaba Cloud metadata\n '168.63.129.16', // Azure WireServer (public range, still host-internal)\n]);\n\n/** Exact IPv6 metadata endpoints. */\nconst CLOUD_METADATA_IPV6 = ['fd00:ec2::254', 'fd00:ec2::23'];\n\nconst BLOCKED_IPV4_RANGES: readonly PrefixRule[] = [\n ipv4Rule('0.0.0.0', 8, '未指定 / “本网络”地址'),\n ipv4Rule('10.0.0.0', 8, '私有地址 (RFC 1918)'),\n ipv4Rule('100.64.0.0', 10, '运营商级 NAT 地址 (CGNAT, RFC 6598)'),\n ipv4Rule('127.0.0.0', 8, '回环地址'),\n ipv4Rule('169.254.0.0', 16, '链路本地地址'),\n ipv4Rule('172.16.0.0', 12, '私有地址 (RFC 1918)'),\n ipv4Rule('192.0.0.0', 24, 'IETF 协议保留地址'),\n ipv4Rule('192.0.2.0', 24, '文档示例地址'),\n ipv4Rule('192.88.99.0', 24, '已废弃的 6to4 中继地址'),\n ipv4Rule('192.168.0.0', 16, '私有地址 (RFC 1918)'),\n ipv4Rule('198.18.0.0', 15, '基准测试保留地址'),\n ipv4Rule('198.51.100.0', 24, '文档示例地址'),\n ipv4Rule('203.0.113.0', 24, '文档示例地址'),\n ipv4Rule('224.0.0.0', 4, '组播地址'),\n ipv4Rule('240.0.0.0', 4, '保留地址(含广播地址)'),\n];\n\n/** Special IPv6 ranges inside 2000::/3 that are still refused. */\nconst BLOCKED_GLOBAL_IPV6_RANGES: readonly PrefixRule[] = [\n ipv6Rule('2001::', 32, 'Teredo 隧道地址'),\n ipv6Rule('2001:2::', 48, '基准测试保留地址'),\n ipv6Rule('2001:10::', 28, 'ORCHID 保留地址'),\n ipv6Rule('2001:db8::', 32, '文档示例地址'),\n ipv6Rule('3fff::', 20, '文档示例地址'),\n];\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\n/** Suffixes that only make sense inside a private network or are reserved (RFC 2606 / 6761 / 6762). */\nconst BLOCKED_SUFFIXES = [\n 'localhost',\n 'local',\n 'localdomain',\n 'internal',\n 'intranet',\n 'private',\n 'corp',\n 'home',\n 'lan',\n 'home.arpa',\n 'arpa',\n 'test',\n 'invalid',\n 'example',\n 'onion',\n];\n\nconst MAX_HOSTNAME_LENGTH = 253;\nconst MAX_LABEL_LENGTH = 63;\n\n/* ------------------------------------------------------------------ */\n/* Parsing */\n/* ------------------------------------------------------------------ */\n\n/**\n * Parse and canonicalise one user-supplied host (domain, IPv4 or IPv6).\n * Throws {@link NettoolsError} (`invalid_input`) for anything that is not a\n * bare host: URLs, ports, paths, user-info, zone ids and percent-encoding are\n * all refused rather than \"helpfully\" stripped.\n */\nexport function parseHostInput(input: string, options: ParseHostOptions = {}): ParsedHost {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n throw invalid('请提供主机名或 IP 地址');\n }\n if (trimmed.length > MAX_HOSTNAME_LENGTH + 2) {\n throw invalid('主机名过长');\n }\n if (/[\\s/\\\\@?#%]/u.test(trimmed)) {\n throw invalid('只接受裸主机名或 IP,不要带协议、端口、路径或特殊字符');\n }\n\n const bracketed = trimmed.startsWith('[') && trimmed.endsWith(']');\n const ipv6Candidate = bracketed ? trimmed.slice(1, -1) : trimmed;\n if (bracketed || trimmed.includes(':')) {\n if (!isIPv6(ipv6Candidate)) {\n throw invalid('不是有效的 IPv6 地址(如需端口请去掉端口)');\n }\n const canonical = canonicalizeWithUrlParser(`[${ipv6Candidate}]`);\n const address = canonical.slice(1, -1);\n return Object.freeze({ kind: 'ipv6', address, bytes: ipv6ToBytes(address) });\n }\n\n const canonical = canonicalizeWithUrlParser(trimmed);\n if (isIPv4(canonical)) {\n return Object.freeze({ kind: 'ipv4', address: canonical, bytes: ipv4ToBytes(canonical) });\n }\n const name = canonical.endsWith('.') ? canonical.slice(0, -1) : canonical;\n validateDomainSyntax(name, options);\n return Object.freeze({ kind: 'domain', name });\n}\n\n/**\n * Run the WHATWG host parser by assigning to `URL#hostname` on a placeholder\n * URL. An invalid host leaves the placeholder untouched, which we detect.\n */\nfunction canonicalizeWithUrlParser(host: string): string {\n const placeholder = 'placeholder.invalid';\n const probe = new URL(`http://${placeholder}/`);\n probe.hostname = host;\n const result = probe.hostname;\n const unchanged = result === placeholder && host.toLowerCase() !== placeholder;\n if (unchanged || result.length === 0) {\n throw invalid('不是有效的主机名或 IP 地址');\n }\n return result;\n}\n\nfunction validateDomainSyntax(name: string, options: ParseHostOptions): void {\n if (name.length === 0 || name.length > MAX_HOSTNAME_LENGTH) {\n throw invalid('主机名长度不合法');\n }\n const labels = name.split('.');\n if (labels.length < (options.minLabels ?? 2)) {\n throw invalid('请提供完整域名(例如 example.com)');\n }\n const labelPattern = options.allowUnderscore\n ? /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/u\n : /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;\n for (const label of labels) {\n if (label.length === 0 || label.length > MAX_LABEL_LENGTH || !labelPattern.test(label)) {\n throw invalid(`域名标签不合法:${label || '(空)'}`);\n }\n }\n const topLevel = labels[labels.length - 1] ?? '';\n if (/^\\d+$/u.test(topLevel)) {\n throw invalid('顶级域不能是纯数字');\n }\n}\n\nfunction invalid(message: string): NettoolsError {\n return new NettoolsError('invalid_input', message);\n}\n\n/* ------------------------------------------------------------------ */\n/* Classification */\n/* ------------------------------------------------------------------ */\n\n/** Classify a parsed host without any network access. Domains are checked by name only. */\nexport function classifyParsedHost(host: ParsedHost): AddressVerdict {\n if (host.kind === 'domain') {\n return classifyHostname(host.name);\n }\n if (host.kind === 'ipv4') {\n return classifyIpv4Bytes(host.bytes);\n }\n return classifyIpv6Bytes(host.bytes);\n}\n\n/** Classify a literal address string (any spelling the URL parser understands). */\nexport function classifyAddress(address: string): AddressVerdict {\n const parsed = parseHostInput(address);\n if (parsed.kind === 'domain') {\n throw invalid(`不是 IP 地址:${address}`);\n }\n return classifyParsedHost(parsed);\n}\n\nexport function classifyHostname(name: string): AddressVerdict {\n const normalized = name.toLowerCase().replace(/\\.$/u, '');\n if (BLOCKED_HOSTNAMES.has(normalized)) {\n return normalized.startsWith('localhost')\n ? blocked('本机主机名')\n : blocked(CLOUD_METADATA_REASON);\n }\n for (const suffix of BLOCKED_SUFFIXES) {\n if (normalized === suffix || normalized.endsWith(`.${suffix}`)) {\n return blocked(`内网或保留域名后缀 .${suffix}`);\n }\n }\n return ALLOWED;\n}\n\nfunction classifyIpv4Bytes(bytes: Uint8Array): AddressVerdict {\n const dotted = Array.from(bytes).join('.');\n if (CLOUD_METADATA_IPV4.has(dotted)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n for (const rule of BLOCKED_IPV4_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nconst IPV4_MAPPED_PREFIX = ipv6ToBytes('::ffff:0:0');\nconst NAT64_WELL_KNOWN_PREFIX = ipv6ToBytes('64:ff9b::');\nconst NAT64_LOCAL_PREFIX = ipv6ToBytes('64:ff9b:1::');\nconst SIX_TO_FOUR_PREFIX = ipv6ToBytes('2002::');\nconst GLOBAL_UNICAST_PREFIX = ipv6ToBytes('2000::');\nconst UNIQUE_LOCAL_PREFIX = ipv6ToBytes('fc00::');\nconst LINK_LOCAL_PREFIX = ipv6ToBytes('fe80::');\nconst SITE_LOCAL_PREFIX = ipv6ToBytes('fec0::');\nconst MULTICAST_PREFIX = ipv6ToBytes('ff00::');\nconst CLOUD_METADATA_IPV6_BYTES = CLOUD_METADATA_IPV6.map((address) => ipv6ToBytes(address));\n\nfunction classifyIpv6Bytes(bytes: Uint8Array): AddressVerdict {\n if (bytes.every((byte) => byte === 0)) {\n return blocked('未指定地址 (::)');\n }\n const isLoopback = bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;\n if (isLoopback) {\n return blocked('回环地址 (::1)');\n }\n for (const metadata of CLOUD_METADATA_IPV6_BYTES) {\n if (matchesPrefix(bytes, metadata, 128)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n }\n if (matchesPrefix(bytes, IPV4_MAPPED_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), 'IPv4 映射地址');\n }\n if (matchesPrefix(bytes, NAT64_WELL_KNOWN_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), 'NAT64 地址');\n }\n if (matchesPrefix(bytes, NAT64_LOCAL_PREFIX, 48)) {\n return blocked('本地 NAT64 地址');\n }\n if (matchesPrefix(bytes, SIX_TO_FOUR_PREFIX, 16)) {\n return classifyEmbeddedIpv4(bytes.slice(2, 6), '6to4 地址');\n }\n if (matchesPrefix(bytes, UNIQUE_LOCAL_PREFIX, 7)) {\n return blocked('唯一本地地址 (ULA, fc00::/7)');\n }\n if (matchesPrefix(bytes, LINK_LOCAL_PREFIX, 10)) {\n return blocked('链路本地地址 (fe80::/10)');\n }\n if (matchesPrefix(bytes, SITE_LOCAL_PREFIX, 10)) {\n return blocked('已废弃的站点本地地址 (fec0::/10)');\n }\n if (matchesPrefix(bytes, MULTICAST_PREFIX, 8)) {\n return blocked('组播地址 (ff00::/8)');\n }\n if (!matchesPrefix(bytes, GLOBAL_UNICAST_PREFIX, 3)) {\n return blocked('非全球单播 IPv6 地址');\n }\n for (const rule of BLOCKED_GLOBAL_IPV6_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nfunction classifyEmbeddedIpv4(embedded: Uint8Array, wrapper: string): AddressVerdict {\n const verdict = classifyIpv4Bytes(embedded);\n if (!verdict.blocked) {\n return verdict;\n }\n const dotted = Array.from(embedded).join('.');\n return blocked(`${wrapper}指向 ${dotted}(${verdict.reason})`);\n}\n\n/* ------------------------------------------------------------------ */\n/* Byte helpers */\n/* ------------------------------------------------------------------ */\n\nfunction matchesPrefix(bytes: Uint8Array, prefix: Uint8Array, bits: number): boolean {\n if (bytes.length !== prefix.length) {\n return false;\n }\n const fullBytes = Math.floor(bits / 8);\n for (let index = 0; index < fullBytes; index += 1) {\n if (bytes[index] !== prefix[index]) {\n return false;\n }\n }\n const remainingBits = bits % 8;\n if (remainingBits === 0) {\n return true;\n }\n const mask = (0xff << (8 - remainingBits)) & 0xff;\n return ((bytes[fullBytes] ?? 0) & mask) === ((prefix[fullBytes] ?? 0) & mask);\n}\n\n/** Dotted-quad IPv4 to 4 bytes. Callers must pass a canonical dotted quad. */\nexport function ipv4ToBytes(address: string): Uint8Array {\n const parts = address.split('.').map((part) => Number(part));\n const valid = parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);\n if (!valid) {\n throw invalid(`不是有效的 IPv4 地址:${address}`);\n }\n return Uint8Array.from(parts);\n}\n\n/** Textual IPv6 (with optional `::` and dotted IPv4 tail) to 16 bytes. */\nexport function ipv6ToBytes(address: string): Uint8Array {\n let text = address.toLowerCase();\n const groups: number[] = [];\n const lastColon = text.lastIndexOf(':');\n const tail = text.slice(lastColon + 1);\n const tailGroups: number[] = [];\n if (tail.includes('.')) {\n const v4 = ipv4ToBytes(tail);\n tailGroups.push(((v4[0] ?? 0) << 8) | (v4[1] ?? 0));\n tailGroups.push(((v4[2] ?? 0) << 8) | (v4[3] ?? 0));\n text = text.slice(0, lastColon + 1) + '0:0';\n }\n const halves = text.split('::');\n if (halves.length > 2) {\n throw invalid(`不是有效的 IPv6 地址:${address}`);\n }\n const head = parseGroups(halves[0] ?? '');\n const rest = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];\n const missing = 8 - head.length - rest.length;\n if (halves.length === 2 ? missing < 1 : missing !== 0) {\n throw invalid(`不是有效的 IPv6 地址:${address}`);\n }\n groups.push(...head);\n for (let index = 0; index < missing && halves.length === 2; index += 1) {\n groups.push(0);\n }\n groups.push(...rest);\n if (tailGroups.length === 2) {\n groups.splice(6, 2, ...tailGroups);\n }\n const bytes = new Uint8Array(16);\n groups.forEach((group, index) => {\n bytes[index * 2] = (group >> 8) & 0xff;\n bytes[index * 2 + 1] = group & 0xff;\n });\n return bytes;\n}\n\nfunction parseGroups(text: string): number[] {\n if (text.length === 0) {\n return [];\n }\n return text.split(':').map((group) => {\n if (!/^[0-9a-f]{1,4}$/u.test(group)) {\n throw invalid(`IPv6 分组不合法:${group}`);\n }\n return Number.parseInt(group, 16);\n });\n}\n\nfunction ipv4Rule(base: string, bits: number, reason: string): PrefixRule {\n return Object.freeze({ prefix: ipv4ToBytes(base), bits, reason });\n}\n\nfunction ipv6Rule(base: string, bits: number, reason: string): PrefixRule {\n return Object.freeze({ prefix: ipv6ToBytes(base), bits, reason });\n}\n\n/** Build the PTR query name for an IP (`in-addr.arpa` / `ip6.arpa`). */\nexport function reverseLookupName(host: ParsedHost): string {\n if (host.kind === 'ipv4') {\n return `${Array.from(host.bytes).reverse().join('.')}.in-addr.arpa`;\n }\n if (host.kind === 'ipv6') {\n const nibbles: string[] = [];\n for (const byte of host.bytes) {\n nibbles.push((byte >> 4).toString(16));\n nibbles.push((byte & 0x0f).toString(16));\n }\n return `${nibbles.reverse().join('.')}.ip6.arpa`;\n }\n throw invalid('反向解析需要 IP 地址');\n}\n\n/** Human label for a parsed host. */\nexport function displayHost(host: ParsedHost): string {\n return host.kind === 'domain' ? host.name : host.address;\n}\n"]}
1
+ {"version":3,"file":"address-policy.js","sourceRoot":"","sources":["../src/address-policy.ts"],"names":[],"mappings":"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","sourcesContent":["import { message, formatMessage, type NettoolsMessage } from './messages.js';\n/**\n * Target address policy (SSRF guard).\n *\n * Network tools probe hosts that users type into chat. Even though the probes\n * themselves run on third-party services (check-host.net, public DoH, RDAP),\n * we refuse anything that points at private, loopback, link-local, reserved or\n * cloud-metadata space so the bot can never be used to map internal networks\n * or poke metadata endpoints through a relay.\n *\n * Design:\n * 1. Canonicalise first. The input is fed through the WHATWG URL host parser,\n * the same parser browsers and Node's fetch use. It turns odd IPv4 spellings\n * (`2130706433`, `0x7f.1`, `017700000001`, `127.1`) into dotted quads,\n * normalises IPv6 and converts IDN to punycode. We then classify the\n * canonical form, so \"what we checked\" is exactly \"what we send\".\n * 2. Classify by bytes. IPv4 and IPv6 are compared as raw bytes against\n * prefix tables. Embedded IPv4 (IPv4-mapped, NAT64, 6to4) is unwrapped and\n * classified as IPv4.\n * 3. Allow-list IPv6. Only global unicast (2000::/3) is accepted, minus the\n * documentation / Teredo / special ranges listed below.\n * 4. Names. Single-label names, internal suffixes (`.local`, `.internal`, ...)\n * and well-known metadata hostnames are refused before any lookup.\n * Hostnames are additionally resolved (see target-guard.ts) and refused\n * when any A/AAAA record is blocked.\n */\nimport { isIPv4, isIPv6 } from 'node:net';\nimport { NettoolsError } from './errors.js';\n\nexport type ParsedHost =\n | { readonly kind: 'ipv4'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'ipv6'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'domain'; readonly name: string };\n\nexport type AddressVerdict =\n | { readonly blocked: false }\n | { readonly blocked: true; readonly reason: string; readonly detail: NettoolsMessage };\n\nexport interface ParseHostOptions {\n /** Allow `_` in labels (needed for DNS names such as `_dmarc.example.com`). */\n readonly allowUnderscore?: boolean;\n /** Minimum number of labels for a domain (default 2, i.e. a full domain). */\n readonly minLabels?: number;\n}\n\nconst ALLOWED: AddressVerdict = Object.freeze({ blocked: false });\n\nfunction blocked(detail: NettoolsMessage): AddressVerdict {\n return Object.freeze({ blocked: true, reason: formatMessage(detail), detail });\n}\n\n/* ------------------------------------------------------------------ */\n/* Tables */\n/* ------------------------------------------------------------------ */\n\ninterface PrefixRule {\n readonly prefix: Uint8Array;\n readonly bits: number;\n readonly reason: NettoolsMessage;\n}\n\nconst CLOUD_METADATA_REASON = message('cloudMetadata');\n\n/** Exact IPv4 metadata endpoints, checked before the range table for a clearer message. */\nconst CLOUD_METADATA_IPV4 = new Set([\n '169.254.169.254', // AWS / GCP / Azure / Oracle / OpenStack instance metadata\n '169.254.170.2', // AWS ECS task metadata\n '169.254.169.123', // AWS Time Sync\n '100.100.100.200', // Alibaba Cloud metadata\n '168.63.129.16', // Azure WireServer (public range, still host-internal)\n]);\n\n/** Exact IPv6 metadata endpoints. */\nconst CLOUD_METADATA_IPV6 = ['fd00:ec2::254', 'fd00:ec2::23'];\n\nconst BLOCKED_IPV4_RANGES: readonly PrefixRule[] = [\n ipv4Rule('0.0.0.0', 8, message('unspecifiedV4')),\n ipv4Rule('10.0.0.0', 8, message('privateV4')),\n ipv4Rule('100.64.0.0', 10, message('cgnat')),\n ipv4Rule('127.0.0.0', 8, message('loopback')),\n ipv4Rule('169.254.0.0', 16, message('linkLocal')),\n ipv4Rule('172.16.0.0', 12, message('privateV4')),\n ipv4Rule('192.0.0.0', 24, message('ietfReserved')),\n ipv4Rule('192.0.2.0', 24, message('documentation')),\n ipv4Rule('192.88.99.0', 24, message('relay6to4')),\n ipv4Rule('192.168.0.0', 16, message('privateV4')),\n ipv4Rule('198.18.0.0', 15, message('benchmark')),\n ipv4Rule('198.51.100.0', 24, message('documentation')),\n ipv4Rule('203.0.113.0', 24, message('documentation')),\n ipv4Rule('224.0.0.0', 4, message('multicast')),\n ipv4Rule('240.0.0.0', 4, message('reservedBroadcast')),\n];\n\n/** Special IPv6 ranges inside 2000::/3 that are still refused. */\nconst BLOCKED_GLOBAL_IPV6_RANGES: readonly PrefixRule[] = [\n ipv6Rule('2001::', 32, message('teredo')),\n ipv6Rule('2001:2::', 48, message('benchmark')),\n ipv6Rule('2001:10::', 28, message('orchid')),\n ipv6Rule('2001:db8::', 32, message('documentation')),\n ipv6Rule('3fff::', 20, message('documentation')),\n];\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\n/** Suffixes that only make sense inside a private network or are reserved (RFC 2606 / 6761 / 6762). */\nconst BLOCKED_SUFFIXES = [\n 'localhost',\n 'local',\n 'localdomain',\n 'internal',\n 'intranet',\n 'private',\n 'corp',\n 'home',\n 'lan',\n 'home.arpa',\n 'arpa',\n 'test',\n 'invalid',\n 'example',\n 'onion',\n];\n\nconst MAX_HOSTNAME_LENGTH = 253;\nconst MAX_LABEL_LENGTH = 63;\n\n/* ------------------------------------------------------------------ */\n/* Parsing */\n/* ------------------------------------------------------------------ */\n\n/**\n * Parse and canonicalise one user-supplied host (domain, IPv4 or IPv6).\n * Throws {@link NettoolsError} (`invalid_input`) for anything that is not a\n * bare host: URLs, ports, paths, user-info, zone ids and percent-encoding are\n * all refused rather than \"helpfully\" stripped.\n */\nexport function parseHostInput(input: string, options: ParseHostOptions = {}): ParsedHost {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n throw invalid(message('hostRequired'));\n }\n if (trimmed.length > MAX_HOSTNAME_LENGTH + 2) {\n throw invalid(message('hostTooLong'));\n }\n if (/[\\s/\\\\@?#%]/u.test(trimmed)) {\n throw invalid(message('bareHost'));\n }\n\n const bracketed = trimmed.startsWith('[') && trimmed.endsWith(']');\n const ipv6Candidate = bracketed ? trimmed.slice(1, -1) : trimmed;\n if (bracketed || trimmed.includes(':')) {\n if (!isIPv6(ipv6Candidate)) {\n throw invalid(message('ipv6NoPort'));\n }\n const canonical = canonicalizeWithUrlParser(`[${ipv6Candidate}]`);\n const address = canonical.slice(1, -1);\n return Object.freeze({ kind: 'ipv6', address, bytes: ipv6ToBytes(address) });\n }\n\n const canonical = canonicalizeWithUrlParser(trimmed);\n if (isIPv4(canonical)) {\n return Object.freeze({ kind: 'ipv4', address: canonical, bytes: ipv4ToBytes(canonical) });\n }\n const name = canonical.endsWith('.') ? canonical.slice(0, -1) : canonical;\n validateDomainSyntax(name, options);\n return Object.freeze({ kind: 'domain', name });\n}\n\n/**\n * Run the WHATWG host parser by assigning to `URL#hostname` on a placeholder\n * URL. An invalid host leaves the placeholder untouched, which we detect.\n */\nfunction canonicalizeWithUrlParser(host: string): string {\n const placeholder = 'placeholder.invalid';\n const probe = new URL(`http://${placeholder}/`);\n probe.hostname = host;\n const result = probe.hostname;\n const unchanged = result === placeholder && host.toLowerCase() !== placeholder;\n if (unchanged || result.length === 0) {\n throw invalid(message('invalidHost'));\n }\n return result;\n}\n\nfunction validateDomainSyntax(name: string, options: ParseHostOptions): void {\n if (name.length === 0 || name.length > MAX_HOSTNAME_LENGTH) {\n throw invalid(message('hostLength'));\n }\n const labels = name.split('.');\n if (labels.length < (options.minLabels ?? 2)) {\n throw invalid(message('fullDomain'));\n }\n const labelPattern = options.allowUnderscore\n ? /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/u\n : /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;\n for (const label of labels) {\n if (label.length === 0 || label.length > MAX_LABEL_LENGTH || !labelPattern.test(label)) {\n throw invalid(message('invalidLabel', { label: label || message('empty') }));\n }\n }\n const topLevel = labels[labels.length - 1] ?? '';\n if (/^\\d+$/u.test(topLevel)) {\n throw invalid(message('numericTld'));\n }\n}\n\nfunction invalid(detail: NettoolsMessage): NettoolsError {\n return new NettoolsError('invalid_input', detail);\n}\n\n/* ------------------------------------------------------------------ */\n/* Classification */\n/* ------------------------------------------------------------------ */\n\n/** Classify a parsed host without any network access. Domains are checked by name only. */\nexport function classifyParsedHost(host: ParsedHost): AddressVerdict {\n if (host.kind === 'domain') {\n return classifyHostname(host.name);\n }\n if (host.kind === 'ipv4') {\n return classifyIpv4Bytes(host.bytes);\n }\n return classifyIpv6Bytes(host.bytes);\n}\n\n/** Classify a literal address string (any spelling the URL parser understands). */\nexport function classifyAddress(address: string): AddressVerdict {\n const parsed = parseHostInput(address);\n if (parsed.kind === 'domain') {\n throw invalid(message('notIp', { address }));\n }\n return classifyParsedHost(parsed);\n}\n\nexport function classifyHostname(name: string): AddressVerdict {\n const normalized = name.toLowerCase().replace(/\\.$/u, '');\n if (BLOCKED_HOSTNAMES.has(normalized)) {\n return normalized.startsWith('localhost')\n ? blocked(message('localhost'))\n : blocked(CLOUD_METADATA_REASON);\n }\n for (const suffix of BLOCKED_SUFFIXES) {\n if (normalized === suffix || normalized.endsWith(`.${suffix}`)) {\n return blocked(message('reservedSuffix', { suffix }));\n }\n }\n return ALLOWED;\n}\n\nfunction classifyIpv4Bytes(bytes: Uint8Array): AddressVerdict {\n const dotted = Array.from(bytes).join('.');\n if (CLOUD_METADATA_IPV4.has(dotted)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n for (const rule of BLOCKED_IPV4_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nconst IPV4_MAPPED_PREFIX = ipv6ToBytes('::ffff:0:0');\nconst NAT64_WELL_KNOWN_PREFIX = ipv6ToBytes('64:ff9b::');\nconst NAT64_LOCAL_PREFIX = ipv6ToBytes('64:ff9b:1::');\nconst SIX_TO_FOUR_PREFIX = ipv6ToBytes('2002::');\nconst GLOBAL_UNICAST_PREFIX = ipv6ToBytes('2000::');\nconst UNIQUE_LOCAL_PREFIX = ipv6ToBytes('fc00::');\nconst LINK_LOCAL_PREFIX = ipv6ToBytes('fe80::');\nconst SITE_LOCAL_PREFIX = ipv6ToBytes('fec0::');\nconst MULTICAST_PREFIX = ipv6ToBytes('ff00::');\nconst CLOUD_METADATA_IPV6_BYTES = CLOUD_METADATA_IPV6.map((address) => ipv6ToBytes(address));\n\nfunction classifyIpv6Bytes(bytes: Uint8Array): AddressVerdict {\n if (bytes.every((byte) => byte === 0)) {\n return blocked(message('unspecifiedV6'));\n }\n const isLoopback = bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;\n if (isLoopback) {\n return blocked(message('loopbackV6'));\n }\n for (const metadata of CLOUD_METADATA_IPV6_BYTES) {\n if (matchesPrefix(bytes, metadata, 128)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n }\n if (matchesPrefix(bytes, IPV4_MAPPED_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), message('mappedV4'));\n }\n if (matchesPrefix(bytes, NAT64_WELL_KNOWN_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), message('nat64'));\n }\n if (matchesPrefix(bytes, NAT64_LOCAL_PREFIX, 48)) {\n return blocked(message('localNat64'));\n }\n if (matchesPrefix(bytes, SIX_TO_FOUR_PREFIX, 16)) {\n return classifyEmbeddedIpv4(bytes.slice(2, 6), message('sixToFour'));\n }\n if (matchesPrefix(bytes, UNIQUE_LOCAL_PREFIX, 7)) {\n return blocked(message('ula'));\n }\n if (matchesPrefix(bytes, LINK_LOCAL_PREFIX, 10)) {\n return blocked(message('linkLocalV6'));\n }\n if (matchesPrefix(bytes, SITE_LOCAL_PREFIX, 10)) {\n return blocked(message('siteLocalV6'));\n }\n if (matchesPrefix(bytes, MULTICAST_PREFIX, 8)) {\n return blocked(message('multicastV6'));\n }\n if (!matchesPrefix(bytes, GLOBAL_UNICAST_PREFIX, 3)) {\n return blocked(message('notGlobalV6'));\n }\n for (const rule of BLOCKED_GLOBAL_IPV6_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nfunction classifyEmbeddedIpv4(embedded: Uint8Array, wrapper: NettoolsMessage): AddressVerdict {\n const verdict = classifyIpv4Bytes(embedded);\n if (!verdict.blocked) {\n return verdict;\n }\n const dotted = Array.from(embedded).join('.');\n return blocked(message('embeddedV4', { wrapper, address: dotted, reason: verdict.detail }));\n}\n\n/* ------------------------------------------------------------------ */\n/* Byte helpers */\n/* ------------------------------------------------------------------ */\n\nfunction matchesPrefix(bytes: Uint8Array, prefix: Uint8Array, bits: number): boolean {\n if (bytes.length !== prefix.length) {\n return false;\n }\n const fullBytes = Math.floor(bits / 8);\n for (let index = 0; index < fullBytes; index += 1) {\n if (bytes[index] !== prefix[index]) {\n return false;\n }\n }\n const remainingBits = bits % 8;\n if (remainingBits === 0) {\n return true;\n }\n const mask = (0xff << (8 - remainingBits)) & 0xff;\n return ((bytes[fullBytes] ?? 0) & mask) === ((prefix[fullBytes] ?? 0) & mask);\n}\n\n/** Dotted-quad IPv4 to 4 bytes. Callers must pass a canonical dotted quad. */\nexport function ipv4ToBytes(address: string): Uint8Array {\n const parts = address.split('.').map((part) => Number(part));\n const valid = parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);\n if (!valid) {\n throw invalid(message('invalidV4', { address }));\n }\n return Uint8Array.from(parts);\n}\n\n/** Textual IPv6 (with optional `::` and dotted IPv4 tail) to 16 bytes. */\nexport function ipv6ToBytes(address: string): Uint8Array {\n let text = address.toLowerCase();\n const groups: number[] = [];\n const lastColon = text.lastIndexOf(':');\n const tail = text.slice(lastColon + 1);\n const tailGroups: number[] = [];\n if (tail.includes('.')) {\n const v4 = ipv4ToBytes(tail);\n tailGroups.push(((v4[0] ?? 0) << 8) | (v4[1] ?? 0));\n tailGroups.push(((v4[2] ?? 0) << 8) | (v4[3] ?? 0));\n text = text.slice(0, lastColon + 1) + '0:0';\n }\n const halves = text.split('::');\n if (halves.length > 2) {\n throw invalid(message('invalidV6', { address }));\n }\n const head = parseGroups(halves[0] ?? '');\n const rest = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];\n const missing = 8 - head.length - rest.length;\n if (halves.length === 2 ? missing < 1 : missing !== 0) {\n throw invalid(message('invalidV6', { address }));\n }\n groups.push(...head);\n for (let index = 0; index < missing && halves.length === 2; index += 1) {\n groups.push(0);\n }\n groups.push(...rest);\n if (tailGroups.length === 2) {\n groups.splice(6, 2, ...tailGroups);\n }\n const bytes = new Uint8Array(16);\n groups.forEach((group, index) => {\n bytes[index * 2] = (group >> 8) & 0xff;\n bytes[index * 2 + 1] = group & 0xff;\n });\n return bytes;\n}\n\nfunction parseGroups(text: string): number[] {\n if (text.length === 0) {\n return [];\n }\n return text.split(':').map((group) => {\n if (!/^[0-9a-f]{1,4}$/u.test(group)) {\n throw invalid(message('invalidV6Group', { group }));\n }\n return Number.parseInt(group, 16);\n });\n}\n\nfunction ipv4Rule(base: string, bits: number, reason: NettoolsMessage): PrefixRule {\n return Object.freeze({ prefix: ipv4ToBytes(base), bits, reason });\n}\n\nfunction ipv6Rule(base: string, bits: number, reason: NettoolsMessage): PrefixRule {\n return Object.freeze({ prefix: ipv6ToBytes(base), bits, reason });\n}\n\n/** Build the PTR query name for an IP (`in-addr.arpa` / `ip6.arpa`). */\nexport function reverseLookupName(host: ParsedHost): string {\n if (host.kind === 'ipv4') {\n return `${Array.from(host.bytes).reverse().join('.')}.in-addr.arpa`;\n }\n if (host.kind === 'ipv6') {\n const nibbles: string[] = [];\n for (const byte of host.bytes) {\n nibbles.push((byte >> 4).toString(16));\n nibbles.push((byte & 0x0f).toString(16));\n }\n return `${nibbles.reverse().join('.')}.ip6.arpa`;\n }\n throw invalid(message('reverseNeedsIp'));\n}\n\n/** Human label for a parsed host. */\nexport function displayHost(host: ParsedHost): string {\n return host.kind === 'domain' ? host.name : host.address;\n}\n"]}