使用华为4G路由器(B311As-853)来做私人短信服务器

这台B311As-853路由器买来也快2年了吧,一直把它作为私有云的应急网络使用,里面插着一张联通卡,当家里异常停电的时候路由器就会将网络出口切换到这台4G路由器,以维持个人服务器与私有云的通信,确保随时能访问到。
这台路由器提供了短信发送服务,但是每次都要登录路由器,这是不是麻烦了点,如果能接口发送那该多好。

(English version translate by GPT-3.5)

前言

我多次尝试后,发现路由器只能通过与它相连的设备进行登录操作,如果通过nginx反代路由器管理地址然后请求nginx的地址,token获取就会报错(我没时间折腾),所以如果希望提供短信服务,就需要一台与路由器相连的设备例如树莓派啥的。我用的是B311As-853路由器,如下这一台,以及它管理界面如下。

B311As-853路由器

B311As-853管理界面

思路

思路其实很简单,我之前不是写过一篇 使用Java编写程序来登录华为WS5200路由器 么,思路基本上与这个差不多,就是找到发送短信接口,然后模拟登录路由器,模拟发送短信,只不过这台B311As-853路由器它登录和请求的通信用的是XML。但是这个功能建议就是给自己发发,或者给自己朋友提供短信,最好别发给很多人,小心被投诉骚扰,毕竟人家是能够看到你的号码的

开始,F12起来,观察登录流程

  1. 首先,进入/html/index.html,里面内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html id="html">
    <head><head><meta name="csrf_token" content="jIe0fYkd06vlXIaEXfIboaFlAJ2TZemf">
    <meta name="csrf_token" content="fHQRMzwE5D0UoOYSDo7F3lHtlJ2vxxoi">

    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta name="format-detection" content="telephone=no"/>
    <meta http-equiv='Pragma' content='no-cache'/>
    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate"/>
    <meta name="description" content="emui webui 6.0"/>
    <meta name="author" content="emui webui 6.0"/>
  2. 其次,请求了/api/webserver/token,获得了一个Token

    发送

    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
    <token>kixxpBushl0g7cwfKBWo8f5x0rUQZ0343GfPcY89AmPAmGe8Rvuz7zgFgadcINsm</token>
    </response>
  3. 接着,仿佛带着Token的某一部分,请求/api/user/challenge_login

    发送头

    1
    __RequestVerificationToken: 3GfPcY89AmPAmGe8Rvuz7zgFgadcINsm

    发送

    1
    2
    3
    4
    5
    6
    <?xml version: "1.0" encoding="UTF-8"?>
    <request>
    <username>admin</username>
    <firstnonce>949fc48d7bd5704f508f89303ea95bec4a7902b842b70c3ff8e4a856bf2936b3</firstnonce>
    <mode>1</mode>
    </request>

    响应头

    1
    __RequestVerificationToken: GvRIT0BRzfKnvh4jPJHbwEHptVKu0pN1

    响应

    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
    <salt>ce9240656b67c976d81626aecceb3430fac98ddce1b8bb249f9fd742d0ecd01a</salt>
    <modeselected>1</modeselected>
    <servernonce>949fc48d7bd5704********SOEVzdqxyVkdOhVKODlspu</servernonce>
    <newType>0</newType>
    <iterations>1000</iterations>
    </response>
  4. 最后调用了/api/user/authentication_login来执行登录

    发送头

    1
    __RequestVerificationToken: GvRIT0BRzfKnvh4jPJHbwEHptVKu0pN1

    发送

    1
    2
    3
    4
    5
    <?xml version: "1.0" encoding="UTF-8"?>
    <request>
    <clientproof>a6cba44883fd3c1efad6644d2ad4d8a03aad825e58da8e451be0494803490b5c</clientproof>
    <finalnonce>949fc48d7bd5704f508f89303ea95bec4a7902b842b70c3ff8e4a856bf2936b36rDZBBWmHpSOEVzdqxyVkdOhVKODlspu</finalnonce>
    </request>

    响应头

    1
    2
    3
    __RequestVerificationToken: 60vwrcRmApKTXp23fXrMggXOKmwfaZpw#YTXJyp0GFYdE6yCRY2PXRhSePegBin0z#OxPBLp9C30mt5ATFdLhOXl7twjSg903W#Xpdf50IOyMOJ4sKiz0u4Q3WGvonhsgmB#aHs0u9wwWIUQyh8GniyAr63d0l75gvMo#ktV97hCbe1mbfBNUs0YV1r6ql5t0j5YB#5yZM9YazQzMNwFBBKLInwXgMR8QLu8wU#ggtvwsAOaU7QeuRATODQgRtyPfh0UOND#drH0yfkP078zY59OoitQPt8kd3wq6lL6#9u5P2khD8EXlbV64tfvSm5UN0KvHgZsr#7iI4fQKHpkvqVO0VLM0dmpbe8CUhRNSf#l83rMlKJPgdrUtBYzd3J4bahpvVdBpff#cARzgMu9e0IcKRjuNbq1giCFGrgHdsZJ#CueDzDma4ND0MFOWpXudq4rOe9geXCms#VZ105qsscX06ovVkV4uC4SrqxQvQD1eg#JK7Vho4MAh2aWk3Aq7bdxGXsQQIC4L8E#cMQ6I9xF0qEEdrAGPXErdW0kn9Qeg7HK#cJqgObOorgKTFlOmN0yLWSAQkvQKlcQM#4HvCvwyahOk9COjx4Iia0Hiz85h0oacZ#n0iPS1JYqo2nbat5JPfUzz3rJkn2cDzP#gnuOtEoRggG3iQOr1r8ledTG8V49RsI5#X08XP2buIl3AkkmSkZwirGvr5jvLMCRL#VqcfsbxrgoNS8CLU7IzpXbA4xkAu9kGm#00sIkIn1AdP53Jk1i7M5AshO6P7kSDlY#bytRkFGTpuEWW0zT5VIfRb52tYdp0h1o#yyNj0UBaPK9cCLZiDpYSZZD5fTSOZ25Z#9nD7B9U1tJXGUW306mD3R6d39uux69CP#8RqOYuAeV03Dk8Cs3azMuQm6X28HLBNm#08bcN2unNt4GToLJhYNch0IcRnTfxkvK#nfRmgrgDXnYjypb2GjaZrV021Lik98Vj#nYWeb59Ga6vHmGW0Yv0Vwp0cNFMkMTDz#G6rcmJnZhFTVjFxYXH7WLETJ0PY1snwk
    __RequestVerificationTokenone: 60vwrcRmApKTXp23fXrMggXOKmwfaZpw
    __RequestVerificationTokentwo: YTXJyp0GFYdE6yCRY2PXRhSePegBin0z

    响应

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
    <serversignature>0289656ca121a4167f79858042119d8dca68d1615824ab59e63873f8e7041497</serversignature>
    <rsapubkeysignature>7d634a04da935fb951889f5dc71abf550028c4e0399116acd826b7115e49c1ca</rsapubkeysignature>
    <rsae>010001</rsae>
    <rsan>e75a0f874d****省略很多字*****58b2f7eb9</rsan>
    </response>
  5. 最后就跳到了/html/content.html里面了

