Using Huawei 4G Router (B311As-853) as a Private SMS Server

I’ve had this B311As-853 router for almost 2 years now. I’ve been using it as an emergency network for my private cloud. It has a China Unicom SIM card inserted, so when there is a power outage at home, the router switches the network connection to this 4G router to maintain communication between my personal server and private cloud, ensuring accessibility at all times.
This router provides SMS sending service, but it’s inconvenient to log in each time. It would be great if we could send SMS through an API.(English version Translated by GPT-3.5, 返回中文)

Introduction

After several attempts, I found that the router can only be logged in through a connected device. If I try to access the router management address through Nginx reverse proxy and request Nginx’s address, the token retrieval will fail (I don’t have time to tinker with it). So, if you want to provide SMS service, you need a device connected to the router, like a Raspberry Pi or similar. I’m using the B311As-853 router shown below, along with its management interface.

B311As-853 Router

B311As-853 Management Interface

Approach

The approach is quite simple. I’ve written an article before on Logging in to Huawei WS5200 Router using Java, and the approach is similar. The task is to find the SMS sending API, simulate logging in to the router, and simulate sending SMS. The only difference is that this B311As-853 router uses XML for login and requests. However, it is recommended to use this feature for sending SMS only to yourself or your friends. It’s better not to send to many people to avoid complaints and harassment, as others can see your number.

Let’s start by inspecting the login process with F12

  1. First, access /html/index.html. The content inside is as follows:

    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. Then, a request is made to /api/webserver/token to obtain a token.

    Sending

    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
    <token>kixxpBushl0g7cwfKBWo8f5x0rUQZ0343GfPcY89AmPAmGe8Rvuz7zgFgadcINsm</token>
    </response>
  3. Next, with a part of the token, a request is made to /api/user/challenge_login.

    Sending headers

    1
    __RequestVerificationToken: 3GfPcY89AmPAmGe8Rvuz7zgFgadcINsm

    Sending

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

    Response headers

    1
    __RequestVerificationToken: GvRIT0BRzfKnvh4jPJHbwEHptVKu0pN1

    Response

    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. Finally, /api/user/authentication_login is called to execute the login.

    Sending headers

    1
    __RequestVerificationToken: GvRIT0BRzfKnvh4jPJHbwEHptVKu0pN1

    Sending

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

    Response headers

    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

    Response

    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. Finally, it redirects to /html/content.html.

The entire process is similar to the previous WS5200 router, with the only difference being the use of XML instead of JSON for communication.

Step by step breakdown

Handling the login part

In the login part, it can be seen that it is mostly unrelated to index.html. The first __RequestVerificationToken comes from the token starting from the 32nd character. In each step, the current token is replaced with response.header.__RequestVerificationToken, and accessing index.html is only for initializing the cookies.

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

The encryption method for login should be consistent with WS5200. The token retrieval part will not be described here. Based on the firstnonce in the login part, which is a random 64-character string according to index.js, the login code can be written as follows. From the console output, it can be seen that the encryption method is consistent with the 5200 router.

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;
}

Console output

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

Continuing to analyze the SMS API flow

Next, let’s analyze the SMS interface part. After logging in, /html/content.html is accessed. The content of this webpage is as follows:

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"/>

Then, a request is made to /api/sms/send-sms to send an SMS.

Request headers

1
__RequestVerificationToken: 6FW0TNffiu2dqYo47NdoSg1T5IbcUPbp

Request

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>

Response headers

1
__RequestVerificationToken: uDlKYFpmpNbMwBQohxi0RLKhMqeOskXK

Response

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

The tokens mentioned above are all different and do not follow any specific pattern. However, since there are multiple requests, it’s likely that some of them use tokens.

Requests

Let’s go through each one starting from /html/content.html. We find:

  1. /api/system/onlineupg uses a token once.

    1
    2
    请求:__RequestVerificationToken: L4fvnQx0GYalJcxUQUxu70mVHpND2ZlL
    响应:__RequestVerificationToken: yUMKeCI7MrYZ070rLgxtwY0MbC0tLLkO
  2. /api/host/info uses a token once.

    1
    2
    请求:__RequestVerificationToken: cCK2EjEqAzXI8CeglGGjuIPBIUeUvzDu
    响应:__RequestVerificationToken: kPxK0M2hRLW7cwAX1p5eoSMH1u5dWO6r
  3. /api/sms/sms-list-contact uses a token.

    1
    2
    请求:__RequestVerificationToken: yUMKeCI7MrYZ070rLgxtwY0MbC0tLLkO
    响应:__RequestVerificationToken: 1g0QZRFK30Ks9AkfrNt0aT0xIAE2xTv3
  4. /api/sms/sms-count-contact uses a token.

    1
    2
    请求:__RequestVerificationToken: kPxK0M2hRLW7cwAX1p5eoSMH1u5dWO6r
    响应:__RequestVerificationToken: 6FW0TNffiu2dqYo47NdoSg1T5IbcUPbp
  5. /api/sms/sms-list-phone uses a token.

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

The pattern is clear. Starting from the login, the previous token is no longer used for the next request. Instead, the token from two steps ago is used, and the initial token is in 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
以此类推

With the above pattern, let’s make some code modifications

Here, I’m using a stack. When a request returns a token, we pop the current token, add the new token, and include the SMS code.

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;
}

The response for send-status is as follows:

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>

Test Results

Let’s write a Main method.

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());
}
}

Console output

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

Process finished with exit code 0

Received on the phone

Mobile message

Oh, don’t forget to write an exit method. I won’t go into detail about the API endpoint and parameters for the exit method.

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;
}

Finally, here’s the complete code

With the Main method, we can create a Spring Boot interface. Since we can already send SMS, it would be great to read messages from the router periodically and automate the response. Ha-ha.

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;
}
}