文章

听说最近破解vpn客户端很火?

听说最近破解vpn客户端很火?

因为发现最近 ttnk 在玩这个,咱也玩一下。因为不擅长二进制,所以玩一玩爪洼岛。

我们此次目标是 fatvpn.app ,我们从它的官网上获取了 apk,在手机上简单使用 mt 管理器查阅后发现 mt 管理器不能满足咱们的要求,故而 mt管理器 解 classes.dex 为 classes.jar 。后用 JavaOctetEditor 反编译了其 classes.jar

image

根据 resource.arsc,查询其 “小胖攀登中…” (vpn_connecting)

image
image

0x7f0f00cd 对应的是 2131689677

常量搜索

image

image

等一下?这家伙没有混淆?一部分有,一部分没有。

从没混淆的地方开始看,com.fat.vpn.data.remote.response.VpnServerNodesResponse

关键逻辑代码

1
2
3
4
5
//z7.j # public final Object p(Object obj)

            VpnNodeEncryptedResponse vpnNodeEncryptedResponse = (VpnNodeEncryptedResponse) obj;
            return new com.google.gson.j().b(e0.b(e0.G(vpnNodeEncryptedResponse.getKey()), vpnNodeEncryptedResponse.getData()), VpnServerNodesResponse.class);

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
// j5.e0
  public static String b(String var0, String var1) {
      e.m(var1, "sSrc");

      try {
         MessageDigest var4 = MessageDigest.getInstance("MD5");
         Charset var2 = a.a;
         byte[] var7 = var0.getBytes(var2);
         //e.l(var7, "this as java.lang.String).getBytes(charset)");
         SecretKeySpec var3 = new SecretKeySpec(var4.digest(var7), "AES");
         IvParameterSpec var11 = new IvParameterSpec(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
         Cipher var8 = Cipher.getInstance("AES/CBC/PKCS5Padding");
         var8.init(2, var3, var11);
         byte[] var9 = Base64.decode(var1, 0);
         byte[] var10 = var8.doFinal(var9, 0, var9.length);
         //e.l(var10, "doFinal(...)");
         var0 = new String(var10, var2);
      } catch (Exception var5) {
         var5.printStackTrace();
         var0 = var5.toString();
      }

      return var0;
   }

    public static String G(String var0) {
      e.m(var0, "data");
      byte[] var1 = Base64.decode(
         "MIIEogIBAAKCAQB/xKmjpGbc6MdYYp0v32JLpIaOvyTrhupgWWcQfRFyuk51p3fo\nf6Eh+M3qp00yQzHtP/MkRP6Ldc5DOP6D9b2s522s7vpzsG3LtBenZZ8xN4usMBuO\nTQzJTvLfCjfLl0gXBDJneKjHPKxLxaPmsFwTS1Mi3cZEMNoA8ns9hmdDwcsCLBAH\nzTjpNcfQN+EQw7mRbKK4n14lLzSfYjjScuE/Oj8WOpy5Wl0/UAoHRLbujPNU26hJ\nlpQa2S6ipvFIBZHjhaU4AesST55XBOLDmPNCaKQV9Nf9RJ2GABazioPAOo+Q7iEf\nEXysNcuhdP7q8gXJ0oNHdyJl6PAZZddiZyB7AgMBAAECggEASJMC8Orvasfmg7Pw\nKUMv6FuZ+vdkF0zZUMU3n8wK3yooavgnSi9E7bEP9hv143j7oRHUIGP4WmseMFzt\nZTNu/Amw6KwOIyyyESVI0lMM673rXnEtFdV6T9bCaiK5srFJx5kgsFl/NTyneZrY\nEK9YfbUpkgJ7HjzJeAREMJxph7h7FsSd2M+MB7m/J+6TGZ6fIsZFcy0vIQLjLSAI\nOuO5T1HEB+m7AFHyvXdAfR+BWKSTIs0pkC2wUgbE6CsMlzx3XvM1DnmUiDF478t0\nUFKEnpQKsnmRSweH7htbmhigiP9kHcidJ8+PNqojy+DGsCjuIoSiOqgGEs1+woYS\nw34pIQKBgQDPmwiPNYxM2OYP7onoBhsBKDmdxGSUnEFmUgmtPpEy5PssJDz/SEoV\nponyW/dPEycn8Bjc+6fnjUi0RuqE2e3JnpSWrhhLBEFhrd16bHVwaHA40IWM8UYs\ngUjmTPdt2DO9+M8ZW+UADUAAlARm/NSZo+Z4PzTBIjW4e4xhxm/2AwKBgQCdjUs4\nVC0qdMHT5ntE5IWIagrXxshacaE8rTAgjYOMaib0Skq593yqksb1kiIF3CeWZACd\nd6qk6Gah5Oc755K8wuixxXZY8vzwzLf5K3NqFkbAhxR251kZoQvmoP/OJDbBxO0Y\n86auuX7m498vIh7raoVWjzXKSHK2/ZyTfEw+KQKBgFH5r7mMtWeqxb1IvZ+muYcN\ncSLA585enNxgTH3iFMd570wQyx0qWEaQSiwu8EqDD5UPk2G+5R/jg+/biMMIooJY\nYefVurX0ajS9yJSMuxq1wopMnE94/fKY4kY94f23v0amNnCW/qe0k68mw04/S1uX\ngmu82YHhlkDQWDBLgO4tAoGAS6xv8rBLuVa3OoY7sw1oLetxJc7usLJfVXuB4EDY\nbHsYFsIQPl5m3K7/LThxawshYJTLztaJege+NAh0IEvMKSodBjXn8DVV1Hsf6mg6\nWTw144d+BtZ771lxE+dEtsiiHFPv5coxxz6Fe3T73/GtlDlnrfm/Rleh8c7Cg/xx\nynECgYEAkIj8otP2kFIqR+5tTl2u3wINlVyPONyWKT0GNWN9am80sUMpYYvI5TUm\nJ47+2709GgiYQWZxSsNCqVPuKM57u1czOo7iRqD2UavL3GiN4zmBbGQ+4TqwPJb1\nk2Y8dpIMwIGRKT9GWLexXYqBLK1JJV3TrJ4CjEA9A/Txy/HByZg=",
         0
      );
      e.l(var1, "decode(...)");
      PrivateKey var2 = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(var1));
      Cipher var4 = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
      var4.init(2, var2);
      byte[] var3 = var4.doFinal(Base64.decode(var0, 0));
      e.l(var3, "doFinal(...)");
      return new String(var3, a.a); //Charset.forName("UTF-8")
   }

