| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.client; |
| |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.jetty.client.api.Authentication; |
| import org.eclipse.jetty.client.api.Connection; |
| import org.eclipse.jetty.client.api.ContentResponse; |
| import org.eclipse.jetty.client.api.Request; |
| import org.eclipse.jetty.client.api.Response; |
| import org.eclipse.jetty.client.api.Result; |
| import org.eclipse.jetty.client.util.BufferingResponseListener; |
| import org.eclipse.jetty.http.HttpField; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| public abstract class AuthenticationProtocolHandler implements ProtocolHandler |
| { |
| public static final int DEFAULT_MAX_CONTENT_LENGTH = 16*1024; |
| public static final Logger LOG = Log.getLogger(AuthenticationProtocolHandler.class); |
| private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+realm=\"([^\"]*)\"(.*)", Pattern.CASE_INSENSITIVE); |
| |
| private final HttpClient client; |
| private final int maxContentLength; |
| private final ResponseNotifier notifier; |
| |
| protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength) |
| { |
| this.client = client; |
| this.maxContentLength = maxContentLength; |
| this.notifier = new ResponseNotifier(); |
| } |
| |
| protected HttpClient getHttpClient() |
| { |
| return client; |
| } |
| |
| protected abstract HttpHeader getAuthenticateHeader(); |
| |
| protected abstract HttpHeader getAuthorizationHeader(); |
| |
| protected abstract URI getAuthenticationURI(Request request); |
| |
| protected abstract String getAuthenticationAttribute(); |
| |
| @Override |
| public Response.Listener getResponseListener() |
| { |
| // Return new instances every time to keep track of the response content |
| return new AuthenticationListener(); |
| } |
| |
| private class AuthenticationListener extends BufferingResponseListener |
| { |
| private AuthenticationListener() |
| { |
| super(maxContentLength); |
| } |
| |
| @Override |
| public void onComplete(Result result) |
| { |
| HttpRequest request = (HttpRequest)result.getRequest(); |
| ContentResponse response = new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding()); |
| if (result.isFailed()) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Authentication challenge failed {}", result.getFailure()); |
| forwardFailureComplete(request, result.getRequestFailure(), response, result.getResponseFailure()); |
| return; |
| } |
| |
| String authenticationAttribute = getAuthenticationAttribute(); |
| HttpConversation conversation = request.getConversation(); |
| if (conversation.getAttribute(authenticationAttribute) != null) |
| { |
| // We have already tried to authenticate, but we failed again |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Bad credentials for {}", request); |
| forwardSuccessComplete(request, response); |
| return; |
| } |
| |
| HttpHeader header = getAuthenticateHeader(); |
| List<Authentication.HeaderInfo> headerInfos = parseAuthenticateHeader(response, header); |
| if (headerInfos.isEmpty()) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Authentication challenge without {} header", header); |
| forwardFailureComplete(request, null, response, new HttpResponseException("HTTP protocol violation: Authentication challenge without " + header + " header", response)); |
| return; |
| } |
| |
| Authentication authentication = null; |
| Authentication.HeaderInfo headerInfo = null; |
| URI authURI = resolveURI(request, getAuthenticationURI(request)); |
| if (authURI != null) |
| { |
| for (Authentication.HeaderInfo element : headerInfos) |
| { |
| authentication = client.getAuthenticationStore().findAuthentication(element.getType(), authURI, element.getRealm()); |
| if (authentication != null) |
| { |
| headerInfo = element; |
| break; |
| } |
| } |
| } |
| if (authentication == null) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("No authentication available for {}", request); |
| forwardSuccessComplete(request, response); |
| return; |
| } |
| |
| try |
| { |
| final Authentication.Result authnResult = authentication.authenticate(request, response, headerInfo, conversation); |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Authentication result {}", authnResult); |
| if (authnResult == null) |
| { |
| forwardSuccessComplete(request, response); |
| return; |
| } |
| |
| conversation.setAttribute(authenticationAttribute, true); |
| |
| URI requestURI = request.getURI(); |
| String path = null; |
| if (requestURI == null) |
| { |
| requestURI = resolveURI(request, null); |
| path = request.getPath(); |
| } |
| Request newRequest = client.copyRequest(request, requestURI); |
| if (path != null) |
| newRequest.path(path); |
| |
| authnResult.apply(newRequest); |
| // Copy existing, explicitly set, authorization headers. |
| copyIfAbsent(request, newRequest, HttpHeader.AUTHORIZATION); |
| copyIfAbsent(request, newRequest, HttpHeader.PROXY_AUTHORIZATION); |
| |
| newRequest.onResponseSuccess(r -> client.getAuthenticationStore().addAuthenticationResult(authnResult)); |
| |
| Connection connection = (Connection)request.getAttributes().get(Connection.class.getName()); |
| if (connection != null) |
| connection.send(newRequest, null); |
| else |
| newRequest.send(null); |
| } |
| catch (Throwable x) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Authentication failed", x); |
| forwardFailureComplete(request, null, response, x); |
| } |
| } |
| |
| private URI resolveURI(HttpRequest request, URI uri) |
| { |
| if (uri != null) |
| return uri; |
| String target = request.getScheme() + "://" + request.getHost(); |
| int port = request.getPort(); |
| if (port > 0) |
| target += ":" + port; |
| return URI.create(target); |
| } |
| |
| private void copyIfAbsent(HttpRequest oldRequest, Request newRequest, HttpHeader header) |
| { |
| HttpField field = oldRequest.getHeaders().getField(header); |
| if (field != null && !newRequest.getHeaders().contains(header)) |
| newRequest.getHeaders().put(field); |
| } |
| |
| private void forwardSuccessComplete(HttpRequest request, Response response) |
| { |
| HttpConversation conversation = request.getConversation(); |
| conversation.updateResponseListeners(null); |
| notifier.forwardSuccessComplete(conversation.getResponseListeners(), request, response); |
| } |
| |
| private void forwardFailureComplete(HttpRequest request, Throwable requestFailure, Response response, Throwable responseFailure) |
| { |
| HttpConversation conversation = request.getConversation(); |
| conversation.updateResponseListeners(null); |
| List<Response.ResponseListener> responseListeners = conversation.getResponseListeners(); |
| if (responseFailure == null) |
| notifier.forwardSuccess(responseListeners, response); |
| else |
| notifier.forwardFailure(responseListeners, response, responseFailure); |
| notifier.notifyComplete(responseListeners, new Result(request, requestFailure, response, responseFailure)); |
| } |
| |
| private List<Authentication.HeaderInfo> parseAuthenticateHeader(Response response, HttpHeader header) |
| { |
| // TODO: these should be ordered by strength |
| List<Authentication.HeaderInfo> result = new ArrayList<>(); |
| List<String> values = Collections.list(response.getHeaders().getValues(header.asString())); |
| for (String value : values) |
| { |
| Matcher matcher = AUTHENTICATE_PATTERN.matcher(value); |
| if (matcher.matches()) |
| { |
| String type = matcher.group(1); |
| String realm = matcher.group(2); |
| String params = matcher.group(3); |
| Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader()); |
| result.add(headerInfo); |
| } |
| } |
| return result; |
| } |
| } |
| } |