]> git.basschouten.com Git - openhab-addons.git/blob
f79a62d44afbe6fb4f5a5271e7fd01647b311325
[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.webexteams.internal;
14
15 import java.io.IOException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
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.util.MultiMap;
32 import org.eclipse.jetty.util.UrlEncoded;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 /**
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.
39  *
40  * @author Tom Deckers - Initial contribution
41  */
42 @NonNullByDefault
43 public class WebexAuthServlet extends HttpServlet {
44     static final long serialVersionUID = 42L;
45     private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
46
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>";
51
52     private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
53
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";
61
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";
71
72     private final Logger logger = LoggerFactory.getLogger(WebexAuthServlet.class);
73     private final WebexAuthService authService;
74     private final String indexTemplate;
75     private final String accountTemplate;
76
77     public WebexAuthServlet(WebexAuthService authService, String indexTemplate, String accountTemplate) {
78         this.authService = authService;
79         this.indexTemplate = indexTemplate;
80         this.accountTemplate = accountTemplate;
81     }
82
83     @Override
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<>();
90
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();
97         }
98     }
99
100     /**
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
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 Webex 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_AUTHORIZED_USER, "");
112         replaceMap.put(KEY_ERROR, "");
113         replaceMap.put(KEY_PAGE_REFRESH, "");
114
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");
121
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()) {
128                 try {
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()));
134                 }
135             }
136         }
137     }
138
139     /**
140      * Formats the HTML of all available Webex Accounts and returns it as a String
141      *
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
145      */
146     private String formatAccounts(String accountTemplate, String servletBaseURL) {
147         final List<WebexTeamsHandler> accounts = authService.getWebexTeamsHandlers();
148
149         return accounts.isEmpty() ? HTML_EMPTY_ACCOUNTS
150                 : accounts.stream().map(p -> formatAccount(accountTemplate, p, servletBaseURL))
151                         .collect(Collectors.joining());
152     }
153
154     /**
155      * Formats the HTML of a Webex Account and returns it as a String
156      *
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
161      */
162     private String formatAccount(String accountTemplate, WebexTeamsHandler handler, String servletBaseURL) {
163         final Map<String, String> map = new HashMap<>();
164
165         map.put(ACCOUNT_ID, handler.getUID().getAsString());
166         map.put(ACCOUNT_NAME, handler.getLabel());
167         final String webexUser = handler.getUser();
168
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, "");
184         } else {
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");
189         }
190
191         map.put(ACCOUNT_TYPE, handler.accountType);
192         map.put(ACCOUNT_AUTHORIZE, handler.formatAuthorizationUrl(servletBaseURL));
193         return replaceKeysFromMap(accountTemplate, map);
194     }
195
196     /**
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.
199      *
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
203      */
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();
207
208         while (m.find()) {
209             try {
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);
214             }
215         }
216         m.appendTail(sb);
217         return sb.toString();
218     }
219 }