Repository Wiki
jason5ng32/MyIP

前后端运行架构与 API 边界

MyIP 的运行入口由两个 Express 应用组成:前端服务器负责构建产物、浏览器路由回退与 /api 代理,后端服务器负责公共请求管线、业务路由及本地数据初始化。本页从服务器入口解释两者如何协作,以及哪些行为属于全局边界、哪些必须由具体业务处理器决定。

目的与范围

本文覆盖运行脚本、生产请求分流、API 中间件顺序、路由注册、缓存策略声明、启动就绪和运行风险。IP 信息、DNS、ASN、报告和用户功能只说明入口及保护策略;各供应商协议、结果结构、数据库实现、用户认证和部署编排应在相应专题中展开。

**证据边界:**本页依据两个服务器入口和包配置编写。没有核验浏览器组件请求代码、Vite 代理配置、公共守卫及业务处理器内部实现,因此不把路由注册推断为完整 API 参数协议,也不把服务器注释视为底层实现的独立验证。运行上下文未提供其他目录页路径,故不生成猜测性的 Wiki 链接。

概述

  • 构建阶段:build 执行 vite build;前端运行服务器读取 dist,而不是现场编译 Vue。
  • 开发阶段:dev 并行运行 Vite 与通过 nodemon 启动的后端;它不启动生产前端服务器。
  • 常规运行阶段:start 并行启动前端和后端 Node 进程。默认前端端口为 18966,后端为 11966。
  • **浏览器访问边界:**前端先把 /api 交给后端代理,再尝试静态文件,最后处理符合条件的 SPA 导航。
  • **后端边界:**统一的请求日志、限流、JSON 解析、默认缓存头和 Referer 守卫位于业务处理器之前;部分路由额外使用参数守卫、本地数据就绪门禁和缓存声明。

依据:package.json、frontend-server.js、backend-server.js。

运行架构

Loading diagram...

Sources: frontend-server.js、backend-server.js。

这里的前端服务器是静态资源与代理入口,并非服务端 Vue 渲染器。后端也挂载了 dist 静态服务,但入口代码没有相同的 setStaticHeaders 与 SPA 回退逻辑。因此,直接访问后端端口不等价于访问前端端口:即使都能读取某些构建产物,深层页面导航与缓存行为仍不同。

图中的路由级中间件是按需组合的,不表示每条 API 都经过 requirePublicIP 或 requireOfflineData。

1. 进程、构建与运行环境

package.json 声明 ES Module、pnpm@12.4.2 和 Node 版本范围 ^24.15.0 || >=26.0.0。重要脚本如下:

脚本实际职责边界说明
dev并行启动 Vite 和 nodemon 后端开发代理地址未在已读源码中核验
buildvite build不启动服务器
postbuild存在时运行缓存清除脚本转发子进程退出状态;没有该文件则不运行
previewvite preview不能据此认为它具有生产前端服务器的代理配置
start-backend预加载 Sentry instrumentation 后启动后端只启动后端
start-frontend启动前端 Express只启动静态资源与代理入口
start并行启动两个 Node 入口脚本本身没有先等待后端就绪
test / checkNode 测试;测试通过后构建测试断言和覆盖范围未读取

源码示例:独立或联合启动

json
"start-backend": "node --import ./sentry-instrument.js backend-server.js", "start-frontend": "node frontend-server.js", "start": "concurrently \"node frontend-server.js\" \"node --import ./sentry-instrument.js backend-server.js\"",

Source: package.json。

这是 scripts 对象的原始片段,不是独立 JSON 文件。后端脚本显式使用 --import;仅用普通 Node 执行后端文件与仓库脚本的预加载行为并不完全一致。

2. 前端入口:代理、静态资源与 SPA 边界

2.1 /api 优先代理

javascript
1frontendApp.use('/api', createProxyMiddleware({ 2 target: `http://localhost:${backEndPort}/api`, 3 changeOrigin: true 4}));

Source: frontend-server.js。

代理目标使用 localhost 和共享的 BACKEND_PORT,并设置 changeOrigin: true。入口没有声明 pathRewrite、代理重试、超时或自定义失败响应;不能为这些行为补充未经验证的项目级保证。生产拓扑变更为远程后端时,单独改端口不足以更换目标主机。

