]> git.basschouten.com Git - openhab-addons.git/blob
1f3e0adee99812d8d86d1d145473342892ad7d0c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.miio.internal.cloud;
14
15 import java.io.IOException;
16 import java.net.CookieStore;
17 import java.net.HttpCookie;
18 import java.net.MalformedURLException;
19 import java.net.URI;
20 import java.net.URL;
21 import java.time.ZonedDateTime;
22 import java.time.format.DateTimeFormatter;
23 import java.util.ArrayList;
24 import java.util.Date;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.Random;
30 import java.util.TimeZone;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.eclipse.jetty.client.HttpResponseException;
38 import org.eclipse.jetty.client.api.ContentResponse;
39 import org.eclipse.jetty.client.api.Request;
40 import org.eclipse.jetty.client.util.FormContentProvider;
41 import org.eclipse.jetty.http.HttpHeader;
42 import org.eclipse.jetty.http.HttpMethod;
43 import org.eclipse.jetty.http.HttpStatus;
44 import org.eclipse.jetty.util.Fields;
45 import org.openhab.binding.miio.internal.MiIoCrypto;
46 import org.openhab.binding.miio.internal.MiIoCryptoException;
47 import org.openhab.binding.miio.internal.Utils;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 import com.google.gson.Gson;
52 import com.google.gson.GsonBuilder;
53 import com.google.gson.JsonElement;
54 import com.google.gson.JsonObject;
55 import com.google.gson.JsonParseException;
56 import com.google.gson.JsonParser;
57 import com.google.gson.JsonSyntaxException;
58
59 /**
60  * The {@link MiCloudConnector} class is used for connecting to the Xiaomi cloud access
61  *
62  * @author Marcel Verpaalen - Initial contribution
63  */
64 @NonNullByDefault
65 public class MiCloudConnector {
66
67     private static final int REQUEST_TIMEOUT_SECONDS = 10;
68     private static final String UNEXPECTED = "Unexpected :";
69     private static final String AGENT_ID = (new Random().ints(65, 70).limit(13)
70             .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString());
71     private static final String USERAGENT = "Android-7.1.1-1.0.0-ONEPLUS A3010-136-" + AGENT_ID
72             + " APP/xiaomi.smarthome APPV/62830";
73     private static Locale locale = Locale.getDefault();
74     private static final TimeZone TZ = TimeZone.getDefault();
75     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("OOOO");
76     private static final Gson GSON = new GsonBuilder().serializeNulls().create();
77
78     private final String clientId;
79
80     private String username;
81     private String password;
82     private String userId = "";
83     private String serviceToken = "";
84     private String ssecurity = "";
85     private int loginFailedCounter = 0;
86     private HttpClient httpClient;
87
88     private final Logger logger = LoggerFactory.getLogger(MiCloudConnector.class);
89
90     public MiCloudConnector(String username, String password, HttpClient httpClient) throws MiCloudException {
91         this.username = username;
92         this.password = password;
93         this.httpClient = httpClient;
94         if (!checkCredentials()) {
95             throw new MiCloudException("username or password can't be empty");
96         }
97         clientId = (new Random().ints(97, 122 + 1).limit(6)
98                 .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString());
99     }
100
101     void startClient() throws MiCloudException {
102         if (!httpClient.isStarted()) {
103             try {
104                 httpClient.start();
105                 CookieStore cookieStore = httpClient.getCookieStore();
106                 // set default cookies
107                 addCookie(cookieStore, "sdkVersion", "accountsdk-18.8.15", "mi.com");
108                 addCookie(cookieStore, "sdkVersion", "accountsdk-18.8.15", "xiaomi.com");
109                 addCookie(cookieStore, "deviceId", this.clientId, "mi.com");
110                 addCookie(cookieStore, "deviceId", this.clientId, "xiaomi.com");
111             } catch (Exception e) {
112                 throw new MiCloudException("No http client cannot be started: " + e.getMessage(), e);
113             }
114         }
115     }
116
117     public void stopClient() {
118         try {
119             this.httpClient.stop();
120         } catch (Exception e) {
121             logger.debug("Error stopping httpclient :{}", e.getMessage(), e);
122         }
123     }
124
125     private boolean checkCredentials() {
126         if (username.trim().isEmpty() || password.trim().isEmpty()) {
127             logger.info("Xiaomi Cloud: username or password missing.");
128             return false;
129         }
130         return true;
131     }
132
133     private String getApiUrl(String country) {
134         return "https://" + ("cn".equalsIgnoreCase(country.trim()) ? "" : country.trim().toLowerCase() + ".")
135                 + "api.io.mi.com/app";
136     }
137
138     public String getClientId() {
139         return clientId;
140     }
141
142     String parseJson(String data) {
143         if (data.contains("&&&START&&&")) {
144             return data.replace("&&&START&&&", "");
145         } else {
146             return UNEXPECTED.concat(data);
147         }
148     }
149
150     public String getMapUrl(String vacuumMap, String country) throws MiCloudException {
151         String url = getApiUrl(country) + "/home/getmapfileurl";
152         Map<String, String> map = new HashMap<String, String>();
153         map.put("data", "{\"obj_name\":\"" + vacuumMap + "\"}");
154         String mapResponse = request(url, map);
155         logger.trace("Response: {}", mapResponse);
156         String errorMsg = "";
157         try {
158             JsonElement response = JsonParser.parseString(mapResponse);
159             if (response.isJsonObject()) {
160                 logger.debug("Received  JSON message {}", response);
161                 if (response.getAsJsonObject().has("result")
162                         && response.getAsJsonObject().get("result").isJsonObject()) {
163                     JsonObject jo = response.getAsJsonObject().get("result").getAsJsonObject();
164                     if (jo.has("url")) {
165                         return jo.get("url").getAsString();
166                     } else {
167                         errorMsg = "Could not get url";
168                     }
169                 } else {
170                     errorMsg = "Could not get result";
171                 }
172             } else {
173                 errorMsg = "Received message is invalid JSON";
174             }
175         } catch (ClassCastException | IllegalStateException e) {
176             errorMsg = "Received message could not be parsed";
177         }
178         logger.debug("{}: {}", errorMsg, mapResponse);
179         return "";
180     }
181
182     public String getDeviceStatus(String device, String country) throws MiCloudException {
183         return request("/home/device_list", country, "{\"dids\":[\"" + device + "\"]}");
184     }
185
186     public String sendRPCCommand(String device, String country, String command) throws MiCloudException {
187         if (device.length() != 8) {
188             logger.debug("Device ID ('{}') incorrect or missing. Command not send: {}", device, command);
189         }
190         if (country.length() > 3 || country.length() < 2) {
191             logger.debug("Country ('{}') incorrect or missing. Command not send: {}", device, command);
192         }
193         String id = "";
194         try {
195             id = String.valueOf(Long.parseUnsignedLong(device, 16));
196         } catch (NumberFormatException e) {
197             String err = "Could not parse device ID ('" + device.toString() + "')";
198             logger.debug("{}", err);
199             throw new MiCloudException(err, e);
200         }
201         return request("/home/rpc/" + id, country, command);
202     }
203
204     public JsonObject getHomeList(String country) {
205         String response = "";
206         try {
207             response = request("/homeroom/gethome", country,
208                     "{\"fg\":false,\"fetch_share\":true,\"fetch_share_dev\":true,\"limit\":300,\"app_ver\":7,\"fetch_cariot\":true}");
209             logger.trace("gethome response: {}", response);
210             final JsonElement resp = JsonParser.parseString(response);
211             if (resp.isJsonObject() && resp.getAsJsonObject().has("result")) {
212                 return resp.getAsJsonObject().get("result").getAsJsonObject();
213             }
214         } catch (JsonParseException e) {
215             logger.info("{} error while parsing rooms: '{}'", e.getMessage(), response);
216         } catch (MiCloudException e) {
217             logger.info("{}", e.getMessage());
218             loginFailedCounter++;
219         }
220         return new JsonObject();
221     }
222
223     public List<CloudDeviceDTO> getDevices(String country) {
224         final String response = getDeviceString(country);
225         List<CloudDeviceDTO> devicesList = new ArrayList<>();
226         try {
227             final JsonElement resp = JsonParser.parseString(response);
228             if (resp.isJsonObject()) {
229                 final JsonObject jor = resp.getAsJsonObject();
230                 if (jor.has("result")) {
231                     CloudDeviceListDTO cdl = GSON.fromJson(jor.get("result"), CloudDeviceListDTO.class);
232                     if (cdl != null) {
233                         devicesList.addAll(cdl.getCloudDevices());
234                         for (CloudDeviceDTO device : devicesList) {
235                             device.setServer(country);
236                             logger.debug("Xiaomi cloud info: {}", device);
237                         }
238                     }
239                 } else {
240                     logger.debug("Response missing result: '{}'", response);
241                 }
242             } else {
243                 logger.debug("Response is not a json object: '{}'", response);
244             }
245         } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
246             loginFailedCounter++;
247             logger.info("Error while parsing devices: {}", e.getMessage());
248         }
249         return devicesList;
250     }
251
252     public String getDeviceString(String country) {
253         String resp;
254         try {
255             resp = request("/home/device_list_page", country, "{\"getVirtualModel\":true,\"getHuamiDevices\":1}");
256             logger.trace("Get devices response: {}", resp);
257             if (resp.length() > 2) {
258                 CloudUtil.saveDeviceInfoFile(resp, country, logger);
259                 return resp;
260             }
261         } catch (MiCloudException e) {
262             logger.info("{}", e.getMessage());
263             loginFailedCounter++;
264         }
265         return "";
266     }
267
268     public String request(String urlPart, String country, String params) throws MiCloudException {
269         Map<String, String> map = new HashMap<String, String>();
270         map.put("data", params);
271         return request(urlPart, country, map);
272     }
273
274     public String request(String urlPart, String country, Map<String, String> params) throws MiCloudException {
275         String url = urlPart.trim();
276         url = getApiUrl(country) + (url.startsWith("/app") ? url.substring(4) : url);
277         String response = request(url, params);
278         logger.debug("Request to '{}' server '{}'. Response: '{}'", country, urlPart, response);
279         return response;
280     }
281
282     public String request(String url, Map<String, String> params) throws MiCloudException {
283         if (this.serviceToken.isEmpty() || this.userId.isEmpty()) {
284             throw new MiCloudException("Cannot execute request. service token or userId missing");
285         }
286         loginFailedCounterCheck();
287         startClient();
288         logger.debug("Send request to {} with data '{}'", url, params.get("data"));
289         Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
290         request.agent(USERAGENT);
291         request.header("x-xiaomi-protocal-flag-cli", "PROTOCAL-HTTP2");
292         request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
293         request.cookie(new HttpCookie("userId", this.userId));
294         request.cookie(new HttpCookie("yetAnotherServiceToken", this.serviceToken));
295         request.cookie(new HttpCookie("serviceToken", this.serviceToken));
296         request.cookie(new HttpCookie("locale", locale.toString()));
297         request.cookie(new HttpCookie("timezone", ZonedDateTime.now().format(FORMATTER)));
298         request.cookie(new HttpCookie("is_daylight", TZ.inDaylightTime(new Date()) ? "1" : "0"));
299         request.cookie(new HttpCookie("dst_offset", Integer.toString(TZ.getDSTSavings())));
300         request.cookie(new HttpCookie("channel", "MI_APP_STORE"));
301
302         if (logger.isTraceEnabled()) {
303             for (HttpCookie cookie : request.getCookies()) {
304                 logger.trace("Cookie set for request ({}) : {} --> {}     (path: {})", cookie.getDomain(),
305                         cookie.getName(), cookie.getValue(), cookie.getPath());
306             }
307         }
308         String method = "POST";
309         request.method(method);
310
311         try {
312             String nonce = CloudUtil.generateNonce(System.currentTimeMillis());
313             String signedNonce = CloudUtil.signedNonce(ssecurity, nonce);
314             String signature = CloudUtil.generateSignature(url.replace("/app", ""), signedNonce, nonce, params);
315
316             Fields fields = new Fields();
317             fields.put("signature", signature);
318             fields.put("_nonce", nonce);
319             fields.put("data", params.get("data"));
320             request.content(new FormContentProvider(fields));
321
322             logger.trace("fieldcontent: {}", fields.toString());
323             final ContentResponse response = request.send();
324             if (response.getStatus() >= HttpStatus.BAD_REQUEST_400
325                     && response.getStatus() < HttpStatus.INTERNAL_SERVER_ERROR_500) {
326                 this.serviceToken = "";
327             }
328             return response.getContentAsString();
329         } catch (HttpResponseException e) {
330             serviceToken = "";
331             logger.debug("Error while executing request to {} :{}", url, e.getMessage());
332             loginFailedCounter++;
333         } catch (InterruptedException | TimeoutException | ExecutionException | IOException e) {
334             logger.debug("Error while executing request to {} :{}", url, e.getMessage());
335             loginFailedCounter++;
336         } catch (MiIoCryptoException e) {
337             logger.debug("Error while decrypting response of request to {} :{}", url, e.getMessage(), e);
338             loginFailedCounter++;
339         }
340         return "";
341     }
342
343     private void addCookie(CookieStore cookieStore, String name, String value, String domain) {
344         HttpCookie cookie = new HttpCookie(name, value);
345         cookie.setDomain("." + domain);
346         cookie.setPath("/");
347         cookieStore.add(URI.create("https://" + domain), cookie);
348     }
349
350     public synchronized boolean login() {
351         if (!checkCredentials()) {
352             return false;
353         }
354         if (!userId.isEmpty() && !serviceToken.isEmpty()) {
355             return true;
356         }
357         logger.debug("Xiaomi cloud login with userid {}", username);
358         try {
359             if (loginRequest()) {
360                 loginFailedCounter = 0;
361             } else {
362                 loginFailedCounter++;
363                 logger.debug("Xiaomi cloud login attempt {}", loginFailedCounter);
364             }
365         } catch (MiCloudException e) {
366             logger.info("Error logging on to Xiaomi cloud ({}): {}", loginFailedCounter, e.getMessage());
367             loginFailedCounter++;
368             serviceToken = "";
369             loginFailedCounterCheck();
370             return false;
371         }
372         return true;
373     }
374
375     void loginFailedCounterCheck() {
376         if (loginFailedCounter > 10) {
377             logger.info("Repeated errors logging on to Xiaomi cloud. Cleaning stored cookies");
378             dumpCookies(".xiaomi.com", true);
379             dumpCookies(".mi.com", true);
380             serviceToken = "";
381             loginFailedCounter = 0;
382         }
383     }
384
385     protected boolean loginRequest() throws MiCloudException {
386         try {
387             startClient();
388             String sign = loginStep1();
389             String location;
390             if (!sign.startsWith("http")) {
391                 location = loginStep2(sign);
392             } else {
393                 location = sign; // seems we already have login location
394             }
395             final ContentResponse responseStep3 = loginStep3(location);
396             switch (responseStep3.getStatus()) {
397                 case HttpStatus.FORBIDDEN_403:
398                     throw new MiCloudException("Access denied. Did you set the correct api-key and/or username?");
399                 case HttpStatus.OK_200:
400                     return true;
401                 default:
402                     logger.trace("request returned status '{}', reason: {}, content = {}", responseStep3.getStatus(),
403                             responseStep3.getReason(), responseStep3.getContentAsString());
404                     throw new MiCloudException(responseStep3.getStatus() + responseStep3.getReason());
405             }
406         } catch (InterruptedException | TimeoutException | ExecutionException e) {
407             throw new MiCloudException("Cannot logon to Xiaomi cloud: " + e.getMessage(), e);
408         } catch (MiIoCryptoException e) {
409             throw new MiCloudException("Error decrypting. Cannot logon to Xiaomi cloud: " + e.getMessage(), e);
410         } catch (MalformedURLException | JsonParseException e) {
411             throw new MiCloudException("Error getting logon URL. Cannot logon to Xiaomi cloud: " + e.getMessage(), e);
412         }
413     }
414
415     private String loginStep1() throws InterruptedException, TimeoutException, ExecutionException, MiCloudException {
416         final ContentResponse responseStep1;
417
418         logger.trace("Xiaomi Login step 1");
419         String url = "https://account.xiaomi.com/pass/serviceLogin?sid=xiaomiio&_json=true";
420         Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
421         request.agent(USERAGENT);
422         request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
423         request.cookie(new HttpCookie("userId", this.userId.length() > 0 ? this.userId : this.username));
424
425         responseStep1 = request.send();
426         final String content = responseStep1.getContentAsString();
427         logger.trace("Xiaomi Login step 1 content response= {}", content);
428         logger.trace("Xiaomi Login step 1 response = {}", responseStep1);
429         try {
430             JsonElement resp = JsonParser.parseString(parseJson(content));
431             CloudLogin1DTO jsonResp = GSON.fromJson(resp, CloudLogin1DTO.class);
432             final String sign = jsonResp != null ? jsonResp.getSign() : null;
433             if (sign != null && !sign.isBlank()) {
434                 logger.trace("Xiaomi Login step 1 sign = {}", sign);
435                 return sign;
436             } else {
437                 logger.debug("Xiaomi Login _sign missing. Maybe still has login cookie.");
438                 return "";
439             }
440         } catch (JsonParseException | IllegalStateException | ClassCastException e) {
441             throw new MiCloudException("Error getting logon sign. Cannot parse response: " + e.getMessage(), e);
442         }
443     }
444
445     private String loginStep2(String sign) throws MiIoCryptoException, InterruptedException, TimeoutException,
446             ExecutionException, MiCloudException, JsonSyntaxException, JsonParseException {
447         String passToken;
448         String cUserId;
449
450         logger.trace("Xiaomi Login step 2");
451         String url = "https://account.xiaomi.com/pass/serviceLoginAuth2";
452         Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
453         request.agent(USERAGENT);
454         request.method(HttpMethod.POST);
455         final ContentResponse responseStep2;
456
457         Fields fields = new Fields();
458         fields.put("sid", "xiaomiio");
459         fields.put("hash", Utils.getHex(MiIoCrypto.md5(password.getBytes())));
460         fields.put("callback", "https://sts.api.io.mi.com/sts");
461         fields.put("qs", "%3Fsid%3Dxiaomiio%26_json%3Dtrue");
462         fields.put("user", username);
463         if (!sign.isEmpty()) {
464             fields.put("_sign", sign);
465         }
466         fields.put("_json", "true");
467
468         request.content(new FormContentProvider(fields));
469         responseStep2 = request.send();
470
471         final String content2 = responseStep2.getContentAsString();
472         logger.trace("Xiaomi login step 2 response = {}", responseStep2);
473         logger.trace("Xiaomi login step 2 content = {}", content2);
474
475         JsonElement resp2 = JsonParser.parseString(parseJson(content2));
476         CloudLoginDTO jsonResp = GSON.fromJson(resp2, CloudLoginDTO.class);
477         if (jsonResp == null) {
478             throw new MiCloudException("Error getting logon details from step 2: " + content2);
479         }
480         ssecurity = jsonResp.getSsecurity();
481         userId = jsonResp.getUserId();
482         cUserId = jsonResp.getcUserId();
483         passToken = jsonResp.getPassToken();
484         String location = jsonResp.getLocation();
485         String code = jsonResp.getCode();
486
487         logger.trace("Xiaomi login ssecurity = {}", ssecurity);
488         logger.trace("Xiaomi login userId = {}", userId);
489         logger.trace("Xiaomi login cUserId = {}", cUserId);
490         logger.trace("Xiaomi login passToken = {}", passToken);
491         logger.trace("Xiaomi login location = {}", location);
492         logger.trace("Xiaomi login code = {}", code);
493         if (0 != jsonResp.getSecurityStatus()) {
494             logger.debug("Xiaomi Cloud Step2 response: {}", parseJson(content2));
495             logger.debug(
496                     """
497                             Xiaomi Login code: {}
498                             SecurityStatus: {}
499                             Pwd code: {}
500                             Location logon URL: {}
501                             In case of login issues check userId/password details are correct.
502                             If login details are correct, try to logon using browser from the openHAB ip using the browser. Alternatively try to complete logon with above URL.\
503                             """,
504                     jsonResp.getCode(), jsonResp.getSecurityStatus(), jsonResp.getPwd(), jsonResp.getLocation());
505         }
506         if (logger.isTraceEnabled()) {
507             dumpCookies(url, false);
508         }
509         if (!location.isEmpty()) {
510             return location;
511         } else {
512             throw new MiCloudException("Error getting logon location URL. Return code: " + code);
513         }
514     }
515
516     private ContentResponse loginStep3(String location)
517             throws MalformedURLException, InterruptedException, TimeoutException, ExecutionException {
518         final ContentResponse responseStep3;
519         Request request;
520         logger.trace("Xiaomi Login step 3 @ {}", (new URL(location)).getHost());
521         request = httpClient.newRequest(location).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
522         request.agent(USERAGENT);
523         request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
524         responseStep3 = request.send();
525         logger.trace("Xiaomi login step 3 content = {}", responseStep3.getContentAsString());
526         logger.trace("Xiaomi login step 3 response = {}", responseStep3);
527         if (logger.isTraceEnabled()) {
528             dumpCookies(location, false);
529         }
530         URI uri = URI.create("http://sts.api.io.mi.com");
531         String serviceToken = extractServiceToken(uri);
532         if (!serviceToken.isEmpty()) {
533             this.serviceToken = serviceToken;
534         }
535         return responseStep3;
536     }
537
538     private void dumpCookies(String url, boolean delete) {
539         if (logger.isTraceEnabled()) {
540             try {
541                 URI uri = URI.create(url);
542                 logger.trace("Cookie dump for {}", uri);
543                 CookieStore cs = httpClient.getCookieStore();
544                 if (cs != null) {
545                     List<HttpCookie> cookies = cs.get(uri);
546                     for (HttpCookie cookie : cookies) {
547                         logger.trace("Cookie ({}) : {} --> {}     (path: {}. Removed: {})", cookie.getDomain(),
548                                 cookie.getName(), cookie.getValue(), cookie.getPath(), delete);
549                         if (delete) {
550                             cs.remove(uri, cookie);
551                         }
552                     }
553                 } else {
554                     logger.trace("Could not create cookiestore from {}", url);
555                 }
556             } catch (IllegalArgumentException e) {
557                 logger.trace("Error dumping cookies from {}: {}", url, e.getMessage(), e);
558             }
559         }
560     }
561
562     private String extractServiceToken(URI uri) {
563         String serviceToken = "";
564         List<HttpCookie> cookies = httpClient.getCookieStore().get(uri);
565         for (HttpCookie cookie : cookies) {
566             logger.trace("Cookie :{} --> {}", cookie.getName(), cookie.getValue());
567             if (cookie.getName().contentEquals("serviceToken")) {
568                 serviceToken = cookie.getValue();
569                 logger.debug("Xiaomi cloud logon successful.");
570                 logger.trace("Xiaomi cloud servicetoken: {}", serviceToken);
571             }
572         }
573         return serviceToken;
574     }
575
576     public boolean hasLoginToken() {
577         return !serviceToken.isEmpty();
578     }
579 }