]> git.basschouten.com Git - openhab-addons.git/blob
64f2c9ffd4807baf652bb7d28efa3761f9066cca
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.netatmo.internal.servlet;
14
15 import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.PARAM_ERROR;
16 import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
26 import javax.servlet.ServletException;
27 import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.util.MultiMap;
33 import org.eclipse.jetty.util.UrlEncoded;
34 import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
35 import org.osgi.service.http.HttpService;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link GrantServlet} manages the authorization with the Netatmo API. The servlet implements the
41  * Authorization Code flow and saves the resulting refreshToken with the bridge.
42  *
43  * @author GaĆ«l L'hopital - Initial contribution
44  */
45 @NonNullByDefault
46 public class GrantServlet extends NetatmoServlet {
47     private static final long serialVersionUID = 4817341543768441689L;
48     private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
49     private static final String TEMPLATE_ACCOUNT = "template/account.html";
50     private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
51
52     // Simple HTML templates for inserting messages.
53     private static final String HTML_ERROR = "<p class='block error'>Call to Netatmo Connect failed with error: %s</p>";
54
55     // Keys present in the account.html
56     private static final String KEY_ERROR = "error";
57     private static final String ACCOUNT_NAME = "account.name";
58     private static final String ACCOUNT_AUTHORIZED_CLASS = "account.authorized";
59     private static final String ACCOUNT_AUTHORIZE = "account.authorize";
60
61     private final Logger logger = LoggerFactory.getLogger(GrantServlet.class);
62     private final @NonNullByDefault({}) ClassLoader classLoader = GrantServlet.class.getClassLoader();
63     private final String accountTemplate;
64
65     public GrantServlet(ApiBridgeHandler handler, HttpService httpService) {
66         super(handler, httpService, "connect");
67         try (InputStream stream = classLoader.getResourceAsStream(TEMPLATE_ACCOUNT)) {
68             accountTemplate = stream != null ? new String(stream.readAllBytes(), StandardCharsets.UTF_8) : "";
69         } catch (IOException e) {
70             throw new IllegalArgumentException("Unable to load template account file. Please file a bug report.");
71         }
72     }
73
74     @Override
75     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
76         logger.debug("Netatmo auth callback servlet received GET request {}.", req.getRequestURI());
77         StringBuffer requestUrl = req.getRequestURL();
78         if (requestUrl != null) {
79             final String servletBaseURL = requestUrl.toString();
80             final Map<String, String> replaceMap = new HashMap<>();
81
82             handleRedirect(replaceMap, servletBaseURL, req.getQueryString());
83
84             String label = handler.getThing().getLabel();
85             replaceMap.put(ACCOUNT_NAME, label != null ? label : "");
86             replaceMap.put(CLIENT_ID, handler.getId());
87             replaceMap.put(ACCOUNT_AUTHORIZED_CLASS, handler.isConnected() ? " authorized" : " Unauthorized");
88             replaceMap.put(ACCOUNT_AUTHORIZE,
89                     handler.formatAuthorizationUrl().queryParam(REDIRECT_URI, servletBaseURL).build().toString());
90             replaceMap.put(REDIRECT_URI, servletBaseURL);
91
92             resp.setContentType(CONTENT_TYPE);
93             resp.getWriter().append(replaceKeysFromMap(accountTemplate, replaceMap));
94             resp.getWriter().close();
95         } else {
96             logger.warn("Unexpected : requestUrl is null");
97         }
98     }
99
100     /**
101      * Handles a possible call from Netatmo to the redirect_uri. If that is the case it will pass the authorization
102      * codes via the url and these are processed. In case of an error this is shown to the user. If the user was
103      * authorized this is passed on to the handler. Based on all these different outcomes the HTML is generated to
104      * inform the user.
105      *
106      * @param replaceMap a map with key String values that will be mapped in the HTML templates.
107      * @param servletBaseURL the servlet base, which should be used as the redirect_uri value
108      * @param queryString the query part of the GET request this servlet is processing
109      */
110     private void handleRedirect(Map<String, String> replaceMap, String servletBaseURL, @Nullable String queryString) {
111         replaceMap.put(KEY_ERROR, "");
112
113         if (queryString != null) {
114             final MultiMap<@Nullable String> params = new MultiMap<>();
115             UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
116             final String reqCode = params.getString(CODE);
117             final String reqState = params.getString(STATE);
118             final String reqError = params.getString(PARAM_ERROR);
119
120             if (reqError != null) {
121                 logger.debug("Netatmo redirected with an error: {}", reqError);
122                 replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError));
123             } else if (reqState != null && reqCode != null) {
124                 handler.openConnection(reqCode, servletBaseURL);
125             }
126         }
127     }
128
129     /**
130      * Replaces all keys from the map found in the template with values from the map. If the key is not found the key
131      * will be kept in the template.
132      *
133      * @param template template to replace keys with values
134      * @param map map with key value pairs to replace in the template
135      * @return a template with keys replaced
136      */
137     private String replaceKeysFromMap(String template, Map<String, String> map) {
138         final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
139         final StringBuffer sb = new StringBuffer();
140
141         while (m.find()) {
142             try {
143                 final String key = m.group(1);
144                 m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
145             } catch (RuntimeException e) {
146                 logger.debug("Error occurred during template filling, cause ", e);
147             }
148         }
149         m.appendTail(sb);
150         return sb.toString();
151     }
152 }