Skip to content

Commit 76715a4

Browse files
committed
Add https proxy support
Closes: #6207
1 parent 86203ec commit 76715a4

19 files changed

Lines changed: 605 additions & 49 deletions

vertx-core/src/main/asciidoc/http.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2407,7 +2407,8 @@ used with binary frames that are no split over multiple frames.
24072407

24082408
=== Using a proxy for HTTP/HTTPS connections
24092409

2410-
The {@link io.vertx.core.http.HttpClient} supports accessing HTTP/HTTPS URLs via an HTTP proxy (e.g. Squid), a _SOCKS4a_, or a _SOCKS5_ proxy.
2410+
The {@link io.vertx.core.http.HttpClient} supports accessing HTTP/HTTPS URLs via an HTTP proxy (e.g. Squid), an HTTPS proxy
2411+
(an HTTP proxy reached over SSL/TLS), a _SOCKS4a_, or a _SOCKS5_ proxy.
24112412
The `CONNECT` protocol uses HTTP/1.x but can connect to HTTP/1.x and HTTP/2 servers.
24122413

24132414
Connecting to `h2c` (unencrypted HTTP/2 servers) is likely not supported by http proxies since they will support HTTP/1.1 only.
@@ -2426,6 +2427,11 @@ When the client connects to an HTTP URL, it connects to the proxy server and pro
24262427

24272428
When the client connects to an HTTPS URL, it asks the proxy to create a tunnel to the remote host with the `CONNECT` method.
24282429

2430+
To connect to the proxy itself over SSL/TLS, set the proxy type to {@link io.vertx.core.net.ProxyType#HTTPS} and configure
2431+
the proxy SSL options with {@link io.vertx.core.net.ProxyOptions#setSslOptions(io.vertx.core.net.ClientSSLOptions)}. The
2432+
proxy SSL options are independent of the target server's SSL options, and hostname verification of the proxy certificate is
2433+
enabled by default.
2434+
24292435
For a SOCKS5 proxy:
24302436

24312437
[source,$lang]

vertx-core/src/main/asciidoc/tcp.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,11 +506,19 @@ the update.
506506

507507
=== Using a proxy for client connections
508508

509-
The {@link io.vertx.core.net.NetClient} supports either an HTTP/1.x _CONNECT_, _SOCKS4a_ or _SOCKS5_ proxy.
509+
The {@link io.vertx.core.net.NetClient} supports either an HTTP/1.x _CONNECT_, an HTTPS (HTTP _CONNECT_ over SSL/TLS),
510+
_SOCKS4a_ or _SOCKS5_ proxy.
510511

511512
The proxy can be configured in the {@link io.vertx.core.net.TcpClientConfig} by setting a
512513
{@link io.vertx.core.net.ProxyOptions} object containing proxy type, hostname, port and optionally username and password.
513514

515+
To reach the proxy itself over SSL/TLS, use the {@link io.vertx.core.net.ProxyType#HTTPS} proxy type and configure the
516+
SSL options for the proxy connection with {@link io.vertx.core.net.ProxyOptions#setSslOptions(io.vertx.core.net.ClientSSLOptions)}.
517+
These options (trust store, optional client certificate, hostname verification) apply to the connection to the proxy and
518+
are independent of the options used for the target server. Hostname verification of the proxy certificate is enabled by
519+
default. Note that {@link io.vertx.core.net.ProxyType#HTTPS} denotes a proxy that is itself reached over SSL/TLS, which is
520+
distinct from the `https_proxy` environment-variable convention of a proxy used for `https` traffic.
521+
514522
Here's an example:
515523

516524
[source,$lang]

vertx-core/src/main/generated/io/vertx/core/net/ProxyOptionsConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, ProxyOp
4747
obj.setConnectTimeout(java.time.Duration.of(((Number)member.getValue()).longValue(), java.time.temporal.ChronoUnit.MILLIS));
4848
}
4949
break;
50+
case "sslOptions":
51+
if (member.getValue() instanceof JsonObject) {
52+
obj.setSslOptions(new io.vertx.core.net.ClientSSLOptions((io.vertx.core.json.JsonObject)member.getValue()));
53+
}
54+
break;
5055
}
5156
}
5257
}
@@ -75,5 +80,8 @@ static void toJson(ProxyOptions obj, java.util.Map<String, Object> json) {
7580
if (obj.getConnectTimeout() != null) {
7681
json.put("connectTimeout", obj.getConnectTimeout().toMillis());
7782
}
83+
if (obj.getSslOptions() != null) {
84+
json.put("sslOptions", obj.getSslOptions().toJson());
85+
}
7886
}
7987
}

vertx-core/src/main/java/io/vertx/core/http/impl/EndpointKey.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,15 @@ private static boolean equals(ProxyOptions options1, ProxyOptions options2) {
7979
Objects.equals(options1.getType(), options2.getType()) &&
8080
Objects.equals(options1.getUsername(), options2.getUsername()) &&
8181
Objects.equals(options1.getPassword(), options2.getPassword()) &&
82-
Objects.equals(options1.getProxyAuthorization(), options2.getProxyAuthorization());
82+
Objects.equals(options1.getProxyAuthorization(), options2.getProxyAuthorization()) &&
83+
Objects.equals(options1.getSslOptions(), options2.getSslOptions());
8384
}
8485
return false;
8586
}
8687

8788
private static int hashCode(ProxyOptions options) {
8889
return Objects.hash(options.getHost(), options.getPort(), options.getType(), options.getUsername(),
89-
options.getPassword(), options.getProxyAuthorization());
90+
options.getPassword(), options.getProxyAuthorization(), options.getSslOptions());
9091
}
9192

9293
@Override

vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,13 @@ private Function<EndpointKey, SharedHttpClientConnectionGroup> httpEndpointProvi
192192
PoolMetrics poolMetrics = HttpClientImpl.this.httpMetrics != null ? vertx.metrics().createPoolMetrics("http", key.authority.toString(), maxPoolSize) : null;
193193
ProxyOptions proxyOptions = key.proxyOptions;
194194
ClientSSLOptions sslOptions = key.sslOptions;
195-
if (proxyOptions != null && !key.ssl && proxyOptions.getType() == ProxyType.HTTP) {
195+
boolean forwardProxy = false;
196+
if (proxyOptions != null && !key.ssl && (proxyOptions.getType() == ProxyType.HTTP || proxyOptions.getType() == ProxyType.HTTPS)) {
196197
SocketAddress server = SocketAddress.inetSocketAddress(proxyOptions.getPort(), proxyOptions.getHost());
197198
key = new EndpointKey(key.ssl, key.protocol, sslOptions, proxyOptions, server, key.authority);
198-
proxyOptions = null;
199+
// Forward mode: the single socket connects to the proxy as the server while the logical
200+
// origin stays plain HTTP (key.ssl == false).
201+
forwardProxy = true;
199202
}
200203
HttpVersion protocol = key.protocol;
201204
List<HttpVersion> protocols;
@@ -204,7 +207,7 @@ private Function<EndpointKey, SharedHttpClientConnectionGroup> httpEndpointProvi
204207
} else {
205208
protocols = List.of(protocol);
206209
}
207-
HttpConnectParams params = new HttpConnectParams(protocols, sslOptions, proxyOptions, key.ssl);
210+
HttpConnectParams params = new HttpConnectParams(protocols, sslOptions, proxyOptions, key.ssl, forwardProxy);
208211
Function<SharedHttpClientConnectionGroup, SharedHttpClientConnectionGroup.Pool> p = group -> {
209212
int queueMaxSize = poolOptions.getMaxWaitQueueSize();
210213
if (transport instanceof TcpHttpClientTransport) {

vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void init(RequestOptions options) {
8989
Boolean followRedirects = options.getFollowRedirects();
9090
long idleTimeout = options.getIdleTimeout();
9191
ProxyOptions proxyOptions = options.getProxyOptions();
92-
if (proxyOptions != null && !useSSL && proxyOptions.getType() == ProxyType.HTTP) {
92+
if (proxyOptions != null && !useSSL && (proxyOptions.getType() == ProxyType.HTTP || proxyOptions.getType() == ProxyType.HTTPS)) {
9393
HostAndPort authority = conn.authority();
9494
if (!ABS_URI_START_PATTERN.matcher(requestURI).find()) {
9595
int defaultPort = 80;

vertx-core/src/main/java/io/vertx/core/http/impl/HttpConnectParams.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,27 @@ public HttpConnectParams(List<HttpVersion> protocols,
1212
ClientSSLOptions sslOptions,
1313
ProxyOptions proxyOptions,
1414
boolean ssl) {
15+
this(protocols, sslOptions, proxyOptions, ssl, false);
16+
}
17+
18+
public HttpConnectParams(List<HttpVersion> protocols,
19+
ClientSSLOptions sslOptions,
20+
ProxyOptions proxyOptions,
21+
boolean ssl,
22+
boolean forwardProxy) {
1523
this.protocols = protocols;
1624
this.sslOptions = sslOptions;
1725
this.proxyOptions = proxyOptions;
1826
this.ssl = ssl;
27+
this.forwardProxy = forwardProxy;
1928
}
2029

2130
public final List<HttpVersion> protocols;
2231
public final ClientSSLOptions sslOptions;
2332
public final ProxyOptions proxyOptions;
33+
2434
public final boolean ssl;
2535

36+
public final boolean forwardProxy;
37+
2638
}

vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ClientConnection.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ public HostAndPort authority() {
149149
return authority;
150150
}
151151

152+
@Override
153+
public boolean isSsl() {
154+
return ssl;
155+
}
156+
152157
@Override
153158
public io.vertx.core.http.impl.HttpClientConnection evictionHandler(Handler<Void> handler) {
154159
evictionHandler = handler;

vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,26 +143,57 @@ private Http2ClientChannelInitializer http2Initializer() {
143143
}
144144
}
145145

146+
/**
147+
* Whether this connection's transport TLS terminates at an HTTPS proxy (forward mode through an
148+
* HTTPS proxy) — the only forward case that uses leg-1 TLS to the proxy.
149+
*/
150+
private static boolean proxyHttps(HttpConnectParams params) {
151+
return params.forwardProxy && params.proxyOptions != null && params.proxyOptions.getType() == ProxyType.HTTPS;
152+
}
153+
154+
/** Whether the socket is encrypted: origin TLS, or leg-1 TLS to an HTTPS proxy (forward mode). */
155+
private static boolean transportSsl(HttpConnectParams params) {
156+
return params.ssl || proxyHttps(params);
157+
}
158+
146159
private void connect(ContextInternal context, HttpConnectParams params, HostAndPort authority, SocketAddress server, Promise<NetSocket> promise) {
160+
boolean forward = params.forwardProxy;
161+
boolean proxyHttps = proxyHttps(params);
162+
boolean transportSsl = transportSsl(params);
163+
147164
ConnectOptions connectOptions = new ConnectOptions();
148165
connectOptions.setRemoteAddress(server);
149-
if (authority != null) {
166+
if (proxyHttps) {
167+
// Forward mode through an HTTPS proxy: the socket's TLS terminates at the proxy, so the peer /
168+
// SNI is the proxy (the server), not the origin authority.
169+
connectOptions.setHost(server.host());
170+
connectOptions.setPort(server.port());
171+
connectOptions.setSniServerName(server.host());
172+
} else if (authority != null) {
150173
connectOptions.setHost(authority.host());
151174
connectOptions.setPort(authority.port());
152-
if (params.ssl && forceSni) {
175+
if (transportSsl && forceSni) {
153176
connectOptions.setSniServerName(authority.host());
154177
}
155178
}
156-
connectOptions.setSsl(params.ssl);
157-
if (params.ssl) {
179+
connectOptions.setSsl(transportSsl);
180+
if (transportSsl) {
181+
// The TLS being configured terminates at the proxy (forward + HTTPS proxy) or at the origin
182+
// (direct, or the leg-2 of a CONNECT tunnel); pick the matching options.
183+
ClientSSLOptions transportSslOptions = proxyHttps ? params.proxyOptions.getSslOptions() : params.sslOptions;
158184
ClientSSLOptions copy;
159-
if (params.sslOptions != null) {
160-
copy = params.sslOptions.copy();
185+
if (transportSslOptions != null) {
186+
copy = transportSslOptions.copy();
161187
} else {
162188
// We might end up using javax.net.ssl.trustStore
163189
copy = new ClientSSLOptions().setHostnameVerificationAlgorithm("HTTPS");
164190
}
165-
boolean useAlpn = params.protocols.contains(HttpVersion.HTTP_2);
191+
if (proxyHttps && copy.getHostnameVerificationAlgorithm() == null) {
192+
// Verify the proxy certificate against the proxy host by default (secure default).
193+
copy.setHostnameVerificationAlgorithm("HTTPS");
194+
}
195+
// No ALPN on a leg-1 TLS connection to the proxy: ALPN belongs to the origin (leg 2).
196+
boolean useAlpn = !proxyHttps && params.protocols.contains(HttpVersion.HTTP_2);
166197
copy.setUseAlpn(useAlpn);
167198
if (useAlpn) {
168199
List<String> list = params.protocols
@@ -174,7 +205,9 @@ private void connect(ContextInternal context, HttpConnectParams params, HostAndP
174205
}
175206
connectOptions.setSslOptions(copy);
176207
}
177-
connectOptions.setProxyOptions(params.proxyOptions);
208+
// Only CONNECT/SOCKS-tunnel through the proxy when NOT forwarding; in forward mode the socket
209+
// connects straight to the proxy (the server) and issues absolute-URI requests.
210+
connectOptions.setProxyOptions(forward ? null : params.proxyOptions);
178211
client.connectInternal(connectOptions, promise, context);
179212
}
180213

@@ -197,7 +230,11 @@ public Future<HttpClientConnection> wrap(ContextInternal context, HttpConnectPar
197230

198231
//
199232
Channel ch = so.channelHandlerContext().channel();
200-
if (params.ssl) {
233+
// Transport TLS may be on because the origin is HTTPS or because leg-1 TLS to an HTTPS proxy is
234+
// in use (forward mode). The connection's ssl flag, however, must reflect the logical origin
235+
// (params.ssl), not the transport.
236+
boolean transportSsl = transportSsl(params);
237+
if (transportSsl) {
201238
String protocol = so.applicationLayerProtocol();
202239
if (protocol == null) {
203240
protocol = "";
@@ -218,7 +255,7 @@ public Future<HttpClientConnection> wrap(ContextInternal context, HttpConnectPar
218255
if (http1Config != null) {
219256
applyHttp1xConnectionOptions(ch.pipeline());
220257
HttpVersion version = "http/1.0".equals(protocol) ? HttpVersion.HTTP_1_0 : HttpVersion.HTTP_1_1;
221-
http1xConnected(version, server, authority, true, context, transportMetrics, metric, ch, clientMetrics, promise);
258+
http1xConnected(version, server, authority, params.ssl, context, transportMetrics, metric, ch, clientMetrics, promise);
222259
} else {
223260
so.close();
224261
promise.tryFail(new IllegalStateException("HTTP/1.1 not supported"));

vertx-core/src/main/java/io/vertx/core/net/ProxyOptions.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class ProxyOptions {
5656
private String proxyAuthorization;
5757
private ProxyType type;
5858
private Duration connectTimeout;
59+
private ClientSSLOptions sslOptions;
5960

6061
/**
6162
* Default constructor.
@@ -80,6 +81,7 @@ public ProxyOptions(ProxyOptions other) {
8081
proxyAuthorization = other.getProxyAuthorization();
8182
type = other.getType();
8283
connectTimeout = other.getConnectTimeout();
84+
sslOptions = other.sslOptions != null ? other.sslOptions.copy() : null;
8385
}
8486

8587
/**
@@ -260,4 +262,32 @@ public ProxyOptions setConnectTimeout(Duration connectTimeout) {
260262
this.connectTimeout = connectTimeout;
261263
return this;
262264
}
265+
266+
/**
267+
* Get the SSL options used for the connection to the proxy itself.
268+
* <p>
269+
* Only relevant when {@link #getType()} is {@link ProxyType#HTTPS}.
270+
*
271+
* @return the proxy SSL options, or {@code null}
272+
*/
273+
public ClientSSLOptions getSslOptions() {
274+
return sslOptions;
275+
}
276+
277+
/**
278+
* Set the SSL options used for the connection to the proxy itself, when the proxy is reached over
279+
* SSL/TLS ({@link ProxyType#HTTPS}).
280+
* <p>
281+
* These options are resolved independently of the origin SSL options, so the proxy presents and
282+
* is validated against its own certificate and trust store. When left unset for an
283+
* {@link ProxyType#HTTPS} proxy, the connection is still established over SSL/TLS using the default
284+
* trust source, with hostname verification enabled against the proxy host.
285+
*
286+
* @param sslOptions the proxy SSL options
287+
* @return a reference to this, so the API can be used fluently
288+
*/
289+
public ProxyOptions setSslOptions(ClientSSLOptions sslOptions) {
290+
this.sslOptions = sslOptions;
291+
return this;
292+
}
263293
}

0 commit comments

Comments
 (0)