其实整个过程跟之前WS5200路由器差不多,甚至除了xml区别没有其他的区别

一步步分出来

处理登录部分

登录部分可以看到,它基本和index.html中没有任何关系,它的第一个__RequestVerificationToken是来自token中的第32位开始,然后在每一步的操作中,从response.header.__RequestVerificationToken替换当前的token,请求index.html也只是初始化一下Cookie

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<response>
<token>kixxpBushl0g7cwfKBWo8f5x0rUQZ034【3GfPcY89AmPAmGe8Rvuz7zgFgadcINsm】 // 第32位开始的内容</token>
</response>

登录的加密方式应该和WS5200的是一致的,token获取部分就不描述了,login部分中的firstnonce,从index.js中看出,是一段长度64位的随机字符串,根据WS5200的经验,写出登录代码如下,从打印输出来看,加密方式和5200的路由器一致

index.js:559

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
private static boolean initToken() {
Request request = new Request.Builder().url(URL + "/html/index.html").get().build();
try(Response response = okHttpClient.newCall(request).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if (response.code() == 200) {
String cookiePath = response.header("Set-Cookie");
int cookieSplit = cookiePath.indexOf(";");
cookie = cookiePath.substring(0, cookieSplit);
String token = findFromXml(accessRouter("/api/webserver/token", ""), "token");
csrfToken = token.substring(32);
System.out.println("Huawei init 4G router version successful!");
return true;

}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}


private static boolean doLogin() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
String firstNonce = randomNonce();
String msg = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><username>admin</username><firstnonce>" + firstNonce + "</firstnonce><mode>1</mode></request>";
String challengeResponse = accessRouter("/api/user/challenge_login", msg);
if(isNull(challengeResponse)) {
return false;
}
int iterations = Integer.parseInt(findFromXml(challengeResponse, "iterations"));
String salt = findFromXml(challengeResponse, "salt");
String serverNonce = findFromXml(challengeResponse, "servernonce");
String authMessage = firstNonce + "," + serverNonce + "," + serverNonce;
KeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), hexToByteArray(salt), iterations, 256);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] salted = f.generateSecret(spec).getEncoded();
byte[] clientKey = getHmac("Client Key", salted);
byte[] storedKey = MessageDigest.getInstance("SHA-256").digest(clientKey);
byte[] authKey = getHmac(authMessage, storedKey);
for (int i = 0; i < clientKey.length; i++) {
clientKey[i] = (byte) (clientKey[i] ^ authKey[i]);
}
String clientProof = bytesToHex(clientKey);