2.2 静态文件缓存按类别分配

setStaticHeaders(res, filePath) 先把路径转换为相对于 dist 的 / 分隔路径,再按下表顺序匹配:

资源类别响应 Cache-Control 策略
assets/、fonts/public,浏览器缓存 365 天,immutable
favicons/public,缓存 30 天
任意层级的 png、jpg、jpeg、webp、svg、icopublic,缓存 7 天
.html、manifest.webmanifest、根级 SEO 文件浏览器 max-age=0,共享缓存 s-maxage=86400,must-revalidate
其他资源public,缓存 1 小时

根级 SEO 集合为 llms.txt、llms-full.txt、sitemap.xml、robots.txt。优先级很重要:assets/ 下的图片先命中一年策略,而不是普通图片的七天策略。源码注释明确把哈希资源、稳定字体和非哈希图片分开;非哈希图片更新需要配合文件名变更,避免旧内容长时间保留。

依据:frontend-server.js。

2.3 SPA 回退不是任意路径兜底

javascript
1frontendApp.use((req, res, next) => { 2 if (req.method !== 'GET' || !req.accepts('html')) return next(); 3 if (req.path.split('/').pop().includes('.')) return next(); 4 res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); 5 res.sendFile(path.join(distDir, 'index.html')); 6});

Source: frontend-server.js。

只有 GET、接受 HTML 且路径末段不含点号的请求才进入回退。判断并不是严格的“存在扩展名”,而是末段任意位置包含 . 就排除。因此带点的客户端路由也不会回退。

