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;
}