diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml
index f820cb0eb3f0..31a4f75d6eb6 100755
--- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml
+++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml
@@ -191,6 +191,7 @@
+
diff --git a/pom.client.xml b/pom.client.xml
index 4978abe8430d..3a54b571e6c9 100644
--- a/pom.client.xml
+++ b/pom.client.xml
@@ -927,6 +927,7 @@
--add-opens com.azure.core/com.azure.core=ALL-UNNAMED
--add-opens com.azure.core/com.azure.core.util.polling=ALL-UNNAMED
+ --add-opens com.azure.core/com.azure.core.http=ALL-UNNAMED
--add-opens com.azure.core/com.azure.core.http.policy=ALL-UNNAMED
--add-opens com.azure.core/com.azure.core.util=ALL-UNNAMED
--add-opens com.azure.ai.textanalytics/com.azure.ai.textanalytics=ALL-UNNAMED
diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java
index 3a027dbb0fdc..d4c97fa63f74 100644
--- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java
+++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java
@@ -4,6 +4,7 @@
package com.azure.core.http.netty;
import com.azure.core.http.ProxyOptions;
+import com.azure.core.util.Configuration;
import com.azure.core.util.logging.ClientLogger;
import io.netty.channel.nio.NioEventLoopGroup;
import reactor.netty.http.client.HttpClient;
@@ -31,18 +32,19 @@ public class NettyAsyncHttpClientBuilder {
private boolean enableWiretap;
private int port = 80;
private NioEventLoopGroup nioEventLoopGroup;
+ private Configuration configuration;
/**
- * Creates a new builder instance, where a builder is capable of generating multiple instances of
- * {@link NettyAsyncHttpClient}.
+ * Creates a new builder instance, where a builder is capable of generating multiple instances of {@link
+ * NettyAsyncHttpClient}.
*/
public NettyAsyncHttpClientBuilder() {
this.baseHttpClient = null;
}
/**
- * Creates a new builder instance, where a builder is capable of generating multiple instances of
- * {@link NettyAsyncHttpClient} based on the provided reactor netty HttpClient.
+ * Creates a new builder instance, where a builder is capable of generating multiple instances of {@link
+ * NettyAsyncHttpClient} based on the provided reactor netty HttpClient.
*
* {@codesnippet com.azure.core.http.netty.from-existing-http-client}
*
@@ -53,8 +55,8 @@ public NettyAsyncHttpClientBuilder(HttpClient nettyHttpClient) {
}
/**
- * Creates a new Netty-backed {@link com.azure.core.http.HttpClient} instance on every call, using the
- * configuration set in the builder at the time of the build method call.
+ * Creates a new Netty-backed {@link com.azure.core.http.HttpClient} instance on every call, using the configuration
+ * set in the builder at the time of the build method call.
*
* @return A new Netty-backed {@link com.azure.core.http.HttpClient} instance.
* @throws IllegalStateException If the builder is configured to use an unknown proxy type.
@@ -70,6 +72,11 @@ public com.azure.core.http.HttpClient build() {
} else {
nettyHttpClient = this.baseHttpClient == null ? HttpClient.create() : this.baseHttpClient;
}
+
+ Configuration buildConfiguration = (configuration == null)
+ ? Configuration.getGlobalConfiguration()
+ : configuration;
+
nettyHttpClient = nettyHttpClient
.port(port)
.wiretap(enableWiretap)
@@ -78,50 +85,57 @@ public com.azure.core.http.HttpClient build() {
tcpConfig = tcpConfig.runOn(nioEventLoopGroup);
}
- if (proxyOptions != null) {
- ProxyProvider.Proxy nettyProxy;
- switch (proxyOptions.getType()) {
- case HTTP:
- nettyProxy = ProxyProvider.Proxy.HTTP;
- break;
- case SOCKS4:
- nettyProxy = ProxyProvider.Proxy.SOCKS4;
- break;
- case SOCKS5:
- nettyProxy = ProxyProvider.Proxy.SOCKS5;
- break;
- default:
- throw logger.logExceptionAsError(new IllegalStateException(
- String.format("Unknown Proxy type '%s' in use. Not configuring Netty proxy.",
- proxyOptions.getType())));
- }
- if (proxyOptions.getUsername() != null) {
- // Netty supports only Basic proxy authentication and we default to it.
- return tcpConfig.proxy(ts -> ts.type(nettyProxy)
- .address(proxyOptions.getAddress())
- .username(proxyOptions.getUsername())
- .password(userName -> proxyOptions.getPassword())
- .build());
- } else {
- return tcpConfig.proxy(ts -> ts.type(nettyProxy).address(proxyOptions.getAddress()));
- }
+ ProxyOptions buildProxyOptions = (proxyOptions == null)
+ ? ProxyOptions.fromConfiguration(buildConfiguration)
+ : proxyOptions;
+
+ if (buildProxyOptions != null) {
+ tcpConfig = tcpConfig.proxy(typeSpec ->
+ typeSpec.type(mapProxyType(buildProxyOptions.getType(), logger))
+ .address(proxyOptions.getAddress())
+ .username(proxyOptions.getUsername())
+ .password(user -> proxyOptions.getPassword())
+ .nonProxyHosts(proxyOptions.getNonProxyHosts()));
}
+
return tcpConfig;
});
+
return new NettyAsyncHttpClient(nettyHttpClient);
}
+ /*
+ * Maps a 'ProxyOptions.Type' to a 'ProxyProvider.Proxy', if the type is unknown or cannot be mapped an
+ * IllegalStateException will be thrown.
+ */
+ private static ProxyProvider.Proxy mapProxyType(ProxyOptions.Type type, ClientLogger logger) {
+ Objects.requireNonNull(type, "'ProxyOptions.getType()' cannot be null.");
+
+ switch (type) {
+ case HTTP:
+ return ProxyProvider.Proxy.HTTP;
+ case SOCKS4:
+ return ProxyProvider.Proxy.SOCKS4;
+ case SOCKS5:
+ return ProxyProvider.Proxy.SOCKS5;
+ default:
+ throw logger.logExceptionAsError(new IllegalStateException(
+ String.format("Unknown proxy type '%s' in use. Use a proxy type from 'ProxyOptions.Type'.", type)));
+ }
+ }
+
/**
* Sets the connection provider.
*
* @param connectionProvider the connection provider
- * @return the updated {@link NettyAsyncHttpClientBuilder} object
+ * @return the updated {@link NettyAsyncHttpClientBuilder} object.
*/
public NettyAsyncHttpClientBuilder connectionProvider(ConnectionProvider connectionProvider) {
// Enables overriding the default reactor-netty connection/channel pool.
this.connectionProvider = connectionProvider;
return this;
}
+
/**
* Sets the {@link ProxyOptions proxy options} that the client will use.
*
@@ -130,7 +144,7 @@ public NettyAsyncHttpClientBuilder connectionProvider(ConnectionProvider connect
* {@codesnippet com.azure.core.http.netty.NettyAsyncHttpClientBuilder#proxy}
*
* @param proxyOptions The proxy configuration to use.
- * @return the updated NettyAsyncHttpClientBuilder object
+ * @return the updated NettyAsyncHttpClientBuilder object.
*/
public NettyAsyncHttpClientBuilder proxy(ProxyOptions proxyOptions) {
// proxyOptions can be null
@@ -142,7 +156,7 @@ public NettyAsyncHttpClientBuilder proxy(ProxyOptions proxyOptions) {
* Enables the Netty wiretap feature.
*
* @param enableWiretap Flag indicating wiretap status
- * @return the updated NettyAsyncHttpClientBuilder object
+ * @return the updated NettyAsyncHttpClientBuilder object.
*/
public NettyAsyncHttpClientBuilder wiretap(boolean enableWiretap) {
this.enableWiretap = enableWiretap;
@@ -153,7 +167,7 @@ public NettyAsyncHttpClientBuilder wiretap(boolean enableWiretap) {
* Sets the port which this client should connect, which by default will be set to port 80.
*
* @param port The port to connect to.
- * @return the updated NettyAsyncHttpClientBuilder object
+ * @return the updated NettyAsyncHttpClientBuilder object.
*/
public NettyAsyncHttpClientBuilder port(int port) {
this.port = port;
@@ -168,10 +182,24 @@ public NettyAsyncHttpClientBuilder port(int port) {
* {@codesnippet com.azure.core.http.netty.NettyAsyncHttpClientBuilder#nioEventLoopGroup}
*
* @param nioEventLoopGroup The {@link NioEventLoopGroup} that will run IO loops.
- * @return the updated NettyAsyncHttpClientBuilder object
+ * @return the updated NettyAsyncHttpClientBuilder object.
*/
public NettyAsyncHttpClientBuilder nioEventLoopGroup(NioEventLoopGroup nioEventLoopGroup) {
this.nioEventLoopGroup = nioEventLoopGroup;
return this;
}
+
+ /**
+ * Sets the configuration store that is used during construction of the HTTP client.
+ *
+ * The default configuration store is a clone of the {@link Configuration#getGlobalConfiguration() global
+ * configuration store}, use {@link Configuration#NONE} to bypass using configuration settings during construction.
+ *
+ * @param configuration The configuration store used to
+ * @return The updated NettyAsyncHttpClientBuilder object.
+ */
+ public NettyAsyncHttpClientBuilder configuration(Configuration configuration) {
+ this.configuration = configuration;
+ return this;
+ }
}
diff --git a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/ReactorNettyClientTests.java b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/ReactorNettyClientTests.java
index 97702f6ea915..e97e54447a75 100644
--- a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/ReactorNettyClientTests.java
+++ b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/ReactorNettyClientTests.java
@@ -37,6 +37,8 @@
import static com.azure.core.http.netty.NettyAsyncHttpClient.ReactorNettyHttpResponse;
import static java.time.Duration.ofMillis;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTimeout;
public class ReactorNettyClientTests {
@@ -179,8 +181,7 @@ public void testServerShutsDownSocketShouldPushErrorToContentFlowable() {
assertTimeout(ofMillis(5000), () -> {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference sock = new AtomicReference<>();
- ServerSocket ss = new ServerSocket(0);
- try {
+ try (ServerSocket ss = new ServerSocket(0)) {
Mono.fromCallable(() -> {
latch.countDown();
Socket socket = ss.accept();
@@ -210,15 +211,13 @@ public void testServerShutsDownSocketShouldPushErrorToContentFlowable() {
HttpClient client = HttpClient.createDefault();
HttpRequest request = new HttpRequest(HttpMethod.GET,
new URL("http://localhost:" + ss.getLocalPort() + "/get"));
+
HttpResponse response = client.send(request).block();
- Assertions.assertEquals(200, response.getStatusCode());
- System.out.println("reading body");
- //
+ assertNotNull(response);
+ assertEquals(200, response.getStatusCode());
+
StepVerifier.create(response.getBodyAsByteArray())
- // .awaitDone(20, TimeUnit.SECONDS)
.verifyError(IOException.class);
- } finally {
- ss.close();
}
});
}
@@ -291,10 +290,7 @@ private static MessageDigest md5Digest() {
}
private static byte[] digest(String s) throws NoSuchAlgorithmException {
- MessageDigest md = MessageDigest.getInstance("MD5");
- md.update(s.getBytes(StandardCharsets.UTF_8));
- byte[] expectedDigest = md.digest();
- return expectedDigest;
+ return MessageDigest.getInstance("MD5").digest(s.getBytes(StandardCharsets.UTF_8));
}
private static final class NumberedByteBuf {
@@ -326,24 +322,22 @@ private static URL url(WireMockServer server, String path) {
}
private static String createLongBody() {
- StringBuilder s = new StringBuilder(10000000);
+ StringBuilder builder = new StringBuilder("abcdefghijk".length() * 1000000);
for (int i = 0; i < 1000000; i++) {
- s.append("abcdefghijk");
+ builder.append("abcdefghijk");
}
- return s.toString();
+
+ return builder.toString();
}
private void checkBodyReceived(String expectedBody, String path) {
- NettyAsyncHttpClient client = new NettyAsyncHttpClient();
- HttpResponse response = doRequest(client, path);
- String s = new String(response.getBodyAsByteArray().block(),
- StandardCharsets.UTF_8);
- Assertions.assertEquals(expectedBody, s);
+ StepVerifier.create(doRequest(new NettyAsyncHttpClient(), path).getBodyAsByteArray())
+ .assertNext(bytes -> assertEquals(expectedBody, new String(bytes, StandardCharsets.UTF_8)))
+ .verifyComplete();
}
private ReactorNettyHttpResponse doRequest(NettyAsyncHttpClient client, String path) {
HttpRequest request = new HttpRequest(HttpMethod.GET, url(server, path));
- ReactorNettyHttpResponse response = (ReactorNettyHttpResponse) client.send(request).block();
- return response;
+ return (ReactorNettyHttpResponse) client.send(request).block();
}
}
diff --git a/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilder.java b/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilder.java
index 9256d7f31fc7..ec01cfbb4df6 100644
--- a/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilder.java
+++ b/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilder.java
@@ -5,12 +5,15 @@
import com.azure.core.http.HttpClient;
import com.azure.core.http.ProxyOptions;
+import com.azure.core.http.okhttp.implementation.OkHttpProxySelector;
+import com.azure.core.util.Configuration;
import com.azure.core.util.logging.ClientLogger;
import okhttp3.ConnectionPool;
import okhttp3.Credentials;
import okhttp3.Dispatcher;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
+
import java.net.Proxy;
import java.time.Duration;
import java.util.ArrayList;
@@ -34,6 +37,7 @@ public class OkHttpAsyncHttpClientBuilder {
private ConnectionPool connectionPool;
private Dispatcher dispatcher;
private ProxyOptions proxyOptions;
+ private Configuration configuration;
/**
* Creates OkHttpAsyncHttpClientBuilder.
@@ -144,6 +148,20 @@ public OkHttpAsyncHttpClientBuilder proxy(ProxyOptions proxyOptions) {
return this;
}
+ /**
+ * Sets the configuration store that is used during construction of the HTTP client.
+ *
+ * The default configuration store is a clone of the {@link Configuration#getGlobalConfiguration() global
+ * configuration store}, use {@link Configuration#NONE} to bypass using configuration settings during construction.
+ *
+ * @param configuration The configuration store used to
+ * @return The updated OkHttpAsyncHttpClientBuilder object.
+ */
+ public OkHttpAsyncHttpClientBuilder configuration(Configuration configuration) {
+ this.configuration = configuration;
+ return this;
+ }
+
/**
* Build a HttpClient with current configurations.
*
@@ -153,59 +171,73 @@ public HttpClient build() {
OkHttpClient.Builder httpClientBuilder = this.okHttpClient == null
? new OkHttpClient.Builder()
: this.okHttpClient.newBuilder();
- //
+
+ // Add each interceptor that has been added.
for (Interceptor interceptor : this.networkInterceptors) {
httpClientBuilder = httpClientBuilder.addNetworkInterceptor(interceptor);
}
- if (this.readTimeout != null) {
- httpClientBuilder = httpClientBuilder.readTimeout(this.readTimeout);
- } else {
- httpClientBuilder = httpClientBuilder.readTimeout(DEFAULT_READ_TIMEOUT);
- }
- if (this.connectionTimeout != null) {
- httpClientBuilder = httpClientBuilder.connectTimeout(this.connectionTimeout);
- } else {
- httpClientBuilder = httpClientBuilder.connectTimeout(DEFAULT_CONNECT_TIMEOUT);
- }
+
+ // Use the configured read timeout if set, otherwise use the default (120s).
+ httpClientBuilder = httpClientBuilder.readTimeout((readTimeout != null) ? readTimeout : DEFAULT_READ_TIMEOUT);
+
+ // Use the configured connection timeout if set, otherwise use the default (60s).
+ httpClientBuilder = (this.connectionTimeout != null)
+ ? httpClientBuilder.connectTimeout(this.connectionTimeout)
+ : httpClientBuilder.connectTimeout(DEFAULT_CONNECT_TIMEOUT);
+
+ // If set use the configured connection pool.
if (this.connectionPool != null) {
httpClientBuilder = httpClientBuilder.connectionPool(connectionPool);
}
+
+ // If set use the configured dispatcher.
if (this.dispatcher != null) {
httpClientBuilder = httpClientBuilder.dispatcher(dispatcher);
}
- if (proxyOptions != null) {
- Proxy.Type proxyType;
- switch (proxyOptions.getType()) {
- case HTTP:
- proxyType = Proxy.Type.HTTP;
- break;
- case SOCKS4:
- case SOCKS5:
- // JDK Proxy.Type.SOCKS identifies SOCKS V4 and V5 proxy.
- proxyType = Proxy.Type.SOCKS;
- break;
- default:
- throw logger.logExceptionAsError(new IllegalStateException(
- String.format("Unknown Proxy type '%s' in use. Not configuring OkHttp proxy.",
- proxyOptions.getType())));
- }
- Proxy proxy = new Proxy(proxyType, this.proxyOptions.getAddress());
- httpClientBuilder = httpClientBuilder.proxy(proxy);
+
+ Configuration buildConfiguration = (configuration == null)
+ ? Configuration.getGlobalConfiguration()
+ : configuration;
+
+ ProxyOptions buildProxyOptions = (proxyOptions == null)
+ ? ProxyOptions.fromConfiguration(buildConfiguration)
+ : proxyOptions;
+
+ if (buildProxyOptions != null) {
+ httpClientBuilder = httpClientBuilder.proxySelector(new OkHttpProxySelector(
+ mapProxyType(buildProxyOptions.getType(), logger), buildProxyOptions.getAddress(),
+ buildProxyOptions.getNonProxyHosts()));
+
if (proxyOptions.getUsername() != null) {
- httpClientBuilder = httpClientBuilder.proxyAuthenticator((route, response) -> {
- // By default azure-core supports only Basic authentication at the moment.
- // If user need other scheme such as Digest then they can use 'configuration'
- // to get access to the underlying builder and can set 'proxyAuthenticator'.
- // In future when we ever support Digest in core-level then we can look at
- // response.challenges and get the scheme from there.
- String credential = Credentials.basic(proxyOptions.getUsername(),
- proxyOptions.getPassword());
- return response.request().newBuilder()
- .header("Proxy-Authorization", credential)
- .build();
- });
+ String basicAuthorizationHeader = Credentials.basic(buildProxyOptions.getUsername(),
+ buildProxyOptions.getPassword());
+
+ httpClientBuilder = httpClientBuilder.proxyAuthenticator((route, response) ->
+ response.request().newBuilder()
+ .header("Proxy-Authorization", basicAuthorizationHeader)
+ .build());
}
}
+
return new OkHttpAsyncHttpClient(httpClientBuilder.build());
}
+
+ /*
+ * Maps a 'ProxyOptions.Type' to a 'ProxyProvider.Proxy', if the type is unknown or cannot be mapped an
+ * IllegalStateException will be thrown.
+ */
+ private static Proxy.Type mapProxyType(ProxyOptions.Type type, ClientLogger logger) {
+ Objects.requireNonNull(type, "'ProxyOptions.getType()' cannot be null.");
+
+ switch (type) {
+ case HTTP:
+ return Proxy.Type.HTTP;
+ case SOCKS4:
+ case SOCKS5:
+ return Proxy.Type.SOCKS;
+ default:
+ throw logger.logExceptionAsError(new IllegalStateException(
+ String.format("Unknown proxy type '%s' in use. Use a proxy type from 'ProxyOptions.Type'.", type)));
+ }
+ }
}
diff --git a/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/implementation/OkHttpProxySelector.java b/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/implementation/OkHttpProxySelector.java
new file mode 100644
index 000000000000..8516131de8d4
--- /dev/null
+++ b/sdk/core/azure-core-http-okhttp/src/main/java/com/azure/core/http/okhttp/implementation/OkHttpProxySelector.java
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.http.okhttp.implementation;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * This class handles selecting the proxy during a request.
+ */
+public final class OkHttpProxySelector extends ProxySelector {
+ private final Proxy.Type proxyType;
+ private final SocketAddress proxyAddress;
+ private final Pattern nonProxyHostsPattern;
+
+ public OkHttpProxySelector(Proxy.Type proxyType, SocketAddress proxyAddress, String nonProxyHosts) {
+ this.proxyType = proxyType;
+ this.proxyAddress = proxyAddress;
+ this.nonProxyHostsPattern = Pattern.compile(nonProxyHosts);
+ }
+
+ @Override
+ public List select(URI uri) {
+ /*
+ * If the 'URI' the request is being sent to matches the 'nonProxyHostsPattern' return no options for proxying,
+ * otherwise return the proxy.
+ */
+ return nonProxyHostsPattern.matcher(uri.toString()).find()
+ ? null
+ : Collections.singletonList(new Proxy(proxyType, proxyAddress));
+ }
+
+ @Override
+ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+ // Ignored.
+ }
+}
diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/ProxyOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/ProxyOptions.java
index a803905bbd1e..b4dcef68e8e0 100644
--- a/sdk/core/azure-core/src/main/java/com/azure/core/http/ProxyOptions.java
+++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/ProxyOptions.java
@@ -3,18 +3,63 @@
package com.azure.core.http;
+import com.azure.core.util.Configuration;
+import com.azure.core.util.CoreUtils;
+import com.azure.core.util.logging.ClientLogger;
+
+import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
/**
* This represents proxy configuration to be used in http clients..
*/
public class ProxyOptions {
+ private static final ClientLogger LOGGER = new ClientLogger(ProxyOptions.class);
+
+ private static final String INVALID_CONFIGURATION_MESSAGE = "'configuration' cannot be 'Configuration.NONE'.";
+ private static final String INVALID_AZURE_PROXY_URL = "Configuration {} is an invalid URL and is being ignored.";
+
+ /*
+ * This indicates whether Java environment proxy configurations are allowed to be used.
+ */
+ private static final String JAVA_PROXY_PREREQUISITE = "java.net.useSystemProxies";
+
+ /*
+ * Java environment variables related to proxies. The protocol is removed since these are the same for 'https' and
+ * 'http', the exception is 'http.nonProxyHosts' as it is used for both.
+ */
+ private static final String JAVA_PROXY_HOST = "proxyHost";
+ private static final String JAVA_PROXY_PORT = "proxyPort";
+ private static final String JAVA_PROXY_USER = "proxyUser";
+ private static final String JAVA_PROXY_PASSWORD = "proxyPassword";
+ private static final String JAVA_NON_PROXY_HOSTS = "http.nonProxyHosts";
+
+ private static final String HTTPS = "https";
+ private static final int DEFAULT_HTTPS_PORT = 443;
+
+ private static final String HTTP = "http";
+ private static final int DEFAULT_HTTP_PORT = 80;
+
+ private static final List> ENVIRONMENT_LOAD_ORDER = Arrays.asList(
+ configuration -> attemptToLoadAzureProxy(configuration, Configuration.PROPERTY_HTTPS_PROXY),
+ configuration -> attemptToLoadAzureProxy(configuration, Configuration.PROPERTY_HTTP_PROXY),
+ configuration -> attemptToLoadJavaProxy(configuration, HTTPS),
+ configuration -> attemptToLoadJavaProxy(configuration, HTTP)
+ );
+
private final InetSocketAddress address;
private final Type type;
private String username;
private String password;
-
+ private String nonProxyHosts;
/**
* Creates ProxyOptions.
@@ -40,6 +85,21 @@ public ProxyOptions setCredentials(String username, String password) {
return this;
}
+ /**
+ * Sets the hosts which bypass the proxy.
+ *
+ *
+ * The expected format of the passed string is a {@code '|'} delimited list of hosts which should bypass the proxy.
+ * Individual host strings may contain regex characters such as {@code '*'}.
+ *
+ * @param nonProxyHosts Hosts that bypass the proxy.
+ * @return the updated ProxyOptions object
+ */
+ public ProxyOptions setNonProxyHosts(String nonProxyHosts) {
+ this.nonProxyHosts = nonProxyHosts;
+ return this;
+ }
+
/**
* @return the address of the proxy.
*/
@@ -68,6 +128,126 @@ public String getPassword() {
return this.password;
}
+ /**
+ * @return the hosts that bypass the proxy.
+ */
+ public String getNonProxyHosts() {
+ return this.nonProxyHosts;
+ }
+
+ /**
+ * Attempts to load a proxy from the environment.
+ *
+ *
+ * Environment configurations are loaded in this order:
+ *
+ * - Azure HTTPS
+ * - Azure HTTP
+ * - Java HTTPS
+ * - Java HTTP
+ *
+ *
+ * Azure proxy configurations will be preferred over Java proxy configurations as they are more closely scoped to
+ * the purpose of the SDK. Additionally, more secure protocols, HTTPS vs HTTP, will be preferred.
+ *
+ *
+ * {@code null} will be returned if no proxy was found in the environment.
+ *
+ * @param configuration The {@link Configuration} that is used to load proxy configurations from the environment.
+ * If {@code null} is passed then {@link Configuration#getGlobalConfiguration()} will be used. If
+ * {@link Configuration#NONE} is passed {@link IllegalArgumentException} will be thrown.
+ * @return A {@link ProxyOptions} reflecting a proxy loaded from the environment, if no proxy is found {@code null}
+ * will be returned.
+ * @throws IllegalArgumentException If {@code configuration} is {@link Configuration#NONE}.
+ */
+ public static ProxyOptions fromConfiguration(Configuration configuration) {
+ if (configuration == Configuration.NONE) {
+ throw LOGGER.logExceptionAsWarning(new IllegalArgumentException(INVALID_CONFIGURATION_MESSAGE));
+ }
+
+ Configuration proxyConfiguration = (configuration == null)
+ ? Configuration.getGlobalConfiguration()
+ : configuration;
+
+ for (Function loader : ENVIRONMENT_LOAD_ORDER) {
+ ProxyOptions proxyOptions = loader.apply(proxyConfiguration);
+ if (proxyOptions != null) {
+ return proxyOptions;
+ }
+ }
+
+ return null;
+ }
+
+ private static ProxyOptions attemptToLoadAzureProxy(Configuration configuration, String proxyProperty) {
+ String proxyConfiguration = configuration.get(proxyProperty);
+
+ // No proxy configuration setup.
+ if (CoreUtils.isNullOrEmpty(proxyConfiguration)) {
+ return null;
+ }
+
+ try {
+ URL proxyUrl = new URL(proxyConfiguration);
+ int port = (proxyUrl.getPort() == -1) ? proxyUrl.getDefaultPort() : proxyUrl.getPort();
+ ProxyOptions proxyOptions = new ProxyOptions(Type.HTTP, new InetSocketAddress(proxyUrl.getHost(), port))
+ .setNonProxyHosts(configuration.get(Configuration.PROPERTY_NO_PROXY));
+
+ String userInfo = proxyUrl.getUserInfo();
+ if (userInfo != null) {
+ String[] usernamePassword = userInfo.split(":", 2);
+ if (usernamePassword.length == 2) {
+ try {
+ proxyOptions.setCredentials(
+ URLDecoder.decode(usernamePassword[0], StandardCharsets.UTF_8.toString()),
+ URLDecoder.decode(usernamePassword[1], StandardCharsets.UTF_8.toString())
+ );
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+ }
+
+ return proxyOptions;
+ } catch (MalformedURLException ex) {
+ LOGGER.warning(INVALID_AZURE_PROXY_URL, proxyProperty);
+ return null;
+ }
+ }
+
+ private static ProxyOptions attemptToLoadJavaProxy(Configuration configuration, String type) {
+ // Not allowed to use Java proxies
+ if (!Boolean.parseBoolean(configuration.get(JAVA_PROXY_PREREQUISITE))) {
+ return null;
+ }
+
+ String host = configuration.get(type + "." + JAVA_PROXY_HOST);
+
+ // No proxy configuration setup.
+ if (CoreUtils.isNullOrEmpty(host)) {
+ return null;
+ }
+
+ int port;
+ try {
+ port = Integer.parseInt(configuration.get(type + "." + JAVA_PROXY_PORT));
+ } catch (NumberFormatException ex) {
+ port = HTTPS.equals(type) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
+ }
+
+ ProxyOptions proxyOptions = new ProxyOptions(Type.HTTP, new InetSocketAddress(host, port))
+ .setNonProxyHosts(configuration.get(JAVA_NON_PROXY_HOSTS));
+
+ String username = configuration.get(type + "." + JAVA_PROXY_USER);
+ String password = configuration.get(type + "." + JAVA_PROXY_PASSWORD);
+
+ if (username != null && password != null) {
+ proxyOptions.setCredentials(username, password);
+ }
+
+ return proxyOptions;
+ }
+
/**
* The type of the proxy.
*/
diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java
new file mode 100644
index 000000000000..63a40795da76
--- /dev/null
+++ b/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java
@@ -0,0 +1,213 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.core.http;
+
+import com.azure.core.util.Configuration;
+import com.azure.core.util.CoreUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * This class tests {@link ProxyOptions}.
+ */
+public class ProxyOptionsTests {
+ private static final String HTTPS = "https";
+ private static final String HTTP = "http";
+
+ private static final String PROXY_HOST = "localhost";
+ private static final String PROXY_USER = "user";
+ private static final String PROXY_PASSWORD = "pass";
+ private static final String NON_PROXY_HOSTS = "notlocalhost";
+
+ private static final String JAVA_PROXY_PREREQUISITE = "java.net.useSystemProxies";
+ private static final String JAVA_NON_PROXY_HOSTS = "http.nonProxyHosts";
+
+ private static final String JAVA_HTTPS_PROXY_HOST = "https.proxyHost";
+ private static final String JAVA_HTTPS_PROXY_PORT = "https.proxyPort";
+ private static final String JAVA_HTTPS_PROXY_USER = "https.proxyUser";
+ private static final String JAVA_HTTPS_PROXY_PASSWORD = "https.proxyPassword";
+
+ private static final String JAVA_HTTP_PROXY_HOST = "http.proxyHost";
+ private static final String JAVA_HTTP_PROXY_PORT = "http.proxyPort";
+ private static final String JAVA_HTTP_PROXY_USER = "http.proxyUser";
+ private static final String JAVA_HTTP_PROXY_PASSWORD = "http.proxyPassword";
+
+ private static final String AZURE_HTTPS_PROXY_HOST_ONLY = String.format("%s://%s", HTTPS, PROXY_HOST);
+ private static final String AZURE_HTTP_PROXY_HOST_ONLY = String.format("%s://%s", HTTP, PROXY_HOST);
+
+ private static final String AZURE_HTTPS_PROXY_WITH_USERNAME = String.format("%s://%s@%s", HTTPS, PROXY_USER,
+ PROXY_HOST);
+ private static final String AZURE_HTTP_PROXY_WITH_USERNAME = String.format("%s://%s@%s", HTTP, PROXY_USER,
+ PROXY_HOST);
+
+ private static final String AZURE_HTTPS_PROXY_WITH_USER_AND_PASS = String.format("%s://%s:%s@%s", HTTPS, PROXY_USER,
+ PROXY_PASSWORD, PROXY_HOST);
+ private static final String AZURE_HTTP_PROXY_WITH_USER_AND_PASS = String.format("%s://%s:%s@%s", HTTP, PROXY_USER,
+ PROXY_PASSWORD, PROXY_HOST);
+
+ /**
+ * Tests that loading a basic configuration from the environment works.
+ */
+ @ParameterizedTest
+ @MethodSource("loadFromEnvironmentSupplier")
+ public void loadFromEnvironment(Configuration configuration, String expectedHost, int expectedPort,
+ String expectedUsername, String expectedPassword, String expectedNonProxyHosts) {
+ ProxyOptions proxyOptions = ProxyOptions.fromConfiguration(configuration);
+
+ assertNotNull(proxyOptions);
+ assertEquals(expectedHost, proxyOptions.getAddress().getHostName());
+ assertEquals(expectedPort, proxyOptions.getAddress().getPort());
+ assertEquals(expectedUsername, proxyOptions.getUsername());
+ assertEquals(expectedPassword, proxyOptions.getPassword());
+ assertEquals(expectedNonProxyHosts, proxyOptions.getNonProxyHosts());
+ }
+
+ private static Stream loadFromEnvironmentSupplier() {
+ return Stream.of(
+ // Basic Azure HTTPS proxy.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_HOST_ONLY),
+ PROXY_HOST, 443, null, null, null),
+
+ // Username only Azure HTTPS proxy.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_WITH_USERNAME),
+ PROXY_HOST, 443, null, null, null),
+
+ // Complete Azure HTTPS proxy.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTPS_PROXY,
+ AZURE_HTTPS_PROXY_WITH_USER_AND_PASS), PROXY_HOST, 443, PROXY_USER, PROXY_PASSWORD, null),
+
+ // Azure HTTPS proxy with non-proxying hosts.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_HOST_ONLY)
+ .put(Configuration.PROPERTY_NO_PROXY, NON_PROXY_HOSTS), PROXY_HOST, 443, null, null, NON_PROXY_HOSTS),
+
+ // Basic Azure HTTP proxy.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_HOST_ONLY),
+ PROXY_HOST, 80, null, null, null),
+
+ // Username only Azure HTTP proxy.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_WITH_USERNAME),
+ PROXY_HOST, 80, null, null, null),
+
+ // Complete Azure HTTP proxy.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTP_PROXY,
+ AZURE_HTTP_PROXY_WITH_USER_AND_PASS), PROXY_HOST, 80, PROXY_USER, PROXY_PASSWORD, null),
+
+ // Azure HTTP proxy with non-proxying hosts.
+ Arguments.of(new Configuration().put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_HOST_ONLY)
+ .put(Configuration.PROPERTY_NO_PROXY, NON_PROXY_HOSTS), PROXY_HOST, 80, null, null, NON_PROXY_HOSTS),
+
+ /*
+ * Setting up tests for loading the Java environment proxy configurations takes additional work as each
+ * piece of the proxy configuration is a separate environment value. The non-proxy hosts will be checked
+ * against the global environment value when it is not being set by the configuration passed by the test
+ * as this value may be setup by the JVM.
+ */
+
+ // Basic Java HTTPS proxy.
+ Arguments.of(createJavaConfiguration(443, null, null, null, true, true),
+ PROXY_HOST, 443, null, null, getJavaNonProxyHosts()),
+
+ // Username only Java HTTPS proxy.
+ Arguments.of(createJavaConfiguration(443, PROXY_USER, null, null, true, true),
+ PROXY_HOST, 443, null, null, getJavaNonProxyHosts()),
+
+ // Complete Java HTTPS proxy.
+ Arguments.of(createJavaConfiguration(443, PROXY_USER, PROXY_PASSWORD, null, true, true),
+ PROXY_HOST, 443, PROXY_USER, PROXY_PASSWORD, getJavaNonProxyHosts()),
+
+ // Java HTTPS proxy with non-proxying hosts.
+ Arguments.of(createJavaConfiguration(443, null, null, NON_PROXY_HOSTS, true, true),
+ PROXY_HOST, 443, null, null, NON_PROXY_HOSTS),
+
+ // Basic Java HTTP proxy.
+ Arguments.of(createJavaConfiguration(80, null, null, null, false, true),
+ PROXY_HOST, 80, null, null, getJavaNonProxyHosts()),
+
+ // Username only Java HTTP proxy.
+ Arguments.of(createJavaConfiguration(80, PROXY_USER, null, null, false, true),
+ PROXY_HOST, 80, null, null, getJavaNonProxyHosts()),
+
+ // Complete Java HTTP proxy.
+ Arguments.of(createJavaConfiguration(80, PROXY_USER, PROXY_PASSWORD, null, false, true),
+ PROXY_HOST, 80, PROXY_USER, PROXY_PASSWORD, getJavaNonProxyHosts()),
+
+ // Java HTTP proxy with non-proxying hosts.
+ Arguments.of(createJavaConfiguration(80, null, null, NON_PROXY_HOSTS, false, true),
+ PROXY_HOST, 80, null, null, NON_PROXY_HOSTS)
+ );
+ }
+
+ /**
+ * Tests that passing {@link Configuration#NONE} into {@link ProxyOptions#fromConfiguration(Configuration)}
+ * will throw an {@link IllegalArgumentException}.
+ */
+ @Test
+ public void loadFromEnvironmentThrowsWhenPassedConfigurationNone() {
+ assertThrows(IllegalArgumentException.class, () -> ProxyOptions.fromConfiguration(Configuration.NONE));
+ }
+
+ /**
+ * Tests that when Java system proxies will only be used if {@code java.net.useSystemProxies} is {@code true}.
+ */
+ @ParameterizedTest
+ @MethodSource("javaProxiesRequireUseSystemProxiesSupplier")
+ public void javaProxiesRequireUseSystemProxies(Configuration configuration) {
+ assertNull(ProxyOptions.fromConfiguration(configuration));
+ }
+
+ private static Stream javaProxiesRequireUseSystemProxiesSupplier() {
+ return Stream.of(
+ // Java HTTPS configuration without 'java.net.useSystemProxies' set.
+ Arguments.of(createJavaConfiguration(443, null, null, null, true, false)),
+
+ // Java HTTP configuration without 'java.net.useSystemProxies' set.
+ Arguments.of(createJavaConfiguration(80, null, null, null, false, false))
+ );
+ }
+
+ private static Configuration createJavaConfiguration(int port, String username, String password,
+ String nonProxyHosts, boolean isHttps, boolean enabled) {
+ Configuration configuration = new Configuration().put(JAVA_PROXY_PREREQUISITE, String.valueOf(enabled));
+ putIfNotNull(configuration, JAVA_NON_PROXY_HOSTS, nonProxyHosts);
+
+ if (isHttps) {
+ configuration.put(JAVA_HTTPS_PROXY_HOST, PROXY_HOST).put(JAVA_HTTPS_PROXY_PORT, String.valueOf(port));
+ configuration = putIfNotNull(configuration, JAVA_HTTPS_PROXY_USER, username);
+ configuration = putIfNotNull(configuration, JAVA_HTTPS_PROXY_PASSWORD, password);
+ } else {
+ configuration.put(JAVA_HTTP_PROXY_HOST, PROXY_HOST).put(JAVA_HTTP_PROXY_PORT, String.valueOf(port));
+ configuration = putIfNotNull(configuration, JAVA_HTTP_PROXY_USER, username);
+ configuration = putIfNotNull(configuration, JAVA_HTTP_PROXY_PASSWORD, password);
+ }
+
+ return configuration;
+ }
+
+ private static Configuration putIfNotNull(Configuration configuration, String name, String value) {
+ /*
+ * If the passed value is null attempt to use the global configuration value. This is done as the Configuration
+ * object will attempt to load the environment if it has no value for a given name.
+ */
+ if (value == null) {
+ value = Configuration.getGlobalConfiguration().get(name);
+ }
+
+ return CoreUtils.isNullOrEmpty(value)
+ ? configuration
+ : configuration.put(name, value);
+ }
+
+ private static String getJavaNonProxyHosts() {
+ return Configuration.getGlobalConfiguration().get(JAVA_NON_PROXY_HOSTS);
+ }
+}