这个限制让缺失的 JS/CSS 资源继续落入未处理请求路径,而不是被错误地返回 HTML。Accept: */* 也可能通过 HTML 接受判断,所以末段检查承担独立的资源保护作用。静态层返回的 /、/index.html 与回退得到的深层页面使用不同缓存策略,后者明确不缓存。

3. API 的全局请求管线

后端注册顺序决定哪些错误能够携带哪些头、进入哪些日志。真实顺序如下:

  1. app.set('trust proxy', 1)。
  2. 可选 /api HTTP 日志。
  3. 可选 /api 请求次数限制。
  4. 可选 /api 渐进延迟。
  5. 全局 express.json({ limit: '500kb' })。
  6. /api 默认 Cache-Control: no-store。
  7. /api 全局 requireReferer。
  8. 路由级守卫、缓存声明与业务处理器。
  9. 后端静态文件服务。
  10. 条件注册的 Sentry Express 错误处理器。

依据:backend-server.js、backend-server.js、backend-server.js。

**不能把后置中间件的保证外推到所有响应:**限流器直接返回的 429,以及 JSON 解析阶段拒绝的请求,均发生在默认 no-store 和 Referer 守卫之前。正常进入该缓存头中间件的 API 响应才首先获得该默认值。

3.1 流量保护与客户端 IP

机制时间窗口与行为豁免
rateLimiter20 分钟;达到配置上限后返回 429 JSON/monitoring
speedLimiter1 小时;超出阈值后按超出次数乘 400ms 延迟,最多 5000ms/monitoring、/maxmind
monitoringLimiter20 分钟 600 次,仅挂在遥测 POST 路由无额外豁免声明

普通限制器配置为 0 时不挂载。MaxMind 只豁免渐进延迟,并没有豁免普通请求次数限制。

getClientIp(req) 按 cf-connecting-ip、x-forwarded-for 首项、cf-connecting-ipv6、req.ip 的顺序获取限流告警记录使用的地址。限制器未显式把这个函数设置为配额键生成器,不能声称该顺序就是计数身份算法。代理信任层数固定为 1,也应与实际入口拓扑一起审核。

3.2 缓存与安全守卫是不同边界

所有已注册 API 都位于 requireReferer 之后,包括用户、报告和遥测路由。守卫的具体允许规则和失败状态码未读取,因此不能把它描述为登录认证或授权系统。

默认不缓存,再由各路由显式调用 cacheable(...),使缓存成为逐路由声明而非全局默认。本文表格中的 TTL 是注册传参和源码注释所声明的策略,不是对 cacheable 内部所有状态码、响应头及条件判断的完整说明。

4. API 注册参考:方法、保护和缓存归属

下表覆盖服务器入口注册的路由。所有路径均以 /api 为前缀;“默认”表示没有路由级 cacheable 声明,正常经过公共缓存中间件后保持 no-store。参数名、响应对象和处理器异常只有在入口明确体现时才列出,不推测完整业务协议。

方法与相对路径路由级处理或守卫缓存声明
GET /service-statusneedsServiceStatus → serviceStatusHandler5 分钟
GET /service-status/detailrequireValidProviderId()、needsServiceStatus → serviceStatusDetailHandler5 分钟
GET /ipinfo、/ipapicom、/ipsb、/ipapiis、/ip2location各自处理器前使用 requirePublicIP()、withTimeZone()1 天
GET /maxmindrequirePublicIP()、needsMaxMind、withTimeZone() → maxmindHandler1 天
GET /whoisnormalizeAsnQuery() → getWhois1 天
GET /github-starsgithubStarsHandler1 天
GET /configsvalidateConfigs;注释说明提供环境变量派生的功能标志1 小时
GET /ooni-blockingrequireValidDomain() → ooniBlockingHandler1 天
GET /globalping-probesglobalpingProbesHandler7 天
GET /asn-profilerequireValidASN() → asnProfileHandler完整结果 7 天;降级策略见下文
GET /cfradarcfRadarHandler;req.query.view 选择注册项 TTL按视图决定,完整性条件控制
GET /asn-historyrequireValidPrefix() → asnHistoryHandler30 天
GET /asn-connectivityrequireValidASN()、needsAsGraph → asnConnectivityHandler30 天
GET /maccheckerneedsOui → macChecker30 天
GET /mapmapHandler1 年
GET /ipcheckingrequirePublicIP()、withTimeZone() → ipCheckingHandler默认
GET /dnsresolverrequireValidDomain('hostname')、requireValidRecordType() → dnsResolver默认
GET /dnsleaktest/session/:tokendnsLeakGetResult默认
GET /invisibilityinvisibilitytestHandler默认
GET /getuserinfogetUserinfo默认
PUT /updateuserachievementupdateUserAchievement默认
GET /report/:idrequireValidReportId() → getReportHandler默认
POST /reportcreateReportHandler默认
POST /persona/evaluatepersonaEvaluateHandler默认
POST /monitoring条件注册;独立限流、raw 解析 → sentryTunnelHandler默认

依据:backend-server.js。缺少显式路由级守卫不等于处理器内部没有校验;例如表格不能证明用户端点允许匿名写入。

4.1 完整结果、降级结果与隐私结果

ASN Profile 并不使用整条路由的离线数据门禁。入口通过完整性谓词和降级 TTL,把部分结果与完整结果分开处理:

javascript
1app.get('/api/asn-profile', requireValidASN(), cacheable(SEVEN_DAYS_CACHE, { 2 cacheIf: isCompleteProfile, 3 degradedMaxAge: () => (isOfflineBootstrapping() ? 0 : ONE_DAY_CACHE), 4}), asnProfileHandler);

Source: backend-server.js。

该声明表达三层策略:完整结果使用七天 TTL;离线初始化期间降级 TTL 为零;初始化结束后降级 TTL 为一天。源码注释解释,这样既避免把启动时数据缺失长期缓存,也减少对持续失败上游的重复访问。具体谓词如何判断字段完整性,需查看相应专题。

Cloudflare Radar 使用 RADAR_VIEWS[req.query.view]?.ttl 与 isCompleteRadarAnswer。入口注释给出 ASN 汇总及前缀列表七天、国家流量三十天、故障信息一小时,并明确部分结果不缓存;非法 view 的实际错误协议不能从这里推出。

报告读取和创建始终没有公共缓存声明。代码注释的理由是:边缘缓存可能超过报告 KV 的有效期,且私人诊断结果不适合公开缓存。这证明的是此处的缓存设计意图,不是对报告存储后端实现的核验。

依据:backend-server.js。

4.2 JSON 与遥测 envelope 的不同入口

普通请求统一经过 express.json({ limit: '500kb' })。源码说明共享报告可能达到约 100KB,所以把解析器上限提高到 500KB;报告处理器还应执行其自身的更紧限制,但该限制值未在本页读取。

遥测路由仅在 VITE_SENTRY_DSN_FRONTEND 为真值时注册,额外安装 express.raw({ type: () => true, limit: '10mb' })。函数形式的 type 用于匹配没有 Content-Type 的二进制 Replay envelope;普通 '*/*' 字符串匹配不满足注释所述需求。这个 raw 解析器仍位于全局 JSON 解析器之后,因此不能概括为“所有遥测内容都绕过 JSON 层、统一允许 10MB”。