image

image

直接请求,521,太乱了不想分析,丢给 Gemini :

这段代码是经过 Jadx 反编译后还原的 Kotlin 协程(Coroutine)代码。虽然它是用 Java 语法表达的,但里面的 label 状态机、-2147483648Integer.MIN_VALUE)以及 ih.a.s(挂起标志 COROUTINE_SUSPENDED)都是非常典型的 Kotlin 挂起函数编译后的底层实现

它的核心功能是:一个带有动态签名(Authorization)验证的网络请求,用于获取 VPN 节点数据。

下面我为你把这段晦涩的反编译代码拆解开,看看它每一步到底做了什么:


1. 状态机与协程初始化 (续借与挂起)

代码一开头和中间的 if (var27 != 0) 分支,是 Kotlin 协程处理异步挂起的经典状态机(State Machine)逻辑。

  • var26 其实是协程的 Continuation(续借对象),里面记录了当前执行到第几步(状态 K)。
  • 状态 0: 第一次进入函数,执行网络请求的初始化、签名计算,并发送第一个异步请求。之后函数会直接返回 ih.a.s(即 COROUTINE_SUSPENDED,挂起)。
  • 状态 1: 当第一个网络请求有结果后,协程框架恢复(Resume)调用此函数,直接跳到状态 1 继续执行。
  • 状态 2: 处理反序列化,拿到最终的 VpnNodeEncryptedResponse 数据。

2. 核心业务逻辑拆解(网络请求与动态签名)

当状态为 0 时(首次触发),它开始构建网络请求。这里使用了类似 Ktor 或者是 Koin 依赖注入的网络客户端。

