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.HttpHeader;
36  import org.eclipse.jetty.http.HttpMethod;
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 = 4096;
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      private static final String AUTHENTICATION_ATTRIBUTE = AuthenticationProtocolHandler.class.getName() + ".authentication";
46  
47      private final HttpClient client;
48      private final int maxContentLength;
49      private final ResponseNotifier notifier;
50  
51      protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength)
52      {
53          this.client = client;
54          this.maxContentLength = maxContentLength;
55          this.notifier = new ResponseNotifier();
56      }
57  
58      protected HttpClient getHttpClient()
59      {
60          return client;
61      }
62  
63      protected abstract HttpHeader getAuthenticateHeader();
64  
65      protected abstract HttpHeader getAuthorizationHeader();
66  
67      protected abstract URI getAuthenticationURI(Request request);
68  
69      @Override
70      public Response.Listener getResponseListener()
71      {
72          // Return new instances every time to keep track of the response content
73          return new AuthenticationListener();
74      }
75  
76      private class AuthenticationListener extends BufferingResponseListener
77      {
78          private AuthenticationListener()
79          {
80              super(maxContentLength);
81          }
82  
83          @Override
84          public void onComplete(Result result)
85          {
86              HttpRequest request = (HttpRequest)result.getRequest();
87              ContentResponse response = new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding());
88              if (result.isFailed())
89              {
90                  Throwable failure = result.getFailure();
91                  if (LOG.isDebugEnabled())
92                      LOG.debug("Authentication challenge failed {}", failure);
93                  forwardFailureComplete(request, result.getRequestFailure(), response, result.getResponseFailure());
94                  return;
95              }
96  
97              HttpConversation conversation = request.getConversation();
98              if (conversation.getAttribute(AUTHENTICATION_ATTRIBUTE) != null)
99              {
100                 // We have already tried to authenticate, but we failed again
101                 if (LOG.isDebugEnabled())
102                     LOG.debug("Bad credentials for {}", request);
103                 forwardSuccessComplete(request, response);
104                 return;
105             }
106 
107             HttpHeader header = getAuthenticateHeader();
108             List<Authentication.HeaderInfo> headerInfos = parseAuthenticateHeader(response, header);
109             if (headerInfos.isEmpty())
110             {
111                 if (LOG.isDebugEnabled())
112                     LOG.debug("Authentication challenge without {} header", header);
113                 forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: Authentication challenge without " + header + " header", response));
114                 return;
115             }
116 
117             Authentication authentication = null;
118             Authentication.HeaderInfo headerInfo = null;
119             URI authURI = getAuthenticationURI(request);
120             if (authURI != null)
121             {
122                 for (Authentication.HeaderInfo element : headerInfos)
123                 {
124                     authentication = client.getAuthenticationStore().findAuthentication(element.getType(), authURI, element.getRealm());
125                     if (authentication != null)
126                     {
127                         headerInfo = element;
128                         break;
129                     }
130                 }
131             }
132             if (authentication == null)
133             {
134                 if (LOG.isDebugEnabled())
135                     LOG.debug("No authentication available for {}", request);
136                 forwardSuccessComplete(request, response);
137                 return;
138             }
139 
140             try
141             {
142                 final Authentication.Result authnResult = authentication.authenticate(request, response, headerInfo, conversation);
143                 if (LOG.isDebugEnabled())
144                     LOG.debug("Authentication result {}", authnResult);
145                 if (authnResult == null)
146                 {
147                     forwardSuccessComplete(request, response);
148                     return;
149                 }
150 
151                 conversation.setAttribute(AUTHENTICATION_ATTRIBUTE, true);
152 
153                 URI requestURI = request.getURI();
154                 String path = null;
155                 if (HttpMethod.CONNECT.is(request.getMethod()))
156                 {
157                     String uri = request.getScheme() + "://" + request.getHost();
158                     int port = request.getPort();
159                     if (port > 0)
160                         uri += ":" + port;
161                     requestURI = URI.create(uri);
162                     path = request.getPath();
163                 }
164                 Request newRequest = client.copyRequest(request, requestURI);
165                 if (path != null)
166                     newRequest.path(path);
167 
168                 authnResult.apply(newRequest);
169 
170                 newRequest.onResponseSuccess(new Response.SuccessListener()
171                 {
172                     @Override
173                     public void onSuccess(Response response)
174                     {
175                         client.getAuthenticationStore().addAuthenticationResult(authnResult);
176                     }
177                 });
178 
179                 Connection connection = (Connection)request.getAttributes().get(Connection.class.getName());
180                 if (connection != null)
181                     connection.send(newRequest, null);
182                 else
183                     newRequest.send(null);
184             }
185             catch (Throwable x)
186             {
187                 if (LOG.isDebugEnabled())
188                     LOG.debug("Authentication failed", x);
189                 forwardFailureComplete(request, null, response, x);
190             }
191         }
192 
193         private void forwardSuccessComplete(HttpRequest request, Response response)
194         {
195             HttpConversation conversation = request.getConversation();
196             conversation.updateResponseListeners(null);
197             notifier.forwardSuccessComplete(conversation.getResponseListeners(), request, response);
198         }
199 
200         private void forwardFailureComplete(HttpRequest request, Throwable requestFailure, Response response, Throwable responseFailure)
201         {
202             HttpConversation conversation = request.getConversation();
203             conversation.updateResponseListeners(null);
204             notifier.forwardFailureComplete(conversation.getResponseListeners(), request, requestFailure, response, responseFailure);
205         }
206 
207         private List<Authentication.HeaderInfo> parseAuthenticateHeader(Response response, HttpHeader header)
208         {
209             // TODO: these should be ordered by strength
210             List<Authentication.HeaderInfo> result = new ArrayList<>();
211             List<String> values = Collections.list(response.getHeaders().getValues(header.asString()));
212             for (String value : values)
213             {
214                 Matcher matcher = AUTHENTICATE_PATTERN.matcher(value);
215                 if (matcher.matches())
216                 {
217                     String type = matcher.group(1);
218                     String realm = matcher.group(2);
219                     String params = matcher.group(3);
220                     Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader());
221                     result.add(headerInfo);
222                 }
223             }
224             return result;
225         }
226     }
227 }