依据:backend-server.js、backend-server.js。

5. 核心请求流程

下面以已经转发到后端、没有被公共前置保护拒绝的 MaxMind 请求说明路由执行顺序:

Loading diagram...

Sources: frontend-server.js、backend-server.js。

该流程中的 503 是入口注释描述的启动门禁行为;具体响应体与门禁错误处理未读取。withTimeZone() 的注册位置已确认,但其字段计算和响应包装方式不在本页推断。

6. 启动生命周期与就绪边界

bootBackend() 是无参数 async 函数,末尾直接调用。它并不等待所有离线下载完成后再开放监听,而是尽量加载已有快照,然后让需要缺失数据的路由承担局部不可用。

Loading diagram...

Source: backend-server.js。

图中的两个初始化任务表示传入 runOfflineBootstrap 的任务集合,不承诺该函数内部的并行调度方式。

关键时序与后果:

  1. 获取 MaxMind City 与 ASN 文件路径;两者均存在才先尝试加载,失败由空 catch 吞掉。
  2. 调用 app.listen,回调记录后端 ready 日志。监听成功不是全量数据就绪。
  3. 在下载之前调用 watchDatasets。注释解释,这样另一个进程发布或人工放入的数据不会被误当成 watcher 的初始基线而漏掉重载。
  4. 等待 runOfflineBootstrap:提交数据集初始化和服务状态初始化任务;数据集任务结束后再次检查 MaxMind,必要时重试加载并记录错误。
  5. 初始化返回后,才启动数据集调度器与服务状态轮询。

入口组装了四类门禁:MaxMind、AS 关系与组织、OUI、服务状态。它们分别检查 isMaxMindReady、isAsRelLoaded 与 isAsOrgLoaded、isOuiLoaded、isServiceStatusPrimed。注释描述下载失败为非致命,并指出不同功能后续可能持续 503 或降级;具体降级数据和状态应以业务专题为准。

源码示例:监听之后执行后台初始化

javascript
1 watchDatasets(datasets); 2 3 await runOfflineBootstrap([ 4 async () => { 5 await bootstrapDatasets(datasets); 6 if (isMaxMindReady()) return; 7 await reloadMaxMindDatabases('startup').catch(() => { 8 logger.error('❌ MaxMind API will return 503 until databases are available: set MAXMIND_ACCOUNT_ID + ' 9 + 'MAXMIND_LICENSE_KEY, or drop GeoLite2-City.mmdb + GeoLite2-ASN.mmdb into common/maxmind-db/'); 10 }); 11 }, 12 bootstrapServiceStatus, 13 ]); 14 15 startDatasetScheduler(datasets); 16 startServiceStatusPolling();

Source: backend-server.js。

7. 配置选项

两个入口都调用 dotenv.config({ quiet: true }),端口和流量阈值通过十进制 parseInt 读取。入口没有额外的数值范围验证,生产配置应避免空白、非法数字和不合理阈值。

配置名入口读取类型默认值或启用条件影响范围
FRONTEND_PORT字符串转整数18966前端监听端口
BACKEND_PORT字符串转整数11966后端监听端口和前端代理目标端口
SECURITY_RATE_LIMIT字符串转整数0,不挂载普通 API 的 20 分钟请求上限
SECURITY_DELAY_AFTER字符串转整数0,不挂载普通 API 的一小时延迟阈值
SECURITY_BLACKLIST_LOG_FILE_PATH字符串空串,不写文件限流告警的可选本地账本
LOG_HTTP字符串精确比较仅 'true' 启用/api 请求日志
VITE_SENTRY_DSN_FRONTEND字符串真值判断未设置则不注册后端遥测转发路由
SENTRY_DSN_BACKEND字符串真值判断未设置则不安装此错误处理器Sentry Express 错误处理
MAXMIND_ACCOUNT_ID、MAXMIND_LICENSE_KEY此入口未解析无法从入口确认默认值MaxMind 不可用时日志提示的恢复配置