String loginMsg = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><clientproof>" + clientProof + "</clientproof><finalnonce>" + serverNonce + "</finalnonce></request>";
String loginRespMsg = accessRouter("/api/user/authentication_login", loginMsg);
System.out.println(loginRespMsg);
return false;
}

控制台返回

1
2
3
4
5
6
7
"C:\Program Files\Ja.....jar com.ruterfu.test.Huawei4GRouterAccessMain
Huawei init 4G router version successful!
<?xml version="1.0" encoding="UTF-8"?><response><serversignature>833580708b5efe211fb99ec96fe788a140a22816ec11a6add3b8538137113a9e</serversignature><rsapubkeysignature>7d634a04da935fb951889f5dc71abf550028c4e0399116acd826b7115e49c1ca</rsapubkeysignature><rsae>010001</rsae><rsan>e75a0f874d93cb0733125296421a0a3d24243f7834d34ea5c4e7502c87646717f21d476c89c01f5da2534430ae229fdf8c1d8f9e5883b0433eb8f8dbb52ea1a9e3338ecb9d4c43de2cce405d94707ef886fb2c31209988ba7ac35210c8820d4df74da8597595746b68db19ea5dab33e98ae159e09ad9955756975a5530042369427e8f42b4516caaf5405fcfa12b43712ed3419b2eb49b60453f764c5393c594919ec602f281d3735032d2e305046d98fa82117651f09ef02b1dab4f755df4cb790ef28443e87f76fe0cfd6550f8ab8862d408ad5f7da7913ced7a1b817974408e16119e8c45a041150c823ad70832c7d33b4f719ef42ee43f7e09f58b2f7eb9</rsan></response>
false

Process finished with exit code 0

继续分析短信接口流程

接下来,分析短信部分的接口,首先是登录后进入/html/content.html,这个网页部分内容是这样的

1
2
3
4
5
6
7
8
9
10
<head><meta name="csrf_token" content="L4fvnQx0GYalJcxUQUxu70mVHpND2ZlL">
<meta name="csrf_token" content="cCK2EjEqAzXI8CeglGGjuIPBIUeUvzDu">

<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv='Pragma' content='no-cache'/>
<meta name="format-detection" content="telephone=no"/>
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate"/>
<meta name="description" content="emui webui 6.0"/>
<meta name="author" content="emui webui 6.0"/>

然后发送短信,显示请求/api/sms/send-sms

请求头

1
__RequestVerificationToken: 6FW0TNffiu2dqYo47NdoSg1T5IbcUPbp

请求

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version: "1.0" encoding="UTF-8"?>
<request>
<Index>-1</Index>
<Phones>
<Phone>13**********</Phone>
</Phones>
<Sca></Sca>
<Content>1</Content>
<Length>1</Length>
<Reserved>1</Reserved>
<Date>2022-04-15 13:39:42</Date>
</request>

响应头

1
__RequestVerificationToken: uDlKYFpmpNbMwBQohxi0RLKhMqeOskXK

响应

1
2
<?xml version="1.0" encoding="UTF-8"?>
<response>OK</response>

可以看到上述的token都乱了,没有什么规律,但是出现这么多次请求,应该有部分用到了token了

请求

