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