Skip to content
Merged
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
Expand Up @@ -34,6 +34,6 @@
*/
public enum ChainElement {

REDIRECT, COMPRESS, BACK_OFF, RETRY, CACHING, PROTOCOL, CONNECT, MAIN_TRANSPORT
REDIRECT, COMPRESS, BACK_OFF, RETRY, CACHING, PROTOCOL, CONNECT, MAIN_TRANSPORT, TLS_REQUIRED

}
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ private ExecInterceptorEntry(

private boolean priorityHeaderDisabled;

private boolean tlsRequired;


/**
* Maps {@code Content-Encoding} tokens to decoder factories in insertion order.
Expand Down Expand Up @@ -901,6 +903,20 @@ public HttpAsyncClientBuilder disableContentCompression() {
return this;
}

/**
* When enabled, the client refuses to establish cleartext connections.
* This disables plain {@code http://}, {@code h2c}, and RFC 2817 TLS upgrade paths.
*
* @param tlsRequired whether to enforce TLS-required routes.
* @return this instance.
*
* @since 5.7
*/
public final HttpAsyncClientBuilder setTlsRequired(final boolean tlsRequired) {
this.tlsRequired = tlsRequired;
return this;
}

/**
* Sets a hard cap on the number of requests allowed to be queued/in-flight
* within the internal async execution pipeline. When the limit is reached,
Expand Down Expand Up @@ -1103,6 +1119,7 @@ public CloseableHttpAsyncClient build() {
authCachingDisabled),
ChainElement.PROTOCOL.name());


// Add request retry executor, if not disabled
if (!automaticRetriesDisabled) {
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
Expand All @@ -1126,6 +1143,10 @@ public CloseableHttpAsyncClient build() {
}
}

if (this.tlsRequired) {
execChainDefinition.addFirst(new TlsRequiredAsyncExec(), ChainElement.TLS_REQUIRED.name());
}


HttpRoutePlanner routePlannerCopy = this.routePlanner;
if (routePlannerCopy == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* ====================================================================
* 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.async;

import java.io.IOException;

import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.UnsupportedSchemeException;
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.core5.annotation.Internal;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;

/**
* Internal async exec interceptor that enforces the "TLS required" client policy.
*
*/
@Internal
final class TlsRequiredAsyncExec implements AsyncExecChainHandler {

@Override
public void execute(
final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {

final HttpRoute route = scope.route;
if (route != null && !route.isSecure()) {
asyncExecCallback.failed(new UnsupportedSchemeException("Cleartext HTTP is disabled (TLS required)"));
}
chain.proceed(request, entityProducer, scope, asyncExecCallback);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ private ExecInterceptorEntry(
private boolean defaultUserAgentDisabled;
private ProxySelector proxySelector;

private boolean tlsRequired;

private List<Closeable> closeables;

public static HttpClientBuilder create() {
Expand Down Expand Up @@ -807,6 +809,20 @@ public final HttpClientBuilder setProxySelector(final ProxySelector proxySelecto
return this;
}

/**
* When enabled, the client refuses to establish cleartext connections.
* This disables plain {@code http://}, {@code h2c}, and RFC 2817 TLS upgrade paths.
*
* @param tlsRequired whether to enforce TLS-required routes.
* @return this instance.
*
* @since 5.7
*/
public final HttpClientBuilder setTlsRequired(final boolean tlsRequired) {
this.tlsRequired = tlsRequired;
return this;
}

/**
* Request exec chain customization and extension.
* <p>
Expand Down Expand Up @@ -999,6 +1015,10 @@ public CloseableHttpClient build() {
}
}

if (this.tlsRequired) {
execChainDefinition.addFirst(new TlsRequiredExec(), ChainElement.TLS_REQUIRED.name());
}

// Add request retry executor, if not disabled
if (!automaticRetriesDisabled) {
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* ====================================================================
* 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.classic;

import java.io.IOException;

import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.UnsupportedSchemeException;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpException;

/**
* Internal exec interceptor that enforces the "TLS required" client policy.
*
*/
@Internal
final class TlsRequiredExec implements ExecChainHandler {

@Override
public ClassicHttpResponse execute(
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain) throws IOException, HttpException {

final HttpRoute route = scope.route;
if (route != null && !route.isSecure()) {
throw new UnsupportedSchemeException("Cleartext HTTP is disabled (TLS required)");
}
return chain.proceed(request, scope);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* ====================================================================
* 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.examples;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.hc.client5.http.UnsupportedSchemeException;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;

/**
* Demonstrates the "TLS-required connections" mode for the async client.
*
* <p>
* When {@code TlsRequired(true)} is enabled, the async client rejects execution when the
* computed {@code HttpRoute} is not secure. This prevents accidental cleartext connections
* such as {@code http://...} and disables cleartext upgrade mechanisms that start without TLS.
* </p>
*
* <p>
* The example triggers a rejection using {@code http://example.com/} and validates the failure
* by unwrapping {@link ExecutionException#getCause()} and checking for
* {@link UnsupportedSchemeException}.
* </p>
*
* @since 5.7
*/
public final class TlsRequiredAsyncExample {

public static void main(final String[] args) throws Exception {
try (final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
.setTlsRequired(true)
.build()) {

client.start();

// 1) Must fail fast with UnsupportedSchemeException
final SimpleHttpRequest http = SimpleRequestBuilder.get("http://example.com/").build();
final Future<SimpleHttpResponse> httpFuture =
client.execute(http, HttpClientContext.create(), null);

try {
final SimpleHttpResponse response = httpFuture.get();
System.out.println("UNEXPECTED: http:// executed with status " + response.getCode());
} catch (final ExecutionException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof UnsupportedSchemeException) {
System.out.println("OK (expected): " + cause.getMessage());
} else {
throw ex;
}
}

// 2) Allowed (may still fail if network/DNS blocked)
final SimpleHttpRequest https = SimpleRequestBuilder.get("https://example.com/").build();
final Future<SimpleHttpResponse> httpsFuture =
client.execute(https, HttpClientContext.create(), null);

try {
final SimpleHttpResponse response = httpsFuture.get();
System.out.println("HTTPS OK: status=" + response.getCode());
} catch (final ExecutionException ex) {
System.err.println("HTTPS failed (network/env): " + ex.getCause());
}
}
}

}
Loading