]> git.basschouten.com Git - openhab-addons.git/blob
002191e92c8d06bfb3606cbdf4bbde01600aca2b
[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.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(MESSAGE_APPDB_FULL, 12)) {
208                 // Massive dump of currently installed apps
209                 // 08f10712 d81f 080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
210                 // --------------08XX12 where XX is not 01 are individual updates and should be ignored
211                 Map<String, String> appNameDB = new HashMap<>();
212                 Map<String, String> appURLDB = new HashMap<>();
213                 int appCount = 0;
214                 int i = 18;
215                 String st = "";
216                 int length;
217                 int current;
218                 StringBuilder appSBPrepend = new StringBuilder();
219                 StringBuilder appSBDN = new StringBuilder();
220
221                 // Load default apps that don't get sent in payload
222
223                 appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
224                 appURLDB.put("com.google.android.tvlauncher", "");
225
226                 appNameDB.put("com.google.android.katniss", "Google app for Android TV");
227                 appURLDB.put("com.google.android.katniss", "");
228
229                 appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
230                 appURLDB.put("com.google.android.katnisspx", "");
231
232                 appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
233                 appURLDB.put("com.google.android.backdrop", "");
234
235                 // Packet will end with 300118f107 after last entry
236
237                 while (i < msg.length() - 10) {
238                     StringBuilder appSBName = new StringBuilder();
239                     StringBuilder appSBURL = new StringBuilder();
240
241                     // There are instances such as plex where multiple apps are sent as part of the same payload
242                     // This is identified when 12 is the beginning of the set
243
244                     st = "" + charArray[i] + "" + charArray[i + 1];
245
246                     if (!DELIMITER_12.equals(st)) {
247                         appSBPrepend = new StringBuilder();
248                         appSBDN = new StringBuilder();
249
250                         appCount++;
251
252                         // App Prepend
253                         // Usually 10 in length but can be longer or shorter so look for 0a twice
254                         do {
255                             st = "" + charArray[i] + "" + charArray[i + 1];
256                             appSBPrepend.append(st);
257                             i += 2;
258                         } while (!DELIMITER_0A.equals(st));
259                         do {
260                             st = "" + charArray[i] + "" + charArray[i + 1];
261                             appSBPrepend.append(st);
262                             i += 2;
263                         } while (!DELIMITER_0A.equals(st));
264                         st = "" + charArray[i] + "" + charArray[i + 1];
265
266                         // Look for a third 0a, but only if 12 is not down the line
267                         // If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
268                         String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
269                         if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
270                             appSBPrepend.append(st);
271                             i += 2;
272                             st = "" + charArray[i] + "" + charArray[i + 1];
273                         }
274
275                         // app DN
276                         length = Integer.parseInt(st, 16) * 2;
277                         i += 2;
278                         current = i;
279                         for (; i < current + length; i = i + 2) {
280                             st = "" + charArray[i] + "" + charArray[i + 1];
281                             appSBDN.append(st);
282                         }
283                     } else {
284                         logger.trace("Second Entry");
285                     }
286
287                     // App Name
288
289                     i += 2; // 12 delimiter
290                     st = "" + charArray[i] + "" + charArray[i + 1];
291                     i += 2;
292                     length = Integer.parseInt(st, 16) * 2;
293                     current = i;
294                     for (; i < current + length; i = i + 2) {
295                         st = "" + charArray[i] + "" + charArray[i + 1];
296                         appSBName.append(st);
297                     }
298
299                     // There are times where there is padding here for no reason beyond the specified length.
300                     // Proceed forward until we get to the 22 delimiter
301
302                     st = "" + charArray[i] + "" + charArray[i + 1];
303                     while (!DELIMITER_22.equals(st)) {
304                         i += 2;
305                         st = "" + charArray[i] + "" + charArray[i + 1];
306                     }
307
308                     // App URL
309                     i += 2; // 22 delimiter
310                     st = "" + charArray[i] + "" + charArray[i + 1];
311                     i += 2;
312                     length = Integer.parseInt(st, 16) * 2;
313                     current = i;
314                     for (; i < current + length; i = i + 2) {
315                         st = "" + charArray[i] + "" + charArray[i + 1];
316                         appSBURL.append(st);
317                     }
318                     st = "" + charArray[i] + "" + charArray[i + 1];
319                     if (!DELIMITER_12.equals(st)) {
320                         i += 4; // terminates 2801
321                     }
322                     String appPrepend = appSBPrepend.toString();
323                     String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
324                     String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
325                     String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
326                     logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
327                             appName, appURL);
328                     appNameDB.put(appDN, appName);
329                     appURLDB.put(appDN, appURL);
330                 }
331                 if (appCount > 0) {
332                     Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
333                     List<String> valueList = new ArrayList<>();
334                     for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
335                         valueList.add(entry.getValue());
336                     }
337                     Collections.sort(valueList);
338                     for (String str : valueList) {
339                         for (Entry<String, String> entry : appNameDB.entrySet()) {
340                             if (entry.getValue().equals(str)) {
341                                 sortedAppNameDB.put(entry.getKey(), str);
342                             }
343                         }
344                     }
345
346                     logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
347                             appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
348                     callback.setAppDB(sortedAppNameDB, appURLDB);
349                 } else {
350                     logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
351                             appNameDB.toString(), appURLDB.toString());
352                 }
353             } else if (msg.startsWith(MESSAGE_APPDB)) {
354                 logger.debug("{} - Individual app update ignored {}", thingId, msg);
355             } else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
356                 // This has something to do with successful command response, maybe.
357                 logger.trace("{} - Good Command Response", thingId);
358             } else if (KEEPALIVE_REPLY.equals(msg)) {
359                 // Keepalive Reply
360                 logger.trace("{} - Keepalive Reply", thingId);
361             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
362                 // 080a 12 0308cf08 180a
363                 logger.debug("PIN Process Started");
364             } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
365                 // This seems to be 20**** when observed. It is unclear what this does.
366                 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
367                 logger.trace("{} - Certificate To Follow", thingId);
368             } else if (msg.startsWith(MESSAGE_SUCCESS)) {
369                 // Successful command received
370                 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
371                 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
372                 //
373                 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
374                 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
375                 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
376                 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
377                 callback.setLoggedIn(true);
378             } else if (TIMEOUT.equals(msg)) {
379                 // Timeout
380                 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
381                 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
382                 logger.debug("{} - Timeout {}", thingId, msg);
383             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
384                 // Get current app command successful. Usually paired with 0807 reply below.
385                 logger.trace("{} - Current App Request Successful", thingId);
386             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
387                 // Current App
388                 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
389                 // 18ec07
390                 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
391                 StringBuilder appName = new StringBuilder();
392                 String lengthStr = "" + charArray[34] + charArray[35];
393                 int length = Integer.parseInt(lengthStr, 16) * 2;
394                 for (int i = 36; i < 36 + length; i++) {
395                     appName.append(charArray[i]);
396                 }
397                 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
398                 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
399             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
400                 // Certificate Reply
401                 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
402                 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
403                 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
404                 // |--------Little Endian Total Payload Length
405                 // |-----------------------Little Endian Remaining Payload Length
406                 // |-----------------------------------Length of SUCCESS
407                 // |--------------------------------------ASCII: SUCCESS
408                 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
409                 // |------------------------------------------------------------Priv Key RSA 2048
410                 // |--------------------------------------------------------------------Cert X.509
411                 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
412                     StringBuilder preamble = new StringBuilder();
413                     StringBuilder privKey = new StringBuilder();
414                     StringBuilder pubKey = new StringBuilder();
415                     int i = 0;
416                     int current;
417                     for (; i < 44; i++) {
418                         preamble.append(charArray[i]);
419                     }
420                     logger.trace("{} - Cert Preamble:   {}", thingId, preamble.toString());
421
422                     i += 2; // 1a
423                     String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
424                             + charArray[i + 1];
425                     int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
426                     i += 4; // length
427                     current = i;
428
429                     logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
430
431                     for (; i < current + privLen; i++) {
432                         privKey.append(charArray[i]);
433                     }
434
435                     logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
436
437                     for (; i < msg.length() - 4; i++) {
438                         pubKey.append(charArray[i]);
439                     }
440
441                     logger.trace("{} - Cert pubKey:  {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
442
443                     logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
444                             msg.length() - privLen - 4);
445
446                     byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
447                     byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
448
449                     String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
450                     String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
451
452                     callback.setKeys(privKeyB64, pubKeyB64);
453                 } else {
454                     logger.info("{} - Pin Process Failed.", thingId);
455                 }
456             } else {
457                 logger.info("{} - Unknown payload received. {}", thingId, msg);
458             }
459         } catch (Exception e) {
460             logger.warn("{} - Message Parser Exception on {}", thingId, msg);
461             logger.warn("{} - Message Parser Caught Exception", thingId, e);
462         }
463     }
464 }