依据:frontend-server.js、backend-server.js、backend-server.js、backend-server.js。

此外,JSON 上限、监控 raw 上限、各缓存时长、监控配额和代理信任层数都是当前入口中的常量,不是本页可确认的环境变量选项。

8. 故障、并发与运维注意事项

8.1 排障时先区分故障所属层

现象优先检查的已验证边界
页面可访问,API 不可用前端与后端是独立进程;检查后端端口与启动日志
深层页面刷新失败是否绕过了前端入口;方法、Accept 和路径末段是否符合回退条件
发布后缺失资源缺失的带点文件不会被回退 HTML 掩盖;检查构建产物和资源 URL
429普通限流与 /monitoring 专用限流是两套配置
仅部分离线数据 API 返回 503监听已开放不代表数据已下载;检查对应门禁和初始化日志
请求在处理器前被拒绝先检查限流、JSON 大小与 Referer 边界,不只查看业务代码
用户结果或报告缓存异常这些路由未声明公共缓存;同时检查外部缓存层是否覆盖源站策略

代理失败的具体状态码、Referer 失败响应、统一业务错误体和未知 API 路径格式没有在已读入口中显式定义;不得当作固定协议对外承诺。

8.2 日志与本地账本不等价于防火墙

限流处理器仅在 req.rateLimit.current === req.rateLimit.limit + 1 时记录一次警告,避免对每个被阻止请求重复刷日志。配置了账本路径才调用 logLimitedIP(ip);该函数更新命中次数,保留地址首次出现时间,并使用带 UTC 偏移的本地时间字符串。

该账本按“异步读取整个文件 → 修改文本 → 写回整个文件”执行,入口没有锁或请求串行队列。并发写入可能覆盖彼此更新,这是由读改写结构推导的风险,不是已执行的压力测试结论。它也没有被读取作为封禁规则,因此不要把文件名中的 blacklist 误解成额外的持久化拦截机制。

依据:backend-server.js。

8.3 性能、扩容与可观测性

  • HTTP 日志默认关闭;开启后 5xx 或错误记为 error、4xx 为 warn、其他为 info。它位于限流之前,因此可以观察 429。
  • 两套普通限流器与监控限流器均未显式配置外部共享 store。入口不足以证明多实例配额全局一致,扩容时需单独核验。
  • 缓存时长针对数据变化速度区分;公开、慢变的数据与个人结果隔离,避免用长期公共缓存覆盖每访客数据。
  • 前后端联合启动没有 readiness 握手;早期前端代理请求可能碰到后端尚未监听,入口没有项目自定义重试机制。
  • 已读入口未显示信号处理或显式停止 watcher、调度器、轮询器的关闭流程,不能保证优雅退出顺序。

依据:backend-server.js、backend-server.js、backend-server.js、package.json。

9. 扩展与验证建议

新增 API 时,应沿用入口中已存在的组合方式,而不是在每个处理器重复基础边界:

  1. 在公共 Referer 中间件之后、静态服务之前注册业务路由。
  2. 明确选择 HTTP 方法、参数守卫以及是否依赖本地数据;不要为无关路由增加全局启动阻塞。
  3. 默认保持不缓存;只有明确属于可共享结果时才声明 TTL,并为部分失败考虑完整性条件。
  4. 涉及特殊二进制载荷时审核解析器顺序,而不只是提高路由的大小限制。
  5. 若添加不同资源类别,注意 setStaticHeaders 的首个匹配分支决定最终静态缓存策略。

这些是基于现有结构的维护建议,并非仓库已经实现的自动检查。可确认的测试入口是 Node 内置测试运行器与 check 脚本;没有读取测试文件,不能宣称代理、缓存、限流或启动门禁已获测试覆盖。

相关链接