Change header encoding to UTF8 in DefaultPartHttpMessageReader

This commit changes the encoding used to parse multipart headers from
ISO-8859-1 to UTF-8, in accordance with RFC 7578.

Closes gh-26736
This commit is contained in:
Arjen Poutsma 2021-03-30 10:51:19 +02:00
parent b651c10e83
commit d83fb09914
4 changed files with 56 additions and 13 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package org.springframework.http.codec.multipart; package org.springframework.http.codec.multipart;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -78,6 +79,8 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
private Mono<Path> fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); private Mono<Path> 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. * 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; 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 <a href="https://tools.ietf.org/html/rfc7578#section-5.1">RFC-7578 Section 5.2</a>
*/
public void setHeadersCharset(Charset headersCharset) {
Assert.notNull(headersCharset, "HeadersCharset must not be null");
this.headersCharset = headersCharset;
}
@Override @Override
public List<MediaType> getReadableMediaTypes() { public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
@ -214,7 +229,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
message.getHeaders().getContentType() + "\"")); message.getHeaders().getContentType() + "\""));
} }
Flux<MultipartParser.Token> tokens = MultipartParser.parse(message.getBody(), boundary, Flux<MultipartParser.Token> tokens = MultipartParser.parse(message.getBody(), boundary,
this.maxHeadersSize); this.maxHeadersSize, this.headersCharset);
return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart,
this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler);
@ -222,7 +237,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
} }
@Nullable @Nullable
private static byte[] boundary(HttpMessage message) { private byte[] boundary(HttpMessage message) {
MediaType contentType = message.getHeaders().getContentType(); MediaType contentType = message.getHeaders().getContentType();
if (contentType != null) { if (contentType != null) {
String boundary = contentType.getParameter("boundary"); 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) == '"') { if (len > 2 && boundary.charAt(0) == '"' && boundary.charAt(len - 1) == '"') {
boundary = boundary.substring(1, len - 1); boundary = boundary.substring(1, len - 1);
} }
return boundary.getBytes(StandardCharsets.ISO_8859_1); return boundary.getBytes(this.headersCharset);
} }
} }
return null; return null;

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@
package org.springframework.http.codec.multipart; package org.springframework.http.codec.multipart;
import java.nio.charset.StandardCharsets; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -69,11 +69,14 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
private final AtomicBoolean requestOutstanding = new AtomicBoolean(); private final AtomicBoolean requestOutstanding = new AtomicBoolean();
private final Charset headersCharset;
private MultipartParser(FluxSink<Token> sink, byte[] boundary, int maxHeadersSize) {
private MultipartParser(FluxSink<Token> sink, byte[] boundary, int maxHeadersSize, Charset headersCharset) {
this.sink = sink; this.sink = sink;
this.boundary = boundary; this.boundary = boundary;
this.maxHeadersSize = maxHeadersSize; this.maxHeadersSize = maxHeadersSize;
this.headersCharset = headersCharset;
this.state = new AtomicReference<>(new PreambleState()); this.state = new AtomicReference<>(new PreambleState());
} }
@ -82,11 +85,13 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
* @param buffers the input buffers * @param buffers the input buffers
* @param boundary the multipart boundary, as found in the {@code Content-Type} header * @param boundary the multipart boundary, as found in the {@code Content-Type} header
* @param maxHeadersSize the maximum buffered header size * @param maxHeadersSize the maximum buffered header size
* @param headersCharset the charset to use for decoding headers
* @return a stream of parsed tokens * @return a stream of parsed tokens
*/ */
public static Flux<Token> parse(Flux<DataBuffer> buffers, byte[] boundary, int maxHeadersSize) { public static Flux<Token> parse(Flux<DataBuffer> buffers, byte[] boundary, int maxHeadersSize,
Charset headersCharset) {
return Flux.create(sink -> { return Flux.create(sink -> {
MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize); MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize, headersCharset);
sink.onCancel(parser::onSinkCancel); sink.onCancel(parser::onSinkCancel);
sink.onRequest(n -> parser.requestBuffer()); sink.onRequest(n -> parser.requestBuffer());
buffers.subscribe(parser); buffers.subscribe(parser);
@ -180,7 +185,7 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
/** /**
* Represents the output of {@link #parse(Flux, byte[], int)}. * Represents the output of {@link #parse(Flux, byte[], int, Charset)}.
*/ */
public abstract static class Token { public abstract static class Token {
@ -372,7 +377,6 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
DataBufferUtils.release(buf); DataBufferUtils.release(buf);
emitHeaders(parseHeaders()); emitHeaders(parseHeaders());
// TODO: no need to check result of changeState, no further statements
changeState(this, new BodyState(), bodyBuf); changeState(this, new BodyState(), bodyBuf);
} }
else { else {
@ -408,7 +412,7 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
} }
DataBuffer joined = this.buffers.get(0).factory().join(this.buffers); DataBuffer joined = this.buffers.get(0).factory().join(this.buffers);
this.buffers.clear(); this.buffers.clear();
String string = joined.toString(StandardCharsets.ISO_8859_1); String string = joined.toString(MultipartParser.this.headersCharset);
DataBufferUtils.release(joined); DataBufferUtils.release(joined);
String[] lines = string.split(HEADER_ENTRY_SEPARATOR); String[] lines = string.split(HEADER_ENTRY_SEPARATOR);
HttpHeaders result = new HttpHeaders(); HttpHeaders result = new HttpHeaders();

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.lang.annotation.Target;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -251,6 +252,24 @@ public class DefaultPartHttpMessageReaderTests {
latch.await(); latch.await();
} }
@ParameterizedDefaultPartHttpMessageReaderTest
public void utf8Headers(String displayName, DefaultPartHttpMessageReader reader) throws InterruptedException {
MockServerHttpRequest request = createRequest(
new ClassPathResource("utf8.multipart", getClass()), "\"simple-boundary\"");
Flux<Part> 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) private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary)
throws InterruptedException { throws InterruptedException {

View File

@ -0,0 +1,5 @@
--simple-boundary
Føø: Bår
This is plain ASCII text.
--simple-boundary--