From 3235f009d5673536be5c668df409ac3f83808d89 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Thu, 25 Jan 2024 15:10:01 +0100 Subject: [PATCH] HTTPCLIENT-751: Support for RFC 2817 (Upgrading to TLS Within HTTP/1.1) --- .../hc/client5/http/config/RequestConfig.java | 40 +++++- .../http/impl/ProtocolSwitchStrategy.java | 75 ++++++++++ .../impl/async/HttpAsyncClientBuilder.java | 4 +- .../impl/async/HttpAsyncMainClientExec.java | 36 ++++- .../http/impl/classic/HttpClientBuilder.java | 4 +- .../http/impl/classic/MainClientExec.java | 28 +++- .../DefaultHttpClientConnectionOperator.java | 10 +- .../PoolingHttpClientConnectionManager.java | 7 +- .../DefaultAsyncClientConnectionOperator.java | 9 +- .../PoolingAsyncClientConnectionManager.java | 32 +++-- .../http/protocol/RequestExpectContinue.java | 4 +- .../client5/http/protocol/RequestUpgrade.java | 85 +++++++++++ .../http/impl/TestProtocolSwitchStrategy.java | 98 +++++++++++++ .../http/impl/classic/TestMainClientExec.java | 13 +- .../TestBasicHttpClientConnectionManager.java | 14 +- .../io/TestHttpClientConnectionOperator.java | 10 +- ...estPoolingHttpClientConnectionManager.java | 14 +- .../http/protocol/TestRequestUpgrade.java | 132 ++++++++++++++++++ 18 files changed, 554 insertions(+), 61 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSwitchStrategy.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestUpgrade.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSwitchStrategy.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRequestUpgrade.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java index 1028dc9df..d747c5b6d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java @@ -62,13 +62,14 @@ public class RequestConfig implements Cloneable { private final TimeValue connectionKeepAlive; private final boolean contentCompressionEnabled; private final boolean hardCancellationEnabled; + private final boolean protocolUpgradeEnabled; /** * Intended for CDI compatibility */ protected RequestConfig() { this(false, null, null, false, false, 0, false, null, null, - DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false); + DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false); } RequestConfig( @@ -86,7 +87,8 @@ public class RequestConfig implements Cloneable { final Timeout responseTimeout, final TimeValue connectionKeepAlive, final boolean contentCompressionEnabled, - final boolean hardCancellationEnabled) { + final boolean hardCancellationEnabled, + final boolean protocolUpgradeEnabled) { super(); this.expectContinueEnabled = expectContinueEnabled; this.proxy = proxy; @@ -103,6 +105,7 @@ public class RequestConfig implements Cloneable { this.connectionKeepAlive = connectionKeepAlive; this.contentCompressionEnabled = contentCompressionEnabled; this.hardCancellationEnabled = hardCancellationEnabled; + this.protocolUpgradeEnabled = protocolUpgradeEnabled; } /** @@ -217,6 +220,13 @@ public class RequestConfig implements Cloneable { return hardCancellationEnabled; } + /** + * @see Builder#setProtocolUpgradeEnabled(boolean) (boolean) + */ + public boolean isProtocolUpgradeEnabled() { + return protocolUpgradeEnabled; + } + @Override protected RequestConfig clone() throws CloneNotSupportedException { return (RequestConfig) super.clone(); @@ -241,6 +251,7 @@ public class RequestConfig implements Cloneable { builder.append(", connectionKeepAlive=").append(connectionKeepAlive); builder.append(", contentCompressionEnabled=").append(contentCompressionEnabled); builder.append(", hardCancellationEnabled=").append(hardCancellationEnabled); + builder.append(", protocolUpgradeEnabled=").append(protocolUpgradeEnabled); builder.append("]"); return builder.toString(); } @@ -265,7 +276,8 @@ public class RequestConfig implements Cloneable { .setResponseTimeout(config.getResponseTimeout()) .setConnectionKeepAlive(config.getConnectionKeepAlive()) .setContentCompressionEnabled(config.isContentCompressionEnabled()) - .setHardCancellationEnabled(config.isHardCancellationEnabled()); + .setHardCancellationEnabled(config.isHardCancellationEnabled()) + .setProtocolUpgradeEnabled(config.isProtocolUpgradeEnabled()); } public static class Builder { @@ -285,6 +297,7 @@ public class RequestConfig implements Cloneable { private TimeValue connectionKeepAlive; private boolean contentCompressionEnabled; private boolean hardCancellationEnabled; + private boolean protocolUpgradeEnabled; Builder() { super(); @@ -294,6 +307,7 @@ public class RequestConfig implements Cloneable { this.connectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT; this.contentCompressionEnabled = true; this.hardCancellationEnabled = true; + this.protocolUpgradeEnabled = true; } /** @@ -570,6 +584,23 @@ public class RequestConfig implements Cloneable { return this; } + /** + * Determines whether the client server should automatically attempt to upgrade + * to a safer or a newer version of the protocol, whenever possible. + *

+ * Presently supported: HTTP/1.1 TLS upgrade + *

+ *

+ * Default: {@code true} + *

+ * + * @since 5.4 + */ + public Builder setProtocolUpgradeEnabled(final boolean protocolUpgradeEnabled) { + this.protocolUpgradeEnabled = protocolUpgradeEnabled; + return this; + } + public RequestConfig build() { return new RequestConfig( expectContinueEnabled, @@ -586,7 +617,8 @@ public class RequestConfig implements Cloneable { responseTimeout, connectionKeepAlive != null ? connectionKeepAlive : DEFAULT_CONN_KEEP_ALIVE, contentCompressionEnabled, - hardCancellationEnabled); + hardCancellationEnabled, + protocolUpgradeEnabled); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSwitchStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSwitchStrategy.java new file mode 100644 index 000000000..eed4bcc57 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSwitchStrategy.java @@ -0,0 +1,75 @@ +/* + * ==================================================================== + * 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 + * . + * + */ +package org.apache.hc.client5.http.impl; + +import java.util.Iterator; + +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpMessage; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.http.message.MessageSupport; +import org.apache.hc.core5.http.ssl.TLS; + +/** + * Protocol switch handler. + * + * @since 5.4 + */ +@Internal +public final class ProtocolSwitchStrategy { + + enum ProtocolSwitch { FAILURE, TLS } + + public ProtocolVersion switchProtocol(final HttpMessage response) throws ProtocolException { + final Iterator it = MessageSupport.iterateTokens(response, HttpHeaders.UPGRADE); + + ProtocolVersion tlsUpgrade = null; + while (it.hasNext()) { + final String token = it.next(); + if (token.startsWith("TLS")) { + // TODO: Improve handling of HTTP protocol token once HttpVersion has a #parse method + try { + tlsUpgrade = token.length() == 3 ? TLS.V_1_2.getVersion() : TLS.parse(token.replace("TLS/", "TLSv")); + } catch (final ParseException ex) { + throw new ProtocolException("Invalid protocol: " + token); + } + } else if (token.equals("HTTP/1.1")) { + // TODO: Improve handling of HTTP protocol token once HttpVersion has a #parse method + } else { + throw new ProtocolException("Unsupported protocol: " + token); + } + } + if (tlsUpgrade == null) { + throw new ProtocolException("Invalid protocol switch response"); + } + return tlsUpgrade; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java index 5fad7a84a..f06a234b8 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java @@ -76,6 +76,7 @@ import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.protocol.RequestAddCookies; import org.apache.hc.client5.http.protocol.RequestDefaultHeaders; import org.apache.hc.client5.http.protocol.RequestExpectContinue; +import org.apache.hc.client5.http.protocol.RequestUpgrade; import org.apache.hc.client5.http.protocol.RequestValidateTrace; import org.apache.hc.client5.http.protocol.ResponseProcessCookies; import org.apache.hc.client5.http.routing.HttpRoutePlanner; @@ -843,7 +844,8 @@ public class HttpAsyncClientBuilder { new H2RequestContent(), new H2RequestConnControl(), new RequestUserAgent(userAgentCopy), - new RequestExpectContinue()); + new RequestExpectContinue(), + new RequestUpgrade()); if (!cookieManagementDisabled) { b.add(RequestAddCookies.INSTANCE); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java index 86eff6164..953d7c6d9 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncMainClientExec.java @@ -40,17 +40,21 @@ import org.apache.hc.client5.http.async.AsyncExecCallback; import org.apache.hc.client5.http.async.AsyncExecChain; import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; +import org.apache.hc.client5.http.impl.ProtocolSwitchStrategy; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.concurrent.CancellableDependency; +import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; 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.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncDataConsumer; @@ -82,6 +86,7 @@ class HttpAsyncMainClientExec implements AsyncExecChainHandler { private final HttpProcessor httpProcessor; private final ConnectionKeepAliveStrategy keepAliveStrategy; private final UserTokenHandler userTokenHandler; + private final ProtocolSwitchStrategy protocolSwitchStrategy; HttpAsyncMainClientExec(final HttpProcessor httpProcessor, final ConnectionKeepAliveStrategy keepAliveStrategy, @@ -89,6 +94,7 @@ class HttpAsyncMainClientExec implements AsyncExecChainHandler { this.httpProcessor = Args.notNull(httpProcessor, "HTTP protocol processor"); this.keepAliveStrategy = keepAliveStrategy; this.userTokenHandler = userTokenHandler; + this.protocolSwitchStrategy = new ProtocolSwitchStrategy(); } @Override @@ -195,7 +201,35 @@ class HttpAsyncMainClientExec implements AsyncExecChainHandler { public void consumeInformation( final HttpResponse response, final HttpContext context) throws HttpException, IOException { - asyncExecCallback.handleInformationResponse(response); + if (response.getCode() == HttpStatus.SC_SWITCHING_PROTOCOLS) { + final ProtocolVersion upgradeProtocol = protocolSwitchStrategy.switchProtocol(response); + if (upgradeProtocol == null || !upgradeProtocol.getProtocol().equals("TLS")) { + throw new ProtocolException("Failure switching protocols"); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Switching to {}", upgradeProtocol); + } + execRuntime.upgradeTls(clientContext, new FutureCallback() { + + @Override + public void completed(final AsyncExecRuntime result) { + LOG.debug("Successfully switched to {}", upgradeProtocol); + } + + @Override + public void failed(final Exception ex) { + asyncExecCallback.failed(ex); + } + + @Override + public void cancelled() { + asyncExecCallback.failed(new InterruptedIOException()); + } + + }); + } else { + asyncExecCallback.handleInformationResponse(response); + } } @Override diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java index 3634ec86d..5c7ec95d1 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java @@ -80,6 +80,7 @@ import org.apache.hc.client5.http.protocol.RequestAddCookies; import org.apache.hc.client5.http.protocol.RequestClientConnControl; import org.apache.hc.client5.http.protocol.RequestDefaultHeaders; import org.apache.hc.client5.http.protocol.RequestExpectContinue; +import org.apache.hc.client5.http.protocol.RequestUpgrade; import org.apache.hc.client5.http.protocol.RequestValidateTrace; import org.apache.hc.client5.http.protocol.ResponseProcessCookies; import org.apache.hc.client5.http.routing.HttpRoutePlanner; @@ -824,7 +825,8 @@ public class HttpClientBuilder { new RequestContent(), new RequestClientConnControl(), new RequestUserAgent(userAgentCopy), - new RequestExpectContinue()); + new RequestExpectContinue(), + new RequestUpgrade()); if (!cookieManagementDisabled) { b.add(RequestAddCookies.INSTANCE); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java index 5e8d9f1ee..31fd19f2a 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/MainClientExec.java @@ -37,6 +37,7 @@ import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.impl.ConnectionShutdownException; +import org.apache.hc.client5.http.impl.ProtocolSwitchStrategy; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; @@ -47,6 +48,9 @@ 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.HttpStatus; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.ProtocolVersion; import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; @@ -74,6 +78,7 @@ public final class MainClientExec implements ExecChainHandler { private final ConnectionReuseStrategy reuseStrategy; private final ConnectionKeepAliveStrategy keepAliveStrategy; private final UserTokenHandler userTokenHandler; + private final ProtocolSwitchStrategy protocolSwitchStrategy; /** * @since 4.4 @@ -89,6 +94,7 @@ public final class MainClientExec implements ExecChainHandler { this.reuseStrategy = Args.notNull(reuseStrategy, "Connection reuse strategy"); this.keepAliveStrategy = Args.notNull(keepAliveStrategy, "Connection keep alive strategy"); this.userTokenHandler = Args.notNull(userTokenHandler, "User token handler"); + this.protocolSwitchStrategy = new ProtocolSwitchStrategy(); } @Override @@ -113,7 +119,27 @@ public final class MainClientExec implements ExecChainHandler { httpProcessor.process(request, request.getEntity(), context); - final ClassicHttpResponse response = execRuntime.execute(exchangeId, request, null, context); + final ClassicHttpResponse response = execRuntime.execute( + exchangeId, + request, + (r, connection, c) -> { + if (r.getCode() == HttpStatus.SC_SWITCHING_PROTOCOLS) { + final ProtocolVersion upgradeProtocol = protocolSwitchStrategy.switchProtocol(r); + if (upgradeProtocol == null || !upgradeProtocol.getProtocol().equals("TLS")) { + throw new ProtocolException("Failure switching protocols"); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Switching to {}", upgradeProtocol); + } + try { + execRuntime.upgradeTls(context); + } catch (final IOException ex) { + throw new HttpException("Failure upgrading to TLS", ex); + } + LOG.debug("Successfully switched to {}", upgradeProtocol); + } + }, + context); context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response); httpProcessor.process(response, response.getEntity(), context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java index 64f0a6483..c95299496 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/DefaultHttpClientConnectionOperator.java @@ -53,6 +53,7 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.config.Lookup; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.protocol.HttpContext; @@ -184,7 +185,7 @@ public class DefaultHttpClientConnectionOperator implements HttpClientConnection final Timeout soTimeout = socketConfig.getSoTimeout(); final SocketAddress socksProxyAddress = socketConfig.getSocksProxyAddress(); final Proxy socksProxy = socksProxyAddress != null ? new Proxy(Proxy.Type.SOCKS, socksProxyAddress) : null; - final int port = this.schemePortResolver.resolve(host); + final int port = this.schemePortResolver.resolve(host.getSchemeName(), host); for (int i = 0; i < remoteAddresses.length; i++) { final InetAddress address = remoteAddresses[i]; final boolean last = i == remoteAddresses.length - 1; @@ -269,13 +270,14 @@ public class DefaultHttpClientConnectionOperator implements HttpClientConnection if (socket == null) { throw new ConnectionClosedException("Connection is closed"); } - final TlsSocketStrategy tlsSocketStrategy = tlsSocketStrategyLookup != null ? tlsSocketStrategyLookup.lookup(host.getSchemeName()) : null; + final String newProtocol = URIScheme.HTTP.same(host.getSchemeName()) ? URIScheme.HTTPS.id : host.getSchemeName(); + final TlsSocketStrategy tlsSocketStrategy = tlsSocketStrategyLookup != null ? tlsSocketStrategyLookup.lookup(newProtocol) : null; if (tlsSocketStrategy != null) { - final int port = this.schemePortResolver.resolve(host); + final int port = this.schemePortResolver.resolve(newProtocol, host); final SSLSocket upgradedSocket = tlsSocketStrategy.upgrade(socket, host.getHostName(), port, attachment, context); conn.bind(upgradedSocket); } else { - throw new UnsupportedSchemeException(host.getSchemeName() + " protocol is not supported"); + throw new UnsupportedSchemeException(newProtocol + " protocol is not supported"); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java index 153e68ef7..9ef411cc6 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/io/PoolingHttpClientConnectionManager.java @@ -505,10 +505,9 @@ public class PoolingHttpClientConnectionManager Args.notNull(endpoint, "Managed endpoint"); final InternalConnectionEndpoint internalEndpoint = cast(endpoint); final PoolEntry poolEntry = internalEndpoint.getValidatedPoolEntry(); - final HttpRoute route = poolEntry.getRoute(); - final HttpHost host = route.getProxyHost() != null ? route.getProxyHost() : route.getTargetHost(); - final TlsConfig tlsConfig = resolveTlsConfig(host); - this.connectionOperator.upgrade(poolEntry.getConnection(), route.getTargetHost(), tlsConfig, context); + final HttpHost target = poolEntry.getRoute().getTargetHost(); + final TlsConfig tlsConfig = resolveTlsConfig(target); + this.connectionOperator.upgrade(poolEntry.getConnection(), target, tlsConfig, context); } @Override diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java index 0a046887d..d31ab382f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/DefaultAsyncClientConnectionOperator.java @@ -45,6 +45,7 @@ import org.apache.hc.core5.concurrent.ComplexFuture; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.concurrent.FutureContribution; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.config.Lookup; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.http.protocol.HttpContext; @@ -174,11 +175,13 @@ final class DefaultAsyncClientConnectionOperator implements AsyncClientConnectio final Object attachment, final HttpContext context, final FutureCallback callback) { - final TlsStrategy tlsStrategy = tlsStrategyLookup != null ? tlsStrategyLookup.lookup(host.getSchemeName()) : null; + final String newProtocol = URIScheme.HTTP.same(host.getSchemeName()) ? URIScheme.HTTPS.id : host.getSchemeName(); + final HttpHost remoteEndpoint = RoutingSupport.normalize(host, schemePortResolver); + final TlsStrategy tlsStrategy = tlsStrategyLookup != null ? tlsStrategyLookup.lookup(newProtocol) : null; if (tlsStrategy != null) { tlsStrategy.upgrade( connection, - host, + remoteEndpoint, attachment, null, new CallbackContribution(callback) { @@ -192,7 +195,7 @@ final class DefaultAsyncClientConnectionOperator implements AsyncClientConnectio }); } else { - callback.failed(new UnsupportedSchemeException(host.getSchemeName() + " protocol is not supported")); + callback.failed(new UnsupportedSchemeException(newProtocol + " protocol is not supported")); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java index f2cc2ffc7..f013ccf9f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/nio/PoolingAsyncClientConnectionManager.java @@ -245,13 +245,21 @@ public class PoolingAsyncClientConnectionManager implements AsyncClientConnectio return connectionConfig != null ? connectionConfig : ConnectionConfig.DEFAULT; } - private TlsConfig resolveTlsConfig(final HttpHost host, final Object attachment) { - if (attachment instanceof TlsConfig) { - return (TlsConfig) attachment; - } + private TlsConfig resolveTlsConfig(final HttpHost host) { final Resolver resolver = this.tlsConfigResolver; - final TlsConfig tlsConfig = resolver != null ? resolver.resolve(host) : null; - return tlsConfig != null ? tlsConfig : TlsConfig.DEFAULT; + TlsConfig tlsConfig = resolver != null ? resolver.resolve(host) : null; + if (tlsConfig == null) { + tlsConfig = TlsConfig.DEFAULT; + } + if (URIScheme.HTTP.same(host.getSchemeName()) + && tlsConfig.getHttpVersionPolicy() == HttpVersionPolicy.NEGOTIATE) { + // Plain HTTP does not support protocol negotiation. + // Fall back to HTTP/1.1 + tlsConfig = TlsConfig.copy(tlsConfig) + .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1) + .build(); + } + return tlsConfig; } @Override @@ -441,7 +449,6 @@ public class PoolingAsyncClientConnectionManager implements AsyncClientConnectio } final InetSocketAddress localAddress = route.getLocalSocketAddress(); final ConnectionConfig connectionConfig = resolveConnectionConfig(route); - final TlsConfig tlsConfig = resolveTlsConfig(host, attachment); final Timeout connectTimeout = timeout != null ? timeout : connectionConfig.getConnectTimeout(); if (LOG.isDebugEnabled()) { @@ -452,9 +459,7 @@ public class PoolingAsyncClientConnectionManager implements AsyncClientConnectio host, localAddress, connectTimeout, - route.isTunnelled() ? TlsConfig.copy(tlsConfig) - .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1) - .build() : tlsConfig, + route.isTunnelled() ? null : resolveTlsConfig(host), context, new FutureCallback() { @@ -499,12 +504,11 @@ public class PoolingAsyncClientConnectionManager implements AsyncClientConnectio final InternalConnectionEndpoint internalEndpoint = cast(endpoint); final PoolEntry poolEntry = internalEndpoint.getValidatedPoolEntry(); final HttpRoute route = poolEntry.getRoute(); - final HttpHost host = route.getProxyHost() != null ? route.getProxyHost() : route.getTargetHost(); - final TlsConfig tlsConfig = resolveTlsConfig(host, attachment); + final HttpHost target = route.getTargetHost(); connectionOperator.upgrade( poolEntry.getConnection(), - route.getTargetHost(), - attachment != null ? attachment : tlsConfig, + target, + attachment != null ? attachment : resolveTlsConfig(target), context, new CallbackContribution(callback) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestExpectContinue.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestExpectContinue.java index a60a7ea15..d78a28c10 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestExpectContinue.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestExpectContinue.java @@ -66,11 +66,11 @@ public class RequestExpectContinue implements HttpRequestInterceptor { Args.notNull(request, "HTTP request"); if (!request.containsHeader(HttpHeaders.EXPECT)) { - final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : HttpVersion.HTTP_1_1; + final HttpClientContext clientContext = HttpClientContext.adapt(context); + final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : clientContext.getProtocolVersion(); // Do not send the expect header if request body is known to be empty if (entity != null && entity.getContentLength() != 0 && !version.lessEquals(HttpVersion.HTTP_1_0)) { - final HttpClientContext clientContext = HttpClientContext.adapt(context); final RequestConfig config = clientContext.getRequestConfig(); if (config.isExpectContinueEnabled()) { request.addHeader(HttpHeaders.EXPECT, HeaderElements.CONTINUE); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestUpgrade.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestUpgrade.java new file mode 100644 index 000000000..d5ca81d22 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestUpgrade.java @@ -0,0 +1,85 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + +package org.apache.hc.client5.http.protocol; + +import java.io.IOException; + +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +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.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @since 5.4 + */ +@Contract(threading = ThreadingBehavior.STATELESS) +public final class RequestUpgrade implements HttpRequestInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(RequestUpgrade.class); + + public RequestUpgrade() { + } + + @Override + public void process( + final HttpRequest request, + final EntityDetails entity, + final HttpContext context) throws HttpException, IOException { + Args.notNull(request, "HTTP request"); + Args.notNull(context, "HTTP context"); + + final HttpClientContext clientContext = HttpClientContext.adapt(context); + final RequestConfig requestConfig = clientContext.getRequestConfig(); + if (requestConfig.isProtocolUpgradeEnabled()) { + final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : clientContext.getProtocolVersion(); + if (!request.containsHeader(HttpHeaders.UPGRADE) && version.getMajor() == 1 && version.getMinor() >= 1) { + if (LOG.isDebugEnabled()) { + LOG.debug("Connection is upgradable: protocol version = {}", version); + } + final String method = request.getMethod(); + if ((Method.OPTIONS.isSame(method) || Method.HEAD.isSame(method) || Method.GET.isSame(method)) && + clientContext.getSSLSession() == null) { + LOG.debug("Connection is upgradable to TLS: method = {}", method); + request.addHeader(HttpHeaders.UPGRADE, "TLS/1.2"); + request.addHeader(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE); + } + } + } + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSwitchStrategy.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSwitchStrategy.java new file mode 100644 index 000000000..1775bf49c --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSwitchStrategy.java @@ -0,0 +1,98 @@ +/* + * ==================================================================== + * 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 + * . + * + */ +package org.apache.hc.client5.http.impl; + +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.ssl.TLS; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Simple tests for {@link DefaultAuthenticationStrategy}. + */ +public class TestProtocolSwitchStrategy { + + ProtocolSwitchStrategy switchStrategy; + + @BeforeEach + void setUp() { + switchStrategy = new ProtocolSwitchStrategy(); + } + + @Test + public void testSwitchToTLS() throws Exception { + final HttpResponse response1 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response1.addHeader(HttpHeaders.UPGRADE, "TLS"); + Assertions.assertEquals(TLS.V_1_2.getVersion(), switchStrategy.switchProtocol(response1)); + + final HttpResponse response2 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response2.addHeader(HttpHeaders.UPGRADE, "TLS/1.3"); + Assertions.assertEquals(TLS.V_1_3.getVersion(), switchStrategy.switchProtocol(response2)); + } + + @Test + public void testSwitchToHTTP11AndTLS() throws Exception { + final HttpResponse response1 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response1.addHeader(HttpHeaders.UPGRADE, "TLS, HTTP/1.1"); + Assertions.assertEquals(TLS.V_1_2.getVersion(), switchStrategy.switchProtocol(response1)); + + final HttpResponse response2 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response2.addHeader(HttpHeaders.UPGRADE, ",, HTTP/1.1, TLS, "); + Assertions.assertEquals(TLS.V_1_2.getVersion(), switchStrategy.switchProtocol(response2)); + + final HttpResponse response3 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response3.addHeader(HttpHeaders.UPGRADE, "HTTP/1.1"); + response3.addHeader(HttpHeaders.UPGRADE, "TLS/1.2"); + Assertions.assertEquals(TLS.V_1_2.getVersion(), switchStrategy.switchProtocol(response3)); + + final HttpResponse response4 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response4.addHeader(HttpHeaders.UPGRADE, "HTTP/1.1"); + response4.addHeader(HttpHeaders.UPGRADE, "TLS/1.2, TLS/1.3"); + Assertions.assertEquals(TLS.V_1_3.getVersion(), switchStrategy.switchProtocol(response4)); + } + + @Test + public void testSwitchInvalid() throws Exception { + final HttpResponse response1 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response1.addHeader(HttpHeaders.UPGRADE, "Crap"); + Assertions.assertThrows(ProtocolException.class, () -> switchStrategy.switchProtocol(response1)); + + final HttpResponse response2 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response2.addHeader(HttpHeaders.UPGRADE, "TLS, huh?"); + Assertions.assertThrows(ProtocolException.class, () -> switchStrategy.switchProtocol(response2)); + + final HttpResponse response3 = new BasicHttpResponse(HttpStatus.SC_SWITCHING_PROTOCOLS); + response3.addHeader(HttpHeaders.UPGRADE, ",,,"); + Assertions.assertThrows(ProtocolException.class, () -> switchStrategy.switchProtocol(response3)); + } + +} \ No newline at end of file diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestMainClientExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestMainClientExec.java index b90104132..1e7c3a65d 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestMainClientExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestMainClientExec.java @@ -56,7 +56,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -@SuppressWarnings({"boxing","static-access"}) // test code public class TestMainClientExec { @Mock @@ -104,7 +103,7 @@ public class TestMainClientExec { final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null); Mockito.verify(httpProcessor).process(request, null, context); - Mockito.verify(execRuntime).execute("test", request, null, context); + Mockito.verify(execRuntime).execute(Mockito.eq("test"), Mockito.same(request), Mockito.any(), Mockito.any()); Mockito.verify(httpProcessor).process(response, responseEntity, context); Assertions.assertEquals(route, context.getHttpRoute()); @@ -134,7 +133,7 @@ public class TestMainClientExec { final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null); - Mockito.verify(execRuntime).execute("test", request, null, context); + Mockito.verify(execRuntime).execute(Mockito.eq("test"), Mockito.same(request), Mockito.any(), Mockito.any()); Mockito.verify(execRuntime, Mockito.times(1)).markConnectionNonReusable(); Mockito.verify(execRuntime, Mockito.never()).releaseEndpoint(); @@ -164,7 +163,7 @@ public class TestMainClientExec { final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null); - Mockito.verify(execRuntime).execute("test", request, null, context); + Mockito.verify(execRuntime).execute(Mockito.eq("test"), Mockito.same(request), Mockito.any(), Mockito.any()); Mockito.verify(execRuntime).markConnectionNonReusable(); Mockito.verify(execRuntime).releaseEndpoint(); @@ -200,7 +199,7 @@ public class TestMainClientExec { final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null); - Mockito.verify(execRuntime).execute("test", request, null, context); + Mockito.verify(execRuntime).execute(Mockito.eq("test"), Mockito.same(request), Mockito.any(), Mockito.any()); Mockito.verify(execRuntime).markConnectionReusable(null, TimeValue.ofMilliseconds(678L)); Mockito.verify(execRuntime, Mockito.never()).releaseEndpoint(); @@ -231,7 +230,7 @@ public class TestMainClientExec { final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null); - Mockito.verify(execRuntime).execute("test", request, null, context); + Mockito.verify(execRuntime).execute(Mockito.eq("test"), Mockito.same(request), Mockito.any(), Mockito.any()); Mockito.verify(execRuntime).releaseEndpoint(); Assertions.assertNotNull(finalResponse); @@ -261,7 +260,7 @@ public class TestMainClientExec { final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); final ClassicHttpResponse finalResponse = mainClientExec.execute(request, scope, null); - Mockito.verify(execRuntime, Mockito.times(1)).execute("test", request, null, context); + Mockito.verify(execRuntime, Mockito.times(1)).execute(Mockito.eq("test"), Mockito.same(request), Mockito.any(), Mockito.any()); Mockito.verify(execRuntime, Mockito.never()).disconnectEndpoint(); Mockito.verify(execRuntime, Mockito.never()).releaseEndpoint(); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestBasicHttpClientConnectionManager.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestBasicHttpClientConnectionManager.java index 94fd2d38c..11075318f 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestBasicHttpClientConnectionManager.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestBasicHttpClientConnectionManager.java @@ -385,7 +385,7 @@ public class TestBasicHttpClientConnectionManager { mgr.setTlsConfig(tlsConfig); Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[] {remote}); - Mockito.when(schemePortResolver.resolve(target)).thenReturn(8443); + Mockito.when(schemePortResolver.resolve(target.getSchemeName(), target)).thenReturn(8443); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy); @@ -399,7 +399,7 @@ public class TestBasicHttpClientConnectionManager { mgr.connect(endpoint1, null, context); Mockito.verify(dnsResolver, Mockito.times(1)).resolve("somehost"); - Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target); + Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target.getSchemeName(), target); Mockito.verify(detachedSocketFactory, Mockito.times(1)).create(null); Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8443), 234); Mockito.verify(tlsSocketStrategy).upgrade(socket, "somehost", 8443, tlsConfig, context); @@ -407,7 +407,7 @@ public class TestBasicHttpClientConnectionManager { mgr.connect(endpoint1, TimeValue.ofMilliseconds(123), context); Mockito.verify(dnsResolver, Mockito.times(2)).resolve("somehost"); - Mockito.verify(schemePortResolver, Mockito.times(2)).resolve(target); + Mockito.verify(schemePortResolver, Mockito.times(2)).resolve(target.getSchemeName(), target); Mockito.verify(detachedSocketFactory, Mockito.times(2)).create(null); Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8443), 123); Mockito.verify(tlsSocketStrategy, Mockito.times(2)).upgrade(socket, "somehost", 8443, tlsConfig, context); @@ -442,15 +442,15 @@ public class TestBasicHttpClientConnectionManager { mgr.setTlsConfig(tlsConfig); Mockito.when(dnsResolver.resolve("someproxy")).thenReturn(new InetAddress[] {remote}); - Mockito.when(schemePortResolver.resolve(proxy)).thenReturn(8080); - Mockito.when(schemePortResolver.resolve(target)).thenReturn(8443); + Mockito.when(schemePortResolver.resolve(proxy.getSchemeName(), proxy)).thenReturn(8080); + Mockito.when(schemePortResolver.resolve(target.getSchemeName(), target)).thenReturn(8443); Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); mgr.connect(endpoint1, null, context); Mockito.verify(dnsResolver, Mockito.times(1)).resolve("someproxy"); - Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(proxy); + Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(proxy.getSchemeName(), proxy); Mockito.verify(detachedSocketFactory, Mockito.times(1)).create(null); Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8080), 234); @@ -458,7 +458,7 @@ public class TestBasicHttpClientConnectionManager { mgr.upgrade(endpoint1, context); - Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target); + Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target.getSchemeName(), target); Mockito.verify(tlsSocketStrategy, Mockito.times(1)).upgrade( socket, "somehost", 8443, tlsConfig, context); } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestHttpClientConnectionOperator.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestHttpClientConnectionOperator.java index 28a40fa31..b3b925f92 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestHttpClientConnectionOperator.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestHttpClientConnectionOperator.java @@ -91,7 +91,7 @@ public class TestHttpClientConnectionOperator { final InetAddress ip2 = InetAddress.getByAddress(new byte[] {127, 0, 0, 2}); Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[] { ip1, ip2 }); - Mockito.when(schemePortResolver.resolve(host)).thenReturn(80); + Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(80); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); final SocketConfig socketConfig = SocketConfig.custom() @@ -129,7 +129,7 @@ public class TestHttpClientConnectionOperator { .build(); Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[] { ip1, ip2 }); - Mockito.when(schemePortResolver.resolve(host)).thenReturn(443); + Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(443); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy); @@ -194,7 +194,7 @@ public class TestHttpClientConnectionOperator { final InetAddress ip2 = InetAddress.getByAddress(new byte[] {10, 0, 0, 2}); Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[] { ip1, ip2 }); - Mockito.when(schemePortResolver.resolve(host)).thenReturn(80); + Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(80); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); Mockito.doThrow(new ConnectException()).when(socket).connect( Mockito.eq(new InetSocketAddress(ip1, 80)), @@ -219,7 +219,7 @@ public class TestHttpClientConnectionOperator { final InetAddress ip = InetAddress.getByAddress(new byte[] {127, 0, 0, 23}); final HttpHost host = new HttpHost(ip); - Mockito.when(schemePortResolver.resolve(host)).thenReturn(80); + Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(80); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); final InetSocketAddress localAddress = new InetSocketAddress(local, 0); @@ -242,7 +242,7 @@ public class TestHttpClientConnectionOperator { Mockito.when(conn.isOpen()).thenReturn(true); Mockito.when(conn.getSocket()).thenReturn(socket); Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy); - Mockito.when(schemePortResolver.resolve(host)).thenReturn(443); + Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(443); final SSLSocket upgradedSocket = Mockito.mock(SSLSocket.class); Mockito.when(tlsSocketStrategy.upgrade( diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java index 8496a8c69..9d37adda9 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/io/TestPoolingHttpClientConnectionManager.java @@ -263,7 +263,7 @@ public class TestPoolingHttpClientConnectionManager { mgr.setDefaultTlsConfig(tlsConfig); Mockito.when(dnsResolver.resolve("somehost")).thenReturn(new InetAddress[]{remote}); - Mockito.when(schemePortResolver.resolve(target)).thenReturn(8443); + Mockito.when(schemePortResolver.resolve(target.getSchemeName(), target)).thenReturn(8443); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy); @@ -277,7 +277,7 @@ public class TestPoolingHttpClientConnectionManager { mgr.connect(endpoint1, null, context); Mockito.verify(dnsResolver, Mockito.times(1)).resolve("somehost"); - Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target); + Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target.getSchemeName(), target); Mockito.verify(detachedSocketFactory, Mockito.times(1)).create(null); Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8443), 234); Mockito.verify(tlsSocketStrategy).upgrade(socket, "somehost", 8443, tlsConfig, context); @@ -285,7 +285,7 @@ public class TestPoolingHttpClientConnectionManager { mgr.connect(endpoint1, TimeValue.ofMilliseconds(123), context); Mockito.verify(dnsResolver, Mockito.times(2)).resolve("somehost"); - Mockito.verify(schemePortResolver, Mockito.times(2)).resolve(target); + Mockito.verify(schemePortResolver, Mockito.times(2)).resolve(target.getSchemeName(), target); Mockito.verify(detachedSocketFactory, Mockito.times(2)).create(null); Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8443), 123); Mockito.verify(tlsSocketStrategy, Mockito.times(2)).upgrade(socket, "somehost", 8443, tlsConfig, context); @@ -330,15 +330,15 @@ public class TestPoolingHttpClientConnectionManager { mgr.setDefaultTlsConfig(tlsConfig); Mockito.when(dnsResolver.resolve("someproxy")).thenReturn(new InetAddress[] {remote}); - Mockito.when(schemePortResolver.resolve(proxy)).thenReturn(8080); - Mockito.when(schemePortResolver.resolve(target)).thenReturn(8443); + Mockito.when(schemePortResolver.resolve(proxy.getSchemeName(), proxy)).thenReturn(8080); + Mockito.when(schemePortResolver.resolve(target.getSchemeName(), target)).thenReturn(8443); Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy); Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket); mgr.connect(endpoint1, null, context); Mockito.verify(dnsResolver, Mockito.times(1)).resolve("someproxy"); - Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(proxy); + Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(proxy.getSchemeName(), proxy); Mockito.verify(detachedSocketFactory, Mockito.times(1)).create(null); Mockito.verify(socket, Mockito.times(1)).connect(new InetSocketAddress(remote, 8080), 234); @@ -347,7 +347,7 @@ public class TestPoolingHttpClientConnectionManager { mgr.upgrade(endpoint1, context); - Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target); + Mockito.verify(schemePortResolver, Mockito.times(1)).resolve(target.getSchemeName(), target); Mockito.verify(tlsSocketStrategy, Mockito.times(1)).upgrade( socket, "somehost", 8443, tlsConfig, context); } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRequestUpgrade.java b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRequestUpgrade.java new file mode 100644 index 000000000..42e126814 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/protocol/TestRequestUpgrade.java @@ -0,0 +1,132 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + +package org.apache.hc.client5.http.protocol; + +import javax.net.ssl.SSLSession; + +import org.apache.hc.client5.http.HeadersMatcher; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class TestRequestUpgrade { + + private RequestUpgrade interceptor; + private HttpClientContext context; + + @BeforeEach + void setUp() { + interceptor = new RequestUpgrade(); + context = HttpClientContext.create(); + } + + @Test + void testUpgrade() throws Exception { + final HttpRequest get = new BasicHttpRequest("GET", "/"); + interceptor.process(get, null, context); + MatcherAssert.assertThat(get.getHeaders(), + HeadersMatcher.same( + new BasicHeader(HttpHeaders.UPGRADE, "TLS/1.2"), + new BasicHeader(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE))); + final HttpRequest options = new BasicHttpRequest("OPTIONS", "/"); + interceptor.process(options, null, context); + MatcherAssert.assertThat(options.getHeaders(), + HeadersMatcher.same( + new BasicHeader(HttpHeaders.UPGRADE, "TLS/1.2"), + new BasicHeader(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE))); + final HttpRequest head = new BasicHttpRequest("HEAD", "/"); + interceptor.process(head, null, context); + MatcherAssert.assertThat(head.getHeaders(), + HeadersMatcher.same( + new BasicHeader(HttpHeaders.UPGRADE, "TLS/1.2"), + new BasicHeader(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE))); + } + + @Test + void testUpgradeDisabled() throws Exception { + context.setRequestConfig(RequestConfig.custom() + .setProtocolUpgradeEnabled(false) + .build()); + final HttpRequest get = new BasicHttpRequest("GET", "/"); + interceptor.process(get, null, context); + Assertions.assertFalse(get.containsHeader(HttpHeaders.UPGRADE)); + } + + @Test + void testDoNotUpgradeHTTP2() throws Exception { + context.setProtocolVersion(HttpVersion.HTTP_2); + final HttpRequest get = new BasicHttpRequest("GET", "/"); + interceptor.process(get, null, context); + Assertions.assertFalse(get.containsHeader(HttpHeaders.UPGRADE)); + } + + @Test + void testDoNotUpgradeHTTP10() throws Exception { + context.setProtocolVersion(HttpVersion.HTTP_1_0); + final HttpRequest get = new BasicHttpRequest("GET", "/"); + interceptor.process(get, null, context); + Assertions.assertFalse(get.containsHeader(HttpHeaders.UPGRADE)); + } + + @Test + void testDoUpgradeIfAlreadyTLS() throws Exception { + context.setAttribute(HttpCoreContext.SSL_SESSION, Mockito.mock(SSLSession.class)); + final HttpRequest get = new BasicHttpRequest("GET", "/"); + interceptor.process(get, null, context); + Assertions.assertFalse(get.containsHeader(HttpHeaders.UPGRADE)); + } + + @Test + void testDoUpgradeNonSafeMethodsOrTrace() throws Exception { + final HttpRequest post = new BasicHttpRequest("POST", "/"); + interceptor.process(post, null, context); + Assertions.assertFalse(post.containsHeader(HttpHeaders.UPGRADE)); + + final HttpRequest put = new BasicHttpRequest("PUT", "/"); + interceptor.process(put, null, context); + Assertions.assertFalse(put.containsHeader(HttpHeaders.UPGRADE)); + + final HttpRequest patch = new BasicHttpRequest("PATCH", "/"); + interceptor.process(patch, null, context); + Assertions.assertFalse(patch.containsHeader(HttpHeaders.UPGRADE)); + + final HttpRequest trace = new BasicHttpRequest("TRACE", "/"); + interceptor.process(trace, null, context); + Assertions.assertFalse(trace.containsHeader(HttpHeaders.UPGRADE)); + } + +}