]> git.basschouten.com Git - openhab-addons.git/blob
2707c22a27ef61a92734b7081e7e876ca14e29e8
[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 Padding? 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 = 20;
105                 int length;
106                 int current;
107
108                 // Hostname
109                 String st = "" + charArray[i] + "" + charArray[i + 1];
110                 length = Integer.parseInt(st, 16) * 2;
111                 i += 2;
112
113                 StringBuilder hostname = new StringBuilder();
114                 current = i;
115
116                 for (; i < current + length; i = i + 2) {
117                     st = "" + charArray[i] + "" + charArray[i + 1];
118                     hostname.append(st);
119                 }
120
121                 i += 2; // 12
122
123                 // ipAddress
124                 st = "" + charArray[i] + "" + charArray[i + 1];
125                 length = Integer.parseInt(st, 16) * 2;
126                 i += 2;
127
128                 StringBuilder ipAddress = new StringBuilder();
129                 current = i;
130
131                 for (; i < current + length; i = i + 2) {
132                     st = "" + charArray[i] + "" + charArray[i + 1];
133                     ipAddress.append(st);
134                 }
135
136                 st = "" + charArray[i] + "" + charArray[i + 1];
137                 while (!DELIMITER_22.equals(st)) {
138                     i += 2;
139                     st = "" + charArray[i] + "" + charArray[i + 1];
140                 }
141
142                 i += 2; // 22
143
144                 // deviceId
145
146                 st = "" + charArray[i] + "" + charArray[i + 1];
147                 length = Integer.parseInt(st, 16) * 2;
148                 i += 2;
149
150                 StringBuilder deviceId = new StringBuilder();
151                 current = i;
152
153                 for (; i < current + length; i = i + 2) {
154                     st = "" + charArray[i] + "" + charArray[i + 1];
155                     deviceId.append(st);
156                 }
157
158                 // architectures
159                 st = "" + charArray[i] + "" + charArray[i + 1];
160                 StringBuilder arch = new StringBuilder();
161                 while (DELIMITER_2A.equals(st)) {
162                     i += 2;
163                     st = "" + charArray[i] + "" + charArray[i + 1];
164                     length = Integer.parseInt(st, 16) * 2;
165                     i += 2;
166                     current = i;
167                     for (; i < current + length; i = i + 2) {
168                         st = "" + charArray[i] + "" + charArray[i + 1];
169                         arch.append(st);
170                     }
171                     st = "" + charArray[i] + "" + charArray[i + 1];
172                     if (DELIMITER_2A.equals(st)) {
173                         arch.append("2c");
174                     }
175                 }
176
177                 String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
178                 String encIpAddress = ShieldTVRequest.encodeMessage(ipAddress.toString());
179                 String encDeviceId = ShieldTVRequest.encodeMessage(deviceId.toString());
180                 String encArch = ShieldTVRequest.encodeMessage(arch.toString());
181                 logger.debug("{} - Hostname: {} - ipAddress: {} - deviceId: {} - arch: {}", thingId, encHostname,
182                         encIpAddress, encDeviceId, encArch);
183                 callback.setHostName(encHostname);
184                 callback.setDeviceID(encDeviceId);
185                 callback.setArch(encArch);
186             } else if (APP_START_SUCCESS.equals(msg)) {
187                 // App successfully started
188                 logger.debug("{} -  App started successfully", thingId);
189             } else if (APP_START_FAILED.equals(msg)) {
190                 // App failed to start
191                 logger.debug("{} - App failed to start", thingId);
192             } else if (msg.startsWith(MESSAGE_APPDB) && msg.startsWith(DELIMITER_0A, 18)) {
193                 // Individual update?
194                 // 08f10712 5808061254 0a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 300118f107
195                 logger.info("{} - Individual App Update - Please Report This: {}", thingId, msg);
196             } else if (msg.startsWith(MESSAGE_APPDB) && (msg.length() > 30)) {
197                 // Massive dump of currently installed apps
198                 // 08f10712 d81f080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
199                 Map<String, String> appNameDB = new HashMap<>();
200                 Map<String, String> appURLDB = new HashMap<>();
201                 int appCount = 0;
202                 int i = 18;
203                 String st = "";
204                 int length;
205                 int current;
206                 StringBuilder appSBPrepend = new StringBuilder();
207                 StringBuilder appSBDN = new StringBuilder();
208
209                 // Load default apps that don't get sent in payload
210
211                 appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
212                 appURLDB.put("com.google.android.tvlauncher", "");
213
214                 appNameDB.put("com.google.android.katniss", "Google app for Android TV");
215                 appURLDB.put("com.google.android.katniss", "");
216
217                 appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
218                 appURLDB.put("com.google.android.katnisspx", "");
219
220                 appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
221                 appURLDB.put("com.google.android.backdrop", "");
222
223                 // Packet will end with 300118f107 after last entry
224
225                 while (i < msg.length() - 10) {
226                     StringBuilder appSBName = new StringBuilder();
227                     StringBuilder appSBURL = new StringBuilder();
228
229                     // There are instances such as plex where multiple apps are sent as part of the same payload
230                     // This is identified when 12 is the beginning of the set
231
232                     st = "" + charArray[i] + "" + charArray[i + 1];
233
234                     if (!DELIMITER_12.equals(st)) {
235                         appSBPrepend = new StringBuilder();
236                         appSBDN = new StringBuilder();
237
238                         appCount++;
239
240                         // App Prepend
241                         // Usually 10 in length but can be longer or shorter so look for 0a twice
242                         do {
243                             st = "" + charArray[i] + "" + charArray[i + 1];
244                             appSBPrepend.append(st);
245                             i += 2;
246                         } while (!DELIMITER_0A.equals(st));
247                         do {
248                             st = "" + charArray[i] + "" + charArray[i + 1];
249                             appSBPrepend.append(st);
250                             i += 2;
251                         } while (!DELIMITER_0A.equals(st));
252                         st = "" + charArray[i] + "" + charArray[i + 1];
253
254                         // Look for a third 0a, but only if 12 is not down the line
255                         // If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
256                         String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
257                         if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
258                             appSBPrepend.append(st);
259                             i += 2;
260                             st = "" + charArray[i] + "" + charArray[i + 1];
261                         }
262
263                         // app DN
264                         length = Integer.parseInt(st, 16) * 2;
265                         i += 2;
266                         current = i;
267                         for (; i < current + length; i = i + 2) {
268                             st = "" + charArray[i] + "" + charArray[i + 1];
269                             appSBDN.append(st);
270                         }
271                     } else {
272                         logger.trace("Second Entry");
273                     }
274
275                     // App Name
276
277                     i += 2; // 12 delimiter
278                     st = "" + charArray[i] + "" + charArray[i + 1];
279                     i += 2;
280                     length = Integer.parseInt(st, 16) * 2;
281                     current = i;
282                     for (; i < current + length; i = i + 2) {
283                         st = "" + charArray[i] + "" + charArray[i + 1];
284                         appSBName.append(st);
285                     }
286
287                     // There are times where there is padding here for no reason beyond the specified length.
288                     // Proceed forward until we get to the 22 delimiter
289
290                     st = "" + charArray[i] + "" + charArray[i + 1];
291                     while (!DELIMITER_22.equals(st)) {
292                         i += 2;
293                         st = "" + charArray[i] + "" + charArray[i + 1];
294                     }
295
296                     // App URL
297                     i += 2; // 22 delimiter
298                     st = "" + charArray[i] + "" + charArray[i + 1];
299                     i += 2;
300                     length = Integer.parseInt(st, 16) * 2;
301                     current = i;
302                     for (; i < current + length; i = i + 2) {
303                         st = "" + charArray[i] + "" + charArray[i + 1];
304                         appSBURL.append(st);
305                     }
306                     st = "" + charArray[i] + "" + charArray[i + 1];
307                     if (!DELIMITER_12.equals(st)) {
308                         i += 4; // terminates 2801
309                     }
310                     String appPrepend = appSBPrepend.toString();
311                     String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
312                     String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
313                     String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
314                     logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
315                             appName, appURL);
316                     appNameDB.put(appDN, appName);
317                     appURLDB.put(appDN, appURL);
318                 }
319                 if (appCount > 0) {
320                     Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
321                     List<String> valueList = new ArrayList<>();
322                     for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
323                         valueList.add(entry.getValue());
324                     }
325                     Collections.sort(valueList);
326                     for (String str : valueList) {
327                         for (Entry<String, String> entry : appNameDB.entrySet()) {
328                             if (entry.getValue().equals(str)) {
329                                 sortedAppNameDB.put(entry.getKey(), str);
330                             }
331                         }
332                     }
333
334                     logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
335                             appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
336                     callback.setAppDB(sortedAppNameDB, appURLDB);
337                 } else {
338                     logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
339                             appNameDB.toString(), appURLDB.toString());
340                 }
341             } else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
342                 // This has something to do with successful command response, maybe.
343             } else if (KEEPALIVE_REPLY.equals(msg)) {
344                 // Keepalive Reply
345             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
346                 // 080a 12 0308cf08 180a
347                 logger.debug("PIN Process Started");
348             } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
349                 // This seems to be 20**** when observed. It is unclear what this does.
350                 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
351             } else if (msg.startsWith(MESSAGE_SUCCESS)) {
352                 // Successful command received
353                 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
354                 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
355                 //
356                 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
357                 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
358                 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
359                 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
360                 callback.setLoggedIn(true);
361             } else if (TIMEOUT.equals(msg)) {
362                 // Timeout
363                 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
364                 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
365                 logger.debug("{} - Timeout {}", thingId, msg);
366             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
367                 // Get current app command successful. Usually paired with 0807 reply below.
368             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
369                 // Current App
370                 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
371                 // 18ec07
372                 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
373                 StringBuilder appName = new StringBuilder();
374                 String lengthStr = "" + charArray[34] + charArray[35];
375                 int length = Integer.parseInt(lengthStr, 16) * 2;
376                 for (int i = 36; i < 36 + length; i++) {
377                     appName.append(charArray[i]);
378                 }
379                 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
380                 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
381             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
382                 // Certificate Reply
383                 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
384                 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
385                 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
386                 // |--------Little Endian Total Payload Length
387                 // |-----------------------Little Endian Remaining Payload Length
388                 // |-----------------------------------Length of SUCCESS
389                 // |--------------------------------------ASCII: SUCCESS
390                 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
391                 // |------------------------------------------------------------Priv Key RSA 2048
392                 // |--------------------------------------------------------------------Cert X.509
393                 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
394                     StringBuilder preamble = new StringBuilder();
395                     StringBuilder privKey = new StringBuilder();
396                     StringBuilder pubKey = new StringBuilder();
397                     int i = 0;
398                     int current;
399                     for (; i < 44; i++) {
400                         preamble.append(charArray[i]);
401                     }
402                     logger.trace("{} - Cert Preamble:   {}", thingId, preamble.toString());
403
404                     i += 2; // 1a
405                     String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
406                             + charArray[i + 1];
407                     int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
408                     i += 4; // length
409                     current = i;
410
411                     logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
412
413                     for (; i < current + privLen; i++) {
414                         privKey.append(charArray[i]);
415                     }
416
417                     logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
418
419                     for (; i < msg.length() - 4; i++) {
420                         pubKey.append(charArray[i]);
421                     }
422
423                     logger.trace("{} - Cert pubKey:  {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
424
425                     logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
426                             msg.length() - privLen - 4);
427
428                     byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
429                     byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
430
431                     String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
432                     String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
433
434                     callback.setKeys(privKeyB64, pubKeyB64);
435                 } else {
436                     logger.info("{} - Pin Process Failed.", thingId);
437                 }
438             } else {
439                 logger.info("{} - Unknown payload received. {}", thingId, msg);
440             }
441         } catch (Exception e) {
442             logger.info("{} - Message Parser Caught Exception", thingId, e);
443         }
444     }
445 }