mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Merge commit 'v7.1.0-M1~1'
This commit is contained in:
@@ -172,17 +172,17 @@ public final class ContentDisposition {
|
||||
sb.append(this.type);
|
||||
}
|
||||
if (this.name != null) {
|
||||
sb.append("; name=\"").append(this.name).append('\"');
|
||||
sb.append("; name=\"");
|
||||
appendName(sb, this.name).append('\"');
|
||||
}
|
||||
if (this.filename != null) {
|
||||
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
|
||||
sb.append("; filename=\"")
|
||||
.append(encodeQuotedPairs(this.filename))
|
||||
.append('\"');
|
||||
sb.append("; filename=\"");
|
||||
appendName(sb, this.filename).append('\"');
|
||||
}
|
||||
else {
|
||||
sb.append("; filename=\"")
|
||||
.append(transliterateToAscii(encodeQuotedPairs(this.filename)))
|
||||
.append(transliterateToAscii(appendName(new StringBuilder(), this.filename).toString()))
|
||||
.append("\"; filename*=")
|
||||
.append(encodeRfc5987Filename(this.filename, this.charset));
|
||||
}
|
||||
@@ -253,7 +253,7 @@ public final class ContentDisposition {
|
||||
part.substring(eqIndex + 2, part.length() - 1) :
|
||||
part.substring(eqIndex + 1));
|
||||
if (attribute.equals("name") ) {
|
||||
name = value;
|
||||
name = (value.indexOf('\\') != -1 ? decodeQuotedPairs(value) : value);
|
||||
}
|
||||
else if (attribute.equals("filename*") ) {
|
||||
int idx1 = value.indexOf('\'');
|
||||
@@ -503,19 +503,20 @@ public final class ContentDisposition {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String encodeQuotedPairs(String filename) {
|
||||
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
|
||||
return filename;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < filename.length() ; i++) {
|
||||
char c = filename.charAt(i);
|
||||
if (c == '"' || c == '\\') {
|
||||
sb.append('\\');
|
||||
private static StringBuilder appendName(StringBuilder buffer, String name) {
|
||||
for (int i = 0; i < name.length() ; i++) {
|
||||
char c = name.charAt(i);
|
||||
// strip control characters
|
||||
if (c <= 0x1F || c == 0x7F) {
|
||||
continue;
|
||||
}
|
||||
sb.append(c);
|
||||
// encode quoted pairs
|
||||
if (c == '"' || c == '\\') {
|
||||
buffer.append('\\');
|
||||
}
|
||||
buffer.append(c);
|
||||
}
|
||||
return sb.toString();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static String decodeQuotedPairs(String filename) {
|
||||
|
||||
@@ -20,8 +20,8 @@ import java.time.Duration;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.util.SseUtils;
|
||||
|
||||
/**
|
||||
* Representation for a Server-Sent Event for use with Spring's reactive Web support.
|
||||
@@ -112,7 +112,7 @@ public final class ServerSentEvent<T> {
|
||||
}
|
||||
if (this.comment != null) {
|
||||
sb.append(':');
|
||||
appendEscaped(this.comment, "\n:", sb);
|
||||
SseUtils.appendFieldValue("", this.comment, sb);
|
||||
sb.append('\n');
|
||||
}
|
||||
if (this.data != null) {
|
||||
@@ -125,30 +125,6 @@ public final class ServerSentEvent<T> {
|
||||
sb.append(fieldName).append(':').append(fieldValue).append('\n');
|
||||
}
|
||||
|
||||
private void appendEscaped(String input, String replacement, StringBuilder sb) {
|
||||
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
|
||||
sb.append(input);
|
||||
}
|
||||
else {
|
||||
int length = input.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = input.charAt(i);
|
||||
if (c == '\r') {
|
||||
if (i + 1 < length && input.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
sb.append(replacement);
|
||||
}
|
||||
else if (c == '\n') {
|
||||
sb.append(replacement);
|
||||
}
|
||||
else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof ServerSentEvent<?> that &&
|
||||
@@ -265,23 +241,18 @@ public final class ServerSentEvent<T> {
|
||||
|
||||
@Override
|
||||
public Builder<T> id(String id) {
|
||||
checkEvent(id);
|
||||
SseUtils.assertNoLineSeparator(id);
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder<T> event(String event) {
|
||||
checkEvent(event);
|
||||
SseUtils.assertNoLineSeparator(event);
|
||||
this.event = event;
|
||||
return this;
|
||||
}
|
||||
|
||||
private static void checkEvent(String content) {
|
||||
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
|
||||
"illegal character '\\n' or '\\r' in event content");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder<T> retry(Duration retry) {
|
||||
this.retry = retry;
|
||||
|
||||
+2
-21
@@ -40,6 +40,7 @@ import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.SseUtils;
|
||||
|
||||
/**
|
||||
* {@code HttpMessageWriter} for {@code "text/event-stream"} responses.
|
||||
@@ -142,27 +143,7 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
|
||||
}
|
||||
|
||||
private void writeStringData(String input, StringBuilder sb) {
|
||||
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
|
||||
sb.append(input);
|
||||
}
|
||||
else {
|
||||
int length = input.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = input.charAt(i);
|
||||
if (c == '\r') {
|
||||
if (i + 1 < length && input.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
sb.append("\ndata:");
|
||||
}
|
||||
else if (c == '\n') {
|
||||
sb.append("\ndata:");
|
||||
}
|
||||
else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
SseUtils.appendFieldValue("data", input, sb);
|
||||
sb.append("\n\n");
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -188,6 +188,9 @@ public class PartEventHttpMessageReader extends LoggingCodecSupport implements H
|
||||
if (this.maxPartSize == -1) {
|
||||
maxSize = this.maxInMemorySize;
|
||||
}
|
||||
else if (this.maxInMemorySize == -1) {
|
||||
maxSize = (int) Math.min(Integer.MAX_VALUE, this.maxPartSize);
|
||||
}
|
||||
else {
|
||||
// maxInMemorySize is an int, so we can safely cast the long result of Math.min
|
||||
maxSize = (int) Math.min(this.maxInMemorySize, this.maxPartSize);
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -141,8 +142,10 @@ abstract class Jaxb2Helper {
|
||||
* </li>
|
||||
* </ol>
|
||||
*/
|
||||
public static Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, Set<QName> names) {
|
||||
return xmlEventFlux.handle(new SplitHandler(names));
|
||||
public static Flux<List<XMLEvent>> split(
|
||||
Flux<XMLEvent> xmlEventFlux, Set<QName> names, XmlEventDecoder.@Nullable ReceivedByteTracker byteTracker) {
|
||||
|
||||
return xmlEventFlux.handle(new SplitHandler(names, byteTracker));
|
||||
}
|
||||
|
||||
|
||||
@@ -150,14 +153,17 @@ abstract class Jaxb2Helper {
|
||||
|
||||
private final Set<QName> names;
|
||||
|
||||
private final XmlEventDecoder.ReceivedByteTracker byteTracker;
|
||||
|
||||
private @Nullable List<XMLEvent> events;
|
||||
|
||||
private int elementDepth = 0;
|
||||
|
||||
private int barrier = Integer.MAX_VALUE;
|
||||
|
||||
public SplitHandler(Set<QName> names) {
|
||||
public SplitHandler(Set<QName> names, XmlEventDecoder.@Nullable ReceivedByteTracker byteTracker) {
|
||||
this.names = names;
|
||||
this.byteTracker = (byteTracker != null ? byteTracker : XmlEventDecoder.ReceivedByteTracker.NO_OP);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,11 +185,19 @@ abstract class Jaxb2Helper {
|
||||
if (event.isEndElement()) {
|
||||
this.elementDepth--;
|
||||
if (this.elementDepth == this.barrier) {
|
||||
this.barrier = Integer.MAX_VALUE;
|
||||
Assert.state(this.events != null, "No XMLEvent List");
|
||||
sink.next(this.events);
|
||||
this.barrier = Integer.MAX_VALUE;
|
||||
this.events = null;
|
||||
}
|
||||
}
|
||||
if (this.events == null) {
|
||||
this.byteTracker.reset();
|
||||
}
|
||||
else if (this.byteTracker.isMaxInMemorySizeExceeded()) {
|
||||
throw new DataBufferLimitException(
|
||||
"Exceeded limit on max bytes per XML node: " + this.byteTracker.getMaxInMemorySize());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,12 +145,16 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
|
||||
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
XmlEventDecoder.ReceivedByteTracker byteTracker =
|
||||
new XmlEventDecoder.ReceivedByteTracker(this.maxInMemorySize);
|
||||
|
||||
Flux<XMLEvent> xmlEventFlux = this.xmlEventDecoder.decode(
|
||||
inputStream, ResolvableType.forClass(XMLEvent.class), mimeType, hints);
|
||||
inputStream, ResolvableType.forClass(XMLEvent.class), mimeType,
|
||||
Hints.merge(hints, XmlEventDecoder.BYTE_TRACKER_HINT, byteTracker));
|
||||
|
||||
Class<?> outputClass = elementType.toClass();
|
||||
Set<QName> typeNames = Jaxb2Helper.toQNames(outputClass);
|
||||
Flux<List<XMLEvent>> splitEvents = Jaxb2Helper.split(xmlEventFlux, typeNames);
|
||||
Flux<List<XMLEvent>> splitEvents = Jaxb2Helper.split(xmlEventFlux, typeNames, byteTracker);
|
||||
|
||||
return splitEvents.map(events -> {
|
||||
Object value = unmarshal(events, outputClass);
|
||||
|
||||
@@ -84,6 +84,12 @@ import org.springframework.util.xml.StaxUtils;
|
||||
*/
|
||||
public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
|
||||
/**
|
||||
* Hint key for a {@link ReceivedByteTracker} instance that callers can use
|
||||
* to track the number of received bytes.
|
||||
*/
|
||||
public static final String BYTE_TRACKER_HINT = XmlEventDecoder.class.getName() + ".byteTracker";
|
||||
|
||||
private static final XMLInputFactory inputFactory = StaxUtils.createDefensiveInputFactory();
|
||||
|
||||
private static final boolean AALTO_PRESENT = ClassUtils.isPresent(
|
||||
@@ -100,10 +106,13 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
|
||||
|
||||
/**
|
||||
* Set the max number of bytes that can be buffered by this decoder. This
|
||||
* is either the size the entire input when decoding as a whole, or when
|
||||
* using async parsing via Aalto XML, it is size one top-level XML tree.
|
||||
* When the limit is exceeded, {@link DataBufferLimitException} is raised.
|
||||
* Set the max number of bytes this decoder should buffer in memory resulting
|
||||
* in a {@link DataBufferLimitException} when the limit is exceeded.
|
||||
* <p>When joining all buffers and decoding as a whole, the limit is applied
|
||||
* to the entire input.
|
||||
* <p>>When using Aalto XML async parsing, the limit does not apply at the
|
||||
* level of this decoder because the XML events parsed from each buffer are
|
||||
* emitted immediately and the buffer is released.
|
||||
* <p>By default this is set to 256K.
|
||||
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
|
||||
* @since 5.1.11
|
||||
@@ -126,7 +135,7 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
if (this.useAalto) {
|
||||
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(this.maxInMemorySize);
|
||||
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(hints);
|
||||
return Flux.from(input)
|
||||
.flatMapIterable(mapper)
|
||||
.doFinally(signalType -> mapper.endOfInput());
|
||||
@@ -155,7 +164,7 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
/*
|
||||
* Separate static class to isolate Aalto dependency.
|
||||
*/
|
||||
private static class AaltoDataBufferToXmlEvent implements Function<DataBuffer, List<? extends XMLEvent>> {
|
||||
private static final class AaltoDataBufferToXmlEvent implements Function<DataBuffer, List<? extends XMLEvent>> {
|
||||
|
||||
private static final AsyncXMLInputFactory inputFactory =
|
||||
StaxUtils.createDefensiveInputFactory(InputFactoryImpl::new);
|
||||
@@ -165,22 +174,19 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
|
||||
private final XMLEventAllocator eventAllocator = EventAllocatorImpl.getDefaultInstance();
|
||||
|
||||
private final int maxInMemorySize;
|
||||
@Nullable
|
||||
private final ReceivedByteTracker byteTracker;
|
||||
|
||||
private int byteCount;
|
||||
|
||||
private int elementDepth;
|
||||
|
||||
|
||||
public AaltoDataBufferToXmlEvent(int maxInMemorySize) {
|
||||
this.maxInMemorySize = maxInMemorySize;
|
||||
private AaltoDataBufferToXmlEvent(@Nullable Map<String, Object> hints) {
|
||||
this.byteTracker = (hints != null ? (ReceivedByteTracker) hints.get(BYTE_TRACKER_HINT) : null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<? extends XMLEvent> apply(DataBuffer dataBuffer) {
|
||||
try {
|
||||
increaseByteCount(dataBuffer);
|
||||
if (this.byteTracker != null) {
|
||||
this.byteTracker.incrementByteCount(dataBuffer);
|
||||
}
|
||||
AsyncByteBufferFeeder inputFeeder = this.streamReader.getInputFeeder();
|
||||
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
|
||||
while (iterator.hasNext()) {
|
||||
@@ -199,12 +205,8 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
if (event.isEndDocument()) {
|
||||
break;
|
||||
}
|
||||
checkDepthAndResetByteCount(event);
|
||||
}
|
||||
}
|
||||
if (this.maxInMemorySize > 0 && this.byteCount > this.maxInMemorySize) {
|
||||
raiseLimitException();
|
||||
}
|
||||
return events;
|
||||
}
|
||||
catch (XMLStreamException ex) {
|
||||
@@ -215,40 +217,55 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
private void increaseByteCount(DataBuffer dataBuffer) {
|
||||
if (this.maxInMemorySize > 0) {
|
||||
if (dataBuffer.readableByteCount() > Integer.MAX_VALUE - this.byteCount) {
|
||||
raiseLimitException();
|
||||
}
|
||||
else {
|
||||
this.byteCount += dataBuffer.readableByteCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDepthAndResetByteCount(XMLEvent event) {
|
||||
if (this.maxInMemorySize > 0) {
|
||||
if (event.isStartElement()) {
|
||||
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
|
||||
this.elementDepth++;
|
||||
}
|
||||
else if (event.isEndElement()) {
|
||||
this.elementDepth--;
|
||||
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void raiseLimitException() {
|
||||
throw new DataBufferLimitException(
|
||||
"Exceeded limit on max bytes per XML top-level node: " + this.maxInMemorySize);
|
||||
}
|
||||
|
||||
public void endOfInput() {
|
||||
this.streamReader.getInputFeeder().endOfInput();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Callers of {@link XmlEventDecoder} that buffer emitted XML events at a
|
||||
* higher level, can pass an instance of this tracker as an
|
||||
* {@link XmlEventDecoder#BYTE_TRACKER_HINT} to monitor the total number of
|
||||
* bytes received, and to reset periodically.
|
||||
* <p>For use with Aalto XML async parsing only, in which case this decoder
|
||||
* parses releases each buffer immediately.
|
||||
*/
|
||||
public static class ReceivedByteTracker {
|
||||
|
||||
/** An instance to use when there is no limit. */
|
||||
public static final ReceivedByteTracker NO_OP = new ReceivedByteTracker(-1);
|
||||
|
||||
private final int maxInMemorySize;
|
||||
|
||||
private int byteCount;
|
||||
|
||||
public ReceivedByteTracker(int maxInMemorySize) {
|
||||
this.maxInMemorySize = maxInMemorySize;
|
||||
}
|
||||
|
||||
public int getMaxInMemorySize() {
|
||||
return this.maxInMemorySize;
|
||||
}
|
||||
|
||||
public boolean isMaxInMemorySizeExceeded() {
|
||||
return (this.maxInMemorySize != -1 && this.byteCount > this.maxInMemorySize);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.byteCount = 0;
|
||||
}
|
||||
|
||||
private void incrementByteCount(DataBuffer buffer) {
|
||||
if (this.maxInMemorySize != -1) {
|
||||
this.byteCount += buffer.readableByteCount();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.byteCount + " bytes";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-3
@@ -212,9 +212,8 @@ class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SameSite getSameSite() {
|
||||
// Adding non-null return site breaks tests.
|
||||
return null;
|
||||
public SameSite getSameSite() {
|
||||
return SameSite.from(this.responseCookie.getSameSite());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -170,12 +170,12 @@ public class EscapedErrors implements Errors {
|
||||
|
||||
@Override
|
||||
public List<FieldError> getFieldErrors() {
|
||||
return this.source.getFieldErrors();
|
||||
return escapeObjectErrors(this.source.getFieldErrors());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable FieldError getFieldError() {
|
||||
return this.source.getFieldError();
|
||||
return escapeObjectError(this.source.getFieldError());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -308,6 +308,9 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
|
||||
throws IOException {
|
||||
|
||||
String location = trimTrailingSlash(request.getRequestURI());
|
||||
if (location.length() > 2 && location.startsWith("//")) {
|
||||
location = (location.charAt(2) != '/' ? location.substring(1) : location);
|
||||
}
|
||||
if (StringUtils.hasText(request.getQueryString())) {
|
||||
location += "?" + request.getQueryString();
|
||||
}
|
||||
|
||||
+4
-2
@@ -299,12 +299,14 @@ public final class UrlHandlerFilter implements WebFilter {
|
||||
@Override
|
||||
public Mono<Void> handleInternal(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String query = request.getURI().getRawQuery();
|
||||
String location = trimTrailingSlash(request);
|
||||
if (location.length() > 2 && location.startsWith("//")) {
|
||||
location = (location.charAt(2) != '/' ? location.substring(1) : location);
|
||||
}
|
||||
String query = request.getURI().getRawQuery();
|
||||
if (StringUtils.hasText(query)) {
|
||||
location += "?" + query;
|
||||
}
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.setStatusCode(this.statusCode);
|
||||
response.getHeaders().set(HttpHeaders.LOCATION, location);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.util;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility methods for writing content as
|
||||
* <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html">Server-Sent Events</a>,
|
||||
* shared by the Servlet and Reactive SSE support.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 7.0.9
|
||||
*/
|
||||
public abstract class SseUtils {
|
||||
|
||||
/**
|
||||
* Append {@code value} to {@code output}, replacing each line separator
|
||||
* ({@code "\n"}, {@code "\r"}, or {@code "\r\n"}) it contains with a new
|
||||
* {@code field} line (that is, {@code "\n" + field + ":"}). This keeps a
|
||||
* multi-line field value from breaking out of the current SSE field when
|
||||
* written on the wire.
|
||||
* @param field the name of the SSE field that {@code value} belongs to
|
||||
* (for example, {@code "data"}), or an empty string for a comment
|
||||
* @param value the field value to escape and append
|
||||
* @param output the {@code StringBuilder} to append the escaped value to
|
||||
*/
|
||||
public static void appendFieldValue(String field, String value, StringBuilder output) {
|
||||
if (value.indexOf('\n') == -1 && value.indexOf('\r') == -1) {
|
||||
output.append(value);
|
||||
return;
|
||||
}
|
||||
String lineSeparatorReplacement = "\n" + field + ":";
|
||||
int length = value.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '\r') {
|
||||
if (i + 1 < length && value.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
output.append(lineSeparatorReplacement);
|
||||
}
|
||||
else if (c == '\n') {
|
||||
output.append(lineSeparatorReplacement);
|
||||
}
|
||||
else {
|
||||
output.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given single-line SSE field value, such as an
|
||||
* {@code id} or {@code event} name, does not contain a line separator.
|
||||
* @param content the field value to check
|
||||
* @throws IllegalArgumentException if {@code content} contains {@code "\n"} or {@code "\r"}
|
||||
*/
|
||||
public static void assertNoLineSeparator(String content) {
|
||||
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
|
||||
"illegal character '\\n' or '\\r' in event content");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -181,6 +181,15 @@ class ContentDispositionTests {
|
||||
assertThat(cd.getFilename()).isEqualTo("foo\\bar \"baz\" qux \\\" quux.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBackslashInName() {
|
||||
String s = "form-data; name=\"foo\\\"bar\"; filename=\"foo.txt\"";
|
||||
ContentDisposition cd = ContentDisposition.parse(s);
|
||||
assertThat(cd.getName()).isEqualTo("foo\"bar");
|
||||
assertThat(cd.getFilename()).isEqualTo("foo.txt");
|
||||
assertThat(cd.toString()).isEqualTo(s);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBackslashInLastPosition() {
|
||||
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"bar\\\"");
|
||||
|
||||
+15
@@ -255,6 +255,21 @@ class PartEventHttpMessageReaderTests extends AbstractLeakCheckingTests {
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void partSizeTooLargeWithUnlimitedMemorySize() {
|
||||
MockServerHttpRequest request = createRequest("simple.multipart", "simple-boundary");
|
||||
|
||||
PartEventHttpMessageReader reader = new PartEventHttpMessageReader();
|
||||
reader.setMaxPartSize(10);
|
||||
reader.setMaxInMemorySize(-1);
|
||||
|
||||
Flux<PartEvent> result = reader.read(forClass(PartEvent.class), request, emptyMap());
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectError(DataBufferLimitException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void formPartTooLarge() {
|
||||
MockServerHttpRequest request = createRequest(
|
||||
|
||||
+40
-6
@@ -35,7 +35,9 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.DecodingException;
|
||||
import org.springframework.core.codec.Hints;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MimeType;
|
||||
@@ -92,7 +94,7 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
|
||||
@Test
|
||||
void splitOneBranches() {
|
||||
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_ROOT), null, null, HINTS);
|
||||
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
|
||||
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(events -> {
|
||||
@@ -113,8 +115,7 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
|
||||
@Test
|
||||
void splitMultipleBranches() {
|
||||
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_CHILD), null, null, HINTS);
|
||||
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
|
||||
|
||||
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(events -> {
|
||||
@@ -143,6 +144,34 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitMultipleBranchesLimitExceeded() {
|
||||
|
||||
Flux<String> source = Flux.just(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
||||
"<root><pojo><foo>foo</foo></pojo>",
|
||||
"<pojo><foo>foofoo</foo>",
|
||||
"<bar>barbar</bar></pojo>",
|
||||
"<root/>");
|
||||
|
||||
XmlEventDecoder.ReceivedByteTracker byteTracker = new XmlEventDecoder.ReceivedByteTracker(30);
|
||||
Map<String, Object> hints = Hints.from(XmlEventDecoder.BYTE_TRACKER_HINT, byteTracker);
|
||||
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(source.map(this::toToDataBuffer), null, null, hints);
|
||||
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), byteTracker);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(events -> {
|
||||
assertThat(events).hasSize(5);
|
||||
assertStartElement(events.get(0), "pojo");
|
||||
assertStartElement(events.get(1), "foo");
|
||||
assertCharacters(events.get(2), "foo");
|
||||
assertEndElement(events.get(3), "foo");
|
||||
assertEndElement(events.get(4), "pojo");
|
||||
})
|
||||
.expectError(DataBufferLimitException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
private static void assertStartElement(XMLEvent event, String expectedLocalName) {
|
||||
assertThat(event.isStartElement()).isTrue();
|
||||
assertThat(event.asStartElement().getName().getLocalPart()).isEqualTo(expectedLocalName);
|
||||
@@ -263,13 +292,18 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
|
||||
|
||||
private Mono<DataBuffer> toDataBufferMono(String value) {
|
||||
return Mono.defer(() -> {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
|
||||
buffer.write(bytes);
|
||||
DataBuffer buffer = toToDataBuffer(value);
|
||||
return Mono.just(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
private DataBuffer toToDataBuffer(String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
|
||||
buffer.write(bytes);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@jakarta.xml.bind.annotation.XmlType
|
||||
@XmlSeeAlso(Child.class)
|
||||
public abstract static class Parent {
|
||||
|
||||
@@ -27,7 +27,6 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -88,28 +87,6 @@ class XmlEventDecoderTests extends AbstractLeakCheckingTests {
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void toXMLEventsWithLimit() {
|
||||
|
||||
this.decoder.setMaxInMemorySize(6);
|
||||
|
||||
Flux<String> source = Flux.just(
|
||||
"<pojo>", "<foo>", "foofoo", "</foo>", "<bar>", "barbarbar", "</bar>", "</pojo>");
|
||||
|
||||
Flux<XMLEvent> events = this.decoder.decode(
|
||||
source.map(this::stringBuffer), null, null, Collections.emptyMap());
|
||||
|
||||
StepVerifier.create(events)
|
||||
.consumeNextWith(e -> assertThat(e.isStartDocument()).isTrue())
|
||||
.consumeNextWith(e -> assertStartElement(e, "pojo"))
|
||||
.consumeNextWith(e -> assertStartElement(e, "foo"))
|
||||
.consumeNextWith(e -> assertCharacters(e, "foofoo"))
|
||||
.consumeNextWith(e -> assertEndElement(e, "foo"))
|
||||
.consumeNextWith(e -> assertStartElement(e, "bar"))
|
||||
.expectError(DataBufferLimitException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void decodeErrorAalto() {
|
||||
Flux<DataBuffer> source = Flux.concat(
|
||||
|
||||
@@ -94,4 +94,25 @@ class EscapedErrorsTests {
|
||||
assertThat(ageError2.getCode()).as("Age error 2 code not escaped").isEqualTo("AGE_NOT_32 <tag>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noArgFieldErrorAccessorsEscapeRejectedValueAndMessage() {
|
||||
TestBean tb = new TestBean();
|
||||
tb.setName("<script>alert(1)</script>");
|
||||
|
||||
Errors errors = new EscapedErrors(new BindException(tb, "tb"));
|
||||
errors.rejectValue("name", "NAME_INVALID", null, "message: <tag>");
|
||||
|
||||
FieldError fieldError = errors.getFieldError();
|
||||
assertThat(fieldError.getDefaultMessage()).as("No-arg getFieldError() message escaped")
|
||||
.isEqualTo("message: <tag>");
|
||||
assertThat(fieldError.getRejectedValue()).as("No-arg getFieldError() rejected value escaped")
|
||||
.isEqualTo("<script>alert(1)</script>");
|
||||
|
||||
FieldError fieldErrorInList = errors.getFieldErrors().get(0);
|
||||
assertThat(fieldErrorInList.getDefaultMessage()).as("No-arg getFieldErrors() message escaped")
|
||||
.isEqualTo("message: <tag>");
|
||||
assertThat(fieldErrorInList.getRejectedValue()).as("No-arg getFieldErrors() rejected value escaped")
|
||||
.isEqualTo("<script>alert(1)</script>");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -74,11 +74,16 @@ class UrlHandlerFilterTests {
|
||||
|
||||
@Test
|
||||
void redirect() throws Exception {
|
||||
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").redirect(status).build();
|
||||
testRedirect("/**", "/path/123/", "/path/123");
|
||||
testRedirect("/**", "//path/123/", "/path/123");
|
||||
testRedirect("/**", "///path/123/", "///path/123");
|
||||
}
|
||||
|
||||
String path = "/path/123";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", path + "/");
|
||||
private void testRedirect(String pattern, String path, String location) throws Exception {
|
||||
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler(pattern).redirect(status).build();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", path);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain chain = new MockFilterChain();
|
||||
|
||||
@@ -89,7 +94,7 @@ class UrlHandlerFilterTests {
|
||||
|
||||
assertThat(chain.getRequest()).isNull();
|
||||
assertThat(response.getStatus()).isEqualTo(status.value());
|
||||
assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(path + "?" + queryString);
|
||||
assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(location + "?" + queryString);
|
||||
assertThat(response.isCommitted()).isTrue();
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -17,12 +17,14 @@
|
||||
package org.springframework.web.filter.reactive;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -59,20 +61,23 @@ class UrlHandlerFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void redirect() {
|
||||
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").redirect(status).build();
|
||||
void redirect() throws URISyntaxException {
|
||||
testRedirect("/**", new URI(null, null, "/path/123/", "foo=bar", null), "/path/123?foo=bar");
|
||||
// no way to create java.net.URI with leading slashes
|
||||
}
|
||||
|
||||
String path = "/path/123";
|
||||
String queryString = "foo=bar";
|
||||
MockServerHttpRequest original = MockServerHttpRequest.get(path + "/?" + queryString).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(original);
|
||||
private static void testRedirect(String pattern, URI uri, String location) {
|
||||
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
|
||||
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler(pattern).redirect(status).build();
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, uri).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
assertThatThrownBy(() -> invokeFilter(filter, exchange))
|
||||
.hasMessageContaining("No argument value was captured");
|
||||
|
||||
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(status);
|
||||
assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create(path + "?" + queryString));
|
||||
assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create(location));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.util;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SseUtils}.
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class SseUtilsTests {
|
||||
|
||||
@Test
|
||||
void appendFieldValueWithoutLineSeparatorAppendsAsIs() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("data:");
|
||||
SseUtils.appendFieldValue("data", "no newlines here", sb);
|
||||
assertThat(sb).hasToString("data:no newlines here");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
@MethodSource("newLineCharacters")
|
||||
void appendFieldValueReplacesLineSeparatorWithFieldPrefix(String newLine, String description) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("data:");
|
||||
SseUtils.appendFieldValue("data", "first" + newLine + "second", sb);
|
||||
assertThat(sb).hasToString("data:first\ndata:second");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
@MethodSource("newLineCharacters")
|
||||
void appendFieldValueUsesEmptyFieldForComments(String newLine, String description) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(":");
|
||||
SseUtils.appendFieldValue("", "first" + newLine + "second", sb);
|
||||
assertThat(sb).hasToString(":first\n:second");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertNoLineSeparatorAcceptsPlainContent() {
|
||||
SseUtils.assertNoLineSeparator("no newlines here");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
@MethodSource("newLineCharacters")
|
||||
void assertNoLineSeparatorRejectsLineSeparator(String newLine, String description) {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
SseUtils.assertNoLineSeparator("first" + newLine + "second"));
|
||||
}
|
||||
|
||||
static Stream<Arguments> newLineCharacters() {
|
||||
return Stream.of(
|
||||
Arguments.of("\n", "LF"),
|
||||
Arguments.of("\r", "CR"),
|
||||
Arguments.of("\r\n", "CRLF")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user