]> git.basschouten.com Git - openhab-addons.git/blob
f78bc388ce1727346aa24cebc1cdc4780b3aed87
[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                 logger.trace("{} - Good Command Response", thingId);
344             } else if (KEEPALIVE_REPLY.equals(msg)) {
345                 // Keepalive Reply
346                 logger.trace("{} - Keepalive Reply", thingId);
347             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
348                 // 080a 12 0308cf08 180a
349                 logger.debug("PIN Process Started");
350             } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
351                 // This seems to be 20**** when observed. It is unclear what this does.
352                 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
353                 logger.trace("{} - Certificate To Follow", thingId);
354             } else if (msg.startsWith(MESSAGE_SUCCESS)) {
355                 // Successful command received
356                 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
357                 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
358                 //
359                 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
360                 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
361                 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
362                 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
363                 callback.setLoggedIn(true);
364             } else if (TIMEOUT.equals(msg)) {
365                 // Timeout
366                 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
367                 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
368                 logger.debug("{} - Timeout {}", thingId, msg);
369             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
370                 // Get current app command successful. Usually paired with 0807 reply below.
371                 logger.trace("{} - Current App Request Successful", thingId);
372             } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
373                 // Current App
374                 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
375                 // 18ec07
376                 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
377                 StringBuilder appName = new StringBuilder();
378                 String lengthStr = "" + charArray[34] + charArray[35];
379                 int length = Integer.parseInt(lengthStr, 16) * 2;
380                 for (int i = 36; i < 36 + length; i++) {
381                     appName.append(charArray[i]);
382                 }
383                 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
384                 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
385             } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
386                 // Certificate Reply
387                 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
388                 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
389                 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
390                 // |--------Little Endian Total Payload Length
391                 // |-----------------------Little Endian Remaining Payload Length
392                 // |-----------------------------------Length of SUCCESS
393                 // |--------------------------------------ASCII: SUCCESS
394                 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
395                 // |------------------------------------------------------------Priv Key RSA 2048
396                 // |--------------------------------------------------------------------Cert X.509
397                 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
398                     StringBuilder preamble = new StringBuilder();
399                     StringBuilder privKey = new StringBuilder();
400                     StringBuilder pubKey = new StringBuilder();
401                     int i = 0;
402                     int current;
403                     for (; i < 44; i++) {
404                         preamble.append(charArray[i]);
405                     }
406                     logger.trace("{} - Cert Preamble:   {}", thingId, preamble.toString());
407
408                     i += 2; // 1a
409                     String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
410                             + charArray[i + 1];
411                     int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
412                     i += 4; // length
413                     current = i;
414
415                     logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
416
417                     for (; i < current + privLen; i++) {
418                         privKey.append(charArray[i]);
419                     }
420
421                     logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
422
423                     for (; i < msg.length() - 4; i++) {
424                         pubKey.append(charArray[i]);
425                     }
426
427                     logger.trace("{} - Cert pubKey:  {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
428
429                     logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
430                             msg.length() - privLen - 4);
431
432                     byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
433                     byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
434
435                     String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
436                     String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
437
438                     callback.setKeys(privKeyB64, pubKeyB64);
439                 } else {
440                     logger.info("{} - Pin Process Failed.", thingId);
441                 }
442             } else {
443                 logger.info("{} - Unknown payload received. {}", thingId, msg);
444             }
445         } catch (Exception e) {
446             logger.info("{} - Message Parser Caught Exception", thingId, e);
447         }
448     }
449 }