diff --git a/gradle/changelog/proxy_support.yaml b/gradle/changelog/proxy_support.yaml new file mode 100644 index 0000000000..417088f22b --- /dev/null +++ b/gradle/changelog/proxy_support.yaml @@ -0,0 +1,6 @@ +- type: Fixed + description: Proxy authentication ([#1773](https://github.com/scm-manager/scm-manager/pull/1773)) +- type: Added + description: Proxy support for pull, push and mirror commands ([#1773](https://github.com/scm-manager/scm-manager/pull/1773)) +- type: Added + description: Option for local proxy configuration to mirror command ([#1773](https://github.com/scm-manager/scm-manager/pull/1773)) diff --git a/scm-core/build.gradle b/scm-core/build.gradle index 4e4bbd1546..f50710fbf2 100644 --- a/scm-core/build.gradle +++ b/scm-core/build.gradle @@ -43,7 +43,9 @@ dependencies { // lombok compileOnly libraries.lombok + testCompileOnly libraries.lombok annotationProcessor libraries.lombok + testAnnotationProcessor libraries.lombok // servlet api implementation libraries.servletApi diff --git a/scm-core/src/main/java/sonia/scm/io/INIConfiguration.java b/scm-core/src/main/java/sonia/scm/io/INIConfiguration.java index 8bf07ce132..63785fd40e 100644 --- a/scm-core/src/main/java/sonia/scm/io/INIConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/io/INIConfiguration.java @@ -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 Wikipedia article */ -public class INIConfiguration -{ +public class INIConfiguration { + + private final Map 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 getSections() - { - return sectionMap.values(); + public Collection getSections() { + return ImmutableList.copyOf(sectionMap.values()); } - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Map sectionMap; } diff --git a/scm-core/src/main/java/sonia/scm/io/INIConfigurationReader.java b/scm-core/src/main/java/sonia/scm/io/INIConfigurationReader.java index 1d3ddf8d34..6d7be1e259 100644 --- a/scm-core/src/main/java/sonia/scm/io/INIConfigurationReader.java +++ b/scm-core/src/main/java/sonia/scm/io/INIConfigurationReader.java @@ -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 -{ +public class INIConfigurationReader extends AbstractReader { - /** 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 line = reader.readLine(); } - if (section != null) - { + if (section != null) { configuration.addSection(section); } } - finally - { - IOUtil.close(input); - } return configuration; } diff --git a/scm-core/src/main/java/sonia/scm/io/INIConfigurationWriter.java b/scm-core/src/main/java/sonia/scm/io/INIConfigurationWriter.java index 694dc45333..e3f74c4257 100644 --- a/scm-core/src/main/java/sonia/scm/io/INIConfigurationWriter.java +++ b/scm-core/src/main/java/sonia/scm/io/INIConfigurationWriter.java @@ -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 -{ +public class INIConfigurationWriter extends AbstractWriter { - /** - * 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); } } } diff --git a/scm-core/src/main/java/sonia/scm/io/INISection.java b/scm-core/src/main/java/sonia/scm/io/INISection.java index 6a186034dc..96c5de7516 100644 --- a/scm-core/src/main/java/sonia/scm/io/INISection.java +++ b/scm-core/src/main/java/sonia/scm/io/INISection.java @@ -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 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 parameters) - { + public INISection(String name, Map 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 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 entry : parameters.entrySet()) - { + for (Map.Entry 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 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 parameters; } diff --git a/scm-core/src/main/java/sonia/scm/net/GlobalProxyConfiguration.java b/scm-core/src/main/java/sonia/scm/net/GlobalProxyConfiguration.java new file mode 100644 index 0000000000..53bebc9232 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/GlobalProxyConfiguration.java @@ -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 getExcludes() { + Set 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()); + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/HttpConnectionOptions.java b/scm-core/src/main/java/sonia/scm/net/HttpConnectionOptions.java new file mode 100644 index 0000000000..a9954f9379 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/HttpConnectionOptions.java @@ -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 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 getKeyManagers() { + if (keyManagers != null) { + return Optional.of(Arrays.copyOf(keyManagers, keyManagers.length)); + } + return Optional.empty(); + } + + /** + * Disable certificate validation. + * WARNING: 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. + * WARNING: 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; + } +} diff --git a/scm-core/src/main/java/sonia/scm/net/HttpURLConnectionFactory.java b/scm-core/src/main/java/sonia/scm/net/HttpURLConnectionFactory.java new file mode 100644 index 0000000000..691dc2845c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/HttpURLConnectionFactory.java @@ -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. + * Note: 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 trustManagerProvider; + private final Connector connector; + private final SSLContextFactory sslContextFactory; + + @Inject + public HttpURLConnectionFactory(GlobalProxyConfiguration globalProxyConfiguration, Provider trustManagerProvider) { + this(globalProxyConfiguration, trustManagerProvider, new DefaultConnector(), new DefaultSSLContextFactory()); + } + + @VisibleForTesting + public HttpURLConnectionFactory(GlobalProxyConfiguration globalProxyConfiguration, Provider 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 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 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; + } + } + +} diff --git a/scm-core/src/main/java/sonia/scm/net/ProxyConfiguration.java b/scm-core/src/main/java/sonia/scm/net/ProxyConfiguration.java new file mode 100644 index 0000000000..c7d2c801a7 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/net/ProxyConfiguration.java @@ -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 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(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MirrorCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MirrorCommandBuilder.java index 99e7c9aa52..5aaef43bc2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MirrorCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MirrorCommandBuilder.java @@ -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 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; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MirrorCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MirrorCommandRequest.java index 03ec51b2b4..0fb9594cf9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MirrorCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MirrorCommandRequest.java @@ -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 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 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 getProxyConfiguration() { + return Optional.ofNullable(proxyConfiguration); + } } diff --git a/scm-core/src/test/java/sonia/scm/io/INIConfigurationReaderTest.java b/scm-core/src/test/java/sonia/scm/io/INIConfigurationReaderTest.java new file mode 100644 index 0000000000..ba008526be --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/io/INIConfigurationReaderTest.java @@ -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"); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/io/INIConfigurationTest.java b/scm-core/src/test/java/sonia/scm/io/INIConfigurationTest.java new file mode 100644 index 0000000000..8f81150ec4 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/io/INIConfigurationTest.java @@ -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); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/io/INIConfigurationWriterTest.java b/scm-core/src/test/java/sonia/scm/io/INIConfigurationWriterTest.java new file mode 100644 index 0000000000..93ee777f95 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/io/INIConfigurationWriterTest.java @@ -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); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/io/INISectionTest.java b/scm-core/src/test/java/sonia/scm/io/INISectionTest.java new file mode 100644 index 0000000000..e542fb54f8 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/io/INISectionTest.java @@ -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); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/net/GlobalProxyConfigurationTest.java b/scm-core/src/test/java/sonia/scm/net/GlobalProxyConfigurationTest.java new file mode 100644 index 0000000000..34809f57d5 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/GlobalProxyConfigurationTest.java @@ -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 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; + + } + +} diff --git a/scm-core/src/test/java/sonia/scm/net/HttpURLConnectionFactoryTest.java b/scm-core/src/test/java/sonia/scm/net/HttpURLConnectionFactoryTest.java new file mode 100644 index 0000000000..fdcbe454b9 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/net/HttpURLConnectionFactoryTest.java @@ -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 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 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 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); + }); + } + + + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/http/WrappedHttpUrlConnection.java b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/http/WrappedHttpUrlConnection.java new file mode 100644 index 0000000000..56c3b23402 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/org/eclipse/jgit/transport/http/WrappedHttpUrlConnection.java @@ -0,0 +1,33 @@ +/* + * 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 org.eclipse.jgit.transport.http; + +import java.net.HttpURLConnection; + +public class WrappedHttpUrlConnection extends JDKHttpConnection { + public WrappedHttpUrlConnection(HttpURLConnection urlConnection) { + super(urlConnection); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHttpTransportRegistration.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHttpTransportRegistration.java index 6094815ed6..985fe4128f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHttpTransportRegistration.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitHttpTransportRegistration.java @@ -25,8 +25,8 @@ package sonia.scm.repository; import org.eclipse.jgit.transport.HttpTransport; -import sonia.scm.web.ScmHttpConnectionFactory; import sonia.scm.plugin.Extension; +import sonia.scm.web.ScmHttpConnectionFactory; import javax.inject.Inject; import javax.servlet.ServletContextEvent; @@ -52,4 +52,5 @@ public class GitHttpTransportRegistration implements ServletContextListener { public void contextDestroyed(ServletContextEvent servletContextEvent) { // Nothing to destroy } + } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMirrorCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMirrorCommand.java index a1825d5412..278811ca8b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMirrorCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMirrorCommand.java @@ -55,7 +55,6 @@ import sonia.scm.repository.api.MirrorCommandResult; import sonia.scm.repository.api.MirrorCommandResult.ResultType; import sonia.scm.repository.api.MirrorFilter; import sonia.scm.repository.api.MirrorFilter.Result; -import sonia.scm.repository.api.Pkcs12ClientCertificateCredential; import sonia.scm.repository.api.UsernamePasswordCredential; import javax.inject.Inject; @@ -389,15 +388,14 @@ public class GitMirrorCommand extends AbstractGitCommand implements MirrorComman .setRefSpecs("refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*") .setForceUpdate(true) .setRemoveDeletedRefs(true) - .setRemote(mirrorCommandRequest.getSourceUrl()); - - mirrorCommandRequest.getCredential(Pkcs12ClientCertificateCredential.class) - .ifPresent(c -> fetchCommand.setTransportConfigCallback(transport -> { + .setRemote(mirrorCommandRequest.getSourceUrl()) + .setTransportConfigCallback(transport -> { if (transport instanceof TransportHttp) { TransportHttp transportHttp = (TransportHttp) transport; - transportHttp.setHttpConnectionFactory(mirrorHttpConnectionProvider.createHttpConnectionFactory(c, mirrorLog)); + transportHttp.setHttpConnectionFactory(mirrorHttpConnectionProvider.createHttpConnectionFactory(mirrorCommandRequest, mirrorLog)); } - })); + }); + mirrorCommandRequest.getCredential(UsernamePasswordCredential.class) .ifPresent(c -> fetchCommand .setCredentialsProvider( diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MirrorHttpConnectionProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MirrorHttpConnectionProvider.java index 329347f62b..e137dc4277 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MirrorHttpConnectionProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/MirrorHttpConnectionProvider.java @@ -24,17 +24,17 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.transport.http.HttpConnectionFactory2; +import org.eclipse.jgit.transport.http.HttpConnectionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.net.HttpConnectionOptions; +import sonia.scm.net.HttpURLConnectionFactory; import sonia.scm.repository.api.Pkcs12ClientCertificateCredential; import sonia.scm.web.ScmHttpConnectionFactory; import javax.inject.Inject; -import javax.inject.Provider; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.TrustManager; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.GeneralSecurityException; @@ -45,15 +45,22 @@ class MirrorHttpConnectionProvider { private static final Logger LOG = LoggerFactory.getLogger(MirrorHttpConnectionProvider.class); - private final Provider trustManagerProvider; + private final HttpURLConnectionFactory httpURLConnectionFactory; @Inject - public MirrorHttpConnectionProvider(Provider trustManagerProvider) { - this.trustManagerProvider = trustManagerProvider; + public MirrorHttpConnectionProvider(HttpURLConnectionFactory httpURLConnectionFactory) { + this.httpURLConnectionFactory = httpURLConnectionFactory; } - public HttpConnectionFactory2 createHttpConnectionFactory(Pkcs12ClientCertificateCredential credential, List log) { - return new ScmHttpConnectionFactory(trustManagerProvider, createKeyManagers(credential, log)); + public HttpConnectionFactory createHttpConnectionFactory(MirrorCommandRequest mirrorCommandRequest, List log) { + HttpConnectionOptions options = new HttpConnectionOptions(); + + mirrorCommandRequest.getCredential(Pkcs12ClientCertificateCredential.class) + .ifPresent(c -> options.withKeyManagers(createKeyManagers(c, log))); + mirrorCommandRequest.getProxyConfiguration() + .ifPresent(options::withProxyConfiguration); + + return new ScmHttpConnectionFactory(httpURLConnectionFactory, options); } private KeyManager[] createKeyManagers(Pkcs12ClientCertificateCredential credential, List log) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmHttpConnectionFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmHttpConnectionFactory.java index 5e157221aa..0dbd8b6f37 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmHttpConnectionFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmHttpConnectionFactory.java @@ -24,75 +24,40 @@ package sonia.scm.web; -import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.transport.http.HttpConnection; -import org.eclipse.jgit.transport.http.JDKHttpConnection; -import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory; -import org.eclipse.jgit.transport.http.NoCheckX509TrustManager; +import org.eclipse.jgit.transport.http.HttpConnectionFactory; +import org.eclipse.jgit.transport.http.WrappedHttpUrlConnection; +import sonia.scm.net.HttpConnectionOptions; +import sonia.scm.net.HttpURLConnectionFactory; import javax.inject.Inject; -import javax.inject.Provider; -import javax.net.ssl.KeyManager; -import javax.net.ssl.TrustManager; -import java.security.GeneralSecurityException; -import java.text.MessageFormat; +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; -public class ScmHttpConnectionFactory extends JDKHttpConnectionFactory { +public class ScmHttpConnectionFactory implements HttpConnectionFactory { - private final Provider trustManagerProvider; - private final KeyManager[] keyManagers; + private final HttpURLConnectionFactory connectionFactory; + private final HttpConnectionOptions options; @Inject - public ScmHttpConnectionFactory(Provider trustManagerProvider) { - this(trustManagerProvider, null); + public ScmHttpConnectionFactory(HttpURLConnectionFactory connectionFactory) { + this(connectionFactory, new HttpConnectionOptions()); } - public ScmHttpConnectionFactory(Provider trustManagerProvider, KeyManager[] keyManagers) { - this.trustManagerProvider = trustManagerProvider; - this.keyManagers = keyManagers; + public ScmHttpConnectionFactory(HttpURLConnectionFactory connectionFactory, HttpConnectionOptions options) { + this.connectionFactory = connectionFactory; + this.options = options; } @Override - public GitSession newSession() { - return new ScmConnectionSession(trustManagerProvider.get(), keyManagers); + public HttpConnection create(URL url) throws IOException { + return new WrappedHttpUrlConnection(connectionFactory.create(url, options)); } - private static class ScmConnectionSession implements GitSession { - - private final TrustManager trustManager; - private final KeyManager[] keyManagers; - - private ScmConnectionSession(TrustManager trustManager, KeyManager[] keyManagers) { - this.trustManager = trustManager; - this.keyManagers = keyManagers; - } - - @Override - @SuppressWarnings("java:S5527") - public JDKHttpConnection configure(HttpConnection connection, - boolean sslVerify) throws GeneralSecurityException { - if (!(connection instanceof JDKHttpConnection)) { - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().httpWrongConnectionType, - JDKHttpConnection.class.getName(), - connection.getClass().getName())); - } - JDKHttpConnection conn = (JDKHttpConnection) connection; - String scheme = conn.getURL().getProtocol(); - if ("https".equals(scheme) && sslVerify) { //$NON-NLS-1$ - // sslVerify == true: use the JDK defaults - conn.configure(keyManagers, new TrustManager[]{trustManager}, null); - } else if ("https".equals(scheme)) { - conn.configure(keyManagers, new TrustManager[]{new NoCheckX509TrustManager()}, null); - conn.setHostnameVerifier((name, value) -> true); - } - - return conn; - } - - @Override - public void close() { - // Nothing - } + @Override + public HttpConnection create(URL url, Proxy proxy) throws IOException { + // we ignore proxy configuration of jgit, because we have our own + return create(url); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHttpTransportRegistrationTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHttpTransportRegistrationTest.java new file mode 100644 index 0000000000..862b2d43bb --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitHttpTransportRegistrationTest.java @@ -0,0 +1,63 @@ +/* + * 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.repository; + +import org.eclipse.jgit.transport.HttpTransport; +import org.eclipse.jgit.transport.http.HttpConnectionFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.web.ScmHttpConnectionFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class GitHttpTransportRegistrationTest { + + @Mock + private ScmHttpConnectionFactory scmHttpConnectionFactory; + + private HttpConnectionFactory capturedFactory; + + @BeforeEach + void captureConnectionFactory() { + this.capturedFactory = HttpTransport.getConnectionFactory(); + } + + @AfterEach + void restoreConnectionFactory() { + HttpTransport.setConnectionFactory(capturedFactory); + } + + @Test + void shouldSetHttpConnectionFactory() { + new GitHttpTransportRegistration(scmHttpConnectionFactory).contextInitialized(null); + assertThat(HttpTransport.getConnectionFactory()).isSameAs(scmHttpConnectionFactory); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMirrorCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMirrorCommandTest.java index 4c591c85f7..3dcad14627 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMirrorCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMirrorCommandTest.java @@ -24,6 +24,7 @@ package sonia.scm.repository.spi; +import com.google.inject.util.Providers; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -37,6 +38,9 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.net.GlobalProxyConfiguration; +import sonia.scm.net.HttpURLConnectionFactory; import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitConfig; import sonia.scm.repository.api.MirrorCommandResult; @@ -47,6 +51,7 @@ import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.security.GPG; import sonia.scm.store.InMemoryConfigurationStoreFactory; +import javax.net.ssl.TrustManager; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -70,7 +75,6 @@ public class GitMirrorCommandTest extends AbstractGitCommandTestBase { public static final Consumer ACCEPT_ALL = r -> { }; public static final Consumer REJECT_ALL = r -> r.setFilter(new DenyAllMirrorFilter()); - private final MirrorHttpConnectionProvider mirrorHttpConnectionProvider = mock(MirrorHttpConnectionProvider.class); private final GPG gpg = mock(GPG.class); private final GitChangesetConverterFactory gitChangesetConverterFactory = new GitChangesetConverterFactory(gpg); private final GitTagConverter gitTagConverter = new GitTagConverter(gpg); @@ -78,6 +82,8 @@ public class GitMirrorCommandTest extends AbstractGitCommandTestBase { private File clone; private GitMirrorCommand command; + + @Before public void bendContextToNewRepository() throws IOException, GitAPIException { clone = tempFolder.newFolder(); @@ -87,6 +93,14 @@ public class GitMirrorCommandTest extends AbstractGitCommandTestBase { SimpleGitWorkingCopyFactory workingCopyFactory = new SimpleGitWorkingCopyFactory( new NoneCachingWorkingCopyPool(new WorkdirProvider(repositoryLocationResolver)), new SimpleMeterRegistry()); + + MirrorHttpConnectionProvider mirrorHttpConnectionProvider = new MirrorHttpConnectionProvider( + new HttpURLConnectionFactory( + new GlobalProxyConfiguration(new ScmConfiguration()), + Providers.of(mock(TrustManager.class)) + ) + ); + command = new GitMirrorCommand( emptyContext, mirrorHttpConnectionProvider, diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/MirrorHttpConnectionProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/MirrorHttpConnectionProviderTest.java new file mode 100644 index 0000000000..37172c7795 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/MirrorHttpConnectionProviderTest.java @@ -0,0 +1,95 @@ +/* + * 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.repository.spi; + +import org.eclipse.jgit.transport.http.HttpConnection; +import org.eclipse.jgit.transport.http.HttpConnectionFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.net.HttpConnectionOptions; +import sonia.scm.net.HttpURLConnectionFactory; +import sonia.scm.net.ProxyConfiguration; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class MirrorHttpConnectionProviderTest { + + @Mock + private HttpURLConnectionFactory internalConnectionFactory; + + @InjectMocks + private MirrorHttpConnectionProvider provider; + + @Captor + private ArgumentCaptor captor; + + @Test + void shouldNotConfigureProxy() throws IOException { + MirrorCommandRequest request = new MirrorCommandRequest(); + + HttpConnectionOptions value = create(request); + + assertThat(value.getProxyConfiguration()).isEmpty(); + } + + @Test + void shouldConfigureProxy() throws IOException { + ProxyConfiguration proxy = mock(ProxyConfiguration.class); + MirrorCommandRequest request = new MirrorCommandRequest(); + request.setProxyConfiguration(proxy); + + HttpConnectionOptions value = create(request); + + assertThat(value.getProxyConfiguration()).containsSame(proxy); + } + + private HttpConnectionOptions create(MirrorCommandRequest request) throws IOException { + List log = new ArrayList<>(); + + HttpConnectionFactory connectionFactory = provider.createHttpConnectionFactory(request, log); + assertThat(connectionFactory).isNotNull(); + + HttpConnection connection = connectionFactory.create(new URL("https://hitchhiker.com")); + assertThat(connection).isNotNull(); + + verify(internalConnectionFactory).create(any(), captor.capture()); + return captor.getValue(); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/ScmHttpConnectionFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/ScmHttpConnectionFactoryTest.java new file mode 100644 index 0000000000..6ccf677634 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/ScmHttpConnectionFactoryTest.java @@ -0,0 +1,82 @@ +/* + * 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.web; + +import org.eclipse.jgit.transport.http.HttpConnection; +import org.eclipse.jgit.transport.http.JDKHttpConnection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.net.HttpConnectionOptions; +import sonia.scm.net.HttpURLConnectionFactory; + +import java.io.IOException; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ScmHttpConnectionFactoryTest { + + @Mock + private HttpURLConnectionFactory internalConnectionFactory; + + @Captor + private ArgumentCaptor connectionOptionsCaptor; + + @Test + void shouldCreateConnection() throws IOException { + ScmHttpConnectionFactory connectionFactory = new ScmHttpConnectionFactory(internalConnectionFactory); + + URL url = new URL("https://scm.hitchhiker.org"); + HttpConnection httpConnection = connectionFactory.create(url, null); + + assertThat(httpConnection) + .isNotNull() + .isInstanceOf(JDKHttpConnection.class); + verify(internalConnectionFactory).create(eq(url), connectionOptionsCaptor.capture()); + assertThat(connectionOptionsCaptor.getValue()).isNotNull(); + } + + @Test + void shouldCreateConnectionWithOptions() throws IOException { + HttpConnectionOptions options = new HttpConnectionOptions(); + ScmHttpConnectionFactory connectionFactory = new ScmHttpConnectionFactory(internalConnectionFactory, options); + + URL url = new URL("https://scm.hitchhiker.org"); + HttpConnection httpConnection = connectionFactory.create(url); + + assertThat(httpConnection) + .isNotNull() + .isInstanceOf(JDKHttpConnection.class); + verify(internalConnectionFactory).create(url, options); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java index 4456605d49..ea34b7b4f8 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchCommand.java @@ -26,16 +26,20 @@ package sonia.scm.repository.spi; import com.aragost.javahg.Changeset; import com.aragost.javahg.commands.CommitCommand; +import com.google.common.annotations.VisibleForTesting; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.repository.Branch; +import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.api.BranchRequest; import sonia.scm.repository.work.WorkingCopy; import sonia.scm.user.User; +import javax.inject.Inject; + /** * Mercurial implementation of the {@link BranchCommand}. * Note that this creates an empty commit to "persist" the new branch. @@ -44,6 +48,12 @@ public class HgBranchCommand extends AbstractWorkingCopyCommand implements Branc private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class); + @Inject + HgBranchCommand(HgCommandContext context, HgRepositoryHandler handler) { + this(context, handler.getWorkingCopyFactory()); + } + + @VisibleForTesting HgBranchCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { super(context, workingCopyFactory); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContextFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContextFactory.java new file mode 100644 index 0000000000..1721c4a5e1 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCommandContextFactory.java @@ -0,0 +1,48 @@ +/* + * 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.repository.spi; + +import sonia.scm.repository.HgConfigResolver; +import sonia.scm.repository.HgRepositoryFactory; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +public class HgCommandContextFactory { + + private final HgConfigResolver configResolver; + private final HgRepositoryFactory repositoryFactory; + + @Inject + public HgCommandContextFactory(HgConfigResolver configResolver, HgRepositoryFactory repositoryFactory) { + this.configResolver = configResolver; + this.repositoryFactory = repositoryFactory; + } + + public HgCommandContext create(Repository repository) { + return new HgCommandContext(configResolver, repositoryFactory, repository); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java index 27f2176392..61b0264952 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIncomingCommand.java @@ -33,6 +33,7 @@ import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.spi.javahg.HgIncomingChangesetCommand; +import javax.inject.Inject; import java.io.File; import java.util.Collections; import java.util.List; @@ -58,6 +59,7 @@ public class HgIncomingCommand extends AbstractCommand * @param context * @param handler */ + @Inject HgIncomingCommand(HgCommandContext context, HgRepositoryHandler handler) { super(context); @@ -87,7 +89,7 @@ public class HgIncomingCommand extends AbstractCommand { if (ex.getCommand().getReturnCode() == NO_INCOMING_CHANGESETS) { - changesets = Collections.EMPTY_LIST; + changesets = Collections.emptyList(); } else { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIniConfigurator.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIniConfigurator.java deleted file mode 100644 index dee0129c84..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgIniConfigurator.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.repository.spi; - -import sonia.scm.io.INIConfiguration; -import sonia.scm.io.INIConfigurationReader; -import sonia.scm.io.INIConfigurationWriter; -import sonia.scm.io.INISection; -import sonia.scm.repository.HgRepositoryHandler; - -import java.io.File; -import java.io.IOException; -import java.net.URI; - -public class HgIniConfigurator { - - private final HgCommandContext context; - private static final String AUTH_SECTION = "auth"; - - public HgIniConfigurator(HgCommandContext context) { - this.context = context; - } - - public void addAuthenticationConfig(RemoteCommandRequest request, String url) throws IOException { - INIConfiguration ini = readIniConfiguration(); - INISection authSection = ini.getSection(AUTH_SECTION); - if (authSection == null) { - authSection = new INISection(AUTH_SECTION); - ini.addSection(authSection); - } - URI parsedUrl = URI.create(url); - authSection.setParameter("import.prefix", parsedUrl.getHost()); - authSection.setParameter("import.schemes", parsedUrl.getScheme()); - authSection.setParameter("import.username", request.getUsername()); - authSection.setParameter("import.password", request.getPassword()); - writeIniConfiguration(ini); - } - - public void removeAuthenticationConfig() throws IOException { - INIConfiguration ini = readIniConfiguration(); - ini.removeSection(AUTH_SECTION); - writeIniConfiguration(ini); - } - - public INIConfiguration readIniConfiguration() throws IOException { - return new INIConfigurationReader().read(getHgrcFile()); - } - - public void writeIniConfiguration(INIConfiguration ini) throws IOException { - new INIConfigurationWriter().write(ini, getHgrcFile()); - } - - public File getHgrcFile() { - return new File(context.getDirectory(), HgRepositoryHandler.PATH_HGRC); - } - -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java index d0a9cb80a3..3bd7410f30 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java @@ -30,6 +30,7 @@ import sonia.scm.repository.HgRepositoryFactory; import sonia.scm.repository.Person; import sonia.scm.repository.Repository; +import javax.inject.Inject; import java.util.Iterator; import java.util.concurrent.Callable; @@ -38,9 +39,10 @@ class HgLazyChangesetResolver implements Callable> { private final HgRepositoryFactory factory; private final Repository repository; - HgLazyChangesetResolver(HgRepositoryFactory factory, Repository repository) { + @Inject + HgLazyChangesetResolver(HgRepositoryFactory factory, HgCommandContext context) { this.factory = factory; - this.repository = repository; + this.repository = context.getScmRepository(); } @Override diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java index f966703e88..2453ed78f6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgModifyCommand.java @@ -30,12 +30,15 @@ import com.aragost.javahg.commands.CommitCommand; import com.aragost.javahg.commands.ExecutionException; import com.aragost.javahg.commands.RemoveCommand; import com.aragost.javahg.commands.StatusCommand; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.NoChangesMadeException; +import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.work.WorkingCopy; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.nio.file.Path; @@ -48,8 +51,13 @@ public class HgModifyCommand extends AbstractWorkingCopyCommand implements Modif private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class); + @Inject + public HgModifyCommand(HgCommandContext context, HgRepositoryHandler handler) { + super(context, handler.getWorkingCopyFactory()); + } - public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { + @VisibleForTesting + HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { super(context, workingCopyFactory); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java index c35de5bfc6..7550a334ef 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgOutgoingCommand.java @@ -33,6 +33,7 @@ import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.spi.javahg.HgOutgoingChangesetCommand; +import javax.inject.Inject; import java.io.File; import java.util.Collections; import java.util.List; @@ -58,7 +59,8 @@ public class HgOutgoingCommand extends AbstractCommand * @param context * @param handler */ - public HgOutgoingCommand(HgCommandContext context, HgRepositoryHandler handler) + @Inject + HgOutgoingCommand(HgCommandContext context, HgRepositoryHandler handler) { super(context); this.handler = handler; @@ -87,7 +89,7 @@ public class HgOutgoingCommand extends AbstractCommand { if (ex.getCommand().getReturnCode() == NO_OUTGOING_CHANGESETS) { - changesets = Collections.EMPTY_LIST; + changesets = Collections.emptyList(); } else { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java index e7e95c61be..478104fff6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java @@ -29,55 +29,58 @@ import com.aragost.javahg.commands.ExecutionException; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.ContextEntry; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.PullResponse; +import javax.inject.Inject; import java.io.IOException; import java.util.List; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + public class HgPullCommand extends AbstractHgPushOrPullCommand implements PullCommand { private static final Logger LOG = LoggerFactory.getLogger(HgPullCommand.class); private final ScmEventBus eventBus; private final HgLazyChangesetResolver changesetResolver; private final HgRepositoryHookEventFactory eventFactory; + private final TemporaryConfigFactory configFactory; + @Inject public HgPullCommand(HgRepositoryHandler handler, HgCommandContext context, ScmEventBus eventBus, HgLazyChangesetResolver changesetResolver, - HgRepositoryHookEventFactory eventFactory + HgRepositoryHookEventFactory eventFactory, + TemporaryConfigFactory configFactory ) { super(handler, context); this.eventBus = eventBus; this.changesetResolver = changesetResolver; this.eventFactory = eventFactory; + this.configFactory = configFactory; } @Override @SuppressWarnings({"java:S3252"}) - public PullResponse pull(PullCommandRequest request) - throws IOException { + public PullResponse pull(PullCommandRequest request) throws IOException { String url = getRemoteUrl(request); - HgIniConfigurator iniConfigurator = new HgIniConfigurator(getContext()); LOG.debug("pull changes from {} to {}", url, getContext().getScmRepository()); - List result; - + TemporaryConfigFactory.Builder builder = configFactory.withContext(context); if (!Strings.isNullOrEmpty(request.getUsername()) && !Strings.isNullOrEmpty(request.getPassword())) { - iniConfigurator.addAuthenticationConfig(request, url); + builder.withCredentials(url, request.getUsername(), request.getPassword()); } + List result; + try { - result = com.aragost.javahg.commands.PullCommand.on(open()).execute(url); + result = builder.call(() -> com.aragost.javahg.commands.PullCommand.on(open()).execute(url)); } catch (ExecutionException ex) { - throw new ImportFailedException(ContextEntry.ContextBuilder.entity(getRepository()).build(), "could not execute pull command", ex); - } finally { - iniConfigurator.removeAuthenticationConfig(); + throw new ImportFailedException(entity(getRepository()).build(), "could not execute pull command", ex); } firePostReceiveRepositoryHookEvent(); @@ -88,4 +91,5 @@ public class HgPullCommand extends AbstractHgPushOrPullCommand implements PullCo private void firePostReceiveRepositoryHookEvent() { eventBus.post(eventFactory.createEvent(context, changesetResolver)); } + } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java index 3c5674d460..96a1028eb1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPushCommand.java @@ -24,47 +24,59 @@ package sonia.scm.repository.spi; +//~--- non-JDK imports -------------------------------------------------------- + import com.aragost.javahg.Changeset; import com.aragost.javahg.commands.ExecutionException; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.HgRepositoryHandler; -import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.PushResponse; +import javax.inject.Inject; import java.io.IOException; import java.util.List; -import static com.aragost.javahg.commands.flags.PushCommandFlags.on; +import static sonia.scm.ContextEntry.ContextBuilder.entity; +//~--- JDK imports ------------------------------------------------------------ + +/** + * + * @author Sebastian Sdorra + */ public class HgPushCommand extends AbstractHgPushOrPullCommand implements PushCommand { private static final Logger LOG = LoggerFactory.getLogger(HgPushCommand.class); - public HgPushCommand(HgRepositoryHandler handler, HgCommandContext context) { + private final TemporaryConfigFactory configFactory; + + @Inject + public HgPushCommand(HgRepositoryHandler handler, HgCommandContext context, TemporaryConfigFactory configFactory) { super(handler, context); + this.configFactory = configFactory; } @Override - public PushResponse push(PushCommandRequest request) - throws IOException { + @SuppressWarnings("java:S3252") // this is how javahg is used + public PushResponse push(PushCommandRequest request) throws IOException { String url = getRemoteUrl(request); LOG.debug("push changes from {} to {}", getRepository(), url); - List result; - HgIniConfigurator iniConfigurator = new HgIniConfigurator(getContext()); - try { - if (!Strings.isNullOrEmpty(request.getUsername()) && !Strings.isNullOrEmpty(request.getPassword())) { - iniConfigurator.addAuthenticationConfig(request, url); - } + TemporaryConfigFactory.Builder builder = configFactory.withContext(context); + if (!Strings.isNullOrEmpty(request.getUsername()) && !Strings.isNullOrEmpty(request.getPassword())) { + builder.withCredentials(url, request.getUsername(), request.getPassword()); + } - result = on(open()).execute(url); + List result; + + try { + result = com.aragost.javahg.commands.PushCommand.on(open()).execute(url); } catch (ExecutionException ex) { - throw new InternalRepositoryException(getRepository(), "could not execute push command", ex); - } finally { - iniConfigurator.removeAuthenticationConfig(); + throw new ImportFailedException(entity(getRepository()).build(), "could not execute pull command", ex); } return new PushResponse(result.size()); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index c93fb381d4..8bc8c900de 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -24,13 +24,9 @@ package sonia.scm.repository.spi; -import com.google.common.io.Closeables; -import sonia.scm.event.ScmEventBus; +import com.google.inject.AbstractModule; +import com.google.inject.Injector; import sonia.scm.repository.Feature; -import sonia.scm.repository.HgConfigResolver; -import sonia.scm.repository.HgRepositoryFactory; -import sonia.scm.repository.HgRepositoryHandler; -import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.CommandNotSupportedException; @@ -68,28 +64,22 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { Feature.MODIFICATIONS_BETWEEN_REVISIONS ); - private final HgRepositoryHandler handler; + private final Injector commandInjector; private final HgCommandContext context; - private final HgLazyChangesetResolver lazyChangesetResolver; - private final HgRepositoryHookEventFactory eventFactory; - private final ScmEventBus eventBus; - HgRepositoryServiceProvider(HgRepositoryHandler handler, - HgConfigResolver configResolver, - HgRepositoryFactory factory, - HgRepositoryHookEventFactory eventFactory, - ScmEventBus eventBus, - Repository repository) { - this.handler = handler; - this.eventBus = eventBus; - this.eventFactory = eventFactory; - this.context = new HgCommandContext(configResolver, factory, repository); - this.lazyChangesetResolver = new HgLazyChangesetResolver(factory, repository); + HgRepositoryServiceProvider(Injector injector, HgCommandContext context) { + this.commandInjector = injector.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + bind(HgCommandContext.class).toInstance(context); + } + }); + this.context = context; } @Override public void close() throws IOException { - Closeables.close(context, true); + context.close(); } @Override @@ -104,7 +94,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @Override public BranchCommand getBranchCommand() { - return new HgBranchCommand(context, handler.getWorkingCopyFactory()); + return commandInjector.getInstance(HgBranchCommand.class); } @Override @@ -124,7 +114,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @Override public IncomingCommand getIncomingCommand() { - return new HgIncomingCommand(context, handler); + return commandInjector.getInstance(HgIncomingCommand.class); } @Override @@ -145,22 +135,22 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @Override public OutgoingCommand getOutgoingCommand() { - return new HgOutgoingCommand(context, handler); + return commandInjector.getInstance(HgOutgoingCommand.class); } @Override public PullCommand getPullCommand() { - return new HgPullCommand(handler, context, eventBus, lazyChangesetResolver, eventFactory); + return commandInjector.getInstance(HgPullCommand.class); } @Override public PushCommand getPushCommand() { - return new HgPushCommand(handler, context); + return commandInjector.getInstance(HgPushCommand.class); } @Override public ModifyCommand getModifyCommand() { - return new HgModifyCommand(context, handler.getWorkingCopyFactory()); + return commandInjector.getInstance(HgModifyCommand.class); } @Override @@ -180,7 +170,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @Override public TagCommand getTagCommand() { - return new HgTagCommand(context, handler.getWorkingCopyFactory()); + return commandInjector.getInstance(HgTagCommand.class); } @Override @@ -190,7 +180,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @Override public UnbundleCommand getUnbundleCommand() { - return new HgUnbundleCommand(context, lazyChangesetResolver, eventFactory); + return commandInjector.getInstance(HgUnbundleCommand.class); } @Override diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java index 76500d3a7e..6627b86413 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java @@ -25,10 +25,8 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; -import sonia.scm.event.ScmEventBus; +import com.google.inject.Injector; import sonia.scm.plugin.Extension; -import sonia.scm.repository.HgConfigResolver; -import sonia.scm.repository.HgRepositoryFactory; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.Repository; @@ -38,34 +36,20 @@ import sonia.scm.repository.Repository; @Extension public class HgRepositoryServiceResolver implements RepositoryServiceResolver { - private final HgRepositoryHandler handler; - private final HgConfigResolver configResolver; - private final HgRepositoryFactory factory; - private final ScmEventBus eventBus; - private final HgRepositoryHookEventFactory eventFactory; + private final Injector injector; + private final HgCommandContextFactory commandContextFactory; @Inject - public HgRepositoryServiceResolver(HgRepositoryHandler handler, - HgConfigResolver configResolver, - HgRepositoryFactory factory, - ScmEventBus eventBus, - HgRepositoryHookEventFactory eventFactory - ) { - this.handler = handler; - this.configResolver = configResolver; - this.factory = factory; - this.eventBus = eventBus; - this.eventFactory = eventFactory; + public HgRepositoryServiceResolver(Injector injector, HgCommandContextFactory commandContextFactory) { + this.injector = injector; + this.commandContextFactory = commandContextFactory; } @Override public HgRepositoryServiceProvider resolve(Repository repository) { - HgRepositoryServiceProvider provider = null; - if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new HgRepositoryServiceProvider(handler, configResolver, factory, eventFactory, eventBus, repository); + return new HgRepositoryServiceProvider(injector, commandContextFactory.create(repository)); } - - return provider; + return null; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagCommand.java index 13459356fd..7e449326d7 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgTagCommand.java @@ -25,21 +25,31 @@ package sonia.scm.repository.spi; import com.aragost.javahg.Repository; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import org.apache.shiro.SecurityUtils; +import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.Tag; import sonia.scm.repository.api.TagCreateRequest; import sonia.scm.repository.api.TagDeleteRequest; import sonia.scm.repository.work.WorkingCopy; import sonia.scm.user.User; +import javax.inject.Inject; + import static sonia.scm.repository.spi.UserFormatter.getUserStringFor; public class HgTagCommand extends AbstractWorkingCopyCommand implements TagCommand { public static final String DEFAULT_BRANCH_NAME = "default"; - public HgTagCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { + @Inject + public HgTagCommand(HgCommandContext context, HgRepositoryHandler handler) { + this(context, handler.getWorkingCopyFactory()); + } + + @VisibleForTesting + HgTagCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) { super(context, workingCopyFactory); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java index 49f6d5973b..bc49f8a22d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.repository.RepositoryHookEvent; import sonia.scm.repository.api.UnbundleResponse; +import javax.inject.Inject; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -44,6 +45,7 @@ public class HgUnbundleCommand implements UnbundleCommand { private final HgLazyChangesetResolver changesetResolver; private final HgRepositoryHookEventFactory eventFactory; + @Inject HgUnbundleCommand(HgCommandContext context, HgLazyChangesetResolver changesetResolver, HgRepositoryHookEventFactory eventFactory diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/TemporaryConfigFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/TemporaryConfigFactory.java new file mode 100644 index 0000000000..7ad9e9f498 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/TemporaryConfigFactory.java @@ -0,0 +1,200 @@ +/* + * 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.repository.spi; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.io.INIConfiguration; +import sonia.scm.io.INIConfigurationReader; +import sonia.scm.io.INIConfigurationWriter; +import sonia.scm.io.INISection; +import sonia.scm.net.GlobalProxyConfiguration; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.util.Util; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.net.URI; + +public class TemporaryConfigFactory { + + private static final Logger LOG = LoggerFactory.getLogger(TemporaryConfigFactory.class); + + private static final String SECTION_PROXY = "http_proxy"; + private static final String SECTION_AUTH = "auth"; + private static final String AUTH_PREFIX = "temporary."; + + private final GlobalProxyConfiguration globalProxyConfiguration; + + @Inject + public TemporaryConfigFactory(GlobalProxyConfiguration globalProxyConfiguration) { + this.globalProxyConfiguration = globalProxyConfiguration; + } + + public Builder withContext(HgCommandContext context) { + return new Builder(context); + } + + public class Builder { + + private final HgCommandContext context; + private String url; + private String username; + private String password; + + private INIConfiguration hgrc; + private INISection previousProxyConfiguration; + + private Builder(HgCommandContext context) { + this.context = context; + } + + public Builder withCredentials(String url, String username, String password) { + this.url = url; + this.username = username; + this.password = password; + return this; + } + + @SuppressWarnings("java:S4042") // we know that we delete a file + public T call(HgCallable callable) throws IOException { + File file = new File(context.getDirectory(), HgRepositoryHandler.PATH_HGRC); + boolean exists = file.exists(); + if (isModificationRequired()) { + setupHgrc(file); + } + try { + return callable.call(); + } finally { + if (!exists && file.exists() && !file.delete()) { + LOG.error("failed to delete temporary hgrc {}", file); + } else if (exists && file.exists()) { + cleanUpHgrc(file); + } + } + } + + private void write(File file) throws IOException { + INIConfigurationWriter writer = new INIConfigurationWriter(); + writer.write(hgrc, file); + } + + private void setupHgrc(File file) throws IOException { + if (file.exists()) { + INIConfigurationReader reader = new INIConfigurationReader(); + hgrc = reader.read(file); + } else { + hgrc = new INIConfiguration(); + } + + if (isAuthenticationEnabled()) { + applyAuthentication(hgrc); + } + + if (globalProxyConfiguration.isEnabled()) { + applyProxyConfiguration(hgrc); + } + + write(file); + } + + private void applyProxyConfiguration(INIConfiguration hgrc) { + previousProxyConfiguration = hgrc.getSection(SECTION_PROXY); + hgrc.removeSection(SECTION_PROXY); + INISection proxy = new INISection(SECTION_PROXY); + proxy.setParameter("host", globalProxyConfiguration.getHost() + ":" + globalProxyConfiguration.getPort()); + + String user = globalProxyConfiguration.getUsername(); + String passwd = globalProxyConfiguration.getPassword(); + if (!Strings.isNullOrEmpty(user) && !Strings.isNullOrEmpty(passwd)) { + proxy.setParameter("user", user); + proxy.setParameter("passwd", passwd); + } + + if (Util.isNotEmpty(globalProxyConfiguration.getExcludes())) { + proxy.setParameter("no", Joiner.on(',').join(globalProxyConfiguration.getExcludes())); + } + + hgrc.addSection(proxy); + } + + private void applyAuthentication(INIConfiguration hgrc) { + INISection auth = hgrc.getSection(SECTION_AUTH); + if (auth == null) { + auth = new INISection(SECTION_AUTH); + hgrc.addSection(auth); + } + + URI uri = URI.create(url); + auth.setParameter(AUTH_PREFIX + "prefix", uri.getHost()); + auth.setParameter(AUTH_PREFIX + "schemes", uri.getScheme()); + auth.setParameter(AUTH_PREFIX + "username", username); + auth.setParameter(AUTH_PREFIX + "password", password); + + } + + private boolean isModificationRequired() { + return isAuthenticationEnabled() || globalProxyConfiguration.isEnabled(); + } + + private boolean isAuthenticationEnabled() { + return !Strings.isNullOrEmpty(url) + && !Strings.isNullOrEmpty(username) + && !Strings.isNullOrEmpty(password); + } + + private void cleanUpHgrc(File file) throws IOException { + INISection auth = hgrc.getSection(SECTION_AUTH); + if (isAuthenticationEnabled() && auth != null) { + for (String key : auth.getParameterKeys()) { + if (key.startsWith(AUTH_PREFIX)) { + auth.removeParameter(key); + } + } + } + + if (globalProxyConfiguration.isEnabled()) { + hgrc.removeSection(SECTION_PROXY); + if (previousProxyConfiguration != null) { + hgrc.addSection(previousProxyConfiguration); + } + } + + if (isModificationRequired()) { + write(file); + } + } + + } + + @FunctionalInterface + public interface HgCallable { + T call() throws IOException; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCommandContextFactoryTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCommandContextFactoryTest.java new file mode 100644 index 0000000000..0a5e7cd4cb --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCommandContextFactoryTest.java @@ -0,0 +1,56 @@ +/* + * 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.repository.spi; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.HgConfigResolver; +import sonia.scm.repository.HgRepositoryFactory; +import sonia.scm.repository.RepositoryTestData; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class HgCommandContextFactoryTest { + + @Mock + private HgConfigResolver configResolver; + + @Mock + private HgRepositoryFactory repositoryFactory; + + @InjectMocks + private HgCommandContextFactory commandContextFactory; + + @Test + void shouldCreateHgCommandContext() { + HgCommandContext hg = commandContextFactory.create(RepositoryTestData.createHeartOfGold("hg")); + assertThat(hg).isNotNull(); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java index f881b665d5..0c661f80c9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java @@ -35,7 +35,7 @@ public class HgLazyChangesetResolverTest extends AbstractHgCommandTestBase { @Test public void shouldResolveChangesets() { - HgLazyChangesetResolver changesetResolver = new HgLazyChangesetResolver(HgTestUtil.createFactory(handler, repositoryDirectory), repository); + HgLazyChangesetResolver changesetResolver = new HgLazyChangesetResolver(HgTestUtil.createFactory(handler, repositoryDirectory), cmdContext); Iterable changesets = changesetResolver.call(); Changeset firstChangeset = changesets.iterator().next(); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java index e67a976d41..7903a92e7b 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModifyCommandTest.java @@ -59,8 +59,7 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase { // we do not want to configure http hooks in this unit test } }; - hgModifyCommand = new HgModifyCommand(cmdContext, workingCopyFactory - ); + hgModifyCommand = new HgModifyCommand(cmdContext, workingCopyFactory); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java index 0813aa2900..254a4c2846 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java @@ -55,7 +55,7 @@ public class HgUnbundleCommandTest extends AbstractHgCommandTestBase { @Before public void initUnbundleCommand() { eventFactory = mock(HgRepositoryHookEventFactory.class); - unbundleCommand = new HgUnbundleCommand(cmdContext, new HgLazyChangesetResolver(HgTestUtil.createFactory(handler, repositoryDirectory), null), eventFactory); + unbundleCommand = new HgUnbundleCommand(cmdContext, new HgLazyChangesetResolver(HgTestUtil.createFactory(handler, repositoryDirectory), cmdContext), eventFactory); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/TemporaryConfigFactoryTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/TemporaryConfigFactoryTest.java new file mode 100644 index 0000000000..af9824898e --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/TemporaryConfigFactoryTest.java @@ -0,0 +1,237 @@ +/* + * 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.repository.spi; + +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.io.INIConfiguration; +import sonia.scm.io.INIConfigurationReader; +import sonia.scm.io.INIConfigurationWriter; +import sonia.scm.io.INISection; +import sonia.scm.net.GlobalProxyConfiguration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +class TemporaryConfigFactoryTest { + + @Mock + private HgCommandContext commandContext; + private TemporaryConfigFactory configFactory; + private ScmConfiguration configuration; + + private Path hgrc; + + @BeforeEach + void setUp(@TempDir Path directory) throws IOException { + Path hg = Files.createDirectories(directory.resolve(".hg")); + hgrc = hg.resolve("hgrc"); + lenient().when(commandContext.getDirectory()).thenReturn(directory.toFile()); + + configuration = new ScmConfiguration(); + configFactory = new TemporaryConfigFactory(new GlobalProxyConfiguration(configuration)); + } + + @Test + void shouldNotCreateHgrc() throws IOException { + configFactory.withContext(commandContext).call(() -> { + assertThat(hgrc).doesNotExist(); + return null; + }); + } + + @Test + void shouldCreateHgrcWithAuthentication() throws IOException { + configFactory + .withContext(commandContext) + .withCredentials("https://hg.hitchhiker.org/repo", "trillian", "secret") + .call(() -> { + INIConfiguration ini = assertHgrc(); + + INISection auth = ini.getSection("auth"); + assertThat(auth).isNotNull(); + assertThat(auth.getParameter("temporary.prefix")).isEqualTo("hg.hitchhiker.org"); + assertThat(auth.getParameter("temporary.schemes")).isEqualTo("https"); + assertThat(auth.getParameter("temporary.username")).isEqualTo("trillian"); + assertThat(auth.getParameter("temporary.password")).isEqualTo("secret"); + + return null; + }); + } + + private INIConfiguration assertHgrc() throws IOException { + assertThat(hgrc).exists(); + + INIConfigurationReader reader = new INIConfigurationReader(); + return reader.read(hgrc.toFile()); + } + + @Test + void shouldCreateHgrcWithSimpleProxyConfiguration() throws IOException { + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.org"); + configuration.setProxyPort(3128); + configFactory.withContext(commandContext).call(() -> { + INIConfiguration ini = assertHgrc(); + + INISection proxy = ini.getSection("http_proxy"); + assertThat(proxy).isNotNull(); + assertThat(proxy.getParameter("host")).isEqualTo("proxy.hitchhiker.org:3128"); + return null; + }); + } + + @Test + void shouldCreateHgrcProxyConfigurationWithAuthentication() throws IOException { + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.org"); + configuration.setProxyPort(3128); + configuration.setProxyUser("trillian"); + configuration.setProxyPassword("secret"); + + configFactory.withContext(commandContext).call(() -> { + INIConfiguration ini = assertHgrc(); + + INISection proxy = ini.getSection("http_proxy"); + assertThat(proxy).isNotNull(); + assertThat(proxy.getParameter("host")).isEqualTo("proxy.hitchhiker.org:3128"); + assertThat(proxy.getParameter("user")).isEqualTo("trillian"); + assertThat(proxy.getParameter("passwd")).isEqualTo("secret"); + return null; + }); + } + + @Test + void shouldCreateHgrcProxyConfigurationWithNoExcludes() throws IOException { + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.org"); + configuration.setProxyPort(3128); + configuration.setProxyExcludes(ImmutableSet.of("hg.hitchhiker.org", "localhost", "127.0.0.1")); + + configFactory.withContext(commandContext).call(() -> { + INIConfiguration ini = assertHgrc(); + + INISection proxy = ini.getSection("http_proxy"); + assertThat(proxy).isNotNull(); + assertThat(proxy.getParameter("no")).isEqualTo("hg.hitchhiker.org,localhost,127.0.0.1"); + return null; + }); + } + + @Test + void shouldRemoveCreatedHgrc() throws IOException { + configFactory + .withContext(commandContext) + .withCredentials("https://hg.hitchhiker.com", "marvin", "brainLikeAPlanet") + .call(() -> { + assertThat(hgrc).exists(); + return null; + }); + assertThat(hgrc).doesNotExist(); + } + + @Test + void shouldKeepAuthenticationInformation() throws IOException { + writeAuthentication(); + + configFactory + .withContext(commandContext) + .withCredentials("https://hg.hitchhiker.com", "marvin", "brainLikeAPlanet") + .call(() -> { + INIConfiguration configuration = assertHgrc(); + INISection auth = configuration.getSection("auth"); + assertThat(auth).isNotNull(); + assertThat(auth.getParameter("a.username")).isEqualTo("dent"); + assertThat(auth.getParameter("temporary.username")).isEqualTo("marvin"); + return null; + }); + + INIConfiguration configuration = assertHgrc(); + INISection auth = configuration.getSection("auth"); + assertThat(auth).isNotNull(); + assertThat(auth.getParameter("a.username")).isEqualTo("dent"); + assertThat(auth.getParameter("temporary.username")).isNull(); + } + + @Test + void shouldRestoreProxyConfiguration() throws IOException { + writeProxyConfiguration(); + + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.org"); + configuration.setProxyPort(3128); + + configFactory.withContext(commandContext).call(() -> { + INIConfiguration ini = assertHgrc(); + + INISection proxy = ini.getSection("http_proxy"); + assertThat(proxy).isNotNull(); + assertThat(proxy.getParameter("host")).isEqualTo("proxy.hitchhiker.org:3128"); + return null; + }); + + INIConfiguration ini = assertHgrc(); + + INISection proxy = ini.getSection("http_proxy"); + assertThat(proxy).isNotNull(); + assertThat(proxy.getParameter("host")).isEqualTo("awesome.hitchhiker.com:3128"); + } + + private void writeAuthentication() throws IOException { + INIConfiguration configuration = new INIConfiguration(); + INISection auth = new INISection("auth"); + auth.setParameter("a.prefix", "awesome.hitchhiker.com"); + auth.setParameter("a.schemes", "ssh"); + auth.setParameter("a.username", "dent"); + auth.setParameter("a.password", "arthur123"); + configuration.addSection(auth); + INIConfigurationWriter writer = new INIConfigurationWriter(); + writer.write(configuration, hgrc.toFile()); + } + + private void writeProxyConfiguration() throws IOException { + INIConfiguration configuration = new INIConfiguration(); + INISection proxy = new INISection("http_proxy"); + proxy.setParameter("host", "awesome.hitchhiker.com:3128"); + proxy.setParameter("user", "dent"); + proxy.setParameter("passwd", "arthur123"); + configuration.addSection(proxy); + INIConfigurationWriter writer = new INIConfigurationWriter(); + writer.write(configuration, hgrc.toFile()); + } + + +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnMirrorCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnMirrorCommand.java index cb47e21966..a759266ee1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnMirrorCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnMirrorCommand.java @@ -24,24 +24,28 @@ package sonia.scm.repository.spi; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.BasicAuthenticationManager; -import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.auth.SVNAuthentication; import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication; import org.tmatesoft.svn.core.auth.SVNSSLAuthentication; import org.tmatesoft.svn.core.wc.SVNWCUtil; import org.tmatesoft.svn.core.wc.admin.SVNAdminClient; +import sonia.scm.net.GlobalProxyConfiguration; +import sonia.scm.net.ProxyConfiguration; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.api.MirrorCommandResult; import sonia.scm.repository.api.Pkcs12ClientCertificateCredential; import sonia.scm.repository.api.UsernamePasswordCredential; +import javax.annotation.Nonnull; import javax.net.ssl.TrustManager; import java.util.ArrayList; import java.util.Collection; @@ -55,10 +59,12 @@ public class SvnMirrorCommand extends AbstractSvnCommand implements MirrorComman private static final Logger LOG = LoggerFactory.getLogger(SvnMirrorCommand.class); private final TrustManager trustManager; + private final GlobalProxyConfiguration globalProxyConfiguration; - SvnMirrorCommand(SvnContext context, TrustManager trustManager) { + SvnMirrorCommand(SvnContext context, TrustManager trustManager, GlobalProxyConfiguration globalProxyConfiguration) { super(context); this.trustManager = trustManager; + this.globalProxyConfiguration = globalProxyConfiguration; } @Override @@ -126,6 +132,28 @@ public class SvnMirrorCommand extends AbstractSvnCommand implements MirrorComman } private SVNAdminClient createAdminClient(SVNURL url, MirrorCommandRequest mirrorCommandRequest) { + BasicAuthenticationManager authenticationManager = createAuthenticationManager(url, mirrorCommandRequest); + return new SVNAdminClient(authenticationManager, SVNWCUtil.createDefaultOptions(true)); + } + + @VisibleForTesting + BasicAuthenticationManager createAuthenticationManager(SVNURL url, MirrorCommandRequest mirrorCommandRequest) { + SVNAuthentication[] authentications = createAuthentications(url, mirrorCommandRequest); + + BasicAuthenticationManager authManager = new BasicAuthenticationManager(authentications) { + @Override + public TrustManager getTrustManager(SVNURL url) { + return trustManager; + } + }; + checkAndApplyProxyConfiguration( + authManager, mirrorCommandRequest.getProxyConfiguration().orElse(globalProxyConfiguration), url + ); + return authManager; + } + + @Nonnull + private SVNAuthentication[] createAuthentications(SVNURL url, MirrorCommandRequest mirrorCommandRequest) { Collection authentications = new ArrayList<>(); mirrorCommandRequest.getCredential(Pkcs12ClientCertificateCredential.class) .map(c -> createTlsAuth(url, c)) @@ -133,15 +161,26 @@ public class SvnMirrorCommand extends AbstractSvnCommand implements MirrorComman mirrorCommandRequest.getCredential(UsernamePasswordCredential.class) .map(c -> SVNPasswordAuthentication.newInstance(c.username(), c.password(), false, url, false)) .ifPresent(authentications::add); - ISVNAuthenticationManager authManager = new BasicAuthenticationManager( - authentications.toArray(new SVNAuthentication[authentications.size()])) { - @Override - public TrustManager getTrustManager(SVNURL url) { - return trustManager; - } - }; + return authentications.toArray(new SVNAuthentication[0]); + } - return new SVNAdminClient(authManager, SVNWCUtil.createDefaultOptions(true)); + private void checkAndApplyProxyConfiguration(BasicAuthenticationManager authManager, ProxyConfiguration proxyConfiguration, SVNURL url) { + if (proxyConfiguration.isEnabled() && !proxyConfiguration.getExcludes().contains(url.getHost())) { + applyProxyConfiguration(authManager, proxyConfiguration); + } + } + + private void applyProxyConfiguration(BasicAuthenticationManager authManager, ProxyConfiguration proxyConfiguration) { + char[] password = null; + if (!Strings.isNullOrEmpty(proxyConfiguration.getPassword())){ + password = proxyConfiguration.getPassword().toCharArray(); + } + authManager.setProxy( + proxyConfiguration.getHost(), + proxyConfiguration.getPort(), + Strings.emptyToNull(proxyConfiguration.getUsername()), + password + ); } private SVNSSLAuthentication createTlsAuth(SVNURL url, Pkcs12ClientCertificateCredential c) { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index 40ce96c6a7..2e55ea105c 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -26,6 +26,7 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; import com.google.common.io.Closeables; +import sonia.scm.net.GlobalProxyConfiguration; import sonia.scm.repository.Feature; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; @@ -65,16 +66,19 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider { private final SvnWorkingCopyFactory workingCopyFactory; private final HookContextFactory hookContextFactory; private final TrustManager trustManager; + private final GlobalProxyConfiguration globalProxyConfiguration; SvnRepositoryServiceProvider(SvnRepositoryHandler handler, Repository repository, SvnWorkingCopyFactory workingCopyFactory, HookContextFactory hookContextFactory, - TrustManager trustManager) { + TrustManager trustManager, + GlobalProxyConfiguration globalProxyConfiguration) { this.context = new SvnContext(repository, handler.getDirectory(repository.getId())); this.workingCopyFactory = workingCopyFactory; this.hookContextFactory = hookContextFactory; this.trustManager = trustManager; + this.globalProxyConfiguration = globalProxyConfiguration; } @Override @@ -149,6 +153,6 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider { @Override public MirrorCommand getMirrorCommand() { - return new SvnMirrorCommand(context, trustManager); + return new SvnMirrorCommand(context, trustManager, globalProxyConfiguration); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java index 27d277a308..4c19dfa14b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java @@ -25,6 +25,7 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; +import sonia.scm.net.GlobalProxyConfiguration; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; @@ -40,16 +41,19 @@ public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { private final SvnWorkingCopyFactory workingCopyFactory; private final HookContextFactory hookContextFactory; private final TrustManager trustManager; + private final GlobalProxyConfiguration globalProxyConfiguration; @Inject public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, SvnWorkingCopyFactory workingCopyFactory, HookContextFactory hookContextFactory, - TrustManager trustManager) { + TrustManager trustManager, + GlobalProxyConfiguration globalProxyConfiguration) { this.handler = handler; this.workingCopyFactory = workingCopyFactory; this.hookContextFactory = hookContextFactory; this.trustManager = trustManager; + this.globalProxyConfiguration = globalProxyConfiguration; } @Override @@ -57,7 +61,9 @@ public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { SvnRepositoryServiceProvider provider = null; if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new SvnRepositoryServiceProvider(handler, repository, workingCopyFactory, hookContextFactory, trustManager); + provider = new SvnRepositoryServiceProvider( + handler, repository, workingCopyFactory, hookContextFactory, trustManager, globalProxyConfiguration + ); } return provider; diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnMirrorCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnMirrorCommandTest.java index 14088e848d..962a992ae1 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnMirrorCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnMirrorCommandTest.java @@ -32,10 +32,15 @@ import org.mockito.junit.MockitoJUnitRunner; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.BasicAuthenticationManager; +import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.auth.SVNAuthentication; +import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.core.wc.SVNWCUtil; import org.tmatesoft.svn.core.wc.admin.SVNAdminClient; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.net.GlobalProxyConfiguration; +import sonia.scm.net.ProxyConfiguration; import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.api.MirrorCommandResult; import sonia.scm.repository.api.SimpleUsernamePasswordCredential; @@ -45,8 +50,11 @@ import java.io.File; import java.io.IOException; import java.util.function.Consumer; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static sonia.scm.repository.api.MirrorCommandResult.ResultType.OK; @RunWith(MockitoJUnitRunner.class) @@ -57,6 +65,8 @@ public class SvnMirrorCommandTest extends AbstractSvnCommandTestBase { private SvnContext emptyContext; + private final ScmConfiguration configuration = new ScmConfiguration(); + @Before public void bendContextToNewRepository() throws IOException, SVNException { emptyContext = createEmptyContext(); @@ -91,10 +101,119 @@ public class SvnMirrorCommandTest extends AbstractSvnCommandTestBase { } @Test - public void shouldUseCredentials() { - MirrorCommandResult result = callMirror(emptyContext, repositoryDirectory, createCredential("svnadmin", "secret")); + public void shouldUseCredentials() throws SVNException { + SVNURL url = SVNURL.parseURIEncoded("https://svn.hitchhiker.com"); - assertThat(result.getResult()).isEqualTo(OK); + MirrorCommandRequest request = new MirrorCommandRequest(); + request.setCredentials( + singletonList(new SimpleUsernamePasswordCredential("trillian", "secret".toCharArray())) + ); + + SvnMirrorCommand mirrorCommand = createMirrorCommand(emptyContext); + BasicAuthenticationManager authenticationManager = mirrorCommand.createAuthenticationManager(url, request); + + SVNAuthentication authentication = authenticationManager.getFirstAuthentication( + ISVNAuthenticationManager.PASSWORD, "Hitchhiker Auth Gate", url + ); + + assertThat(authentication).isInstanceOfSatisfying(SVNPasswordAuthentication.class, passwordAuth -> { + assertThat(passwordAuth.getUserName()).isEqualTo("trillian"); + assertThat(passwordAuth.getPasswordValue()).isEqualTo("secret".toCharArray()); + }); + } + + @Test + public void shouldUseTrustManager() throws SVNException { + SVNURL url = SVNURL.parseURIEncoded("https://svn.hitchhiker.com"); + + SvnMirrorCommand mirrorCommand = createMirrorCommand(emptyContext); + BasicAuthenticationManager authenticationManager = mirrorCommand.createAuthenticationManager(url, new MirrorCommandRequest()); + + assertThat(authenticationManager.getTrustManager(url)).isSameAs(trustManager); + } + + @Test + public void shouldApplySimpleProxySettings() throws SVNException { + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.com"); + configuration.setProxyPort(3128); + + BasicAuthenticationManager authenticationManager = createAuthenticationManager(); + + assertThat(authenticationManager.getProxyHost()).isEqualTo("proxy.hitchhiker.com"); + assertThat(authenticationManager.getProxyPort()).isEqualTo(3128); + assertThat(authenticationManager.getProxyUserName()).isNull(); + assertThat(authenticationManager.getProxyPasswordValue()).isNull(); + } + + @Test + public void shouldApplyProxySettingsWithCredentials() throws SVNException { + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.com"); + configuration.setProxyPort(3128); + configuration.setProxyUser("trillian"); + configuration.setProxyPassword("secret"); + + BasicAuthenticationManager authenticationManager = createAuthenticationManager(); + + assertThat(authenticationManager.getProxyHost()).isEqualTo("proxy.hitchhiker.com"); + assertThat(authenticationManager.getProxyPort()).isEqualTo(3128); + assertThat(authenticationManager.getProxyUserName()).isEqualTo("trillian"); + assertThat(authenticationManager.getProxyPasswordValue()).isEqualTo("secret".toCharArray()); + } + + @Test + public void shouldSkipProxySettingsIfDisabled() throws SVNException { + configuration.setEnableProxy(false); + configuration.setProxyServer("proxy.hitchhiker.com"); + configuration.setProxyPort(3128); + + BasicAuthenticationManager authenticationManager = createAuthenticationManager(); + + assertThat(authenticationManager.getProxyHost()).isNull(); + } + + @Test + public void shouldSkipProxySettingsIfHostIsOnExcludeList() throws SVNException { + configuration.setEnableProxy(true); + configuration.setProxyServer("proxy.hitchhiker.com"); + configuration.setProxyPort(3128); + configuration.setProxyExcludes(singleton("svn.hitchhiker.com")); + + BasicAuthenticationManager authenticationManager = createAuthenticationManager(); + + assertThat(authenticationManager.getProxyHost()).isNull(); + } + + @Test + public void shouldApplyLocalProxySettings() throws SVNException { + MirrorCommandRequest request = new MirrorCommandRequest(); + request.setProxyConfiguration(createProxyConfiguration()); + + BasicAuthenticationManager authenticationManager = createAuthenticationManager(request); + assertThat(authenticationManager.getProxyHost()).isEqualTo("proxy.hitchhiker.com"); + assertThat(authenticationManager.getProxyPort()).isEqualTo(3128); + assertThat(authenticationManager.getProxyUserName()).isNull(); + assertThat(authenticationManager.getProxyPasswordValue()).isNull(); + } + + private ProxyConfiguration createProxyConfiguration() { + ProxyConfiguration configuration = mock(ProxyConfiguration.class); + when(configuration.isEnabled()).thenReturn(true); + when(configuration.getHost()).thenReturn("proxy.hitchhiker.com"); + when(configuration.getPort()).thenReturn(3128); + return configuration; + } + + private BasicAuthenticationManager createAuthenticationManager() throws SVNException { + return createAuthenticationManager(new MirrorCommandRequest()); + } + + private BasicAuthenticationManager createAuthenticationManager(MirrorCommandRequest request) throws SVNException { + SVNURL url = SVNURL.parseURIEncoded("https://svn.hitchhiker.com"); + + SvnMirrorCommand mirrorCommand = createMirrorCommand(emptyContext); + return mirrorCommand.createAuthenticationManager(url, request); } private MirrorCommandResult callMirrorUpdate(SvnContext context, File source) { @@ -115,11 +234,7 @@ public class SvnMirrorCommandTest extends AbstractSvnCommandTestBase { } private SvnMirrorCommand createMirrorCommand(SvnContext context) { - return new SvnMirrorCommand(context, trustManager); - } - - private Consumer createCredential(String username, String password) { - return request -> request.setCredentials(singletonList(new SimpleUsernamePasswordCredential(username, password.toCharArray()))); + return new SvnMirrorCommand(context, trustManager, new GlobalProxyConfiguration(configuration)); } private SvnContext createEmptyContext() throws SVNException, IOException { diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java index 75acfc97d2..44b6ae8f57 100644 --- a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java @@ -24,39 +24,26 @@ package sonia.scm.net.ahc; -//~--- non-JDK imports -------------------------------------------------------- -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.Multimap; -import com.google.common.io.Closeables; import com.google.inject.Inject; -import org.apache.shiro.codec.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.net.Proxies; -import sonia.scm.net.TrustAllHostnameVerifier; -import sonia.scm.net.TrustAllTrustManager; +import sonia.scm.net.HttpConnectionOptions; +import sonia.scm.net.HttpURLConnectionFactory; import sonia.scm.trace.Span; import sonia.scm.trace.Tracer; import sonia.scm.util.HttpUtil; import javax.annotation.Nonnull; -import javax.inject.Provider; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; import java.io.IOException; import java.io.OutputStream; -import java.net.*; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.Arrays; import java.util.Set; -//~--- JDK imports ------------------------------------------------------------ - /** * Default implementation of the {@link AdvancedHttpClient}. The default * implementation uses {@link HttpURLConnection}. @@ -64,136 +51,51 @@ import java.util.Set; * @author Sebastian Sdorra * @since 1.46 */ -public class DefaultAdvancedHttpClient extends AdvancedHttpClient -{ - - /** proxy authorization header */ - @VisibleForTesting - static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization"; - - /** connection timeout */ - @VisibleForTesting - static final int TIMEOUT_CONNECTION = 30000; - - /** read timeout */ - @VisibleForTesting - static final int TIMEOUT_RAED = 1200000; - - /** credential separator */ - private static final String CREDENTIAL_SEPARATOR = ":"; - - /** basic authentication prefix */ - private static final String PREFIX_BASIC_AUTHENTICATION = "Basic "; +public class DefaultAdvancedHttpClient extends AdvancedHttpClient { /** * the logger for DefaultAdvancedHttpClient */ - private static final Logger logger = - LoggerFactory.getLogger(DefaultAdvancedHttpClient.class); + private static final Logger LOG = LoggerFactory.getLogger(DefaultAdvancedHttpClient.class); + + private final HttpURLConnectionFactory connectionFactory; + private final Tracer tracer; + private final Set contentTransformers; - //~--- constructors --------------------------------------------------------- - /** - * Constructs a new {@link DefaultAdvancedHttpClient}. - * - * - * @param configuration scm-manager main configuration - * @param contentTransformers content transformer - * @param sslContextProvider ssl context provider - */ @Inject - public DefaultAdvancedHttpClient(ScmConfiguration configuration, - Tracer tracer, Set contentTransformers, Provider sslContextProvider) - { - this.configuration = configuration; + public DefaultAdvancedHttpClient(HttpURLConnectionFactory connectionFactory, Tracer tracer, Set contentTransformers) { + this.connectionFactory = connectionFactory; this.tracer = tracer; this.contentTransformers = contentTransformers; - this.sslContextProvider = sslContextProvider; } - //~--- methods -------------------------------------------------------------- - - /** - * Creates a new {@link HttpURLConnection} from the given {@link URL}. The - * method is visible for testing. - * - * - * @param url url - * - * @return new {@link HttpURLConnection} - * - * @throws IOException - */ - @VisibleForTesting - protected HttpURLConnection createConnection(URL url) throws IOException - { - return (HttpURLConnection) url.openConnection(); - } - - /** - * Creates a new proxy {@link HttpURLConnection} from the given {@link URL} - * and {@link SocketAddress}. The method is visible for testing. - * - * - * @param url url - * @param address proxy socket address - * - * @return new proxy {@link HttpURLConnection} - * - * @throws IOException - */ - @VisibleForTesting - protected HttpURLConnection createProxyConnecton(URL url, - SocketAddress address) - throws IOException - { - return (HttpURLConnection) url.openConnection(new Proxy(Proxy.Type.HTTP, - address)); - } - - /** - * {@inheritDoc} - */ @Override - protected ContentTransformer createTransformer(Class type, String contentType) - { + protected ContentTransformer createTransformer(Class type, String contentType) { ContentTransformer responsible = null; - for (ContentTransformer transformer : contentTransformers) - { - if (transformer.isResponsible(type, contentType)) - { + for (ContentTransformer transformer : contentTransformers) { + if (transformer.isResponsible(type, contentType)) { responsible = transformer; break; } } - if (responsible == null) - { + if (responsible == null) { throw new ContentTransformerNotFoundException( - "could not find content transformer for content type ".concat( - contentType)); + "could not find content transformer for content type ".concat(contentType) + ); } return responsible; } - /** - * Executes the given request and returns the server response. - * - * - * @param request http request - * - * @return server response - * - * @throws IOException - */ @Override protected AdvancedHttpResponse request(BaseHttpRequest request) throws IOException { String spanKind = request.getSpanKind(); if (Strings.isNullOrEmpty(spanKind)) { - logger.debug("execute request {} without tracing", request.getUrl()); + LOG.debug("execute request {} without tracing", request.getUrl()); return doRequest(request); } return doRequestWithTracing(request); @@ -230,15 +132,9 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient @Nonnull private DefaultAdvancedHttpResponse doRequest(BaseHttpRequest request) throws IOException { HttpURLConnection connection = openConnection(request, new URL(request.getUrl())); - - applyBaseSettings(request, connection); - - if (connection instanceof HttpsURLConnection) { - applySSLSettings(request, (HttpsURLConnection) connection); - } + connection.setRequestMethod(request.getMethod()); Content content = null; - if (request instanceof AdvancedHttpRequestWithBody) { AdvancedHttpRequestWithBody ahrwb = (AdvancedHttpRequestWithBody) request; @@ -259,164 +155,43 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient applyContent(connection, content); } - return new DefaultAdvancedHttpResponse(this, connection, - connection.getResponseCode(), connection.getResponseMessage()); + return new DefaultAdvancedHttpResponse( + this, connection, connection.getResponseCode(), connection.getResponseMessage() + ); } - private void appendProxyAuthentication(HttpURLConnection connection) - { - String username = configuration.getProxyUser(); - String password = configuration.getProxyPassword(); - - if (!Strings.isNullOrEmpty(username) ||!Strings.isNullOrEmpty(password)) - { - logger.debug("append proxy authentication header for user {}", username); - - String auth = username.concat(CREDENTIAL_SEPARATOR).concat(password); - - auth = Base64.encodeToString(auth.getBytes()); - connection.addRequestProperty(HEADER_PROXY_AUTHORIZATION, - PREFIX_BASIC_AUTHENTICATION.concat(auth)); - } - } - - private void applyBaseSettings(BaseHttpRequest request, - HttpURLConnection connection) - throws ProtocolException - { - connection.setRequestMethod(request.getMethod()); - connection.setReadTimeout(TIMEOUT_RAED); - connection.setConnectTimeout(TIMEOUT_CONNECTION); - } - - private void applyContent(HttpURLConnection connection, Content content) - throws IOException - { + private void applyContent(HttpURLConnection connection, Content content) throws IOException { connection.setDoOutput(true); - - OutputStream output = null; - - try - { - output = connection.getOutputStream(); + try (OutputStream output = connection.getOutputStream()) { content.process(output); } - finally - { - Closeables.close(output, true); - } } - private void applyHeaders(BaseHttpRequest request, - HttpURLConnection connection) - { + private void applyHeaders(BaseHttpRequest request, HttpURLConnection connection) { Multimap headers = request.getHeaders(); - - for (String key : headers.keySet()) - { - for (String value : headers.get(key)) - { + for (String key : headers.keySet()) { + for (String value : headers.get(key)) { connection.addRequestProperty(key, value); } } } - private void applySSLSettings(BaseHttpRequest request, - HttpsURLConnection connection) - { - if (request.isDisableCertificateValidation()) - { - logger.trace("disable certificate validation"); - - try - { - TrustManager[] trustAllCerts = new TrustManager[] { - new TrustAllTrustManager() }; - SSLContext sc = SSLContext.getInstance("TLS"); - - sc.init(null, trustAllCerts, new java.security.SecureRandom()); - connection.setSSLSocketFactory(sc.getSocketFactory()); - } - catch (KeyManagementException | NoSuchAlgorithmException ex) - { - logger.error("could not disable certificate validation", ex); - } - } - else - { - logger.trace("set ssl socket factory from provider"); - connection.setSSLSocketFactory(sslContextProvider.get().getSocketFactory()); - } - - if (request.isDisableHostnameValidation()) - { - logger.trace("disable hostname validation"); - connection.setHostnameVerifier(new TrustAllHostnameVerifier()); - } + private HttpURLConnection openConnection(BaseHttpRequest request, URL url) throws IOException { + return connectionFactory.create(url, createOptionsFromRequest(request)); } - private HttpURLConnection openConnection(BaseHttpRequest request, URL url) - throws IOException - { - HttpURLConnection connection; - - if (isProxyEnabled(request)) - { - connection = openProxyConnection(url); - appendProxyAuthentication(connection); + private HttpConnectionOptions createOptionsFromRequest(BaseHttpRequest request) { + HttpConnectionOptions options = new HttpConnectionOptions(); + if (request.isDisableCertificateValidation()) { + options.withDisableCertificateValidation(); } - else - { - if (request.isIgnoreProxySettings()) - { - logger.trace("ignore proxy settings"); - } - - if (logger.isDebugEnabled()) { - logger.debug("fetch {}", url.toExternalForm()); - } - - connection = createConnection(url); + if (request.isDisableHostnameValidation()) { + options.withDisabledHostnameValidation(); } - - return connection; + if (request.isIgnoreProxySettings()) { + options.withIgnoreProxySettings(); + } + return options; } - private HttpURLConnection openProxyConnection(URL url) - throws IOException - { - if (logger.isDebugEnabled()) - { - logger.debug("fetch '{}' using proxy {}:{}", url.toExternalForm(), - configuration.getProxyServer(), configuration.getProxyPort()); - } - - SocketAddress address = - new InetSocketAddress(configuration.getProxyServer(), - configuration.getProxyPort()); - - return createProxyConnecton(url, address); - } - - //~--- get methods ---------------------------------------------------------- - - private boolean isProxyEnabled(BaseHttpRequest request) - { - return !request.isIgnoreProxySettings() - && Proxies.isEnabled(configuration, request.getUrl()); - } - - //~--- fields --------------------------------------------------------------- - - /** scm-manager main configuration */ - private final ScmConfiguration configuration; - - /** set of content transformers */ - private final Set contentTransformers; - - /** ssl context provider */ - private final Provider sslContextProvider; - - /** tracer used for request tracing */ - private final Tracer tracer; } diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java index b649d69a73..084dbd0e9e 100644 --- a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java +++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponse.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.net.ahc; //~--- non-JDK imports -------------------------------------------------------- @@ -61,7 +61,7 @@ public class DefaultAdvancedHttpResponse extends AdvancedHttpResponse * @param statusText response status text */ DefaultAdvancedHttpResponse(DefaultAdvancedHttpClient client, - HttpURLConnection connection, int status, String statusText) +HttpURLConnection connection, int status, String statusText) { this.client = client; this.connection = connection; diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java index d81c806471..b2cbfc2f7e 100644 --- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java @@ -24,110 +24,135 @@ package sonia.scm.net.ahc; -//~--- non-JDK imports -------------------------------------------------------- - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.google.inject.util.Providers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.config.ScmConfiguration; -import sonia.scm.net.SSLContextProvider; -import sonia.scm.net.TrustAllHostnameVerifier; +import sonia.scm.net.GlobalProxyConfiguration; +import sonia.scm.net.HttpURLConnectionFactory; import sonia.scm.trace.Span; import sonia.scm.trace.Tracer; import sonia.scm.util.HttpUtil; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.SocketAddress; -import java.net.URL; +import java.net.Proxy; import java.util.HashSet; import java.util.Set; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) -public class DefaultAdvancedHttpClientTest -{ +@ExtendWith(MockitoExtension.class) +class DefaultAdvancedHttpClientTest { + + private static final int TIMEOUT_CONNECTION = 30000; + private static final int TIMEOUT_READ = 1200000; + + @Mock + private HttpsURLConnection connection; + + @Mock + private Tracer tracer; + + @Mock + private Span span; + + @Mock + private TrustManager trustManager; + + private Set transformers; + + private ScmConfiguration configuration; + + private DefaultAdvancedHttpClient client; + + private Proxy proxy; + + @BeforeEach + void setUp() { + configuration = new ScmConfiguration(); + transformers = new HashSet<>(); + HttpURLConnectionFactory connectionFactory = new HttpURLConnectionFactory( + new GlobalProxyConfiguration(configuration), + Providers.of(trustManager), + (url, proxy) -> { + this.proxy = proxy; + return connection; + }, + () -> SSLContext.getInstance("TLS") + ); + + client = new DefaultAdvancedHttpClient(connectionFactory, tracer, transformers); + lenient().when(tracer.span(anyString())).thenReturn(span); + } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testApplyBaseSettings() throws IOException - { - new AdvancedHttpRequest(client, HttpMethod.GET, - "https://www.scm-manager.org").request(); + void shouldApplyBaseSettings() throws IOException { + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).request(); + verify(connection).setRequestMethod(HttpMethod.GET); - verify(connection).setReadTimeout(DefaultAdvancedHttpClient.TIMEOUT_RAED); - verify(connection).setConnectTimeout( - DefaultAdvancedHttpClient.TIMEOUT_CONNECTION); + verify(connection).setReadTimeout(TIMEOUT_READ); + verify(connection).setConnectTimeout(TIMEOUT_CONNECTION); verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "0"); } - @Test(expected = ContentTransformerNotFoundException.class) - public void testContentTransformerNotFound(){ - client.createTransformer(String.class, "text/plain"); + @Test + void shouldThrowContentTransformerNotFound(){ + assertThrows(ContentTransformerNotFoundException.class, () -> client.createTransformer(String.class, "text/plain")); } @Test - public void testContentTransformer(){ + void shouldCreateContentTransformer() { ContentTransformer transformer = mock(ContentTransformer.class); when(transformer.isResponsible(String.class, "text/plain")).thenReturn(Boolean.TRUE); transformers.add(transformer); ContentTransformer t = client.createTransformer(String.class, "text/plain"); - assertSame(transformer, t); + assertThat(t).isSameAs(transformer); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testApplyContent() throws IOException - { + void shouldApplyContent() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); when(connection.getOutputStream()).thenReturn(baos); - AdvancedHttpRequestWithBody request = - new AdvancedHttpRequestWithBody(client, HttpMethod.PUT, - "https://www.scm-manager.org"); + AdvancedHttpRequestWithBody request = new AdvancedHttpRequestWithBody( + client, HttpMethod.PUT, "https://www.scm-manager.org" + ); request.stringContent("test").request(); verify(connection).setDoOutput(true); verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "4"); - assertEquals("test", baos.toString("UTF-8")); + assertThat(baos.toString("UTF-8")).isEqualTo("test"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testApplyHeaders() throws IOException - { - AdvancedHttpRequest request = new AdvancedHttpRequest(client, - HttpMethod.POST, - "http://www.scm-manager.org"); + void shouldApplyHeaders() throws IOException { + AdvancedHttpRequest request = new AdvancedHttpRequest( + client, HttpMethod.POST, "http://www.scm-manager.org" + ); request.header("Header-One", "One").header("Header-Two", "Two").request(); verify(connection).setRequestMethod(HttpMethod.POST); @@ -135,18 +160,11 @@ public class DefaultAdvancedHttpClientTest verify(connection).addRequestProperty("Header-Two", "Two"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testApplyMultipleHeaders() throws IOException - { - AdvancedHttpRequest request = new AdvancedHttpRequest(client, - HttpMethod.POST, - "http://www.scm-manager.org"); + void shouldApplyMultipleHeaders() throws IOException { + AdvancedHttpRequest request = new AdvancedHttpRequest( + client, HttpMethod.POST, "http://www.scm-manager.org" + ); request.header("Header-One", "One").header("Header-One", "Two").request(); verify(connection).setRequestMethod(HttpMethod.POST); @@ -154,118 +172,71 @@ public class DefaultAdvancedHttpClientTest verify(connection).addRequestProperty("Header-One", "Two"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testBodyRequestWithoutContent() throws IOException - { - AdvancedHttpRequestWithBody request = - new AdvancedHttpRequestWithBody(client, HttpMethod.PUT, - "https://www.scm-manager.org"); + void shouldReturnRequestWithoutContent() throws IOException { + AdvancedHttpRequestWithBody request = new AdvancedHttpRequestWithBody( + client, HttpMethod.PUT, "https://www.scm-manager.org"); request.request(); verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "0"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testDisableCertificateValidation() throws IOException - { - AdvancedHttpRequest request = new AdvancedHttpRequest(client, - HttpMethod.GET, - "https://www.scm-manager.org"); + void shouldDisableCertificateValidation() throws IOException { + AdvancedHttpRequest request = new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ); request.disableCertificateValidation(true).request(); + verify(connection).setSSLSocketFactory(any(SSLSocketFactory.class)); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testDisableHostnameValidation() throws IOException - { - AdvancedHttpRequest request = new AdvancedHttpRequest(client, - HttpMethod.GET, - "https://www.scm-manager.org"); + void shouldDisableHostnameValidation() throws IOException { + AdvancedHttpRequest request = new AdvancedHttpRequest( + client, HttpMethod.GET,"https://www.scm-manager.org" + ); request.disableHostnameValidation(true).request(); - verify(connection).setHostnameVerifier(any(TrustAllHostnameVerifier.class)); + + verify(connection).setHostnameVerifier(any(HostnameVerifier.class)); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testIgnoreProxy() throws IOException - { + void shouldIgnoreProxySettings() throws IOException { configuration.setProxyServer("proxy.scm-manager.org"); configuration.setProxyPort(8090); configuration.setEnableProxy(true); - new AdvancedHttpRequest(client, HttpMethod.GET, - "https://www.scm-manager.org").ignoreProxySettings(true).request(); - assertFalse(client.proxyConnection); + + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).ignoreProxySettings(true).request(); + + assertThat(proxy).isNull(); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testProxyConnection() throws IOException - { + void shouldUseProxyConnection() throws IOException { configuration.setProxyServer("proxy.scm-manager.org"); configuration.setProxyPort(8090); configuration.setEnableProxy(true); - new AdvancedHttpRequest(client, HttpMethod.GET, - "https://www.scm-manager.org").request(); - assertTrue(client.proxyConnection); - } - /** - * Method description - * - * - * @throws IOException - */ - @Test - public void testProxyWithAuthentication() throws IOException - { - configuration.setProxyServer("proxy.scm-manager.org"); - configuration.setProxyPort(8090); - configuration.setProxyUser("tricia"); - configuration.setProxyPassword("tricias secret"); - configuration.setEnableProxy(true); - new AdvancedHttpRequest(client, HttpMethod.GET, - "https://www.scm-manager.org").request(); - assertTrue(client.proxyConnection); - verify(connection).addRequestProperty( - DefaultAdvancedHttpClient.HEADER_PROXY_AUTHORIZATION, - "Basic dHJpY2lhOnRyaWNpYXMgc2VjcmV0"); + new AdvancedHttpRequest( + client, HttpMethod.GET,"https://www.scm-manager.org" + ).request(); + + assertThat(proxy).isNotNull(); } @Test - public void shouldCreateTracingSpan() throws IOException { + void shouldCreateTracingSpan() throws IOException { when(connection.getResponseCode()).thenReturn(200); - new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").spanKind("spaceships").request(); + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).spanKind("spaceships").request(); + verify(tracer).span("spaceships"); verify(span).label("url", "https://www.scm-manager.org"); verify(span).label("method", "GET"); @@ -275,10 +246,13 @@ public class DefaultAdvancedHttpClientTest } @Test - public void shouldCreateFailedTracingSpan() throws IOException { + void shouldCreateFailedTracingSpan() throws IOException { when(connection.getResponseCode()).thenReturn(500); - new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").request(); + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).request(); + verify(tracer).span("HTTP Request"); verify(span).label("url", "https://www.scm-manager.org"); verify(span).label("method", "GET"); @@ -288,16 +262,20 @@ public class DefaultAdvancedHttpClientTest } @Test - public void shouldCreateFailedTracingSpanOnIOException() throws IOException { + void shouldCreateFailedTracingSpanOnIOException() throws IOException { when(connection.getResponseCode()).thenThrow(new IOException("failed")); boolean thrown = false; try { - new AdvancedHttpRequest(client, HttpMethod.DELETE, "http://failing.host").spanKind("failures").request(); + + new AdvancedHttpRequest( + client, HttpMethod.DELETE, "http://failing.host" + ).spanKind("failures").request(); + } catch (IOException ex) { thrown = true; } - assertTrue(thrown); + assertThat(thrown).isTrue(); verify(tracer).span("failures"); verify(span).label("url", "http://failing.host"); @@ -309,19 +287,24 @@ public class DefaultAdvancedHttpClientTest } @Test - public void shouldNotCreateSpan() throws IOException { + void shouldNotCreateSpan() throws IOException { when(connection.getResponseCode()).thenReturn(200); - new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org") - .disableTracing().request(); + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).disableTracing().request(); + verify(tracer, never()).span(anyString()); } @Test - public void shouldNotTraceRequestIfAcceptedResponseCode() throws IOException { + void shouldNotTraceRequestIfAcceptedResponseCode() throws IOException { when(connection.getResponseCode()).thenReturn(400); - new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").acceptStatusCodes(400).request(); + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).acceptStatusCodes(400).request(); + verify(tracer).span("HTTP Request"); verify(span).label("status", 400); verify(span, never()).failed(); @@ -329,120 +312,16 @@ public class DefaultAdvancedHttpClientTest } @Test - public void shouldTraceRequestAsFailedIfAcceptedResponseCodeDoesntMatch() throws IOException { + void shouldTraceRequestAsFailedIfAcceptedResponseCodeDoesntMatch() throws IOException { when(connection.getResponseCode()).thenReturn(401); - new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").acceptStatusCodes(400).request(); + new AdvancedHttpRequest( + client, HttpMethod.GET, "https://www.scm-manager.org" + ).acceptStatusCodes(400).request(); + verify(tracer).span("HTTP Request"); verify(span).label("status", 401); verify(span).failed(); verify(span).close(); } - - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - */ - @Before - public void setUp() - { - configuration = new ScmConfiguration(); - transformers = new HashSet<>(); - client = new TestingAdvacedHttpClient(configuration, transformers); - when(tracer.span(anyString())).thenReturn(span); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 15/05/01 - * @author Enter your name here... - */ - public class TestingAdvacedHttpClient extends DefaultAdvancedHttpClient - { - - /** - * Constructs ... - * - * - * @param configuration - * @param transformers - */ - public TestingAdvacedHttpClient(ScmConfiguration configuration, Set transformers) - { - super(configuration, tracer, transformers, new SSLContextProvider()); - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param url - * - * @return - * - * @throws IOException - */ - @Override - protected HttpURLConnection createConnection(URL url) throws IOException - { - return connection; - } - - /** - * Method description - * - * - * @param url - * @param address - * - * @return - * - * @throws IOException - */ - @Override - protected HttpURLConnection createProxyConnecton(URL url, - SocketAddress address) - throws IOException - { - proxyConnection = true; - - return connection; - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private boolean proxyConnection = false; - } - - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private TestingAdvacedHttpClient client; - - /** Field description */ - private ScmConfiguration configuration; - - /** Field description */ - @Mock - private HttpsURLConnection connection; - - /** Field description */ - private Set transformers; - - @Mock - private Tracer tracer; - - @Mock - private Span span; } diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java index e358820a2e..2dc982544d 100644 --- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java +++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java @@ -31,101 +31,73 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.io.ByteSource; - import org.hamcrest.Matchers; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import sonia.scm.config.ScmConfiguration; - -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.net.HttpURLConnectionFactory; +import sonia.scm.trace.Tracer; import java.io.ByteArrayInputStream; import java.io.IOException; - import java.net.HttpURLConnection; - -import java.util.HashSet; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; -import sonia.scm.net.SSLContextProvider; -import sonia.scm.trace.Tracer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +//~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) -public class DefaultAdvancedHttpResponseTest -{ +@ExtendWith(MockitoExtension.class) +class DefaultAdvancedHttpResponseTest { + + @Mock + private HttpURLConnection connection; private DefaultAdvancedHttpClient client; - @Before - public void setUpClient() { - client = new DefaultAdvancedHttpClient(new ScmConfiguration(), tracer, new HashSet<>(), new SSLContextProvider()); + @BeforeEach + void setUpClient() { + client = new DefaultAdvancedHttpClient(mock(HttpURLConnectionFactory.class), mock(Tracer.class), Collections.emptySet()); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testContentAsByteSource() throws IOException - { - ByteArrayInputStream bais = - new ByteArrayInputStream("test".getBytes(Charsets.UTF_8)); - + void shouldReturnContentAsByteSource() throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream("test".getBytes(Charsets.UTF_8)); when(connection.getInputStream()).thenReturn(bais); - AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, - connection, 200, "OK"); + AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, connection, 200, "OK"); ByteSource content = response.contentAsByteSource(); - assertEquals("test", content.asCharSource(Charsets.UTF_8).read()); + assertThat(content.asCharSource(Charsets.UTF_8).read()).isEqualTo("test"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - @SuppressWarnings("unchecked") - public void testContentAsByteSourceWithFailedRequest() throws IOException - { - ByteArrayInputStream bais = - new ByteArrayInputStream("test".getBytes(Charsets.UTF_8)); - + void shouldReturnContentAsByteSourceEvenForFailedRequests() throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream("test".getBytes(Charsets.UTF_8)); when(connection.getInputStream()).thenThrow(IOException.class); when(connection.getErrorStream()).thenReturn(bais); - AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, - connection, 404, "NOT FOUND"); + AdvancedHttpResponse response = new DefaultAdvancedHttpResponse(client, connection, 404, "NOT FOUND"); ByteSource content = response.contentAsByteSource(); - assertEquals("test", content.asCharSource(Charsets.UTF_8).read()); + assertThat(content.asCharSource(Charsets.UTF_8).read()).isEqualTo("test"); } - /** - * Method description - * - */ @Test - public void testGetHeaders() - { + void shouldReturnHeaders() { LinkedHashMap> map = Maps.newLinkedHashMap(); List test = Lists.newArrayList("One", "Two"); @@ -136,14 +108,7 @@ public class DefaultAdvancedHttpResponseTest connection, 200, "OK"); Multimap headers = response.getHeaders(); - assertThat(headers.get("Test"), Matchers.contains("One", "Two")); - assertTrue(headers.get("Test-2").isEmpty()); + assertThat(headers.get("Test")).containsOnly("One", "Two"); + assertThat(headers.get("Test-2")).isEmpty(); } - - /** Field description */ - @Mock - private HttpURLConnection connection; - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Tracer tracer; }