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: + *

    + *
  1. Azure HTTPS
  2. + *
  3. Azure HTTP
  4. + *
  5. Java HTTPS
  6. + *
  7. Java HTTP
  8. + *
+ * + * 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); + } +}