第一步:设置 URL 与基础配置

1
2
3
4
lf.c var6 = new lf.c(); // 这通常是一个 HttpClient 或者是 HttpRequestBuilder
j0.b(var6.a, "https://api.fatvpn.pro/api/v2/vpnServerNodes?version=84");
pa.a.P(var6, new d("text", "plain", u.s)); // 设置 Content-Type 为 text/plain

它配置了你要访问的那个 URL,并指定了数据格式。

第二步:动态生成 Authorization 签名(重点:防爬核心)

接下来这一大段 try-catch 是全自动生成安全请求头的核心算法。它通过 Android 系统获取了应用自身的签名(Certificate SHA1 指纹)

  1. 获取 Context: 通过 Koin 依赖注入框架拿到当前 App 的 Context。 ```java var7 = mj.a.b; // Koin 注入检查 var10 = (Context)var7.e(…); var10 = var10.getApplicationContext();
1
2
3
4
5
6
7

2. **提取 SHA1 指纹:** 读取当前 APK 的签名信息(底层调用了类似 `getPackageManager().getPackageInfo(..., GET_SIGNATURES)` 的操作,这里被封装在 `i.f(var10)` 中),并将其转化为字节数组,然后计算它的 **SHA1** 哈希值。
```java
byte[] var18 = i.f(var10).toByteArray();
byte[] var19 = MessageDigest.getInstance("SHA1").digest(var18);

  1. 转大写字符串: 将 SHA1 字节数组转换为形如 AA:BB:CC... 的十六进制字符串,并强制转换为大写。 ```java String var20 = i.a(var19); var16 = var20.toUpperCase(Locale.getDefault());
1
2
3
4
5
6
7
8
9
10
11

4. **Base64 与切片处理:** * 将这个大写的 SHA1 字符串再转成字节,进行 **Base64 编码** 得到 `var21`。
* 接着创建了一个范围对象 `xh.e(0, 15, 1)`(代表从 0 到 15)。
* 截取 Base64 字符串的前 16 位字符(`substring(0, 15 + 1)`)作为最终的密钥。


```java
String var21 = Base64.encodeToString((byte[])var28, 0);
String var22 = var21.substring(0, 16); // 截取前16位

  1. 强加 Request Headers: 将截取出来的 16 位字符串作为 Authorization 请求头传过去。 ```java oc1.u(var6, “Authorization”, var22); // 写入 Header oc1.u(var6, “Cache-Control”, “no-cache”);
1
2
3
4
5
6
7
8
9
10
11
12
13
14


> 💡 **为什么请求这个 URL 需要这样?** > 难怪你直接请求这个 URL 拿不到数据,因为服务器在后台会校验你的 `Authorization` 请求头。而这个请求头是基于**正版 App 证书的 SHA1 指纹**经过 `SHA1 -> UpperCase -> Base64 -> Substring(0,16)` 动态计算出来的。如果你用浏览器或普通 cURL 去请求,由于没有这个特定 Header,服务器会直接拒绝(返回 401 或 403)。

---

### 第三步:发起异步网络请求

```java
k var23 = new k(var6, this.a);
var26.K = 1; // 状态机切换到 1
var28 = var23.b(var26); // 发起挂起请求(网络I/O)

这里使用了协程挂起。当网络请求还在路上时,函数直接 return 退出,不阻塞主线程。


第四步:解析加密的返回数据

当请求成功回来,状态机进入 状态 1 或状态 2

1
2
3
4
5
6
b var24 = ((mf.c)var10).b();
var28 = x.b(VpnNodeEncryptedResponse.class); // 获取该类的反射信息
// ...
var26.K = 2; // 状态机切换到 2
var10 = (Context)var24.a((ag.a)var28, var26); // 反序列化

服务器返回的二进制流在这里被反序列化成了 VpnNodeEncryptedResponse 对象。

注意这个类的名字:VpnNodeEncryptedResponse(VPN节点加密响应)。这意味着即使你突破了前面的 Authorization 认证拿到了二进制流,里面的内容依然是经过加密的(可能包含密文和 IV 偏移量),需要 App 内部的解密算法(如 AES)解密后,才能看到真正的节点 IP、端口和配置。


