Refactored connection routing and protocol execution code in the classic exec chain

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1793325 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2017-05-01 13:06:48 +00:00
parent bde89fee57
commit d43ee7177b
13 changed files with 1103 additions and 1320 deletions

View File

@ -29,6 +29,6 @@ package org.apache.hc.client5.http.impl.sync;
public enum ChainElements {
REDIRECT, BACK_OFF, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, PROTOCOL, MAIN_TRANSPORT
REDIRECT, BACK_OFF, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, PROTOCOL, CONNECT, MAIN_TRANSPORT
}

View File

@ -0,0 +1,282 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.sync;
import java.io.IOException;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RouteTracker;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.routing.HttpRouteDirector;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecChainHandler;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.StatusLine;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.util.Args;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Request executor in the HTTP request execution chain
* that is responsible for establishing connection to the target
* origin server as specified by the current route.
*
* @since 5.0
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
public final class ConnectExec implements ExecChainHandler {
private final Logger log = LogManager.getLogger(getClass());
private final ConnectionReuseStrategy reuseStrategy;
private final HttpProcessor proxyHttpProcessor;
private final AuthenticationStrategy proxyAuthStrategy;
private final HttpAuthenticator authenticator;
private final HttpRouteDirector routeDirector;
public ConnectExec(
final ConnectionReuseStrategy reuseStrategy,
final HttpProcessor proxyHttpProcessor,
final AuthenticationStrategy proxyAuthStrategy) {
Args.notNull(reuseStrategy, "Connection reuse strategy");
Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
this.reuseStrategy = reuseStrategy;
this.proxyHttpProcessor = proxyHttpProcessor;
this.proxyAuthStrategy = proxyAuthStrategy;
this.authenticator = new HttpAuthenticator();
this.routeDirector = new BasicRouteDirector();
}
@Override
public ClassicHttpResponse execute(
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain) throws IOException, HttpException {
Args.notNull(request, "HTTP request");
Args.notNull(scope, "Scope");
final HttpRoute route = scope.route;
final HttpClientContext context = scope.clientContext;
final ExecRuntime execRuntime = scope.execRuntime;
if (!execRuntime.isConnectionAcquired()) {
final Object userToken = context.getUserToken();
execRuntime.acquireConnection(route, userToken, context);
}
try {
if (!execRuntime.isConnected()) {
this.log.debug("Opening connection " + route);
final RouteTracker tracker = new RouteTracker(route);
int step;
do {
final HttpRoute fact = tracker.toRoute();
step = this.routeDirector.nextStep(route, fact);
switch (step) {
case HttpRouteDirector.CONNECT_TARGET:
execRuntime.connect(context);
tracker.connectTarget(route.isSecure());
break;
case HttpRouteDirector.CONNECT_PROXY:
execRuntime.connect(context);
final HttpHost proxy = route.getProxyHost();
tracker.connectProxy(proxy, false);
break;
case HttpRouteDirector.TUNNEL_TARGET: {
final boolean secure = createTunnelToTarget(route, request, execRuntime, context);
this.log.debug("Tunnel to target created.");
tracker.tunnelTarget(secure);
} break;
case HttpRouteDirector.TUNNEL_PROXY: {
// The most simple example for this case is a proxy chain
// of two proxies, where P1 must be tunnelled to P2.
// route: Source -> P1 -> P2 -> Target (3 hops)
// fact: Source -> P1 -> Target (2 hops)
final int hop = fact.getHopCount()-1; // the hop to establish
final boolean secure = createTunnelToProxy(route, hop, context);
this.log.debug("Tunnel to proxy created.");
tracker.tunnelProxy(route.getHopTarget(hop), secure);
} break;
case HttpRouteDirector.LAYER_PROTOCOL:
execRuntime.upgradeTls(context);
tracker.layerProtocol(route.isSecure());
break;
case HttpRouteDirector.UNREACHABLE:
throw new HttpException("Unable to establish route: " +
"planned = " + route + "; current = " + fact);
case HttpRouteDirector.COMPLETE:
break;
default:
throw new IllegalStateException("Unknown step indicator "
+ step + " from RouteDirector.");
}
} while (step > HttpRouteDirector.COMPLETE);
}
return chain.proceed(request, scope);
} catch (final IOException | HttpException | RuntimeException ex) {
execRuntime.discardConnection();
throw ex;
}
}
/**
* Creates a tunnel to the target server.
* The connection must be established to the (last) proxy.
* A CONNECT request for tunnelling through the proxy will
* be created and sent, the response received and checked.
* This method does <i>not</i> processChallenge the connection with
* information about the tunnel, that is left to the caller.
*/
private boolean createTunnelToTarget(
final HttpRoute route,
final HttpRequest request,
final ExecRuntime execRuntime,
final HttpClientContext context) throws HttpException, IOException {
final RequestConfig config = context.getRequestConfig();
final HttpHost target = route.getTargetHost();
final HttpHost proxy = route.getProxyHost();
final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
ClassicHttpResponse response = null;
final String authority = target.toHostString();
final ClassicHttpRequest connect = new BasicClassicHttpRequest("CONNECT", target, authority);
connect.setVersion(HttpVersion.HTTP_1_1);
this.proxyHttpProcessor.process(connect, null, context);
while (response == null) {
if (!execRuntime.isConnected()) {
execRuntime.connect(context);
}
connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
response = execRuntime.execute(connect, context);
final int status = response.getCode();
if (status < HttpStatus.SC_SUCCESS) {
throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
}
if (config.isAuthenticationEnabled()) {
if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response,
proxyAuthExchange, context)) {
if (this.authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
this.proxyAuthStrategy, proxyAuthExchange, context)) {
// Retry request
if (this.reuseStrategy.keepAlive(request, response, context)) {
this.log.debug("Connection kept alive");
// Consume response content
final HttpEntity entity = response.getEntity();
EntityUtils.consume(entity);
} else {
execRuntime.disconnect();
}
response = null;
}
}
}
}
final int status = response.getCode();
if (status >= HttpStatus.SC_REDIRECTION) {
// Buffer response content
final HttpEntity entity = response.getEntity();
if (entity != null) {
response.setEntity(new BufferedHttpEntity(entity));
}
execRuntime.disconnect();
throw new TunnelRefusedException("CONNECT refused by proxy: " +
new StatusLine(response), response);
}
// How to decide on security of the tunnelled connection?
// The socket factory knows only about the segment to the proxy.
// Even if that is secure, the hop to the target may be insecure.
// Leave it to derived classes, consider insecure by default here.
return false;
}
/**
* Creates a tunnel to an intermediate proxy.
* This method is <i>not</i> implemented in this class.
* It just throws an exception here.
*/
private boolean createTunnelToProxy(
final HttpRoute route,
final int hop,
final HttpClientContext context) throws HttpException {
// Have a look at createTunnelToTarget and replicate the parts
// you need in a custom derived class. If your proxies don't require
// authentication, it is not too hard. But for the stock version of
// HttpClient, we cannot make such simplifying assumptions and would
// have to include proxy authentication code. The HttpComponents team
// is currently not in a position to support rarely used code of this
// complexity. Feel free to submit patches that refactor the code in
// createTunnelToTarget to facilitate re-use for proxy tunnelling.
throw new HttpException("Proxy chains are not supported.");
}
}

View File

@ -99,6 +99,7 @@ import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http.protocol.HttpProcessorBuilder;
import org.apache.hc.core5.http.protocol.RequestContent;
import org.apache.hc.core5.http.protocol.RequestTargetHost;
@ -725,6 +726,7 @@ public class HttpClientBuilder {
reuseStrategyCopy = DefaultConnectionReuseStrategy.INSTANCE;
}
}
ConnectionKeepAliveStrategy keepAliveStrategyCopy = this.keepAliveStrategy;
if (keepAliveStrategyCopy == null) {
keepAliveStrategyCopy = DefaultConnectionKeepAliveStrategy.INSTANCE;
@ -759,14 +761,14 @@ public class HttpClientBuilder {
final NamedElementChain<ExecChainHandler> execChainDefinition = new NamedElementChain<>();
execChainDefinition.addLast(
new MainClientExec(
reuseStrategyCopy,
keepAliveStrategyCopy,
new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)),
targetAuthStrategyCopy,
proxyAuthStrategyCopy,
userTokenHandlerCopy),
new MainClientExec(reuseStrategyCopy, keepAliveStrategyCopy, userTokenHandlerCopy),
ChainElements.MAIN_TRANSPORT.name());
execChainDefinition.addFirst(
new ConnectExec(
reuseStrategyCopy,
new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)),
proxyAuthStrategyCopy),
ChainElements.CONNECT.name());
final HttpProcessorBuilder b = HttpProcessorBuilder.create();
if (requestInterceptors != null) {
@ -813,8 +815,9 @@ public class HttpClientBuilder {
}
}
}
final HttpProcessor httpProcessor = b.build();
execChainDefinition.addFirst(
new ProtocolExec(b.build()),
new ProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy),
ChainElements.PROTOCOL.name());
// Add request retry executor, if not disabled

View File

@ -32,18 +32,9 @@ import java.io.InterruptedIOException;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RouteTracker;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.NonRepeatableRequestException;
import org.apache.hc.client5.http.protocol.UserTokenHandler;
import org.apache.hc.client5.http.routing.HttpRouteDirector;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecChainHandler;
import org.apache.hc.client5.http.sync.ExecRuntime;
@ -54,21 +45,7 @@ import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.RequestLine;
import org.apache.hc.core5.http.message.StatusLine;
import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http.protocol.RequestTargetHost;
import org.apache.hc.core5.net.URIAuthority;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
import org.apache.logging.log4j.LogManager;
@ -78,9 +55,6 @@ import org.apache.logging.log4j.Logger;
* The last request executor in the HTTP request execution chain
* that is responsible for execution of request / response
* exchanges with the opposite endpoint.
* This executor will automatically retry the request in case
* of an authentication challenge by an intermediate proxy or
* by the target server.
*
* @since 4.3
*/
@ -91,12 +65,7 @@ final class MainClientExec implements ExecChainHandler {
private final ConnectionReuseStrategy reuseStrategy;
private final ConnectionKeepAliveStrategy keepAliveStrategy;
private final HttpProcessor proxyHttpProcessor;
private final AuthenticationStrategy targetAuthStrategy;
private final AuthenticationStrategy proxyAuthStrategy;
private final HttpAuthenticator authenticator;
private final UserTokenHandler userTokenHandler;
private final HttpRouteDirector routeDirector;
/**
* @since 4.4
@ -104,37 +73,15 @@ final class MainClientExec implements ExecChainHandler {
public MainClientExec(
final ConnectionReuseStrategy reuseStrategy,
final ConnectionKeepAliveStrategy keepAliveStrategy,
final HttpProcessor proxyHttpProcessor,
final AuthenticationStrategy targetAuthStrategy,
final AuthenticationStrategy proxyAuthStrategy,
final UserTokenHandler userTokenHandler) {
Args.notNull(reuseStrategy, "Connection reuse strategy");
Args.notNull(keepAliveStrategy, "Connection keep alive strategy");
Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
Args.notNull(targetAuthStrategy, "Target authentication strategy");
Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
Args.notNull(userTokenHandler, "User token handler");
this.authenticator = new HttpAuthenticator();
this.routeDirector = new BasicRouteDirector();
this.reuseStrategy = reuseStrategy;
this.keepAliveStrategy = keepAliveStrategy;
this.proxyHttpProcessor = proxyHttpProcessor;
this.targetAuthStrategy = targetAuthStrategy;
this.proxyAuthStrategy = proxyAuthStrategy;
this.userTokenHandler = userTokenHandler;
}
public MainClientExec(
final ConnectionReuseStrategy reuseStrategy,
final ConnectionKeepAliveStrategy keepAliveStrategy,
final AuthenticationStrategy targetAuthStrategy,
final AuthenticationStrategy proxyAuthStrategy,
final UserTokenHandler userTokenHandler) {
this(reuseStrategy, keepAliveStrategy,
new DefaultHttpProcessor(new RequestTargetHost()),
targetAuthStrategy, proxyAuthStrategy, userTokenHandler);
}
@Override
public ClassicHttpResponse execute(
final ClassicHttpRequest request,
@ -146,116 +93,15 @@ final class MainClientExec implements ExecChainHandler {
final HttpClientContext context = scope.clientContext;
final ExecRuntime execRuntime = scope.execRuntime;
RequestEntityProxy.enhance(request);
Object userToken = context.getUserToken();
if (!execRuntime.isConnectionAcquired()) {
execRuntime.acquireConnection(route, userToken, context);
}
try {
final AuthExchange targetAuthExchange = context.getAuthExchange(route.getTargetHost());
final AuthExchange proxyAuthExchange = route.getProxyHost() != null ?
context.getAuthExchange(route.getProxyHost()) : new AuthExchange();
ClassicHttpResponse response;
for (int execCount = 1;; execCount++) {
if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) {
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity.");
}
if (!execRuntime.isConnected()) {
this.log.debug("Opening connection " + route);
try {
establishRoute(route, request, execRuntime, context);
} catch (final TunnelRefusedException ex) {
if (this.log.isDebugEnabled()) {
this.log.debug(ex.getMessage());
}
response = ex.getResponse();
break;
}
}
if (this.log.isDebugEnabled()) {
this.log.debug("Executing request " + new RequestLine(request));
}
if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
if (this.log.isDebugEnabled()) {
this.log.debug("Target auth state: " + targetAuthExchange.getState());
}
this.authenticator.addAuthResponse(
route.getTargetHost(), ChallengeType.TARGET, request, targetAuthExchange, context);
}
if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
if (this.log.isDebugEnabled()) {
this.log.debug("Proxy auth state: " + proxyAuthExchange.getState());
}
this.authenticator.addAuthResponse(
route.getProxyHost(), ChallengeType.PROXY, request, proxyAuthExchange, context);
}
response = execRuntime.execute(request, context);
// The connection is in or can be brought to a re-usable state.
if (reuseStrategy.keepAlive(request, response, context)) {
// Set the idle duration of this connection
final TimeValue duration = keepAliveStrategy.getKeepAliveDuration(response, context);
if (this.log.isDebugEnabled()) {
final String s;
if (duration != null) {
s = "for " + duration;
} else {
s = "indefinitely";
}
this.log.debug("Connection can be kept alive " + s);
}
execRuntime.setConnectionValidFor(duration);
execRuntime.markConnectionReusable();
} else {
execRuntime.markConnectionNonReusable();
}
if (request.getMethod().equalsIgnoreCase("TRACE")) {
// Do not perform authentication for TRACE request
break;
}
if (needAuthentication(
targetAuthExchange, proxyAuthExchange, route, request, response, context)) {
// Make sure the response body is fully consumed, if present
final HttpEntity entity = response.getEntity();
if (execRuntime.isConnectionReusable()) {
EntityUtils.consume(entity);
} else {
execRuntime.disconnect();
if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
&& proxyAuthExchange.getAuthScheme() != null
&& proxyAuthExchange.getAuthScheme().isConnectionBased()) {
this.log.debug("Resetting proxy auth state");
proxyAuthExchange.reset();
}
if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
&& targetAuthExchange.getAuthScheme() != null
&& targetAuthExchange.getAuthScheme().isConnectionBased()) {
this.log.debug("Resetting target auth state");
targetAuthExchange.reset();
}
}
// discard previous auth headers
final HttpRequest original = scope.originalRequest;
if (!original.containsHeader(HttpHeaders.AUTHORIZATION)) {
request.removeHeaders(HttpHeaders.AUTHORIZATION);
}
if (!original.containsHeader(HttpHeaders.PROXY_AUTHORIZATION)) {
request.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
}
} else {
break;
}
if (this.log.isDebugEnabled()) {
this.log.debug("Executing request " + new RequestLine(request));
}
RequestEntityProxy.enhance(request);
final ClassicHttpResponse response = execRuntime.execute(request, context);
Object userToken = context.getUserToken();
if (userToken == null) {
userToken = userTokenHandler.getUserToken(route, context);
context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
@ -264,6 +110,24 @@ final class MainClientExec implements ExecChainHandler {
execRuntime.setConnectionState(userToken);
}
// The connection is in or can be brought to a re-usable state.
if (reuseStrategy.keepAlive(request, response, context)) {
// Set the idle duration of this connection
final TimeValue duration = keepAliveStrategy.getKeepAliveDuration(response, context);
if (this.log.isDebugEnabled()) {
final String s;
if (duration != null) {
s = "for " + duration;
} else {
s = "indefinitely";
}
this.log.debug("Connection can be kept alive " + s);
}
execRuntime.setConnectionValidFor(duration);
execRuntime.markConnectionReusable();
} else {
execRuntime.markConnectionNonReusable();
}
// check for entity, release connection if possible
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
@ -284,213 +148,7 @@ final class MainClientExec implements ExecChainHandler {
execRuntime.discardConnection();
throw ex;
}
}
/**
* Establishes the target route.
*/
void establishRoute(
final HttpRoute route,
final HttpRequest request,
final ExecRuntime execRuntime,
final HttpClientContext context) throws HttpException, IOException {
final RouteTracker tracker = new RouteTracker(route);
int step;
do {
final HttpRoute fact = tracker.toRoute();
step = this.routeDirector.nextStep(route, fact);
switch (step) {
case HttpRouteDirector.CONNECT_TARGET:
execRuntime.connect(context);
tracker.connectTarget(route.isSecure());
break;
case HttpRouteDirector.CONNECT_PROXY:
execRuntime.connect(context);
final HttpHost proxy = route.getProxyHost();
tracker.connectProxy(proxy, false);
break;
case HttpRouteDirector.TUNNEL_TARGET: {
final boolean secure = createTunnelToTarget(route, request, execRuntime, context);
this.log.debug("Tunnel to target created.");
tracker.tunnelTarget(secure);
} break;
case HttpRouteDirector.TUNNEL_PROXY: {
// The most simple example for this case is a proxy chain
// of two proxies, where P1 must be tunnelled to P2.
// route: Source -> P1 -> P2 -> Target (3 hops)
// fact: Source -> P1 -> Target (2 hops)
final int hop = fact.getHopCount()-1; // the hop to establish
final boolean secure = createTunnelToProxy(route, hop, context);
this.log.debug("Tunnel to proxy created.");
tracker.tunnelProxy(route.getHopTarget(hop), secure);
} break;
case HttpRouteDirector.LAYER_PROTOCOL:
execRuntime.upgradeTls(context);
tracker.layerProtocol(route.isSecure());
break;
case HttpRouteDirector.UNREACHABLE:
throw new HttpException("Unable to establish route: " +
"planned = " + route + "; current = " + fact);
case HttpRouteDirector.COMPLETE:
break;
default:
throw new IllegalStateException("Unknown step indicator "
+ step + " from RouteDirector.");
}
} while (step > HttpRouteDirector.COMPLETE);
}
/**
* Creates a tunnel to the target server.
* The connection must be established to the (last) proxy.
* A CONNECT request for tunnelling through the proxy will
* be created and sent, the response received and checked.
* This method does <i>not</i> processChallenge the connection with
* information about the tunnel, that is left to the caller.
*/
private boolean createTunnelToTarget(
final HttpRoute route,
final HttpRequest request,
final ExecRuntime execRuntime,
final HttpClientContext context) throws HttpException, IOException {
final RequestConfig config = context.getRequestConfig();
final HttpHost target = route.getTargetHost();
final HttpHost proxy = route.getProxyHost();
final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
ClassicHttpResponse response = null;
final String authority = target.toHostString();
final ClassicHttpRequest connect = new BasicClassicHttpRequest("CONNECT", target, authority);
connect.setVersion(HttpVersion.HTTP_1_1);
this.proxyHttpProcessor.process(connect, null, context);
while (response == null) {
if (!execRuntime.isConnected()) {
execRuntime.connect(context);
}
connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
response = execRuntime.execute(connect, context);
final int status = response.getCode();
if (status < HttpStatus.SC_SUCCESS) {
throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
}
if (config.isAuthenticationEnabled()) {
if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response,
proxyAuthExchange, context)) {
if (this.authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
this.proxyAuthStrategy, proxyAuthExchange, context)) {
// Retry request
if (this.reuseStrategy.keepAlive(request, response, context)) {
this.log.debug("Connection kept alive");
// Consume response content
final HttpEntity entity = response.getEntity();
EntityUtils.consume(entity);
} else {
execRuntime.disconnect();
}
response = null;
}
}
}
}
final int status = response.getCode();
if (status >= HttpStatus.SC_REDIRECTION) {
// Buffer response content
final HttpEntity entity = response.getEntity();
if (entity != null) {
response.setEntity(new BufferedHttpEntity(entity));
}
execRuntime.disconnect();
execRuntime.discardConnection();
throw new TunnelRefusedException("CONNECT refused by proxy: " +
new StatusLine(response), response);
}
// How to decide on security of the tunnelled connection?
// The socket factory knows only about the segment to the proxy.
// Even if that is secure, the hop to the target may be insecure.
// Leave it to derived classes, consider insecure by default here.
return false;
}
/**
* Creates a tunnel to an intermediate proxy.
* This method is <i>not</i> implemented in this class.
* It just throws an exception here.
*/
private boolean createTunnelToProxy(
final HttpRoute route,
final int hop,
final HttpClientContext context) throws HttpException {
// Have a look at createTunnelToTarget and replicate the parts
// you need in a custom derived class. If your proxies don't require
// authentication, it is not too hard. But for the stock version of
// HttpClient, we cannot make such simplifying assumptions and would
// have to include proxy authentication code. The HttpComponents team
// is currently not in a position to support rarely used code of this
// complexity. Feel free to submit patches that refactor the code in
// createTunnelToTarget to facilitate re-use for proxy tunnelling.
throw new HttpException("Proxy chains are not supported.");
}
private boolean needAuthentication(
final AuthExchange targetAuthExchange,
final AuthExchange proxyAuthExchange,
final HttpRoute route,
final ClassicHttpRequest request,
final HttpResponse response,
final HttpClientContext context) {
final RequestConfig config = context.getRequestConfig();
if (config.isAuthenticationEnabled()) {
final URIAuthority authority = request.getAuthority();
final String scheme = request.getScheme();
HttpHost target = authority != null ? new HttpHost(authority, scheme) : route.getTargetHost();;
if (target.getPort() < 0) {
target = new HttpHost(
target.getHostName(),
route.getTargetHost().getPort(),
target.getSchemeName());
}
final boolean targetAuthRequested = this.authenticator.isChallenged(
target, ChallengeType.TARGET, response, targetAuthExchange, context);
HttpHost proxy = route.getProxyHost();
// if proxy is not set use target host instead
if (proxy == null) {
proxy = route.getTargetHost();
}
final boolean proxyAuthRequested = this.authenticator.isChallenged(
proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
if (targetAuthRequested) {
return this.authenticator.prepareAuthResponse(target, ChallengeType.TARGET, response,
this.targetAuthStrategy, targetAuthExchange, context);
}
if (proxyAuthRequested) {
return this.authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
this.proxyAuthStrategy, proxyAuthExchange, context);
}
}
return false;
}
}

View File

@ -1,149 +0,0 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.sync;
import java.io.IOException;
import java.io.InterruptedIOException;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RequestClientConnControl;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecChainHandler;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
import org.apache.hc.core5.http.protocol.HttpCoreContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http.protocol.RequestContent;
import org.apache.hc.core5.http.protocol.RequestTargetHost;
import org.apache.hc.core5.http.protocol.RequestUserAgent;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.VersionInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Request executor that implements the most fundamental aspects of
* the HTTP specification and the most straight-forward request / response
* exchange with the target server. This executor does not support
* execution via proxy and will make no attempts to retry the request
* in case of a redirect, authentication challenge or I/O error.
*
* @since 4.3
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
final class MinimalClientExec implements ExecChainHandler {
private final Logger log = LogManager.getLogger(getClass());
private final ConnectionReuseStrategy reuseStrategy;
private final ConnectionKeepAliveStrategy keepAliveStrategy;
private final HttpProcessor httpProcessor;
public MinimalClientExec(
final ConnectionReuseStrategy reuseStrategy,
final ConnectionKeepAliveStrategy keepAliveStrategy) {
this.reuseStrategy = reuseStrategy != null ? reuseStrategy : DefaultConnectionReuseStrategy.INSTANCE;
this.keepAliveStrategy = keepAliveStrategy != null ? keepAliveStrategy : DefaultConnectionKeepAliveStrategy.INSTANCE;
this.httpProcessor = new DefaultHttpProcessor(
new RequestContent(),
new RequestTargetHost(),
new RequestClientConnControl(),
new RequestUserAgent(VersionInfo.getSoftwareInfo(
"Apache-HttpClient", "org.apache.hc.client5", getClass())));
}
@Override
public ClassicHttpResponse execute(
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain) throws IOException, HttpException {
Args.notNull(request, "HTTP request");
Args.notNull(scope, "Scope");
final HttpRoute route = scope.route;
final HttpClientContext context = scope.clientContext;
final ExecRuntime execRuntime = scope.execRuntime;
if (!execRuntime.isConnectionAcquired()) {
execRuntime.acquireConnection(route, null, context);
}
try {
if (!execRuntime.isConnected()) {
execRuntime.connect(context);
}
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
httpProcessor.process(request, request.getEntity(), context);
final ClassicHttpResponse response = execRuntime.execute(request, context);
httpProcessor.process(response, response.getEntity(), context);
// The connection is in or can be brought to a re-usable state.
if (reuseStrategy.keepAlive(request, response, context)) {
// Set the idle duration of this connection
final TimeValue duration = keepAliveStrategy.getKeepAliveDuration(response, context);
execRuntime.setConnectionValidFor(duration);
execRuntime.markConnectionReusable();
} else {
execRuntime.markConnectionNonReusable();
}
// check for entity, release connection if possible
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
// connection not needed and (assumed to be) in re-usable state
execRuntime.releaseConnection();
return new CloseableHttpResponse(response, null);
} else {
ResponseEntityProxy.enchance(response, execRuntime);
return new CloseableHttpResponse(response, execRuntime);
}
} catch (final ConnectionShutdownException ex) {
final InterruptedIOException ioex = new InterruptedIOException("Connection has been shut down");
ioex.initCause(ex);
execRuntime.discardConnection();
throw ioex;
} catch (final HttpException | RuntimeException | IOException ex) {
execRuntime.discardConnection();
throw ex;
}
}
}

