diff --git a/spring-cloud-dataflow-configuration-metadata/pom.xml b/spring-cloud-dataflow-configuration-metadata/pom.xml
index 64c96a5128..7082a44ea8 100644
--- a/spring-cloud-dataflow-configuration-metadata/pom.xml
+++ b/spring-cloud-dataflow-configuration-metadata/pom.xml
@@ -35,6 +35,10 @@
org.springframework
spring-web
+
+ io.projectreactor.netty
+ reactor-netty
+
com.fasterxml.jackson.core
jackson-core
diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java
new file mode 100644
index 0000000000..13e6aa32a5
--- /dev/null
+++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnContainerTestManual.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.dataflow.container.registry.authorization;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
+import org.springframework.cloud.dataflow.configuration.metadata.ApplicationConfigurationMetadataResolverAutoConfiguration;
+import org.springframework.cloud.dataflow.configuration.metadata.container.DefaultContainerImageMetadataResolver;
+import org.springframework.cloud.dataflow.container.registry.ContainerRegistryAutoConfiguration;
+import org.springframework.cloud.dataflow.container.registry.ContainerRegistryConfiguration;
+import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * This test is aimed at performing a manual test against a deployed container registry;
+ * In order to invoke this test populate the fields of DropAuthorizationHeaderOnContainerTestManual.TestApplication
+ * named registryDomainName, registryUser, registrySecret and imageNameAndTag
+ *
+ * The image should be one built with spring-boot:build-image or paketo so that is has a label named 'org.springframework.boot.version'
+ * For docker hub use:
+ * registryDomainName="registry-1.docker.io",
+ * registryUser="docker user"
+ * registrySecret="docker access token"
+ * imageNameAndTag="springcloudstream/s3-sink-rabbit:5.0.0"
+ *
+ * @author Corneil du Plessis
+ */
+public class DropAuthorizationHeaderOnContainerTestManual {
+
+ private static final String registryDomainName = "registry-1.docker.io";
+ private static final String registryUser = "";
+ private static final String registrySecret = "";
+ private static final String imageNameAndTag = "springcloudstream/s3-sink-rabbit:5.0.0";
+
+ private AnnotationConfigApplicationContext context;
+
+ @AfterEach
+ void clean() {
+ if (context != null) {
+ context.close();
+ }
+ context = null;
+ }
+
+ @Test
+ void testContainerImageLabels() {
+ context = new AnnotationConfigApplicationContext(TestApplication.class);
+ DefaultContainerImageMetadataResolver imageMetadataResolver = context.getBean(DefaultContainerImageMetadataResolver.class);
+ Map imageLabels = imageMetadataResolver.getImageLabels(registryDomainName + "/" + imageNameAndTag);
+ System.out.println("imageLabels:" + imageLabels.keySet());
+ assertThat(imageLabels).containsKey("org.springframework.boot.version");
+ }
+
+ @Import({ContainerRegistryAutoConfiguration.class, ApplicationConfigurationMetadataResolverAutoConfiguration.class})
+ @AutoConfigureWebClient
+ static class TestApplication {
+
+ @Bean
+ @Primary
+ ContainerRegistryProperties containerRegistryProperties() {
+ ContainerRegistryProperties properties = new ContainerRegistryProperties();
+ ContainerRegistryConfiguration registryConfiguration = new ContainerRegistryConfiguration();
+ registryConfiguration.setRegistryHost(registryDomainName);
+ registryConfiguration.setAuthorizationType(ContainerRegistryConfiguration.AuthorizationType.dockeroauth2);
+ registryConfiguration.setUser(registryUser);
+ registryConfiguration.setSecret(registrySecret);
+ properties.setRegistryConfigurations(Collections.singletonMap(registryDomainName, registryConfiguration));
+
+ return properties;
+ }
+ }
+}
diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java
index 0b7809b65a..80cfa743e8 100644
--- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java
+++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest.java
@@ -42,8 +42,6 @@
* @author Adam J. Weigold
* @author Corneil du Plessis
*/
-//TODO: Boot3x followup
-@Disabled("TODO: Boot3x `org.springframework.web.client.HttpClientErrorException$BadRequest: 400 : [no body]` is thrown by REST Template")
public class DropAuthorizationHeaderOnSignedS3RequestRedirectStrategyTest {
@RegisterExtension
public final static S3SignedRedirectRequestServerResource s3SignedRedirectRequestServerResource =
diff --git a/spring-cloud-dataflow-container-registry/pom.xml b/spring-cloud-dataflow-container-registry/pom.xml
index 8656ce7f43..9bd6153af3 100644
--- a/spring-cloud-dataflow-container-registry/pom.xml
+++ b/spring-cloud-dataflow-container-registry/pom.xml
@@ -42,6 +42,10 @@
org.springframework
spring-web
+
+ io.projectreactor.netty
+ reactor-netty
+
com.fasterxml.jackson.core
jackson-core
diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java
index 6e6c78099f..a4c8c21813 100644
--- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java
+++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerImageRestTemplateFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the original author or authors.
+ * Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,40 +16,32 @@
package org.springframework.cloud.dataflow.container.registry;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.cookie.StandardCookieSpec;
-import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
-import org.apache.hc.client5.http.impl.classic.HttpClients;
-import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
-import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
-import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
-import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
-import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.config.Lookup;
-import org.apache.hc.core5.http.config.RegistryBuilder;
+import javax.net.ssl.SSLException;
+
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.http.client.HttpClientRequest;
+import reactor.netty.transport.ProxyProvider;
import org.springframework.boot.web.client.RestTemplateBuilder;
-import org.springframework.cloud.dataflow.container.registry.authorization.DropAuthorizationHeaderRequestRedirectStrategy;
import org.springframework.http.MediaType;
-import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ReactorNettyClientRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
-
/**
* On demand creates a cacheable {@link RestTemplate} instances for the purpose of the Container Registry access.
* Created RestTemplates can be configured to use Http Proxy and/or bypassing the SSL verification.
@@ -83,49 +75,31 @@
*
* @author Christian Tzolov
* @author Cheng Guan Poh
+ * @author Corneil du Plessis
*/
public class ContainerImageRestTemplateFactory {
+ private static final String CUSTOM_REGISTRY = "custom-registry";
+ private static final String AMZ_CREDENTIAL = "X-Amz-Credential";
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String AZURECR_URI_SUFFIX = "azurecr.io";
+ private static final String BASIC_AUTH = "Basic";
+
private final RestTemplateBuilder restTemplateBuilder;
private final ContainerRegistryProperties properties;
/**
- * Depends on the disablesSslVerification and useHttpProxy a 4 different RestTemplate configurations might be
+ * Depends on the skipSslVerification and withHttpProxy and extra map with multiple configurations might be
* used at the same time for interacting with different container registries.
- * The cache map allows reusing the RestTemplates for given useHttpProxy and disablesSslVerification combination.
+ * The cache map allows reusing the RestTemplates for given withHttpProxy and skipSslVerification and extra map combination.
*/
private final ConcurrentHashMap restTemplateCache;
/**
- * Unique key for any useHttpProxy and disablesSslVerification combination.
+ * Unique key for any withHttpProxy and skipSslVerification combination.
*/
- private static class CacheKey {
- private final boolean disablesSslVerification;
- private final boolean useHttpProxy;
-
- public CacheKey(boolean disablesSslVerification, boolean useHttpProxy) {
- this.disablesSslVerification = disablesSslVerification;
- this.useHttpProxy = useHttpProxy;
- }
-
- static CacheKey of(boolean disablesSslVerification, boolean useHttpProxy) {
- return new CacheKey(disablesSslVerification, useHttpProxy);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- CacheKey cacheKey = (CacheKey) o;
- return disablesSslVerification == cacheKey.disablesSslVerification && useHttpProxy == cacheKey.useHttpProxy;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(disablesSslVerification, useHttpProxy);
- }
- }
+ record CacheKey(boolean skipSslVerification, boolean withHttpProxy, Map extra) {};
public ContainerImageRestTemplateFactory(RestTemplateBuilder restTemplateBuilder, ContainerRegistryProperties properties) {
this.restTemplateBuilder = restTemplateBuilder;
@@ -133,87 +107,126 @@ public ContainerImageRestTemplateFactory(RestTemplateBuilder restTemplateBuilder
this.restTemplateCache = new ConcurrentHashMap();
}
+ /**
+ * Obtain a configured RestTemplate for interacting with container registry.
+ * @param skipSslVerification indicates we want to trust all certificates.
+ * @param withHttpProxy indicates we want to use configured proxy.
+ * @return A configured RestTemplate with the given ssl and proxy settings.
+ */
public RestTemplate getContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy) {
return this.getContainerRestTemplate(skipSslVerification, withHttpProxy, Collections.emptyMap());
}
+ /**
+ * Obtain a configured RestTemplate for interacting with container registry.
+ * @param skipSslVerification indicates that we want to trust all certificates.
+ * @param withHttpProxy indicates we want to use the configure proxy host and port.
+ * @param extra by adding entry custom-registry=registry-domain we expect to remove Authorization headers.
+ * @return A configured RestTemplate with the given ssl and proxy and extra settings.
+ */
public RestTemplate getContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra) {
+ var cacheKey = new CacheKey(skipSslVerification, withHttpProxy, new HashMap<>(extra));
try {
- CacheKey cacheKey = CacheKey.of(skipSslVerification, withHttpProxy);
- if (!this.restTemplateCache.containsKey(cacheKey)) {
- RestTemplate restTemplate = createContainerRestTemplate(skipSslVerification, withHttpProxy, extra);
- this.restTemplateCache.putIfAbsent(cacheKey, restTemplate);
- }
- return this.restTemplateCache.get(cacheKey);
+ return this.restTemplateCache.computeIfAbsent(cacheKey, (key) -> createContainerRestTemplate(key.skipSslVerification(), key.withHttpProxy(), key.extra()));
}
catch (Exception e) {
- throw new ContainerRegistryException(
- "Failed to create Container Image RestTemplate for disableSsl:"
- + skipSslVerification + ", httpProxy:" + withHttpProxy, e);
+ throw new ContainerRegistryException("Failed to create Container Image RestTemplate for disableSsl:" + skipSslVerification + ", httpProxy:" + withHttpProxy, e);
}
}
- private RestTemplate createContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra)
- throws NoSuchAlgorithmException, KeyManagementException {
-
- if (!skipSslVerification) {
- // Create a RestTemplate that uses custom request factory
- return this.initRestTemplate(HttpClients.custom(), withHttpProxy, extra);
- }
-
- // Trust manager that blindly trusts all SSL certificates.
- TrustManager[] trustAllCerts = new TrustManager[] {
- new X509TrustManager() {
- public java.security.cert.X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0];
- }
+ private RestTemplate createContainerRestTemplate(boolean skipSslVerification, boolean withHttpProxy, Map extra) {
+ HttpClient client = httpClientBuilder(skipSslVerification, extra);
+ return initRestTemplate(client, withHttpProxy, extra);
+ }
- public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
- }
+ /**
+ * Amazon, Azure and Custom Container Registry services require special treatment for the Authorization headers when the
+ * HTTP request are forwarded to 3rd party services.
+ *
+ * Amazon:
+ * The Amazon S3 API supports two Authentication Methods (https://amzn.to/2Dg9sga):
+ * (1) HTTP Authorization header and (2) Query string parameters (often referred to as a pre-signed URL).
+ *
+ * But only one auth mechanism is allowed at a time. If the http request contains both an Authorization header and
+ * an pre-signed URL parameters then an error is thrown.
+ *
+ * Container Registries often use AmazonS3 as a backend object store. If HTTP Authorization header
+ * is used to authenticate with the Container Registry and then this registry redirect the request to a S3 storage
+ * using pre-signed URL authentication, the redirection will fail.
+ *
+ * Solution is to implement a HTTP redirect strategy that removes the original Authorization headers when the request is
+ * redirected toward an Amazon signed URL.
+ *
+ * Azure:
+ * Azure have same type of issues as S3 so header needs to be dropped as well.
+ * (https://docs.microsoft.com/en-us/azure/container-registry/container-registry-faq#authentication-information-is-not-given-in-the-correct-format-on-direct-rest-api-calls)
+ *
+ * Custom:
+ * Custom Container Registry may have same type of issues as S3 so header needs to be dropped as well.
+ */
+ private HttpClient httpClientBuilder(boolean skipSslVerification, Map extra) {
- public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
- }
+ try {
+ SslContextBuilder builder = skipSslVerification
+ ? SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
+ : SslContextBuilder.forClient();
+ SslContext sslContext = builder.build();
+ HttpClient client = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
+
+ return client.followRedirect(true, (entries, httpClientRequest) -> {
+ if (shouldRemoveAuthorization(httpClientRequest, extra)) {
+ HttpHeaders httpHeaders = httpClientRequest.requestHeaders();
+ removeAuthorization(httpHeaders);
+ removeAuthorization(entries);
+ httpClientRequest.headers(httpHeaders);
}
- };
- SSLContext sslContext = SSLContext.getInstance("SSL");
- // Install trust manager to SSL Context.
- sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
-
- // Create a RestTemplate that uses custom request factory
- return initRestTemplate(
- httpClientBuilder(sslContext),
- withHttpProxy,
- extra);
+ });
+ }
+ catch (SSLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean shouldRemoveAuthorization(HttpClientRequest request, Map extra) {
+ HttpMethod method = request.method();
+ if(!method.equals(HttpMethod.GET) && !method.equals(HttpMethod.HEAD)) {
+ return false;
+ }
+ if (request.uri().contains(AMZ_CREDENTIAL)) {
+ return true;
+ }
+ if (request.uri().contains(AZURECR_URI_SUFFIX)) {
+ return request.requestHeaders()
+ .entries()
+ .stream()
+ .anyMatch(entry -> entry.getKey().equalsIgnoreCase(AUTHORIZATION_HEADER)
+ && entry.getValue().contains(BASIC_AUTH));
+ }
+ return extra.containsKey(CUSTOM_REGISTRY) && request.uri().contains(extra.get(CUSTOM_REGISTRY));
}
- private HttpClientBuilder httpClientBuilder(SSLContext sslContext) {
- // Register http/s connection factories
- Lookup connSocketFactoryLookup = RegistryBuilder. create()
- .register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))
- .register("http", new PlainConnectionSocketFactory())
- .build();
- return HttpClients.custom()
- .setConnectionManager(new BasicHttpClientConnectionManager(connSocketFactoryLookup));
+
+ private static void removeAuthorization(HttpHeaders headers) {
+ Set authHeaders = headers.entries()
+ .stream()
+ .filter(entry -> entry.getKey().equalsIgnoreCase(AUTHORIZATION_HEADER)).map(Map.Entry::getKey).collect(Collectors.toSet());
+ authHeaders.forEach(authHeader -> headers.remove(authHeader));
}
- private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean withHttpProxy, Map extra) {
- clientBuilder.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(StandardCookieSpec.RELAXED).build());
+ private RestTemplate initRestTemplate(HttpClient client, boolean withHttpProxy, Map extra) {
// Set the HTTP proxy if configured.
if (withHttpProxy) {
if (!properties.getHttpProxy().isEnabled()) {
throw new ContainerRegistryException("Registry Configuration uses a HttpProxy but non is configured!");
}
- HttpHost proxy = new HttpHost(properties.getHttpProxy().getHost(), properties.getHttpProxy().getPort());
- clientBuilder.setProxy(proxy);
+ ProxyProvider.Builder builder = ProxyProvider.builder()
+ .type(ProxyProvider.Proxy.HTTP)
+ .host(properties.getHttpProxy().getHost())
+ .port(properties.getHttpProxy().getPort());
+ client.proxy(typeSpec -> builder.build());
}
- HttpComponentsClientHttpRequestFactory customRequestFactory =
- new HttpComponentsClientHttpRequestFactory(
- clientBuilder
- .setRedirectStrategy(new DropAuthorizationHeaderRequestRedirectStrategy(extra))
- // Azure redirects may contain double slashes and on default those are normilised
- .setDefaultRequestConfig(RequestConfig.custom().build())
- .build());
+ ClientHttpRequestFactory customRequestFactory = new ReactorNettyClientRequestFactory(client);
// DockerHub response's media-type is application/octet-stream although the content is in JSON.
// Similarly the Github CR response's media-type is always text/plain although the content is in JSON.
@@ -221,6 +234,7 @@ private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean w
// include application/octet-stream and text/plain.
MappingJackson2HttpMessageConverter octetSupportJsonConverter = new MappingJackson2HttpMessageConverter();
ArrayList mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes());
+ mediaTypeList.add(MediaType.APPLICATION_JSON);
mediaTypeList.add(MediaType.APPLICATION_OCTET_STREAM);
mediaTypeList.add(MediaType.TEXT_PLAIN);
octetSupportJsonConverter.setSupportedMediaTypes(mediaTypeList);
diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java
deleted file mode 100644
index e7741b9562..0000000000
--- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/DropAuthorizationHeaderRequestRedirectStrategy.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright 2020-2021 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.cloud.dataflow.container.registry.authorization;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Arrays;
-import java.util.Map;
-
-import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.classic.methods.HttpHead;
-import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
-import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.HttpResponse;
-import org.apache.hc.core5.http.protocol.HttpContext;
-
-import org.springframework.util.StringUtils;
-
-/**
- * Amazon, Azure and Custom Container Registry services require special treatment for the Authorization headers when the
- * HTTP request are forwarded to 3rd party services.
- *
- * Amazon:
- * The Amazon S3 API supports two Authentication Methods (https://amzn.to/2Dg9sga):
- * (1) HTTP Authorization header and (2) Query string parameters (often referred to as a pre-signed URL).
- *
- * But only one auth mechanism is allowed at a time. If the http request contains both an Authorization header and
- * an pre-signed URL parameters then an error is thrown.
- *
- * Container Registries often use AmazonS3 as a backend object store. If HTTP Authorization header
- * is used to authenticate with the Container Registry and then this registry redirect the request to a S3 storage
- * using pre-signed URL authentication, the redirection will fail.
- *
- * Solution is to implement a HTTP redirect strategy that removes the original Authorization headers when the request is
- * redirected toward an Amazon signed URL.
- *
- * Azure:
- * Azure have same type of issues as S3 so header needs to be dropped as well.
- * (https://docs.microsoft.com/en-us/azure/container-registry/container-registry-faq#authentication-information-is-not-given-in-the-correct-format-on-direct-rest-api-calls)
- *
- * Custom:
- * Custom Container Registry may have same type of issues as S3 so header needs to be dropped as well.
- *
- * @author Adam J. Weigold
- * @author Janne Valkealahti
- * @author Christian Tzolov
- * @author Cheng Guan Poh
- */
-public class DropAuthorizationHeaderRequestRedirectStrategy extends DefaultRedirectStrategy {
-
- private static final String CUSTOM_REGISTRY = "custom-registry";
-
- private static final String AMZ_CREDENTIAL = "X-Amz-Credential";
-
- private static final String AUTHORIZATION_HEADER = "Authorization";
-
- private static final String AZURECR_URI_SUFFIX = "azurecr.io";
-
- private static final String BASIC_AUTH = "Basic";
-
- /**
- * Additional registry specific configuration properties - usually used inside the Registry authorizer
- * implementations (eg. the AwsEcrAuthorizer implementation).
- */
- private Map extra;
-
- public DropAuthorizationHeaderRequestRedirectStrategy(Map extra) {
- this.extra = extra;
- }
-
- @Override
- public URI getLocationURI(final HttpRequest request, final HttpResponse response,
- final HttpContext context) throws HttpException {
-
- URI httpUriRequest = super.getLocationURI(request, response, context);
- String query = httpUriRequest.getQuery();
- String method = request.getMethod();
-
- // Handle Amazon requests
- if (StringUtils.hasText(query) && query.contains(AMZ_CREDENTIAL)) {
- if (isHeadOrGetMethod(method)) {
- try {
- return new DropAuthorizationHeaderHttpRequestBase(httpUriRequest, method).getUri();
- } catch (URISyntaxException e) {
- throw new HttpException("Unable to get location URI", e);
- }
- }
- }
-
- // Handle Azure requests
- try {
- if (request.getUri().getRawPath().contains(AZURECR_URI_SUFFIX)) {
- if (isHeadOrGetMethod(method)) {
- return (new DropAuthorizationHeaderHttpRequestBase(httpUriRequest, method) {
- // Drop headers only for the Basic Auth and leave unchanged for OAuth2
- @Override
- protected boolean isDropHeader(String name, Object value) {
- return name.equalsIgnoreCase(AUTHORIZATION_HEADER) && StringUtils.hasText((String) value) && ((String)value).contains(BASIC_AUTH);
- }
- }).getUri();
- }
- }
-
-
- // Handle Custom requests
- if (extra.containsKey(CUSTOM_REGISTRY) && request.getUri().getRawPath().contains(extra.get(CUSTOM_REGISTRY))) {
- if (isHeadOrGetMethod(method)) {
- return new DropAuthorizationHeaderHttpRequestBase(httpUriRequest, method).getUri();
- }
- }
- } catch (URISyntaxException e) {
- throw new HttpException("Unable to get Locaction URI", e);
- }
- return httpUriRequest;
- }
-
- private boolean isHeadOrGetMethod(String method) {
- return StringUtils.hasText(method)
- && (method.equalsIgnoreCase(HttpHead.METHOD_NAME) || method.equalsIgnoreCase(HttpGet.METHOD_NAME));
- }
-
- /**
- * Overrides all header setter methods to filter out the Authorization headers.
- */
- static class DropAuthorizationHeaderHttpRequestBase extends HttpUriRequestBase {
-
- private final String method;
-
- DropAuthorizationHeaderHttpRequestBase(URI uri, String method) {
- super(method, uri);
- this.method = method;
- }
-
- @Override
- public String getMethod() {
- return this.method;
- }
-
- @Override
- public void addHeader(Header header) {
- if (!isDropHeader(header)) {
- super.addHeader(header);
- }
- }
-
- @Override
- public void addHeader(String name, Object value) {
- if (!isDropHeader(name, value)) {
- super.addHeader(name, value);
- }
- }
-
- @Override
- public void setHeader(Header header) {
- if (!isDropHeader(header)) {
- super.setHeader(header);
- }
- }
-
- @Override
- public void setHeader(String name, Object value) {
- if (!isDropHeader(name, value)) {
- super.setHeader(name, value);
- }
- }
-
- @Override
- public void setHeaders(Header[] headers) {
- Header[] filteredHeaders = Arrays.stream(headers)
- .filter(header -> !isDropHeader(header))
- .toArray(Header[]::new);
- super.setHeaders(filteredHeaders);
- }
-
- protected boolean isDropHeader(Header header) {
- return isDropHeader(header.getName(), header.getValue());
- }
-
- protected boolean isDropHeader(String name, Object value) {
- return name.equalsIgnoreCase(AUTHORIZATION_HEADER);
- }
- }
-}