Repository: Unleash/unleash-client-java
Pull request #137 (2021-05-21 13:42:20)

feat(#136): Adding support for custom proxy
Making it possible to specify a custom proxy to be used only when communicating with unleash backend

ivarconr: Thanks for the contribution. Looks decent. Will do a proper review next week. 

Patch:
diff --git a/README.md b/README.md
index 42fdae98..29802ecf 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ UnleashConfig config = UnleashConfig.builder()
             .appName("java-test")
             .instanceId("instance x")
             .unleashAPI("http://unleash.herokuapp.com/api/")
+            .customHttpHeader("Authorization", "API token")
             .build();
 
 Unleash unleash = new DefaultUnleash(config);
@@ -117,6 +118,7 @@ UnleashConfig config = new UnleashConfig.Builder()
             .appName("java-test")
             .instanceId("instance x")
             .unleashAPI("http://unleash.herokuapp.com/api/")
+            .customHttpHeader("Authorization", "API token")
             .unleashContextProvider(contextProvider)
             .build();
 
@@ -157,6 +159,7 @@ UnleashConfig unleashConfig = UnleashConfig.builder()
                 .appName("my-app")
                 .instanceId("my-instance-1")
                 .unleashAPI(unleashAPI)
+                .customHttpHeader("Authorization", "API token")
                 .customHttpHeadersProvider(provider)
                 .build();
 ```
@@ -173,6 +176,7 @@ UnleashConfig unleashConfig = UnleashConfig.builder()
     .appName("my-app")
     .instanceId("my-instance-1")
     .unleashAPI(unleashAPI)
+    .customHttpHeader("Authorization", "API token")
     .subscriber(new UnleashSubscriber() {
         @Override
         public void onReady(UnleashReady ready) {
@@ -207,6 +211,7 @@ to enable support for basic auth against a http proxy, you can simply enable the
 UnleashConfig config = UnleashConfig.builder()
     .appName("my-app")
     .unleashAPI("http://unleash.org")
+    .customHttpHeader("Authorization", "API token")
     .enableProxyAuthenticationByJvmProperties()
     .build();
 ```
diff --git a/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java b/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java
index c99824ee..ec36266c 100644
--- a/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java
+++ b/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java
@@ -85,7 +85,11 @@ public class UnleashMetricsSender {
 
         HttpURLConnection connection = null;
         try {
-            connection = (HttpURLConnection) url.openConnection();
+            if (this.unleashConfig.getProxy() != null) {
+                connection = (HttpURLConnection) url.openConnection(this.unleashConfig.getProxy());
+            } else {
+                connection = (HttpURLConnection) url.openConnection();
+            }
             connection.setConnectTimeout(CONNECT_TIMEOUT);
             connection.setReadTimeout(CONNECT_TIMEOUT);
             connection.setRequestMethod("POST");
diff --git a/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java b/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
index 7b51acaa..43f210f7 100644
--- a/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
+++ b/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
@@ -101,7 +101,12 @@ public final class HttpToggleFetcher implements ToggleFetcher {
     }
 
     private HttpURLConnection openConnection(URL url) throws IOException {
-        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        HttpURLConnection connection;
+        if (this.unleashConfig.getProxy() != null) {
+            connection = (HttpURLConnection) url.openConnection(this.unleashConfig.getProxy());
+        } else {
+            connection = (HttpURLConnection) url.openConnection();
+        }
         connection.setConnectTimeout(CONNECT_TIMEOUT);
         connection.setReadTimeout(CONNECT_TIMEOUT);
         connection.setRequestProperty("Accept", "application/json");
diff --git a/src/main/java/no/finn/unleash/util/UnleashConfig.java b/src/main/java/no/finn/unleash/util/UnleashConfig.java
index 82d9a6df..fb08f3a8 100644
--- a/src/main/java/no/finn/unleash/util/UnleashConfig.java
+++ b/src/main/java/no/finn/unleash/util/UnleashConfig.java
@@ -41,6 +41,7 @@ public class UnleashConfig {
     private final UnleashSubscriber unleashSubscriber;
     @Nullable private final Strategy fallbackStrategy;
     @Nullable private final ToggleBootstrapProvider toggleBootstrapProvider;
+    @Nullable private final Proxy proxy;
 
     private UnleashConfig(
             @Nullable URI unleashAPI,
@@ -61,7 +62,9 @@ public class UnleashConfig {
             @Nullable UnleashScheduledExecutor unleashScheduledExecutor,
             @Nullable UnleashSubscriber unleashSubscriber,
             @Nullable Strategy fallbackStrategy,
-            @Nullable ToggleBootstrapProvider unleashBootstrapProvider) {
+            @Nullable ToggleBootstrapProvider unleashBootstrapProvider,
+            @Nullable Proxy proxy,
+            @Nullable Authenticator proxyAuthenticator) {
 
         if (appName == null) {
             throw new IllegalStateException("You are required to specify the unleash appName");
@@ -89,8 +92,10 @@ public class UnleashConfig {
             this.fallbackStrategy = fallbackStrategy;
         }
 
-        if (isProxyAuthenticationByJvmProperties) {
+        if (isProxyAuthenticationByJvmProperties && proxyAuthenticator == null) {
             enableProxyAuthentication();
+        } else if (proxyAuthenticator != null) {
+            Authenticator.setDefault(proxyAuthenticator);
         }
 
         this.unleashAPI = unleashAPI;
@@ -112,6 +117,7 @@ public class UnleashConfig {
         this.unleashScheduledExecutor = unleashScheduledExecutor;
         this.unleashSubscriber = unleashSubscriber;
         this.toggleBootstrapProvider = unleashBootstrapProvider;
+        this.proxy = proxy;
     }
 
     public static Builder builder() {
@@ -129,7 +135,7 @@ public class UnleashConfig {
     private void enableProxyAuthentication() {
         // http.proxyUser http.proxyPassword is only consumed by Apache HTTP Client, for
         // HttpUrlConnection we have to define an Authenticator
-        Authenticator.setDefault(new ProxyAuthenticator());
+        Authenticator.setDefault(new SystemProxyAuthenticator());
     }
 
     public URI getUnleashAPI() {
@@ -214,8 +220,12 @@ public class UnleashConfig {
         return toggleBootstrapProvider;
     }
 
-    static class ProxyAuthenticator extends Authenticator {
+    @Nullable
+    public Proxy getProxy() {
+        return proxy;
+    }
 
+    static class SystemProxyAuthenticator extends Authenticator {
         @Override
         protected @Nullable PasswordAuthentication getPasswordAuthentication() {
             if (getRequestorType() == RequestorType.PROXY) {
@@ -227,8 +237,36 @@ public class UnleashConfig {
 
                 // Only apply PasswordAuthentication to requests to the proxy itself - if not set
                 // just ignore
-                if (getRequestingHost().equalsIgnoreCase(proxyHost)
-                        && Integer.parseInt(proxyPort) == getRequestingPort()) {
+                if (getRequestingHost().equalsIgnoreCase(proxyHost) && Integer.parseInt(proxyPort) == getRequestingPort()) {
+                    return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
+                }
+            }
+            return null;
+        }
+    }
+
+    static class CustomProxyAuthenticator extends Authenticator {
+
+        private final Proxy proxy;
+        private final String proxyUser;
+        private final String proxyPassword;
+
+        public CustomProxyAuthenticator(Proxy proxy, String proxyUser, String proxyPassword) {
+            this.proxy = proxy;
+            this.proxyUser = proxyUser;
+            this.proxyPassword = proxyPassword;
+        }
+
+        @Override
+        protected @Nullable PasswordAuthentication getPasswordAuthentication() {
+            if (getRequestorType() == RequestorType.PROXY && proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) {
+                final String proxyHost = ((InetSocketAddress) proxy.address()).getHostName();
+                final int proxyPort = ((InetSocketAddress) proxy.address()).getPort();
+
+                // Only apply PasswordAuthentication to requests to the proxy
+                // itself - if not set
+                // just ignore
+                if (getRequestingHost().equalsIgnoreCase(proxyHost) && proxyPort == getRequestingPort()) {
                     return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
                 }
             }
@@ -259,6 +297,8 @@ public class UnleashConfig {
         private boolean isProxyAuthenticationByJvmProperties;
         private Strategy fallbackStrategy;
         private @Nullable ToggleBootstrapProvider toggleBootstrapProvider;
+        private @Nullable Proxy proxy;
+        private @Nullable Authenticator proxyAuthenticator;
 
         private static String getHostname() {
             String hostName = System.getProperty("hostname");
@@ -371,6 +411,21 @@ public class UnleashConfig {
             return this;
         }
 
+        public Builder proxy(Proxy proxy) {
+            this.proxy = proxy;
+            return this;
+        }
+
+        public Builder proxy(
+                Proxy proxy, @Nullable String proxyUser, @Nullable String proxyPassword) {
+            this.proxy = proxy;
+
+            if (proxyUser != null && proxyPassword != null) {
+                this.proxyAuthenticator = new CustomProxyAuthenticator(proxy, proxyUser, proxyPassword);
+            }
+            return this;
+        }
+
         private String getBackupFile() {
             if (backupFile != null) {
                 return backupFile;
@@ -401,7 +456,9 @@ public class UnleashConfig {
                             .orElseGet(UnleashScheduledExecutorImpl::getInstance),
                     Optional.ofNullable(unleashSubscriber).orElseGet(NoOpSubscriber::new),
                     fallbackStrategy,
-                    toggleBootstrapProvider);
+                    toggleBootstrapProvider,
+                    proxy,
+                    proxyAuthenticator);
         }
 
         public String getDefaultSdkVersion() {


Test patch:
diff --git a/src/test/java/no/finn/unleash/ManualTesting.java b/src/test/java/no/finn/unleash/ManualTesting.java
index 40d0cec4..4e44cb58 100644
--- a/src/test/java/no/finn/unleash/ManualTesting.java
+++ b/src/test/java/no/finn/unleash/ManualTesting.java
@@ -29,6 +29,7 @@ public class ManualTesting {
                         .appName("java-test")
                         .instanceId("instance y")
                         .unleashAPI("https://unleash.herokuapp.com/api/")
+                        .customHttpHeader("Authorization", "3bd74da5b341d868443134377ba5d802ea1e6fa2d2a948276ade1f092bec8d92")
                         .subscriber(
                                 new UnleashSubscriber() {
                                     @Override
diff --git a/src/test/java/no/finn/unleash/util/UnleashConfigTest.java b/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
index 2a6bbf79..edb5788e 100644
--- a/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
+++ b/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
@@ -15,7 +15,8 @@ import java.util.HashMap;
 import java.util.Map;
 import no.finn.unleash.CustomHttpHeadersProvider;
 import no.finn.unleash.DefaultCustomHttpHeadersProviderImpl;
-import no.finn.unleash.util.UnleashConfig.ProxyAuthenticator;
+import no.finn.unleash.util.UnleashConfig.CustomProxyAuthenticator;
+import no.finn.unleash.util.UnleashConfig.SystemProxyAuthenticator;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.function.Executable;
 import org.mockito.Mockito;
@@ -222,7 +223,7 @@ public class UnleashConfigTest {
 
         Field authenticator = Authenticator.class.getDeclaredField("theAuthenticator");
         authenticator.setAccessible(true);
-        ProxyAuthenticator proxyAuthenticator = (ProxyAuthenticator) authenticator.get(null);
+        SystemProxyAuthenticator proxyAuthenticator = (SystemProxyAuthenticator) authenticator.get(null);
 
         Field requestingAuthType =
                 proxyAuthenticator
@@ -257,4 +258,62 @@ public class UnleashConfigTest {
         assertThat(new String(passwordAuthentication.getPassword())).isEqualTo(proxyPassword);
         assertThat(config.isProxyAuthenticationByJvmProperties()).isEqualTo(true);
     }
+
+    @Test
+    public void should_use_specified_proxy() throws IllegalAccessException, NoSuchFieldException {
+
+        String proxyHost = "proxy-host";
+        int proxyPort = 8080;
+        String proxyUser = "my-proxy-user";
+        String proxyPassword = "my-secret";
+
+        UnleashConfig config =
+                UnleashConfig.builder()
+                        .appName("my-app")
+                        .unleashAPI("http://unleash.org")
+                        .proxy(
+                                new Proxy(
+                                        Proxy.Type.HTTP,
+                                        new InetSocketAddress(proxyHost, proxyPort)),
+                                proxyUser,
+                                proxyPassword)
+                        .build();
+
+        Field authenticator = Authenticator.class.getDeclaredField("theAuthenticator");
+        authenticator.setAccessible(true);
+        CustomProxyAuthenticator proxyAuthenticator = (CustomProxyAuthenticator) authenticator.get(null);
+
+        Field requestingAuthType =
+                proxyAuthenticator
+                        .getClass()
+                        .getSuperclass()
+                        .getDeclaredField("requestingAuthType");
+        requestingAuthType.setAccessible(true);
+        requestingAuthType.set(proxyAuthenticator, RequestorType.PROXY);
+
+        Field requestingProtocol =
+                proxyAuthenticator
+                        .getClass()
+                        .getSuperclass()
+                        .getDeclaredField("requestingProtocol");
+        requestingProtocol.setAccessible(true);
+        requestingProtocol.set(proxyAuthenticator, "http");
+
+        Field requestingHost =
+                proxyAuthenticator.getClass().getSuperclass().getDeclaredField("requestingHost");
+        requestingHost.setAccessible(true);
+        requestingHost.set(proxyAuthenticator, proxyHost);
+
+        Field requestingPort =
+                proxyAuthenticator.getClass().getSuperclass().getDeclaredField("requestingPort");
+        requestingPort.setAccessible(true);
+        requestingPort.set(proxyAuthenticator, 8080);
+
+        PasswordAuthentication passwordAuthentication = proxyAuthenticator.getPasswordAuthentication();
+
+        assertThat(passwordAuthentication.getUserName()).isEqualTo(proxyUser);
+        assertThat(new
+            String(passwordAuthentication.getPassword())).isEqualTo(proxyPassword);
+        assertThat(config.isProxyAuthenticationByJvmProperties()).isEqualTo(false);
+    }
 }