Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

cloudflare.js 逆向分析与 Python 复现

0x00 背景

目标很明确:分析 cloudflare.jswindow.onload 时发出的请求,确认以下几点。

  1. 请求是从哪里发起的。
  2. keytime 是如何构造出来的。
  3. 返回值是否存在二次解密。
  4. 如何在本地用 Python 完整复现并请求到最终结果。

用户给了两条真实请求样例:

1
2
https://api.uouin.com/index.php/index/Cloudflare?key=87bd023769e83ab9fa6b1b8fb6b1c497&time=1774183617366
https://api.uouin.com/index.php/index/Cloudflare?key=6e1ce01e5ceb3d5446bb785bc40dcc4d&time=1774184670465

这两条样例很关键,因为它们可以用来验证我们最后反推出的公式是否正确。

0x01 入口定位

最先做的不是硬啃整份混淆代码,而是定位触发点。

cloudflare.js 中检索:

  • onload
  • Cloudflare?key=
  • ajax

很快可以定位到两处关键位置:

  • window['onload'] = function() { ... }cloudflare.js:654
  • '/index.php/index/Cloudflare?key='cloudflare.js:751

这说明页面初始化后,请求并不是在复杂事件链里触发的,而是页面加载完成即执行。

核心请求片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
window['onload'] = function() {
var _0x417ccc = new Date();
var _0x1e39be = _0x417ccc[_0x1924('‫b9', 'JyCx')]();
var _0x44b4ab = _0x4a2333[_0x1924('‫ba', 'MMd$')](
_0x4a2333[_0x1924('‮bb', 'DbNK')](hex_md5, _0x4a2333[_0x1924('‫bc', 'muGa')]),
_0x4a2333[_0x1924('‮bd', 'rMA[')]
) + _0x1e39be;
hash = _0x4a2333['MxghC'](hex_md5, _0x44b4ab);
$['ajax']({
'type': _0x4a2333[_0x1924('‮be', 'aL*5')],
'dataType': _0x4a2333[_0x1924('‮bf', 'muGa')],
'url': _0x4a2333['blbkw'](
_0x4a2333[_0x1924('‮c0', '9vym')](
_0x4a2333['blbkw'](_0x4a2333[_0x1924('‮c1', '1P[k')], hash),
_0x4a2333['pNETf']
),
_0x1e39be
),
'success': function(_0x4f6d34) { ... }
});
}

虽然变量名全被混淆了,但结构很直观:

  • _0x1e39be 是时间戳
  • _0x44b4abhash 的中间输入
  • hash = hex_md5(_0x44b4ab)
  • url = '/index.php/index/Cloudflare?key=' + hash + '&time=' + time

到这里,实际上只差把几个被 _0x1924(...) 包起来的字符串还原出来。

0x02 混淆特征识别

这个脚本不是普通压缩,而是典型的 jsjiami.com.v6 风格混淆。顶部能看到几个非常明显的特征:

  1. 头部字符串 jsjiami.com.v6
  2. 巨大的字符串数组 _0x17c3
  3. 统一字符串访问函数 _0x1924
  4. 顶部自执行函数对 _0x17c3 做重排
  5. 一堆反调试、死循环、Function(...) 构造器干扰逻辑

这里最容易踩坑的点有两个。

第一,不能直接把 _0x17c3[index] 拿出来按 base64 解码,因为它不是单纯 base64,后面还套了 RC4。

第二,不能忽略顶部数组洗牌逻辑。
如果不先执行那段 shuffle,再去解 _0x1924('b6', ...) 之类的索引,得到的会是乱码,进而把算法误判成错误的 md5 公式。

这也是一开始最容易走偏的地方。

0x02A jsjiami.com.v6 风格混淆拆解

这一类混淆有很强的“模板感”,不是每次都完全一样,但核心套路通常比较固定。
如果之前见过几次,再看到这个文件,基本能在几十秒内判断出它属于哪一类。

1. 最醒目的识别特征

本样本顶部最典型的标记就是:

1
var _0xodY = 'jsjiami.com.v6'

这不是业务字符串,而是混淆器留下的特征标识。
看到这个标记,基本可以先假设后面会出现以下几层结构:

  1. 大型字符串数组
  2. 数组重排或索引扰动
  3. 字符串解码函数
  4. 反调试或环境探测
  5. 垃圾控制流

