From 04a40a4c683561725b410d7ab8e7a724540987e6 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 28 Oct 2020 12:04:33 -0500 Subject: [PATCH] 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. This is a cherry-pick of the changes in d5b2836ec90c5a94b2201c074db5e3fcc99a9204 which were lost in a forward-merge. Fixes gh-23957 --- .../docker/transport/HttpClientTransport.java | 22 +++++-- .../platform/docker/DockerApiTests.java | 8 +-- .../transport/HttpClientTransportTests.java | 64 ++++++++++++++++--- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index 01c9995f08..f2d0794202 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -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; @@ -132,8 +132,7 @@ abstract class HttpClientTransport implements HttpTransport { private Response execute(HttpEntityEnclosingRequestBase request, String contentType, IOConsumer writer) { - request.setHeader(HttpHeaders.CONTENT_TYPE, contentType); - request.setEntity(new WritableHttpEntity(writer)); + request.setEntity(new WritableHttpEntity(contentType, writer)); return execute(request); } @@ -193,7 +192,8 @@ abstract class HttpClientTransport implements HttpTransport { private final IOConsumer writer; - WritableHttpEntity(IOConsumer writer) { + WritableHttpEntity(String contentType, IOConsumer writer) { + setContentType(contentType); this.writer = writer; } @@ -204,6 +204,9 @@ abstract class HttpClientTransport implements HttpTransport { @Override public long getContentLength() { + if (this.contentType != null && this.contentType.getValue().equals("application/json")) { + return calculateStringContentLength(); + } return -1; } @@ -222,6 +225,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; + } + } + } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 0d5d100b3d..fac9554e16 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -337,10 +337,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 @@ -359,10 +359,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); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java index 29d26e8a19..13587e1351 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java @@ -64,6 +64,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; @@ -155,44 +157,90 @@ class HttpClientTransportTests { } @Test - void postWithContentShouldExecuteHttpPost() throws Exception { + void postWithJsonContentShouldExecuteHttpPost() throws Exception { + String content = "test"; givenClientWillReturnResponse(); 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(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"; + givenClientWillReturnResponse(); + 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(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON); 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("test"); + assertThat(writeToString(entity)).isEqualTo(content); assertThat(response.getContent()).isSameAs(this.content); } @Test - void putWithContentShouldExecuteHttpPut() throws Exception { + void putWithJsonContentShouldExecuteHttpPut() throws Exception { + String content = "test"; givenClientWillReturnResponse(); 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(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"; + givenClientWillReturnResponse(); + 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(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON); 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("test"); + assertThat(writeToString(entity)).isEqualTo(content); assertThat(response.getContent()).isSameAs(this.content); }