]> git.basschouten.com Git - openhab-addons.git/blob
c61c0293a0ef305241988cf3dbbde55c8fea1158
[openhab-addons.git] /
1 /*
2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3  *
4  * Copyright (c) 2013-2015 Oracle and/or its affiliates. All rights reserved.
5  *
6  * The contents of this file are subject to the terms of either the GNU
7  * General Public License Version 2 only ("GPL") or the Common Development
8  * and Distribution License("CDDL") (collectively, the "License").  You
9  * may not use this file except in compliance with the License.  You can
10  * obtain a copy of the License at
11  * http://glassfish.java.net/public/CDDL+GPL_1_1.html
12  * or packager/legal/LICENSE.txt.  See the License for the specific
13  * language governing permissions and limitations under the License.
14  *
15  * When distributing the software, include this License Header Notice in each
16  * file and include the License file at packager/legal/LICENSE.txt.
17  *
18  * GPL Classpath Exception:
19  * Oracle designates this particular file as subject to the "Classpath"
20  * exception as provided by Oracle in the GPL Version 2 section of the License
21  * file that accompanied this code.
22  *
23  * Modifications:
24  * If applicable, add the following below the License Header, with the fields
25  * enclosed by brackets [] replaced by your own identifying information:
26  * "Portions Copyright [year] [name of copyright owner]"
27  *
28  * Contributor(s):
29  * If you wish your version of this file to be governed by only the CDDL or
30  * only the GPL Version 2, indicate your decision by adding "[Contributor]
31  * elects to include this software in this distribution under the [CDDL or GPL
32  * Version 2] license."  If you don't indicate a single choice of license, a
33  * recipient has the option to distribute your version of this file under
34  * either the CDDL, the GPL Version 2 or to extend the choice of license to
35  * its licensees as provided above.  However, if you add GPL Version 2 code
36  * and therefore, elected the GPL Version 2 license, then the option applies
37  * only if the new code is made subject to such option by the copyright
38  * holder.
39  */
40
41 package org.openhab.binding.lametrictime.api.authentication;
42
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.net.URI;
46 import java.nio.charset.Charset;
47 import java.util.List;
48 import java.util.Map;
49
50 import javax.annotation.Priority;
51 import javax.ws.rs.Priorities;
52 import javax.ws.rs.client.Client;
53 import javax.ws.rs.client.ClientBuilder;
54 import javax.ws.rs.client.ClientRequestContext;
55 import javax.ws.rs.client.ClientRequestFilter;
56 import javax.ws.rs.client.ClientResponseContext;
57 import javax.ws.rs.client.ClientResponseFilter;
58 import javax.ws.rs.client.Entity;
59 import javax.ws.rs.client.Invocation;
60 import javax.ws.rs.client.WebTarget;
61 import javax.ws.rs.core.Configuration;
62 import javax.ws.rs.core.HttpHeaders;
63 import javax.ws.rs.core.MediaType;
64 import javax.ws.rs.core.MultivaluedHashMap;
65 import javax.ws.rs.core.MultivaluedMap;
66 import javax.ws.rs.core.Response;
67
68 /**
69  * Http Authentication filter that provides basic and digest authentication (based on RFC 2617).
70  *
71  * @author Miroslav Fuksa
72  */
73 @Priority(Priorities.AUTHENTICATION)
74 class HttpAuthenticationFilter implements ClientRequestFilter, ClientResponseFilter {
75
76     /**
77      * Authentication type.
78      */
79     enum Type {
80         /**
81          * Basic authentication.
82          */
83         BASIC
84     }
85
86     private static final String REQUEST_PROPERTY_FILTER_REUSED = "org.openhab.binding.lametrictime.api.authentication.HttpAuthenticationFilter.reused";
87     private static final String REQUEST_PROPERTY_OPERATION = "org.openhab.binding.lametrictime.api.authentication.HttpAuthenticationFilter.operation";
88
89     /**
90      * Encoding used for authentication calculations.
91      */
92     static final Charset CHARACTER_SET = Charset.forName("iso-8859-1");
93
94     private final HttpAuthenticationFeature.Mode mode;
95
96     private final BasicAuthenticator basicAuth;
97
98     /**
99      * Create a new filter instance.
100      *
101      * @param mode Mode.
102      * @param basicCredentials Basic credentials (can be {@code null} if this filter does not work in the
103      *            basic mode or if no default credentials are defined).
104      * @param digestCredentials Digest credentials (can be {@code null} if this filter does not work in the
105      *            digest mode or if no default credentials are defined).
106      * @param configuration Configuration (non-{@code null}).
107      */
108     HttpAuthenticationFilter(HttpAuthenticationFeature.Mode mode, Credentials basicCredentials,
109             Configuration configuration) {
110         this.mode = mode;
111         switch (mode) {
112             case BASIC_PREEMPTIVE:
113             case BASIC_NON_PREEMPTIVE:
114                 this.basicAuth = new BasicAuthenticator(basicCredentials);
115                 break;
116             case UNIVERSAL:
117                 this.basicAuth = new BasicAuthenticator(basicCredentials);
118                 break;
119             default:
120                 throw new IllegalStateException("Not implemented.");
121         }
122     }
123
124     @Override
125     public void filter(ClientRequestContext request) throws IOException {
126         if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) {
127             return;
128         }
129
130         if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
131             return;
132         }
133
134         Type operation = null;
135         if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) {
136             basicAuth.filterRequest(request);
137             operation = Type.BASIC;
138         } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) {
139             // do nothing
140         }
141
142         if (operation != null) {
143             request.setProperty(REQUEST_PROPERTY_OPERATION, operation);
144         }
145     }
146
147     @Override
148     public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException {
149         if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) {
150             return;
151         }
152
153         Type result = null; // which authentication is requested: BASIC or DIGEST
154         boolean authenticate;
155
156         if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) {
157             String authString = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE);
158             if (authString != null) {
159                 final String upperCaseAuth = authString.trim().toUpperCase();
160                 if (upperCaseAuth.startsWith("BASIC")) {
161                     result = Type.BASIC;
162                 } else {
163                     // unknown authentication -> this filter cannot authenticate with this method
164                     return;
165                 }
166             }
167             authenticate = true;
168         } else {
169             authenticate = false;
170         }
171
172         if (mode == HttpAuthenticationFeature.Mode.BASIC_PREEMPTIVE) {
173             // do nothing -> 401 will be returned to the client
174         } else if (mode == HttpAuthenticationFeature.Mode.BASIC_NON_PREEMPTIVE) {
175             if (authenticate && result == Type.BASIC) {
176                 basicAuth.filterResponseAndAuthenticate(request, response);
177             }
178         } else if (mode == HttpAuthenticationFeature.Mode.UNIVERSAL) {
179             if (authenticate) {
180                 boolean success = false;
181
182                 // now we have the challenge response and we can authenticate
183                 if (result == Type.BASIC) {
184                     success = basicAuth.filterResponseAndAuthenticate(request, response);
185                 }
186             }
187         }
188     }
189
190     private String getCacheKey(ClientRequestContext request) {
191         return request.getUri().toString() + ":" + request.getMethod();
192     }
193
194     /**
195      * Repeat the {@code request} with provided {@code newAuthorizationHeader}
196      * and update the {@code response} with newest response data.
197      *
198      * @param request Request context.
199      * @param response Response context (will be updated with the new response data).
200      * @param newAuthorizationHeader {@code Authorization} header that should be added to the new request.
201      * @return {@code true} is the authentication was successful ({@code true} if 401 response code was not returned;
202      *         {@code false} otherwise).
203      */
204     static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response,
205             String newAuthorizationHeader) {
206         Client client = ClientBuilder.newClient(request.getConfiguration());
207         String method = request.getMethod();
208         MediaType mediaType = request.getMediaType();
209         URI lUri = request.getUri();
210
211         WebTarget resourceTarget = client.target(lUri);
212
213         Invocation.Builder builder = resourceTarget.request(mediaType);
214
215         MultivaluedMap<String, Object> newHeaders = new MultivaluedHashMap<String, Object>();
216
217         for (Map.Entry<String, List<Object>> entry : request.getHeaders().entrySet()) {
218             if (HttpHeaders.AUTHORIZATION.equals(entry.getKey())) {
219                 continue;
220             }
221             newHeaders.put(entry.getKey(), entry.getValue());
222         }
223
224         newHeaders.add(HttpHeaders.AUTHORIZATION, newAuthorizationHeader);
225         builder.headers(newHeaders);
226
227         builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true");
228
229         Invocation invocation;
230         if (request.getEntity() == null) {
231             invocation = builder.build(method);
232         } else {
233             invocation = builder.build(method, Entity.entity(request.getEntity(), request.getMediaType()));
234         }
235         Response nextResponse = invocation.invoke();
236
237         if (nextResponse.hasEntity()) {
238             response.setEntityStream(nextResponse.readEntity(InputStream.class));
239         }
240         MultivaluedMap<String, String> headers = response.getHeaders();
241         headers.clear();
242         headers.putAll(nextResponse.getStringHeaders());
243         response.setStatus(nextResponse.getStatus());
244
245         return response.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode();
246     }
247
248     /**
249      * Credentials (username + password).
250      */
251     static class Credentials {
252
253         private final String username;
254         private final byte[] password;
255
256         /**
257          * Create a new credentials from username and password as byte array.
258          *
259          * @param username Username.
260          * @param password Password as byte array.
261          */
262         Credentials(String username, byte[] password) {
263             this.username = username;
264             this.password = password;
265         }
266
267         /**
268          * Create a new credentials from username and password as {@link String}.
269          *
270          * @param username Username.
271          * @param password {@code String} password.
272          */
273         Credentials(String username, String password) {
274             this.username = username;
275             this.password = password == null ? null : password.getBytes(CHARACTER_SET);
276         }
277
278         /**
279          * Return username.
280          *
281          * @return username.
282          */
283         String getUsername() {
284             return username;
285         }
286
287         /**
288          * Return password as byte array.
289          *
290          * @return Password string in byte array representation.
291          */
292         byte[] getPassword() {
293             return password;
294         }
295     }
296
297     private static Credentials extractCredentials(ClientRequestContext request, Type type) {
298         String usernameKey = null;
299         String passwordKey = null;
300         if (type == null) {
301             usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_USERNAME;
302             passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_PASSWORD;
303         } else if (type == Type.BASIC) {
304             usernameKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME;
305             passwordKey = HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD;
306         }
307
308         String userName = (String) request.getProperty(usernameKey);
309         if (userName != null && !userName.equals("")) {
310             byte[] pwdBytes;
311             Object password = request.getProperty(passwordKey);
312             if (password instanceof byte[]) {
313                 pwdBytes = ((byte[]) password);
314             } else if (password instanceof String) {
315                 pwdBytes = ((String) password).getBytes(CHARACTER_SET);
316             } else {
317                 throw new RequestAuthenticationException("Passwort invalid.");
318             }
319             return new Credentials(userName, pwdBytes);
320         }
321         return null;
322     }
323
324     /**
325      * Get credentials actual for the current request. Priorities in credentials selection are the following:
326      * <ol>
327      * <li>Basic/digest specific credentials defined in the request properties</li>
328      * <li>Common credentials defined in the request properties</li>
329      * <li>{@code defaultCredentials}</li>
330      * </ol>
331      *
332      * @param request Request from which credentials should be extracted.
333      * @param defaultCredentials Default credentials (can be {@code null}).
334      * @param type Type of requested credentials.
335      * @return Credentials or {@code null} if no credentials are found and {@code defaultCredentials} are {@code null}.
336      * @throws RequestAuthenticationException in case the {@code username} or {@code password} is invalid.
337      */
338     static Credentials getCredentials(ClientRequestContext request, Credentials defaultCredentials, Type type) {
339         Credentials commonCredentials = extractCredentials(request, type);
340
341         if (commonCredentials != null) {
342             return commonCredentials;
343         } else {
344             Credentials specificCredentials = extractCredentials(request, null);
345
346             return specificCredentials != null ? specificCredentials : defaultCredentials;
347         }
348     }
349 }