Skip to content

Commit 68435f2

Browse files
HTTPCLIENT-2397 - TLS-Required mode: add setTlsOnly(boolean) to classic and async builders to reject cleartext routes. (#777)
Fail fast with UnsupportedSchemeException when the computed route is not secure.
1 parent 1c96bde commit 68435f2

File tree

7 files changed

+352
-1
lines changed

7 files changed

+352
-1
lines changed

httpclient5/src/main/java/org/apache/hc/client5/http/impl/ChainElement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
*/
3535
public enum ChainElement {
3636

37-
REDIRECT, COMPRESS, BACK_OFF, RETRY, CACHING, PROTOCOL, CONNECT, MAIN_TRANSPORT
37+
REDIRECT, COMPRESS, BACK_OFF, RETRY, CACHING, PROTOCOL, CONNECT, MAIN_TRANSPORT, TLS_REQUIRED
3838

3939
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ private ExecInterceptorEntry(
273273

274274
private boolean priorityHeaderDisabled;
275275

276+
private boolean tlsRequired;
277+
276278

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

906+
/**
907+
* When enabled, the client refuses to establish cleartext connections.
908+
* This disables plain {@code http://}, {@code h2c}, and RFC 2817 TLS upgrade paths.
909+
*
910+
* @param tlsRequired whether to enforce TLS-required routes.
911+
* @return this instance.
912+
*
913+
* @since 5.7
914+
*/
915+
public final HttpAsyncClientBuilder setTlsRequired(final boolean tlsRequired) {
916+
this.tlsRequired = tlsRequired;
917+
return this;
918+
}
919+
904920
/**
905921
* Sets a hard cap on the number of requests allowed to be queued/in-flight
906922
* within the internal async execution pipeline. When the limit is reached,
@@ -1103,6 +1119,7 @@ public CloseableHttpAsyncClient build() {
11031119
authCachingDisabled),
11041120
ChainElement.PROTOCOL.name());
11051121

1122+
11061123
// Add request retry executor, if not disabled
11071124
if (!automaticRetriesDisabled) {
11081125
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
@@ -1126,6 +1143,10 @@ public CloseableHttpAsyncClient build() {
11261143
}
11271144
}
11281145

1146+
if (this.tlsRequired) {
1147+
execChainDefinition.addFirst(new TlsRequiredAsyncExec(), ChainElement.TLS_REQUIRED.name());
1148+
}
1149+
11291150

11301151
HttpRoutePlanner routePlannerCopy = this.routePlanner;
11311152
if (routePlannerCopy == null) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl.async;
28+
29+
import java.io.IOException;
30+
31+
import org.apache.hc.client5.http.HttpRoute;
32+
import org.apache.hc.client5.http.UnsupportedSchemeException;
33+
import org.apache.hc.client5.http.async.AsyncExecCallback;
34+
import org.apache.hc.client5.http.async.AsyncExecChain;
35+
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
36+
import org.apache.hc.core5.annotation.Internal;
37+
import org.apache.hc.core5.http.HttpException;
38+
import org.apache.hc.core5.http.HttpRequest;
39+
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
40+
41+
/**
42+
* Internal async exec interceptor that enforces the "TLS required" client policy.
43+
*
44+
*/
45+
@Internal
46+
final class TlsRequiredAsyncExec implements AsyncExecChainHandler {
47+
48+
@Override
49+
public void execute(
50+
final HttpRequest request,
51+
final AsyncEntityProducer entityProducer,
52+
final AsyncExecChain.Scope scope,
53+
final AsyncExecChain chain,
54+
final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
55+
56+
final HttpRoute route = scope.route;
57+
if (route != null && !route.isSecure()) {
58+
asyncExecCallback.failed(new UnsupportedSchemeException("Cleartext HTTP is disabled (TLS required)"));
59+
}
60+
chain.proceed(request, entityProducer, scope, asyncExecCallback);
61+
}
62+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ private ExecInterceptorEntry(
237237
private boolean defaultUserAgentDisabled;
238238
private ProxySelector proxySelector;
239239

240+
private boolean tlsRequired;
241+
240242
private List<Closeable> closeables;
241243

242244
public static HttpClientBuilder create() {
@@ -807,6 +809,20 @@ public final HttpClientBuilder setProxySelector(final ProxySelector proxySelecto
807809
return this;
808810
}
809811

812+
/**
813+
* When enabled, the client refuses to establish cleartext connections.
814+
* This disables plain {@code http://}, {@code h2c}, and RFC 2817 TLS upgrade paths.
815+
*
816+
* @param tlsRequired whether to enforce TLS-required routes.
817+
* @return this instance.
818+
*
819+
* @since 5.7
820+
*/
821+
public final HttpClientBuilder setTlsRequired(final boolean tlsRequired) {
822+
this.tlsRequired = tlsRequired;
823+
return this;
824+
}
825+
810826
/**
811827
* Request exec chain customization and extension.
812828
* <p>
@@ -999,6 +1015,10 @@ public CloseableHttpClient build() {
9991015
}
10001016
}
10011017

1018+
if (this.tlsRequired) {
1019+
execChainDefinition.addFirst(new TlsRequiredExec(), ChainElement.TLS_REQUIRED.name());
1020+
}
1021+
10021022
// Add request retry executor, if not disabled
10031023
if (!automaticRetriesDisabled) {
10041024
HttpRequestRetryStrategy retryStrategyCopy = this.retryStrategy;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl.classic;
28+
29+
import java.io.IOException;
30+
31+
import org.apache.hc.client5.http.HttpRoute;
32+
import org.apache.hc.client5.http.UnsupportedSchemeException;
33+
import org.apache.hc.client5.http.classic.ExecChain;
34+
import org.apache.hc.client5.http.classic.ExecChainHandler;
35+
import org.apache.hc.core5.annotation.Internal;
36+
import org.apache.hc.core5.http.ClassicHttpRequest;
37+
import org.apache.hc.core5.http.ClassicHttpResponse;
38+
import org.apache.hc.core5.http.HttpException;
39+
40+
/**
41+
* Internal exec interceptor that enforces the "TLS required" client policy.
42+
*
43+
*/
44+
@Internal
45+
final class TlsRequiredExec implements ExecChainHandler {
46+
47+
@Override
48+
public ClassicHttpResponse execute(
49+
final ClassicHttpRequest request,
50+
final ExecChain.Scope scope,
51+
final ExecChain chain) throws IOException, HttpException {
52+
53+
final HttpRoute route = scope.route;
54+
if (route != null && !route.isSecure()) {
55+
throw new UnsupportedSchemeException("Cleartext HTTP is disabled (TLS required)");
56+
}
57+
return chain.proceed(request, scope);
58+
}
59+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.examples;
28+
29+
import java.util.concurrent.ExecutionException;
30+
import java.util.concurrent.Future;
31+
32+
import org.apache.hc.client5.http.UnsupportedSchemeException;
33+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
34+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
35+
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
36+
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
37+
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
38+
import org.apache.hc.client5.http.protocol.HttpClientContext;
39+
40+
/**
41+
* Demonstrates the "TLS-required connections" mode for the async client.
42+
*
43+
* <p>
44+
* When {@code TlsRequired(true)} is enabled, the async client rejects execution when the
45+
* computed {@code HttpRoute} is not secure. This prevents accidental cleartext connections
46+
* such as {@code http://...} and disables cleartext upgrade mechanisms that start without TLS.
47+
* </p>
48+
*
49+
* <p>
50+
* The example triggers a rejection using {@code http://example.com/} and validates the failure
51+
* by unwrapping {@link ExecutionException#getCause()} and checking for
52+
* {@link UnsupportedSchemeException}.
53+
* </p>
54+
*
55+
* @since 5.7
56+
*/
57+
public final class TlsRequiredAsyncExample {
58+
59+
public static void main(final String[] args) throws Exception {
60+
try (final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
61+
.setTlsRequired(true)
62+
.build()) {
63+
64+
client.start();
65+
66+
// 1) Must fail fast with UnsupportedSchemeException
67+
final SimpleHttpRequest http = SimpleRequestBuilder.get("http://example.com/").build();
68+
final Future<SimpleHttpResponse> httpFuture =
69+
client.execute(http, HttpClientContext.create(), null);
70+
71+
try {
72+
final SimpleHttpResponse response = httpFuture.get();
73+
System.out.println("UNEXPECTED: http:// executed with status " + response.getCode());
74+
} catch (final ExecutionException ex) {
75+
final Throwable cause = ex.getCause();
76+
if (cause instanceof UnsupportedSchemeException) {
77+
System.out.println("OK (expected): " + cause.getMessage());
78+
} else {
79+
throw ex;
80+
}
81+
}
82+
83+
// 2) Allowed (may still fail if network/DNS blocked)
84+
final SimpleHttpRequest https = SimpleRequestBuilder.get("https://example.com/").build();
85+
final Future<SimpleHttpResponse> httpsFuture =
86+
client.execute(https, HttpClientContext.create(), null);
87+
88+
try {
89+
final SimpleHttpResponse response = httpsFuture.get();
90+
System.out.println("HTTPS OK: status=" + response.getCode());
91+
} catch (final ExecutionException ex) {
92+
System.err.println("HTTPS failed (network/env): " + ex.getCause());
93+
}
94+
}
95+
}
96+
97+
}

0 commit comments

Comments
 (0)