blob: fb00bb07bb3e63f132006f43ee0e265686785e03 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2015 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.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.HttpMethod;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Utility class that handles HTTP redirects.
* <p>
* Applications can disable redirection via {@link Request#followRedirects(boolean)}
* and then rely on this class to perform the redirect in a simpler way, for example:
* <pre>
* HttpRedirector redirector = new HttpRedirector(httpClient);
*
* Request request = httpClient.newRequest("http://host/path").followRedirects(false);
* ContentResponse response = request.send();
* while (redirector.isRedirect(response))
* {
* // Validate the redirect URI
* if (!validate(redirector.extractRedirectURI(response)))
* break;
*
* Result result = redirector.redirect(request, response);
* request = result.getRequest();
* response = result.getResponse();
* }
* </pre>
*/
public class HttpRedirector
{
private static final Logger LOG = Log.getLogger(HttpRedirector.class);
private static final String SCHEME_REGEXP = "(^https?)";
private static final String AUTHORITY_REGEXP = "([^/\\?#]+)";
// The location may be relative so the scheme://authority part may be missing
private static final String DESTINATION_REGEXP = "(" + SCHEME_REGEXP + "://" + AUTHORITY_REGEXP + ")?";
private static final String PATH_REGEXP = "([^\\?#]*)";
private static final String QUERY_REGEXP = "([^#]*)";
private static final String FRAGMENT_REGEXP = "(.*)";
private static final Pattern URI_PATTERN = Pattern.compile(DESTINATION_REGEXP + PATH_REGEXP + QUERY_REGEXP + FRAGMENT_REGEXP);
private static final String ATTRIBUTE = HttpRedirector.class.getName() + ".redirects";
private final HttpClient client;
private final ResponseNotifier notifier;
public HttpRedirector(HttpClient client)
{
this.client = client;
this.notifier = new ResponseNotifier();
}
/**
* @param response the response to check for redirects
* @return whether the response code is a HTTP redirect code
*/
public boolean isRedirect(Response response)
{
switch (response.getStatus())
{
case 301:
case 302:
case 303:
case 307:
case 308:
return true;
default:
return false;
}
}
/**
* Redirects the given {@code response}, blocking until the redirect is complete.
*
* @param request the original request that triggered the redirect
* @param response the response to the original request
* @return a {@link Result} object containing the request to the redirected location and its response
* @throws InterruptedException if the thread is interrupted while waiting for the redirect to complete
* @throws ExecutionException if the redirect failed
* @see #redirect(Request, Response, Response.CompleteListener)
*/
public Result redirect(Request request, Response response) throws InterruptedException, ExecutionException
{
final AtomicReference<Result> resultRef = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
Request redirect = redirect(request, response, new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
resultRef.set(new Result(result.getRequest(),
result.getRequestFailure(),
new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding()),
result.getResponseFailure()));
latch.countDown();
}
});
try
{
latch.await();
Result result = resultRef.get();
if (result.isFailed())
throw new ExecutionException(result.getFailure());
return result;
}
catch (InterruptedException x)
{
// If the application interrupts, we need to abort the redirect
redirect.abort(x);
throw x;
}
}
/**
* Redirects the given {@code response} asynchronously.
*
* @param request the original request that triggered the redirect
* @param response the response to the original request
* @param listener the listener that receives response events
* @return the request to the redirected location
*/
public Request redirect(Request request, Response response, Response.CompleteListener listener)
{
if (isRedirect(response))
{
String location = response.getHeaders().get("Location");
URI newURI = extractRedirectURI(response);
if (newURI != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Redirecting to {} (Location: {})", newURI, location);
return redirect(request, response, listener, newURI);
}
else
{
fail(request, response, new HttpResponseException("Invalid 'Location' header: " + location, response));
return null;
}
}
else
{
fail(request, response, new HttpResponseException("Cannot redirect: " + response, response));
return null;
}
}
/**
* Extracts and sanitizes (by making it absolute and escaping paths and query parameters)
* the redirect URI of the given {@code response}.
*
* @param response the response to extract the redirect URI from
* @return the absolute redirect URI, or null if the response does not contain a valid redirect location
*/
public URI extractRedirectURI(Response response)
{
String location = response.getHeaders().get("location");
if (location != null)
return sanitize(location);
return null;
}
private URI sanitize(String location)
{
// Redirects should be valid, absolute, URIs, with properly escaped paths and encoded
// query parameters. However, shit happens, and here we try our best to recover.
try
{
// Direct hit first: if passes, we're good
return new URI(location);
}
catch (URISyntaxException x)
{
Matcher matcher = URI_PATTERN.matcher(location);
if (matcher.matches())
{
String scheme = matcher.group(2);
String authority = matcher.group(3);
String path = matcher.group(4);
String query = matcher.group(5);
if (query.length() == 0)
query = null;
String fragment = matcher.group(6);
if (fragment.length() == 0)
fragment = null;
try
{
return new URI(scheme, authority, path, query, fragment);
}
catch (URISyntaxException xx)
{
// Give up
}
}
return null;
}
}
private Request redirect(Request request, Response response, Response.CompleteListener listener, URI newURI)
{
if (!newURI.isAbsolute())
newURI = request.getURI().resolve(newURI);
int status = response.getStatus();
switch (status)
{
case 301:
{
String method = request.getMethod();
if (HttpMethod.GET.is(method) || HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
return redirect(request, response, listener, newURI, method);
else if (HttpMethod.POST.is(method))
return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
fail(request, response, new HttpResponseException("HTTP protocol violation: received 301 for non GET/HEAD/POST/PUT request", response));
return null;
}
case 302:
{
String method = request.getMethod();
if (HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
return redirect(request, response, listener, newURI, method);
else
return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
}
case 303:
{
String method = request.getMethod();
if (HttpMethod.HEAD.is(method))
return redirect(request, response, listener, newURI, method);
else
return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
}
case 307:
case 308:
{
// Keep same method
return redirect(request, response, listener, newURI, request.getMethod());
}
default:
{
fail(request, response, new HttpResponseException("Unhandled HTTP status code " + status, response));
return null;
}
}
}
private Request redirect(Request request, Response response, Response.CompleteListener listener, URI location, String method)
{
HttpRequest httpRequest = (HttpRequest)request;
HttpConversation conversation = httpRequest.getConversation();
Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE);
if (redirects == null)
redirects = 0;
if (redirects < client.getMaxRedirects())
{
++redirects;
conversation.setAttribute(ATTRIBUTE, redirects);
return sendRedirect(httpRequest, response, listener, location, method);
}
else
{
fail(request, response, new HttpResponseException("Max redirects exceeded " + redirects, response));
return null;
}
}
private Request sendRedirect(final HttpRequest httpRequest, Response response, Response.CompleteListener listener, URI location, String method)
{
try
{
Request redirect = client.copyRequest(httpRequest, location);
// Use given method
redirect.method(method);
redirect.onRequestBegin(new Request.BeginListener()
{
@Override
public void onBegin(Request redirect)
{
Throwable cause = httpRequest.getAbortCause();
if (cause != null)
redirect.abort(cause);
}
});
redirect.send(listener);
return redirect;
}
catch (Throwable x)
{
fail(httpRequest, response, x);
return null;
}
}
protected void fail(Request request, Response response, Throwable failure)
{
HttpConversation conversation = ((HttpRequest)request).getConversation();
conversation.updateResponseListeners(null);
List<Response.ResponseListener> listeners = conversation.getResponseListeners();
notifier.notifyFailure(listeners, response, failure);
notifier.notifyComplete(listeners, new Result(request, response, failure));
}
}