总结:如何模拟这个请求?

如果你想自己写程序成功抓到这个接口的数据,你需要:

  1. 拿到这个 APK 官方正版的 SHA1 证书指纹 字符串(例如:7A:4B:92...)。
  2. 将该字符串转为大写,做 Base64 编码,截取前 16 位。
  3. 伪装 HTTP 请求,在 Headers 中加入:
    • Authorization: <你计算出的16位字符串>
    • Cache-Control: no-cache
    • Content-Type: text/plain
  4. 发送请求,拿到二进制流后,再去代码里全局搜索 VpnNodeEncryptedResponse 这个类,找出它的解密(Decrypt)方法。

于是让 Gemini 写了代码

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import base64
import hashlib
import json
import requests
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Hash import SHA1
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import unpad

# 1. 填入你从反编译代码中拿到的硬编码 RSA 私钥 Base64
RSA_PRIVATE_KEY_B64 = """
MIIEogIBAAKCAQB/xKmjpGbc6MdYYp0v32JLpIaOvyTrhupgWWcQfRFyuk51p3fo
f6Eh+M3qp00yQzHtP/MkRP6Ldc5DOP6D9b2s522s7vpzsG3LtBenZZ8xN4usMBuO
TQzJTvLfCjfLl0gXBDJneKjHPKxLxaPmsFwTS1Mi3cZEMNoA8ns9hmdDwcsCLBAH
zTjpNcfQN+EQw7mRbKK4n14lLzSfYjjScuE/Oj8WOpy5Wl0/UAoHRLbujPNU26hJ
nlpQa2S6ipvFIBZHjhaU4AesST55XBOLDmPNCaKQV9Nf9RJ2GABazioPAOo+Q7iEf
EXysNcuhdP7q8gXJ0oNHdyJl6PAZZddiZyB7AgMBAAECggEASJMC8Orvasfmg7Pw
KUMv6FuZ+vdkF0zZUMU3n8wK3yooavgnSi9E7bEP9hv143j7oRHUIGP4WmseMFzt
ZTNu/Amw6KwOIyyyESVI0lMM673rXnEtFdV6T9bCaiK5srFJx5kgsFl/NTyneZrY
EK9YfbUpkgJ7HjzJeAREMJxph7h7FsSd2M+MB7m/J+6TGZ6fIsZFcy0vIQLjLSAI
OuO5T1HEB+m7AFHyvXdAfR+BWKSTIs0pkC2wUgbE6CsMlzx3XvM1DnmUiDF478t0
UFKEnpQKsnmRSweH7htbmhigiP9kHcidJ8+PNqojy+DGsCjuIoSiOqgGEs1+woYS
w34pIQKBgQDPmwiPNYxM2OYP7onoBhsBKDmdxGSUnEFmUgmtPpEy5PssJDz/SEoV
ponyW/dPEycn8Bjc+6fnjUi0RuqE2e3JnpSWrhhLBEFhrd16bHVwaHA40IWM8UYs
gUjmTPdt2DO9+M8ZW+UADUAAlARm/NSZo+Z4PzTBIjW4e4xhxm/2AwKBgQCdjUs4
VC0qdMHT5ntE5IWIagrXxshacaE8rTAgjYOMaib0Skq593yqksb1kiIF3CeWZACd
nd6qk6Gah5Oc755K8wuixxXZY8vzwzLf5K3NqFkbAhxR251kZoQvmoP/OJDbBxO0Y
86auuX7m498vIh7raoVWjzXKSHK2/ZyTfEw+KQKBgFH5r7mMtWeqxb1IvZ+muYcN
cSLA585enNxgTH3iFMd570wQyx0qWEaQSiwu8EqDD5UPk2G+5R/jg+/biMMIooJY
YefVurX0ajS9yJSMuxq1wopMnE94/fKY4kY94f23v0amNnCW/qe0k68mw04/S1uX
gmu82YHhlkDQWDBLgO4tAoGAS6xv8rBLuVa3OoY7sw1oLetxJc7usLJfVXuB4EDY
bHsYFsIQPl5m3K7/LThxawshYJTLztaJege+NAh0IEvMKSodBjXn8DVV1Hsf6mg6
WTw144d+BtZ771lxE+dEtsiiHFPv5coxxz6Fe3T73/GtlDlnrfm/Rleh8c7Cg/xx
ynECgYEAkIj8otP2kFIqR+5tTl2u3wINlVyPONyWKT0GNWN9am80sUMpYYvI5TUm
J47+2709GgiYQWZxSsNCqVPuKM57u1czOo7iRqD2UavL3GiN4zmBbGQ+4TqwPJb1
k2Y8dpIMwIGRKT9GWLexXYqBLK1JJV3TrJ4CjEA9A/Txy/HByZg="
"""

