Configurable limits with Synchronoss

See gh-23884
This commit is contained in:
Brian Clozel
2019-10-22 22:13:44 +02:00
committed by Rossen Stoyanchev
parent 69bcdfc17f
commit ea6f51e9e3
3 changed files with 286 additions and 52 deletions

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2002-2019 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.http.codec.multipart;
/**
* @author Brian Clozel
*/
@SuppressWarnings("serial")
public class MultipartException extends RuntimeException {
public MultipartException(String message) {
super(message);
}
public MultipartException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -40,9 +40,11 @@ import org.synchronoss.cloud.nio.multipart.NioMultipartParser;
import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener;
import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory;
import org.synchronoss.cloud.nio.stream.storage.StreamStorage;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SignalType;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Hints;
@@ -69,6 +71,7 @@ import org.springframework.util.Assert;
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @author Brian Clozel
* @since 5.0
* @see <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
* @see MultipartHttpMessageReader
@@ -79,6 +82,12 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final PartBodyStreamStorageFactory streamStorageFactory = new DefaultPartBodyStreamStorageFactory();
private long maxPartCount = -1;
private long maxFilePartSize = -1;
private long maxPartSize = -1;
public SynchronossPartHttpMessageReader() {
this.bufferFactory = new DefaultDataBufferFactory();
}
@@ -87,6 +96,55 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
this.bufferFactory = bufferFactory;
}
/**
* Get the maximum number of parts allowed in a single multipart request.
* @since 5.1.11
*/
public long getMaxPartCount() {
return maxPartCount;
}
/**
* Configure the maximum number of parts allowed in a single multipart request.
* @since 5.1.11
*/
public void setMaxPartCount(long maxPartCount) {
this.maxPartCount = maxPartCount;
}
/**
* Get the maximum size of a file part.
* @since 5.1.11
*/
public long getMaxFilePartSize() {
return this.maxFilePartSize;
}
/**
* Configure the the maximum size of a file part.
* @since 5.1.11
*/
public void setMaxFilePartSize(long maxFilePartSize) {
this.maxFilePartSize = maxFilePartSize;
}
/**
* Get the maximum size of a part.
* @since 5.1.11
*/
public long getMaxPartSize() {
return this.maxPartSize;
}
/**
* Configure the maximum size of a part.
* For limits on file parts, use the dedicated {@link #setMaxFilePartSize(long)}.
* @since 5.1.11
*/
public void setMaxPartSize(long maxPartSize) {
this.maxPartSize = maxPartSize;
}
@Override
public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
@@ -101,7 +159,8 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
@Override
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory))
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory,
new MultipartSizeLimiter(getMaxPartCount(), getMaxFilePartSize(), getMaxPartSize())))
.doOnNext(part -> {
if (!Hints.isLoggingSuppressed(hints)) {
LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " +
@@ -119,11 +178,60 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
}
private static class MultipartSizeLimiter {
private final long maxPartCount;
private final long maxFilePartSize;
private final long maxPartSize;
private boolean currentIsFilePart;
private long currentPartCount;
private long currentPartSize;
public MultipartSizeLimiter(long maxPartCount, long maxFilePartSize, long maxPartSize) {
this.maxPartCount = maxPartCount;
this.maxFilePartSize = maxFilePartSize;
this.maxPartSize = maxPartSize;
}
public void startPart(boolean isFilePart) {
this.currentPartCount++;
this.currentIsFilePart = isFilePart;
if (this.maxPartCount != -1 && this.currentPartCount > this.maxPartCount) {
throw new IllegalStateException("Exceeded limit on maximum number of multipart parts");
}
}
public void endPart() {
this.currentPartSize = 0L;
this.currentIsFilePart = false;
}
public void checkCurrentPartSize(long addedBytes) {
this.currentPartSize += addedBytes;
if (this.currentIsFilePart && this.maxFilePartSize != -1 && this.currentPartSize > this.maxFilePartSize) {
throw new IllegalStateException("Exceeded limit on max size of multipart file : " + this.maxFilePartSize);
}
else if (!this.currentIsFilePart && this.maxPartSize != -1 && this.currentPartSize > this.maxPartSize) {
throw new IllegalStateException("Exceeded limit on max size of multipart part : " + this.maxPartSize);
}
}
}
/**
* Consume and feed input to the Synchronoss parser, then listen for parser
* output events and adapt to {@code Flux<Sink<Part>>}.
* Consume {@code DataBuffer} as a {@code BaseSubscriber} of the request body
* and feed it as input to the Synchronoss parser. Also listen for parser
* output events and adapt them to {@code Flux<Sink<Part>>} to emit parts
* for subscribers.
*/
private static class SynchronossPartGenerator implements Consumer<FluxSink<Part>> {
private static class SynchronossPartGenerator extends BaseSubscriber<DataBuffer>
implements Consumer<FluxSink<Part>> {
private final ReactiveHttpInputMessage inputMessage;
@@ -131,12 +239,19 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final PartBodyStreamStorageFactory streamStorageFactory;
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory,
PartBodyStreamStorageFactory streamStorageFactory) {
private final MultipartSizeLimiter limiter;
private NioMultipartParserListener listener;
private NioMultipartParser parser;
public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory,
PartBodyStreamStorageFactory streamStorageFactory, MultipartSizeLimiter limiter) {
this.inputMessage = inputMessage;
this.bufferFactory = bufferFactory;
this.streamStorageFactory = streamStorageFactory;
this.streamStorageFactory = new PartBodyStreamStorageFactoryDecorator(streamStorageFactory, limiter);
this.limiter = limiter;
}
@Override
@@ -149,40 +264,53 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
Charset charset = Optional.ofNullable(mediaType.getCharset()).orElse(StandardCharsets.UTF_8);
MultipartContext context = new MultipartContext(mediaType.toString(), length, charset.name());
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter, this.bufferFactory, context);
NioMultipartParser parser = Multipart
this.listener = new FluxSinkAdapterListener(emitter, this.bufferFactory, context, this.limiter);
this.parser = Multipart
.multipart(context)
.usePartBodyStreamStorageFactory(this.streamStorageFactory)
.forNIO(listener);
// long to int downcast vs. keeping the default 16Kb value
//.withHeadersSizeLimit(this.limiter.maxPartSize)
.forNIO(this.listener);
this.inputMessage.getBody().subscribe(this);
}
this.inputMessage.getBody().subscribe(buffer -> {
byte[] resultBytes = new byte[buffer.readableByteCount()];
buffer.read(resultBytes);
try {
parser.write(resultBytes);
}
catch (IOException ex) {
listener.onError("Exception thrown while providing input to the parser", ex);
}
finally {
DataBufferUtils.release(buffer);
}
}, ex -> {
try {
parser.close();
listener.onError("Request body input error", ex);
}
catch (IOException ex2) {
listener.onError("Exception thrown while closing the parser", ex2);
}
}, () -> {
try {
parser.close();
}
catch (IOException ex) {
listener.onError("Exception thrown while closing the parser", ex);
}
});
@Override
protected void hookOnNext(DataBuffer buffer) {
int readableByteCount = buffer.readableByteCount();
this.limiter.checkCurrentPartSize(readableByteCount);
byte[] resultBytes = new byte[readableByteCount];
buffer.read(resultBytes);
try {
parser.write(resultBytes);
}
catch (IOException ex) {
this.cancel();
listener.onError("Exception thrown while providing input to the parser", ex);
}
finally {
DataBufferUtils.release(buffer);
}
}
@Override
protected void hookOnError(Throwable throwable) {
this.cancel();
listener.onError("Could not parse multipart request", throwable);
}
@Override
protected void hookOnCancel() {
this.cancel();
}
@Override
protected void hookFinally(SignalType type) {
try {
parser.close();
}
catch (IOException ex) {
listener.onError("Exception thrown while closing the parser", ex);
}
}
private int getContentLength(HttpHeaders headers) {
@@ -192,6 +320,28 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
}
}
private static class PartBodyStreamStorageFactoryDecorator implements PartBodyStreamStorageFactory {
private final PartBodyStreamStorageFactory streamStorageFactory;
private final MultipartSizeLimiter limiter;
public PartBodyStreamStorageFactoryDecorator(PartBodyStreamStorageFactory streamStorageFactory,
MultipartSizeLimiter limiter) {
this.streamStorageFactory = streamStorageFactory;
this.limiter = limiter;
}
@Override
public StreamStorage newStreamStorageForPartBody(Map<String, List<String>> partHeaders, int partIndex) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(partHeaders);
String filename = MultipartUtils.getFileName(httpHeaders);
this.limiter.startPart(filename != null);
return streamStorageFactory.newStreamStorageForPartBody(partHeaders, partIndex);
}
}
/**
* Listen for parser output and adapt to {@code Flux<Sink<Part>>}.
@@ -204,12 +354,16 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
private final MultipartContext context;
private final MultipartSizeLimiter limiter;
private final AtomicInteger terminated = new AtomicInteger(0);
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory factory, MultipartContext context) {
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory factory,
MultipartContext context, MultipartSizeLimiter limiter) {
this.sink = sink;
this.bufferFactory = factory;
this.context = context;
this.limiter = limiter;
}
@Override
@@ -217,6 +371,7 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
this.sink.next(createPart(storage, httpHeaders));
this.limiter.endPart();
}
private Part createPart(StreamStorage storage, HttpHeaders httpHeaders) {
@@ -236,7 +391,7 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
@Override
public void onError(String message, Throwable cause) {
if (this.terminated.getAndIncrement() == 0) {
this.sink.error(new RuntimeException(message, cause));
this.sink.error(new MultipartException(message, cause));
}
}