Proxy support for pull, push and mirror commands (#1773)

Apply proxy support for jGit by extracting the required functionality from the DefaultAdvancedHttpClient into its own class HttpURLConnectionFactory. This new class is now used by the DefaultAdvancedHttpClient and jGit.
The HttpURLConnection also fixes proxy server authentication, which was non functional in DefaultAdvancedHttpClient.
The proxy support for SVNKit is implemented by using the provided method of the BasicAuthenticationManager.
For mercurial the support is configured by writing the required settings to a temporary hgrc file.
This commit is contained in:
Sebastian Sdorra
2021-08-19 11:27:51 +02:00
committed by GitHub
parent a7bb67f36b
commit 7f9f4e566c
54 changed files with 2996 additions and 1098 deletions

View File

@@ -21,83 +21,61 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
//~--- JDK imports ------------------------------------------------------------
import com.google.common.collect.ImmutableList;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Configuration in the ini format.
* The format consists of sections, keys and values.
*
* @author Sebastian Sdorra
* @see <a href="https://en.wikipedia.org/wiki/INI_file">Wikipedia article</a>
*/
public class INIConfiguration
{
public class INIConfiguration {
private final Map<String, INISection> sectionMap = new LinkedHashMap<>();
/**
* Constructs ...
*
* Add a new section to the configuration.
* @param section section
*/
public INIConfiguration()
{
this.sectionMap = new LinkedHashMap<>();
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param section
*/
public void addSection(INISection section)
{
public void addSection(INISection section) {
sectionMap.put(section.getName(), section);
}
/**
* Method description
*
*
* @param name
* Remove an existing section from the configuration.
* @param name name of the section
*/
public void removeSection(String name)
{
public void removeSection(String name) {
sectionMap.remove(name);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param name
*
* @return
* Returns a section by its name or {@code null} if the section does not exists.
* @param name name of the section
* @return section or null
*/
public INISection getSection(String name)
{
@Nullable
public INISection getSection(String name) {
return sectionMap.get(name);
}
/**
* Method description
*
*
* @return
* Returns all sections of the configuration.
* @return all sections
*/
public Collection<INISection> getSections()
{
return sectionMap.values();
public Collection<INISection> getSections() {
return ImmutableList.copyOf(sectionMap.values());
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private Map<String, INISection> sectionMap;
}

View File

@@ -21,83 +21,52 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* Read configuration in ini format from files and streams.
* @author Sebastian Sdorra
*/
public class INIConfigurationReader extends AbstractReader<INIConfiguration>
{
public class INIConfigurationReader extends AbstractReader<INIConfiguration> {
/** Field description */
private static final Pattern sectionPattern =
Pattern.compile("\\[([^\\]]+)\\]");
private static final Pattern sectionPattern = Pattern.compile("\\[([^]]+)]");
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param input
*
* @return
*
* @throws IOException
*/
@Override
public INIConfiguration read(InputStream input) throws IOException
{
public INIConfiguration read(InputStream input) throws IOException {
INIConfiguration configuration = new INIConfiguration();
try
{
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
INISection section = null;
String line = reader.readLine();
while (line != null)
{
while (line != null) {
line = line.trim();
Matcher sectionMatcher = sectionPattern.matcher(line);
if (sectionMatcher.matches())
{
if (sectionMatcher.matches()) {
String name = sectionMatcher.group(1);
if (section != null)
{
if (section != null) {
configuration.addSection(section);
}
section = new INISection(name);
}
else if ((section != null) &&!line.startsWith(";")
&&!line.startsWith("#"))
{
} else if ((section != null) && !line.startsWith(";") && !line.startsWith("#")) {
int index = line.indexOf('=');
if (index > 0)
{
if (index > 0) {
String key = line.substring(0, index).trim();
String value = line.substring(index + 1, line.length()).trim();
String value = line.substring(index + 1).trim();
section.setParameter(key, value);
}
@@ -106,15 +75,10 @@ public class INIConfigurationReader extends AbstractReader<INIConfiguration>
line = reader.readLine();
}
if (section != null)
{
if (section != null) {
configuration.addSection(section);
}
}
finally
{
IOUtil.close(input);
}
return configuration;
}

View File

@@ -21,55 +21,25 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
/**
*
* Write configurations in ini format to file and streams.
* @author Sebastian Sdorra
*/
public class INIConfigurationWriter extends AbstractWriter<INIConfiguration>
{
public class INIConfigurationWriter extends AbstractWriter<INIConfiguration> {
/**
* Method description
*
*
* @param object
* @param output
*
* @throws IOException
*/
@Override
public void write(INIConfiguration object, OutputStream output)
throws IOException
{
PrintWriter writer = null;
try
{
writer = new PrintWriter(output);
for (INISection section : object.getSections())
{
public void write(INIConfiguration object, OutputStream output) throws IOException {
try (PrintWriter writer = new PrintWriter(output)) {
for (INISection section : object.getSections()) {
writer.println(section.toString());
}
writer.flush();
}
finally
{
IOUtil.close(writer);
}
}
}

View File

@@ -21,76 +21,98 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
//~--- JDK imports ------------------------------------------------------------
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A section of {@link INIConfiguration}.
* The section consists of keys and values.
*
* @author Sebastian Sdorra
*/
public class INISection
{
public class INISection {
private final String name;
private final Map<String, String> parameters;
/**
* Constructs ...
*
*
* @param name
* Constructs a new empty section with the given name.
* @param name name of the section
*/
public INISection(String name)
{
public INISection(String name) {
this.name = name;
this.parameters = new LinkedHashMap<>();
}
/**
* Constructs ...
* Constructs a new section with the given name and parameters.
*
*
* @param name
* @param parameters
* @param name name of the section
* @param initialParameters initial parameter
*/
public INISection(String name, Map<String, String> parameters)
{
public INISection(String name, Map<String, String> initialParameters) {
this.name = name;
this.parameters = parameters;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param key
*/
public void removeParameter(String key)
{
parameters.put(key, name);
this.parameters = new LinkedHashMap<>(initialParameters);
}
/**
* Method description
*
*
* @return
* Returns the name of the section.
* @return name of section
*/
public String getName() {
return name;
}
/**
* Returns the value of the parameter with the given key or {@code null} if the given parameter does not exist.
* @param key key of parameter
* @return value of parameter or {@code null}
*/
public String getParameter(String key) {
return parameters.get(key);
}
/**
* Returns all parameter keys of the section.
* @return all parameters of section
*/
public Collection<String> getParameterKeys() {
return ImmutableList.copyOf(parameters.keySet());
}
/**
* Sets the parameter with the given key to the given value.
* @param key key of parameter
* @param value value of parameter
*/
public void setParameter(String key, String value) {
parameters.put(key, value);
}
/**
* Remove parameter with the given name from the section.
* @param key name of parameter
*/
public void removeParameter(String key) {
parameters.remove(key);
}
@Override
public String toString()
{
public String toString() {
String s = System.getProperty("line.separator");
StringBuilder out = new StringBuilder();
out.append("[").append(name).append("]").append(s);
for (Map.Entry<String, String> entry : parameters.entrySet())
{
for (Map.Entry<String, String> entry : parameters.entrySet()) {
out.append(entry.getKey()).append(" = ").append(entry.getValue());
out.append(s);
}
@@ -98,62 +120,4 @@ public class INISection
return out.toString();
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getName()
{
return name;
}
/**
* Method description
*
*
* @param key
*
* @return
*/
public String getParameter(String key)
{
return parameters.get(key);
}
/**
* Method description
*
*
* @return
*/
public Collection<String> getParameterKeys()
{
return parameters.keySet();
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param key
* @param value
*/
public void setParameter(String key, String value)
{
parameters.put(key, value);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String name;
/** Field description */
private Map<String, String> parameters;
}

View File

@@ -0,0 +1,89 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.net;
import com.google.common.base.Strings;
import sonia.scm.config.ScmConfiguration;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
/**
* The {@link GlobalProxyConfiguration} is an adapter between {@link ProxyConfiguration} and {@link ScmConfiguration}.
* Whenever proxy settings are required, the {@link GlobalProxyConfiguration} should be used instead of using
* {@link ScmConfiguration} directly. This makes it easier to support local proxy configurations.
*
* @since 2.23.0
*/
public final class GlobalProxyConfiguration implements ProxyConfiguration {
private final ScmConfiguration configuration;
@Inject
public GlobalProxyConfiguration(ScmConfiguration configuration) {
this.configuration = configuration;
}
@Override
public boolean isEnabled() {
return configuration.isEnableProxy();
}
@Override
public String getHost() {
return configuration.getProxyServer();
}
@Override
public int getPort() {
return configuration.getProxyPort();
}
@Override
public Collection<String> getExcludes() {
Set<String> excludes = configuration.getProxyExcludes();
if (excludes == null) {
return Collections.emptyList();
}
return excludes;
}
@Override
public String getUsername() {
return configuration.getProxyUser();
}
@Override
public String getPassword() {
return configuration.getProxyPassword();
}
@Override
public boolean isAuthenticationRequired() {
return !Strings.isNullOrEmpty(getUsername()) && !Strings.isNullOrEmpty(getPassword());
}
}

View File

@@ -0,0 +1,165 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.net;
import com.google.common.annotations.VisibleForTesting;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import javax.annotation.Nullable;
import javax.net.ssl.KeyManager;
import java.net.URL;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Options for establishing a http connection.
* The options can be used to create a new http connection
* with {@link HttpURLConnectionFactory#create(URL, HttpConnectionOptions)}.
*
* @since 2.23.0
*/
@Getter
@ToString
@EqualsAndHashCode
public final class HttpConnectionOptions {
@VisibleForTesting
static final int DEFAULT_CONNECTION_TIMEOUT = 30000;
@VisibleForTesting
static final int DEFAULT_READ_TIMEOUT = 1200000;
@Nullable
private ProxyConfiguration proxyConfiguration;
@Nullable
private KeyManager[] keyManagers;
private boolean disableCertificateValidation = false;
private boolean disableHostnameValidation = false;
private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
private int readTimeout = DEFAULT_READ_TIMEOUT;
private boolean ignoreProxySettings = false;
/**
* Returns optional local proxy configuration.
* @return local proxy configuration or empty optional
*/
public Optional<ProxyConfiguration> getProxyConfiguration() {
return Optional.ofNullable(proxyConfiguration);
}
/**
* Return optional array of key managers for client certificate authentication.
*
* @return array of key managers or empty optional
*/
public Optional<KeyManager[]> getKeyManagers() {
if (keyManagers != null) {
return Optional.of(Arrays.copyOf(keyManagers, keyManagers.length));
}
return Optional.empty();
}
/**
* Disable certificate validation.
* <b>WARNING:</b> This option should only be used for internal test.
* It should never be used in production, because it is high security risk.
*
* @return {@code this}
*/
public HttpConnectionOptions withDisableCertificateValidation() {
this.disableCertificateValidation = true;
return this;
}
/**
* Disable hostname validation.
* <b>WARNING:</b> This option should only be used for internal test.
* It should never be used in production, because it is high security risk.
*
* @return {@code this}
*/
public HttpConnectionOptions withDisabledHostnameValidation() {
this.disableHostnameValidation = true;
return this;
}
/**
* Configure the connection timeout.
* @param timeout timeout
* @param unit unit of the timeout
* @return {@code this}
*/
public HttpConnectionOptions withConnectionTimeout(long timeout, TimeUnit unit) {
this.connectionTimeout = (int) unit.toMillis(timeout);
return this;
}
/**
* Configure the read timeout.
* @param timeout timeout
* @param unit unit of the timeout
* @return {@code this}
*/
public HttpConnectionOptions withReadTimeout(long timeout, TimeUnit unit) {
this.readTimeout = (int) unit.toMillis(timeout);
return this;
}
/**
* Configure a local proxy configuration, if no configuration is set the global default configuration will be used.
* @param proxyConfiguration local proxy configuration
* @return {@code this}
*/
public HttpConnectionOptions withProxyConfiguration(ProxyConfiguration proxyConfiguration) {
this.proxyConfiguration = proxyConfiguration;
return this;
}
/**
* Configure key managers for client certificate authentication.
* @param keyManagers key managers
* @return {@code this}
*/
public HttpConnectionOptions withKeyManagers(@Nullable KeyManager... keyManagers) {
if (keyManagers != null) {
this.keyManagers = Arrays.copyOf(keyManagers, keyManagers.length);
}
return this;
}
/**
* Ignore proxy settings completely regardless if a local proxy configuration or a global configuration is configured.
* @return {@code this}
*/
public HttpConnectionOptions withIgnoreProxySettings() {
this.ignoreProxySettings = true;
return this;
}
}

View File

@@ -0,0 +1,345 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.net;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Collection;
/**
* The {@link HttpURLConnectionFactory} simplifies the correct configuration of {@link HttpURLConnection}.
* It sets timeout, proxy, ssl and authentication configurations to provide better defaults and respect SCM-Manager
* settings.
* <b>Note:</b> This class should only be used if a third party library requires an {@link HttpURLConnection}.
* In all other cases the {@link sonia.scm.net.ahc.AdvancedHttpClient} should be used.
*/
public final class HttpURLConnectionFactory {
private static final Logger LOG = LoggerFactory.getLogger(HttpURLConnectionFactory.class);
static {
// Allow basic authentication for proxies
// https://stackoverflow.com/a/1626616
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
// Set the default authenticator to our thread local authenticator
Authenticator.setDefault(new ThreadLocalAuthenticator());
}
private final GlobalProxyConfiguration globalProxyConfiguration;
private final Provider<TrustManager> trustManagerProvider;
private final Connector connector;
private final SSLContextFactory sslContextFactory;
@Inject
public HttpURLConnectionFactory(GlobalProxyConfiguration globalProxyConfiguration, Provider<TrustManager> trustManagerProvider) {
this(globalProxyConfiguration, trustManagerProvider, new DefaultConnector(), new DefaultSSLContextFactory());
}
@VisibleForTesting
public HttpURLConnectionFactory(GlobalProxyConfiguration globalProxyConfiguration, Provider<TrustManager> trustManagerProvider, Connector connector, SSLContextFactory sslContextFactory) {
this.globalProxyConfiguration = globalProxyConfiguration;
this.trustManagerProvider = trustManagerProvider;
this.connector = connector;
this.sslContextFactory = sslContextFactory;
}
/**
* Creates a new {@link HttpURLConnection} from the given url with default options.
* @param url url
* @return a new connection with default options.
* @throws IOException
*/
public HttpURLConnection create(URL url) throws IOException {
return create(url, new HttpConnectionOptions());
}
/**
* Creates a new {@link HttpURLConnection} from the given url and options.
* @param url url
* @param options options for the new connection
* @return a new connection with the given options
* @throws IOException
*/
public HttpURLConnection create(URL url, HttpConnectionOptions options) throws IOException {
Preconditions.checkArgument(options != null, "Options are required");
return new InternalConnectionFactory(options).create(url);
}
private class InternalConnectionFactory {
private final HttpConnectionOptions options;
private InternalConnectionFactory(HttpConnectionOptions options) {
this.options = options;
}
HttpURLConnection create(URL url) throws IOException {
// clear authentication this is required,
// because we are not able to remove the authentication from thread local
ThreadLocalAuthenticator.clear();
ProxyConfiguration proxyConfiguration = options.getProxyConfiguration().orElse(globalProxyConfiguration);
if (isProxyEnabled(proxyConfiguration, url)) {
return openProxyConnection(proxyConfiguration, url);
}
return configure(connector.connect(url, null));
}
private boolean isProxyEnabled(ProxyConfiguration proxyConfiguration, URL url) {
return !options.isIgnoreProxySettings()
&& proxyConfiguration.isEnabled()
&& !isHostExcluded(proxyConfiguration, url);
}
private boolean isHostExcluded(ProxyConfiguration proxyConfiguration, URL url) {
Collection<String> excludes = proxyConfiguration.getExcludes();
if (excludes == null) {
return false;
}
return excludes.contains(url.getHost());
}
private HttpURLConnection openProxyConnection(ProxyConfiguration configuration, URL url) throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug(
"open connection to '{}' using proxy {}:{}",
url.toExternalForm(), configuration.getHost(), configuration.getPort()
);
}
SocketAddress address = new InetSocketAddress(configuration.getHost(), configuration.getPort());
HttpURLConnection connection = configure(connector.connect(url, new Proxy(Proxy.Type.HTTP, address)));
if (configuration.isAuthenticationRequired()) {
// Set the authentication for the proxy server for the current thread.
// This becomes obsolete with java 9,
// because the HttpURLConnection of java 9 has a setAuthenticator method
// which makes it possible to set a proxy authentication for a single request.
ThreadLocalAuthenticator.set(configuration);
}
return connection;
}
private HttpURLConnection configure(URLConnection urlConnection) {
if (!(urlConnection instanceof HttpURLConnection)) {
throw new IllegalArgumentException("only http(s) urls are supported");
}
HttpURLConnection connection = (HttpURLConnection) urlConnection;
applyBaseSettings(connection);
if (connection instanceof HttpsURLConnection) {
applySSLSettings((HttpsURLConnection) connection);
}
return connection;
}
private void applySSLSettings(HttpsURLConnection connection) {
connection.setSSLSocketFactory(createSSLContext().getSocketFactory());
if (options.isDisableHostnameValidation()) {
disableHostnameVerification(connection);
}
}
private SSLContext createSSLContext() {
return createSSLContext(createTrustManager(), options.getKeyManagers().orElse(null));
}
private TrustManager createTrustManager() {
if (options.isDisableCertificateValidation()) {
LOG.warn("certificate validation is disabled");
return new TrustAllTrustManager();
}
return trustManagerProvider.get();
}
private SSLContext createSSLContext(TrustManager trustManager, KeyManager[] keyManagers) {
try {
SSLContext sc = sslContextFactory.create();
sc.init(keyManagers, new TrustManager[]{trustManager}, null);
return sc;
} catch (KeyManagementException | NoSuchAlgorithmException ex) {
throw new IllegalStateException("failed to configure ssl context", ex);
}
}
private void disableHostnameVerification(HttpsURLConnection connection) {
LOG.trace("disable hostname validation");
connection.setHostnameVerifier(new TrustAllHostnameVerifier());
}
private void applyBaseSettings(HttpURLConnection connection) {
connection.setReadTimeout(options.getReadTimeout());
connection.setConnectTimeout(options.getConnectionTimeout());
}
}
@Value
@VisibleForTesting
static class ProxyAuthentication {
String server;
String username;
char[] password;
}
@VisibleForTesting
static class ThreadLocalAuthenticator extends Authenticator {
private static final ThreadLocal<ProxyAuthentication> AUTHENTICATION = new ThreadLocal<>();
static void set(ProxyConfiguration proxyConfiguration) {
LOG.trace("configure proxy authentication for this thread");
AUTHENTICATION.set(create(proxyConfiguration));
}
static void clear() {
LOG.trace("release proxy authentication");
AUTHENTICATION.remove();
}
@Nullable
static ProxyAuthentication get() {
return AUTHENTICATION.get();
}
@Nonnull
private static ProxyAuthentication create(ProxyConfiguration proxyConfiguration) {
return new ProxyAuthentication(
proxyConfiguration.getHost(),
proxyConfiguration.getUsername(),
Strings.nullToEmpty(proxyConfiguration.getPassword()).toCharArray()
);
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.PROXY) {
ProxyAuthentication authentication = get();
if (authentication != null && authentication.getServer().equals(getRequestingHost())) {
LOG.debug("use proxy authentication for host {}", authentication.getServer());
return new PasswordAuthentication(authentication.getUsername(), authentication.getPassword());
}
}
return null;
}
}
@VisibleForTesting
@FunctionalInterface
public interface Connector {
URLConnection connect(URL url, @Nullable Proxy proxy) throws IOException;
}
private static class DefaultConnector implements Connector {
@Override
public URLConnection connect(URL url, @Nullable Proxy proxy) throws IOException {
if (proxy != null) {
return url.openConnection(proxy);
}
return url.openConnection();
}
}
@VisibleForTesting
@FunctionalInterface
public interface SSLContextFactory {
SSLContext create() throws NoSuchAlgorithmException;
}
@VisibleForTesting
static class DefaultSSLContextFactory implements SSLContextFactory {
@Override
public SSLContext create() throws NoSuchAlgorithmException {
return SSLContext.getInstance("TLS");
}
}
@SuppressWarnings("java:S4830")
@VisibleForTesting
static class TrustAllTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// accept everything
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// accept everything
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
@SuppressWarnings("java:S5527")
@VisibleForTesting
static class TrustAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.net;
import java.util.Collection;
/**
* Proxy server configuration.
*
* @since 2.23.0
*/
public interface ProxyConfiguration {
/**
* Returns {@code true} if proxy configuration is enabled.
* @return {@code true} if enabled
*/
boolean isEnabled();
/**
* Return the hostname or ip address of the proxy server.
* @return proxy server hostname or ip address
*/
String getHost();
/**
* Returns port of the proxy server.
* @return port of proxy server
*/
int getPort();
/**
* Returns a list of hostnames which should not be routed over the proxy server.
* @return list of excluded hostnames
*/
Collection<String> getExcludes();
/**
* Returns the username for proxy server authentication.
* @return username for authentication
*/
String getUsername();
/**
* Returns thr password for proxy server authentication.
* @return password for authentication
*/
String getPassword();
/**
* Return {@code true} if the proxy server required authentication.
* @return {@code true} if authentication is required
*/
boolean isAuthenticationRequired();
}

View File

@@ -32,7 +32,9 @@ import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.MirrorCommand;
import sonia.scm.repository.spi.MirrorCommandRequest;
import sonia.scm.security.PublicKey;
import sonia.scm.net.ProxyConfiguration;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -57,6 +59,9 @@ public final class MirrorCommandBuilder {
private List<PublicKey> publicKeys = emptyList();
private MirrorFilter filter = new MirrorFilter() {};
@Nullable
private ProxyConfiguration proxyConfiguration;
MirrorCommandBuilder(MirrorCommand mirrorCommand, Repository targetRepository) {
this.mirrorCommand = mirrorCommand;
this.targetRepository = targetRepository;
@@ -94,6 +99,18 @@ public final class MirrorCommandBuilder {
return this;
}
/**
* Set the proxy configuration which should be used to access the source repository of the mirror.
* If not proxy configuration is set the global configuration should be used instead.
* @param proxyConfiguration proxy configuration to access the source repository
* @return {@code this}
* @since 2.23.0
*/
public MirrorCommandBuilder setProxyConfiguration(ProxyConfiguration proxyConfiguration) {
this.proxyConfiguration = proxyConfiguration;
return this;
}
public MirrorCommandResult initialCall() {
LOG.info("Creating mirror for {} in repository {}", sourceUrl, targetRepository);
MirrorCommandRequest mirrorCommandRequest = createRequest();
@@ -112,6 +129,7 @@ public final class MirrorCommandBuilder {
mirrorCommandRequest.setCredentials(credentials);
mirrorCommandRequest.setFilter(filter);
mirrorCommandRequest.setPublicKeys(publicKeys);
mirrorCommandRequest.setProxyConfiguration(proxyConfiguration);
Preconditions.checkArgument(mirrorCommandRequest.isValid(), "source url has to be specified");
return mirrorCommandRequest;
}

View File

@@ -29,7 +29,9 @@ import org.apache.commons.lang.StringUtils;
import sonia.scm.repository.api.Credential;
import sonia.scm.repository.api.MirrorFilter;
import sonia.scm.security.PublicKey;
import sonia.scm.net.ProxyConfiguration;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -49,6 +51,9 @@ public final class MirrorCommandRequest {
private List<PublicKey> publicKeys = emptyList();
private MirrorFilter filter = new MirrorFilter() {};
@Nullable
private ProxyConfiguration proxyConfiguration;
public String getSourceUrl() {
return sourceUrl;
}
@@ -92,4 +97,22 @@ public final class MirrorCommandRequest {
public List<PublicKey> getPublicKeys() {
return Collections.unmodifiableList(publicKeys);
}
/**
* Use the provided proxy configuration for the connection to the source repository.
* @param proxyConfiguration proxy configuration
* @since 2.23.0
*/
public void setProxyConfiguration(ProxyConfiguration proxyConfiguration) {
this.proxyConfiguration = proxyConfiguration;
}
/**
* Returns an optional proxy configuration which is used for the connection to the source repository.
* @return optional proxy configuration or empty
* @since 2.23.0
*/
public Optional<ProxyConfiguration> getProxyConfiguration() {
return Optional.ofNullable(proxyConfiguration);
}
}

View File

@@ -0,0 +1,74 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class INIConfigurationReaderTest {
private final INIConfigurationReader reader = new INIConfigurationReader();
@Test
void shouldReadIni(@TempDir Path directory) throws IOException {
String ini = String.join(System.getProperty("line.separator"),
"[one]",
"a = b",
"[two]",
"c = d",
"",
"[three]",
"e = f",
"",
""
);
Path file = directory.resolve("config.ini");
Files.write(file, ini.getBytes(StandardCharsets.UTF_8));
INIConfiguration configuration = reader.read(file.toFile());
INISection one = configuration.getSection("one");
assertThat(one).isNotNull();
assertThat(one.getParameter("a")).isEqualTo("b");
INISection two = configuration.getSection("two");
assertThat(two).isNotNull();
assertThat(two.getParameter("c")).isEqualTo("d");
INISection three = configuration.getSection("three");
assertThat(three).isNotNull();
assertThat(three.getParameter("e")).isEqualTo("f");
}
}

View File

@@ -0,0 +1,70 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class INIConfigurationTest {
@Test
void shouldAddAndGet() {
INIConfiguration configuration = new INIConfiguration();
INISection section = new INISection("one");
configuration.addSection(section);
assertThat(configuration.getSection("one")).isSameAs(section);
}
@Test
void shouldRemoveExistingSection() {
INIConfiguration configuration = new INIConfiguration();
INISection section = new INISection("one");
configuration.addSection(section);
configuration.removeSection("one");
assertThat(configuration.getSection("one")).isNull();
}
@Test
void shouldAllowRemoveDuringIteration() {
INIConfiguration configuration = new INIConfiguration();
configuration.addSection(new INISection("one"));
configuration.addSection(new INISection("two"));
configuration.addSection(new INISection("three"));
for (INISection section : configuration.getSections()) {
if (section.getName().startsWith("t")) {
configuration.removeSection(section.getName());
}
}
assertThat(configuration.getSections()).hasSize(1);
}
}

View File

@@ -0,0 +1,66 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class INIConfigurationWriterTest {
@Test
void shouldWriteIni(@TempDir Path directory) throws IOException {
Path path = directory.resolve("config.ini");
INIConfiguration configuration = new INIConfiguration();
INISection one = new INISection("one");
one.setParameter("a", "b");
configuration.addSection(one);
INISection two = new INISection("two");
two.setParameter("c", "d");
configuration.addSection(two);
INIConfigurationWriter writer = new INIConfigurationWriter();
writer.write(configuration, path.toFile());
String expected = String.join(System.getProperty("line.separator"),
"[one]",
"a = b",
"",
"[two]",
"c = d",
"",
""
);
assertThat(path).hasContent(expected);
}
}

View File

@@ -0,0 +1,102 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.io;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class INISectionTest {
@Test
void shouldReturnName() {
assertThat(new INISection("test").getName()).isEqualTo("test");
}
@Test
void shouldSetAndGet() {
INISection section = new INISection("section");
section.setParameter("one", "1");
assertThat(section.getParameter("one")).isEqualTo("1");
}
@Test
void shouldOverwriteExistingKey() {
INISection section = new INISection("section");
section.setParameter("one", "1");
section.setParameter("one", "2");
assertThat(section.getParameter("one")).isEqualTo("2");
}
@Test
void shouldReturnNullForNonExistingKeys() {
INISection section = new INISection("section");
assertThat(section.getParameter("one")).isNull();
}
@Test
void shouldRemoveExistingKey() {
INISection section = new INISection("section");
section.setParameter("one", "1");
section.removeParameter("one");
assertThat(section.getParameter("one")).isNull();
}
@Test
void shouldReturnSectionInIniFormat() {
INISection section = new INISection("section");
section.setParameter("one", "1");
section.setParameter("two", "2");
String expected = String.join(System.getProperty("line.separator"),
"[section]",
"one = 1",
"two = 2",
""
);
assertThat(section).hasToString(expected);
}
@Test
void shouldAllowRemoveDuringIteration() {
INISection section = new INISection("section");
section.setParameter("one", "one");
section.setParameter("two", "two");
section.setParameter("three", "three");
for (String key : section.getParameterKeys()) {
if (key.startsWith("t")) {
section.removeParameter(key);
}
}
assertThat(section.getParameterKeys()).hasSize(1);
}
}

View File

@@ -0,0 +1,101 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.net;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import lombok.Value;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import sonia.scm.config.ScmConfiguration;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class GlobalProxyConfigurationTest {
@Test
void shouldDelegateProxyConfigurationMethods() {
ScmConfiguration scmConfig = createScmConfiguration("marvin", "brainLikeAPlanet");
GlobalProxyConfiguration configuration = new GlobalProxyConfiguration(scmConfig);
assertThat(configuration.isEnabled()).isEqualTo(scmConfig.isEnableProxy());
assertThat(configuration.getHost()).isEqualTo(scmConfig.getProxyServer());
assertThat(configuration.getPort()).isEqualTo(scmConfig.getProxyPort());
assertThat(configuration.getUsername()).isEqualTo(scmConfig.getProxyUser());
assertThat(configuration.getPassword()).isEqualTo(scmConfig.getProxyPassword());
assertThat(configuration.getExcludes()).isSameAs(scmConfig.getProxyExcludes());
}
@MethodSource("createInvalidCredentials")
@ParameterizedTest(name = "shouldReturnFalseForInvalidCredentials[{index}]")
void shouldReturnFalseForInvalidCredentials(Credentials credentials) {
GlobalProxyConfiguration configuration = new GlobalProxyConfiguration(createScmConfiguration(credentials));
assertThat(configuration.isAuthenticationRequired()).isFalse();
}
@Test
void shouldReturnTrueForValidCredentials() {
GlobalProxyConfiguration configuration = new GlobalProxyConfiguration(createScmConfiguration("marvin", "secret"));
assertThat(configuration.isAuthenticationRequired()).isTrue();
}
private ScmConfiguration createScmConfiguration(Credentials credentials) {
return createScmConfiguration(credentials.getUsername(), credentials.getPassword());
}
private ScmConfiguration createScmConfiguration(String username, String password) {
ScmConfiguration scmConfig = new ScmConfiguration();
scmConfig.setEnableProxy(true);
scmConfig.setProxyServer("proxy.hitchhiker.com");
scmConfig.setProxyPort(3128);
scmConfig.setProxyUser(username);
scmConfig.setProxyPassword(password);
scmConfig.setProxyExcludes(ImmutableSet.of("localhost", "127.0.0.1"));
return scmConfig;
}
private static List<Credentials> createInvalidCredentials() {
return ImmutableList.of(
new Credentials(null, null),
new Credentials("", ""),
new Credentials("trillian", null),
new Credentials("trillian", ""),
new Credentials(null, "secret"),
new Credentials("", "secret")
);
}
@Value
private static class Credentials {
String username;
String password;
}
}

View File

@@ -0,0 +1,356 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.net;
import com.google.common.collect.ImmutableSet;
import com.google.inject.util.Providers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.net.HttpURLConnectionFactory.DefaultSSLContextFactory;
import sonia.scm.net.HttpURLConnectionFactory.ProxyAuthentication;
import sonia.scm.net.HttpURLConnectionFactory.ThreadLocalAuthenticator;
import sonia.scm.net.HttpURLConnectionFactory.TrustAllHostnameVerifier;
import sonia.scm.net.HttpURLConnectionFactory.TrustAllTrustManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class HttpURLConnectionFactoryTest {
@Test
void shouldFailWithNonHttpURL() throws MalformedURLException {
URLConnection urlConnection = mock(URLConnection.class);
HttpURLConnectionFactory factory = new HttpURLConnectionFactory(
new GlobalProxyConfiguration(new ScmConfiguration()),
() -> null,
(url, proxy) -> urlConnection,
new DefaultSSLContextFactory()
);
URL url = new URL("ftp://ftp.hitchhiker.com");
assertThrows(IllegalArgumentException.class, () -> factory.create(url));
}
@Test
void shouldFailWithInvalidStateException() throws IOException {
HttpURLConnectionFactory factory = new HttpURLConnectionFactory(
new GlobalProxyConfiguration(new ScmConfiguration()),
() -> mock(TrustManager.class),
(url, proxy) -> mock(HttpsURLConnection.class),
() -> SSLContext.getInstance("TheAlgoThatDoesNotExists")
);
URL url = new URL("https://hitchhiker.com");
assertThrows(IllegalStateException.class, () -> factory.create(url));
}
@Test
void shouldCreateHttpConnection() throws IOException {
URLConnection urlConnection = mock(HttpURLConnection.class);
HttpURLConnectionFactory factory = new HttpURLConnectionFactory(
new GlobalProxyConfiguration(new ScmConfiguration()),
() -> null,
(url, proxy) -> urlConnection,
new DefaultSSLContextFactory()
);
HttpURLConnection connection = factory.create(new URL("http://hitchhiker.com"));
assertThat(connection).isNotNull();
}
@Test
void shouldThrowWithNonExistentConnectionOptions() throws MalformedURLException {
URLConnection urlConnection = mock(HttpURLConnection.class);
HttpURLConnectionFactory factory = new HttpURLConnectionFactory(
new GlobalProxyConfiguration(new ScmConfiguration()),
() -> null,
(url, proxy) -> urlConnection,
new DefaultSSLContextFactory()
);
final URL url = new URL("http://hitchhiker.com");
assertThrows(IllegalArgumentException.class, () -> factory.create(url, null));
}
@Nested
class HttpsConnectionTests {
private ScmConfiguration configuration;
@Mock
private TrustManager trustManager;
private SSLContext sslContext;
private HttpURLConnectionFactory connectionFactory;
private Proxy usedProxy;
@BeforeEach
void setUpConnectionFactory() throws NoSuchAlgorithmException {
this.configuration = new ScmConfiguration();
this.sslContext = spy(new DefaultSSLContextFactory().create());
this.connectionFactory = new HttpURLConnectionFactory(
new GlobalProxyConfiguration(configuration),
Providers.of(trustManager),
(url, proxy) -> {
this.usedProxy = proxy;
return mock(HttpsURLConnection.class);
},
() -> sslContext
);
}
@Test
void shouldCreateDefaultHttpConnection() throws IOException {
HttpURLConnection connection = connectionFactory.create(new URL("https://hitchhiker.org"));
verify(connection).setConnectTimeout(HttpConnectionOptions.DEFAULT_CONNECTION_TIMEOUT);
verify(connection).setReadTimeout(HttpConnectionOptions.DEFAULT_READ_TIMEOUT);
assertThat(usedProxy).isNull();
}
@Test
void shouldUseProvidedConnectionTimeout() throws IOException {
HttpURLConnection connection = connectionFactory.create(
new URL("https://hitchhiker.org"),
new HttpConnectionOptions().withConnectionTimeout(5L, TimeUnit.SECONDS)
);
verify(connection).setConnectTimeout(5000);
}
@Test
void shouldUseProvidedReadTimeout() throws IOException {
HttpURLConnection connection = connectionFactory.create(
new URL("https://hitchhiker.org"),
new HttpConnectionOptions().withReadTimeout(3L, TimeUnit.SECONDS)
);
verify(connection).setReadTimeout(3000);
}
@Test
void shouldCreateProxyConnection() throws IOException {
configuration.setEnableProxy(true);
configuration.setProxyServer("proxy.hitchhiker.com");
configuration.setProxyPort(3128);
connectionFactory.create(new URL("https://hitchhiker.org"));
assertUsedProxy("proxy.hitchhiker.com", 3128);
}
@Test
void shouldNotCreateProxyConnectionIfHostIsOnTheExcludeList() throws IOException {
configuration.setEnableProxy(true);
configuration.setProxyServer("proxy.hitchhiker.com");
configuration.setProxyPort(3128);
configuration.setProxyExcludes(ImmutableSet.of("localhost", "hitchhiker.org", "127.0.0.1"));
connectionFactory.create(new URL("https://hitchhiker.org"));
assertThat(usedProxy).isNull();
}
@Test
void shouldNotCreateProxyConnectionWithIgnoreOption() throws IOException {
configuration.setEnableProxy(true);
configuration.setProxyServer("proxy.hitchhiker.com");
configuration.setProxyPort(3128);
connectionFactory.create(
new URL("https://hitchhiker.org"), new HttpConnectionOptions().withIgnoreProxySettings()
);
assertThat(usedProxy).isNull();
}
@Test
void shouldCreateProxyConnectionWithAuthentication() throws IOException {
configuration.setEnableProxy(true);
configuration.setProxyServer("proxy.hitchhiker.org");
configuration.setProxyPort(3129);
configuration.setProxyUser("marvin");
configuration.setProxyPassword("brainLikeAPlanet");
connectionFactory.create(new URL("https://hitchhiker.org"));
assertUsedProxy("proxy.hitchhiker.org", 3129);
assertProxyAuthentication("marvin", "brainLikeAPlanet");
}
private void assertProxyAuthentication(String username, String password) {
ProxyAuthentication proxyAuthentication = ThreadLocalAuthenticator.get();
assertThat(proxyAuthentication).isNotNull();
assertThat(proxyAuthentication.getUsername()).isEqualTo(username);
assertThat(proxyAuthentication.getPassword()).isEqualTo(password.toCharArray());
}
@Test
void shouldCreateProxyConnectionFromOptions() throws IOException {
ScmConfiguration localProxyConf = new ScmConfiguration();
localProxyConf.setEnableProxy(true);
localProxyConf.setProxyServer("prox.hitchhiker.net");
localProxyConf.setProxyPort(3127);
localProxyConf.setProxyUser("trillian");
localProxyConf.setProxyPassword("secret");
connectionFactory.create(
new URL("https://hitchhiker.net"),
new HttpConnectionOptions().withProxyConfiguration(new GlobalProxyConfiguration(localProxyConf))
);
assertUsedProxy("prox.hitchhiker.net", 3127);
assertProxyAuthentication("trillian", "secret");
}
@Test
void shouldNotUsePreviousProxyAuthentication() throws IOException {
ScmConfiguration localProxyConf = new ScmConfiguration();
localProxyConf.setEnableProxy(true);
localProxyConf.setProxyServer("proxy.hitchhiker.net");
localProxyConf.setProxyPort(3127);
localProxyConf.setProxyUser("trillian");
localProxyConf.setProxyPassword("secret");
URL url = new URL("https://hitchhiker.net");
HttpConnectionOptions options = new HttpConnectionOptions()
.withProxyConfiguration(new GlobalProxyConfiguration(localProxyConf));
connectionFactory.create(url, options);
assertUsedProxy("proxy.hitchhiker.net", 3127);
assertProxyAuthentication("trillian", "secret");
localProxyConf.setEnableProxy(false);
connectionFactory.create(url, options);
assertThat(usedProxy).isNull();
assertThat(ThreadLocalAuthenticator.get()).isNull();
}
@Test
void shouldUseProvidedTrustManagerForHttpsConnections() throws IOException, KeyManagementException {
HttpURLConnection connection = connectionFactory.create(new URL("https://hitchhiker.net"));
TrustManager[] trustManagers = usedTrustManagers(connection);
assertThat(trustManagers).containsOnly(trustManager);
}
@Test
void shouldUseTrustAllTrustManager() throws IOException, KeyManagementException {
HttpURLConnection connection = connectionFactory.create(
new URL("https://hitchhiker.net"),
new HttpConnectionOptions().withDisableCertificateValidation()
);
TrustManager[] trustManagers = usedTrustManagers(connection);
assertThat(trustManagers).hasSize(1).hasOnlyElementsOfType(TrustAllTrustManager.class);
}
@Test
void shouldUseTrustAllHostnameVerifier() throws IOException {
HttpURLConnection connection = connectionFactory.create(
new URL("https://hitchhiker.net"),
new HttpConnectionOptions().withDisabledHostnameValidation()
);
assertThat(connection).isInstanceOfSatisfying(
HttpsURLConnection.class, https -> {
ArgumentCaptor<HostnameVerifier> captor = ArgumentCaptor.forClass(HostnameVerifier.class);
verify(https).setHostnameVerifier(captor.capture());
assertThat(captor.getValue()).isInstanceOf(TrustAllHostnameVerifier.class);
}
);
}
@Test
void shouldUseProvidedKeyManagers() throws IOException, KeyManagementException {
KeyManager keyManager = mock(KeyManager.class);
connectionFactory.create(
new URL("https://hitchhiker.net"),
new HttpConnectionOptions().withKeyManagers(keyManager)
);
ArgumentCaptor<KeyManager[]> captor = ArgumentCaptor.forClass(KeyManager[].class);
verify(sslContext).init(captor.capture(), any(), eq(null));
KeyManager[] keyManagers = captor.getValue();
assertThat(keyManagers).containsOnly(keyManager);
}
private TrustManager[] usedTrustManagers(HttpURLConnection connection) throws KeyManagementException {
ArgumentCaptor<TrustManager[]> captor = ArgumentCaptor.forClass(TrustManager[].class);
assertThat(connection).isInstanceOfSatisfying(
HttpsURLConnection.class, https -> verify(https).setSSLSocketFactory(any())
);
verify(sslContext).init(eq(null), captor.capture(), eq(null));
verify(sslContext).getSocketFactory();
return captor.getValue();
}
private void assertUsedProxy(String host, int port) {
assertThat(usedProxy).isNotNull();
assertThat(usedProxy.address()).isInstanceOfSatisfying(InetSocketAddress.class, inet -> {
assertThat(inet.getHostName()).isEqualTo(host);
assertThat(inet.getPort()).isEqualTo(port);
});
}
}
}