From d5b2836ec90c5a94b2201c074db5e3fcc99a9204 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 28 Oct 2020 12:04:33 -0500 Subject: [PATCH 1/4] 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 --- .../docker/transport/HttpClientTransport.java | 22 +++++-- .../platform/docker/DockerApiTests.java | 8 +-- .../transport/HttpClientTransportTests.java | 66 ++++++++++++++++--- 3 files changed, 78 insertions(+), 18 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 20395f41b6..f1b3556035 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; @@ -118,8 +118,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); } @@ -172,7 +171,8 @@ abstract class HttpClientTransport implements HttpTransport { private final IOConsumer writer; - WritableHttpEntity(IOConsumer writer) { + WritableHttpEntity(String contentType, IOConsumer 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; + } + } + } /** 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 c61cd6a4d3..9f08f03e7b 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 @@ -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); 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 1e6e256c42..8b913d4507 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 @@ -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); } From 124c148c51e4e6bc9ce0e6484ce498fc95cb6254 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 28 Oct 2020 22:27:19 +0100 Subject: [PATCH 2/4] Upgrade to Spring Integration 5.2.9 Closes gh-23956 --- spring-boot-project/spring-boot-dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 88c14693f0..25a4e0c114 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -193,7 +193,7 @@ Moore-SR11 5.2.10.RELEASE 1.0.5.RELEASE - 5.2.8.RELEASE + 5.2.9.RELEASE 2.3.12.BUILD-SNAPSHOT 2.3.3.RELEASE 2.0.5.RELEASE From 11b55f650c639373940cd9855f01d0b1190739f3 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 28 Oct 2020 22:28:05 +0100 Subject: [PATCH 3/4] Upgrade to Spring Kafka 2.3.12 Closes gh-23786 --- spring-boot-project/spring-boot-dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 25a4e0c114..4823e1368b 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -194,7 +194,7 @@ 5.2.10.RELEASE 1.0.5.RELEASE 5.2.9.RELEASE - 2.3.12.BUILD-SNAPSHOT + 2.3.12.RELEASE 2.3.3.RELEASE 2.0.5.RELEASE 1.2.5.RELEASE From 425a2084ac1c2793c65643225f6ba0dda0345320 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 28 Oct 2020 22:32:55 +0100 Subject: [PATCH 4/4] Upgrade to Spring Kafka 2.5.7 Closes gh-23787 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 36acf95b47..1969c5d6d4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1690,7 +1690,7 @@ bom { ] } } - library("Spring Kafka", "2.5.7.BUILD-SNAPSHOT") { + library("Spring Kafka", "2.5.7.RELEASE") { group("org.springframework.kafka") { modules = [ "spring-kafka",