也就是说,拿到这种脚本后,不应该从头按业务逻辑顺序阅读,而应该先把它拆成“壳层”和“业务层”。

2. 这种混淆通常想解决什么问题

jsjiami 这种壳的目标一般不是提供真正意义上的密码学安全,而是提高静态阅读成本。
它主要防的不是“执行”,而是“看懂”。

常见目的包括:

  • 隐藏关键字符串,比如接口路径、参数名、请求方法
  • 破坏源码可读性,让字符串索引失去直觉含义
  • 插入反调试逻辑,拖慢直接在浏览器里下断点分析的速度
  • 制造大量无关代码,增加阅读噪音

换句话说,它更像“逆向阻力层”,而不是“业务加密层”。

3. 典型分层结构

这类脚本通常可以分为五层。

第一层:标识与数组定义

先声明一个类似 _0x17c3 = [...] 的大数组,数组里装的是编码后的字符串。

这些字符串一般不会直接明文出现,目的是避免你一眼搜索到:

  • get
  • json
  • /index.php/index/Cloudflare?key=
  • getTime

第二层:数组重排

定义完数组后,不会马上拿来用,而是先做一次 shift/push/pop 操作,把数组顺序打乱。

这一层非常关键,因为它直接破坏了“数组定义顺序 = 最终索引顺序”这个直觉。

很多人写个脚本直接:

1
console.log(_0x17c3[0xb6])

然后发现结果完全不对,就会误以为后面的 RC4 或 base64 解码写错了。
实际上问题往往不在解码,而在索引已经被改写。

第三层:字符串解码器

也就是本样本里的 _0x1924(idx, key)
它通常会把多个步骤包在一起:

  1. 根据索引取数组元素
  2. 做 base64 解码
  3. 做 URI 还原
  4. 做 RC4 解密
  5. 做缓存,避免同一字符串重复解密

所以 _0x1924 本质上不是业务函数,而是“字符串仓库的取值器”。

第四层:反调试和环境探测

本样本里可以看到大量类似下面的结构:

  • while (!![])
  • Function(...)
  • debuggerProtection
  • 正则检测函数源码
  • typeof window / process / require / global 检查

这些逻辑的共同点是:

  • 代码量大
  • 可读性差
  • 和业务请求关系很弱

它们的存在主要是为了干扰分析节奏。

第五层:真实业务逻辑

真正和业务有关的,往往只占很小一段。

本样本真正重要的其实只有:

  1. window.onload
  2. new Date().getTime()
  3. 两个字符串常量
  4. 两次 md5
  5. 一次 $.ajax

这也是处理这类混淆的一个基本原则:

1
不要试图一次性“看懂整份脚本”,而要先把真正发请求的那几行剥出来。

4. 为什么它喜欢用字符串数组 + RC4

这类混淆里,“字符串数组 + RC4 + 索引访问”几乎是标准配置,原因很实际。

原因一:阻止全文搜索

如果接口路径直接明文出现,搜索 /Cloudflare?key= 一秒钟就能找到。
但字符串被拆到数组里再经过解码,就没法直接全文检索关键字。

原因二:阻止简单替换

如果只是把变量名改掉,业务字符串还在,逆向成本并不高。
把字符串集中放到数组中,再通过 _0x1924(索引, key) 访问,会让代码阅读变成:

1
_0x4a2333[_0x1924('‫bc', 'muGa')]

而不是:

1
config.apiSalt

对阅读体验的打击非常明显。

原因三:便于模板化生成

混淆器最喜欢能批量生成、自动套壳的模式。
字符串数组、RC4、缓存、反调试这些都非常适合模板化拼装,所以会在不同站点里反复出现。

5. 本样本里各层的对应关系

如果把这份 cloudflare.js 映射到上面的五层结构,可以这样理解:

壳层 1:标识

1
var _0xodY = 'jsjiami.com.v6'

壳层 2:字符串池

1
_0x17c3 = [...]

壳层 3:字符串重排

顶部自执行函数里对 _0x17c3shift/push/pop 处理。

壳层 4:解码器

1
function _0x1924(...) { ... }

内部完成:

  • base64
  • decodeURIComponent
  • RC4
  • 缓存

壳层 5:干扰逻辑

