Repository Wiki
jason5ng32/MyIP

Whois 查询与注册信息

Whois 工具为域名、公网 IP 和 ASN 提供注册信息查询。实现并非统一调用传统 WHOIS:域名优先使用 WHOIS,IP 优先使用 RDAP,ASN 则只使用 RDAP;前端通过统一的 __raw 文本字段展示结果。

目的与范围

本页覆盖输入识别、GET /api/whois、WHOIS/RDAP 选择与回退、IANA 引导数据、返回结构、前端展示及运行边界。ASN 排名、网络拓扑、前缀与 RPKI 等属于 ASN 档案能力;IP 地理定位、DNS 解析和整体部署不在本页展开。

本文依据已读取的实现片段。共享输入校验器、路由守卫、缓存响应头中间件、上游请求封装及完整 RDAP 文本格式化函数未展开读取,因此不推断它们的内部规则、超时实现或测试保证。

概述

查询类型第一来源回退来源前端展示
域名whoiser.domain(),传统 WHOISrdapDomain()按服务提供方分组的折叠面板
公网 IPv4/IPv6rdapIp()whoiser.ip(),限制为一跳单个文本块
ASNrdapAutnum()无单个文本块

这一差异来自上游协议特性:传统域名 WHOIS 可提供丰富记录,但部分新顶级域缺少可用的端口 43 服务;IP 的 RDAP 路径可以避免 WHOIS 客户端无法处理的 rwhois referral。这里的 __raw 是兼容展示字段:WHOIS 路径返回原始文本,RDAP 路径则从 JSON 生成类 WHOIS 文本,不能把两者都理解为服务器返回的原始报文。

依据:get-whois.js、rdap.js。

架构与入口

Loading diagram...

Sources: Whois.vue、backend-server.js、get-whois.js、rdap.js。

所有 /api 路由先经过默认 Cache-Control: no-store 设置和 requireReferer。Whois 再按注册顺序执行 ASN 规范化、缓存策略与处理器。它没有挂载本地 MaxMind 数据就绪守卫;其查找依赖外部注册服务,而非本地地理定位库。

实际路由声明如下,ONE_DAY_CACHE 在同一文件中为 24 * 60 * 60:

javascript
app.get('/api/whois', normalizeAsnQuery(), cacheable(ONE_DAY_CACHE), getWhois);

Source: backend-server.js。

这里可以确认路由声明了一天的缓存策略,但不能据此断言服务端缓存了查询结果、所有错误响应都可缓存,或某个部署一定有边缘缓存命中;这些取决于中间件与部署环境。

输入到请求:前端控制流

输入识别与规范化

Whois.vue 的 onSubmit() 先记录 trackEvent('Section', 'StartClick', 'Whois'),清空错误、提供方列表和旧结果,再调用 validInput()。校验成功才发出请求。

识别顺序是 ASN、域名、IP,而不是先判断 IP:

  1. 调用 parseAsnInput(input, { requirePrefix: true });成功后把类型设为 asn,发送 AS${asn}。组件明确不接受裸数字作为 ASN。
  2. 调用 formatURL():没有小写 http:// 或 https:// 前缀时先补 http://,通过 URL 取 hostname,再取最后两个点分段,使用 isValidDomain() 校验。
  3. 域名识别失败后再检测 IP。有效但不可用作公网地址的输入显示 whois.reservedIP,不发送请求。
  4. 其他输入显示 whois.invalidURL。

域名规范化的实际实现:

javascript
1const formatURL = (domain) => { 2 if (!domain.match(/^https?:\/\//)) domain = 'http://' + domain; 3 try { 4 const url = new URL(domain); 5 const parts = url.hostname.split('.'); 6 const mainDomain = parts.slice(-2).join('.'); 7 if (isValidDomain(mainDomain)) return mainDomain; 8 } catch { /* noop */ } 9 return false; 10};

Source: Whois.vue。

关键边界:这不是公共后缀列表算法。 最后两段截取会丢弃子域,也不能正确识别多级公共后缀下的可注册域。例如按该代码计算,example.co.uk 会被截成 co.uk。后端则不执行这一截取,因此前端查询与直接 API 调用可能查询不同名称。

请求状态与结果展示

getWhoisResults(query) 将状态设为 running,通过 fetch 访问同源 API。运行期间输入框和按钮被禁用,并显示 Spinner;请求完成、4xx 返回及异常路径都会将状态恢复为 idle。

成功响应还需满足展示条件:

  • 域名:getProviders() 只保留键名匹配域名形式且具有真值 __raw 的条目,至少一个条目才能显示。提供方标题转为大写。
  • IP/ASN:要求顶层 data.__raw 为真值。
  • 满足 HTTP 成功但不满足上述结构时,仍显示 whois.fetchError。
  • 只有成功的域名查询发送 emitAppEvent('whois:lookup', { query });IP 和 ASN 分支没有发送此事件。

域名文本会去掉每行起始的 1~20 个空格,并在第一个 \nFor more information 位置截断;IP/ASN 文本去掉匹配 # 注释行的内容、开头空行及末尾一个换行。这些是展示清理,并不修改服务端记录。模板使用文本插值放入 <pre>,没有通过 v-html 渲染上游文本。

依据:Whois.vue、Whois.vue。

查询分支与回退机制

后端校验

处理器从 req.query.q 读取参数。缺失、空字符串或非字符串返回 400;这也拒绝 Express 对重复 q 参数生成的数组。随后先匹配 ^AS(\d+)$。代码依赖路由守卫提前把 ASN 规范化为 AS<n>,直接调用处理器不能假设小写 ASN 等输入也受支持。

非 ASN 输入必须满足 IP 校验或后端域名正则。域名正则要求点分隔的字母数字标签,可在标签内部使用连字符,末段必须是至少两个 ASCII 字母;处理器本身不接收完整 URL,也不执行 trim()。公网判断在语法判断之后,非公网 IP 返回 400,避免浪费外部查询。

依据:get-whois.js。

三条路径

Loading diagram...

Source: get-whois.js。

ASN: 将数字部分转换为 Number 后调用 rdapAutnum()。isAutnumMissing() 判断“ASN 不存在”或“没有 RDAP 端点”错误并映射为 404;其余错误记录 error 日志并返回 500。

IP: 任意 rdapIp() 异常都会触发 warn 日志并继续 WHOIS,包括 RDAP 的“未找到”。回退请求使用 5 秒超时、follow: 1、raw: true。其目的不是继续追踪 referral,而是限制到一跳,规避不兼容 referral。回退结果直接返回,不进行域名分支那样的原始文本完整性检查。

javascript
const ipinfo = await whoiser.ip(query, { timeout: 5000, follow: 1, raw: true }); return res.json(ipinfo);

Source: get-whois.js。

域名: 先执行 WHOIS,保留隐私相关数据,最多跟随两跳。WHOIS 抛错会被吞掉;正常返回但没有任何服务提供方携带非空字符串 __raw 时也回退 RDAP。该判定只检查文本存在,不分析文本是否真正代表有效注册信息。

javascript
1function domainHasWhoisText(result) { 2 if (!result || typeof result !== 'object') return false; 3 return Object.values(result).some( 4 (v) => v && typeof v === 'object' && typeof v.__raw === 'string' && v.__raw.length > 0, 5 ); 6}

Source: get-whois.js。

因此空白字符串以外的任意非空字符串、甚至仅含空格的字符串,都能满足后端这个判定。扩展时若希望识别“无记录”文本,需要显式增加语义判断,而不是依赖现有条件。

RDAP:端点选择、数据结构与生命周期

IANA 引导与缓存

loadBootstrap(file) 从 https://data.iana.org/rdap/ 获取引导数据,以文件名作为 bootstrapCache 的键。条目包含 data 和 expiresAt,有效期为 24 小时:未过期直接返回,缺失或过期则重新请求;只有响应成功并解析 JSON 后才写入缓存。非成功 HTTP 状态记录日志并抛出 RDAP bootstrap failed: <status>。

四类引导文件分别为 dns.json、ipv4.json、ipv6.json 和 asn.json。此缓存保存的是“哪个注册服务负责哪段资源”的映射,而不是某个域名、IP 或 ASN 的注册结果。

Loading diagram...

Source: rdap.js。

三类端点匹配算法

类别匹配方式端点选择请求路径
域名取最后一段 TLD,不区分大小写匹配服务条目匹配条目的 urls[0]/domain/ 加 encodeURIComponent(domain)
IP解析 CIDR、检查地址族、选择包含 IP 的最长前缀优先列表中的 HTTPS,否则首个 URL/ip/ 加 IP 原文
ASN按引导文件顺序匹配单值或闭区间 N-M优先 HTTPS,否则首个 URL/autnum/ 加数字 ASN

IP 的最长前缀匹配只在找到严格更长的匹配时替换结果,因此同长度匹配保留先前选中的端点。无效 CIDR 或地址族不符的条目会跳过;无法转为大整数的输入返回 null。ASN 则是第一个匹配区间,不是最长前缀算法。

IPv6 路径特意不使用 encodeURIComponent():代码注释说明某些经 RIR 重定向到达的服务会拒绝编码为 %3A 的冒号。所有路径都只选中一个引导端点,没有遍历备用 URL 的实现。

依据:rdap.js。

返回结构与注册信息

域名 RDAP 返回对象以所选基础 URL 的 hostname 为键,内层保留 RDAP JSON 并添加生成的 __raw;它有意模拟 whoiser.domain() 的多提供方结构。IP 返回顶层 RDAP JSON 加 __raw,模拟 whoiser.ip()。

ASN 不透传整份 RDAP JSON,而通过 parseAutnum(data, asn, host) 提取以下字段:

字段提取方式与缺省值
asn调用方传入的 ASN
handle、nameRDAP 同名字段,缺省 null
rir已知 hostname 映射为 ARIN、RIPE NCC、APNIC、LACNIC、AFRINIC;否则 hostname,最终可为 null
statusRDAP status,缺省空数组
registeredregistration 事件日期,缺省 null
lastChangedlast changed 事件日期,缺省 null
registrant首个 registrant 实体的 vCard fn,否则 org,否则 null
country顶层 data.country,缺省 null
abuse首个 abuse 实体的第一条 email,缺省 null
__rawformatAutnum(data) 生成的文本

实体角色查找使用深度优先遍历:先检查当前实体,再查其子实体,最后继续同级实体。事件先按 eventAction 写入对象,因此同类事件重复出现时后面的日期覆盖前面的。vCard 提取保留每种属性的值数组,忽略 version;字段解析没有把所有联系人合并为一份注册人记录。

javascript
1 const events = {}; 2 for (const e of data.events || []) events[e.eventAction] = e.eventDate; 3 const registrant = findEntityByRole(data.entities, 'registrant'); 4 const registrantCard = registrant ? extractVcard(registrant) : {}; 5 const abuseCard = extractVcard(findEntityByRole(data.entities, 'abuse'));

Source: rdap.js。

rir 使用选中的引导基础 URL 的 hostname,而不是读取最终响应 URL;发生 HTTP 重定向时不能把该字段解释为最终响应服务的精确标识。完整文本格式化实现未在本次摘录中读取,因此不列出未经核实的 __raw 行格式。

依据:rdap.js。

API 与配置参考

HTTP 接口

GET /api/whois

  • 必需查询参数:q,单个非空字符串。
  • 业务输入:域名、公网 IP、经过路由规范化的 AS<n>。
  • 成功:返回对应查询分支的 JSON,三种类型的结构不同,不存在统一的 data 包装层。
  • 处理器错误:返回包含 error 字段的 JSON;共享守卫可能在进入处理器前结束请求,其具体状态与响应体未核实。
  • 不要把前端接受完整 URL 的行为视为 HTTP API 契约;URL 提取是在 Vue 组件中完成的。

实际调用代码:

javascript
const response = await fetch(`/api/whois?q=${query}`);

Source: Whois.vue。

RDAP 模块函数

以下是源码中的 JavaScript 签名形式;返回语义来自函数实现,而非 TypeScript 类型声明。

函数参数与返回失败行为
rdapDomain(domain, { timeoutMs = 5000 } = {})域名与可选请求超时;异步返回提供方键控对象无端点、404、其他非成功状态抛 Error;引导或网络失败向上传播
rdapIp(ip, { timeoutMs = 5000 } = {})IP 与可选请求超时;异步返回 RDAP 字段和 __raw无端点、404、其他非成功状态抛 Error
rdapAutnum(asn, { timeoutMs = 5000 } = {})ASN 经字符串化、去除开头 AS、转数字;异步返回解析字段无端点、404、其他非成功状态抛 Error
findIpEndpoint(services, ip)IANA 服务数组和 IP;返回 URL 或 null无匹配返回 null
findAutnumEndpoint(services, asn)服务数组和数字 ASN;返回首个匹配 URL 或 null无匹配返回 null
parseAutnum(data, asn, host)RDAP JSON、ASN、服务 hostname;返回注册信息对象不构成完整的上游 JSON schema 校验
isAutnumMissing(error)按错误消息前缀识别 ASN 无记录;返回布尔值对缺失消息按空字符串处理

依据:rdap.js。

参数与常量

本次已读实现没有 Whois 专属环境变量;下列值是代码常量或函数参数,不应描述为现成的部署开关。

参数/常量类型默认值作用
RDAP timeoutMsnumber,毫秒5000传给具体注册记录请求的 fetchUpstream
WHOIS timeoutnumber,毫秒5000IP 和域名 WHOIS 请求选项
IP WHOIS follownumber1限制 referral 跟随
域名 WHOIS follownumber2域名查询跟随上限选项
WHOIS rawbooleantrue请求原始文本
域名 ignorePrivacybooleanfalse不启用忽略隐私记录选项
CACHE_TTL_MSnumber,毫秒86400000IANA 引导数据内存有效期
ONE_DAY_CACHEnumber,秒86400Whois 路由传入的缓存策略时长
BOOTSTRAP_BASEstringhttps://data.iana.org/rdap/IANA 引导文件地址

注意:引导文件请求直接调用 fetchUpstream(BOOTSTRAP_BASE + file),并没有传入查询函数的 timeoutMs。其默认超时取决于共享封装,不能用此表中的 5 秒替代。

依据:get-whois.js、rdap.js、backend-server.js。

错误、边界与并发

HTTP 错误映射

场景后端处理前端最终提示
缺失/非字符串 q400,No address providedwhois.noData
非合法 IP/域名400,Invalid IP or addresswhois.noData
非公网 IP400,Not a public IP address若请求到达后端则为 whois.noData;前端通常先显示 whois.reservedIP
ASN 无记录或无端点404whois.noData
域名无 RDAP 端点404whois.noData
域名 RDAP 返回 404处理器实际返回 500whois.fetchError
IP RDAP 失败、WHOIS 也抛错500,使用 WHOIS 错误消息whois.fetchError
其他 RDAP 故障500whois.fetchError
HTTP 成功但缺少可展示文本后端可能已经返回 200whois.fetchError

域名 RDAP 404 的差异尤其重要:rdapDomain() 抛出 Domain not found: ...,处理器却只将以 No RDAP endpoint for 开头的错误映射为 404,其余都返回 500。不能依据前端注释把“未注册域名”概括为一定返回 4xx。

前端对非成功响应不读取错误 JSON:低于 500 的失败状态统一显示无数据,500 及以上抛出本地错误后进入通用异常提示。因此服务端日志比 UI 更适合区分上游故障原因。

依据:get-whois.js、rdap.js、Whois.vue。

并发与一致性

  • 前端是交互级防重复,不是请求协调器。 输入和按钮在运行时禁用,但 onSubmit() 本身没有检查运行状态;实现没有 AbortController、请求序号或“只接受最后一次响应”的逻辑。若通过其他途径触发重叠调用,共享的 type、providers 和结果状态存在被不同请求交叉使用的可能。
  • 引导缓存没有请求合并。 bootstrapCache 存储已解析数据,不存储在途 Promise;同一文件冷启动或过期时,多个并发请求可能同时访问 IANA。
  • 缓存刷新失败不提供旧值回退。 已过期条目不会被返回;新请求失败向上抛出。
  • 没有跨进程共享的证据。 Map 属于当前模块所在进程,不能视为多实例一致缓存;重启后需要重新获取引导数据。

依据:Whois.vue、rdap.js。

性能、运行维护与扩展

性能与排障

回退是串行执行,5 秒是单项查询选项而非整个接口的端到端预算。冷启动还会增加 IANA 引导请求;域名需要先等待 WHOIS 返回或失败,IP 则先等待 RDAP。已读实现没有应用层重试循环,回退不能等同于重试;共享请求封装和第三方库的内部行为未核实。

排障时建议按实际链路检查:

  1. 比较用户输入与规范化后 q,尤其是多级公共后缀、完整 URL、ASN 前缀及公网 IP 校验。
  2. 判断 HTTP 成功还是仅缺少 __raw;前端都可能显示通用失败,但根因不同。
  3. IP 路径查看 whois: RDAP IP lookup failed, trying WHOIS,确认是否已经切到传统协议。
  4. 域名首次 WHOIS 异常会被吞掉,不应期待存在对应错误日志;最终 RDAP 失败才有明确日志。
  5. 区分 IANA 引导失败与注册端点失败。前者影响一类资源的端点发现,后者影响已选中的服务查询。
  6. 核查运行环境到 IANA/RDAP HTTPS 和 WHOIS 端口 43 的连通性;仅网页可访问并不证明所有查询路径可用。

扩展时应保留的契约

  • 新增上游时保持域名的“提供方 → __raw”结构,以及 IP/ASN 的顶层 __raw;否则现有组件无法直接展示。
  • 新增注册机构可扩展 RIR_BY_HOST,未知机构目前以 hostname 显示,不会仅因缺少映射而失败。
  • 修改域名错误分类时同步调整处理器状态映射,避免仅修正底层消息却改变前端“无数据/请求失败”的语义。
  • 若优化域名规范化,应替换最后两段截取策略,并验证前后端查询目标一致。
  • 若增加缓存请求合并或超时预算,应分别考虑引导获取、注册查询和协议回退,而不是只改变一个 5000 常量。

这些是基于现有实现边界的维护建议,不代表已经存在的功能。

测试与核实边界

findIpEndpoint、findAutnumEndpoint、parseAutnum 的源码注释明确说明其导出用于测试,但本次有限读取未包含测试文件,不能声称已验证具体覆盖率或用例通过情况。建议重点回归:最长前缀及地址族匹配、ASN 区间边界、嵌套实体角色、WHOIS 空结果回退、域名 RDAP 404 映射、重复 q 参数、缓存过期并发与多级公共后缀输入。

相关链接

Sources

(4 files)
common
frontend/components/advanced-tools