]> git.basschouten.com Git - openhab-addons.git/blob
7d75f244545fc2b25dad2eb6df6296a8a89ba2a8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.amazonechocontrol.internal;
14
15 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.*;
16
17 import java.io.IOException;
18 import java.io.UnsupportedEncodingException;
19 import java.net.URISyntaxException;
20 import java.net.URLDecoder;
21 import java.net.URLEncoder;
22 import java.nio.charset.StandardCharsets;
23 import java.util.*;
24 import java.util.stream.Collectors;
25
26 import javax.net.ssl.HttpsURLConnection;
27 import javax.servlet.ServletException;
28 import javax.servlet.http.HttpServlet;
29 import javax.servlet.http.HttpServletRequest;
30 import javax.servlet.http.HttpServletResponse;
31
32 import org.apache.commons.lang.StringEscapeUtils;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.amazonechocontrol.internal.handler.AccountHandler;
36 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
37 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
38 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.PairedDevice;
39 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
40 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonMusicProvider;
41 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonNotificationSound;
42 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists;
43 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonPlaylists.PlayList;
44 import org.openhab.core.thing.Thing;
45 import org.osgi.service.http.HttpService;
46 import org.osgi.service.http.NamespaceException;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51 import com.google.gson.JsonSyntaxException;
52
53 /**
54  * Provides the following functions
55  * --- Login ---
56  * Simple http proxy to forward the login dialog from amazon to the user through the binding
57  * so the user can enter a captcha or other extended login information
58  * --- List of devices ---
59  * Used to get the device information of new devices which are currently not known
60  * --- List of IDs ---
61  * Simple possibility for a user to get the ids needed for writing rules
62  *
63  * @author Michael Geramb - Initial Contribution
64  */
65 @NonNullByDefault
66 public class AccountServlet extends HttpServlet {
67
68     private static final long serialVersionUID = -1453738923337413163L;
69     private static final String FORWARD_URI_PART = "/FORWARD/";
70     private static final String PROXY_URI_PART = "/PROXY/";
71
72     private final Logger logger = LoggerFactory.getLogger(AccountServlet.class);
73
74     private final HttpService httpService;
75     private final String servletUrlWithoutRoot;
76     private final String servletUrl;
77     private final AccountHandler account;
78     private final String id;
79     private @Nullable Connection connectionToInitialize;
80     private final Gson gson;
81
82     public AccountServlet(HttpService httpService, String id, AccountHandler account, Gson gson) {
83         this.httpService = httpService;
84         this.account = account;
85         this.id = id;
86         this.gson = gson;
87
88         try {
89             servletUrlWithoutRoot = "amazonechocontrol/" + URLEncoder.encode(id, "UTF8");
90             servletUrl = "/" + servletUrlWithoutRoot;
91
92             httpService.registerServlet(servletUrl, this, null, httpService.createDefaultHttpContext());
93         } catch (UnsupportedEncodingException | NamespaceException | ServletException e) {
94             throw new IllegalStateException(e.getMessage());
95         }
96     }
97
98     private Connection reCreateConnection() {
99         Connection oldConnection = connectionToInitialize;
100         if (oldConnection == null) {
101             oldConnection = account.findConnection();
102         }
103         return new Connection(oldConnection, this.gson);
104     }
105
106     public void dispose() {
107         httpService.unregister(servletUrl);
108     }
109
110     @Override
111     protected void doPut(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
112             throws ServletException, IOException {
113         doVerb("PUT", req, resp);
114     }
115
116     @Override
117     protected void doDelete(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
118             throws ServletException, IOException {
119         doVerb("DELETE", req, resp);
120     }
121
122     @Override
123     protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
124             throws ServletException, IOException {
125         doVerb("POST", req, resp);
126     }
127
128     void doVerb(String verb, @Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
129         if (req == null) {
130             return;
131         }
132         if (resp == null) {
133             return;
134         }
135         String requestUri = req.getRequestURI();
136         if (requestUri == null) {
137             return;
138         }
139         String baseUrl = requestUri.substring(servletUrl.length());
140         String uri = baseUrl;
141         String queryString = req.getQueryString();
142         if (queryString != null && queryString.length() > 0) {
143             uri += "?" + queryString;
144         }
145
146         Connection connection = this.account.findConnection();
147         if (connection != null && uri.equals("/changedomain")) {
148             Map<String, String[]> map = req.getParameterMap();
149             String[] domainArray = map.get("domain");
150             if (domainArray == null) {
151                 logger.warn("Could not determine domain");
152                 return;
153             }
154             String domain = domainArray[0];
155             String loginData = connection.serializeLoginData();
156             Connection newConnection = new Connection(null, this.gson);
157             if (newConnection.tryRestoreLogin(loginData, domain)) {
158                 account.setConnection(newConnection);
159             }
160             resp.sendRedirect(servletUrl);
161             return;
162         }
163         if (uri.startsWith(PROXY_URI_PART)) {
164             // handle proxy request
165
166             if (connection == null) {
167                 returnError(resp, "Account not online");
168                 return;
169             }
170             String getUrl = "https://alexa." + connection.getAmazonSite() + "/"
171                     + uri.substring(PROXY_URI_PART.length());
172
173             String postData = null;
174             if (verb == "POST" || verb == "PUT") {
175                 postData = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
176             }
177
178             this.handleProxyRequest(connection, resp, verb, getUrl, null, postData, true, connection.getAmazonSite());
179             return;
180         }
181
182         // handle post of login page
183         connection = this.connectionToInitialize;
184         if (connection == null) {
185             returnError(resp, "Connection not in initialize mode.");
186             return;
187         }
188
189         resp.addHeader("content-type", "text/html;charset=UTF-8");
190
191         Map<String, String[]> map = req.getParameterMap();
192         StringBuilder postDataBuilder = new StringBuilder();
193         for (String name : map.keySet()) {
194             if (postDataBuilder.length() > 0) {
195                 postDataBuilder.append('&');
196             }
197
198             postDataBuilder.append(name);
199             postDataBuilder.append('=');
200             String value = "";
201             if (name.equals("failedSignInCount")) {
202                 value = "ape:AA==";
203             } else {
204                 String[] strings = map.get(name);
205                 if (strings != null && strings.length > 0 && strings[0] != null) {
206                     value = strings[0];
207                 }
208             }
209             postDataBuilder.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name()));
210         }
211
212         uri = req.getRequestURI();
213         if (uri == null || !uri.startsWith(servletUrl)) {
214             returnError(resp, "Invalid request uri '" + uri + "'");
215             return;
216         }
217         String relativeUrl = uri.substring(servletUrl.length()).replace(FORWARD_URI_PART, "/");
218
219         String site = connection.getAmazonSite();
220         if (relativeUrl.startsWith("/ap/signin")) {
221             site = "amazon.com";
222         }
223         String postUrl = "https://www." + site + relativeUrl;
224         queryString = req.getQueryString();
225         if (queryString != null && queryString.length() > 0) {
226             postUrl += "?" + queryString;
227         }
228         String referer = "https://www." + site;
229         String postData = postDataBuilder.toString();
230         handleProxyRequest(connection, resp, "POST", postUrl, referer, postData, false, site);
231     }
232
233     @Override
234     protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
235         if (req == null) {
236             return;
237         }
238         if (resp == null) {
239             return;
240         }
241         String requestUri = req.getRequestURI();
242         if (requestUri == null) {
243             return;
244         }
245         String baseUrl = requestUri.substring(servletUrl.length());
246         String uri = baseUrl;
247         String queryString = req.getQueryString();
248         if (queryString != null && queryString.length() > 0) {
249             uri += "?" + queryString;
250         }
251         logger.debug("doGet {}", uri);
252         try {
253             Connection connection = this.connectionToInitialize;
254             if (uri.startsWith(FORWARD_URI_PART) && connection != null) {
255                 String getUrl = "https://www." + connection.getAmazonSite() + "/"
256                         + uri.substring(FORWARD_URI_PART.length());
257
258                 this.handleProxyRequest(connection, resp, "GET", getUrl, null, null, false, connection.getAmazonSite());
259                 return;
260             }
261
262             connection = this.account.findConnection();
263             if (uri.startsWith(PROXY_URI_PART)) {
264                 // handle proxy request
265
266                 if (connection == null) {
267                     returnError(resp, "Account not online");
268                     return;
269                 }
270                 String getUrl = "https://alexa." + connection.getAmazonSite() + "/"
271                         + uri.substring(PROXY_URI_PART.length());
272
273                 this.handleProxyRequest(connection, resp, "GET", getUrl, null, null, false, connection.getAmazonSite());
274                 return;
275             }
276
277             if (connection != null && connection.verifyLogin()) {
278                 // handle commands
279                 if (baseUrl.equals("/logout") || baseUrl.equals("/logout/")) {
280                     this.connectionToInitialize = reCreateConnection();
281                     this.account.setConnection(null);
282                     resp.sendRedirect(this.servletUrl);
283                     return;
284                 }
285                 // handle commands
286                 if (baseUrl.equals("/newdevice") || baseUrl.equals("/newdevice/")) {
287                     this.connectionToInitialize = new Connection(null, this.gson);
288                     this.account.setConnection(null);
289                     resp.sendRedirect(this.servletUrl);
290                     return;
291                 }
292
293                 if (baseUrl.equals("/devices") || baseUrl.equals("/devices/")) {
294                     handleDevices(resp, connection);
295                     return;
296                 }
297                 if (baseUrl.equals("/changeDomain") || baseUrl.equals("/changeDomain/")) {
298                     handleChangeDomain(resp, connection);
299                     return;
300                 }
301                 if (baseUrl.equals("/ids") || baseUrl.equals("/ids/")) {
302                     String serialNumber = getQueryMap(queryString).get("serialNumber");
303                     Device device = account.findDeviceJson(serialNumber);
304                     if (device != null) {
305                         Thing thing = account.findThingBySerialNumber(device.serialNumber);
306                         handleIds(resp, connection, device, thing);
307                         return;
308                     }
309                 }
310                 // return hint that everything is ok
311                 handleDefaultPageResult(resp, "The Account is logged in.", connection);
312                 return;
313             }
314             connection = this.connectionToInitialize;
315             if (connection == null) {
316                 connection = this.reCreateConnection();
317                 this.connectionToInitialize = connection;
318             }
319
320             if (!uri.equals("/")) {
321                 String newUri = req.getServletPath() + "/";
322                 resp.sendRedirect(newUri);
323                 return;
324             }
325
326             String html = connection.getLoginPage();
327             returnHtml(connection, resp, html, "amazon.com");
328         } catch (URISyntaxException | InterruptedException e) {
329             logger.warn("get failed with uri syntax error", e);
330         }
331     }
332
333     public Map<String, String> getQueryMap(@Nullable String query) {
334         Map<String, String> map = new HashMap<>();
335         if (query != null) {
336             String[] params = query.split("&");
337             for (String param : params) {
338                 String[] elements = param.split("=");
339                 if (elements.length == 2) {
340                     String name = elements[0];
341                     String value = "";
342                     try {
343                         value = URLDecoder.decode(elements[1], "UTF8");
344                     } catch (UnsupportedEncodingException e) {
345                         logger.info("Unsupported encoding", e);
346                     }
347                     map.put(name, value);
348                 }
349             }
350         }
351         return map;
352     }
353
354     private void handleChangeDomain(HttpServletResponse resp, Connection connection) {
355         StringBuilder html = createPageStart("Change Domain");
356         html.append("<form action='");
357         html.append(servletUrl);
358         html.append("/changedomain' method='post'>\nDomain:\n<input type='text' name='domain' value='");
359         html.append(connection.getAmazonSite());
360         html.append("'>\n<br>\n<input type=\"submit\" value=\"Submit\">\n</form>");
361
362         createPageEndAndSent(resp, html);
363     }
364
365     private void handleDefaultPageResult(HttpServletResponse resp, String message, Connection connection)
366             throws IOException {
367         StringBuilder html = createPageStart("");
368         html.append(StringEscapeUtils.escapeHtml(message));
369         // logout link
370         html.append(" <a href='" + servletUrl + "/logout' >");
371         html.append(StringEscapeUtils.escapeHtml("Logout"));
372         html.append("</a>");
373         // newdevice link
374         html.append(" | <a href='" + servletUrl + "/newdevice' >");
375         html.append(StringEscapeUtils.escapeHtml("Logout and create new device id"));
376         html.append("</a>");
377         // customer id
378         html.append("<br>Customer Id: ");
379         html.append(StringEscapeUtils.escapeHtml(connection.getCustomerId()));
380         // customer name
381         html.append("<br>Customer Name: ");
382         html.append(StringEscapeUtils.escapeHtml(connection.getCustomerName()));
383         // device name
384         html.append("<br>App name: ");
385         html.append(StringEscapeUtils.escapeHtml(connection.getDeviceName()));
386         // connection
387         html.append("<br>Connected to: ");
388         html.append(StringEscapeUtils.escapeHtml(connection.getAlexaServer()));
389         // domain
390         html.append(" <a href='");
391         html.append(servletUrl);
392         html.append("/changeDomain'>Change</a>");
393
394         // Main UI link
395         html.append("<br><a href='/#!/settings/things/" + BINDING_ID + ":"
396                 + URLEncoder.encode(THING_TYPE_ACCOUNT.getId(), "UTF8") + ":" + URLEncoder.encode(id, "UTF8") + "'>");
397         html.append(StringEscapeUtils.escapeHtml("Check Thing in Main UI"));
398         html.append("</a><br><br>");
399
400         // device list
401         html.append(
402                 "<table><tr><th align='left'>Device</th><th align='left'>Serial Number</th><th align='left'>State</th><th align='left'>Thing</th><th align='left'>Family</th><th align='left'>Type</th><th align='left'>Customer Id</th></tr>");
403         for (Device device : this.account.getLastKnownDevices()) {
404
405             html.append("<tr><td>");
406             html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.accountName)));
407             html.append("</td><td>");
408             html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.serialNumber)));
409             html.append("</td><td>");
410             html.append(StringEscapeUtils.escapeHtml(device.online ? "Online" : "Offline"));
411             html.append("</td><td>");
412             Thing accountHandler = account.findThingBySerialNumber(device.serialNumber);
413             if (accountHandler != null) {
414                 html.append("<a href='" + servletUrl + "/ids/?serialNumber="
415                         + URLEncoder.encode(device.serialNumber, "UTF8") + "'>"
416                         + StringEscapeUtils.escapeHtml(accountHandler.getLabel()) + "</a>");
417             } else {
418                 html.append("<a href='" + servletUrl + "/ids/?serialNumber="
419                         + URLEncoder.encode(device.serialNumber, "UTF8") + "'>"
420                         + StringEscapeUtils.escapeHtml("Not defined") + "</a>");
421             }
422             html.append("</td><td>");
423             html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.deviceFamily)));
424             html.append("</td><td>");
425             html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.deviceType)));
426             html.append("</td><td>");
427             html.append(StringEscapeUtils.escapeHtml(nullReplacement(device.deviceOwnerCustomerId)));
428             html.append("</td>");
429             html.append("</tr>");
430         }
431         html.append("</table>");
432         createPageEndAndSent(resp, html);
433     }
434
435     private void handleDevices(HttpServletResponse resp, Connection connection)
436             throws IOException, URISyntaxException, InterruptedException {
437         returnHtml(connection, resp,
438                 "<html>" + StringEscapeUtils.escapeHtml(connection.getDeviceListJson()) + "</html>");
439     }
440
441     private String nullReplacement(@Nullable String text) {
442         if (text == null) {
443             return "<unknown>";
444         }
445         return text;
446     }
447
448     StringBuilder createPageStart(String title) {
449         StringBuilder html = new StringBuilder();
450         html.append("<html><head><title>"
451                 + StringEscapeUtils.escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel()));
452         if (!title.isEmpty()) {
453             html.append(" - ");
454             html.append(StringEscapeUtils.escapeHtml(title));
455         }
456         html.append("</title><head><body>");
457         html.append("<h1>" + StringEscapeUtils.escapeHtml(BINDING_NAME + " - " + this.account.getThing().getLabel()));
458         if (!title.isEmpty()) {
459             html.append(" - ");
460             html.append(StringEscapeUtils.escapeHtml(title));
461         }
462         html.append("</h1>");
463         return html;
464     }
465
466     private void createPageEndAndSent(HttpServletResponse resp, StringBuilder html) {
467         // account overview link
468         html.append("<br><a href='" + servletUrl + "/../' >");
469         html.append(StringEscapeUtils.escapeHtml("Account overview"));
470         html.append("</a><br>");
471
472         html.append("</body></html>");
473         resp.addHeader("content-type", "text/html;charset=UTF-8");
474         try {
475             resp.getWriter().write(html.toString());
476         } catch (IOException e) {
477             logger.warn("return html failed with IO error", e);
478         }
479     }
480
481     private void handleIds(HttpServletResponse resp, Connection connection, Device device, @Nullable Thing thing)
482             throws IOException, URISyntaxException {
483         StringBuilder html;
484         if (thing != null) {
485             html = createPageStart("Channel Options - " + thing.getLabel());
486         } else {
487             html = createPageStart("Device Information - No thing defined");
488         }
489         renderBluetoothMacChannel(connection, device, html);
490         renderAmazonMusicPlaylistIdChannel(connection, device, html);
491         renderPlayAlarmSoundChannel(connection, device, html);
492         renderMusicProviderIdChannel(connection, html);
493         renderCapabilities(connection, device, html);
494         createPageEndAndSent(resp, html);
495     }
496
497     private void renderCapabilities(Connection connection, Device device, StringBuilder html) {
498         html.append("<h2>Capabilities</h2>");
499         html.append("<table><tr><th align='left'>Name</th></tr>");
500         device.getCapabilities().forEach(capability -> html.append("<tr><td>")
501                 .append(StringEscapeUtils.escapeHtml(capability)).append("</td></tr>"));
502         html.append("</table>");
503     }
504
505     private void renderMusicProviderIdChannel(Connection connection, StringBuilder html) {
506         html.append("<h2>").append(StringEscapeUtils.escapeHtml("Channel " + CHANNEL_MUSIC_PROVIDER_ID))
507                 .append("</h2>");
508         html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
509         List<JsonMusicProvider> musicProviders = connection.getMusicProviders();
510         for (JsonMusicProvider musicProvider : musicProviders) {
511             List<String> properties = musicProvider.supportedProperties;
512             String providerId = musicProvider.id;
513             String displayName = musicProvider.displayName;
514             if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase") && providerId != null
515                     && !providerId.isEmpty() && "AVAILABLE".equals(musicProvider.availability) && displayName != null
516                     && !displayName.isEmpty()) {
517                 html.append("<tr><td>");
518                 html.append(StringEscapeUtils.escapeHtml(displayName));
519                 html.append("</td><td>");
520                 html.append(StringEscapeUtils.escapeHtml(providerId));
521                 html.append("</td></tr>");
522             }
523         }
524         html.append("</table>");
525     }
526
527     private void renderPlayAlarmSoundChannel(Connection connection, Device device, StringBuilder html) {
528         html.append("<h2>").append(StringEscapeUtils.escapeHtml("Channel " + CHANNEL_PLAY_ALARM_SOUND)).append("</h2>");
529         List<JsonNotificationSound> notificationSounds = List.of();
530         String errorMessage = "No notifications sounds found";
531         try {
532             notificationSounds = connection.getNotificationSounds(device);
533         } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException
534                 | InterruptedException e) {
535             errorMessage = e.getLocalizedMessage();
536         }
537         if (!notificationSounds.isEmpty()) {
538             html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
539             for (JsonNotificationSound notificationSound : notificationSounds) {
540                 if (notificationSound.folder == null && notificationSound.providerId != null
541                         && notificationSound.id != null && notificationSound.displayName != null) {
542                     String providerSoundId = notificationSound.providerId + ":" + notificationSound.id;
543
544                     html.append("<tr><td>");
545                     html.append(StringEscapeUtils.escapeHtml(notificationSound.displayName));
546                     html.append("</td><td>");
547                     html.append(StringEscapeUtils.escapeHtml(providerSoundId));
548                     html.append("</td></tr>");
549                 }
550             }
551             html.append("</table>");
552         } else {
553             html.append(StringEscapeUtils.escapeHtml(errorMessage));
554         }
555     }
556
557     private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device device, StringBuilder html) {
558         html.append("<h2>").append(StringEscapeUtils.escapeHtml("Channel " + CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID))
559                 .append("</h2>");
560
561         JsonPlaylists playLists = null;
562         String errorMessage = "No playlists found";
563         try {
564             playLists = connection.getPlaylists(device);
565         } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException
566                 | InterruptedException e) {
567             errorMessage = e.getLocalizedMessage();
568         }
569
570         if (playLists != null) {
571             Map<String, PlayList @Nullable []> playlistMap = playLists.playlists;
572             if (playlistMap != null && !playlistMap.isEmpty()) {
573                 html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
574
575                 for (PlayList[] innerLists : playlistMap.values()) {
576                     {
577                         if (innerLists != null && innerLists.length > 0) {
578                             PlayList playList = innerLists[0];
579                             if (playList != null && playList.playlistId != null && playList.title != null) {
580                                 html.append("<tr><td>");
581                                 html.append(StringEscapeUtils.escapeHtml(nullReplacement(playList.title)));
582                                 html.append("</td><td>");
583                                 html.append(StringEscapeUtils.escapeHtml(nullReplacement(playList.playlistId)));
584                                 html.append("</td></tr>");
585                             }
586                         }
587                     }
588                 }
589                 html.append("</table>");
590             } else {
591                 html.append(StringEscapeUtils.escapeHtml(errorMessage));
592             }
593         }
594     }
595
596     private void renderBluetoothMacChannel(Connection connection, Device device, StringBuilder html) {
597         html.append("<h2>").append(StringEscapeUtils.escapeHtml("Channel " + CHANNEL_BLUETOOTH_MAC)).append("</h2>");
598         JsonBluetoothStates bluetoothStates = connection.getBluetoothConnectionStates();
599         if (bluetoothStates == null) {
600             return;
601         }
602         BluetoothState[] innerStates = bluetoothStates.bluetoothStates;
603         if (innerStates == null) {
604             return;
605         }
606         for (BluetoothState state : innerStates) {
607             if (state == null) {
608                 continue;
609             }
610             String stateDeviceSerialNumber = state.deviceSerialNumber;
611             if ((stateDeviceSerialNumber == null && device.serialNumber == null)
612                     || (stateDeviceSerialNumber != null && stateDeviceSerialNumber.equals(device.serialNumber))) {
613                 List<PairedDevice> pairedDeviceList = state.getPairedDeviceList();
614                 if (pairedDeviceList.size() > 0) {
615                     html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
616                     for (PairedDevice pairedDevice : pairedDeviceList) {
617                         html.append("<tr><td>");
618                         html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.friendlyName)));
619                         html.append("</td><td>");
620                         html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.address)));
621                         html.append("</td></tr>");
622                     }
623                     html.append("</table>");
624                 } else {
625                     html.append(StringEscapeUtils.escapeHtml("No bluetooth devices paired"));
626                 }
627             }
628         }
629     }
630
631     void handleProxyRequest(Connection connection, HttpServletResponse resp, String verb, String url,
632             @Nullable String referer, @Nullable String postData, boolean json, String site) throws IOException {
633         HttpsURLConnection urlConnection;
634         try {
635             Map<String, String> headers = null;
636             if (referer != null) {
637                 headers = new HashMap<>();
638                 headers.put("Referer", referer);
639             }
640
641             urlConnection = connection.makeRequest(verb, url, postData, json, false, headers, 0);
642             if (urlConnection.getResponseCode() == 302) {
643                 {
644                     String location = urlConnection.getHeaderField("location");
645                     if (location.contains("/ap/maplanding")) {
646                         try {
647                             connection.registerConnectionAsApp(location);
648                             account.setConnection(connection);
649                             handleDefaultPageResult(resp, "Login succeeded", connection);
650                             this.connectionToInitialize = null;
651                             return;
652                         } catch (URISyntaxException | ConnectionException e) {
653                             returnError(resp,
654                                     "Login to '" + connection.getAmazonSite() + "' failed: " + e.getLocalizedMessage());
655                             this.connectionToInitialize = null;
656                             return;
657                         }
658                     }
659
660                     String startString = "https://www." + connection.getAmazonSite() + "/";
661                     String newLocation = null;
662                     if (location.startsWith(startString) && connection.getIsLoggedIn()) {
663                         newLocation = servletUrl + PROXY_URI_PART + location.substring(startString.length());
664                     } else if (location.startsWith(startString)) {
665                         newLocation = servletUrl + FORWARD_URI_PART + location.substring(startString.length());
666                     } else {
667                         startString = "/";
668                         if (location.startsWith(startString)) {
669                             newLocation = servletUrl + FORWARD_URI_PART + location.substring(startString.length());
670                         }
671                     }
672                     if (newLocation != null) {
673                         logger.debug("Redirect mapped from {} to {}", location, newLocation);
674
675                         resp.sendRedirect(newLocation);
676                         return;
677                     }
678                     returnError(resp, "Invalid redirect to '" + location + "'");
679                     return;
680                 }
681             }
682         } catch (URISyntaxException | ConnectionException | InterruptedException e) {
683             returnError(resp, e.getLocalizedMessage());
684             return;
685         }
686         String response = connection.convertStream(urlConnection);
687         returnHtml(connection, resp, response, site);
688     }
689
690     private void returnHtml(Connection connection, HttpServletResponse resp, String html) {
691         returnHtml(connection, resp, html, connection.getAmazonSite());
692     }
693
694     private void returnHtml(Connection connection, HttpServletResponse resp, String html, String amazonSite) {
695         String resultHtml = html.replace("action=\"/", "action=\"" + servletUrl + "/")
696                 .replace("action=\"&#x2F;", "action=\"" + servletUrl + "/")
697                 .replace("https://www." + amazonSite + "/", servletUrl + "/")
698                 .replace("https://www." + amazonSite + ":443" + "/", servletUrl + "/")
699                 .replace("https:&#x2F;&#x2F;www." + amazonSite + "&#x2F;", servletUrl + "/")
700                 .replace("https:&#x2F;&#x2F;www." + amazonSite + ":443" + "&#x2F;", servletUrl + "/")
701                 .replace("http://www." + amazonSite + "/", servletUrl + "/")
702                 .replace("http:&#x2F;&#x2F;www." + amazonSite + "&#x2F;", servletUrl + "/");
703
704         resp.addHeader("content-type", "text/html;charset=UTF-8");
705         try {
706             resp.getWriter().write(resultHtml);
707         } catch (IOException e) {
708             logger.warn("return html failed with IO error", e);
709         }
710     }
711
712     void returnError(HttpServletResponse resp, @Nullable String errorMessage) {
713         try {
714             String message = errorMessage != null ? errorMessage : "null";
715             resp.getWriter().write("<html>" + StringEscapeUtils.escapeHtml(message) + "<br><a href='" + servletUrl
716                     + "'>Try again</a></html>");
717         } catch (IOException e) {
718             logger.info("Returning error message failed", e);
719         }
720     }
721 }