diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactory.java index a163a0ac..70d191b2 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactory.java @@ -15,27 +15,41 @@ */ package org.springframework.cloud.config.server.environment; -import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; import org.eclipse.jgit.transport.http.HttpConnection; import org.eclipse.jgit.transport.http.apache.HttpClientConnection; import org.springframework.cloud.config.server.support.HttpClientSupport; +import org.springframework.util.StringUtils; + +import static java.util.stream.Collectors.toMap; /** * @author Dylan Roberts */ public class HttpClientConfigurableHttpConnectionFactory implements ConfigurableHttpConnectionFactory { + Log log = LogFactory.getLog(getClass()); - private Map httpClientsByUri = new HashMap<>(); + private static final String PLACEHOLDER_PATTERN = "\\{(\\w+)}"; + + Map httpClientBuildersByUri = new LinkedHashMap<>(); @Override public void addConfiguration(MultipleJGitEnvironmentProperties environmentProperties) @@ -53,22 +67,81 @@ public class HttpClientConfigurableHttpConnectionFactory implements Configurable @Override public HttpConnection create(URL url, Proxy proxy) throws IOException { - return new HttpClientConnection(url.toString(), proxy, lookupHttpClientBuilder(url).build()); + return new HttpClientConnection(url.toString(), null, lookupHttpClientBuilder(url).build()); } private void addHttpClient(JGitEnvironmentProperties properties) throws GeneralSecurityException { if (properties.getUri().startsWith("http")) { - httpClientsByUri.put(properties.getUri(), HttpClientSupport.builder(properties)); + httpClientBuildersByUri.put(properties.getUri(), HttpClientSupport.builder(properties)); } } - private HttpClientBuilder lookupHttpClientBuilder(URL url) throws MalformedURLException { - String spec = url.toString(); - HttpClientBuilder builder = httpClientsByUri.get(spec); - while (builder == null) { - spec = spec.substring(0, spec.lastIndexOf(File.separator)); - builder = httpClientsByUri.get(spec); + private HttpClientBuilder lookupHttpClientBuilder(final URL url) { + Map builderMap = httpClientBuildersByUri.entrySet().stream().filter(entry -> { + String key = entry.getKey(); + String spec = getUrlWithPlaceholders(url, key); + if (spec.equals(key)) { + return true; + } + int index = spec.lastIndexOf("/"); + while (index != -1) { + spec = spec.substring(0, index); + if (spec.equals(key)) { + return true; + } + index = spec.lastIndexOf("/"); + } + return false; + }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (builderMap.isEmpty()) { + log.warn(String.format("No custom http config found for URL: %s", url)); + return HttpClients.custom(); } - return builder; + if (builderMap.size() > 1) { + log.error(String.format("More than one git repo URL template matched URL: %s, proxy and skipSslValidation config won't be applied. Matched templates: %s", url, builderMap.keySet().stream().collect(Collectors.joining(", ")))); + return HttpClients.custom(); + } + return new ArrayList<>(builderMap.values()).get(0); } + + private String getUrlWithPlaceholders(URL url, String key) { + String spec = url.toString(); + String[] tokens = key.split(PLACEHOLDER_PATTERN); + if (tokens.length > 1) { + List placeholders = getPlaceholders(key); + List values = getValues(spec, tokens); + if (placeholders.size() == values.size()) { + for (int i = 0; i < values.size(); i++) { + spec = spec.replace(values.get(i), String.format("{%s}", placeholders.get(i))); + } + } + } + return spec; + } + + private List getValues(String spec, String[] tokens) { + List values = new LinkedList<>(); + for (String token : tokens) { + String[] valueTokens = spec.split(token); + if (!StringUtils.isEmpty(valueTokens[0])) { + values.add(valueTokens[0]); + } + if (valueTokens.length > 1) { + spec = valueTokens[1]; + } + } + return values; + } + + private List getPlaceholders(String key) { + Pattern pattern = Pattern.compile(PLACEHOLDER_PATTERN); + Matcher matcher = pattern.matcher(key); + List placeholders = new LinkedList<>(); + while (matcher.find()) { + placeholders.add(matcher.group(1)); + } + return placeholders; + } + } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryHttpProxyTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/ConfigurableHttpConnectionFactoryIntegrationTests.java similarity index 87% rename from spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryHttpProxyTests.java rename to spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/ConfigurableHttpConnectionFactoryIntegrationTests.java index d7bc11c7..fbb62160 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryHttpProxyTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/ConfigurableHttpConnectionFactoryIntegrationTests.java @@ -53,7 +53,7 @@ import static org.hamcrest.Matchers.instanceOf; /** * @author Dylan Roberts */ -public class JGitEnvironmentRepositoryHttpProxyTests { +public class ConfigurableHttpConnectionFactoryIntegrationTests { private static final ProxyHostProperties AUTHENTICATED_HTTP_PROXY = new ProxyHostProperties(); static { AUTHENTICATED_HTTP_PROXY.setHost("http://authenticated.http.proxy"); @@ -112,6 +112,20 @@ public class JGitEnvironmentRepositoryHttpProxyTests { makeRequest(httpClient, "https://somehost"); } + @Test + public void httpsProxy_placeholderUrl() throws Exception { + new SpringApplicationBuilder(TestConfiguration.class) + .web(WebApplicationType.NONE) + .properties(gitProperties("https://myrepo/{placeholder1}/{placeholder2}-repo.git", null, HTTPS_PROXY)) + .run(); + HttpClient httpClient = getHttpClientForUrl("https://myrepo/someplaceholdervalue/anotherplaceholdervalue-repo.git"); + expectedException.expectCause(allOf( + instanceOf(UnknownHostException.class), + hasProperty("message", containsString(HTTPS_PROXY.getHost())))); + + makeRequest(httpClient, "https://somehost"); + } + @Test public void httpsProxy_notCalled() throws Exception { String repoUrl = "https://myrepo/repo.git"; @@ -157,6 +171,20 @@ public class JGitEnvironmentRepositoryHttpProxyTests { makeRequest(httpClient, "http://somehost"); } + @Test + public void httpProxy_placeholderUrl() throws Exception { + new SpringApplicationBuilder(TestConfiguration.class) + .web(WebApplicationType.NONE) + .properties(gitProperties("https://myrepo/{placeholder}-repo.git", HTTP_PROXY, null)) + .run(); + HttpClient httpClient = getHttpClientForUrl("https://myrepo/someplaceholdervalue-repo.git"); + expectedException.expectCause(allOf( + instanceOf(UnknownHostException.class), + hasProperty("message", containsString(HTTP_PROXY.getHost())))); + + makeRequest(httpClient, "http://somehost"); + } + @Test public void httpProxy_notCalled() throws Exception { String repoUrl = "https://myrepo/repo.git"; diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactoryTest.java new file mode 100644 index 00000000..03764048 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactoryTest.java @@ -0,0 +1,185 @@ +package org.springframework.cloud.config.server.environment; + +import java.lang.reflect.Field; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.eclipse.jgit.transport.http.HttpConnection; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HttpClientConfigurableHttpConnectionFactoryTest { + + private HttpClientConfigurableHttpConnectionFactory connectionFactory; + + @Before + public void setUp() { + connectionFactory = new HttpClientConfigurableHttpConnectionFactory(); + } + + @Test + public void noConfigAdded() throws Exception { + HttpConnection actual = connectionFactory.create(new URL("http://localhost/test.git")); + + assertThat(actual).isNotNull(); + } + + @Test + public void matchingUrl() throws Exception { + String url = "http://localhost/test.git"; + MultipleJGitEnvironmentProperties properties = new MultipleJGitEnvironmentProperties(); + properties.setUri(url); + connectionFactory.addConfiguration(properties); + + HttpConnection actualConnection = connectionFactory.create(new URL(url)); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.values().stream().findFirst().get(); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + @Test + public void longerUrl() throws Exception { + MultipleJGitEnvironmentProperties properties = new MultipleJGitEnvironmentProperties(); + String url = "http://localhost/test.git"; + properties.setUri(url); + connectionFactory.addConfiguration(properties); + + HttpConnection actualConnection = connectionFactory.create(new URL(url + "/some/path.properties")); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.values().stream().findFirst().get(); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + @Test + public void urlWithPlaceholders() throws Exception { + MultipleJGitEnvironmentProperties properties = new MultipleJGitEnvironmentProperties(); + properties.setUri("http://localhost/{placeholder}-test.git"); + connectionFactory.addConfiguration(properties); + + HttpConnection actualConnection = connectionFactory.create(new URL("http://localhost/value-test.git" + "/some/path.properties")); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.values().stream().findFirst().get(); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + @Test + public void composite_sameHost() throws Exception { + MultipleJGitEnvironmentProperties properties1 = new MultipleJGitEnvironmentProperties(); + properties1.setUri("http://localhost/test1.git"); + MultipleJGitEnvironmentProperties properties2 = new MultipleJGitEnvironmentProperties(); + properties2.setUri("http://localhost/test2.git"); + connectionFactory.addConfiguration(properties1); + connectionFactory.addConfiguration(properties2); + + HttpConnection actualConnection = connectionFactory.create(new URL(properties1.getUri())); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.get(properties1.getUri()); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + @Test + public void composite_differentHost() throws Exception { + MultipleJGitEnvironmentProperties properties1 = new MultipleJGitEnvironmentProperties(); + properties1.setUri("http://localhost1/test.git"); + MultipleJGitEnvironmentProperties properties2 = new MultipleJGitEnvironmentProperties(); + properties2.setUri("http://localhost2/test.git"); + connectionFactory.addConfiguration(properties1); + connectionFactory.addConfiguration(properties2); + + HttpConnection actualConnection = connectionFactory.create(new URL(properties1.getUri())); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.get(properties1.getUri()); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + @Test + public void composite_urlsWithPlaceholders() throws Exception { + MultipleJGitEnvironmentProperties properties1 = new MultipleJGitEnvironmentProperties(); + properties1.setUri("http://localhost/path/{placeholder3}/more/test.git"); + MultipleJGitEnvironmentProperties properties2 = new MultipleJGitEnvironmentProperties(); + properties2.setUri("http://localhost/{placeholder1}/path/{placeholder2}-test.git"); + connectionFactory.addConfiguration(properties1); + connectionFactory.addConfiguration(properties2); + + HttpConnection actualConnection = connectionFactory.create(new URL(properties2.getUri() + .replace("{placeholder1}", "value1") + .replace("{placeholder2}", "value2"))); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.get(properties2.getUri()); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + @Test + public void composite_urlsWithPlaceholders_identicalTemplatesWontBeResolvedProperly() throws Exception { + MultipleJGitEnvironmentProperties properties1 = new MultipleJGitEnvironmentProperties(); + properties1.setUri("http://localhost/{placeholder3}/path/{placeholder4}-test.git"); + MultipleJGitEnvironmentProperties properties2 = new MultipleJGitEnvironmentProperties(); + properties2.setUri("http://localhost/{placeholder1}/path/{placeholder2}-test.git"); + connectionFactory.addConfiguration(properties1); + connectionFactory.addConfiguration(properties2); + + HttpConnection actualConnection = connectionFactory.create(new URL(properties2.getUri() + .replace("{placeholder1}", "value1") + .replace("{placeholder2}", "value2"))); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.get(properties2.getUri()); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isNotSameAs(expectedHttpClientBuilder); + } + + @Test + public void composite_longerUrlsWithPlaceholders() throws Exception { + MultipleJGitEnvironmentProperties properties1 = new MultipleJGitEnvironmentProperties(); + properties1.setUri("http://localhost/path/{placeholder3}/{placeholder4}-test.git"); + MultipleJGitEnvironmentProperties properties2 = new MultipleJGitEnvironmentProperties(); + properties2.setUri("http://localhost/{placeholder1}/path/{placeholder2}-test.git"); + connectionFactory.addConfiguration(properties1); + connectionFactory.addConfiguration(properties2); + + HttpConnection actualConnection = connectionFactory.create(new URL(properties2.getUri() + .replace("{placeholder1}", "value1") + .replace("{placeholder2}", "value2") + "/some/path.properties")); + + HttpClientBuilder expectedHttpClientBuilder = connectionFactory.httpClientBuildersByUri.get(properties2.getUri()); + HttpClientBuilder actualHttpClientBuilder = getActualHttpClientBuilder(actualConnection); + assertThat(actualHttpClientBuilder).isSameAs(expectedHttpClientBuilder); + } + + private HttpClient getActualHttpClient(HttpConnection actualConnection) { + Field clientField = ReflectionUtils.findField(actualConnection.getClass(), "client"); + ReflectionUtils.makeAccessible(clientField); + return (HttpClient) ReflectionUtils.getField(clientField, actualConnection); + } + + private HttpClientBuilder getActualHttpClientBuilder(HttpConnection actualConnection) { + HttpClient actualHttpClient = getActualHttpClient(actualConnection); + Field closeablesField = ReflectionUtils.findField(actualHttpClient.getClass(), "closeables"); + ReflectionUtils.makeAccessible(closeablesField); + List closables = (List) ReflectionUtils.getField(closeablesField, actualHttpClient); + return closables.stream() + .map(o -> { + Field builderField = Arrays.stream(o.getClass().getDeclaredFields()).filter(field -> HttpClientBuilder.class.isAssignableFrom(field.getType())).findFirst().orElse(null); + if (builderField != null) { + ReflectionUtils.makeAccessible(builderField); + return ReflectionUtils.getField(builderField, o); + } + return null; + }) + .filter(Objects::nonNull) + .map(HttpClientBuilder.class::cast).findFirst().get(); + } +} \ No newline at end of file