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

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

View File

@@ -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))

View File

@@ -43,7 +43,9 @@ dependencies {
// lombok
compileOnly libraries.lombok
testCompileOnly libraries.lombok
annotationProcessor libraries.lombok
testAnnotationProcessor libraries.lombok
// servlet api
implementation libraries.servletApi

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

@@ -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<TrustManager> trustManagerProvider;
private final HttpURLConnectionFactory httpURLConnectionFactory;
@Inject
public MirrorHttpConnectionProvider(Provider<TrustManager> trustManagerProvider) {
this.trustManagerProvider = trustManagerProvider;
public MirrorHttpConnectionProvider(HttpURLConnectionFactory httpURLConnectionFactory) {
this.httpURLConnectionFactory = httpURLConnectionFactory;
}
public HttpConnectionFactory2 createHttpConnectionFactory(Pkcs12ClientCertificateCredential credential, List<String> log) {
return new ScmHttpConnectionFactory(trustManagerProvider, createKeyManagers(credential, log));
public HttpConnectionFactory createHttpConnectionFactory(MirrorCommandRequest mirrorCommandRequest, List<String> 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<String> log) {

View File

@@ -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<TrustManager> trustManagerProvider;
private final KeyManager[] keyManagers;
private final HttpURLConnectionFactory connectionFactory;
private final HttpConnectionOptions options;
@Inject
public ScmHttpConnectionFactory(Provider<TrustManager> trustManagerProvider) {
this(trustManagerProvider, null);
public ScmHttpConnectionFactory(HttpURLConnectionFactory connectionFactory) {
this(connectionFactory, new HttpConnectionOptions());
}
public ScmHttpConnectionFactory(Provider<TrustManager> 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);
}
}

View File

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

View File

@@ -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<MirrorCommandRequest> ACCEPT_ALL = r -> {
};
public static final Consumer<MirrorCommandRequest> 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,

View File

@@ -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<HttpConnectionOptions> 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<String> 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();
}
}

View File

@@ -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<HttpConnectionOptions> 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);
}
}

View File

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

View File

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

View File

@@ -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
{

View File

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

View File

@@ -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<Iterable<Changeset>> {
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

View File

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

View File

@@ -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
{

View File

@@ -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<Changeset> 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<Changeset> 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));
}
}

View File

@@ -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<Changeset> 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<Changeset> 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());

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -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> T call(HgCallable<T> 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> {
T call() throws IOException;
}
}

View File

@@ -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();
}
}

View File

@@ -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<Changeset> changesets = changesetResolver.call();
Changeset firstChangeset = changesets.iterator().next();

View File

@@ -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

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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<SVNAuthentication> 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) {

View File

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

View File

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

View File

@@ -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<MirrorCommandRequest> 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 {

View File

@@ -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<ContentTransformer> 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<ContentTransformer> contentTransformers, Provider<SSLContext> sslContextProvider)
{
this.configuration = configuration;
public DefaultAdvancedHttpClient(HttpURLConnectionFactory connectionFactory, Tracer tracer, Set<ContentTransformer> 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<String, String> 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<ContentTransformer> contentTransformers;
/** ssl context provider */
private final Provider<SSLContext> sslContextProvider;
/** tracer used for request tracing */
private final Tracer tracer;
}

View File

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

View File

@@ -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<ContentTransformer> 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<ContentTransformer> 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<ContentTransformer> transformers;
@Mock
private Tracer tracer;
@Mock
private Span span;
}

View File

@@ -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<String, List<String>> map = Maps.newLinkedHashMap();
List<String> test = Lists.newArrayList("One", "Two");
@@ -136,14 +108,7 @@ public class DefaultAdvancedHttpResponseTest
connection, 200, "OK");
Multimap<String, String> 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;
}