com.azure.core.http.ProxyOptions Maven / Gradle / Ivy
Show all versions of azure-core Show documentation
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.core.http;
import com.azure.core.implementation.ImplUtils;
import com.azure.core.util.Configuration;
import com.azure.core.util.ConfigurationProperty;
import com.azure.core.util.ConfigurationPropertyBuilder;
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.Proxy;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* 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_AZURE_PROXY_URL = "URL is invalid and is being ignored.";
/*
* This indicates whether system proxy configurations (HTTPS_PROXY, HTTP_PROXY) are allowed to be used.
*/
private static final String JAVA_SYSTEM_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;
/*
* The 'http.nonProxyHosts' system property is expected to be delimited by '|', but don't split escaped '|'s.
*/
private static final Pattern HTTP_NON_PROXY_HOSTS_SPLIT = Pattern.compile("(? NON_PROXY_PROPERTY = ConfigurationPropertyBuilder.ofString(ConfigurationProperties.HTTP_PROXY_NON_PROXY_HOSTS)
.shared(true)
.logValue(true)
.build();
private static final ConfigurationProperty HOST_PROPERTY = ConfigurationPropertyBuilder.ofString(ConfigurationProperties.HTTP_PROXY_HOST)
.shared(true)
.logValue(true)
.build();
private static final ConfigurationProperty PORT_PROPERTY = ConfigurationPropertyBuilder.ofInteger(ConfigurationProperties.HTTP_PROXY_PORT)
.shared(true)
.defaultValue(DEFAULT_HTTPS_PORT)
.build();
private static final ConfigurationProperty USER_PROPERTY = ConfigurationPropertyBuilder.ofString(ConfigurationProperties.HTTP_PROXY_USER)
.shared(true)
.logValue(true)
.build();
private static final ConfigurationProperty PASSWORD_PROPERTY = ConfigurationPropertyBuilder.ofString(ConfigurationProperties.HTTP_PROXY_PASSWORD)
.shared(true)
.build();
private final InetSocketAddress address;
private final Type type;
private String username;
private String password;
private String nonProxyHosts;
/**
* Creates ProxyOptions.
*
* @param type the proxy type
* @param address the proxy address (ip and port number)
*/
public ProxyOptions(Type type, InetSocketAddress address) {
this.type = type;
this.address = address;
}
/**
* Set the proxy credentials.
*
* @param username proxy user name
* @param password proxy password
* @return the updated ProxyOptions object
*/
public ProxyOptions setCredentials(String username, String password) {
this.username = Objects.requireNonNull(username, "'username' cannot be null.");
this.password = Objects.requireNonNull(password, "'password' cannot be null.");
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 = sanitizeJavaHttpNonProxyHosts(nonProxyHosts);
return this;
}
/**
* Gets the address of the proxy.
*
* @return the address of the proxy.
*/
public InetSocketAddress getAddress() {
return address;
}
/**
* Gets the type of the prxoy.
*
* @return the type of the proxy.
*/
public Type getType() {
return type;
}
/**
* Gets the proxy username.
*
* @return the proxy username.
*/
public String getUsername() {
return this.username;
}
/**
* Gets the proxy password.
*
* @return the proxy password.
*/
public String getPassword() {
return this.password;
}
/**
* Gets the host that bypass the proxy.
*
* @return the hosts that bypass the proxy.
*/
public String getNonProxyHosts() {
return this.nonProxyHosts;
}
/**
* Attempts to load a proxy from the configuration.
*
* If a proxy is found and loaded the proxy address is DNS resolved.
*
* 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.
* @return A {@link ProxyOptions} reflecting a proxy loaded from the environment, if no proxy is found {@code null}
* will be returned.
*/
public static ProxyOptions fromConfiguration(Configuration configuration) {
return fromConfiguration(configuration, false);
}
/**
* Attempts to load a proxy from the environment.
*
* If a proxy is found and loaded, the proxy address is DNS resolved based on {@code createUnresolved}. When {@code
* createUnresolved} is true resolving {@link #getAddress()} may be required before using the address in network
* calls.
*
* 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.
* @param createUnresolved Flag determining whether the returned {@link ProxyOptions} is unresolved.
* @return A {@link ProxyOptions} reflecting a proxy loaded from the environment, if no proxy is found {@code null}
* will be returned.
*/
public static ProxyOptions fromConfiguration(Configuration configuration, boolean createUnresolved) {
Configuration proxyConfiguration = (configuration == null)
? Configuration.getGlobalConfiguration()
: configuration;
return attemptToLoadProxy(proxyConfiguration, createUnresolved);
}
private static ProxyOptions attemptToLoadProxy(Configuration configuration, boolean createUnresolved) {
if (configuration == Configuration.NONE) {
return null;
}
ProxyOptions proxyOptions = null;
// System proxy configuration is only possible through system properties.
// Only use system proxies when the prerequisite property is 'true'.
if (Boolean.parseBoolean(configuration.get(JAVA_SYSTEM_PROXY_PREREQUISITE))) {
proxyOptions = attemptToLoadSystemProxy(configuration, createUnresolved, Configuration.PROPERTY_HTTPS_PROXY);
if (proxyOptions != null) {
LOGGER.verbose("Using proxy created from HTTPS_PROXY environment variable.");
return proxyOptions;
}
proxyOptions = attemptToLoadSystemProxy(configuration, createUnresolved, Configuration.PROPERTY_HTTP_PROXY);
if (proxyOptions != null) {
LOGGER.verbose("Using proxy created from HTTP_PROXY environment variable.");
return proxyOptions;
}
}
proxyOptions = attemptToLoadAzureSdkProxy(configuration, createUnresolved);
if (proxyOptions != null) {
return proxyOptions;
}
proxyOptions = attemptToLoadJavaProxy(configuration, createUnresolved, HTTPS);
if (proxyOptions != null) {
LOGGER.verbose("Using proxy created from JVM HTTPS system properties.");
return proxyOptions;
}
proxyOptions = attemptToLoadJavaProxy(configuration, createUnresolved, HTTP);
if (proxyOptions != null) {
LOGGER.verbose("Using proxy created from JVM HTTP system properties.");
return proxyOptions;
}
return null;
}
private static ProxyOptions attemptToLoadSystemProxy(Configuration configuration, boolean createUnresolved,
String proxyProperty) {
String proxyConfiguration = configuration.get(proxyProperty);
// No proxy configuration setup.
if (CoreUtils.isNullOrEmpty(proxyConfiguration)) {
return null;
}
try {
// TODO (alzimmer): UrlBuilder needs to add support for userinfo
// https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1
URL proxyUrl = ImplUtils.createUrl(proxyConfiguration);
int port = (proxyUrl.getPort() == -1) ? proxyUrl.getDefaultPort() : proxyUrl.getPort();
InetSocketAddress socketAddress = (createUnresolved)
? InetSocketAddress.createUnresolved(proxyUrl.getHost(), port)
: new InetSocketAddress(proxyUrl.getHost(), port);
ProxyOptions proxyOptions = new ProxyOptions(ProxyOptions.Type.HTTP, socketAddress);
String nonProxyHostsString = configuration.get(Configuration.PROPERTY_NO_PROXY);
if (!CoreUtils.isNullOrEmpty(nonProxyHostsString)) {
proxyOptions.nonProxyHosts = sanitizeNoProxy(nonProxyHostsString);
LOGGER.atVerbose()
.addKeyValue("regex", proxyOptions.nonProxyHosts)
.log("Using non-proxy hosts");
}
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.atWarning()
.addKeyValue("url", proxyProperty)
.log(INVALID_AZURE_PROXY_URL);
return null;
}
}
/*
* Helper function that sanitizes 'NO_PROXY' into a Pattern safe string.
*/
static String sanitizeNoProxy(String noProxyString) {
return sanitizeNonProxyHosts(NO_PROXY_SPLIT.split(noProxyString));
}
private static ProxyOptions attemptToLoadJavaProxy(Configuration configuration, boolean createUnresolved,
String type) {
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;
}
String nonProxyHostsString = configuration.get(JAVA_NON_PROXY_HOSTS);
String username = configuration.get(type + "." + JAVA_PROXY_USER);
String password = configuration.get(type + "." + JAVA_PROXY_PASSWORD);
return createOptions(host, port, nonProxyHostsString, username, password, createUnresolved);
}
private static ProxyOptions attemptToLoadAzureSdkProxy(Configuration configuration, boolean createUnresolved) {
String host = configuration.get(HOST_PROPERTY);
// No proxy configuration setup.
if (CoreUtils.isNullOrEmpty(host)) {
return null;
}
int port = configuration.get(PORT_PROPERTY);
String nonProxyHostsString = configuration.get(NON_PROXY_PROPERTY);
String username = configuration.get(USER_PROPERTY);
String password = configuration.get(PASSWORD_PROPERTY);
return createOptions(host, port, nonProxyHostsString, username, password, createUnresolved);
}
private static ProxyOptions createOptions(String host, int port, String nonProxyHostsString, String username, String password, boolean createUnresolved) {
InetSocketAddress socketAddress = (createUnresolved)
? InetSocketAddress.createUnresolved(host, port)
: new InetSocketAddress(host, port);
ProxyOptions proxyOptions = new ProxyOptions(ProxyOptions.Type.HTTP, socketAddress);
if (!CoreUtils.isNullOrEmpty(nonProxyHostsString)) {
proxyOptions.nonProxyHosts = sanitizeJavaHttpNonProxyHosts(nonProxyHostsString);
LOGGER.atVerbose()
.addKeyValue("regex", proxyOptions.nonProxyHosts)
.log("Using non-proxy host regex");
}
if (username != null && password != null) {
proxyOptions.setCredentials(username, password);
}
return proxyOptions;
}
/*
* Helper function that sanitizes 'http.nonProxyHosts' into a Pattern safe string.
*/
static String sanitizeJavaHttpNonProxyHosts(String nonProxyHostsString) {
return sanitizeNonProxyHosts(HTTP_NON_PROXY_HOSTS_SPLIT.split(nonProxyHostsString));
}
private static String sanitizeNonProxyHosts(String[] nonProxyHosts) {
StringBuilder sanitizedBuilder = new StringBuilder();
for (int i = 0; i < nonProxyHosts.length; i++) {
if (i > 0) {
sanitizedBuilder.append("|");
}
String prefixWildcard = "";
String suffixWildcard = "";
String sanitizedNonProxyHost = nonProxyHosts[i];
/*
* If the non-proxy host begins with either '.', '*', '.*', or any of the previous with a trailing '?'
* substring the non-proxy host and set the wildcard prefix.
*/
if (sanitizedNonProxyHost.startsWith(".")) {
prefixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(1);
} else if (sanitizedNonProxyHost.startsWith(".?")) {
prefixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(2);
} else if (sanitizedNonProxyHost.startsWith("*?")) {
prefixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(2);
} else if (sanitizedNonProxyHost.startsWith("*")) {
prefixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(1);
} else if (sanitizedNonProxyHost.startsWith(".*?")) {
prefixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(3);
} else if (sanitizedNonProxyHost.startsWith(".*")) {
prefixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(2);
}
/*
* Same with the ending of the non-proxy host, if it has a suffix wildcard trim the non-proxy host and
* retain the suffix wildcard.
*/
if (sanitizedNonProxyHost.endsWith(".")) {
suffixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(0, sanitizedNonProxyHost.length() - 2);
} else if (sanitizedNonProxyHost.endsWith(".?")) {
suffixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(0, sanitizedNonProxyHost.length() - 3);
} else if (sanitizedNonProxyHost.endsWith("*?")) {
suffixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(0, sanitizedNonProxyHost.length() - 3);
} else if (sanitizedNonProxyHost.endsWith("*")) {
suffixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(0, sanitizedNonProxyHost.length() - 2);
} else if (sanitizedNonProxyHost.endsWith(".*?")) {
suffixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(0, sanitizedNonProxyHost.length() - 4);
} else if (sanitizedNonProxyHost.endsWith(".*")) {
suffixWildcard = ".*?";
sanitizedNonProxyHost = sanitizedNonProxyHost.substring(0, sanitizedNonProxyHost.length() - 3);
}
try {
// Sanitize the non-proxy host before any appending to prevent errant characters being added to the
// final response if the non-proxy host isn't a valid Pattern.
String attemptToSanitizeAsRegex = sanitizedNonProxyHost;
attemptToSanitizeAsRegex = UNESCAPED_PERIOD.matcher(attemptToSanitizeAsRegex).replaceAll("\\\\.");
attemptToSanitizeAsRegex = ANY.matcher(attemptToSanitizeAsRegex).replaceAll("\\.*?");
sanitizedNonProxyHost = Pattern.compile(attemptToSanitizeAsRegex).pattern();
} catch (PatternSyntaxException ex) {
/*
* Replace the non-proxy host with the sanitized value.
*
* The body of the non-proxy host is quoted to handle scenarios such a '127.0.0.1' or '*.azure.com'
* where without quoting the '.' in the string would be treated as the match any character instead of
* the literal '.' character.
*/
sanitizedNonProxyHost = Pattern.quote(sanitizedNonProxyHost);
}
sanitizedBuilder.append("(")
.append(prefixWildcard)
.append(sanitizedNonProxyHost)
.append(suffixWildcard)
.append(")");
}
return sanitizedBuilder.toString();
}
/**
* The type of the proxy.
*/
public enum Type {
/**
* HTTP proxy type.
*/
HTTP(Proxy.Type.HTTP),
/**
* SOCKS4 proxy type.
*/
SOCKS4(Proxy.Type.SOCKS),
/**
* SOCKS5 proxy type.
*/
SOCKS5(Proxy.Type.SOCKS);
private final Proxy.Type proxyType;
Type(Proxy.Type proxyType) {
this.proxyType = proxyType;
}
/**
* Get the {@link Proxy.Type} equivalent of this type.
*
* @return the proxy type
*/
public Proxy.Type toProxyType() {
return proxyType;
}
}
/**
* Lists available configuration property names for HTTP {@link ProxyOptions}.
*/
private static class ConfigurationProperties {
/**
* Represents a list of hosts that should be reached directly, bypassing the proxy.
* This is a list of patterns separated by '|'. The patterns may start or end with a '*' for wildcards.
* Any host matching one of these patterns will be reached through a direct connection instead of through a proxy.
*
* Default value is {@code null}
*/
public static final String HTTP_PROXY_NON_PROXY_HOSTS = "http.proxy.non-proxy-hosts";
/**
* The HTTP host name of the proxy server.
*
* Default value is {@code null}.
*/
public static final String HTTP_PROXY_HOST = "http.proxy.hostname";
/**
* The port number of the proxy server.
*
* Default value is {@code 443}.
*/
public static final String HTTP_PROXY_PORT = "http.proxy.port";
/**
* The HTTP proxy server user.
* Default value is {@code null}.
*/
public static final String HTTP_PROXY_USER = "http.proxy.username";
/**
* The HTTP proxy server password.
* Default value is {@code null}.
*/
public static final String HTTP_PROXY_PASSWORD = "http.proxy.password";
}
}