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