]> git.basschouten.com Git - openhab-addons.git/blob
9c512010a405c71059709b74efd299fa9559db51
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.androidtv.internal.protocol.shieldtv;
14
15 import static org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConstants.*;
16
17 import java.util.ArrayList;
18 import java.util.Base64;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.LinkedHashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Map.Entry;
25
26 import javax.xml.bind.DatatypeConverter;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * Class responsible for parsing incoming ShieldTV messages. Calls back to an object implementing the
34  * ShieldTVMessageParserCallbacks interface.
35  *
36  * Adapted from Lutron Leap binding
37  *
38  * @author Ben Rosenblum - Initial contribution
39  */
40
41 @NonNullByDefault
42 public class ShieldTVMessageParser {
43     private final Logger logger = LoggerFactory.getLogger(ShieldTVMessageParser.class);
44
45     private final ShieldTVConnectionManager callback;
46
47     public ShieldTVMessageParser(ShieldTVConnectionManager callback) {
48         this.callback = callback;
49     }
50
51     public void handleMessage(String msg) {
52         if (msg.trim().isEmpty()) {
53             return; // Ignore empty lines
54         }
55
56         String thingId = callback.getThingID();
57         String hostName = callback.getHostName();
58         logger.trace("{} - Received ShieldTV message from: {} - Message: {}", thingId, hostName, msg);
59
60         callback.validMessageReceived();
61
62         char[] charArray = msg.toCharArray();
63
64         try {
65             // All lengths are little endian when larger than 0xff
66             if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_SHORTNAME, 8)) {
67                 // Pre-login Hostname of Shield Replied
68                 // 080a 12 1408e807 12 0f08e807 12 LEN Hostname 18d7fd04 180a
69                 // 080a 12 1d08e807 12 180801 12 LEN Hostname 18d7fd04 180a
70                 // 080a 12 2208e807 12 1d08e807 12 LEN Hostname 18d7fd04 180a
71                 // Each chunk ends in 12
72                 // 4th chunk represent length of the name.
73                 // 5th chunk is the name
74                 int chunk = 0;
75                 int i = 0;
76                 String st = "";
77                 StringBuilder hostname = new StringBuilder();
78                 while (chunk < 3) {
79                     st = "" + charArray[i] + "" + charArray[i + 1];
80                     if (DELIMITER_12.equals(st)) {
81                         chunk++;
82                     }
83                     i += 2;
84                 }
85                 st = "" + charArray[i] + "" + charArray[i + 1];
86                 i += 2;
87                 int length = Integer.parseInt(st, 16) * 2;
88                 int current = i;
89                 for (; i < current + length; i = i + 2) {
90                     st = "" + charArray[i] + "" + charArray[i + 1];
91                     hostname.append(st);
92                 }
93                 logger.trace("{} - Shield Hostname: {} {}", thingId, hostname, length);
94                 String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
95                 logger.debug("{} - Shield Hostname Encoded: {}", thingId, encHostname);
96                 callback.setHostName(encHostname);
97             } else if (msg.startsWith(MESSAGE_HOSTNAME)) {
98                 // Longer hostname reply
99                 // 080b 12 5b08b510 12 TOTALLEN? 0a LEN Hostname 12 LEN IPADDR 18 9b46 22 LEN DeviceID 2a LEN arm64-v8a
100                 // 2a LEN armeabi-v7a 2a LEN armeabi 180b
101                 // It's possible for there to be more or less of the arm lists
102                 logger.trace("{} - Longer Hostname Reply", thingId);
103
104                 int i = 18;
105                 int length;
106                 int current;
107
108                 StringBuilder hostname = new StringBuilder();
109                 StringBuilder ipAddress = new StringBuilder();
110                 StringBuilder deviceId = new StringBuilder();
111                 StringBuilder arch = new StringBuilder();
112
113                 String st = "" + charArray[i] + "" + charArray[i + 1];
114
115                 if (DELIMITER_0A.equals(st)) {
116                     i += 2; // 0a
117                     // Hostname
118                     st = "" + charArray[i] + "" + charArray[i + 1];
119                     length = Integer.parseInt(st, 16) * 2;
120                     i += 2;
121
122                     current = i;
123
124                     for (; i < current + length; i = i + 2) {
125                         st = "" + charArray[i] + "" + charArray[i + 1];
126                         hostname.append(st);
127                     }
128                 }
129                 st = "" + charArray[i] + "" + charArray[i + 1];
130
131                 if (DELIMITER_12.equals(st)) {
132                     i += 2; // 12
133
134                     // ipAddress
135                     st = "" + charArray[i] + "" + charArray[i + 1];
136                     length = Integer.parseInt(st, 16) * 2;
137                     i += 2;
138
139                     current = i;
140
141                     for (; i < current + length; i = i + 2) {
142                         st = "" + charArray[i] + "" + charArray[i + 1];
143                         ipAddress.append(st);
144                     }
145                 }
146
147                 st = "" + charArray[i] + "" + charArray[i + 1];
148                 if (DELIMITER_18.equals(st)) {
149                     while (!DELIMITER_22.equals(st)) {
150                         i += 2;
151                         st = "" + charArray[i] + "" + charArray[i + 1];
152                     }
153                 }
154
155                 st = "" + charArray[i] + "" + charArray[i + 1];
156                 if (DELIMITER_22.equals(st)) {
157                     i += 2; // 22
158
159                     // deviceId
160
161                     st = "" + charArray[i] + "" + charArray[i + 1];
162                     length = Integer.parseInt(st, 16) * 2;
163                     i += 2;
164
165                     current = i;
166
167                     for (; i < current + length; i = i + 2) {
168                         st = "" + charArray[i] + "" + charArray[i + 1];
169                         deviceId.append(st);
170                     }
171                 }
172
173                 // architectures
174                 st = "" + charArray[i] + "" + charArray[i + 1];
175                 if (DELIMITER_2A.equals(st)) {
176                     while (DELIMITER_2A.equals(st)) {
177                         i += 2;
178                         st = "" + charArray[i] + "" + charArray[i + 1];
179                         length = Integer.parseInt(st, 16) * 2;
180                         i += 2;
181                         current = i;
182                         for (; i < current + length; i = i + 2) {
183                             st = "" + charArray[i] + "" + charArray[i + 1];
184                             arch.append(st);
185                         }
186                         st = "" + charArray[i] + "" + charArray[i + 1];
187                         if (DELIMITER_2A.equals(st)) {
188                             arch.append("2c");
189                         }
190                     }
191                 }
192                 String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
193                 String encIpAddress = ShieldTVRequest.encodeMessage(ipAddress.toString());
194                 String encDeviceId = ShieldTVRequest.encodeMessage(deviceId.toString());
195                 String encArch = ShieldTVRequest.encodeMessage(arch.toString());
196                 logger.debug("{} - Hostname: {} - ipAddress: {} - deviceId: {} - arch: {}", thingId, encHostname,
197                         encIpAddress, encDeviceId, encArch);
198                 callback.setHostName(encHostname);
199                 callback.setDeviceID(encDeviceId);
200                 callback.setArch(encArch);
201             } else if (APP_START_SUCCESS.equals(msg)) {
202                 // App successfully started
203                 logger.debug("{} -  App started successfully", thingId);
204             } else if (APP_START_FAILED.equals(msg)) {
205                 // App failed to start
206                 logger.debug("{} - App failed to start", thingId);
207             } else if (msg.startsWith(MESSAGE_APPDB) && msg.startsWith(DELIMITER_0A, 18)) {
208                 // Individual update?
209                 // 08f10712 5808061254 0a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 300118f107
210                 logger.info("{} - Individual App Update - Please Report This: {}", thingId, msg);
211             } else if (msg.startsWith(MESSAGE_APPDB) && (msg.length() > 30)) {
212                 // Massive dump of currently installed apps
213                 // 08f10712 d81f080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
214                 Map<String, String> appNameDB = new HashMap<>();
215                 Map<String, String> appURLDB = new HashMap<>();
216                 int appCount = 0;
217                 int i = 18;
218                 String st = "";
219                 int length;
220                 int current;
221                 StringBuilder appSBPrepend = new StringBuilder();
222                 StringBuilder appSBDN = new StringBuilder();
223
224                 // Load default apps that don't get sent in payload
225
226                 appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
227                 appURLDB.put("com.google.android.tvlauncher", "");
228
229                 appNameDB.put("com.google.android.katniss", "Google app for Android TV");
230                 appURLDB.put("com.google.android.katniss", "");
231
232                 appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
233                 appURLDB.put("com.google.android.katnisspx", "");
234
235                 appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
236                 appURLDB.put("com.google.android.backdrop", "");
237
238                 // Packet will end with 300118f107 after last entry
239
240                 while (i < msg.length() - 10) {
241                     StringBuilder appSBName = new StringBuilder();
242                     StringBuilder appSBURL = new StringBuilder();
243
244                     // There are instances such as plex where multiple apps are sent as part of the same payload
245                     // This is identified when 12 is the beginning of the set
246
247                     st = "" + charArray[i] + "" + charArray[i + 1];
248
249                     if (!DELIMITER_12.equals(st)) {
250                         appSBPrepend = new StringBuilder();
251                         appSBDN = new StringBuilder();
252
253                         appCount++;
254
255                         // App Prepend
256                         // Usually 10 in length but can be longer or shorter so look for 0a twice
257                         do {
258                             st = "" + charArray[i] + "" + charArray[i + 1];
259                             appSBPrepend.append(st);
260                             i += 2;
261                         } while (!DELIMITER_0A.equals(st));
262                         do {
263                             st = "" + charArray[i] + "" + charArray[i + 1];
264                             appSBPrepend.append(st);
265                             i += 2;
266                         } while (!DELIMITER_0A.equals(st));
267                         st = "" + charArray[i] + "" + charArray[i + 1];
268
269                         // Look for a third 0a, but only if 12 is not down the line
270                         // If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
271                         String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
272                         if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
273                             appSBPrepend.append(st);
274                             i += 2;
275                             st = "" + charArray[i] + "" + charArray[i + 1];
276                         }
277
278                         // app DN
279                         length = Integer.parseInt(st, 16) * 2;
280                         i += 2;
281                         current = i;
282                         for (; i < current + length; i = i + 2) {
283                             st = "" + charArray[i] + "" + charArray[i + 1];
284                             appSBDN.append(st);
285                         }
286                     } else {
287                         logger.trace("Second Entry");
288                     }
289
290                     // App Name
291
292                     i += 2; // 12 delimiter
293                     st = "" + charArray[i] + "" + charArray[i + 1];
294                     i += 2;
295                     length = Integer.parseInt(st, 16) * 2;
296                     current = i;
297                     for (; i < current + length; i = i + 2) {
298                         st = "" + charArray[i] + "" + charArray[i + 1];
299                         appSBName.append(st);
300                     }
301
302                     // There are times where there is padding here for no reason beyond the specified length.
303                     // Proceed forward until we get to the 22 delimiter
304
305                     st = "" + charArray[i] + "" + charArray[i + 1];
306                     while (!DELIMITER_22.equals(st)) {
307                         i += 2;
308                         st = "" + charArray[i] + "" + charArray[i + 1];
309                     }
310
311                     // App URL
312                     i += 2; // 22 delimiter
313                     st = "" + charArray[i] + "" + charArray[i + 1];
314                     i += 2;
315                     length = Integer.parseInt(st, 16) * 2;
316                     current = i;
317                     for (; i < current + length; i = i + 2) {
318                         st = "" + charArray[i] + "" + charArray[i + 1];
319                         appSBURL.append(st);
320                     }
321                     st = "" + charArray[i] + "" + charArray[i + 1];
322                     if (!DELIMITER_12.equals(st)) {
323                         i += 4; // terminates 2801
324                     }
325                     String appPrepend = appSBPrepend.toString();
326                     String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
327                     String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
328                     String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
329                     logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
330                             appName, appURL);
331                     appNameDB.put(appDN, appName);
332                     appURLDB.put(appDN, appURL);
333                 }
334                 if (appCount > 0) {
335                     Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
336                     List<String> valueList = new ArrayList<>();
337                     for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
338                         valueList.add(entry.getValue());
339                     }
340                     Collections.sort(valueList);
341                     for (String str : valueList) {
342                         for (Entry<String, String> entry : appNameDB.entrySet()) {
343                             if (entry.getValue().equals(str)) {
344                                 sortedAppNameDB.put(entry.getKey(), str);
345                             }
346                         }
347                     }
348
349                     logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
350                             appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
351                     callback.setAppDB(sortedAppNameDB, appURLDB);
352                 } else {
353                     logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
354                             appNameDB.toString(), appURLDB.toString());
355                 }
356             } else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
357                 // This has something to do with successful command response, maybe.
358                 logger.trace("{} - Good Command Response", thingId);
359             } else if (KEEPALIVE_REPLY.equals(msg)) {
360                 // Keepalive Reply
361                 logger.trace("{} - Keepalive Reply", thingId);
362             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
363                 // 080a 12 0308cf08 180a
364                 logger.debug("PIN Process Started");
365             } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
366                 // This seems to be 20**** when observed. It is unclear what this does.
367                 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
368                 logger.trace("{} - Certificate To Follow", thingId);
369             } else if (msg.startsWith(MESSAGE_SUCCESS)) {
370                 // Successful command received
371                 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
372                 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
373                 //
374                 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
375                 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
376                 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
377                 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
378                 callback.setLoggedIn(true);
379             } else if (TIMEOUT.equals(msg)) {
380                 // Timeout
381                 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
382                 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
383                 logger.debug("{} - Timeout {}", thingId, msg);
384             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
385                 // Get current app command successful. Usually paired with 0807 reply below.
386                 logger.trace("{} - Current App Request Successful", thingId);
387             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
388                 // Current App
389                 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
390                 // 18ec07
391                 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
392                 StringBuilder appName = new StringBuilder();
393                 String lengthStr = "" + charArray[34] + charArray[35];
394                 int length = Integer.parseInt(lengthStr, 16) * 2;
395                 for (int i = 36; i < 36 + length; i++) {
396                     appName.append(charArray[i]);
397                 }
398                 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
399                 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
400             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
401                 // Certificate Reply
402                 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
403                 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
404                 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
405                 // |--------Little Endian Total Payload Length
406                 // |-----------------------Little Endian Remaining Payload Length
407                 // |-----------------------------------Length of SUCCESS
408                 // |--------------------------------------ASCII: SUCCESS
409                 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
410                 // |------------------------------------------------------------Priv Key RSA 2048
411                 // |--------------------------------------------------------------------Cert X.509
412                 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
413                     StringBuilder preamble = new StringBuilder();
414                     StringBuilder privKey = new StringBuilder();
415                     StringBuilder pubKey = new StringBuilder();
416                     int i = 0;
417                     int current;
418                     for (; i < 44; i++) {
419                         preamble.append(charArray[i]);
420                     }
421                     logger.trace("{} - Cert Preamble:   {}", thingId, preamble.toString());
422
423                     i += 2; // 1a
424                     String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
425                             + charArray[i + 1];
426                     int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
427                     i += 4; // length
428                     current = i;
429
430                     logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
431
432                     for (; i < current + privLen; i++) {
433                         privKey.append(charArray[i]);
434                     }
435
436                     logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
437
438                     for (; i < msg.length() - 4; i++) {
439                         pubKey.append(charArray[i]);
440                     }
441
442                     logger.trace("{} - Cert pubKey:  {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
443
444                     logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
445                             msg.length() - privLen - 4);
446
447                     byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
448                     byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
449
450                     String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
451                     String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
452
453                     callback.setKeys(privKeyB64, pubKeyB64);
454                 } else {
455                     logger.info("{} - Pin Process Failed.", thingId);
456                 }
457             } else {
458                 logger.info("{} - Unknown payload received. {}", thingId, msg);
459             }
460         } catch (Exception e) {
461             logger.warn("{} - Message Parser Exception on {}", thingId, msg);
462             logger.warn("{} - Message Parser Caught Exception", thingId, e);
463         }
464     }
465 }