NVDA (Non-Visual Desktop Access) 是一款开源的、免费的、强大的、运行于 Windows 操作系统的屏幕阅读软件。
2024 年 4 月 4 日起,有部分 NVDA 中文社区镜像源插件(下称镜像插件)用户反应 NVDA 出现无限重启现象。
笔者对此做了些浅显的研究,且发此文,尝试理清此次故障的来龙去脉。
前情提要
由于一些不可抗力,中国区的用户在使用 NVDA 软件过程中,检测更新、插件商店等功能往往面临糟糕的网络环境。
有鉴于此,NVDA 中文社区无偿开设了 NVDA 中文社区镜像站,旨在解决翻译插件(谷歌接口),软件更新、插件商店等功能的可用性问题。
NVDA 中文社区镜像站主要通过 镜像插件面向中国区用户提供服务,可 点此了解。
故障概述
由于 NVDA 中文社区镜像站所关联的域名资源逾期,致使服务商(NameCheap)将 NVDA 中文社区镜像站所关联的域名解析到停放页面。
部分使用 镜像插件的用户,其NVDA 插件商店从停放页面处获取到错误的 JSON 数据(实为 HTML 网页源码)。
错误的 JSON 数据被 NVDA 插件商店写入 %appdata%\nvda\addonStore\_cachedCompatibleAddons.json
文件(下称“插件商店信息缓存”文件)的 data 字段中。
NVDA 下次重新启动时,插件商店初始化过程中,从“插件商店信息缓存”文件 data 字段读入错误的 JSON 数据,引发 JSONDecodeError 异常。
NVDA 未有妥善处理该异常,致使 NVDA 无限重启。
时间梳理
2024-04-04 16:21:15
NVDA 中文社区镜像站域名逾期。
2024-04-04 16:32:39
NVDA 中文社区镜像站域名解析被 NameCheap 修改(不晚于该时间点)。
镜像插件部分用户开始受此次故障影响(不早于该时间点)。
2024-04-05 09:05:45
NVDA 中文社区镜像站域名续费并恢复解析。
截至本文开始撰写(2024-04-10)
镜像插件少部分用户仍受此次故障影响。
注: 以上时间根据 nvdadr.com 的 whois 信息,及 nvdadr.com 逾期被 NameCheap 解析到要求续费页面申请的 SSL 证书信息中获得。
技术分析
此次故障是由多个环节上的小差错结合在一起,继而酿成的灾难性后果。
下文将尽可能从技术层面分析各环节上的失误,并且加以反思。
管理人员的疏忽
毋庸讳言的是,此次故障 nvdadr.com 域的管理人员的疏忽是第一推动力。
通常的,镜像服务异常只会影响镜像服务的可用性。然而种种巧合作用之下,却导致整个 NVDA 服务的异常,虽使人始料未及,却也给人警醒。
个人精力始终有限,对于重要服务(尤其是用户数量可观的服务),实施多负责人制度或能避免因个人的疏忽大意造成的失误。
离谱的 NameCheap
我发誓,此次故障 NameCheap 的迷惑操作起了不可或缺的、神来之笔的助攻。
观察 镜像插件不难发现,其提供给用于插件更新的 addonStore.network.BASE_URL
是 HTTPS 协议的链接。
前文提到,由于 NVDA 获取到了错误的 JSON 数据进而引发了后续的异常。然而按照设计预期,即使插件商店镜像服务 nvaccess.mirror.nvdadr.com 被解析到其他 IP,也是无法获取到任何数据的。
因为 HTTPS 的特性,要求服务器上必须部署了该域名有效的 SSL 证书才能正确握手建立连接。如果无法建立连接,自然无法获取数据,更遑论错误数据。因此 HTTPS 协议在这里其实充当了一道保险。
然而 NameCheap 的神来一笔却直接拗断了 HTTPS 这跟保险丝。
先是打破域名逾期暂停解析、72 小时后停放其他页面的行业惯例,在域名逾期不超过 11 分钟即“篡改”解析,将 nvaccess.mirror.nvdadr.com 解析到 199.59.243.225。
其次是在域名宽限期内未经授权自行签发 SSL 证书,还可耻的利用了 Let’s Encrypt,在域名逾期 11 分钟后及申请了一张有效期 90 天的 SSL 证书,仅仅为了在 HTTPS 连接展示他们的页面。这种操作堪称离离原上谱,SSL 证书对网站安全至关重要,身为域名注册商竟能如此随意。
最后将来自 nvaccess.mirror.nvdadr.com 的所有请求都定位到同一页面,无论请求什么文件都返回 200 响应码。直接导致了 NVDA 将错误内容(网页源代码)写入了“插件商店信息缓存”文件。
对于 NameCheap 笔者没什么好说的了,只能恳切劝告各位,必坑 NameCheap 保平安!
NVDA 的两处关键性失误
可以坦言的是,此次故障 NVDA 对数据不谨慎的保存和处理是根本性原因。
笔者尝试复现 NVDA 无限重启故障,安装镜像插件后,先尝试在本地将 nvaccess.mirror.nvdadr.com 解析道其他 IP 和 0.0.0.0。
反复获取插件商店数据,虽无法获取到数据,但 NVDA 均未见明显异常,
说明 NVDA 有妥善处理网络连接不正确的异常。
其次,将 addonStore.network.BASE_URL 指向笔者自己的网站。
获取插件数据,在 NVDA 日志中观察到打印出 Error 信息,形如 Unable to get data from API (...), response (404): ...
,并且未将 404 网页源代码写入“插件商店信息缓存”文件。
说明 NVDA 有妥善处理未获取道网络文件的异常。
随后,笔者将 NVDA 插件商店请求的 /addonStore/cacheHash.json 文件和 /addonStore/<地区>/all/<版本>.json(addonStore/zh_CN/all/2024.1.0.json) 文件手动部署到网站目录,并向前者写入 "abcde"
,向后者写入 你好
。
获取插件商店数据,观察到日志打印出异常,且“插件商店信息缓存”文件 data 字段已经写入 你好 ,此时重启 NVDA 即触发无限循环。
此处 NVDA 似乎存在两处失误。
第一,未检验从远程取得的 JSON 数据有效性即将数据写入“插件商店信息缓存”文件 data 字段中。
第二,NVDA 启动时,未妥善处理读入数据可能存在的异常,致使主线程抛出异常后终止。且 NVDA 在主线程异常退出后未妥善结束程序,反而再次尝试启动,导致无限重启。
多线程数据处理问题(推测)
本节纯属个人根据现象做出的推断,未曾结合实际代码。
细心的朋友可能注意到,上文中笔者为 /addonStore/cacheHash.json 文件指定了一个特殊的格式,用引号括起的字符串。
这是因为 NVDA 插件商店必须检测到有效的 cacheHash 字符串(仅使用引号 " 括起的字符串)才会将 /addonStore/<地区>/all/<版本>.json 中获取到的内容写入“插件商店信息缓存”文件中。
问题来了,如果 NameCheap 将所有请求都定位到同一页面(停放页),那么 镜像插件用户就不会获取到有效的 cacheHash 字符串,自然不会写入错误数据到“插件商店信息缓存”文件中,无限重启的故障就不会发生。
然而部分 镜像插件用户无限重启的故障是切实存在的,他们不会像笔者一样闲得无聊刻意给 NVDA 插件商店传递一个精心构造的数据,那么他们从哪里获取到的有效 cacheHash 呢?
于是笔者将 nvaccess.mirror.nvdadr.com 在本地解析到 199.59.243.225,并且启用 镜像插件,停用代理支持插件,模拟出部分受影响的 镜像插件用户的网络环境。
现象观察:
经笔者仔细观察,发现手动删除“插件商店信息缓存”文件后,错误的“插件商店信息缓存”文件总是在 NVDA 重新启动之后再次出现。
此外,无论如何刷新 NVDA 插件商店,都无法使 NVDA 生成该文件。
另外,尝试在 NVDA Python 控制台中手动调用 addonStore.dataManager.initialize(),仍然无法生成错误的“插件商店信息缓存”文件。
打开 NVDA 重启后产生的错误“插件商店信息缓存”文件,观察其 cacheHash 字段,发现始终与 nvaccess.org/addonStore/cacheHash.json 保持一致。
综上得出推论:
当 镜像插件被调用之前,程序事先调用过一次 addonStore.dataManager.initialize() (下称主调用),此时 addonStore.network.BASE_URL 为 https://nvaccess.org/addonStore 。
随后 镜像插件被调用,插件将 addonStore.network.BASE_URL 设置为 https://nvaccess.mirror.nvdadr.com/addonStore/ 并再次调用 addonStore.dataManager.initialize() (下称插件调用)。
由于 nvaccess.org 的访问速度缓慢,当 nvaccess.mirror.nvdadr.com 的结果返回时(插件调用), nvaccess.org 的结果尚未返回(主调用)。
此时由于插件调用获取的 cacheHash 和 data 字段的 JSON 数据异常,插件调用异常退出,部分数据没有妥善清理。
不久,主调用获取到有效的 cacheHash 数据, cacheHash 数据以其体积小巧的“优势”得以于 data 字段的 JSON 数据之前返回。
主调用检测到有效的 cacheHash 数据和插件调用未及清理的错误 data 字段数据,将其写入“插件商店信息缓存”文件中,并妥善清理了数据。
当主调用获取的 data 字段数据姗姗来迟,由于 cacheHash 等数据已被妥善清理,故此没有将正确的内容写入“插件商店信息缓存”文件中。
进一步验证:
将 镜像插件稍作修改,替换 nvaccess.mirror.nvdadr.com 为笔者的网站域名。
删除笔者网站先前部署的 /addonStore/cacheHash.json 文件。
重启 NVDA,观察到错误的“插件商店信息缓存”文件已经生成,且日志中存在获取笔者网站 /addonStore/cacheHash.json 文件的 response (404) 相关打印。
对比“插件商店信息缓存”文件中 cacheHash 字段数据和 nvaccess.org/addonStore/cacheHash.json 文件中数据一致。
总结
正如上文所述,此次故障是多个环节的小差错,种种巧合之下结合在一起才酿成的灾难性后果。单看每个环节的小差错,似乎都不足以导致此次事故。
这也提醒我们,风起于青萍之末,浪成于微澜之间。
希望这篇抛砖引玉的复盘文章,能给运维人员、开发者门以参考。
若有疏漏不当之处,诚请批评雅正。