Skip to content

Commit

Permalink
HTTPCLIENT-751: Support for RFC 2817 (Upgrading to TLS Within HTTP/1.1)
Browse files Browse the repository at this point in the history
  • Loading branch information
ok2c committed Jan 26, 2024
1 parent 9aa4640 commit 56b508f
Show file tree
Hide file tree
Showing 18 changed files with 554 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -86,7 +87,8 @@ protected RequestConfig() {
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;
Expand All @@ -103,6 +105,7 @@ protected RequestConfig() {
this.connectionKeepAlive = connectionKeepAlive;
this.contentCompressionEnabled = contentCompressionEnabled;
this.hardCancellationEnabled = hardCancellationEnabled;
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
}

/**
Expand Down Expand Up @@ -217,6 +220,13 @@ public boolean isHardCancellationEnabled() {
return hardCancellationEnabled;
}

/**
* @see Builder#setProtocolUpgradeEnabled(boolean) (boolean)
*/
public boolean isProtocolUpgradeEnabled() {
return protocolUpgradeEnabled;
}

@Override
protected RequestConfig clone() throws CloneNotSupportedException {
return (RequestConfig) super.clone();
Expand All @@ -241,6 +251,7 @@ public String toString() {
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();
}
Expand All @@ -265,7 +276,8 @@ public static RequestConfig.Builder copy(final RequestConfig config) {
.setResponseTimeout(config.getResponseTimeout())
.setConnectionKeepAlive(config.getConnectionKeepAlive())
.setContentCompressionEnabled(config.isContentCompressionEnabled())
.setHardCancellationEnabled(config.isHardCancellationEnabled());
.setHardCancellationEnabled(config.isHardCancellationEnabled())
.setProtocolUpgradeEnabled(config.isProtocolUpgradeEnabled());
}

public static class Builder {
Expand All @@ -285,6 +297,7 @@ public static class Builder {
private TimeValue connectionKeepAlive;
private boolean contentCompressionEnabled;
private boolean hardCancellationEnabled;
private boolean protocolUpgradeEnabled;

Builder() {
super();
Expand All @@ -294,6 +307,7 @@ public static class Builder {
this.connectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;
this.contentCompressionEnabled = true;
this.hardCancellationEnabled = true;
this.protocolUpgradeEnabled = true;
}

/**
Expand Down Expand Up @@ -570,6 +584,23 @@ public Builder setHardCancellationEnabled(final boolean hardCancellationEnabled)
return this;
}

/**
* Determines whether the client server should automatically attempt to upgrade
* to a safer or a newer version of the protocol, whenever possible.
* <p>
* Presently supported: HTTP/1.1 TLS upgrade
* </p>
* <p>
* Default: {@code true}
* </p>
*
* @since 5.4
*/
public Builder setProtocolUpgradeEnabled(final boolean protocolUpgradeEnabled) {
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
return this;
}

public RequestConfig build() {
return new RequestConfig(
expectContinueEnabled,
Expand All @@ -586,7 +617,8 @@ public RequestConfig build() {
responseTimeout,
connectionKeepAlive != null ? connectionKeepAlive : DEFAULT_CONN_KEEP_ALIVE,
contentCompressionEnabled,
hardCancellationEnabled);
hardCancellationEnabled,
protocolUpgradeEnabled);
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
* <http://www.apache.org/>.
*
*/
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<String> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
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;
Expand Down Expand Up @@ -843,7 +844,8 @@ public CloseableHttpAsyncClient build() {
new H2RequestContent(),
new H2RequestConnControl(),
new RequestUserAgent(userAgentCopy),
new RequestExpectContinue());
new RequestExpectContinue(),
new RequestUpgrade());
if (!cookieManagementDisabled) {
b.add(RequestAddCookies.INSTANCE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,21 @@
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;
Expand Down Expand Up @@ -82,13 +86,15 @@ 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,
final UserTokenHandler userTokenHandler) {
this.httpProcessor = Args.notNull(httpProcessor, "HTTP protocol processor");
this.keepAliveStrategy = keepAliveStrategy;
this.userTokenHandler = userTokenHandler;
this.protocolSwitchStrategy = new ProtocolSwitchStrategy();
}

@Override
Expand Down Expand Up @@ -195,7 +201,35 @@ public void endStream() throws IOException {
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<AsyncExecRuntime>() {

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
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;
Expand Down Expand Up @@ -824,7 +825,8 @@ public CloseableHttpClient build() {
new RequestContent(),
new RequestClientConnControl(),
new RequestUserAgent(userAgentCopy),
new RequestExpectContinue());
new RequestExpectContinue(),
new RequestUpgrade());
if (!cookieManagementDisabled) {
b.add(RequestAddCookies.INSTANCE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
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;
Expand All @@ -47,6 +48,9 @@
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;
Expand Down Expand Up @@ -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
Expand All @@ -89,6 +94,7 @@ public MainClientExec(
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
Expand All @@ -113,7 +119,27 @@ public ClassicHttpResponse execute(

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);
Expand Down
Loading

0 comments on commit 56b508f

Please sign in to comment.