2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.netatmo.internal.servlet;
15 import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.PARAM_ERROR;
16 import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.util.HashMap;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
26 import javax.servlet.ServletException;
27 import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse;
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;
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.
43 * @author Gaƫl L'hopital - Initial contribution
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";
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>";
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";
61 private final Logger logger = LoggerFactory.getLogger(GrantServlet.class);
62 private final @NonNullByDefault({}) ClassLoader classLoader = GrantServlet.class.getClassLoader();
63 private final String accountTemplate;
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.");
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<>();
82 handleRedirect(replaceMap, servletBaseURL, req.getQueryString());
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);
92 resp.setContentType(CONTENT_TYPE);
93 resp.getWriter().append(replaceKeysFromMap(accountTemplate, replaceMap));
94 resp.getWriter().close();
96 logger.warn("Unexpected : requestUrl is null");
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
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
110 private void handleRedirect(Map<String, String> replaceMap, String servletBaseURL, @Nullable String queryString) {
111 replaceMap.put(KEY_ERROR, "");
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);
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);
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.
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
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();
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);
150 return sb.toString();