View File

@ -28,30 +28,40 @@
package org.apache.hc.client5.http.impl.sync;
import java.io.IOException;
import java.io.InterruptedIOException;
import org.apache.hc.client5.http.CancellableAware;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.impl.ExecSupport;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.ClientProtocolException;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.protocol.RequestClientConnControl;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
import org.apache.hc.core5.http.protocol.BasicHttpContext;
import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpCoreContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http.protocol.RequestContent;
import org.apache.hc.core5.http.protocol.RequestTargetHost;
import org.apache.hc.core5.http.protocol.RequestUserAgent;
import org.apache.hc.core5.net.URIAuthority;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.VersionInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -66,16 +76,21 @@ public class MinimalHttpClient extends CloseableHttpClient {
private final Logger log = LogManager.getLogger(getClass());
private final HttpClientConnectionManager connManager;
private final MinimalClientExec execChain;
private final ConnectionReuseStrategy reuseStrategy;
private final HttpRequestExecutor requestExecutor;
private final HttpProcessor httpProcessor;
MinimalHttpClient(final HttpClientConnectionManager connManager) {
super();
this.connManager = Args.notNull(connManager, "HTTP connection manager");
this.execChain = new MinimalClientExec(
DefaultConnectionReuseStrategy.INSTANCE,
DefaultConnectionKeepAliveStrategy.INSTANCE);
this.requestExecutor = new HttpRequestExecutor(DefaultConnectionReuseStrategy.INSTANCE);
this.reuseStrategy = DefaultConnectionReuseStrategy.INSTANCE;
this.requestExecutor = new HttpRequestExecutor(this.reuseStrategy);
this.httpProcessor = new DefaultHttpProcessor(
new RequestContent(),
new RequestTargetHost(),
new RequestClientConnControl(),
new RequestUserAgent(VersionInfo.getSoftwareInfo(
"Apache-HttpClient", "org.apache.hc.client5", getClass())));
}
@Override
@ -85,28 +100,70 @@ public class MinimalHttpClient extends CloseableHttpClient {
final HttpContext context) throws IOException {
Args.notNull(target, "Target host");
Args.notNull(request, "HTTP request");
if (request.getScheme() == null) {
request.setScheme(target.getSchemeName());
}
if (request.getAuthority() == null) {
request.setAuthority(new URIAuthority(target));
}
final HttpClientContext clientContext = HttpClientContext.adapt(
context != null ? context : new BasicHttpContext());
RequestConfig config = null;
if (request instanceof Configurable) {
config = ((Configurable) request).getConfig();
}
if (config != null) {
clientContext.setRequestConfig(config);
}
final HttpRoute route = new HttpRoute(target.getPort() > 0 ? target : new HttpHost(
target.getHostName(),
DefaultSchemePortResolver.INSTANCE.resolve(target),
target.getSchemeName()));
final ExecRuntime execRuntime = new ExecRuntimeImpl(log, connManager, requestExecutor,
request instanceof CancellableAware ? (CancellableAware) request : null);
try {
if (request.getScheme() == null) {
request.setScheme(target.getSchemeName());
if (!execRuntime.isConnectionAcquired()) {
execRuntime.acquireConnection(route, null, clientContext);
}
if (request.getAuthority() == null) {
request.setAuthority(new URIAuthority(target));
if (!execRuntime.isConnected()) {
execRuntime.connect(clientContext);
}
final HttpClientContext localcontext = HttpClientContext.adapt(
context != null ? context : new BasicHttpContext());
RequestConfig config = null;
if (request instanceof Configurable) {
config = ((Configurable) request).getConfig();
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
httpProcessor.process(request, request.getEntity(), context);
final ClassicHttpResponse response = execRuntime.execute(request, clientContext);
httpProcessor.process(response, response.getEntity(), context);
if (reuseStrategy.keepAlive(request, response, context)) {
execRuntime.markConnectionReusable();
} else {
execRuntime.markConnectionNonReusable();
}
if (config != null) {
localcontext.setRequestConfig(config);
// check for entity, release connection if possible
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
// connection not needed and (assumed to be) in re-usable state
execRuntime.releaseConnection();
return new CloseableHttpResponse(response, null);
} else {
ResponseEntityProxy.enchance(response, execRuntime);
return new CloseableHttpResponse(response, execRuntime);
}
final ExecRuntime execRuntime = new ExecRuntimeImpl(log, connManager, requestExecutor,
request instanceof CancellableAware ? (CancellableAware) request : null);
final ExecChain.Scope scope = new ExecChain.Scope(new HttpRoute(target), request, execRuntime, localcontext);
final ClassicHttpResponse response = this.execChain.execute(ExecSupport.copy(request), scope, null);
return CloseableHttpResponse.adapt(response);
} catch (final ConnectionShutdownException ex) {
final InterruptedIOException ioex = new InterruptedIOException("Connection has been shut down");
ioex.initCause(ex);
execRuntime.discardConnection();
throw ioex;
} catch (final RuntimeException | IOException ex) {
execRuntime.discardConnection();
throw ex;
} catch (final HttpException httpException) {
execRuntime.discardConnection();
throw new ClientProtocolException(httpException);
}
}

View File

@ -30,33 +30,46 @@ package org.apache.hc.client5.http.impl.sync;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.StandardMethods;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.CredentialsStore;
import org.apache.hc.client5.http.auth.util.CredentialSupport;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.NonRepeatableRequestException;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecChainHandler;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.client5.http.utils.URIUtils;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.protocol.HttpCoreContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.net.URIAuthority;
import org.apache.hc.core5.util.Args;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Request executor in the request execution chain that is responsible
* for implementation of HTTP specification requirements.
* Internally this executor relies on a {@link HttpProcessor} to populate
* requisite HTTP request headers, process HTTP response headers and processChallenge
* session state in {@link HttpClientContext}.
* <p>
* Further responsibilities such as communication with the opposite
* endpoint is delegated to the next executor in the request execution
@ -68,11 +81,21 @@ import org.apache.hc.core5.util.Args;
@Contract(threading = ThreadingBehavior.IMMUTABLE)
final class ProtocolExec implements ExecChainHandler {
private final HttpProcessor httpProcessor;
private final Logger log = LogManager.getLogger(getClass());
public ProtocolExec(final HttpProcessor httpProcessor) {
Args.notNull(httpProcessor, "HTTP protocol processor");
this.httpProcessor = httpProcessor;
private final HttpProcessor httpProcessor;
private final AuthenticationStrategy targetAuthStrategy;
private final AuthenticationStrategy proxyAuthStrategy;
private final HttpAuthenticator authenticator;
public ProtocolExec(
final HttpProcessor httpProcessor,
final AuthenticationStrategy targetAuthStrategy,
final AuthenticationStrategy proxyAuthStrategy) {
this.httpProcessor = Args.notNull(httpProcessor, "HTTP protocol processor");
this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
this.authenticator = new HttpAuthenticator();
}
@Override
@ -85,46 +108,150 @@ final class ProtocolExec implements ExecChainHandler {
final HttpRoute route = scope.route;
final HttpClientContext context = scope.clientContext;
final ExecRuntime execRuntime = scope.execRuntime;
if (route.getProxyHost() != null && !route.isTunnelled()) {
try {
URI uri = request.getUri();
if (!uri.isAbsolute()) {
final HttpHost target = route.getTargetHost();
uri = URIUtils.rewriteURI(uri, target, true);
} else {
uri = URIUtils.rewriteURI(uri);
}
request.setPath(uri.toASCIIString());
} catch (final URISyntaxException ex) {
throw new ProtocolException("Invalid URI: " + request.getRequestUri(), ex);
}
}
final URIAuthority authority = request.getAuthority();
if (authority != null) {
final CredentialsProvider credsProvider = context.getCredentialsProvider();
if (credsProvider instanceof CredentialsStore) {
CredentialSupport.extractFromAuthority(authority, (CredentialsStore) credsProvider);
}
}
// Run request protocol interceptors
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
this.httpProcessor.process(request, request.getEntity(), context);
final ClassicHttpResponse response = chain.proceed(request, scope);
try {
// Run response protocol interceptors
context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
this.httpProcessor.process(response, response.getEntity(), context);
return response;
final HttpHost target = route.getTargetHost();
final HttpHost proxy = route.getProxyHost();
if (proxy != null && !route.isTunnelled()) {
try {
URI uri = request.getUri();
if (!uri.isAbsolute()) {
uri = URIUtils.rewriteURI(uri, target, true);
} else {
uri = URIUtils.rewriteURI(uri);
}
request.setPath(uri.toASCIIString());
} catch (final URISyntaxException ex) {
throw new ProtocolException("Invalid request URI: " + request.getRequestUri(), ex);
}
}
final URIAuthority authority = request.getAuthority();
if (authority != null) {
final CredentialsProvider credsProvider = context.getCredentialsProvider();
if (credsProvider instanceof CredentialsStore) {
CredentialSupport.extractFromAuthority(authority, (CredentialsStore) credsProvider);
}
}
final AuthExchange targetAuthExchange = context.getAuthExchange(target);
final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
for (int execCount = 1;; execCount++) {
if (execCount > 1) {
final HttpEntity entity = request.getEntity();
if (entity != null && !entity.isRepeatable()) {
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity.");
}
}
// Run request protocol interceptors
context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
httpProcessor.process(request, request.getEntity(), context);
if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
if (log.isDebugEnabled()) {
log.debug("Target auth state: " + targetAuthExchange.getState());
}
authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, context);
}
if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
if (log.isDebugEnabled()) {
log.debug("Proxy auth state: " + proxyAuthExchange.getState());
}
authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context);
}
final ClassicHttpResponse response = chain.proceed(request, scope);
context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
httpProcessor.process(response, response.getEntity(), context);
if (request.getMethod().equalsIgnoreCase(StandardMethods.TRACE.name())) {
// Do not perform authentication for TRACE request
return response;
}
if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, context)) {
// Make sure the response body is fully consumed, if present
final HttpEntity entity = response.getEntity();
if (execRuntime.isConnectionReusable()) {
EntityUtils.consume(entity);
} else {
execRuntime.disconnect();
if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
&& proxyAuthExchange.getAuthScheme() != null
&& proxyAuthExchange.getAuthScheme().isConnectionBased()) {
log.debug("Resetting proxy auth state");
proxyAuthExchange.reset();
}
if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
&& targetAuthExchange.getAuthScheme() != null
&& targetAuthExchange.getAuthScheme().isConnectionBased()) {
log.debug("Resetting target auth state");
targetAuthExchange.reset();
}
}
// Reset request headers
final ClassicHttpRequest original = scope.originalRequest;
request.setHeaders();
for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
request.addHeader(it.next());
}
} else {
return response;
}
}
} catch (final RuntimeException | HttpException | IOException ex) {
response.close();
execRuntime.discardConnection();
throw ex;
}
}
private boolean needAuthentication(
final AuthExchange targetAuthExchange,
final AuthExchange proxyAuthExchange,
final HttpRoute route,
final ClassicHttpRequest request,
final HttpResponse response,
final HttpClientContext context) {
final RequestConfig config = context.getRequestConfig();
if (config.isAuthenticationEnabled()) {
final URIAuthority authority = request.getAuthority();
final String scheme = request.getScheme();
HttpHost target = authority != null ? new HttpHost(authority, scheme) : route.getTargetHost();;
if (target.getPort() < 0) {
target = new HttpHost(
target.getHostName(),
route.getTargetHost().getPort(),
target.getSchemeName());
}
final boolean targetAuthRequested = authenticator.isChallenged(
target, ChallengeType.TARGET, response, targetAuthExchange, context);
HttpHost proxy = route.getProxyHost();
// if proxy is not set use target host instead
if (proxy == null) {
proxy = route.getTargetHost();
}
final boolean proxyAuthRequested = authenticator.isChallenged(
proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
if (targetAuthRequested) {
return authenticator.prepareAuthResponse(target, ChallengeType.TARGET, response,
targetAuthStrategy, targetAuthExchange, context);
}
if (proxyAuthRequested) {
return authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
}
}
return false;
}
}

View File

@ -55,20 +55,6 @@ class RequestEntityProxy implements HttpEntity {
return entity instanceof RequestEntityProxy;
}
static boolean isRepeatable(final ClassicHttpRequest request) {
final HttpEntity entity = request.getEntity();
if (entity != null) {
if (isEnhanced(entity)) {
final RequestEntityProxy proxy = (RequestEntityProxy) entity;
if (!proxy.isConsumed()) {
return true;
}
}
return entity.isRepeatable();
}
return true;
}
private final HttpEntity original;
private boolean consumed = false;
@ -87,7 +73,11 @@ class RequestEntityProxy implements HttpEntity {
@Override
public boolean isRepeatable() {
return original.isRepeatable();
if (!consumed) {
return true;
} else {
return original.isRepeatable();
}
}
@Override

View File

@ -40,6 +40,7 @@ import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.NoHttpResponseException;
import org.apache.hc.core5.util.Args;
@ -99,7 +100,8 @@ final class RetryExec implements ExecChainHandler {
if (this.log.isDebugEnabled()) {
this.log.debug(ex.getMessage(), ex);
}
if (!RequestEntityProxy.isRepeatable(request)) {
final HttpEntity entity = request.getEntity();
if (entity != null && !entity.isRepeatable()) {
this.log.debug("Cannot retry non-repeatable request");
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity", ex);

View File

@ -0,0 +1,359 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.sync;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RouteInfo;
import org.apache.hc.client5.http.auth.AuthChallenge;
import org.apache.hc.client5.http.auth.AuthScheme;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.client5.http.sync.methods.HttpGet;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@SuppressWarnings({"boxing","static-access"}) // test code
public class TestConnectExec {
@Mock
private ConnectionReuseStrategy reuseStrategy;
@Mock
private HttpProcessor proxyHttpProcessor;
@Mock
private AuthenticationStrategy proxyAuthStrategy;
@Mock
private ExecRuntime execRuntime;
@Mock
private ExecChain execChain;
private ConnectExec exec;
private HttpHost target;
private HttpHost proxy;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
exec = new ConnectExec(reuseStrategy, proxyHttpProcessor, proxyAuthStrategy);
target = new HttpHost("foo", 80);
proxy = new HttpHost("bar", 8888);
}
@Test
public void testExecAcquireConnection() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
response.setEntity(EntityBuilder.create()
.setStream(new ByteArrayInputStream(new byte[]{}))
.build());
context.setUserToken("Blah");
Mockito.when(execRuntime.isConnectionAcquired()).thenReturn(false);
Mockito.when(execRuntime.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(false);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime).acquireConnection(route, "Blah", context);
Mockito.verify(execRuntime).connect(context);
}
@Test
public void testEstablishDirectRoute() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime).connect(context);
Mockito.verify(execRuntime, Mockito.never()).execute(Mockito.<ClassicHttpRequest>any(), Mockito.<HttpClientContext>any());
}
@Test
public void testEstablishRouteDirectProxy() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, false);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime).connect(context);
Mockito.verify(execRuntime, Mockito.never()).execute(Mockito.<ClassicHttpRequest>any(), Mockito.<HttpClientContext>any());
}
@Test
public void testEstablishRouteViaProxyTunnel() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(execRuntime.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime).connect(context);
final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(execRuntime).execute(
reqCaptor.capture(),
Mockito.same(context));
final HttpRequest connect = reqCaptor.getValue();
Assert.assertNotNull(connect);
Assert.assertEquals("CONNECT", connect.getMethod());
Assert.assertEquals(HttpVersion.HTTP_1_1, connect.getVersion());
Assert.assertEquals("foo:80", connect.getRequestUri());
}
@Test(expected = HttpException.class)
public void testEstablishRouteViaProxyTunnelUnexpectedResponse() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(101, "Lost");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(execRuntime.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
}
@Test(expected = HttpException.class)
public void testEstablishRouteViaProxyTunnelFailure() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(500, "Boom");
response.setEntity(new StringEntity("Ka-boom"));
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(execRuntime.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
try {
exec.execute(request, scope, execChain);
} catch (final TunnelRefusedException ex) {
final ClassicHttpResponse r = ex.getResponse();
Assert.assertEquals("Ka-boom", EntityUtils.toString(r.getEntity()));
Mockito.verify(execRuntime).disconnect();
Mockito.verify(execRuntime).discardConnection();
throw ex;
}
}
@Test
public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengePersistentConnection() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(execRuntime.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
Mockito.when(proxyAuthStrategy.select(
Mockito.eq(ChallengeType.PROXY),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime).connect(context);
Mockito.verify(instream1).close();
}
@Test
public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengeNonPersistentConnection() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.FALSE);
Mockito.when(execRuntime.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
Mockito.when(proxyAuthStrategy.select(
Mockito.eq(ChallengeType.PROXY),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
Mockito.verify(execRuntime).connect(context);
Mockito.verify(instream1, Mockito.never()).close();
Mockito.verify(execRuntime).disconnect();
}
@Test(expected = HttpException.class)
public void testEstablishRouteViaProxyTunnelMultipleHops() throws Exception {
final HttpHost proxy1 = new HttpHost("this", 8888);
final HttpHost proxy2 = new HttpHost("that", 8888);
final HttpRoute route = new HttpRoute(target, null, new HttpHost[] {proxy1, proxy2},
true, RouteInfo.TunnelType.TUNNELLED, RouteInfo.LayerType.LAYERED);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
exec.execute(request, scope, execChain);
}
static class ConnectionState {
private boolean connected;
public Answer connectAnswer() {
return new Answer() {
@Override
public Object answer(final InvocationOnMock invocationOnMock) throws Throwable {
connected = true;
return null;
}
};
}
public Answer<Boolean> isConnectedAnswer() {
return new Answer<Boolean>() {
@Override
public Boolean answer(final InvocationOnMock invocationOnMock) throws Throwable {
return connected;
}
};
};
}
}

View File

@ -27,52 +27,28 @@
package org.apache.hc.client5.http.impl.sync;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.Map;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RouteInfo;
import org.apache.hc.client5.http.auth.AuthChallenge;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.impl.auth.NTLMScheme;
import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.NonRepeatableRequestException;
import org.apache.hc.client5.http.protocol.UserTokenHandler;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.client5.http.sync.methods.HttpGet;
import org.apache.hc.client5.http.sync.methods.HttpPost;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.util.TimeValue;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@ -87,27 +63,18 @@ public class TestMainClientExec {
@Mock
private ConnectionKeepAliveStrategy keepAliveStrategy;
@Mock
private HttpProcessor proxyHttpProcessor;
@Mock
private AuthenticationStrategy targetAuthStrategy;
@Mock
private AuthenticationStrategy proxyAuthStrategy;
@Mock
private UserTokenHandler userTokenHandler;
@Mock
private ExecRuntime endpoint;
private MainClientExec mainClientExec;
private HttpHost target;
private HttpHost proxy;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
mainClientExec = new MainClientExec(reuseStrategy, keepAliveStrategy, proxyHttpProcessor, targetAuthStrategy,
proxyAuthStrategy, userTokenHandler);
mainClientExec = new MainClientExec(reuseStrategy, keepAliveStrategy, userTokenHandler);
target = new HttpHost("foo", 80);
proxy = new HttpHost("bar", 8888);
}
@Test
@ -131,8 +98,6 @@ public class TestMainClientExec {
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint, Mockito.times(1)).markConnectionNonReusable();
Mockito.verify(endpoint, Mockito.never()).releaseConnection();
@ -162,8 +127,6 @@ public class TestMainClientExec {
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).markConnectionNonReusable();
Mockito.verify(endpoint).releaseConnection();
@ -200,8 +163,6 @@ public class TestMainClientExec {
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).markConnectionReusable();
Mockito.verify(endpoint, Mockito.never()).releaseConnection();
@ -234,8 +195,6 @@ public class TestMainClientExec {
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).releaseConnection();
@ -355,355 +314,6 @@ public class TestMainClientExec {
}
}
@Test
public void testExecRequestRetryOnAuthChallenge() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://foo/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(401, "Huh?");
response1.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final InputStream instream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {2, 3, 4}));
response2.setEntity(EntityBuilder.create()
.setStream(instream2)
.build());
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(target), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(targetAuthStrategy.select(
Mockito.eq(ChallengeType.TARGET),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
Mockito.when(endpoint.isConnectionReusable()).thenReturn(true);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null);
Mockito.verify(endpoint, Mockito.times(2)).execute(request, context);
Mockito.verify(instream1).close();
Mockito.verify(instream2, Mockito.never()).close();
Assert.assertNotNull(finalResponse);
Assert.assertEquals(200, finalResponse.getCode());
}
@Test
public void testExecEntityEnclosingRequestRetryOnAuthChallenge() throws Exception {
final HttpRoute route = new HttpRoute(target, proxy);
final ClassicHttpRequest request = new HttpGet("http://foo/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(401, "Huh?");
response1.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final InputStream instream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {2, 3, 4}));
response2.setEntity(EntityBuilder.create()
.setStream(instream2)
.build());
final HttpClientContext context = new HttpClientContext();
final AuthExchange proxyAuthExchange = new AuthExchange();
proxyAuthExchange.setState(AuthExchange.State.SUCCESS);
proxyAuthExchange.select(new NTLMScheme());
context.setAuthExchange(proxy, proxyAuthExchange);
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(target), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.FALSE);
Mockito.when(targetAuthStrategy.select(
Mockito.eq(ChallengeType.TARGET),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null);
Mockito.verify(endpoint, Mockito.times(2)).execute(request, context);
Mockito.verify(endpoint).disconnect();
Mockito.verify(instream2, Mockito.never()).close();
Assert.assertNotNull(finalResponse);
Assert.assertEquals(200, finalResponse.getCode());
Assert.assertNull(proxyAuthExchange.getAuthScheme());
}
@Test(expected = NonRepeatableRequestException.class)
public void testExecEntityEnclosingRequest() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final HttpPost request = new HttpPost("http://foo/test");
final InputStream instream0 = new ByteArrayInputStream(new byte[] {1, 2, 3});
request.setEntity(EntityBuilder.create()
.setStream(instream0)
.build());
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(401, "Huh?");
response1.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = new ByteArrayInputStream(new byte[] {1, 2, 3});
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(target), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenAnswer(new Answer<HttpResponse>() {
@Override
public HttpResponse answer(final InvocationOnMock invocationOnMock) throws Throwable {
final Object[] args = invocationOnMock.getArguments();
final ClassicHttpRequest requestEE = (ClassicHttpRequest) args[0];
requestEE.getEntity().writeTo(new ByteArrayOutputStream());
return response1;
}
});
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(targetAuthStrategy.select(
Mockito.eq(ChallengeType.TARGET),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
mainClientExec.execute(request, scope, null);
}
@Test
public void testEstablishDirectRoute() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
mainClientExec.establishRoute(route, request, endpoint, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint, Mockito.never()).execute(Mockito.<ClassicHttpRequest>any(), Mockito.<HttpClientContext>any());
}
@Test
public void testEstablishRouteDirectProxy() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, false);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
mainClientExec.establishRoute(route, request, endpoint, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint, Mockito.never()).execute(Mockito.<ClassicHttpRequest>any(), Mockito.<HttpClientContext>any());
}
@Test
public void testEstablishRouteViaProxyTunnel() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(endpoint.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response);
mainClientExec.establishRoute(route, request, endpoint, context);
Mockito.verify(endpoint).connect(context);
final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(endpoint).execute(
reqCaptor.capture(),
Mockito.same(context));
final HttpRequest connect = reqCaptor.getValue();
Assert.assertNotNull(connect);
Assert.assertEquals("CONNECT", connect.getMethod());
Assert.assertEquals(HttpVersion.HTTP_1_1, connect.getVersion());
Assert.assertEquals("foo:80", connect.getRequestUri());
}
@Test(expected = HttpException.class)
public void testEstablishRouteViaProxyTunnelUnexpectedResponse() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(101, "Lost");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(endpoint.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response);
mainClientExec.establishRoute(route, request, endpoint, context);
}
@Test(expected = HttpException.class)
public void testEstablishRouteViaProxyTunnelFailure() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(500, "Boom");
response.setEntity(new StringEntity("Ka-boom"));
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(endpoint.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response);
try {
mainClientExec.establishRoute(route, request, endpoint, context);
} catch (final TunnelRefusedException ex) {
final ClassicHttpResponse r = ex.getResponse();
Assert.assertEquals("Ka-boom", EntityUtils.toString(r.getEntity()));
Mockito.verify(endpoint).disconnect();
Mockito.verify(endpoint).discardConnection();
throw ex;
}
}
@Test
public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengePersistentConnection() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(endpoint.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
Mockito.when(proxyAuthStrategy.select(
Mockito.eq(ChallengeType.PROXY),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
mainClientExec.establishRoute(route, request, endpoint, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(instream1).close();
}
@Test
public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengeNonPersistentConnection() throws Exception {
final HttpRoute route = new HttpRoute(target, null, proxy, true);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.FALSE);
Mockito.when(endpoint.execute(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
Mockito.when(proxyAuthStrategy.select(
Mockito.eq(ChallengeType.PROXY),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
mainClientExec.establishRoute(route, request, endpoint, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(instream1, Mockito.never()).close();
Mockito.verify(endpoint).disconnect();
}
@Test(expected = HttpException.class)
public void testEstablishRouteViaProxyTunnelMultipleHops() throws Exception {
final HttpHost proxy1 = new HttpHost("this", 8888);
final HttpHost proxy2 = new HttpHost("that", 8888);
final HttpRoute route = new HttpRoute(target, null, new HttpHost[] {proxy1, proxy2},
true, RouteInfo.TunnelType.TUNNELLED, RouteInfo.LayerType.LAYERED);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ConnectionState connectionState = new ConnectionState();
Mockito.doAnswer(connectionState.connectAnswer()).when(endpoint).connect(Mockito.<HttpClientContext>any());
Mockito.when(endpoint.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
mainClientExec.establishRoute(route, request, endpoint, context);
}
static class ConnectionState {
private boolean connected;

View File

@ -1,313 +0,0 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.sync;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.ConnectionShutdownException;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.client5.http.sync.methods.HttpGet;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.util.TimeValue;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@SuppressWarnings({"boxing","static-access"}) // test code
public class TestMinimalClientExec {
@Mock
private ConnectionReuseStrategy reuseStrategy;
@Mock
private ConnectionKeepAliveStrategy keepAliveStrategy;
@Mock
private ExecRuntime endpoint;
private MinimalClientExec minimalClientExec;
private HttpHost target;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
minimalClientExec = new MinimalClientExec(reuseStrategy, keepAliveStrategy);
target = new HttpHost("foo", 80);
}
@Test
public void testExecRequestNonPersistentConnection() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
response.setEntity(EntityBuilder.create()
.setStream(new ByteArrayInputStream(new byte[]{}))
.build());
Mockito.when(endpoint.isConnectionAcquired()).thenReturn(false);
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(false);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = minimalClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).markConnectionNonReusable();
Mockito.verify(endpoint, Mockito.never()).releaseConnection();
Assert.assertNotNull(finalResponse);
Assert.assertTrue(finalResponse instanceof CloseableHttpResponse);
}
@Test
public void testExecRequestNonPersistentConnectionNoResponseEntity() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
response.setEntity(null);
Mockito.when(endpoint.isConnectionAcquired()).thenReturn(false);
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(false);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = minimalClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).markConnectionNonReusable();
Mockito.verify(endpoint).releaseConnection();
Assert.assertNotNull(finalResponse);
Assert.assertTrue(finalResponse instanceof CloseableHttpResponse);
}
@Test
public void testExecRequestPersistentConnection() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
// The entity is streaming
response.setEntity(EntityBuilder.create()
.setStream(new ByteArrayInputStream(new byte[]{}))
.build());
Mockito.when(endpoint.isConnected()).thenReturn(false);
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(true);
Mockito.when(keepAliveStrategy.getKeepAliveDuration(
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(TimeValue.ofMillis(678L));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = minimalClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).markConnectionReusable();
Mockito.verify(endpoint, Mockito.never()).releaseConnection();
Assert.assertNotNull(finalResponse);
Assert.assertTrue(finalResponse instanceof CloseableHttpResponse);
}
@Test
public void testExecRequestPersistentConnectionNoResponseEntity() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
Mockito.when(endpoint.isConnected()).thenReturn(false);
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(true);
Mockito.when(keepAliveStrategy.getKeepAliveDuration(
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(TimeValue.ofMillis(678L));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = minimalClientExec.execute(request, scope, null);
Mockito.verify(endpoint).acquireConnection(route, null, context);
Mockito.verify(endpoint).connect(context);
Mockito.verify(endpoint).execute(request, context);
Mockito.verify(endpoint).releaseConnection();
Assert.assertNotNull(finalResponse);
Assert.assertTrue(finalResponse instanceof CloseableHttpResponse);
}
@Test
public void testExecRequestConnectionRelease() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
// The entity is streaming
response.setEntity(EntityBuilder.create()
.setStream(new ByteArrayInputStream(new byte[]{}))
.build());
Mockito.when(endpoint.isConnected()).thenReturn(Boolean.TRUE);
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenReturn(response);
Mockito.when(reuseStrategy.keepAlive(
Mockito.same(request),
Mockito.same(response),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.FALSE);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ClassicHttpResponse finalResponse = minimalClientExec.execute(request, scope, null);
Mockito.verify(endpoint, Mockito.times(1)).execute(request, context);
Mockito.verify(endpoint, Mockito.never()).disconnect();
Mockito.verify(endpoint, Mockito.never()).releaseConnection();
Assert.assertNotNull(finalResponse);
Assert.assertTrue(finalResponse instanceof CloseableHttpResponse);
finalResponse.close();
Mockito.verify(endpoint).disconnect();
Mockito.verify(endpoint).discardConnection();
}
@Test(expected=InterruptedIOException.class)
public void testExecConnectionShutDown() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenThrow(new ConnectionShutdownException());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
try {
minimalClientExec.execute(request, scope, null);
} catch (Exception ex) {
Mockito.verify(endpoint).discardConnection();
throw ex;
}
}
@Test(expected=RuntimeException.class)
public void testExecRuntimeException() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenThrow(new RuntimeException("Ka-boom"));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
try {
minimalClientExec.execute(request, scope, null);
} catch (final Exception ex) {
Mockito.verify(endpoint).discardConnection();
throw ex;
}
}
@Test(expected=HttpException.class)
public void testExecHttpException() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenThrow(new HttpException("Ka-boom"));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
try {
minimalClientExec.execute(request, scope, null);
} catch (final Exception ex) {
Mockito.verify(endpoint).discardConnection();
throw ex;
}
}
@Test(expected=IOException.class)
public void testExecIOException() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://bar/test");
Mockito.when(endpoint.execute(
Mockito.same(request),
Mockito.<HttpClientContext>any())).thenThrow(new IOException("Ka-boom"));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
try {
minimalClientExec.execute(request, scope, null);
} catch (final Exception ex) {
Mockito.verify(endpoint).discardConnection();
throw ex;
}
}
}

View File

@ -26,22 +26,41 @@
*/
package org.apache.hc.client5.http.impl.sync;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.auth.AuthChallenge;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.impl.auth.NTLMScheme;
import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.NonRepeatableRequestException;
import org.apache.hc.client5.http.sync.ExecChain;
import org.apache.hc.client5.http.sync.ExecRuntime;
import org.apache.hc.client5.http.sync.methods.HttpGet;
import org.apache.hc.client5.http.sync.methods.HttpPost;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.junit.Assert;
@ -50,6 +69,8 @@ import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@SuppressWarnings({"static-access"}) // test code
public class TestProtocolExec {
@ -57,18 +78,24 @@ public class TestProtocolExec {
@Mock
private HttpProcessor httpProcessor;
@Mock
private AuthenticationStrategy targetAuthStrategy;
@Mock
private AuthenticationStrategy proxyAuthStrategy;
@Mock
private ExecChain chain;
@Mock
private ExecRuntime endpoint;
private ExecRuntime execRuntime;
private ProtocolExec protocolExec;
private HttpHost target;
private HttpHost proxy;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
protocolExec = new ProtocolExec(httpProcessor);
protocolExec = new ProtocolExec(httpProcessor, targetAuthStrategy, proxyAuthStrategy);
target = new HttpHost("foo", 80);
proxy = new HttpHost("bar", 8888);
}
@Test
@ -83,7 +110,7 @@ public class TestProtocolExec {
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenReturn(response);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
protocolExec.execute(request, scope, chain);
Mockito.verify(httpProcessor).process(request, null, context);
@ -107,7 +134,7 @@ public class TestProtocolExec {
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenReturn(response);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
protocolExec.execute(request, scope, chain);
Assert.assertEquals(new URI("http://bar/test"), request.getUri());
final CredentialsProvider credentialsProvider = context.getCredentialsProvider();
@ -129,11 +156,11 @@ public class TestProtocolExec {
Mockito.<ExecChain.Scope>any())).thenReturn(response);
Mockito.doThrow(new HttpException("Ooopsie")).when(httpProcessor).process(
Mockito.same(response), Mockito.isNull(EntityDetails.class), Mockito.<HttpContext>any());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
try {
protocolExec.execute(request, scope, chain);
} catch (final Exception ex) {
Mockito.verify(response).close();
Mockito.verify(execRuntime).discardConnection();
throw ex;
}
}
@ -150,11 +177,11 @@ public class TestProtocolExec {
Mockito.<ExecChain.Scope>any())).thenReturn(response);
Mockito.doThrow(new IOException("Ooopsie")).when(httpProcessor).process(
Mockito.same(response), Mockito.isNull(EntityDetails.class), Mockito.<HttpContext>any());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
try {
protocolExec.execute(request, scope, chain);
} catch (final Exception ex) {
Mockito.verify(response).close();
Mockito.verify(execRuntime).discardConnection();
throw ex;
}
}
@ -171,13 +198,143 @@ public class TestProtocolExec {
Mockito.<ExecChain.Scope>any())).thenReturn(response);
Mockito.doThrow(new RuntimeException("Ooopsie")).when(httpProcessor).process(
Mockito.same(response), Mockito.isNull(EntityDetails.class), Mockito.<HttpContext>any());
final ExecChain.Scope scope = new ExecChain.Scope(route, request, endpoint, context);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
try {
protocolExec.execute(request, scope, chain);
} catch (final Exception ex) {
Mockito.verify(response).close();
Mockito.verify(execRuntime).discardConnection();
throw ex;
}
}
@Test
public void testExecRequestRetryOnAuthChallenge() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final ClassicHttpRequest request = new HttpGet("http://foo/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(401, "Huh?");
response1.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final InputStream instream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {2, 3, 4}));
response2.setEntity(EntityBuilder.create()
.setStream(instream2)
.build());
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(target), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response1, response2);
Mockito.when(targetAuthStrategy.select(
Mockito.eq(ChallengeType.TARGET),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
Mockito.when(execRuntime.isConnectionReusable()).thenReturn(true);
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
final ClassicHttpResponse finalResponse = protocolExec.execute(request, scope, chain);
Mockito.verify(chain, Mockito.times(2)).proceed(request, scope);
Mockito.verify(instream1).close();
Mockito.verify(instream2, Mockito.never()).close();
Assert.assertNotNull(finalResponse);
Assert.assertEquals(200, finalResponse.getCode());
}
@Test
public void testExecEntityEnclosingRequestRetryOnAuthChallenge() throws Exception {
final HttpRoute route = new HttpRoute(target, proxy);
final ClassicHttpRequest request = new HttpGet("http://foo/test");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(401, "Huh?");
response1.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
final InputStream instream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {2, 3, 4}));
response2.setEntity(EntityBuilder.create()
.setStream(instream2)
.build());
final HttpClientContext context = new HttpClientContext();
final AuthExchange proxyAuthExchange = new AuthExchange();
proxyAuthExchange.setState(AuthExchange.State.SUCCESS);
proxyAuthExchange.select(new NTLMScheme());
context.setAuthExchange(proxy, proxyAuthExchange);
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(target), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response1, response2);
Mockito.when(targetAuthStrategy.select(
Mockito.eq(ChallengeType.TARGET),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
final ClassicHttpResponse finalResponse = protocolExec.execute(request, scope, chain);
Mockito.verify(chain, Mockito.times(2)).proceed(request, scope);
Mockito.verify(execRuntime).disconnect();
Mockito.verify(instream2, Mockito.never()).close();
Assert.assertNotNull(finalResponse);
Assert.assertEquals(200, finalResponse.getCode());
Assert.assertNull(proxyAuthExchange.getAuthScheme());
}
@Test(expected = NonRepeatableRequestException.class)
public void testExecEntityEnclosingRequest() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = new HttpClientContext();
final HttpPost request = new HttpPost("http://foo/test");
final InputStream instream0 = new ByteArrayInputStream(new byte[] {1, 2, 3});
request.setEntity(EntityBuilder.create()
.setStream(instream0)
.build());
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(401, "Huh?");
response1.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=test");
final InputStream instream1 = new ByteArrayInputStream(new byte[] {1, 2, 3});
response1.setEntity(EntityBuilder.create()
.setStream(instream1)
.build());
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(target), new UsernamePasswordCredentials("user", "pass".toCharArray()));
context.setCredentialsProvider(credentialsProvider);
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenAnswer(new Answer<HttpResponse>() {
@Override
public HttpResponse answer(final InvocationOnMock invocationOnMock) throws Throwable {
final Object[] args = invocationOnMock.getArguments();
final ClassicHttpRequest requestEE = (ClassicHttpRequest) args[0];
requestEE.getEntity().writeTo(new ByteArrayOutputStream());
return response1;
}
});
Mockito.when(targetAuthStrategy.select(
Mockito.eq(ChallengeType.TARGET),
Mockito.<Map<String, AuthChallenge>>any(),
Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
protocolExec.execute(request, scope, chain);
}
}