Provide content-length header to Docker API calls
Docker daemon authorization plugins reject POST or PUT requests that have a content type `application/json` header but no content length header. This commit ensures that a content length header is provided in these cases. Fixes gh-22840
This commit is contained in:
parent
d79c23ef89
commit
d5b2836ec9
|
|
@ -16,13 +16,13 @@
|
|||
|
||||
package org.springframework.boot.buildpack.platform.docker.transport;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.HttpClient;
|
||||
|
|
@ -118,8 +118,7 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
|
||||
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
|
||||
IOConsumer<OutputStream> writer) {
|
||||
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
request.setEntity(new WritableHttpEntity(writer));
|
||||
request.setEntity(new WritableHttpEntity(contentType, writer));
|
||||
return execute(request);
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +171,8 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
|
||||
private final IOConsumer<OutputStream> writer;
|
||||
|
||||
WritableHttpEntity(IOConsumer<OutputStream> writer) {
|
||||
WritableHttpEntity(String contentType, IOConsumer<OutputStream> writer) {
|
||||
setContentType(contentType);
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +183,9 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
|
||||
@Override
|
||||
public long getContentLength() {
|
||||
if (this.contentType != null && this.contentType.getValue().equals("application/json")) {
|
||||
return calculateStringContentLength();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +204,17 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
return true;
|
||||
}
|
||||
|
||||
private int calculateStringContentLength() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
this.writer.accept(bytes);
|
||||
return bytes.toByteArray().length;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -262,10 +262,10 @@ class DockerApiTests {
|
|||
.willReturn(responseOf("create-container-response.json"));
|
||||
ContainerReference containerReference = this.api.create(config);
|
||||
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
verify(http()).post(any(), any(), this.writer.capture());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
|
||||
assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -284,10 +284,10 @@ class DockerApiTests {
|
|||
given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
|
||||
ContainerReference containerReference = this.api.create(config, content);
|
||||
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
verify(http()).post(any(), any(), this.writer.capture());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
|
||||
assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
|
||||
verify(http()).put(any(), any(), this.writer.capture());
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(2000);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ class HttpClientTransportTests {
|
|||
|
||||
private static final String APPLICATION_JSON = "application/json";
|
||||
|
||||
private static final String APPLICATION_X_TAR = "application/x-tar";
|
||||
|
||||
@Mock
|
||||
private CloseableHttpClient client;
|
||||
|
||||
|
|
@ -124,42 +126,86 @@ class HttpClientTransportTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void postWithContentShouldExecuteHttpPost() throws Exception {
|
||||
void postWithJsonContentShouldExecuteHttpPost() throws Exception {
|
||||
String content = "test";
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.post(this.uri, APPLICATION_JSON,
|
||||
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
|
||||
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPost.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
|
||||
assertThat(entity.isRepeatable()).isFalse();
|
||||
assertThat(entity.getContentLength()).isEqualTo(-1);
|
||||
assertThat(entity.getContentLength()).isEqualTo(content.length());
|
||||
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON);
|
||||
assertThat(entity.isStreaming()).isTrue();
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
|
||||
assertThat(writeToString(entity)).isEqualTo("test");
|
||||
assertThat(writeToString(entity)).isEqualTo(content);
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putWithContentShouldExecuteHttpPut() throws Exception {
|
||||
void postWithArchiveContentShouldExecuteHttpPost() throws Exception {
|
||||
String content = "test";
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.post(this.uri, APPLICATION_X_TAR,
|
||||
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPost.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(entity.isRepeatable()).isFalse();
|
||||
assertThat(entity.getContentLength()).isEqualTo(-1);
|
||||
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
|
||||
assertThat(entity.isStreaming()).isTrue();
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
|
||||
assertThat(writeToString(entity)).isEqualTo(content);
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putWithJsonContentShouldExecuteHttpPut() throws Exception {
|
||||
String content = "test";
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.put(this.uri, APPLICATION_JSON,
|
||||
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
|
||||
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPut.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
|
||||
assertThat(entity.isRepeatable()).isFalse();
|
||||
assertThat(entity.getContentLength()).isEqualTo(-1);
|
||||
assertThat(entity.getContentLength()).isEqualTo(content.length());
|
||||
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON);
|
||||
assertThat(entity.isStreaming()).isTrue();
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
|
||||
assertThat(writeToString(entity)).isEqualTo("test");
|
||||
assertThat(writeToString(entity)).isEqualTo(content);
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putWithArchiveContentShouldExecuteHttpPut() throws Exception {
|
||||
String content = "test";
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.put(this.uri, APPLICATION_X_TAR,
|
||||
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPut.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(entity.isRepeatable()).isFalse();
|
||||
assertThat(entity.getContentLength()).isEqualTo(-1);
|
||||
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
|
||||
assertThat(entity.isStreaming()).isTrue();
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
|
||||
assertThat(writeToString(entity)).isEqualTo(content);
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue