diff --git a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java index 9f81c35da..370f1a1e3 100644 --- a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java @@ -262,6 +262,7 @@ public HttpConfiguration createHttpConfiguration(BasicConfiguration basicConfigu proxy, proxyAuth, socketTimeout, + socketFactory, sslSocketFactory, trustManager, headers.build() diff --git a/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java b/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java index f110eb19c..9415fe8b1 100644 --- a/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.util.Map; +import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; @@ -16,17 +17,20 @@ final class HttpConfigurationImpl implements HttpConfiguration { final Proxy proxy; final HttpAuthentication proxyAuth; final Duration socketTimeout; + final SocketFactory socketFactory; final SSLSocketFactory sslSocketFactory; final X509TrustManager trustManager; final ImmutableMap defaultHeaders; HttpConfigurationImpl(Duration connectTimeout, Proxy proxy, HttpAuthentication proxyAuth, - Duration socketTimeout, SSLSocketFactory sslSocketFactory, X509TrustManager trustManager, + Duration socketTimeout, SocketFactory socketFactory, + SSLSocketFactory sslSocketFactory, X509TrustManager trustManager, ImmutableMap defaultHeaders) { this.connectTimeout = connectTimeout; this.proxy = proxy; this.proxyAuth = proxyAuth; this.socketTimeout = socketTimeout; + this.socketFactory = socketFactory; this.sslSocketFactory = sslSocketFactory; this.trustManager = trustManager; this.defaultHeaders = defaultHeaders; @@ -51,6 +55,11 @@ public HttpAuthentication getProxyAuthentication() { public Duration getSocketTimeout() { return socketTimeout; } + + @Override + public SocketFactory getSocketFactory() { + return socketFactory; + } @Override public SSLSocketFactory getSslSocketFactory() { diff --git a/src/main/java/com/launchdarkly/sdk/server/Util.java b/src/main/java/com/launchdarkly/sdk/server/Util.java index 3ffd243b6..1827080e5 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Util.java +++ b/src/main/java/com/launchdarkly/sdk/server/Util.java @@ -43,6 +43,10 @@ static void configureHttpClientBuilder(HttpConfiguration config, OkHttpClient.Bu .readTimeout(config.getSocketTimeout()) .writeTimeout(config.getSocketTimeout()) .retryOnConnectionFailure(false); // we will implement our own retry logic + + if (config.getSocketFactory() != null) { + builder.socketFactory(config.getSocketFactory()); + } if (config.getSslSocketFactory() != null) { builder.sslSocketFactory(config.getSslSocketFactory(), config.getTrustManager()); diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java index 7fa33d889..9a3ca86c4 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java @@ -6,6 +6,7 @@ import java.time.Duration; +import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; @@ -45,6 +46,7 @@ public abstract class HttpConfigurationBuilder implements HttpConfigurationFacto protected String proxyHost; protected int proxyPort; protected Duration socketTimeout = DEFAULT_SOCKET_TIMEOUT; + protected SocketFactory socketFactory; protected SSLSocketFactory sslSocketFactory; protected X509TrustManager trustManager; protected String wrapperName; @@ -105,6 +107,19 @@ public HttpConfigurationBuilder socketTimeout(Duration socketTimeout) { return this; } + /** + * Specifies a custom socket configuration for HTTP connections to LaunchDarkly. + *

+ * This uses the standard Java interfaces for configuring socket connections. + * + * @param socketFactory the socket factory + * @return the builder + */ + public HttpConfigurationBuilder socketFactory(SocketFactory socketFactory) { + this.socketFactory = socketFactory; + return this; + } + /** * Specifies a custom security configuration for HTTPS connections to LaunchDarkly. *

diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java index 1d47867a8..1a2ab19b3 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java @@ -6,6 +6,7 @@ import java.time.Duration; import java.util.Map; +import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; @@ -53,6 +54,15 @@ public interface HttpConfiguration { * @return the socket timeout; must not be null */ Duration getSocketTimeout(); + + /** + * The configured socket factory for insecure connections. + * + * @return a SocketFactory or null + */ + default SocketFactory getSocketFactory() { + return null; + } /** * The configured socket factory for secure connections. diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java index b806997a4..73dcc3754 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java @@ -16,6 +16,7 @@ import java.util.concurrent.TimeUnit; import static com.launchdarkly.sdk.server.TestComponents.clientContext; +import static com.launchdarkly.sdk.server.TestUtil.makeSocketFactorySingleHost; import static com.launchdarkly.sdk.server.TestHttpUtil.httpsServerWithSelfSignedCert; import static com.launchdarkly.sdk.server.TestHttpUtil.makeStartedServer; import static com.launchdarkly.sdk.server.interfaces.EventSender.EventDataKind.ANALYTICS; @@ -32,6 +33,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -300,6 +302,25 @@ public void httpClientCanUseCustomTlsConfig() throws Exception { } } + @Test + public void httpClientCanUseCustomSocketFactory() throws Exception { + try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) { + HttpUrl serverUrl = server.url("/"); + LDConfig config = new LDConfig.Builder() + .http(Components.httpConfiguration().socketFactory(makeSocketFactorySingleHost(serverUrl.host(), serverUrl.port()))) + .build(); + + try (EventSender es = makeEventSender(config)) { + EventSender.Result result = es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, URI.create("http://localhost")); + + assertTrue(result.isSuccess()); + assertFalse(result.isMustShutDown()); + } + + assertEquals(1, server.getRequestCount()); + } + } + @Test public void baseUriDoesNotNeedTrailingSlash() throws Exception { try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) { diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java index 1158ca4eb..520018d3e 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java @@ -8,9 +8,11 @@ import java.net.URI; import java.util.Map; +import javax.net.SocketFactory; import javax.net.ssl.SSLHandshakeException; import static com.launchdarkly.sdk.server.TestComponents.clientContext; +import static com.launchdarkly.sdk.server.TestUtil.makeSocketFactorySingleHost; import static com.launchdarkly.sdk.server.TestHttpUtil.httpsServerWithSelfSignedCert; import static com.launchdarkly.sdk.server.TestHttpUtil.jsonResponse; import static com.launchdarkly.sdk.server.TestHttpUtil.makeStartedServer; @@ -169,6 +171,24 @@ public void httpClientCanUseCustomTlsConfig() throws Exception { } } + @Test + public void httpClientCanUseCustomSocketFactory() throws Exception { + URI localhostUri = URI.create("http://localhost"); + try (MockWebServer server = makeStartedServer(jsonResponse(allDataJson))) { + HttpUrl serverUrl = server.url("/"); + LDConfig config = new LDConfig.Builder() + .http(Components.httpConfiguration().socketFactory(makeSocketFactorySingleHost(serverUrl.host(), serverUrl.port()))) + .build(); + + try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(makeHttpConfig(config), localhostUri)) { + FeatureRequestor.AllData data = r.getAllData(false); + verifyExpectedData(data); + + assertEquals(1, server.getRequestCount()); + } + } + } + @Test public void httpClientCanUseProxyConfig() throws Exception { URI fakeBaseUri = URI.create("http://not-a-real-host"); diff --git a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java index c258501c2..c6e8ede71 100644 --- a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java @@ -48,6 +48,7 @@ import static com.launchdarkly.sdk.server.TestComponents.dataStoreThatThrowsException; import static com.launchdarkly.sdk.server.TestHttpUtil.eventStreamResponse; import static com.launchdarkly.sdk.server.TestHttpUtil.makeStartedServer; +import static com.launchdarkly.sdk.server.TestUtil.makeSocketFactorySingleHost; import static com.launchdarkly.sdk.server.TestUtil.requireDataSourceStatus; import static com.launchdarkly.sdk.server.TestUtil.shouldNotTimeOut; import static com.launchdarkly.sdk.server.TestUtil.shouldTimeOut; @@ -669,6 +670,27 @@ public void httpClientCanUseCustomTlsConfig() throws Exception { } } } + + @Test + public void httpClientCanUseCustomSocketFactory() throws Exception { + final ConnectionErrorSink errorSink = new ConnectionErrorSink(); + URI localhostUri = URI.create("http://localhost"); + try (MockWebServer server = makeStartedServer(eventStreamResponse(STREAM_RESPONSE_WITH_EMPTY_DATA))) { + HttpUrl serverUrl = server.url("/"); + LDConfig config = new LDConfig.Builder() + .http(Components.httpConfiguration().socketFactory(makeSocketFactorySingleHost(serverUrl.host(), serverUrl.port()))) + .build(); + + try (StreamProcessor sp = createStreamProcessorWithRealHttp(config, localhostUri)) { + sp.connectionErrorHandler = errorSink; + Future ready = sp.start(); + ready.get(); + + assertNull(errorSink.errors.peek()); + assertEquals(1, server.getRequestCount()); + } + } + } @Test public void httpClientCanUseProxyConfig() throws Exception { diff --git a/src/test/java/com/launchdarkly/sdk/server/TestUtil.java b/src/test/java/com/launchdarkly/sdk/server/TestUtil.java index 4e1b3cf40..9c647926a 100644 --- a/src/test/java/com/launchdarkly/sdk/server/TestUtil.java +++ b/src/test/java/com/launchdarkly/sdk/server/TestUtil.java @@ -17,6 +17,10 @@ import org.hamcrest.TypeSafeDiagnosingMatcher; import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; import java.time.Duration; import java.time.Instant; import java.util.HashSet; @@ -29,6 +33,8 @@ import java.util.concurrent.TimeoutException; import java.util.function.Supplier; +import javax.net.SocketFactory; + import static com.launchdarkly.sdk.server.DataModel.FEATURES; import static com.launchdarkly.sdk.server.DataModel.SEGMENTS; import static org.hamcrest.MatcherAssert.assertThat; @@ -255,4 +261,65 @@ protected boolean matchesSafely(LDValue item, Description mismatchDescription) { } }; } + + // returns a socket factory that creates sockets that only connect to host and port + static SocketFactorySingleHost makeSocketFactorySingleHost(String host, int port) { + return new SocketFactorySingleHost(host, port); + } + + private static final class SocketSingleHost extends Socket { + private final String host; + private final int port; + + SocketSingleHost(String host, int port) { + this.host = host; + this.port = port; + } + + @Override public void connect(SocketAddress endpoint) throws IOException { + super.connect(new InetSocketAddress(this.host, this.port), 0); + } + + @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { + super.connect(new InetSocketAddress(this.host, this.port), timeout); + } + } + + public static final class SocketFactorySingleHost extends SocketFactory { + private final String host; + private final int port; + + public SocketFactorySingleHost(String host, int port) { + this.host = host; + this.port = port; + } + + @Override public Socket createSocket() throws IOException { + return new SocketSingleHost(this.host, this.port); + } + + @Override public Socket createSocket(String host, int port) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + + @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + + @Override public Socket createSocket(InetAddress host, int port) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + + @Override public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(this.host, this.port)); + return socket; + } + } } diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java index 73253d4eb..6b2b6bea6 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java @@ -17,6 +17,7 @@ import java.security.cert.X509Certificate; import java.time.Duration; +import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; @@ -46,6 +47,7 @@ public void testDefaults() { assertNull(hc.getProxy()); assertNull(hc.getProxyAuthentication()); assertEquals(DEFAULT_SOCKET_TIMEOUT, hc.getSocketTimeout()); + assertNull(hc.getSocketFactory()); assertNull(hc.getSslSocketFactory()); assertNull(hc.getTrustManager()); assertEquals(buildBasicHeaders().build(), ImmutableMap.copyOf(hc.getDefaultHeaders())); @@ -99,6 +101,15 @@ public void testSocketTimeout() { assertEquals(DEFAULT_SOCKET_TIMEOUT, hc2.getSocketTimeout()); } + @Test + public void testSocketFactory() { + SocketFactory sf = new StubSocketFactory(); + HttpConfiguration hc = Components.httpConfiguration() + .socketFactory(sf) + .createHttpConfiguration(BASIC_CONFIG); + assertSame(sf, hc.getSocketFactory()); + } + @Test public void testSslOptions() { SSLSocketFactory sf = new StubSSLSocketFactory(); @@ -125,6 +136,30 @@ public void testWrapperWithVersion() { .createHttpConfiguration(BASIC_CONFIG); assertEquals("Scala/0.1.0", ImmutableMap.copyOf(hc.getDefaultHeaders()).get("X-LaunchDarkly-Wrapper")); } + + public static class StubSocketFactory extends SocketFactory { + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return null; + } + + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return null; + } + + public Socket createSocket(InetAddress host, int port) throws IOException { + return null; + } + + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return null; + } + + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return null; + } + } public static class StubSSLSocketFactory extends SSLSocketFactory { public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)