一个个找,从/html/content.html开始找,发现

  1. /api/system/onlineupg 使用了一次token

    1
    2
    请求:__RequestVerificationToken: L4fvnQx0GYalJcxUQUxu70mVHpND2ZlL
    响应:__RequestVerificationToken: yUMKeCI7MrYZ070rLgxtwY0MbC0tLLkO
  2. /api/host/info 使用一次token

    1
    2
    请求:__RequestVerificationToken: cCK2EjEqAzXI8CeglGGjuIPBIUeUvzDu
    响应:__RequestVerificationToken: kPxK0M2hRLW7cwAX1p5eoSMH1u5dWO6r
  3. /api/sms/sms-list-contact 使用一次

    1
    2
    请求:__RequestVerificationToken: yUMKeCI7MrYZ070rLgxtwY0MbC0tLLkO
    响应:__RequestVerificationToken: 1g0QZRFK30Ks9AkfrNt0aT0xIAE2xTv3
  4. /api/sms/sms-count-contact 使用一次

    1
    2
    请求:__RequestVerificationToken: kPxK0M2hRLW7cwAX1p5eoSMH1u5dWO6r
    响应:__RequestVerificationToken: 6FW0TNffiu2dqYo47NdoSg1T5IbcUPbp
  5. /api/sms/sms-list-phone 使用一次

    1
    2
    请求:__RequestVerificationToken: 1g0QZRFK30Ks9AkfrNt0aT0xIAE2xTv3
    响应:__RequestVerificationToken: 1VbQWlAWROiyTSvEVcXMYPvlxJ010920

规律非常明显,从登录后开始,不再是上一次返回的token被下一个请求使用,而是使用上上一个token,而初始token是在content.html中。

1
2
3
4
5
6
7
8
content.html 返回token1, token2
/api/system/onlineupg 使用token1,返回token3
/api/host/info 使用token2,返回token4
/api/sms/sms-list-contact 使用token3,返回token5
/api/sms/sms-count-contact 使用token4,返回token6
/api/sms/sms-list-phone 使用token5,返回token7
/api/sms/send-sms 使用token6,返回token8
以此类推

有如上的规律,就得稍微改写下代码了

我这里用Stack,当某个请求返回了token时,将当前token pop出去,添加新的token,加上短信的代码

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
public List<String> sendSms(List<String> phoneNumber, String message) {
// 构建消息体
String sendBody = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><Index>-1</Index><Phones><Phone>" + String.join(",", phoneNumber) + "</Phone></Phones><Sca></Sca><Content>" + message + "</Content><Length>" + message.length() + "</Length><Reserved>1</Reserved><Date>" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()) + "</Date></request>";
// 发送消息
String body = accessRouter("/api/sms/send-sms", sendBody);
// 如果有OK,表示发送成功
if (body != null && body.contains("<response>OK</response>")) {
// 发送成功后,等待4.5秒去请求status查看发送状态,确认是否发送成功
try {
Thread.sleep(4500);
} catch (InterruptedException e) {
e.printStackTrace();
}
String resultString = accessRouter("/api/sms/send-status", "");
int index = resultString == null ? -1 : resultString.indexOf("<SucPhone>");
int index2 = index == -1 ? -1 : resultString.indexOf("</SucPhone>");
if (index >= 0 && index2 > index) {
// 获取发送成功的号码
String successBody = resultString.substring(index + 10, index2).trim();
List<String> successList = Arrays.asList(split(successBody, ","));
return phoneNumber.stream().filter(successList::contains).collect(Collectors.toList());
}
}
return null;
}

上面send-status的返回内容如下

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<response>
<Phone></Phone>
<SucPhone>13000000000</SucPhone>
<FailPhone></FailPhone>
<TotalCount>1</TotalCount>
<CurIndex>1</CurIndex>
</response>

测试结果

写个Main方法

1
2
3
4
5
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
if(initToken() && doLogin()) {
System.out.println(sendSms(Collections.singletonList("13000000000"), "这是用接口发出去的短信").toString());
}
}

控制台输出

1
2
3
4
5
Huawei init 4G router version successful!
Huawei 4G Router Login successful
[13000000000]

Process finished with exit code 0

手机收到的

手机消息效果