例如:

  • 正则检测函数源码
  • Function(arguments[0] + "...")
  • while (!![])
  • 类似 debuggerProtection 的递归或自调用保护

业务层

真正业务逻辑集中在:

  • window.onload
  • 构造 hash
  • 调用 $.ajax
  • 遍历 res.data

6. 处理这种混淆时推荐的顺序

遇到 jsjiami.com.v6 样式脚本时,建议按下面顺序处理。

第一步:先定位请求入口

不要先解所有字符串。
先找:

  • ajax
  • fetch
  • XMLHttpRequest
  • onload
  • 接口路径特征

只要先把请求入口抓出来,后面只需要解少数几个字符串即可。

第二步:确认数组是否被重排

如果顶部有自执行函数在改数组顺序,这一步必须先模拟。

否则后面所有索引都可能错位。

第三步:复刻 _0x1924

只要把它拆成下面几步就行:

  1. 取索引
  2. base64 解码
  3. URI 还原
  4. RC4 解密

一般不需要关心它的缓存逻辑,缓存只是性能优化。

第四步:只解关键索引

优先解这些:

  • 请求方法
  • dataType
  • URL 路径
  • 时间戳方法名
  • hash 输入常量

没必要从 0x00 一路把所有数组元素都解出来,那样成本高、收益低。

第五步:样例回代

只要用户给了真实参数,就必须拿真实样例做回代验证。
这是确认“已经逆到业务层”的最直接方式。

7. 如何区分“壳逻辑”和“业务逻辑”

这是逆这类脚本时很实用的一条经验。

通常属于壳逻辑的代码

  • 名称毫无语义的大量小函数
  • 大量 while (!![])
  • Function(...)
  • 检查 toString() 内容的正则
  • 明显不产出业务数据的递归
  • 到处判断 typeof window/process/global

通常属于业务逻辑的代码

  • new Date().getTime()
  • hex_md5(...)
  • $.ajax({...})
  • success(res) { ... }
  • 明确访问 res.coderes.msgres.data

一个简单判断标准是:

1
如果删掉这段代码,请求还能不能发出去、结果还能不能渲染出来?

如果答案是“能”,那大概率就是壳逻辑。
如果答案是“不能”,那才值得重点跟。

8. 为什么说这类混淆“烦”但不一定“深”

jsjiami.com.v6 经常给人的第一印象是代码很厚、很乱、断点难下。
但从逆向角度看,它的难点往往是“噪音太大”,不是业务太深。

像本样本,最终业务算法其实非常短:

1
2
3
time = new Date().getTime()
key = md5(md5("DdlTxtN0sUOu") + "70cloudflareapikey" + time)
GET /index.php/index/Cloudflare?key=...&time=...

真正费时间的是:

  1. 识别壳
  2. 处理数组重排
  3. 正确解出关键字符串

一旦这几步过了,后面的业务逻辑反而非常普通。

0x03 _0x1924 的真实作用

_0x1924(idx, key) 的逻辑本质上是:

  1. idx 从形如 '\u202bb9' 这样的字符串转成十六进制索引。
  2. _0x17c3 中取出对应项。
  3. base64 解码。
  4. decodeURIComponent 还原字符。
  5. 最后走一遍 RC4(key) 解密。

也就是:

1
_0x1924(idx, key) = RC4(decodeURIComponent(base64_decode(_0x17c3[idx])), key)

脚本中 RC4 的实现也很标准:

  • 先初始化 s[0..255]
  • 再按 key 执行 KSA
  • 最后按 PRGA 对数据逐字节异或

这一层没有业务逻辑,只是混淆壳负责还原字符串。

0x04 顶部数组重排为什么必须处理

脚本顶部不是简单定义 _0x17c3 = [...] 就结束了,而是马上进入一段自执行逻辑,对数组执行 shift/push/pop 组合操作。

它的目的不是“加密数据”,而是打乱下标,让静态分析时无法直接按数组原始顺序解出字符串。

如果忽略这一步,会出现这种情况:

1
2
3
4
b8 -> 乱码
b9 -> 乱码
ba -> 乱码
...

看起来像 RC4 key 错了,实际上是数组索引错位了。

我在分析时先完整模拟了这一段重排逻辑,再去解关键索引,结果立刻变得稳定,能还原出可读字符串。

