Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* ====================================================================
* 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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

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.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.net.PercentCodec;
import org.apache.hc.core5.util.Args;

/**
* Codec for the HTTP {@code ALPN} header field (RFC 7639).
*
* @since 5.7
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
@Internal
public final class AlpnHeaderSupport {

private AlpnHeaderSupport() {
}

/**
* Formats a list of raw ALPN protocol IDs into a single {@code ALPN} header.
*/
public static Header formatValue(final List<String> protocolIds) {
Args.notEmpty(protocolIds, "protocolIds");
return MessageSupport.headerOfTokens(HttpHeaders.ALPN, protocolIds, AlpnHeaderSupport::encodeId);
}

/**
* Parses an {@code ALPN} header into decoded protocol IDs.
*
* @throws ProtocolException if a token is not a well-formed percent-encoded protocol ID.
*/
public static List<String> parseValue(final Header header) throws ProtocolException {
final List<String> tokens = new ArrayList<>();
MessageSupport.parseTokens(header, tokens::add);
final List<String> out = new ArrayList<>(tokens.size());
for (final String token : tokens) {
out.add(decodeId(token));
}
return out;
}

/**
* Encodes a single raw protocol ID to canonical token form using the HTTP token codec
* from core, which keeps RFC 7230 {@code tchar} octets literal and percent-encodes the
* rest (including {@code '%'}) with uppercase hexadecimal.
*/
public static String encodeId(final String id) {
Args.notBlank(id, "id");
return PercentCodec.HTTP_TOKEN.encode(id);
}

