View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.client;
20  
21  import java.net.URI;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import org.eclipse.jetty.client.api.Authentication;
29  import org.eclipse.jetty.client.api.Connection;
30  import org.eclipse.jetty.client.api.ContentResponse;
31  import org.eclipse.jetty.client.api.Request;
32  import org.eclipse.jetty.client.api.Response;
33  import org.eclipse.jetty.client.api.Result;
34  import org.eclipse.jetty.client.util.BufferingResponseListener;
35  import org.eclipse.jetty.http.HttpField;
36  import org.eclipse.jetty.http.HttpHeader;
37  import org.eclipse.jetty.util.log.Log;
38  import org.eclipse.jetty.util.log.Logger;
39  
40  public abstract class AuthenticationProtocolHandler implements ProtocolHandler
41  {
42      public static final int DEFAULT_MAX_CONTENT_LENGTH = 16*1024;
43      public static final Logger LOG = Log.getLogger(AuthenticationProtocolHandler.class);
44      private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+realm=\"([^\"]*)\"(.*)", Pattern.CASE_INSENSITIVE);
45  
46      private final HttpClient client;
47      private final int maxContentLength;
48      private final ResponseNotifier notifier;
49  
50      protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength)
51      {
52          this.client = client;
53          this.maxContentLength = maxContentLength;
54          this.notifier = new ResponseNotifier();
55      }
56  
57      protected HttpClient getHttpClient()
58      {
59          return client;
60      }
61  
62      protected abstract HttpHeader getAuthenticateHeader();
63  
64      protected abstract HttpHeader getAuthorizationHeader();
65  
66      protected abstract URI getAuthenticationURI(Request request);
67  
68      protected abstract String getAuthenticationAttribute();
69  
70      @Override
71      public Response.Listener getResponseListener()
72      {
73          // Return new instances every time to keep track of the response content
74          return new AuthenticationListener();
75      }
76  
77      private class AuthenticationListener extends BufferingResponseListener
78      {
79          private AuthenticationListener()
80          {
81              super(maxContentLength);
82          }
83  
84          @Override
85          public void onComplete(Result result)
86          {
87              HttpRequest request = (HttpRequest)result.getRequest();
88              ContentResponse response = new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding());
89              if (result.isFailed())
90              {
91                  if (LOG.isDebugEnabled())
92                      LOG.debug("Authentication challenge failed {}", result.getFailure());
93                  forwardFailureComplete(request, result.getRequestFailure(), response, result.getResponseFailure());
94                  return;
95              }
96  
97              String authenticationAttribute = getAuthenticationAttribute();
98              HttpConversation conversation = request.getConversation();
99              if (conversation.getAttribute(authenticationAttribute) != null)
100             {
101                 // We have already tried to authenticate, but we failed again
102                 if (LOG.isDebugEnabled())
103                     LOG.debug("Bad credentials for {}", request);
104                 forwardSuccessComplete(request, response);
105                 return;
106             }
107 
108             HttpHeader header = getAuthenticateHeader();
109             List<Authentication.HeaderInfo> headerInfos = parseAuthenticateHeader(response, header);
110             if (headerInfos.isEmpty())
111             {
112                 if (LOG.isDebugEnabled())
113                     LOG.debug("Authentication challenge without {} header", header);
114                 forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: Authentication challenge without " + header + " header", response));
115                 return;
116             }
117 
118             Authentication authentication = null;
119             Authentication.HeaderInfo headerInfo = null;
120             URI authURI = resolveURI(request, getAuthenticationURI(request));
121             if (authURI != null)
122             {
123                 for (Authentication.HeaderInfo element : headerInfos)
124                 {
125                     authentication = client.getAuthenticationStore().findAuthentication(element.getType(), authURI, element.getRealm());
126                     if (authentication != null)
127                     {
128                         headerInfo = element;
129                         break;
130                     }
131                 }
132             }
133             if (authentication == null)
134             {
135                 if (LOG.isDebugEnabled())
136                     LOG.debug("No authentication available for {}", request);
137                 forwardSuccessComplete(request, response);
138                 return;
139             }
140 
141             try
142             {
143                 final Authentication.Result authnResult = authentication.authenticate(request, response, headerInfo, conversation);
144                 if (LOG.isDebugEnabled())
145                     LOG.debug("Authentication result {}", authnResult);
146                 if (authnResult == null)
147                 {
148                     forwardSuccessComplete(request, response);
149                     return;
150                 }
151 
152                 conversation.setAttribute(authenticationAttribute, true);
153 
154                 URI requestURI = request.getURI();
155                 String path = null;
156                 if (requestURI == null)
157                 {
158                     requestURI = resolveURI(request, null);
159                     path = request.getPath();
160                 }
161                 Request newRequest = client.copyRequest(request, requestURI);
162                 if (path != null)
163                     newRequest.path(path);
164 
165                 authnResult.apply(newRequest);
166                 // Copy existing, explicitly set, authorization headers.
167                 copyIfAbsent(request, newRequest, HttpHeader.AUTHORIZATION);
168                 copyIfAbsent(request, newRequest, HttpHeader.PROXY_AUTHORIZATION);
169 
170                 newRequest.onResponseSuccess(r -> client.getAuthenticationStore().addAuthenticationResult(authnResult));
171 
172                 Connection connection = (Connection)request.getAttributes().get(Connection.class.getName());
173                 if (connection != null)
174                     connection.send(newRequest, null);
175                 else
176                     newRequest.send(null);
177             }
178             catch (Throwable x)
179             {
180                 if (LOG.isDebugEnabled())
181                     LOG.debug("Authentication failed", x);
182                 forwardFailureComplete(request, null, response, x);
183             }
184         }
185 
186         private URI resolveURI(HttpRequest request, URI uri)
187         {
188             if (uri != null)
189                 return uri;
190             String target = request.getScheme() + "://" + request.getHost();
191             int port = request.getPort();
192             if (port > 0)
193                 target += ":" + port;
194             return URI.create(target);
195         }
196 
197         private void copyIfAbsent(HttpRequest oldRequest, Request newRequest, HttpHeader header)
198         {
199             HttpField field = oldRequest.getHeaders().getField(header);
200             if (field != null && !newRequest.getHeaders().contains(header))
201                 newRequest.getHeaders().put(field);
202         }
203 
204         private void forwardSuccessComplete(HttpRequest request, Response response)
205         {
206             HttpConversation conversation = request.getConversation();
207             conversation.updateResponseListeners(null);
208             notifier.forwardSuccessComplete(conversation.getResponseListeners(), request, response);
209         }
210 
211         private void forwardFailureComplete(HttpRequest request, Throwable requestFailure, Response response, Throwable responseFailure)
212         {
213             HttpConversation conversation = request.getConversation();
214             conversation.updateResponseListeners(null);
215             List<Response.ResponseListener> responseListeners = conversation.getResponseListeners();
216             if (responseFailure == null)
217                 notifier.forwardSuccess(responseListeners, response);
218             else
219                 notifier.forwardFailure(responseListeners, response, responseFailure);
220             notifier.notifyComplete(responseListeners, new Result(request, requestFailure, response, responseFailure));
221         }
222 
223         private List<Authentication.HeaderInfo> parseAuthenticateHeader(Response response, HttpHeader header)
224         {
225             // TODO: these should be ordered by strength
226             List<Authentication.HeaderInfo> result = new ArrayList<>();
227             List<String> values = Collections.list(response.getHeaders().getValues(header.asString()));
228             for (String value : values)
229             {
230                 Matcher matcher = AUTHENTICATE_PATTERN.matcher(value);
231                 if (matcher.matches())
232                 {
233                     String type = matcher.group(1);
234                     String realm = matcher.group(2);
235                     String params = matcher.group(3);
236                     Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader());
237                     result.add(headerInfo);
238                 }
239             }
240             return result;
241         }
242     }
243 }