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.webexteams.internal;
15 import java.io.IOException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.HashMap;
18 import java.util.List;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import java.util.stream.Collectors;
24 import javax.servlet.ServletException;
25 import javax.servlet.http.HttpServlet;
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.util.MultiMap;
32 import org.eclipse.jetty.util.UrlEncoded;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * The {@link WebexAuthServlet} manages the authorization with the Webex API. The servlet implements the
38 * Authorization Code flow and saves the resulting refreshToken with the bridge.
40 * @author Tom Deckers - Initial contribution
43 public class WebexAuthServlet extends HttpServlet {
44 static final long serialVersionUID = 42L;
45 private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
47 // Simple HTML templates for inserting messages.
48 private static final String HTML_EMPTY_ACCOUNTS = "<p class='block'>Manually add a Webex Account to authorize it here.<p>";
49 private static final String HTML_USER_AUTHORIZED = "<div class='row authorized'>Account authorized for user %s.</div>";
50 private static final String HTML_ERROR = "<p class='block error'>Call to Webex failed with error: %s</p>";
52 private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
54 // Keys present in the index.html
55 private static final String KEY_PAGE_REFRESH = "pageRefresh";
56 private static final String HTML_META_REFRESH_CONTENT = "<meta http-equiv='refresh' content='10; url=%s'>";
57 private static final String KEY_AUTHORIZED_USER = "authorizedUser";
58 private static final String KEY_ERROR = "error";
59 private static final String KEY_ACCOUNTS = "accounts";
60 private static final String KEY_REDIRECT_URI = "redirectUri";
62 // Keys present in the account.html
63 private static final String ACCOUNT_ID = "account.id";
64 private static final String ACCOUNT_NAME = "account.name";
65 private static final String ACCOUNT_USER_ID = "account.user";
66 private static final String ACCOUNT_TYPE = "account.type";
67 private static final String ACCOUNT_AUTHORIZE = "account.authorize";
68 private static final String ACCOUNT_SHOWBTN = "account.showbtn";
69 private static final String ACCOUNT_SHWOMSG = "account.showmsg";
70 private static final String ACCOUNT_MSG = "account.msg";
72 private final Logger logger = LoggerFactory.getLogger(WebexAuthServlet.class);
73 private final WebexAuthService authService;
74 private final String indexTemplate;
75 private final String accountTemplate;
77 public WebexAuthServlet(WebexAuthService authService, String indexTemplate, String accountTemplate) {
78 this.authService = authService;
79 this.indexTemplate = indexTemplate;
80 this.accountTemplate = accountTemplate;
84 protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
85 throws ServletException, IOException {
86 if (req != null && resp != null) {
87 logger.debug("Webex auth callback servlet received GET request {}.", req.getRequestURI());
88 final String servletBaseURL = req.getRequestURL().toString();
89 final Map<String, String> replaceMap = new HashMap<>();
91 handleRedirect(replaceMap, servletBaseURL, req.getQueryString());
92 resp.setContentType(CONTENT_TYPE);
93 replaceMap.put(KEY_REDIRECT_URI, servletBaseURL);
94 replaceMap.put(KEY_ACCOUNTS, formatAccounts(this.accountTemplate, servletBaseURL));
95 resp.getWriter().append(replaceKeysFromMap(this.indexTemplate, replaceMap));
96 resp.getWriter().close();
101 * Handles a possible call from Webex to the redirect_uri. If that is the case Webex 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 Webex 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_AUTHORIZED_USER, "");
112 replaceMap.put(KEY_ERROR, "");
113 replaceMap.put(KEY_PAGE_REFRESH, "");
115 if (queryString != null) {
116 final MultiMap<String> params = new MultiMap<>();
117 UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
118 final String reqCode = params.getString("code");
119 final String reqState = params.getString("state");
120 final String reqError = params.getString("error");
122 replaceMap.put(KEY_PAGE_REFRESH,
123 params.isEmpty() ? "" : String.format(HTML_META_REFRESH_CONTENT, servletBaseURL));
124 if (reqError != null && !reqError.isBlank()) {
125 logger.debug("Webex redirected with an error: {}", reqError);
126 replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError));
127 } else if (!reqState.isBlank()) {
129 replaceMap.put(KEY_AUTHORIZED_USER, String.format(HTML_USER_AUTHORIZED,
130 authService.authorize(servletBaseURL, reqState, reqCode)));
131 } catch (WebexTeamsException e) {
132 logger.debug("Exception during authorizaton: ", e);
133 replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, e.getMessage()));
140 * Formats the HTML of all available Webex Accounts and returns it as a String
142 * @param accountTemplate The account template to format the account values in
143 * @param servletBaseURL the redirect_uri to be used in the authorization url created on the authorization button.
144 * @return A String with the accounts formatted with the account template
146 private String formatAccounts(String accountTemplate, String servletBaseURL) {
147 final List<WebexTeamsHandler> accounts = authService.getWebexTeamsHandlers();
149 return accounts.isEmpty() ? HTML_EMPTY_ACCOUNTS
150 : accounts.stream().map(p -> formatAccount(accountTemplate, p, servletBaseURL))
151 .collect(Collectors.joining());
155 * Formats the HTML of a Webex Account and returns it as a String
157 * @param accountTemplate The account template to format the account values in
158 * @param handler The handler for the account to format
159 * @param servletBaseURL the redirect_uri to be used in the authorization url created on the authorization button.
160 * @return A String with the account formatted with the account template
162 private String formatAccount(String accountTemplate, WebexTeamsHandler handler, String servletBaseURL) {
163 final Map<String, String> map = new HashMap<>();
165 map.put(ACCOUNT_ID, handler.getUID().getAsString());
166 map.put(ACCOUNT_NAME, handler.getLabel());
167 final String webexUser = handler.getUser();
169 if (!handler.isConfigured()) {
170 map.put(ACCOUNT_USER_ID, "");
171 map.put(ACCOUNT_SHOWBTN, "u-hide");
172 map.put(ACCOUNT_SHWOMSG, "u-show");
173 map.put(ACCOUNT_MSG, "Configure account.");
174 } else if (handler.isAuthorized()) {
175 map.put(ACCOUNT_USER_ID, String.format("Authorized user: %s", webexUser));
176 map.put(ACCOUNT_SHOWBTN, "u-hide");
177 map.put(ACCOUNT_SHWOMSG, "u-show");
178 map.put(ACCOUNT_MSG, "Authorized.");
179 } else if (webexUser.isBlank()) {
180 map.put(ACCOUNT_USER_ID, "Unauthorized user");
181 map.put(ACCOUNT_SHOWBTN, "u-show");
182 map.put(ACCOUNT_SHWOMSG, "u-hide");
183 map.put(ACCOUNT_MSG, "");
185 map.put(ACCOUNT_USER_ID, "");
186 map.put(ACCOUNT_SHOWBTN, "u-hide");
187 map.put(ACCOUNT_SHWOMSG, "u-show");
188 map.put(ACCOUNT_MSG, "UNKNOWN");
191 map.put(ACCOUNT_TYPE, handler.accountType);
192 map.put(ACCOUNT_AUTHORIZE, handler.formatAuthorizationUrl(servletBaseURL));
193 return replaceKeysFromMap(accountTemplate, map);
197 * Replaces all keys from the map found in the template with values from the map. If the key is not found the key
198 * will be kept in the template.
200 * @param template template to replace keys with values
201 * @param map map with key value pairs to replace in the template
202 * @return a template with keys replaced
204 private String replaceKeysFromMap(String template, Map<String, String> map) {
205 final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
206 final StringBuffer sb = new StringBuffer();
210 final String key = m.group(1);
211 m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
212 } catch (RuntimeException e) {
213 logger.debug("Error occurred during template filling, cause ", e);
217 return sb.toString();