-
Notifications
You must be signed in to change notification settings - Fork 583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The authorization header was not dropped by redirect strategy as expected. #5984
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -31,9 +32,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 +49,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 +88,7 @@ | |
* | ||
* @author Christian Tzolov | ||
* @author Cheng Guan Poh | ||
* @author Corneil du Plessis | ||
*/ | ||
public class ContainerImageRestTemplateFactory { | ||
|
||
|
@@ -197,30 +203,34 @@ private HttpClientBuilder httpClientBuilder(SSLContext sslContext) { | |
private RestTemplate initRestTemplate(HttpClientBuilder clientBuilder, boolean withHttpProxy, Map<String, String> extra) { | ||
|
||
clientBuilder.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(StandardCookieSpec.RELAXED).build()); | ||
|
||
DefaultRoutePlanner routePlanner; | ||
// 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); | ||
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on your reply to the mailing list comment, it sounds like you are going to pursue option 2) which is to append a custom request interceptor immediately after RedirectExec and strip undesired headers from requests. I appreciate the scrappiness of figuring out how to get this to work in this case but I would really really like to NOT own this copy of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After a lot of back and forth with extra exec interceptor and request interceptor the only conclusion I have is that we need a modified version of |
||
// Azure redirects may contain double slashes and on default those are normalised | ||
.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. | ||
// Therefore we extend the MappingJackson2HttpMessageConverter media-types to | ||
// include application/octet-stream and text/plain. | ||
MappingJackson2HttpMessageConverter octetSupportJsonConverter = new MappingJackson2HttpMessageConverter(); | ||
ArrayList<MediaType> mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes()); | ||
List<MediaType> mediaTypeList = new ArrayList(octetSupportJsonConverter.getSupportedMediaTypes()); | ||
mediaTypeList.add(MediaType.APPLICATION_OCTET_STREAM); | ||
mediaTypeList.add(MediaType.TEXT_PLAIN); | ||
octetSupportJsonConverter.setSupportedMediaTypes(mediaTypeList); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,69 @@ 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, false); | ||
return createResponseURI(method, httpUriRequest); | ||
} | ||
} | ||
|
||
// 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, true); | ||
return createResponseURI(method, httpUriRequest); | ||
} | ||
} | ||
|
||
// Handle Custom requests | ||
if (extra.containsKey(CUSTOM_REGISTRY) | ||
&& request.getUri().getRawPath().contains(extra.get(CUSTOM_REGISTRY))) { | ||
if (isHeadOrGetMethod(method)) { | ||
removeAuthorizationHeader(request, false); | ||
return createResponseURI(method, httpUriRequest); | ||
} | ||
} | ||
} | ||
} catch (URISyntaxException e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice cleanup! |
||
throw new HttpException("Unable to get Locaction URI", e); | ||
catch (URISyntaxException e) { | ||
throw new HttpException("Unable to get Location 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 URI createResponseURI(String method, URI httpUriRequest) throws HttpException { | ||
try { | ||
if (isHeadMethod(method)) { | ||
return new HttpHead(httpUriRequest).getUri(); | ||
} | ||
} | ||
|
||
@Override | ||
public void addHeader(String name, Object value) { | ||
if (!isDropHeader(name, value)) { | ||
super.addHeader(name, value); | ||
else { | ||
return new HttpGet(httpUriRequest).getUri(); | ||
} | ||
} | ||
|
||
@Override | ||
public void setHeader(Header header) { | ||
if (!isDropHeader(header)) { | ||
super.setHeader(header); | ||
} | ||
catch (URISyntaxException e) { | ||
throw new HttpException("Unable to get location URI", e); | ||
} | ||
} | ||
|
||
@Override | ||
public void setHeader(String name, Object value) { | ||
if (!isDropHeader(name, value)) { | ||
super.setHeader(name, value); | ||
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)))) { | ||
request.removeHeaders(header.getName()); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
@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); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[UNRELATED-TO-YOUR-CHANGES] These 2 controllers are terribly named and there is no comment telling us that these are test controllers used to mimic what S3 signed redirects look like.
Because it is a test, we do not need to instrument it w/ log statements to debug. In that case, we can just walk through it in a debugger. Do you mind removing these debugging log statements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done