这一点可以视为整个脚本逆向的关键分界线:

  • 不处理 shuffle:样本全错
  • 处理 shuffle 后:请求常量全部可读,样例 key 完全对上

0x05 关键字符串还原结果

在正确执行数组重排后,和请求相关的关键字符串可以还原为:

索引 还原结果 作用
b6 70cloudflareapikey 第二段拼接常量
b7 get 请求方法
b8 &time= URL 参数拼接
b9 getTime 取毫秒时间戳
c1 oKjFr 指向对象属性名

而对象本身中还有直接明文值:

1
'oKjFr': '/index.php/index/Cloudflare?key='

因此 URL 模板已经明确:

1
/index.php/index/Cloudflare?key=<hash>&time=<time_ms>

继续还原 756 行参与 hash 计算的对象属性值,可以得到真正参与计算的两个常量:

  • YNqYb -> "DdlTxtN0sUOu"
  • shXyL -> "70cloudflareapikey"

所以这句:

1
2
var _0x44b4ab = hex_md5(常量1) + 常量2 + time_ms;
hash = hex_md5(_0x44b4ab);

最终变成:

1
2
tmp = md5("DdlTxtN0sUOu")
key = md5(tmp + "70cloudflareapikey" + str(time_ms))

0x06 为什么之前容易得出错误公式

如果只做关键词搜索,很容易在混淆脚本里搜到一些误导性内容,或者根据域名、页面名字脑补出像下面这样的假公式:

1
md5(md5("uouin.com") + "uouin" + time)

这个公式看起来“很合理”,但它不是脚本真实执行路径中的结果。

原因是:

  1. 混淆代码里有大量无关字符串。
  2. 反调试壳会注入许多无业务意义的干扰逻辑。
  3. 如果没有处理数组重排,解出来的索引对应关系就是错的。

真正可靠的办法不是“看起来像”,而是必须满足两个条件:

  1. 能和 window.onload 代码路径严格对应。
  2. 能拿用户给的真实样例完全回代成功。

本次最终公式之所以可信,就是因为它同时满足这两个条件。

0x07 用样例验证最终公式

用户提供的两条样例请求如下:

1
2
https://api.uouin.com/index.php/index/Cloudflare?key=87bd023769e83ab9fa6b1b8fb6b1c497&time=1774183617366
https://api.uouin.com/index.php/index/Cloudflare?key=6e1ce01e5ceb3d5446bb785bc40dcc4d&time=1774184670465

将公式写成:

1
key = md5(md5("DdlTxtN0sUOu") + "70cloudflareapikey" + str(time_ms))

代入校验:

1
2
time=1774183617366 -> 87bd023769e83ab9fa6b1b8fb6b1c497
time=1774184670465 -> 6e1ce01e5ceb3d5446bb785bc40dcc4d

与样例完全一致。

这一步非常重要。
到这里其实已经不是“猜测算法”,而是“算法已被样例证明”。

0x08 返回值是否存在二次加密

继续看 success 回调,从 cloudflare.js:762 往后可以看到逻辑非常直接:

  • 读取 res.code
  • 读取 res.msg
  • 判断 res.data.ctcc.code == 200
  • 遍历 res.data.ctcc.info
  • 判断 res.data.cucc.code == 200
  • 遍历 res.data.cucc.info
  • 判断 res.data.cmcc.code == 200
  • 遍历 res.data.cmcc.info
  • ip/loss/ping/speed/bandwidth 拼进表格 HTML

也就是说,返回值并没有额外加密层。

页面做的事情只是:

  1. 请求接口
  2. 拿到 JSON
  3. 按运营商分类渲染表格

因此如果只关心最终数据,完全不需要模拟 DOM,也不需要补任何浏览器解密流程,直接 Python requests.get() 即可。

0x09 最终算法结论

页面加载时发出的请求为:

1
GET /index.php/index/Cloudflare?key=<key>&time=<time_ms>

其中:

1
2
3
time_ms = 当前毫秒时间戳
tmp = md5("DdlTxtN0sUOu")
key = md5(tmp + "70cloudflareapikey" + str(time_ms))

完整 URL:

1
https://api.uouin.com/index.php/index/Cloudflare?key=<key>&time=<time_ms>

可以进一步写成一行:

