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