diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 5aa3761fc4..f1e5c59db5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -78,6 +79,8 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private Charset headersCharset = StandardCharsets.UTF_8; + /** * Configure the maximum amount of memory that is allowed per headers section of each part. @@ -188,6 +191,18 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements this.streaming = streaming; } + /** + * Sets the character set used to decode headers. Defaults to + * UTF-8 as per RFC 7578. + * @param headersCharset the charset to use for decoding headers + * @since 5.3.6 + * @see RFC-7578 Section 5.2 + */ + public void setHeadersCharset(Charset headersCharset) { + Assert.notNull(headersCharset, "HeadersCharset must not be null"); + this.headersCharset = headersCharset; + } + @Override public List getReadableMediaTypes() { return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); @@ -214,7 +229,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements message.getHeaders().getContentType() + "\"")); } Flux tokens = MultipartParser.parse(message.getBody(), boundary, - this.maxHeadersSize); + this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); @@ -222,7 +237,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements } @Nullable - private static byte[] boundary(HttpMessage message) { + private byte[] boundary(HttpMessage message) { MediaType contentType = message.getHeaders().getContentType(); if (contentType != null) { String boundary = contentType.getParameter("boundary"); @@ -231,7 +246,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements if (len > 2 && boundary.charAt(0) == '"' && boundary.charAt(len - 1) == '"') { boundary = boundary.substring(1, len - 1); } - return boundary.getBytes(StandardCharsets.ISO_8859_1); + return boundary.getBytes(this.headersCharset); } } return null; diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java index deed6c3279..3d9ab7f2b3 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.http.codec.multipart; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -69,11 +69,14 @@ final class MultipartParser extends BaseSubscriber { private final AtomicBoolean requestOutstanding = new AtomicBoolean(); + private final Charset headersCharset; - private MultipartParser(FluxSink sink, byte[] boundary, int maxHeadersSize) { + + private MultipartParser(FluxSink sink, byte[] boundary, int maxHeadersSize, Charset headersCharset) { this.sink = sink; this.boundary = boundary; this.maxHeadersSize = maxHeadersSize; + this.headersCharset = headersCharset; this.state = new AtomicReference<>(new PreambleState()); } @@ -82,11 +85,13 @@ final class MultipartParser extends BaseSubscriber { * @param buffers the input buffers * @param boundary the multipart boundary, as found in the {@code Content-Type} header * @param maxHeadersSize the maximum buffered header size + * @param headersCharset the charset to use for decoding headers * @return a stream of parsed tokens */ - public static Flux parse(Flux buffers, byte[] boundary, int maxHeadersSize) { + public static Flux parse(Flux buffers, byte[] boundary, int maxHeadersSize, + Charset headersCharset) { return Flux.create(sink -> { - MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize); + MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize, headersCharset); sink.onCancel(parser::onSinkCancel); sink.onRequest(n -> parser.requestBuffer()); buffers.subscribe(parser); @@ -180,7 +185,7 @@ final class MultipartParser extends BaseSubscriber { /** - * Represents the output of {@link #parse(Flux, byte[], int)}. + * Represents the output of {@link #parse(Flux, byte[], int, Charset)}. */ public abstract static class Token { @@ -372,7 +377,6 @@ final class MultipartParser extends BaseSubscriber { DataBufferUtils.release(buf); emitHeaders(parseHeaders()); - // TODO: no need to check result of changeState, no further statements changeState(this, new BodyState(), bodyBuf); } else { @@ -408,7 +412,7 @@ final class MultipartParser extends BaseSubscriber { } DataBuffer joined = this.buffers.get(0).factory().join(this.buffers); this.buffers.clear(); - String string = joined.toString(StandardCharsets.ISO_8859_1); + String string = joined.toString(MultipartParser.this.headersCharset); DataBufferUtils.release(joined); String[] lines = string.split(HEADER_ENTRY_SEPARATOR); HttpHeaders result = new HttpHeaders(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java index 075ad7c24f..8e812e720f 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -251,6 +252,24 @@ public class DefaultPartHttpMessageReaderTests { latch.await(); } + @ParameterizedDefaultPartHttpMessageReaderTest + public void utf8Headers(String displayName, DefaultPartHttpMessageReader reader) throws InterruptedException { + MockServerHttpRequest request = createRequest( + new ClassPathResource("utf8.multipart", getClass()), "\"simple-boundary\""); + + Flux result = reader.read(forClass(Part.class), request, emptyMap()); + + CountDownLatch latch = new CountDownLatch(1); + StepVerifier.create(result) + .consumeNextWith(part -> { + assertThat(part.headers()).containsEntry("Føø", Collections.singletonList("Bår")); + testPart(part, null, "This is plain ASCII text.", latch); + }) + .verifyComplete(); + + latch.await(); + } + private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary) throws InterruptedException { diff --git a/spring-web/src/test/resources/org/springframework/http/codec/multipart/utf8.multipart b/spring-web/src/test/resources/org/springframework/http/codec/multipart/utf8.multipart new file mode 100644 index 0000000000..7867df68ae --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/codec/multipart/utf8.multipart @@ -0,0 +1,5 @@ +--simple-boundary +Føø: Bår + +This is plain ASCII text. +--simple-boundary--