Skip to content

Commit 645da07

Browse files
committed
Prevent blowing connections number for reoccurring SSLContentFatory instances
Signed-off-by: jansupol <jan.supol@oracle.com>
1 parent 2ee39d5 commit 645da07

File tree

2 files changed

+187
-10
lines changed

2 files changed

+187
-10
lines changed

core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
6767
import org.glassfish.jersey.client.spi.Connector;
6868
import org.glassfish.jersey.internal.util.PropertiesHelper;
69+
import org.glassfish.jersey.internal.util.collection.LRU;
6970
import org.glassfish.jersey.internal.util.collection.LazyValue;
7071
import org.glassfish.jersey.internal.util.collection.UnsafeValue;
7172
import org.glassfish.jersey.internal.util.collection.Value;
@@ -83,7 +84,7 @@ public class HttpUrlConnector implements Connector {
8384
private static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders";
8485
// Avoid multi-thread uses of HttpsURLConnection.getDefaultSSLSocketFactory() because it does not implement a
8586
// proper lazy-initialization. See https://github.com/jersey/jersey/issues/3293
86-
private static final LazyValue<SSLSocketFactory> DEFAULT_SSL_SOCKET_FACTORY =
87+
private static final Value<SSLSocketFactory> DEFAULT_SSL_SOCKET_FACTORY =
8788
Values.lazy((Value<SSLSocketFactory>) () -> HttpsURLConnection.getDefaultSSLSocketFactory());
8889
// The list of restricted headers is extracted from sun.net.www.protocol.http.HttpURLConnection
8990
private static final String[] restrictedHeaders = {
@@ -114,7 +115,12 @@ public class HttpUrlConnector implements Connector {
114115
private final boolean fixLengthStreaming;
115116
private final boolean setMethodWorkaround;
116117
private final boolean isRestrictedHeaderPropertySet;
117-
private LazyValue<SSLSocketFactory> sslSocketFactory;
118+
private Value<SSLSocketFactory> sslSocketFactory;
119+
120+
// SSLContext#getSocketFactory not idempotent
121+
// JDK KeepAliveCache keeps connections per Factory
122+
// SSLContext set per request blows that -> keep factory in LRU
123+
private final LRU<SSLContext, SSLSocketFactory> sslSocketFactoryCache = LRU.create();
118124

119125
private final ConnectorExtension<HttpURLConnection, IOException> connectorExtension
120126
= new HttpUrlExpect100ContinueConnectorExtension();
@@ -143,6 +149,13 @@ public HttpUrlConnector(
143149
this.fixLengthStreaming = fixLengthStreaming;
144150
this.setMethodWorkaround = setMethodWorkaround;
145151

152+
this.sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() {
153+
@Override
154+
public SSLSocketFactory get() {
155+
return client.getSslContext().getSocketFactory();
156+
}
157+
});
158+
146159
// check if sun.net.http.allowRestrictedHeaders system property has been set and log the result
147160
// the property is being cached in the HttpURLConnection, so this is only informative - there might
148161
// already be some connection(s), that existed before the property was set/changed.
@@ -342,16 +355,23 @@ private void secureConnection(
342355
}
343356
}
344357

345-
private void setSslContextFactory(Client client, ClientRequest request) {
358+
protected void setSslContextFactory(Client client, ClientRequest request) {
346359
final Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class);
347360

348-
sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() {
349-
@Override
350-
public SSLSocketFactory get() {
351-
final SSLContext ctx = supplier == null ? client.getSslContext() : supplier.get();
352-
return ctx.getSocketFactory();
353-
}
354-
});
361+
if (supplier != null) {
362+
sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() { // lazy for double-check locking if multiple requests
363+
@Override
364+
public SSLSocketFactory get() {
365+
SSLContext sslContext = supplier.get();
366+
SSLSocketFactory factory = sslSocketFactoryCache.getIfPresent(sslContext);
367+
if (factory == null) {
368+
factory = sslContext.getSocketFactory();
369+
sslSocketFactoryCache.put(sslContext, factory);
370+
}
371+
return factory;
372+
}
373+
});
374+
}
355375
}
356376

357377
private ClientResponse _apply(final ClientRequest request) throws IOException {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.client;
18+
19+
import org.glassfish.jersey.client.internal.HttpUrlConnector;
20+
import org.glassfish.jersey.client.spi.Connector;
21+
import org.glassfish.jersey.internal.MapPropertiesDelegate;
22+
import org.glassfish.jersey.internal.PropertiesDelegate;
23+
import org.hamcrest.MatcherAssert;
24+
import org.hamcrest.Matchers;
25+
import org.junit.jupiter.api.Test;
26+
27+
import javax.net.ssl.HttpsURLConnection;
28+
import javax.net.ssl.SSLContext;
29+
import javax.net.ssl.SSLSocketFactory;
30+
import javax.ws.rs.client.Client;
31+
import javax.ws.rs.client.ClientBuilder;
32+
import javax.ws.rs.core.Response;
33+
import java.io.IOException;
34+
import java.net.HttpURLConnection;
35+
import java.net.URISyntaxException;
36+
import java.net.URL;
37+
import java.net.URLConnection;
38+
import java.util.concurrent.atomic.AtomicReference;
39+
import java.util.function.Supplier;
40+
41+
public class SSLSocketFactoryTest {
42+
static final AtomicReference<SSLSocketFactory> factoryHolder = new AtomicReference<>();
43+
static SSLSocketFactory defaultSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
44+
45+
// @Test
46+
// Alternative test
47+
// Check KeepAliveCache#get(URL url, Object obj)
48+
public void testSingleConnection() throws InterruptedException, IOException {
49+
Client client = ClientBuilder.newClient();
50+
51+
for (int i = 0; i < 3; i++) {
52+
try (Response response = client.target("https://www.spiegel.de")
53+
.request()
54+
.get()) {
55+
56+
response.readEntity(String.class);
57+
System.out.println(String.format("response = %s", response));
58+
Thread.sleep(1000);
59+
}
60+
}
61+
62+
System.in.read();
63+
}
64+
65+
@Test
66+
public void testSslContextFactoryOnClientIsSameForConsecutiveRequests() throws IOException, URISyntaxException {
67+
int firstRequestFactory, secondRequestFactory = 0;
68+
Client client = ClientBuilder.newClient();
69+
HttpUrlConnectorProvider.ConnectionFactory connectionFactory = (url) -> (HttpURLConnection) url.openConnection();
70+
SSLSocketFactoryConnector connector = (SSLSocketFactoryConnector) new SSlSocketFactoryUrlConnectorProvider()
71+
.createHttpUrlConnector(client, connectionFactory, 4096, true, false);
72+
URL url = new URL("https://somewhere.whereever:8080");
73+
URLConnection urlConnection = url.openConnection();
74+
75+
// First Request
76+
connector.setSslContextFactory(client, new ClientRequest(url.toURI(),
77+
(ClientConfig) client.getConfiguration(), new MapPropertiesDelegate()));
78+
connector.secureConnection((JerseyClient) client, (HttpURLConnection) urlConnection);
79+
firstRequestFactory = factoryHolder.get().hashCode();
80+
81+
// reset to the default socketFactory
82+
((HttpsURLConnection) urlConnection).setSSLSocketFactory(defaultSocketFactory);
83+
84+
// Second Request
85+
connector.setSslContextFactory(client, new ClientRequest(url.toURI(),
86+
(ClientConfig) client.getConfiguration(), new MapPropertiesDelegate()));
87+
connector.secureConnection((JerseyClient) client, (HttpURLConnection) urlConnection);
88+
secondRequestFactory = factoryHolder.get().hashCode();
89+
90+
MatcherAssert.assertThat(firstRequestFactory, Matchers.equalTo(secondRequestFactory));
91+
}
92+
93+
@Test
94+
public void testSslContextFactoryOnRequestIsSameForConsecutiveRequests() throws IOException, URISyntaxException {
95+
int firstRequestFactory, secondRequestFactory = 0;
96+
Client client = ClientBuilder.newClient();
97+
SSLContext sslContext = new SslContextClientBuilder().build();
98+
HttpUrlConnectorProvider.ConnectionFactory connectionFactory = (url) -> (HttpURLConnection) url.openConnection();
99+
SSLSocketFactoryConnector connector = (SSLSocketFactoryConnector) new SSlSocketFactoryUrlConnectorProvider()
100+
.createHttpUrlConnector(client, connectionFactory, 4096, true, false);
101+
URL url = new URL("https://somewhere.whereever:8080");
102+
URLConnection urlConnection = url.openConnection();
103+
PropertiesDelegate propertiesDelegate = new MapPropertiesDelegate();
104+
propertiesDelegate.setProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, (Supplier<SSLContext>) () -> sslContext);
105+
106+
// First Request
107+
connector.setSslContextFactory(client, new ClientRequest(url.toURI(),
108+
(ClientConfig) client.getConfiguration(), propertiesDelegate));
109+
connector.secureConnection((JerseyClient) client, (HttpURLConnection) urlConnection);
110+
firstRequestFactory = factoryHolder.get().hashCode();
111+
112+
// reset to the default socketFactory
113+
((HttpsURLConnection) urlConnection).setSSLSocketFactory(defaultSocketFactory);
114+
115+
// Second Request
116+
connector.setSslContextFactory(client, new ClientRequest(url.toURI(),
117+
(ClientConfig) client.getConfiguration(), propertiesDelegate));
118+
connector.secureConnection((JerseyClient) client, (HttpURLConnection) urlConnection);
119+
secondRequestFactory = factoryHolder.get().hashCode();
120+
121+
MatcherAssert.assertThat(firstRequestFactory, Matchers.equalTo(secondRequestFactory));
122+
}
123+
124+
private static class SSLSocketFactoryConnector extends HttpUrlConnector {
125+
public SSLSocketFactoryConnector(Client client, HttpUrlConnectorProvider.ConnectionFactory connectionFactory,
126+
int chunkSize, boolean fixLengthStreaming, boolean setMethodWorkaround) {
127+
super(client, connectionFactory, chunkSize, fixLengthStreaming, setMethodWorkaround);
128+
}
129+
130+
@Override
131+
protected void secureConnection(JerseyClient client, HttpURLConnection uc) {
132+
super.secureConnection(client, uc);
133+
if (HttpsURLConnection.class.isInstance(uc)) {
134+
SSLSocketFactory factory = ((HttpsURLConnection) uc).getSSLSocketFactory();
135+
factoryHolder.set(factory);
136+
}
137+
}
138+
139+
@Override
140+
protected void setSslContextFactory(Client client, ClientRequest request) {
141+
super.setSslContextFactory(client, request);
142+
}
143+
}
144+
145+
private static class SSlSocketFactoryUrlConnectorProvider extends HttpUrlConnectorProvider {
146+
@Override
147+
protected Connector createHttpUrlConnector(Client client, ConnectionFactory connectionFactory, int chunkSize,
148+
boolean fixLengthStreaming, boolean setMethodWorkaround) {
149+
return new SSLSocketFactoryConnector(
150+
client,
151+
connectionFactory,
152+
chunkSize,
153+
fixLengthStreaming,
154+
setMethodWorkaround);
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)