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:
parent
b651c10e83
commit
d83fb09914
|
@ -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<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.
|
||||
|
@ -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 <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
|
||||
public List<MediaType> getReadableMediaTypes() {
|
||||
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
|
||||
|
@ -214,7 +229,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
|
|||
message.getHeaders().getContentType() + "\""));
|
||||
}
|
||||
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,
|
||||
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;
|
||||
|
|
|
@ -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<DataBuffer> {
|
|||
|
||||
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.boundary = boundary;
|
||||
this.maxHeadersSize = maxHeadersSize;
|
||||
this.headersCharset = headersCharset;
|
||||
this.state = new AtomicReference<>(new PreambleState());
|
||||
}
|
||||
|
||||
|
@ -82,11 +85,13 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
|||
* @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<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 -> {
|
||||
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<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 {
|
||||
|
||||
|
@ -372,7 +377,6 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
|||
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> {
|
|||
}
|
||||
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();
|
||||
|
|
|
@ -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<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)
|
||||
throws InterruptedException {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
--simple-boundary
|
||||
Føø: Bår
|
||||
|
||||
This is plain ASCII text.
|
||||
--simple-boundary--
|
Loading…
Reference in New Issue