]> git.basschouten.com Git - openhab-addons.git/blob
0d902d36d6ccde675ef8ba2c3d03922594f52304
[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             List<String> properties = musicProvider.supportedProperties;
503             String providerId = musicProvider.id;
504             String displayName = musicProvider.displayName;
505             if (properties != null && properties.contains("Alexa.Music.PlaySearchPhrase")
506                     && StringUtils.isNotEmpty(providerId) && StringUtils.equals(musicProvider.availability, "AVAILABLE")
507                     && StringUtils.isNotEmpty(displayName)) {
508                 html.append("<tr><td>");
509                 html.append(StringEscapeUtils.escapeHtml(displayName));
510                 html.append("</td><td>");
511                 html.append(StringEscapeUtils.escapeHtml(providerId));
512                 html.append("</td></tr>");
513             }
514         }
515         html.append("</table>");
516     }
517
518     private void renderPlayAlarmSoundChannel(Connection connection, Device device, StringBuilder html) {
519         html.append("<h2>" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_PLAY_ALARM_SOUND) + "</h2>");
520         JsonNotificationSound[] notificationSounds = null;
521         String errorMessage = "No notifications sounds found";
522         try {
523             notificationSounds = connection.getNotificationSounds(device);
524         } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException e) {
525             errorMessage = e.getLocalizedMessage();
526         }
527         if (notificationSounds != null) {
528             html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
529             for (JsonNotificationSound notificationSound : notificationSounds) {
530                 if (notificationSound.folder == null && notificationSound.providerId != null
531                         && notificationSound.id != null && notificationSound.displayName != null) {
532                     String providerSoundId = notificationSound.providerId + ":" + notificationSound.id;
533
534                     html.append("<tr><td>");
535                     html.append(StringEscapeUtils.escapeHtml(notificationSound.displayName));
536                     html.append("</td><td>");
537                     html.append(StringEscapeUtils.escapeHtml(providerSoundId));
538                     html.append("</td></tr>");
539                 }
540             }
541             html.append("</table>");
542         } else {
543             html.append(StringEscapeUtils.escapeHtml(errorMessage));
544         }
545     }
546
547     private void renderAmazonMusicPlaylistIdChannel(Connection connection, Device device, StringBuilder html) {
548         html.append("<h2>" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID) + "</h2>");
549
550         JsonPlaylists playLists = null;
551         String errorMessage = "No playlists found";
552         try {
553             playLists = connection.getPlaylists(device);
554         } catch (IOException | HttpException | URISyntaxException | JsonSyntaxException | ConnectionException e) {
555             errorMessage = e.getLocalizedMessage();
556         }
557
558         if (playLists != null) {
559             Map<String, PlayList @Nullable []> playlistMap = playLists.playlists;
560             if (playlistMap != null && !playlistMap.isEmpty()) {
561                 html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
562
563                 for (PlayList[] innerLists : playlistMap.values()) {
564                     {
565                         if (innerLists != null && innerLists.length > 0) {
566                             PlayList playList = innerLists[0];
567                             if (playList != null && playList.playlistId != null && playList.title != null) {
568                                 html.append("<tr><td>");
569                                 html.append(StringEscapeUtils.escapeHtml(nullReplacement(playList.title)));
570                                 html.append("</td><td>");
571                                 html.append(StringEscapeUtils.escapeHtml(nullReplacement(playList.playlistId)));
572                                 html.append("</td></tr>");
573                             }
574                         }
575                     }
576                 }
577                 html.append("</table>");
578             } else {
579                 html.append(StringEscapeUtils.escapeHtml(errorMessage));
580             }
581         }
582     }
583
584     private void renderBluetoothMacChannel(Connection connection, Device device, StringBuilder html) {
585         html.append("<h2>" + StringEscapeUtils.escapeHtml("Channel " + CHANNEL_BLUETOOTH_MAC) + "</h2>");
586         JsonBluetoothStates bluetoothStates = connection.getBluetoothConnectionStates();
587         if (bluetoothStates == null) {
588             return;
589         }
590         BluetoothState[] innerStates = bluetoothStates.bluetoothStates;
591         if (innerStates == null) {
592             return;
593         }
594         for (BluetoothState state : innerStates) {
595             if (state == null) {
596                 continue;
597             }
598             if ((state.deviceSerialNumber == null && device.serialNumber == null)
599                     || (state.deviceSerialNumber != null && state.deviceSerialNumber.equals(device.serialNumber))) {
600                 PairedDevice[] pairedDeviceList = state.pairedDeviceList;
601                 if (pairedDeviceList != null && pairedDeviceList.length > 0) {
602                     html.append("<table><tr><th align='left'>Name</th><th align='left'>Value</th></tr>");
603                     for (PairedDevice pairedDevice : pairedDeviceList) {
604                         html.append("<tr><td>");
605                         html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.friendlyName)));
606                         html.append("</td><td>");
607                         html.append(StringEscapeUtils.escapeHtml(nullReplacement(pairedDevice.address)));
608                         html.append("</td></tr>");
609                     }
610                     html.append("</table>");
611                 } else {
612                     html.append(StringEscapeUtils.escapeHtml("No bluetooth devices paired"));
613                 }
614             }
615         }
616     }
617
618     void handleProxyRequest(Connection connection, HttpServletResponse resp, String verb, String url,
619             @Nullable String referer, @Nullable String postData, boolean json, String site) throws IOException {
620         HttpsURLConnection urlConnection;
621         try {
622             Map<String, String> headers = null;
623             if (referer != null) {
624                 headers = new HashMap<>();
625                 headers.put("Referer", referer);
626             }
627
628             urlConnection = connection.makeRequest(verb, url, postData, json, false, headers, 0);
629             if (urlConnection.getResponseCode() == 302) {
630                 {
631                     String location = urlConnection.getHeaderField("location");
632                     if (location.contains("/ap/maplanding")) {
633                         try {
634                             connection.registerConnectionAsApp(location);
635                             account.setConnection(connection);
636                             handleDefaultPageResult(resp, "Login succeeded", connection);
637                             this.connectionToInitialize = null;
638                             return;
639                         } catch (URISyntaxException | ConnectionException e) {
640                             returnError(resp,
641                                     "Login to '" + connection.getAmazonSite() + "' failed: " + e.getLocalizedMessage());
642                             this.connectionToInitialize = null;
643                             return;
644                         }
645                     }
646
647                     String startString = "https://www." + connection.getAmazonSite() + "/";
648                     String newLocation = null;
649                     if (location.startsWith(startString) && connection.getIsLoggedIn()) {
650                         newLocation = servletUrl + PROXY_URI_PART + location.substring(startString.length());
651                     } else if (location.startsWith(startString)) {
652                         newLocation = servletUrl + FORWARD_URI_PART + location.substring(startString.length());
653                     } else {
654                         startString = "/";
655                         if (location.startsWith(startString)) {
656                             newLocation = servletUrl + FORWARD_URI_PART + location.substring(startString.length());
657                         }
658                     }
659                     if (newLocation != null) {
660                         logger.debug("Redirect mapped from {} to {}", location, newLocation);
661
662                         resp.sendRedirect(newLocation);
663                         return;
664                     }
665                     returnError(resp, "Invalid redirect to '" + location + "'");
666                     return;
667                 }
668             }
669         } catch (URISyntaxException | ConnectionException e) {
670             returnError(resp, e.getLocalizedMessage());
671             return;
672         }
673         String response = connection.convertStream(urlConnection);
674         returnHtml(connection, resp, response, site);
675     }
676
677     private void returnHtml(Connection connection, HttpServletResponse resp, String html) {
678         returnHtml(connection, resp, html, connection.getAmazonSite());
679     }
680
681     private void returnHtml(Connection connection, HttpServletResponse resp, String html, String amazonSite) {
682         String resultHtml = html.replace("action=\"/", "action=\"" + servletUrl + "/")
683                 .replace("action=\"&#x2F;", "action=\"" + servletUrl + "/")
684                 .replace("https://www." + amazonSite + "/", servletUrl + "/")
685                 .replace("https://www." + amazonSite + ":443" + "/", servletUrl + "/")
686                 .replace("https:&#x2F;&#x2F;www." + amazonSite + "&#x2F;", servletUrl + "/")
687                 .replace("https:&#x2F;&#x2F;www." + amazonSite + ":443" + "&#x2F;", servletUrl + "/")
688                 .replace("http://www." + amazonSite + "/", servletUrl + "/")
689                 .replace("http:&#x2F;&#x2F;www." + amazonSite + "&#x2F;", servletUrl + "/");
690
691         resp.addHeader("content-type", "text/html;charset=UTF-8");
692         try {
693             resp.getWriter().write(resultHtml);
694         } catch (IOException e) {
695             logger.warn("return html failed with IO error", e);
696         }
697     }
698
699     void returnError(HttpServletResponse resp, @Nullable String errorMessage) {
700         try {
701             String message = errorMessage != null ? errorMessage : "null";
702             resp.getWriter().write("<html>" + StringEscapeUtils.escapeHtml(message) + "<br><a href='" + servletUrl
703                     + "'>Try again</a></html>");
704         } catch (IOException e) {
705             logger.info("Returning error message failed", e);
706         }
707     }
708 }