哦别忘了写个退出方法,退出方法请求接口和参数我就不详细说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static boolean logout() {
try {
String msg = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><Logout>1</Logout></request>";
String challenge = accessRouter("/api/user/logout", msg);
boolean success = challenge != null && challenge.contains("<response>OK</response>");
if(success) {
System.out.println("Huawei 4G router logout successful");
} else {
System.out.println("Huawei 4G router logout failed, response " + (challenge == null ? "NULL" : challenge));
}
return success;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

最后,附上完整代码

有Main方法,写个springboot接口调用下不就成了。既然都能发送短信了,那么是否能够定时从路由器去读取短信然后做自动化响应是不是也是极好的哈哈。

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package com.ruterfu.test;

import okhttp3.*;

import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;


public class Huawei4GRouterAccessMain {

private static final Stack<String> csrfToken = new Stack<>();
private static final OkHttpClient okHttpClient = new OkHttpClient();
private static String cookie;
private static final String URL = "http://192.168.8.1";
private static final String PASSWORD = "路由器密码";

public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
if(initToken() && doLogin()) {
System.out.println(sendSms(Collections.singletonList("手机号码"), "短信内容").toString());
logout();
}
}

private static boolean initToken() {
// 请求index.html用来初始化Cookie
Request request = new Request.Builder().url(URL + "/html/index.html").get().build();
try(Response response = okHttpClient.newCall(request).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if (response.code() == 200) {
String cookiePath = response.header("Set-Cookie");
int cookieSplit = cookiePath.indexOf(";");
cookie = cookiePath.substring(0, cookieSplit);
// 然后请求token获取初始的token值
String token = findFromXml(accessRouter("/api/webserver/token", ""), "token");
csrfToken.push(token.length() > 32 ? token.substring(32) : "");
System.out.println("Huawei init 4G router version successful!");
return true;

}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}


private static boolean doLogin() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
// firstNonce是一个随机64位的hash字符串,
String firstNonce = randomNonce();
// 构建登录body
String msg = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><username>admin</username><firstnonce>" + firstNonce + "</firstnonce><mode>1</mode></request>";
// 执行登录
String challengeResponse = accessRouter("/api/user/challenge_login", msg);
if(isNull(challengeResponse)) {
return false;
}
// 从上面的msg中获得响应,然后拼接成密钥请求过去
int iterations = Integer.parseInt(findFromXml(challengeResponse, "iterations"));
String salt = findFromXml(challengeResponse, "salt");
String serverNonce = findFromXml(challengeResponse, "servernonce");
String authMessage = firstNonce + "," + serverNonce + "," + serverNonce;
KeySpec spec = new PBEKeySpec(PASSWORD.toCharArray(), hexToByteArray(salt), iterations, 256);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] salted = f.generateSecret(spec).getEncoded();
byte[] clientKey = getHmac("Client Key", salted);
byte[] storedKey = MessageDigest.getInstance("SHA-256").digest(clientKey);
byte[] authKey = getHmac(authMessage, storedKey);
for (int i = 0; i < clientKey.length; i++) {
clientKey[i] = (byte) (clientKey[i] ^ authKey[i]);
}
String clientProof = bytesToHex(clientKey);
// 构建登录的body
String loginMsg = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><clientproof>" + clientProof + "</clientproof><finalnonce>" + serverNonce + "</finalnonce></request>";
String loginRespMsg = accessRouter("/api/user/authentication_login", loginMsg);
// 读取content.html,这一步有概率会返回12005,我也不知道为什么,但是重试又好了。。
if(loginRespMsg != null && loginRespMsg.contains("serversignature")) {
csrfToken.clear();
Request request = new Request.Builder().url(URL + "/html/content.html")
.addHeader("Cookie", cookie)
.get().build();
try(Response response = okHttpClient.newCall(request).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if (response.code() == 200) {
String bodyString = responseBody == null ? null : responseBody.string();
// 如果返回了如下内容,就视为失败,这是之前总结的经验
if(bodyString != null && !bodyString.contains("EMUI.RebootController.rebootInfo")) {
System.err.println("Huawei 4G Router Login failed, cannot access content.html");
return false;
}
// 否则,找到header头,找到里面的2个meta
int start = bodyString == null ? -1 : bodyString.indexOf("<head>");
int end = start == -1 ? -1 : bodyString.indexOf("</head>");
if(start >= 0 && end > start) {
String headerContent = bodyString.substring(start + 6, end);
String[] singleLine = split(headerContent, "\n");

for (String s : singleLine) {
if(s.contains("<meta") && s.contains("csrf_token")) {
int startC = s.indexOf("content=\"");
int endC = startC == -1 ? -1 : s.indexOf("\">");
if(startC >= 0 && endC > startC) {
// 将csrfToken推入Stack中
csrfToken.push(s.substring(startC + 9, endC));
}
}
}
System.out.println("Huawei 4G Router Login successful");
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}

public static List<String> sendSms(List<String> phoneNumber, String message) {
// 构建消息体
String sendBody = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><Index>-1</Index><Phones><Phone>" + String.join(",", phoneNumber) + "</Phone></Phones><Sca></Sca><Content>" + message + "</Content><Length>" + message.length() + "</Length><Reserved>1</Reserved><Date>" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()) + "</Date></request>";
// 发送消息
String body = accessRouter("/api/sms/send-sms", sendBody);
// 如果有OK,表示发送成功
if (body != null && body.contains("<response>OK</response>")) {
// 发送成功后,等待4.5秒去请求status查看发送状态,确认是否发送成功
try {
Thread.sleep(4500);
} catch (InterruptedException e) {
e.printStackTrace();
}
String resultString = accessRouter("/api/sms/send-status", "");
int index = resultString == null ? -1 : resultString.indexOf("<SucPhone>");
int index2 = index == -1 ? -1 : resultString.indexOf("</SucPhone>");
if (index >= 0 && index2 > index) {
// 获取发送成功的号码
String successBody = resultString.substring(index + 10, index2).trim();
List<String> successList = Arrays.asList(split(successBody, ","));
return phoneNumber.stream().filter(successList::contains).collect(Collectors.toList());
}
}
return null;
}

public static boolean logout() {
try {
String msg = "<?xml version: \"1.0\" encoding=\"UTF-8\"?><request><Logout>1</Logout></request>";
String challenge = accessRouter("/api/user/logout", msg);
boolean success = challenge != null && challenge.contains("<response>OK</response>");
if(success) {
System.out.println("Huawei 4G router logout successful");
} else {
System.out.println("Huawei 4G router logout failed, response " + (challenge == null ? "NULL" : challenge));
}
return success;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

private static String accessRouter(String url, String body) {
Request.Builder request = new Request.Builder().url(URL + url).header("Cookie", cookie);
if(isNull(body)) {
request.get();
} else {
request.post(RequestBody.create(body, MediaType.parse("application/x-www-form-urlencoded")));
}
if(csrfToken.size() > 0) {
request.header("__RequestVerificationToken", csrfToken.pop());
}

try(Response response = okHttpClient.newCall(request.build()).execute(); ResponseBody responseBody = response.body() != null ? response.body() : null) {
if(response.code() == 200) {
String csrfTokenInHeader = response.header("__RequestVerificationToken");
if(!isNull(csrfTokenInHeader)) {
int index = csrfTokenInHeader.indexOf("#");
csrfToken.push(index == -1 ? csrfTokenInHeader : csrfTokenInHeader.substring(0, index));
}
String newHeader = response.header("Set-Cookie");
if(newHeader != null) {
cookie = newHeader;
}
if(responseBody == null) {
return "";
} else {
return responseBody.string();
}

}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 不用Xml解析我懒,不就是取得<key><key/>的内容么,直接截取下够用了
*/
private static String findFromXml(String xml, String key) {
if(isNull(xml)) {
return "";
} else {
String start = "<" + key + ">";
int startI = xml.indexOf(start);
int endI = startI == -1 ? -1 : xml.indexOf("</" + key + ">");
if(startI >= 0 && endI > startI) {
return xml.substring(startI + start.length(), endI).trim();
}
return "";

}
}
/** 下面开始都是工具了 */
private static boolean isNull(String s) {
return s == null || s.length() == 0;
}
private static String randomNonce() {
String rand = "abcdef1234567890";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 64; i++) {
int number = random.nextInt(rand.length());
sb.append(rand.charAt(number));
}
return sb.toString();
}
private static byte[] getHmac(String key, byte[] input) throws NoSuchAlgorithmException, InvalidKeyException {
String hmacName = "HmacSHA256";
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.US_ASCII), hmacName);
Mac mac = Mac.getInstance(hmacName);
mac.init(secretKeySpec);
mac.update(input);
return mac.doFinal();
}
public static byte[] hexToByteArray(String inHex){
int hexLength = inHex.length();
byte[] result;
if (hexLength % 2 == 1){
hexLength++;
result = new byte[(hexLength / 2)];
inHex = "0" + inHex;
}else {
result = new byte[(hexLength / 2)];
}
int j=0;
for (int i = 0; i < hexLength; i += 2){
result[j]=(byte)Integer.parseInt(inHex.substring(i, i + 2),16);
j++;
}
return result;
}
public static String bytesToHex(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if(hex.length() < 2){
sb.append(0);
}
sb.append(hex);
}
return sb.toString();
}
private static String[] split(String str, String token) {
StringTokenizer st = new StringTokenizer(str, token);
String[] s = new String[st.countTokens()];
int t = 0;

while(st.hasMoreTokens()) {
if (t < s.length) {
s[t++] = st.nextToken().trim();
}
}

return s;
}
}