Commit d5b2836e authored by Scott Frederick's avatar Scott Frederick

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
parent d79c23ef
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
package org.springframework.boot.buildpack.platform.docker.transport; package org.springframework.boot.buildpack.platform.docker.transport;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI; import java.net.URI;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost; import org.apache.http.HttpHost;
import org.apache.http.StatusLine; import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
...@@ -118,8 +118,7 @@ abstract class HttpClientTransport implements HttpTransport { ...@@ -118,8 +118,7 @@ abstract class HttpClientTransport implements HttpTransport {
private Response execute(HttpEntityEnclosingRequestBase request, String contentType, private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
IOConsumer<OutputStream> writer) { IOConsumer<OutputStream> writer) {
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType); request.setEntity(new WritableHttpEntity(contentType, writer));
request.setEntity(new WritableHttpEntity(writer));
return execute(request); return execute(request);
} }
...@@ -172,7 +171,8 @@ abstract class HttpClientTransport implements HttpTransport { ...@@ -172,7 +171,8 @@ abstract class HttpClientTransport implements HttpTransport {
private final IOConsumer<OutputStream> writer; private final IOConsumer<OutputStream> writer;
WritableHttpEntity(IOConsumer<OutputStream> writer) { WritableHttpEntity(String contentType, IOConsumer<OutputStream> writer) {
setContentType(contentType);
this.writer = writer; this.writer = writer;
} }
...@@ -183,6 +183,9 @@ abstract class HttpClientTransport implements HttpTransport { ...@@ -183,6 +183,9 @@ abstract class HttpClientTransport implements HttpTransport {
@Override @Override
public long getContentLength() { public long getContentLength() {
if (this.contentType != null && this.contentType.getValue().equals("application/json")) {
return calculateStringContentLength();
}
return -1; return -1;
} }
...@@ -201,6 +204,17 @@ abstract class HttpClientTransport implements HttpTransport { ...@@ -201,6 +204,17 @@ abstract class HttpClientTransport implements HttpTransport {
return true; 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 { ...@@ -262,10 +262,10 @@ class DockerApiTests {
.willReturn(responseOf("create-container-response.json")); .willReturn(responseOf("create-container-response.json"));
ContainerReference containerReference = this.api.create(config); ContainerReference containerReference = this.api.create(config);
assertThat(containerReference.toString()).isEqualTo("e90e34656806"); assertThat(containerReference.toString()).isEqualTo("e90e34656806");
ByteArrayOutputStream out = new ByteArrayOutputStream();
verify(http()).post(any(), any(), this.writer.capture()); verify(http()).post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
this.writer.getValue().accept(out); this.writer.getValue().accept(out);
assertThat(out.toByteArray()).hasSizeGreaterThan(130); assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
} }
@Test @Test
...@@ -284,10 +284,10 @@ class DockerApiTests { ...@@ -284,10 +284,10 @@ class DockerApiTests {
given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
ContainerReference containerReference = this.api.create(config, content); ContainerReference containerReference = this.api.create(config, content);
assertThat(containerReference.toString()).isEqualTo("e90e34656806"); assertThat(containerReference.toString()).isEqualTo("e90e34656806");
ByteArrayOutputStream out = new ByteArrayOutputStream();
verify(http()).post(any(), any(), this.writer.capture()); verify(http()).post(any(), any(), this.writer.capture());
ByteArrayOutputStream out = new ByteArrayOutputStream();
this.writer.getValue().accept(out); 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()); verify(http()).put(any(), any(), this.writer.capture());
this.writer.getValue().accept(out); this.writer.getValue().accept(out);
assertThat(out.toByteArray()).hasSizeGreaterThan(2000); assertThat(out.toByteArray()).hasSizeGreaterThan(2000);
......
...@@ -62,6 +62,8 @@ class HttpClientTransportTests { ...@@ -62,6 +62,8 @@ class HttpClientTransportTests {
private static final String APPLICATION_JSON = "application/json"; private static final String APPLICATION_JSON = "application/json";
private static final String APPLICATION_X_TAR = "application/x-tar";
@Mock @Mock
private CloseableHttpClient client; private CloseableHttpClient client;
...@@ -124,42 +126,86 @@ class HttpClientTransportTests { ...@@ -124,42 +126,86 @@ class HttpClientTransportTests {
} }
@Test @Test
void postWithContentShouldExecuteHttpPost() throws Exception { void postWithJsonContentShouldExecuteHttpPost() throws Exception {
String content = "test";
given(this.entity.getContent()).willReturn(this.content); given(this.entity.getContent()).willReturn(this.content);
given(this.statusLine.getStatusCode()).willReturn(200); given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.post(this.uri, APPLICATION_JSON, 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(entity.isRepeatable()).isFalse();
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(content);
assertThat(response.getContent()).isSameAs(this.content);
}
@Test
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()); verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue(); HttpUriRequest request = this.requestCaptor.getValue();
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
assertThat(request).isInstanceOf(HttpPost.class); assertThat(request).isInstanceOf(HttpPost.class);
assertThat(request.getURI()).isEqualTo(this.uri); assertThat(request.getURI()).isEqualTo(this.uri);
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
assertThat(entity.isRepeatable()).isFalse(); assertThat(entity.isRepeatable()).isFalse();
assertThat(entity.getContentLength()).isEqualTo(-1); assertThat(entity.getContentLength()).isEqualTo(-1);
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
assertThat(entity.isStreaming()).isTrue(); assertThat(entity.isStreaming()).isTrue();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
assertThat(writeToString(entity)).isEqualTo("test"); assertThat(writeToString(entity)).isEqualTo(content);
assertThat(response.getContent()).isSameAs(this.content); assertThat(response.getContent()).isSameAs(this.content);
} }
@Test @Test
void putWithContentShouldExecuteHttpPut() throws Exception { void putWithJsonContentShouldExecuteHttpPut() throws Exception {
String content = "test";
given(this.entity.getContent()).willReturn(this.content); given(this.entity.getContent()).willReturn(this.content);
given(this.statusLine.getStatusCode()).willReturn(200); given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.put(this.uri, APPLICATION_JSON, 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(entity.isRepeatable()).isFalse();
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(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()); verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue(); HttpUriRequest request = this.requestCaptor.getValue();
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
assertThat(request).isInstanceOf(HttpPut.class); assertThat(request).isInstanceOf(HttpPut.class);
assertThat(request.getURI()).isEqualTo(this.uri); assertThat(request.getURI()).isEqualTo(this.uri);
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
assertThat(entity.isRepeatable()).isFalse(); assertThat(entity.isRepeatable()).isFalse();
assertThat(entity.getContentLength()).isEqualTo(-1); assertThat(entity.getContentLength()).isEqualTo(-1);
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
assertThat(entity.isStreaming()).isTrue(); assertThat(entity.isStreaming()).isTrue();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
assertThat(writeToString(entity)).isEqualTo("test"); assertThat(writeToString(entity)).isEqualTo(content);
assertThat(response.getContent()).isSameAs(this.content); assertThat(response.getContent()).isSameAs(this.content);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment