From 88ddf2b9d8adf44fe01775097ac04db32e500c12 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Tue, 15 Oct 2024 14:28:05 +0200 Subject: [PATCH 1/3] The registry stub was rejecting requests that contained an authorization header instead of rejecting requests that didn't. --- ...OnSignedS3RequestRedirectStrategyTest.java | 2 - .../S3SignedRedirectRequestController.java | 3 +- .../registry/ContainerRegistryService.java | 95 +++++++++++-------- .../S3SignedRedirectRequestController.java | 3 +- 4 files changed, 57 insertions(+), 46 deletions(-) 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-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java index a3a5f2fe67..d7b3c5ab1e 100644 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java @@ -32,6 +32,7 @@ /** * @author Adam J. Weigold + * @author Corneil du Plessis */ @RestController public class S3SignedRedirectRequestController { @@ -68,7 +69,7 @@ public ResponseEntity> getBlobRedirect(@RequestHeader("Autho @RequestMapping("/test/docker/registry/v2/blobs/test/data") public ResponseEntity getSignedBlob(@RequestHeader Map headers) { - if (headers.containsKey("authorization")) { + if (!headers.containsKey("authorization")) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } return buildFromString("{\"config\": {\"Labels\": {\"foo\": \"bar\"} } }"); diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java index 5719fe41d0..f87b20cc06 100644 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -46,9 +47,9 @@ public class ContainerRegistryService { private static final Logger logger = LoggerFactory.getLogger(ContainerRegistryService.class); - private static final List SUPPORTED_MANIFEST_MEDIA_TYPES = - Collections.unmodifiableList(Arrays.asList(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE, - ContainerRegistryProperties.DOCKER_IMAGE_MANIFEST_MEDIA_TYPE)); + private static final List SUPPORTED_MANIFEST_MEDIA_TYPES = Collections + .unmodifiableList(Arrays.asList(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE, + ContainerRegistryProperties.DOCKER_IMAGE_MANIFEST_MEDIA_TYPE)); private static final String HTTPS_SCHEME = "https"; @@ -88,35 +89,38 @@ public Map getContainerRegistryConfigura } /** - * Get the tag information for the given container image identified by its repository and registry. - * The registry information is expected to be set via container registry configuration in SCDF. + * Get the tag information for the given container image identified by its repository + * and registry. The registry information is expected to be set via container registry + * configuration in SCDF. * @param registryName the container registry name * @param repositoryName the image repository name * @return the list of tags for the image */ public List getTags(String registryName, String repositoryName) { try { - ContainerRegistryConfiguration containerRegistryConfiguration = this.registryConfigurations.get(registryName); + ContainerRegistryConfiguration containerRegistryConfiguration = this.registryConfigurations + .get(registryName); Map properties = new HashMap<>(); properties.put(DockerOAuth2RegistryAuthorizer.DOCKER_REGISTRY_REPOSITORY_FIELD_KEY, repositoryName); - HttpHeaders httpHeaders = new HttpHeaders(this.registryAuthorizerMap.get(containerRegistryConfiguration.getAuthorizationType()).getAuthorizationHeaders( - containerRegistryConfiguration, properties)); + HttpHeaders httpHeaders = new HttpHeaders( + this.registryAuthorizerMap.get(containerRegistryConfiguration.getAuthorizationType()) + .getAuthorizationHeaders(containerRegistryConfiguration, properties)); httpHeaders.set(HttpHeaders.ACCEPT, "application/json"); UriComponents manifestUriComponents = UriComponentsBuilder.newInstance() - .scheme(HTTPS_SCHEME) - .host(containerRegistryConfiguration.getRegistryHost()) - .path(TAGS_LIST_PATH) - .build().expand(repositoryName); + .scheme(HTTPS_SCHEME) + .host(containerRegistryConfiguration.getRegistryHost()) + .path(TAGS_LIST_PATH) + .build() + .expand(repositoryName); RestTemplate requestRestTemplate = this.containerImageRestTemplateFactory.getContainerRestTemplate( containerRegistryConfiguration.isDisableSslVerification(), - containerRegistryConfiguration.isUseHttpProxy(), - containerRegistryConfiguration.getExtra()); + containerRegistryConfiguration.isUseHttpProxy(), containerRegistryConfiguration.getExtra()); - ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), - HttpMethod.GET, new HttpEntity<>(httpHeaders), Map.class); - return (List) manifest.getBody().get(TAGS_FIELD); + ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), HttpMethod.GET, + new HttpEntity<>(httpHeaders), Map.class); + return (List) manifest.getBody().get(TAGS_FIELD); } catch (Exception e) { logger.error("Exception getting tag information for the {} from {}", repositoryName, registryName); @@ -132,28 +136,25 @@ public List getTags(String registryName, String repositoryName) { public Map getRepositories(String registryName) { try { ContainerRegistryConfiguration containerRegistryConfiguration = this.registryConfigurations - .get(registryName); + .get(registryName); Map properties = new HashMap<>(); properties.put(DockerOAuth2RegistryAuthorizer.DOCKER_REGISTRY_REPOSITORY_FIELD_KEY, registryName); HttpHeaders httpHeaders = new HttpHeaders( this.registryAuthorizerMap.get(containerRegistryConfiguration.getAuthorizationType()) - .getAuthorizationHeaders( - containerRegistryConfiguration, properties)); + .getAuthorizationHeaders(containerRegistryConfiguration, properties)); httpHeaders.set(HttpHeaders.ACCEPT, "application/json"); UriComponents manifestUriComponents = UriComponentsBuilder.newInstance() - .scheme(HTTPS_SCHEME) - .host(containerRegistryConfiguration.getRegistryHost()) - .path(CATALOG_LIST_PATH) - .build(); - + .scheme(HTTPS_SCHEME) + .host(containerRegistryConfiguration.getRegistryHost()) + .path(CATALOG_LIST_PATH) + .build(); RestTemplate requestRestTemplate = this.containerImageRestTemplateFactory.getContainerRestTemplate( containerRegistryConfiguration.isDisableSslVerification(), - containerRegistryConfiguration.isUseHttpProxy(), - containerRegistryConfiguration.getExtra()); + containerRegistryConfiguration.isUseHttpProxy(), containerRegistryConfiguration.getExtra()); - ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), - HttpMethod.GET, new HttpEntity<>(httpHeaders), Map.class); + ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), HttpMethod.GET, + new HttpEntity<>(httpHeaders), Map.class); return manifest.getBody(); } catch (Exception e) { @@ -206,18 +207,20 @@ public T getImageManifest(ContainerRegistryRequest registryRequest, Class // Docker Registry HTTP V2 API pull manifest ContainerImage containerImage = registryRequest.getContainerImage(); UriComponents manifestUriComponents = UriComponentsBuilder.newInstance() - .scheme(HTTPS_SCHEME) - .host(containerImage.getHostname()) - .port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null) - .path(IMAGE_MANIFEST_REFERENCE_PATH) - .build().expand(containerImage.getRepository(), containerImage.getRepositoryReference()); + .scheme(HTTPS_SCHEME) + .host(containerImage.getHostname()) + .port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null) + .path(IMAGE_MANIFEST_REFERENCE_PATH) + .build() + .expand(containerImage.getRepository(), containerImage.getRepositoryReference()); - ResponseEntity manifest = registryRequest.getRestTemplate().exchange(manifestUriComponents.toUri(), - HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); + ResponseEntity manifest = registryRequest.getRestTemplate() + .exchange(manifestUriComponents.toUri(), HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); return manifest.getBody(); } - public T getImageBlob(ContainerRegistryRequest registryRequest, String configDigest, Class responseClassType) { + public T getImageBlob(ContainerRegistryRequest registryRequest, String configDigest, + Class responseClassType) { ContainerImage containerImage = registryRequest.getContainerImage(); HttpHeaders httpHeaders = new HttpHeaders(registryRequest.getAuthHttpHeaders()); @@ -227,11 +230,19 @@ public T getImageBlob(ContainerRegistryRequest registryRequest, String confi .host(containerImage.getHostname()) .port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null) .path(IMAGE_BLOB_DIGEST_PATH) - .build().expand(containerImage.getRepository(), configDigest); - - ResponseEntity blob = registryRequest.getRestTemplate().exchange(blobUriComponents.toUri(), - HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); + .build() + .expand(containerImage.getRepository(), configDigest); + try { + logger.info("getImageBlob:request:{},{}", blobUriComponents.toUri(), httpHeaders); + ResponseEntity blob = registryRequest.getRestTemplate() + .exchange(blobUriComponents.toUri(), HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); - return blob.getBody(); + return blob.getStatusCode().is2xxSuccessful() ? blob.getBody() : null; + } + catch (RestClientException x) { + logger.error("getImageBlob:exception:" + x, x); + return null; + } } + } diff --git a/spring-cloud-dataflow-container-registry/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java b/spring-cloud-dataflow-container-registry/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java index a3a5f2fe67..d7b3c5ab1e 100644 --- a/spring-cloud-dataflow-container-registry/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java +++ b/spring-cloud-dataflow-container-registry/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java @@ -32,6 +32,7 @@ /** * @author Adam J. Weigold + * @author Corneil du Plessis */ @RestController public class S3SignedRedirectRequestController { @@ -68,7 +69,7 @@ public ResponseEntity> getBlobRedirect(@RequestHeader("Autho @RequestMapping("/test/docker/registry/v2/blobs/test/data") public ResponseEntity getSignedBlob(@RequestHeader Map headers) { - if (headers.containsKey("authorization")) { + if (!headers.containsKey("authorization")) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } return buildFromString("{\"config\": {\"Labels\": {\"foo\": \"bar\"} } }"); From 9e939352029e4393703ee6669439d7cb67fca1fc Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 16 Oct 2024 15:34:14 +0200 Subject: [PATCH 2/3] Replace RedirectExec with modified chain handler. Fixes #5989 --- .../S3SignedRedirectRequestController.java | 11 +- .../ContainerImageRestTemplateFactory.java | 30 ++- ...rizationHeaderRequestRedirectStrategy.java | 146 +++++------ .../authorization/SpecialRedirectExec.java | 226 ++++++++++++++++++ 4 files changed, 315 insertions(+), 98 deletions(-) create mode 100644 spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/SpecialRedirectExec.java diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java index d7b3c5ab1e..3762c0ab76 100644 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java @@ -19,6 +19,9 @@ import java.util.Collections; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.springframework.cloud.dataflow.container.registry.ContainerRegistryProperties; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -36,6 +39,7 @@ */ @RestController public class S3SignedRedirectRequestController { + private final static Logger logger = LoggerFactory.getLogger(S3SignedRedirectRequestController.class); @RequestMapping("/service/token") public ResponseEntity> getToken() { @@ -53,6 +57,7 @@ public ResponseEntity getManifests(@RequestHeader("Authorization") Str @RequestMapping("/v2/test/s3-redirect-image/blobs/signed_redirect_digest") public ResponseEntity> getBlobRedirect(@RequestHeader("Authorization") String token) { if (!"bearer my_token_999".equals(token.trim().toLowerCase())) { + logger.info("getBlobRedirect=BAD_REQUEST"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } HttpHeaders redirectHeaders = new HttpHeaders(); @@ -63,13 +68,15 @@ public ResponseEntity> getBlobRedirect(@RequestHeader("Autho "&X-Amz-Expires=1200" + "&X-Amz-SignedHeaders=host" + "&X-Amz-Signature=test"); - + logger.info("getBlobRedirect:{}", redirectHeaders); return new ResponseEntity<>(redirectHeaders, HttpStatus.TEMPORARY_REDIRECT); } @RequestMapping("/test/docker/registry/v2/blobs/test/data") public ResponseEntity getSignedBlob(@RequestHeader Map headers) { - if (!headers.containsKey("authorization")) { + logger.info("getSignedBlob:{}", headers); + if (headers.containsKey("authorization")) { + logger.info("getSignedBlob=BAD_REQUEST"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } return buildFromString("{\"config\": {\"Labels\": {\"foo\": \"bar\"} } }"); 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..936ce3daff 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 @@ -31,9 +31,13 @@ import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.cookie.StandardCookieSpec; +import org.apache.hc.client5.http.impl.ChainElement; +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; 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.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; import org.apache.hc.client5.http.socket.ConnectionSocketFactory; import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; @@ -44,12 +48,12 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cloud.dataflow.container.registry.authorization.DropAuthorizationHeaderRequestRedirectStrategy; +import org.springframework.cloud.dataflow.container.registry.authorization.SpecialRedirectExec; import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 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,6 +87,7 @@ * * @author Christian Tzolov * @author Cheng Guan Poh + * @author Corneil du Plessis */ public class ContainerImageRestTemplateFactory { @@ -197,7 +202,7 @@ private HttpClientBuilder httpClientBuilder(SSLContext sslContext) { private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean withHttpProxy, Map extra) { clientBuilder.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(StandardCookieSpec.RELAXED).build()); - + DefaultRoutePlanner routePlanner; // Set the HTTP proxy if configured. if (withHttpProxy) { if (!properties.getHttpProxy().isEnabled()) { @@ -205,15 +210,22 @@ private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean w } HttpHost proxy = new HttpHost(properties.getHttpProxy().getHost(), properties.getHttpProxy().getPort()); clientBuilder.setProxy(proxy); + routePlanner = new DefaultProxyRoutePlanner(proxy, DefaultSchemePortResolver.INSTANCE); + } + else { + routePlanner = new DefaultRoutePlanner(DefaultSchemePortResolver.INSTANCE); } - 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()); + DropAuthorizationHeaderRequestRedirectStrategy redirectStrategy = new DropAuthorizationHeaderRequestRedirectStrategy( + extra); + HttpComponentsClientHttpRequestFactory customRequestFactory = new HttpComponentsClientHttpRequestFactory( + clientBuilder.setRedirectStrategy(redirectStrategy) + .replaceExecInterceptor(ChainElement.REDIRECT.name(), + new SpecialRedirectExec(routePlanner, redirectStrategy)) + // Azure redirects may contain double slashes and on default those are + // normilised + .setDefaultRequestConfig(RequestConfig.custom().build()) + .build()); // 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. 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 index e7741b9562..53e98c84dd 100644 --- 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 @@ -18,12 +18,10 @@ 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; @@ -62,6 +60,7 @@ * @author Janne Valkealahti * @author Christian Tzolov * @author Cheng Guan Poh + * @author Corneil du Plessis */ public class DropAuthorizationHeaderRequestRedirectStrategy extends DefaultRedirectStrategy { @@ -92,109 +91,82 @@ public URI getLocationURI(final HttpRequest request, final HttpResponse response 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); - } - } + removeAuthorizationHeader(request, response, false); + try { + if (isHeadMethod(method)) { + return new HttpHead(httpUriRequest).getUri(); + } + else { + return new HttpGet(httpUriRequest).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(); + try { + if (request.getUri().getRawPath().contains(AZURECR_URI_SUFFIX)) { + if (isHeadOrGetMethod(method)) { + removeAuthorizationHeader(request, response, true); + if (isHeadMethod(method)) { + return new HttpHead(httpUriRequest).getUri(); + } + else { + return new HttpGet(httpUriRequest).getUri(); + } + } + } + + // Handle Custom requests + if (extra.containsKey(CUSTOM_REGISTRY) + && request.getUri().getRawPath().contains(extra.get(CUSTOM_REGISTRY))) { + if (isHeadOrGetMethod(method)) { + removeAuthorizationHeader(request, response, false); + if (isHeadMethod(method)) { + return new HttpHead(httpUriRequest).getUri(); + } + else { + return new HttpGet(httpUriRequest).getUri(); + } + } } } - } catch (URISyntaxException e) { + 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); + private static void removeAuthorizationHeader(HttpRequest request, HttpResponse response, boolean onlyBasicAuth) { + for (Header header : response.getHeaders()) { + if (header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER) + && (!onlyBasicAuth || (onlyBasicAuth && header.getValue().contains(BASIC_AUTH)))) { + response.removeHeaders(header.getName()); + break; } } - - @Override - public void addHeader(String name, Object value) { - if (!isDropHeader(name, value)) { - super.addHeader(name, value); + for (Header header : request.getHeaders()) { + if (header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER) + && (!onlyBasicAuth || (onlyBasicAuth && header.getValue().contains(BASIC_AUTH)))) { + request.removeHeaders(header.getName()); + break; } } + } - @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()); - } + private boolean isHeadOrGetMethod(String method) { + return StringUtils.hasText(method) + && (method.equalsIgnoreCase(HttpHead.METHOD_NAME) || method.equalsIgnoreCase(HttpGet.METHOD_NAME)); + } - protected boolean isDropHeader(String name, Object value) { - return name.equalsIgnoreCase(AUTHORIZATION_HEADER); - } + private boolean isHeadMethod(String method) { + return StringUtils.hasText(method) && method.equalsIgnoreCase(HttpHead.METHOD_NAME); } + } diff --git a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/SpecialRedirectExec.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/SpecialRedirectExec.java new file mode 100644 index 0000000000..282f5891bf --- /dev/null +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/authorization/SpecialRedirectExec.java @@ -0,0 +1,226 @@ +package org.springframework.cloud.dataflow.container.registry.authorization; + +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +import java.io.IOException; +import java.net.URI; +import java.util.Objects; + +import org.apache.hc.client5.http.CircularRedirectException; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.RedirectException; +import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.classic.ExecChain; +import org.apache.hc.client5.http.classic.ExecChainHandler; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.protocol.RedirectLocations; +import org.apache.hc.client5.http.protocol.RedirectStrategy; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.apache.hc.client5.http.utils.URIUtils; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Essential copy of RedirectExec to override the behaviour allowing the use of current request when creating a request builder. + * + * @author Corneil du Plessis + */ +@Contract(threading = ThreadingBehavior.STATELESS) +@Internal +public final class SpecialRedirectExec implements ExecChainHandler { + + private static final Logger LOG = LoggerFactory.getLogger(SpecialRedirectExec.class); + + private final RedirectStrategy redirectStrategy; + private final HttpRoutePlanner routePlanner; + + public SpecialRedirectExec( + final HttpRoutePlanner routePlanner, + final RedirectStrategy redirectStrategy) { + super(); + Args.notNull(routePlanner, "HTTP route planner"); + Args.notNull(redirectStrategy, "HTTP redirect strategy"); + this.routePlanner = routePlanner; + this.redirectStrategy = redirectStrategy; + } + + @Override + public ClassicHttpResponse execute( + final ClassicHttpRequest request, + final ExecChain.Scope scope, + final ExecChain chain) throws IOException, HttpException { + Args.notNull(request, "HTTP request"); + Args.notNull(scope, "Scope"); + + final HttpClientContext context = scope.clientContext; + RedirectLocations redirectLocations = context.getRedirectLocations(); + if (redirectLocations == null) { + redirectLocations = new RedirectLocations(); + context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations); + } + redirectLocations.clear(); + + final RequestConfig config = context.getRequestConfig(); + final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50; + ClassicHttpRequest originalRequest = scope.originalRequest; + ClassicHttpRequest currentRequest = request; + ExecChain.Scope currentScope = scope; + for (int redirectCount = 0;;) { + final String exchangeId = currentScope.exchangeId; + final ClassicHttpResponse response = chain.proceed(currentRequest, currentScope); + try { + if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(request, response, context)) { + final HttpEntity requestEntity = request.getEntity(); + if (requestEntity != null && !requestEntity.isRepeatable()) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} cannot redirect non-repeatable request", exchangeId); + } + return response; + } + if (redirectCount >= maxRedirects) { + throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded"); + } + redirectCount++; + + final URI redirectUri = this.redirectStrategy.getLocationURI(currentRequest, response, context); + if (LOG.isDebugEnabled()) { + LOG.debug("{} redirect requested to location '{}'", exchangeId, redirectUri); + } + + final HttpHost newTarget = URIUtils.extractHost(redirectUri); + if (newTarget == null) { + throw new ProtocolException("Redirect URI does not specify a valid host name: " + + redirectUri); + } + + if (!config.isCircularRedirectsAllowed()) { + if (redirectLocations.contains(redirectUri)) { + throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'"); + } + } + redirectLocations.add(redirectUri); + + final int statusCode = response.getCode(); + final ClassicRequestBuilder redirectBuilder; + switch (statusCode) { + case HttpStatus.SC_MOVED_PERMANENTLY: + case HttpStatus.SC_MOVED_TEMPORARILY: + if (Method.POST.isSame(request.getMethod())) { + redirectBuilder = ClassicRequestBuilder.get(); + } else { + redirectBuilder = ClassicRequestBuilder.copy(currentRequest); + } + break; + case HttpStatus.SC_SEE_OTHER: + if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) { + redirectBuilder = ClassicRequestBuilder.get(); + } else { + redirectBuilder = ClassicRequestBuilder.copy(currentRequest); + } + break; + default: + redirectBuilder = ClassicRequestBuilder.copy(currentRequest); + } + redirectBuilder.setUri(redirectUri); + + final HttpRoute currentRoute = currentScope.route; + if (!Objects.equals(currentRoute.getTargetHost(), newTarget)) { + final HttpRoute newRoute = this.routePlanner.determineRoute(newTarget, context); + if (!Objects.equals(currentRoute, newRoute)) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} new route required", exchangeId); + } + final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost()); + if (LOG.isDebugEnabled()) { + LOG.debug("{} resetting target auth state", exchangeId); + } + targetAuthExchange.reset(); + if (currentRoute.getProxyHost() != null) { + final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost()); + if (proxyAuthExchange.isConnectionBased()) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} resetting proxy auth state", exchangeId); + } + proxyAuthExchange.reset(); + } + } + currentScope = new ExecChain.Scope( + currentScope.exchangeId, + newRoute, + currentScope.originalRequest, + currentScope.execRuntime, + currentScope.clientContext); + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("{} redirecting to '{}' via {}", exchangeId, redirectUri, currentRoute); + } + originalRequest = redirectBuilder.build(); + currentRequest = redirectBuilder.build(); + + EntityUtils.consume(response.getEntity()); + response.close(); + } else { + return response; + } + } catch (final RuntimeException | IOException ex) { + response.close(); + throw ex; + } catch (final HttpException ex) { + // Protocol exception related to a direct. + // The underlying connection may still be salvaged. + try { + EntityUtils.consume(response.getEntity()); + } catch (final IOException ioex) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} I/O error while releasing connection", exchangeId, ioex); + } + } finally { + response.close(); + } + throw ex; + } + } + } +} From 8b9038669b4bb471004ebab50ecf1d5876453c48 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Thu, 17 Oct 2024 13:54:57 +0200 Subject: [PATCH 3/3] Extract createResponseURI Remove comments from test controller. Reverted ContainerRegistryService Add SpecialRedirectExec to replace RedirectExec. Ensure DropAuthorizationHeaderRequestRedirectStrategy changes request headers used to create the redirect request. Fixes #5989 --- .../S3SignedRedirectRequestController.java | 5 - .../ContainerImageRestTemplateFactory.java | 12 +-- .../registry/ContainerRegistryService.java | 95 ++++++++----------- ...rizationHeaderRequestRedirectStrategy.java | 53 ++++------- 4 files changed, 67 insertions(+), 98 deletions(-) diff --git a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java index 3762c0ab76..52ad13de12 100644 --- a/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java +++ b/spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/container/registry/authorization/support/S3SignedRedirectRequestController.java @@ -39,7 +39,6 @@ */ @RestController public class S3SignedRedirectRequestController { - private final static Logger logger = LoggerFactory.getLogger(S3SignedRedirectRequestController.class); @RequestMapping("/service/token") public ResponseEntity> getToken() { @@ -57,7 +56,6 @@ public ResponseEntity getManifests(@RequestHeader("Authorization") Str @RequestMapping("/v2/test/s3-redirect-image/blobs/signed_redirect_digest") public ResponseEntity> getBlobRedirect(@RequestHeader("Authorization") String token) { if (!"bearer my_token_999".equals(token.trim().toLowerCase())) { - logger.info("getBlobRedirect=BAD_REQUEST"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } HttpHeaders redirectHeaders = new HttpHeaders(); @@ -68,15 +66,12 @@ public ResponseEntity> getBlobRedirect(@RequestHeader("Autho "&X-Amz-Expires=1200" + "&X-Amz-SignedHeaders=host" + "&X-Amz-Signature=test"); - logger.info("getBlobRedirect:{}", redirectHeaders); return new ResponseEntity<>(redirectHeaders, HttpStatus.TEMPORARY_REDIRECT); } @RequestMapping("/test/docker/registry/v2/blobs/test/data") public ResponseEntity getSignedBlob(@RequestHeader Map headers) { - logger.info("getSignedBlob:{}", headers); if (headers.containsKey("authorization")) { - logger.info("getSignedBlob=BAD_REQUEST"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } return buildFromString("{\"config\": {\"Labels\": {\"foo\": \"bar\"} } }"); 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 936ce3daff..1b4f8a6ada 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 @@ -21,6 +21,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -216,14 +217,11 @@ private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean w routePlanner = new DefaultRoutePlanner(DefaultSchemePortResolver.INSTANCE); } - DropAuthorizationHeaderRequestRedirectStrategy redirectStrategy = new DropAuthorizationHeaderRequestRedirectStrategy( - extra); + DropAuthorizationHeaderRequestRedirectStrategy redirectStrategy = new DropAuthorizationHeaderRequestRedirectStrategy(extra); HttpComponentsClientHttpRequestFactory customRequestFactory = new HttpComponentsClientHttpRequestFactory( clientBuilder.setRedirectStrategy(redirectStrategy) - .replaceExecInterceptor(ChainElement.REDIRECT.name(), - new SpecialRedirectExec(routePlanner, redirectStrategy)) - // Azure redirects may contain double slashes and on default those are - // normilised + .replaceExecInterceptor(ChainElement.REDIRECT.name(), new SpecialRedirectExec(routePlanner, redirectStrategy)) + // Azure redirects may contain double slashes and on default those are normalised .setDefaultRequestConfig(RequestConfig.custom().build()) .build()); @@ -232,7 +230,7 @@ private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean w // Therefore we extend the MappingJackson2HttpMessageConverter media-types to // include application/octet-stream and text/plain. MappingJackson2HttpMessageConverter octetSupportJsonConverter = new MappingJackson2HttpMessageConverter(); - ArrayList mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes()); + List mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes()); 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/ContainerRegistryService.java b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java index f87b20cc06..5719fe41d0 100644 --- a/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java +++ b/spring-cloud-dataflow-container-registry/src/main/java/org/springframework/cloud/dataflow/container/registry/ContainerRegistryService.java @@ -31,7 +31,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -47,9 +46,9 @@ public class ContainerRegistryService { private static final Logger logger = LoggerFactory.getLogger(ContainerRegistryService.class); - private static final List SUPPORTED_MANIFEST_MEDIA_TYPES = Collections - .unmodifiableList(Arrays.asList(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE, - ContainerRegistryProperties.DOCKER_IMAGE_MANIFEST_MEDIA_TYPE)); + private static final List SUPPORTED_MANIFEST_MEDIA_TYPES = + Collections.unmodifiableList(Arrays.asList(ContainerRegistryProperties.OCI_IMAGE_MANIFEST_MEDIA_TYPE, + ContainerRegistryProperties.DOCKER_IMAGE_MANIFEST_MEDIA_TYPE)); private static final String HTTPS_SCHEME = "https"; @@ -89,38 +88,35 @@ public Map getContainerRegistryConfigura } /** - * Get the tag information for the given container image identified by its repository - * and registry. The registry information is expected to be set via container registry - * configuration in SCDF. + * Get the tag information for the given container image identified by its repository and registry. + * The registry information is expected to be set via container registry configuration in SCDF. * @param registryName the container registry name * @param repositoryName the image repository name * @return the list of tags for the image */ public List getTags(String registryName, String repositoryName) { try { - ContainerRegistryConfiguration containerRegistryConfiguration = this.registryConfigurations - .get(registryName); + ContainerRegistryConfiguration containerRegistryConfiguration = this.registryConfigurations.get(registryName); Map properties = new HashMap<>(); properties.put(DockerOAuth2RegistryAuthorizer.DOCKER_REGISTRY_REPOSITORY_FIELD_KEY, repositoryName); - HttpHeaders httpHeaders = new HttpHeaders( - this.registryAuthorizerMap.get(containerRegistryConfiguration.getAuthorizationType()) - .getAuthorizationHeaders(containerRegistryConfiguration, properties)); + HttpHeaders httpHeaders = new HttpHeaders(this.registryAuthorizerMap.get(containerRegistryConfiguration.getAuthorizationType()).getAuthorizationHeaders( + containerRegistryConfiguration, properties)); httpHeaders.set(HttpHeaders.ACCEPT, "application/json"); UriComponents manifestUriComponents = UriComponentsBuilder.newInstance() - .scheme(HTTPS_SCHEME) - .host(containerRegistryConfiguration.getRegistryHost()) - .path(TAGS_LIST_PATH) - .build() - .expand(repositoryName); + .scheme(HTTPS_SCHEME) + .host(containerRegistryConfiguration.getRegistryHost()) + .path(TAGS_LIST_PATH) + .build().expand(repositoryName); RestTemplate requestRestTemplate = this.containerImageRestTemplateFactory.getContainerRestTemplate( containerRegistryConfiguration.isDisableSslVerification(), - containerRegistryConfiguration.isUseHttpProxy(), containerRegistryConfiguration.getExtra()); + containerRegistryConfiguration.isUseHttpProxy(), + containerRegistryConfiguration.getExtra()); - ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), HttpMethod.GET, - new HttpEntity<>(httpHeaders), Map.class); - return (List) manifest.getBody().get(TAGS_FIELD); + ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), + HttpMethod.GET, new HttpEntity<>(httpHeaders), Map.class); + return (List) manifest.getBody().get(TAGS_FIELD); } catch (Exception e) { logger.error("Exception getting tag information for the {} from {}", repositoryName, registryName); @@ -136,25 +132,28 @@ public List getTags(String registryName, String repositoryName) { public Map getRepositories(String registryName) { try { ContainerRegistryConfiguration containerRegistryConfiguration = this.registryConfigurations - .get(registryName); + .get(registryName); Map properties = new HashMap<>(); properties.put(DockerOAuth2RegistryAuthorizer.DOCKER_REGISTRY_REPOSITORY_FIELD_KEY, registryName); HttpHeaders httpHeaders = new HttpHeaders( this.registryAuthorizerMap.get(containerRegistryConfiguration.getAuthorizationType()) - .getAuthorizationHeaders(containerRegistryConfiguration, properties)); + .getAuthorizationHeaders( + containerRegistryConfiguration, properties)); httpHeaders.set(HttpHeaders.ACCEPT, "application/json"); UriComponents manifestUriComponents = UriComponentsBuilder.newInstance() - .scheme(HTTPS_SCHEME) - .host(containerRegistryConfiguration.getRegistryHost()) - .path(CATALOG_LIST_PATH) - .build(); + .scheme(HTTPS_SCHEME) + .host(containerRegistryConfiguration.getRegistryHost()) + .path(CATALOG_LIST_PATH) + .build(); + RestTemplate requestRestTemplate = this.containerImageRestTemplateFactory.getContainerRestTemplate( containerRegistryConfiguration.isDisableSslVerification(), - containerRegistryConfiguration.isUseHttpProxy(), containerRegistryConfiguration.getExtra()); + containerRegistryConfiguration.isUseHttpProxy(), + containerRegistryConfiguration.getExtra()); - ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), HttpMethod.GET, - new HttpEntity<>(httpHeaders), Map.class); + ResponseEntity manifest = requestRestTemplate.exchange(manifestUriComponents.toUri(), + HttpMethod.GET, new HttpEntity<>(httpHeaders), Map.class); return manifest.getBody(); } catch (Exception e) { @@ -207,20 +206,18 @@ public T getImageManifest(ContainerRegistryRequest registryRequest, Class // Docker Registry HTTP V2 API pull manifest ContainerImage containerImage = registryRequest.getContainerImage(); UriComponents manifestUriComponents = UriComponentsBuilder.newInstance() - .scheme(HTTPS_SCHEME) - .host(containerImage.getHostname()) - .port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null) - .path(IMAGE_MANIFEST_REFERENCE_PATH) - .build() - .expand(containerImage.getRepository(), containerImage.getRepositoryReference()); + .scheme(HTTPS_SCHEME) + .host(containerImage.getHostname()) + .port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null) + .path(IMAGE_MANIFEST_REFERENCE_PATH) + .build().expand(containerImage.getRepository(), containerImage.getRepositoryReference()); - ResponseEntity manifest = registryRequest.getRestTemplate() - .exchange(manifestUriComponents.toUri(), HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); + ResponseEntity manifest = registryRequest.getRestTemplate().exchange(manifestUriComponents.toUri(), + HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); return manifest.getBody(); } - public T getImageBlob(ContainerRegistryRequest registryRequest, String configDigest, - Class responseClassType) { + public T getImageBlob(ContainerRegistryRequest registryRequest, String configDigest, Class responseClassType) { ContainerImage containerImage = registryRequest.getContainerImage(); HttpHeaders httpHeaders = new HttpHeaders(registryRequest.getAuthHttpHeaders()); @@ -230,19 +227,11 @@ public T getImageBlob(ContainerRegistryRequest registryRequest, String confi .host(containerImage.getHostname()) .port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null) .path(IMAGE_BLOB_DIGEST_PATH) - .build() - .expand(containerImage.getRepository(), configDigest); - try { - logger.info("getImageBlob:request:{},{}", blobUriComponents.toUri(), httpHeaders); - ResponseEntity blob = registryRequest.getRestTemplate() - .exchange(blobUriComponents.toUri(), HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); + .build().expand(containerImage.getRepository(), configDigest); - return blob.getStatusCode().is2xxSuccessful() ? blob.getBody() : null; - } - catch (RestClientException x) { - logger.error("getImageBlob:exception:" + x, x); - return null; - } - } + ResponseEntity blob = registryRequest.getRestTemplate().exchange(blobUriComponents.toUri(), + HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType); + return blob.getBody(); + } } 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 index 53e98c84dd..23df339e07 100644 --- 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 @@ -94,18 +94,8 @@ public URI getLocationURI(final HttpRequest request, final HttpResponse response // Handle Amazon requests if (StringUtils.hasText(query) && query.contains(AMZ_CREDENTIAL)) { if (isHeadOrGetMethod(method)) { - removeAuthorizationHeader(request, response, false); - try { - if (isHeadMethod(method)) { - return new HttpHead(httpUriRequest).getUri(); - } - else { - return new HttpGet(httpUriRequest).getUri(); - } - } - catch (URISyntaxException e) { - throw new HttpException("Unable to get location URI", e); - } + removeAuthorizationHeader(request, false); + return createResponseURI(method, httpUriRequest); } } @@ -113,13 +103,8 @@ public URI getLocationURI(final HttpRequest request, final HttpResponse response try { if (request.getUri().getRawPath().contains(AZURECR_URI_SUFFIX)) { if (isHeadOrGetMethod(method)) { - removeAuthorizationHeader(request, response, true); - if (isHeadMethod(method)) { - return new HttpHead(httpUriRequest).getUri(); - } - else { - return new HttpGet(httpUriRequest).getUri(); - } + removeAuthorizationHeader(request, true); + return createResponseURI(method, httpUriRequest); } } @@ -127,30 +112,32 @@ public URI getLocationURI(final HttpRequest request, final HttpResponse response if (extra.containsKey(CUSTOM_REGISTRY) && request.getUri().getRawPath().contains(extra.get(CUSTOM_REGISTRY))) { if (isHeadOrGetMethod(method)) { - removeAuthorizationHeader(request, response, false); - if (isHeadMethod(method)) { - return new HttpHead(httpUriRequest).getUri(); - } - else { - return new HttpGet(httpUriRequest).getUri(); - } + removeAuthorizationHeader(request, false); + return createResponseURI(method, httpUriRequest); } } } catch (URISyntaxException e) { - throw new HttpException("Unable to get Locaction URI", e); + throw new HttpException("Unable to get Location URI", e); } return httpUriRequest; } - private static void removeAuthorizationHeader(HttpRequest request, HttpResponse response, boolean onlyBasicAuth) { - for (Header header : response.getHeaders()) { - if (header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER) - && (!onlyBasicAuth || (onlyBasicAuth && header.getValue().contains(BASIC_AUTH)))) { - response.removeHeaders(header.getName()); - break; + private URI createResponseURI(String method, URI httpUriRequest) throws HttpException { + try { + if (isHeadMethod(method)) { + return new HttpHead(httpUriRequest).getUri(); } + else { + return new HttpGet(httpUriRequest).getUri(); + } + } + catch (URISyntaxException e) { + throw new HttpException("Unable to get location URI", e); } + } + + private static void removeAuthorizationHeader(HttpRequest request, boolean onlyBasicAuth) { for (Header header : request.getHeaders()) { if (header.getName().equalsIgnoreCase(AUTHORIZATION_HEADER) && (!onlyBasicAuth || (onlyBasicAuth && header.getValue().contains(BASIC_AUTH)))) {