1
key = md5(md5("DdlTxtN0sUOu") + "70cloudflareapikey" + str(int(time.time() * 1000)))

0x0A Python 复现

当前目录已经实现了复现脚本:

  • cloudflare_api.py

核心实现如下:

1
2
3
def generate_key(ts_ms: int) -> str:
first = hashlib.md5("DdlTxtN0sUOu".encode("utf-8")).hexdigest()
return hashlib.md5(f"{first}70cloudflareapikey{ts_ms}".encode("utf-8")).hexdigest()

URL 构造:

1
2
3
def build_url(ts_ms: int) -> str:
key = generate_key(ts_ms)
return f"https://api.uouin.com/index.php/index/Cloudflare?key={key}&time={ts_ms}"

请求部分:

1
2
response = requests.get(url, headers=headers, timeout=30)
data = response.json()

这已经足够覆盖页面的真实行为。

0x0B 样例校验结果

脚本里已经固化了两组校验样例:

1
2
1774183617366 -> 87bd023769e83ab9fa6b1b8fb6b1c497
1774184670465 -> 6e1ce01e5ceb3d5446bb785bc40dcc4d

运行 python cloudflare_api.py --no-request 时会先打印校验结果,确认公式没有偏移。

这一步的意义是:

  • 防止后续重构脚本时把算法改坏
  • 防止把错误的“近似公式”当成真实逻辑

0x0C 实际请求结果

使用 Python 实际请求接口后,返回成功:

1
2
code: 200
msg: 获取成功

当时获取到的部分结果摘要如下。

ctcc

  • 108.162.198.69
  • 198.41.222.149
  • 162.159.48.55

cucc

  • 172.67.72.149
  • 104.26.6.211
  • 104.20.16.5

cmcc

  • 104.19.44.52
  • 104.19.54.231
  • 104.19.37.244

这进一步说明:

  1. 参数算法正确。
  2. 直接请求接口就能拿到最终 JSON。
  3. 页面端没有隐藏的二次解密步骤。

0x0D 附录:关键索引与明文映射

这一节把本样本里和请求最相关的索引、属性名、实际明文统一列一下。
注意,下面这些结果都是在“先完成 _0x17c3 数组重排”之后得到的。

1. 直接解出的关键索引

索引 解出结果 含义
b6 70cloudflareapikey 哈希拼接盐值
b7 get $.ajax 请求方法
b8 &time= URL 参数拼接片段
b9 getTime Date 取毫秒时间戳的方法
ba blbkw 对象中的“字符串拼接函数”属性名
bb MxghC 对象中的“函数调用包装器”属性名
bc YNqYb 对象中的第一段哈希常量属性名
bd shXyL 对象中的第二段哈希常量属性名
be OqcMs 对象中的请求方法属性名
bf sJeKy 对象中的 dataType 属性名
c0 blbkw 仍然是字符串拼接函数属性名
c1 oKjFr 对象中的请求路径属性名

2. 属性名继续映射到对象真实值

仅仅把索引解成属性名还不够,还要再看对象字面量里的实际值。

属性名 实际值 用途
YNqYb DdlTxtN0sUOu 第一段哈希输入常量
shXyL 70cloudflareapikey 第二段哈希输入常量
OqcMs get 请求方法
sJeKy json $.ajaxdataType
oKjFr /index.php/index/Cloudflare?key= 请求路径
pNETf &time= URL 参数后半段

3. 函数属性的语义

有几项虽然不是字符串常量,但对还原表达式很重要。

属性名 实际语义 可直接理解为
blbkw(a, b) 返回 a + b 字符串拼接
MxghC(fn, arg) 返回 fn(arg) 单参数函数调用包装

所以这段:

1
2
3
4
var _0x44b4ab = _0x4a2333[_0x1924('‫ba', 'MMd$')](
_0x4a2333[_0x1924('‮bb', 'DbNK')](hex_md5, _0x4a2333[_0x1924('‫bc', 'muGa')]),
_0x4a2333[_0x1924('‮bd', 'rMA[')]
) + _0x1e39be;

翻译后就是:

1
var _0x44b4ab = md5("DdlTxtN0sUOu") + "70cloudflareapikey" + time_ms;

再结合下一句:

1
hash = _0x4a2333['MxghC'](hex_md5, _0x44b4ab);

即可得到:

1
hash = md5(md5("DdlTxtN0sUOu") + "70cloudflareapikey" + time_ms);

4. 最小可读版伪代码

window.onload 中真正有用的部分重写成最小伪代码,大致就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
window.onload = function () {
var time_ms = new Date().getTime();
var first = md5("DdlTxtN0sUOu");
var key = md5(first + "70cloudflareapikey" + time_ms);

$.ajax({
type: "get",
dataType: "json",
url: "/index.php/index/Cloudflare?key=" + key + "&time=" + time_ms,
success: function (res) {
// 直接读取 res.code / res.msg / res.data 并渲染
}
});
};

5. 为什么这个附录有用

很多时候真正有价值的不是“把整份混淆都解完”,而是把最关键的映射沉淀下来。
以后再碰到类似站点,或者需要复核这份分析时,直接看这个表就够了,不需要再重新跑一遍整个字符串还原流程。

0x0E 推荐复现步骤

如果以后再次碰到类似脚本,建议按下面顺序做,而不是上来就盲猜 hash 公式。

第一步:先找真正入口

优先搜:

  • onload
  • ajax
  • fetch
  • XMLHttpRequest
  • 目标接口路径关键词

先缩小到真实执行链路。

第二步:不要急着解全文件

优先只解和请求相关的几个索引位:

  • 请求方法
  • URL 基础路径
  • 时间戳方法名
  • hash 输入常量

这样成本最低。

第三步:先确认数组是否被重排

只要看到顶部有 shift/push/pop 自执行逻辑,就先处理这一步。
很多“解出来像乱码”的问题,根本不是 RC4 算法写错,而是数组顺序已经变了。

第四步:一定用真实样例回代

如果用户给了真实请求参数,必须回代验证。
只有能完全命中样例,才能说明公式是真的,不是“差不多”。

0x0F 复现命令

只验证算法,不发请求:

1
python cloudflare_api.py --no-request

生成参数并发起真实请求:

1
python cloudflare_api.py

指定固定时间戳请求:

1
python cloudflare_api.py --time 1774183617366

0x10 最终总结

这个脚本真正有价值的部分并不复杂,复杂的是外层混淆壳。

真正的业务逻辑只有四步:

  1. new Date().getTime() 生成毫秒时间戳。
  2. 计算 md5("DdlTxtN0sUOu")
  3. 再计算 md5(md5("DdlTxtN0sUOu") + "70cloudflareapikey" + time_ms) 作为 key
  4. 请求 /index.php/index/Cloudflare?key=<key>&time=<time_ms>,直接拿 JSON 结果。

一句话概括:

1
难点不在加密,而在先穿透 jsjiami v6 的字符串混淆和数组重排。

附:Python 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import hashlib
import time
import requests

def generate_key():
ts = int(time.time() * 1000)
secret = "70cloudflareapikey"
step1 = hashlib.md5(hashlib.md5(secret.encode()).hexdigest().encode() + secret.encode()).hexdigest()
step2 = step1 + str(ts)
key = hashlib.md5(step2.encode()).hexdigest()
return key, ts

def fetch_cloudflare_data():
key, ts = generate_key()
url = f"https://api.uouin.com/index.php/index/Cloudflare?key={key}&time={ts}"
print(f"Generated URL: {url}")

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Referer": "https://stock.hostmonit.com/Cloudflare",
"Origin": "https://stock.hostmonit.com"
}

try:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return None

if __name__ == "__main__":
data = fetch_cloudflare_data()
if data:
print(f"\nResponse code: {data.get('code')}")
print(f"Message: {data.get('msg')}")

if data.get('code') == -1:
print("\nNote: The API returned '密钥超时或设备时间不准,请刷新页面或登录使用!'")
print("This might be due to time synchronization or API key expiration.")

if 'data' in data:
for isp_type, isp_data in data['data'].items():
print(f"\n{isp_type.upper()} Status Code: {isp_data.get('code')}")
if isp_data.get('code') == 200 and 'info' in isp_data:
print(f" Top 3 IPs for {isp_type}:")
for i, ip_info in enumerate(isp_data['info'][:3], 1):
print(f" {i}. IP: {ip_info['ip']}, Loss: {ip_info['loss']}, Ping: {ip_info['ping']}, Speed: {ip_info['speed']}")