/**
* Decodes a percent-encoded token to a raw protocol ID using UTF-8.
* <p>
* A {@code '%'} that is not followed by two hexadecimal digits is a malformed
* token and is rejected as a protocol error.
*
* @throws ProtocolException if the token contains malformed percent-encoding.
*/
public static String decodeId(final String token) throws ProtocolException {
Args.notBlank(token, "token");
for (int i = 0; i < token.length(); i++) {
if (token.charAt(i) == '%') {
if (i + 2 >= token.length()
|| Character.digit(token.charAt(i + 1), 16) < 0
|| Character.digit(token.charAt(i + 2), 16) < 0) {
throw new ProtocolException("Malformed percent-encoding in ALPN protocol id: " + token);
}
i += 2;
}
}
return PercentCodec.decode(token, StandardCharsets.UTF_8);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -47,6 +48,7 @@
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.AlpnHeaderSupport;
import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
import org.apache.hc.client5.http.impl.auth.AuthenticationHandler;
import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
Expand Down Expand Up @@ -76,6 +78,8 @@
import org.apache.hc.core5.http.nio.RequestChannel;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http2.HttpVersionPolicy;
import org.apache.hc.core5.http2.ssl.H2TlsSupport;
import org.apache.hc.core5.util.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -426,6 +430,15 @@ public void produceRequest(final RequestChannel requestChannel,
final HttpRequest connect = new BasicHttpRequest(Method.CONNECT, nextHop, nextHop.toHostString());
connect.setVersion(HttpVersion.HTTP_1_1);

// RFC 7639: advertise the same ALPN protocols the tunnel's TLS layer will offer, derived
// from the target's HttpVersionPolicy published on the context by the connection manager,
// so the header cannot diverge from the protocol actually negotiated inside the tunnel.
if (scope.route.isSecure()) {
final HttpVersionPolicy configured = clientContext.getHttpVersionPolicy();
final HttpVersionPolicy versionPolicy = configured != null ? configured : HttpVersionPolicy.NEGOTIATE;
connect.setHeader(AlpnHeaderSupport.formatValue(
Arrays.asList(H2TlsSupport.selectApplicationProtocols(versionPolicy))));
}
proxyHttpProcessor.process(connect, null, clientContext);
authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, clientContext);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
package org.apache.hc.client5.http.impl.classic;

import java.io.IOException;
import java.util.Arrays;

import org.apache.hc.client5.http.AuthenticationStrategy;
import org.apache.hc.client5.http.EndpointInfo;
Expand All @@ -40,6 +41,7 @@
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.AlpnHeaderSupport;
import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
import org.apache.hc.client5.http.impl.auth.AuthenticationHandler;
import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
Expand All @@ -65,6 +67,8 @@
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.http2.HttpVersionPolicy;
import org.apache.hc.core5.http2.ssl.H2TlsSupport;
import org.apache.hc.core5.util.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -139,7 +143,6 @@ public ClassicHttpResponse execute(
step = this.routeDirector.nextStep(route, fact);

switch (step) {

case HttpRouteDirector.CONNECT_TARGET:
execRuntime.connectEndpoint(context);
tracker.connectTarget(route.isSecure());
Expand All @@ -162,11 +165,8 @@ public ClassicHttpResponse execute(
}
break;

case HttpRouteDirector.TUNNEL_PROXY: {
// Proxy chains are not supported by HttpClient.
// Fail fast instead of attempting an untested tunnel to an intermediate proxy.
case HttpRouteDirector.TUNNEL_PROXY:
throw new HttpException("Proxy chains are not supported.");
}

case HttpRouteDirector.LAYER_PROTOCOL:
execRuntime.upgradeTls(context);
Expand Down Expand Up @@ -197,14 +197,6 @@ public ClassicHttpResponse execute(
}
}

/**
* 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 ClassicHttpResponse createTunnelToTarget(
final String exchangeId,
final HttpRoute route,
Expand All @@ -228,6 +220,16 @@ private ClassicHttpResponse createTunnelToTarget(
final ClassicHttpRequest connect = new BasicClassicHttpRequest(Method.CONNECT, target, authority);
connect.setVersion(HttpVersion.HTTP_1_1);

// RFC 7639: advertise the same ALPN protocols the tunnel's TLS layer will offer, derived
// from the target's HttpVersionPolicy published on the context by the connection manager,
// so the header cannot diverge from the protocol actually negotiated inside the tunnel.
if (route.isSecure()) {
final HttpVersionPolicy configured = context.getHttpVersionPolicy();
final HttpVersionPolicy versionPolicy = configured != null ? configured : HttpVersionPolicy.NEGOTIATE;
connect.setHeader(AlpnHeaderSupport.formatValue(
Arrays.asList(H2TlsSupport.selectApplicationProtocols(versionPolicy))));
}

this.proxyHttpProcessor.process(connect, null, context);

while (response == null) {
Expand Down Expand Up @@ -262,12 +264,10 @@ private ClassicHttpResponse createTunnelToTarget(
authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
}
if (updated) {
// Retry request
if (this.reuseStrategy.keepAlive(connect, response, context)) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} connection kept alive", exchangeId);
}
// Consume response content
final HttpEntity entity = response.getEntity();
EntityUtils.consume(entity);
} else {
Expand Down Expand Up @@ -295,26 +295,11 @@ private ClassicHttpResponse createTunnelToTarget(
return null;
}

/**
* 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.");
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.apache.hc.client5.http.io.HttpClientConnectionOperator;
import org.apache.hc.client5.http.io.LeaseRequest;
import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.core5.annotation.Contract;
Expand Down Expand Up @@ -552,6 +553,8 @@ public void connect(final ConnectionEndpoint endpoint, final TimeValue timeout,
final HttpHost firstHop = route.getProxyHost() != null ? route.getProxyHost() : route.getTargetHost();
final SocketConfig socketConfig = resolveSocketConfig(route);
final ConnectionConfig connectionConfig = resolveConnectionConfig(route);
final TlsConfig tlsConfig = resolveTlsConfig(route.getTargetHost());
HttpClientContext.castOrCreate(context).setHttpVersionPolicy(tlsConfig.getHttpVersionPolicy());
final Timeout connectTimeout = timeout != null ? Timeout.of(timeout.getDuration(), timeout.getTimeUnit()) : connectionConfig.getConnectTimeout();
if (LOG.isDebugEnabled()) {
LOG.debug("{} connecting endpoint to {} ({})", ConnPoolSupport.getId(endpoint), firstHop, connectTimeout);
Expand All @@ -565,7 +568,7 @@ public void connect(final ConnectionEndpoint endpoint, final TimeValue timeout,
route.getLocalSocketAddress(),
connectTimeout,
socketConfig,
route.isTunnelled() ? null : resolveTlsConfig(route.getTargetHost()),
route.isTunnelled() ? null : tlsConfig,
context);
if (LOG.isDebugEnabled()) {
LOG.debug("{} connected {}", ConnPoolSupport.getId(endpoint), ConnPoolSupport.getId(conn));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.apache.hc.client5.http.nio.AsyncClientConnectionOperator;
import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint;
import org.apache.hc.client5.http.nio.ManagedAsyncClientConnection;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
Expand Down Expand Up @@ -504,13 +505,15 @@ public Future<AsyncConnectionEndpoint> connect(
if (LOG.isDebugEnabled()) {
LOG.debug("{} connecting endpoint to {} ({})", ConnPoolSupport.getId(endpoint), firstHop, connectTimeout);
}
final TlsConfig targetTlsConfig = resolveTlsConfig(route.getTargetHost());
HttpClientContext.castOrCreate(context).setHttpVersionPolicy(targetTlsConfig.getHttpVersionPolicy());
final Object connectAttachment;
if (route.isTunnelled()) {
connectAttachment = null;
} else if (attachment instanceof TlsConfig) {
connectAttachment = attachment;
} else {
connectAttachment = resolveTlsConfig(route.getTargetHost());
connectAttachment = targetTlsConfig;
}

final Future<ManagedAsyncClientConnection> connectFuture = connectionOperator.connect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.apache.hc.core5.http.config.Lookup;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpCoreContext;
import org.apache.hc.core5.http2.HttpVersionPolicy;

/**
* Client execution {@link HttpContext}. This class can be re-used for
Expand Down Expand Up @@ -201,6 +202,7 @@ public static HttpClientContext create() {
private AuthCache authCache;
private Object userToken;
private RequestConfig requestConfig;
private HttpVersionPolicy versionPolicy;

/**
* Stores the {@code nextnonce} value provided by the server in an HTTP response.
Expand Down Expand Up @@ -488,6 +490,27 @@ public void setNextNonce(final String nextNonce) {
this.nextNonce = nextNonce;
}

/**
* Represents the {@link HttpVersionPolicy} resolved for the target of the current route. The
* connection manager populates this attribute before the connection is established so that
* protocol interceptors can act on the effective TLS policy, for instance to advertise the
* matching ALPN protocol identifiers on a {@code CONNECT} request.
*
* @since 5.7
*/
@Internal
public HttpVersionPolicy getHttpVersionPolicy() {
return versionPolicy;
}

/**
* @since 5.7
*/
@Internal
public void setHttpVersionPolicy(final HttpVersionPolicy versionPolicy) {
this.versionPolicy = versionPolicy;
}

/**
* Internal adaptor class that delegates all its method calls to a plain {@link HttpContext}.
* To be removed in the future.
Expand Down
Loading
Loading