# 固定配置
AES_IV = bytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
TARGET_URL = "https://api.fatvpn.pro/api/v2/vpnServerNodes?version=84"


def decrypt_rsa_key(encrypted_key_b64: str) -> str:
    """第一步:使用硬编码的 RSA 私钥解密出临时字符串密钥"""
    # 格式化私钥文本
    key_data = RSA_PRIVATE_KEY_B64.strip().replace("\n", "")
    private_key_bytes = base64.b64decode(key_data)

    # 导入私钥
    rsa_key = RSA.import_key(private_key_bytes)

    # 注意 Java 层的 Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding")
    # 对应 Crypto 库中需要显式指定 hashAlgo=SHA1
    cipher_rsa = PKCS1_OAEP.new(rsa_key, hashAlgo=SHA1)

    encrypted_bytes = base64.b64decode(encrypted_key_b64)
    decrypted_bytes = cipher_rsa.decrypt(encrypted_bytes)

    return decrypted_bytes.decode("utf-8")


def decrypt_aes_data(temp_key_str: str, encrypted_data_b64: str) -> str:
    """第二步:将临时密钥转成 MD5 作为 AES Key,用 AES-128-CBC 解密出真正数据"""
    # 1. 计算临时密钥字符串的 MD5
    md5_hash = hashlib.md5()
    md5_hash.update(temp_key_str.encode("utf-8"))
    aes_key = md5_hash.digest()  # 16字节的二进制,对应 AES-128

    # 2. 初始化 AES 解密器 (CBC 模式)
    cipher_aes = AES.new(aes_key, AES.MODE_CBC, AES_IV)

    # 3. 解密并移除 PKCS5/PKCS7 填充
    encrypted_bytes = base64.b64decode(encrypted_data_b64)
    decrypted_padded = cipher_aes.decrypt(encrypted_bytes)
    decrypted_bytes = unpad(decrypted_padded, AES.block_size)

    return decrypted_bytes.decode("utf-8")


def main():
    # 伪装请求头,避免被风控(Authorization 根据你前一步抓包或计算的结果填入)
    # 如果前面分析的 Authorization 生成无误,可以自行计算填入。这里假设你可以通过抓包拿到现成的,或者不需要
    headers = {
        "User-Agent": "Ktor client",
        "Cache-Control": "no-cache",
        "Content-Type": "text/plain",
        "Authorization": "N0Q6Q0Y6Nzg6RDE6"
    }

    print(f"正在请求接口: {TARGET_URL} ...")
    try:
        response = requests.get(TARGET_URL, headers=headers, timeout=15)
        if response.status_code != 200:
            print(f"请求失败,状态码: {response.status_code}")
            return

        # 拿到服务器返回的加密 JSON 对象 (包含 key 和 data 字段)
        encrypted_json = response.json()
        encrypted_key = encrypted_json.get("key")
        encrypted_data = encrypted_json.get("data")

        if not encrypted_key or not encrypted_data:
            print("返回的 JSON 数据结构不完整,未找到 key 或 data 字段。")
            return

        print("成功获取加密数据,开始执行 RSA 解密...")
        # 1. 解密出中间 key
        temp_key = decrypt_rsa_key(encrypted_key)

        print("RSA 解密成功,开始执行 AES 解密...")
        # 2. 用中间 key 解密出真正的明文 JSON
        clear_text_json_str = decrypt_aes_data(temp_key, encrypted_data)

        print("\n🎉 解密成功!标准的明文 JSON 如下:")
        # 格式化输出最终的 JSON
        final_json_object = json.loads(clear_text_json_str)
        print(json.dumps(final_json_object, indent=4, ensure_ascii=False))

    except Exception as e:
        print(f"解密过程中发生错误: {e}")


