]> git.basschouten.com Git - openhab-addons.git/blob
35b165f045c35e65c40c59a2e00f883799566a5b
[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.bticinosmarther.internal.account;
14
15 import java.io.IOException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.Set;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import java.util.stream.Collectors;
23
24 import javax.servlet.ServletException;
25 import javax.servlet.http.HttpServlet;
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.http.HttpStatus;
32 import org.eclipse.jetty.util.MultiMap;
33 import org.eclipse.jetty.util.UrlEncoded;
34 import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
35 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
36 import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * The {@code SmartherAuthorizationServlet} class acts as the registered endpoint for the user to automatically manage
42  * the BTicino/Legrand API authorization process.
43  * The servlet follows the OAuth2 Authorization Code flow, saving the resulting refreshToken within the Smarther Bridge.
44  *
45  * @author Fabio Possieri - Initial contribution
46  */
47 @NonNullByDefault
48 public class SmartherAuthorizationServlet extends HttpServlet {
49
50     private static final long serialVersionUID = 5199173744807168342L;
51
52     private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
53     private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
54
55     // Http request parameters
56     private static final String PARAM_CODE = "code";
57     private static final String PARAM_STATE = "state";
58     private static final String PARAM_ERROR = "error";
59
60     // Simple HTML templates for inserting messages.
61     private static final String HTML_EMPTY_APPLICATIONS = "<p class='block'>Manually add a Smarther Bridge to authorize it here<p>";
62     private static final String HTML_BRIDGE_AUTHORIZED = "<p class='block authorized'>Bridge authorized for Client Id %s</p>";
63     private static final String HTML_ERROR = "<p class='block error'>Call to Smarther API gateway failed with error: %s</p>";
64
65     private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
66
67     // Keys present in the index.html
68     private static final String KEY_PAGE_REFRESH = "pageRefresh";
69     private static final String HTML_META_REFRESH_CONTENT = "<meta http-equiv='refresh' content='10; url=%s'>";
70     private static final String KEY_AUTHORIZED_BRIDGE = "authorizedBridge";
71     private static final String KEY_ERROR = "error";
72     private static final String KEY_APPLICATIONS = "applications";
73     private static final String KEY_REDIRECT_URI = "redirectUri";
74     // Keys present in the application.html
75     private static final String APPLICATION_ID = "application.id";
76     private static final String APPLICATION_NAME = "application.name";
77     private static final String APPLICATION_LOCATIONS = "application.locations";
78     private static final String APPLICATION_AUTHORIZED_CLASS = "application.authorized";
79     private static final String APPLICATION_AUTHORIZE = "application.authorize";
80
81     private final Logger logger = LoggerFactory.getLogger(SmartherAuthorizationServlet.class);
82
83     private final SmartherAccountService accountService;
84     private final String indexTemplate;
85     private final String applicationTemplate;
86
87     /**
88      * Constructs a {@code SmartherAuthorizationServlet} associated to the given {@link SmartherAccountService} service
89      * and with the given html index/application templates.
90      *
91      * @param accountService
92      *            the account service to associate to the servlet
93      * @param indexTemplate
94      *            the html template to use as index page for the user
95      * @param applicationTemplate
96      *            the html template to use as application page for the user
97      */
98     public SmartherAuthorizationServlet(SmartherAccountService accountService, String indexTemplate,
99             String applicationTemplate) {
100         this.accountService = accountService;
101         this.indexTemplate = indexTemplate;
102         this.applicationTemplate = applicationTemplate;
103     }
104
105     @Override
106     protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
107             throws ServletException, IOException {
108         if (request != null && response != null) {
109             final String servletBaseURL = extractServletBaseURL(request);
110             logger.debug("Authorization callback servlet received GET request {}", servletBaseURL);
111
112             // Handle the received data
113             final Map<String, String> replaceMap = new HashMap<>();
114             handleSmartherRedirect(replaceMap, servletBaseURL, request.getQueryString());
115
116             // Build a http 200 (Success) response for the caller
117             response.setContentType(CONTENT_TYPE);
118             response.setStatus(HttpStatus.OK_200);
119             replaceMap.put(KEY_REDIRECT_URI, servletBaseURL);
120             replaceMap.put(KEY_APPLICATIONS, formatApplications(applicationTemplate, servletBaseURL));
121             response.getWriter().append(replaceKeysFromMap(indexTemplate, replaceMap));
122             response.getWriter().close();
123         } else if (response != null) {
124             // Build a http 400 (Bad Request) error response for the caller
125             response.setContentType(CONTENT_TYPE);
126             response.setStatus(HttpStatus.BAD_REQUEST_400);
127             response.getWriter().close();
128         } else {
129             throw new ServletException("Authorization callback with null request/response");
130         }
131     }
132
133     /**
134      * Extracts the servlet base url from the received http request, handling eventual reverse proxy.
135      *
136      * @param request
137      *            the received http request
138      *
139      * @return a string containing the servlet base url
140      */
141     private String extractServletBaseURL(HttpServletRequest request) {
142         final StringBuffer requestURL = request.getRequestURL();
143
144         // Try to infer the real protocol from request headers
145         final String realProtocol = StringUtil.defaultIfBlank(request.getHeader(X_FORWARDED_PROTO),
146                 request.getScheme());
147
148         return requestURL.replace(0, requestURL.indexOf(":"), realProtocol).toString();
149     }
150
151     /**
152      * Handles a call from BTicino/Legrand API gateway to the redirect_uri, dispatching the authorization flow to the
153      * proper authorization handler.
154      * If the user was authorized, this is passed on to the handler; in case of an error, this is shown to the user.
155      * Based on all these different outcomes the html response is generated to inform the user.
156      *
157      * @param replaceMap
158      *            a map with key string values to use in the html templates
159      * @param servletBaseURL
160      *            the servlet base url to compose the correct API gateway redirect_uri
161      * @param queryString
162      *            the querystring part of the received request, may be {@code null}
163      */
164     private void handleSmartherRedirect(Map<String, String> replaceMap, String servletBaseURL,
165             @Nullable String queryString) {
166         replaceMap.put(KEY_AUTHORIZED_BRIDGE, "");
167         replaceMap.put(KEY_ERROR, "");
168         replaceMap.put(KEY_PAGE_REFRESH, "");
169
170         if (queryString != null) {
171             final MultiMap<String> params = new MultiMap<>();
172             UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
173             final String reqCode = params.getString(PARAM_CODE);
174             final String reqState = params.getString(PARAM_STATE);
175             final String reqError = params.getString(PARAM_ERROR);
176
177             replaceMap.put(KEY_PAGE_REFRESH,
178                     params.isEmpty() ? "" : String.format(HTML_META_REFRESH_CONTENT, servletBaseURL));
179             if (!StringUtil.isBlank(reqError)) {
180                 logger.debug("Authorization redirected with an error: {}", reqError);
181                 replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError));
182             } else if (!StringUtil.isBlank(reqState)) {
183                 try {
184                     logger.trace("Received from authorization - state:[{}] code:[{}]", reqState, reqCode);
185                     replaceMap.put(KEY_AUTHORIZED_BRIDGE, String.format(HTML_BRIDGE_AUTHORIZED,
186                             accountService.dispatchAuthorization(servletBaseURL, reqState, reqCode)));
187                 } catch (SmartherGatewayException e) {
188                     logger.debug("Exception during authorizaton: ", e);
189                     replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, e.getMessage()));
190                 }
191             }
192         }
193     }
194
195     /**
196      * Returns an html formatted text representing all the available Smarther Bridge applications.
197      *
198      * @param applicationTemplate
199      *            the html template to format the application with
200      * @param servletBaseURL
201      *            the redirect_uri to link to the authorization button as authorization url
202      *
203      * @return a string containing the html formatted text
204      */
205     private String formatApplications(String applicationTemplate, String servletBaseURL) {
206         final Set<SmartherAccountHandler> applications = accountService.getSmartherAccountHandlers();
207
208         return applications.isEmpty() ? HTML_EMPTY_APPLICATIONS
209                 : applications.stream().map(p -> formatApplication(applicationTemplate, p, servletBaseURL))
210                         .collect(Collectors.joining());
211     }
212
213     /**
214      * Returns an html formatted text representing a given Smarther Bridge application.
215      *
216      * @param applicationTemplate
217      *            the html template to format the application with
218      * @param handler
219      *            the Smarther application handler to use
220      * @param servletBaseURL
221      *            the redirect_uri to link to the authorization button as authorization url
222      *
223      * @return a string containing the html formatted text
224      */
225     private String formatApplication(String applicationTemplate, SmartherAccountHandler handler,
226             String servletBaseURL) {
227         final Map<String, String> map = new HashMap<>();
228
229         map.put(APPLICATION_ID, handler.getUID().getAsString());
230         map.put(APPLICATION_NAME, handler.getLabel());
231
232         if (handler.isAuthorized()) {
233             final String availableLocations = Location.toNameString(handler.getLocations());
234             map.put(APPLICATION_AUTHORIZED_CLASS, " authorized");
235             map.put(APPLICATION_LOCATIONS, String.format(" (Available locations: %s)", availableLocations));
236         } else {
237             map.put(APPLICATION_AUTHORIZED_CLASS, "");
238             map.put(APPLICATION_LOCATIONS, "");
239         }
240
241         map.put(APPLICATION_AUTHORIZE, handler.formatAuthorizationUrl(servletBaseURL));
242         return replaceKeysFromMap(applicationTemplate, map);
243     }
244
245     /**
246      * Replaces all keys found in the template with the values matched from the map.
247      * If a key is not found in the map, it is kept unchanged in the template.
248      *
249      * @param template
250      *            the template to replace keys on
251      * @param map
252      *            the map containing the key/value pairs to replace in the template
253      *
254      * @return a string containing the resulting template after the replace process
255      */
256     private String replaceKeysFromMap(String template, Map<String, String> map) {
257         final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
258         final StringBuffer sb = new StringBuffer();
259
260         while (m.find()) {
261             try {
262                 final String key = m.group(1);
263                 m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
264             } catch (RuntimeException e) {
265                 logger.warn("Error occurred during template filling, cause ", e);
266             }
267         }
268         m.appendTail(sb);
269         return sb.toString();
270     }
271 }