if __name__ == "__main__":
    main()

于是成功 Cloudflare 521

怎么又失败呢?呜呜呜!

干脆直接在隔离环境中跑一遍好了。把应用下载一下,平板上跑一跑,唔的咋回事你怎么自己的也连接不了wc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
    "type": "https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-521/",
    "title": "Error 521: Web server is down",
    "status": 521,
    "detail": "Cloudflare attempted to connect to the origin web server, but the connection was refused. The origin is down, blocking Cloudflare IPs, or not accepting connections on the configured port.",
    "instance": "a050eeb8dfa3ba3c",
    "error_code": 521,
    "error_name": "origin_down",
    "error_category": "origin",
    "ray_id": "a050eeb8dfa3ba3c",
    "timestamp": "2026-06-01T20:22:48Z",
    "zone": "api.fatvpn.pro",
    "cloudflare_error": true,
    "retryable": true,
    "retry_after": 120,
    "owner_action_required": true,
    "what_you_should_do": "**Wait and retry.** Back off for at least 120 seconds. If the error persists, the origin is offline or blocking Cloudflare — the website operator must restart the origin or whitelist Cloudflare IPs.",
    "footer": "This error was generated by Cloudflare on behalf of the website owner."
}

抓取失败,原本的应用也打不开。

算了,换一个标的,反正咱的请求和真应用的请求差不多,就算咱是成功的好了。

之前把平板用 Any webview 弄了一下,到了 108,刚刚掉回了 74,几年过去,于是 Lsposed 竟年久失修了,呜呜叹息。

于是继续吧,随便去网络上搜索 “vpn 安卓”,也不逆向了,下载后直接抓网络包先。

接下来,模板更改为 com.yingmao.clash ,代号 八爪鱼,来自 https://who168.xunshanpro.com/182235/bzy.apk

捕捉到其请求

GET https://app.bazhuayujiasu.cc:8700/7air2vr7ikiqzgvxz/api/v1/client/subscribe?token=26bdf16e3bbf55131c9ea517e5570a82&flag=clash

直接导入v2rayNG试试看👀

去除&flag=clash,成功导入。

注意到其请求中,7air2vr7ikiqzgvxz 和 token 为魔法,如果更改,会是什么效果呢?

1
2
3
{
    "message": "token is error"
}

于是再看看如果改动另一个魔法数字

改了也不行。

那么token和这个魔法数字是从网络来的吗?

查到了,前一个魔法数字写死的,后一个传参过去的。 重新下载安装,发现token一致。要么通过账户密码拼接,要么是通过某次秘闻传递。

通过关键词 flag=clash,查到了 c9.p 其拼接字符串部分物料来自 BeforeNodeChooseActivity 来自 UIMainActivity.Companion.getSubUrl

于是尝试,找到了 httpSuccess 方法(诡异的,为什么都是只混淆了一半,另一半不混淆),于是是来自 SubscriptionRsp 其实这里插入个hook或者像之前那样直接测网络链接即可。但是咱需要全合成!于是HttpApi,唔的注解驱动,kotlin原来那么好用嘛?于是找到 getAirportNode

1
2
3
4
@Nullable
@POST("/netbarcloud/vpn/airportNode.do")
@Headers({"User-Agent: Octopus_Android"})
Object getAirportNode(@NotNull @QueryMap Map<String, String> map, @NotNull @Header("token") String str, @NotNull Continuation<? super SubscriptionRsp> continuation);

草,怎么传的是 null

好多注解和咳特灵,绕晕了。 败北,绕晕了。

本文由作者按照 CC BY 4.0 进行授权

© 洛铃. 保留部分权利。

本站总访问量 本站访客数人次

🚩🚩🚩🚩🚩🚩