io.inverno.mod.http.server.internal.multipart.MultipartFormDataBodyDecoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of inverno-http-server Show documentation
Show all versions of inverno-http-server Show documentation
Inverno HTTP 1.x/2 server module
/*
* Copyright 2020 Jeremy KUHN
*
* 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
*
* http://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 io.inverno.mod.http.server.internal.multipart;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpConstants;
import io.inverno.core.annotation.Bean;
import io.inverno.core.annotation.Bean.Visibility;
import io.inverno.mod.base.converter.ObjectConverter;
import io.inverno.mod.base.resource.MediaTypes;
import io.inverno.mod.http.base.header.Header;
import io.inverno.mod.http.base.header.HeaderService;
import io.inverno.mod.http.base.header.Headers;
import io.inverno.mod.http.base.internal.header.ContentTypeCodec;
import io.inverno.mod.http.server.Part;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.SignalType;
/**
*
* A multipart/form-data payload decoder implementation as defined by RFC 7578.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
/*
* Usefull:
* - https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
*/
@Bean(visibility = Visibility.PRIVATE)
public class MultipartFormDataBodyDecoder implements MultipartDecoder {
private final HeaderService headerService;
private final ObjectConverter parameterConverter;
/**
*
* Creates a multipart/form-data payload decoder.
*
*
* @param headerService the header service
* @param parameterConverter a string object converter
*/
public MultipartFormDataBodyDecoder(HeaderService headerService, ObjectConverter parameterConverter) {
this.headerService = headerService;
this.parameterConverter = parameterConverter;
}
@Override
public Flux decode(Flux data, Headers.ContentType contentType) {
if(contentType == null || !contentType.getMediaType().equalsIgnoreCase(MediaTypes.MULTIPART_FORM_DATA)) {
throw new IllegalArgumentException("Content type is not " + MediaTypes.MULTIPART_FORM_DATA);
}
if(contentType.getBoundary() == null) {
throw new IllegalArgumentException("Missing multipart form data boundary");
}
// Why parts flux is sequential: we have to emit all parts before we can send response back to the client?
// => request data flux emits chunk, the same thread is used for
// subscription as a result no parts can be emitted before the chunk is fully
// processed, this explain why we received batch of parts: they all result from
// one single request data chunk
return Flux.create(emitter -> {
data.subscribe(new BodyDataSubscriber(contentType, emitter));
});
}
private DecoderTask boundary(ByteBuf buffer, BodyDataSubscriber context) throws MalformedBodyException {
String delimiter = context.getDelimiter();
int readerIndex = buffer.readerIndex();
if(!this.skipControlCharacters(buffer)) {
buffer.readerIndex(readerIndex);
}
this.skipOneLine(buffer);
readerIndex = buffer.readerIndex();
int delimiterPos = 0;
int len = delimiter.length();
while(buffer.isReadable() && delimiterPos < len) {
byte nextByte = buffer.readByte();
if(nextByte == delimiter.charAt(delimiterPos)) {
delimiterPos++;
}
else {
// We were expecting a valid delimiter but we didn't find it
throw new MalformedBodyException("No delimiter found");
}
}
if(delimiterPos < len - 1) {
// Not enough data
buffer.readerIndex(readerIndex);
return null;
}
// determine if we have an opening or closing delimiter
if(buffer.isReadable()) {
// \r\n | \n => opening
// -- => closing
byte nextByte = buffer.readByte();
if(nextByte == HttpConstants.CR) {
if(buffer.isReadable()) {
nextByte = buffer.readByte();
if(nextByte == HttpConstants.LF) {
// opening boundary
return this::headers;
}
else {
// We were expecting a valid delimiter but we didn't find it
throw new MalformedBodyException("No delimiter found");
}
}
}
else if(nextByte == HttpConstants.LF) {
// opening boundary
return this::headers;
}
else if(nextByte == '-') {
if(buffer.isReadable()) {
nextByte = buffer.readByte();
if(nextByte == '-') {
// TODO if we want to use this for multipart/mixed then we must also look for \r\n
// closing boundary
return this::end;
}
}
}
}
// Not enough data
buffer.readerIndex(readerIndex);
return null;
}
private DecoderTask headers(ByteBuf buffer, BodyDataSubscriber context) throws MalformedBodyException {
while(!this.skipOneLine(buffer)) {
Header headerField = this.headerService.decode(buffer, context.contentType.getCharset());
if(headerField != null) {
context.addDecodedHeader(headerField);
}
else {
return null;
}
}
// We have all headers, we can create the part
Headers.ContentDisposition partContentDispositionHeader = context.getDecodedHeader(Headers.NAME_CONTENT_DISPOSITION);
if(partContentDispositionHeader == null || partContentDispositionHeader.getPartName() == null) {
throw new MalformedBodyException("Missing content disposition");
}
Headers.ContentType partContentTypeHeader = context.getDecodedHeader(Headers.NAME_CONTENT_TYPE);
if(partContentTypeHeader != null && partContentTypeHeader.getMediaType().equalsIgnoreCase(MediaTypes.MULTIPART_MIXED)) {
context.startMultipartMixed(partContentTypeHeader);
return this::boundary;
}
else {
String partName = partContentDispositionHeader.getPartName();
String partFilename = partContentDispositionHeader.getFilename();
if(context.isMultipartMixed() && partFilename == null) {
throw new MalformedBodyException("Field not supported in mixed multipart");
}
if(partContentTypeHeader == null && partFilename != null) {
context.addDecodedHeader(new ContentTypeCodec.ContentType(MediaTypes.APPLICATION_OCTET_STREAM, context.contentType.getCharset(), null, null));
}
context.startPart(new GenericPart(this.parameterConverter, partName, partFilename, context.getAllDecodedHeaders()));
return this::data;
}
}
private DecoderTask data(ByteBuf buffer, BodyDataSubscriber context) {
String delimiter = context.getDelimiter();
int readerIndex = buffer.readerIndex();
int delimiterLength = delimiter.length();
Integer delimiterIndex = null;
Integer delimiterReaderIndex = null;
while(buffer.isReadable()) {
byte nextByte = buffer.readByte();
if(nextByte == HttpConstants.CR) {
if(!buffer.isReadable()) {
delimiterIndex = 0;
delimiterReaderIndex = buffer.readerIndex() - 1;
}
else if(buffer.getByte(buffer.readerIndex()) == HttpConstants.LF) {
buffer.readByte();
delimiterIndex = 0;
delimiterReaderIndex = buffer.readerIndex() - 2;
}
}
else if(nextByte == HttpConstants.LF) {
delimiterIndex = 0;
delimiterReaderIndex = buffer.readerIndex() - 1;
}
else if(delimiterIndex != null) {
if(nextByte == delimiter.codePointAt(delimiterIndex)) {
delimiterIndex++;
if(delimiterIndex == delimiterLength) {
// We found the delimiter
if(context.getPart().getData().isPresent()) {
FluxSink partDataEmitter = context.getPart().getData().get();
if(readerIndex < delimiterReaderIndex) {
partDataEmitter.next(buffer.retainedSlice(readerIndex, delimiterReaderIndex - readerIndex));
}
}
return this::end;
}
}
else {
delimiterIndex = delimiterReaderIndex = null;
}
}
}
int dataLength = (delimiterReaderIndex != null ? delimiterReaderIndex : buffer.readerIndex()) - readerIndex;
if(dataLength > 0 && context.getPart().getData().isPresent()) {
FluxSink partDataEmitter = context.getPart().getData().get();
partDataEmitter.next(buffer.retainedSlice(readerIndex, dataLength));
}
buffer.readerIndex(readerIndex + dataLength);
return null;
}
private DecoderTask end(ByteBuf buffer, BodyDataSubscriber context) {
context.endPart();
int readerIndex = buffer.readerIndex();
if(buffer.readableBytes() >= 2) {
if(buffer.readByte() == '-' && buffer.readByte() == '-') {
if(context.isMultipartMixed()) {
context.endMultipartMixed();
return this::boundary;
}
else {
context.complete();
return null;
}
}
else {
buffer.readerIndex(readerIndex);
this.skipOneLine(buffer);
return this::headers;
}
}
else {
return null;
}
}
private boolean skipControlCharacters(ByteBuf buffer) {
try {
for (;;) {
char c = (char) buffer.readUnsignedByte();
if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
buffer.readerIndex(buffer.readerIndex() - 1);
break;
}
}
}
catch (IndexOutOfBoundsException e1) {
return false;
}
return true;
}
private boolean skipOneLine(ByteBuf buffer) {
if (!buffer.isReadable()) {
return false;
}
byte nextByte = buffer.readByte();
if (nextByte == HttpConstants.CR) {
if (!buffer.isReadable()) {
buffer.readerIndex(buffer.readerIndex() - 1);
return false;
}
nextByte = buffer.readByte();
if (nextByte == HttpConstants.LF) {
return true;
}
buffer.readerIndex(buffer.readerIndex() - 2);
return false;
}
if (nextByte == HttpConstants.LF) {
return true;
}
buffer.readerIndex(buffer.readerIndex() - 1);
return false;
}
@FunctionalInterface
private static interface DecoderTask {
DecoderTask run(ByteBuf buffer, BodyDataSubscriber context) throws MalformedBodyException;
}
/**
*
* Request data publisher subscriber.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
private class BodyDataSubscriber extends BaseSubscriber {
private final Headers.ContentType contentType;
private final FluxSink emitter;
private ByteBuf keepBuffer;
private final String delimiter;
private String mixedDelimiter;
private DecoderTask task;
private GenericPart part;
private Map> decodedHeaders;
public BodyDataSubscriber(Headers.ContentType contentType, FluxSink emitter) {
this.contentType = contentType;
this.delimiter = "--" + contentType.getBoundary();
this.task = MultipartFormDataBodyDecoder.this::boundary;
this.emitter = emitter;
this.emitter.onCancel(() -> this.cancel());
}
public String getDelimiter() {
return this.mixedDelimiter != null ? this.mixedDelimiter : this.delimiter;
}
public void startMultipartMixed(Headers.ContentType partContentType) throws MalformedBodyException {
if(this.mixedDelimiter != null) {
throw new MalformedBodyException("Nested multipart mixed not allowed");
}
if(partContentType.getBoundary() == null) {
throw new MalformedBodyException("Missing multipart mixed boundary");
}
this.mixedDelimiter = "--" + partContentType.getBoundary();
this.part = null;
this.decodedHeaders = null;
}
public boolean isMultipartMixed() {
return this.mixedDelimiter != null;
}
public void endMultipartMixed() {
this.mixedDelimiter = null;
this.part = null;
this.decodedHeaders = null;
}
public void startPart(GenericPart part) {
this.part = part;
this.emitter.next(this.part);
}
public GenericPart getPart() {
return this.part;
}
public void endPart() {
if(this.part != null) {
this.part.getData().ifPresent(emitter -> emitter.complete());
this.part = null;
this.decodedHeaders = null;
}
}
public void complete() {
this.emitter.complete();
this.cancel();
}
public void addDecodedHeader(Header decodedHeaderField) {
if(this.decodedHeaders == null) {
this.decodedHeaders = new LinkedHashMap<>();
}
List headerFieldList = this.decodedHeaders.get(decodedHeaderField.getHeaderName());
if(headerFieldList == null) {
headerFieldList = new ArrayList<>();
this.decodedHeaders.put(decodedHeaderField.getHeaderName(), headerFieldList);
}
headerFieldList.add(decodedHeaderField);
}
public Map> getAllDecodedHeaders() {
return this.decodedHeaders;
}
@SuppressWarnings("unchecked")
public List getDecodedHeaders(String name) {
return (List)this.decodedHeaders.get(name);
}
public T getDecodedHeader(String name) {
List headers = this.getDecodedHeaders(name);
if(headers == null || headers.isEmpty()) {
return null;
}
if(headers.size() > 1) {
throw new IllegalStateException("Invalid request");
}
return headers.get(0);
}
@Override
protected void hookOnNext(ByteBuf value) {
final ByteBuf buffer;
try {
if(this.keepBuffer != null && this.keepBuffer.isReadable()) {
buffer = Unpooled.wrappedBuffer(this.keepBuffer, value);
}
else {
buffer = value;
}
}
catch(Exception e) {
this.emitter.error(e);
this.cancel();
value.release();
return;
}
try {
DecoderTask currentTask = this.task;
while( (currentTask = currentTask.run(buffer, this)) != null) {
this.task = currentTask;
}
if(buffer.isReadable()) {
if(this.keepBuffer != null) {
this.keepBuffer.discardReadBytes();
this.keepBuffer.writeBytes(buffer);
}
else {
this.keepBuffer = Unpooled.unreleasableBuffer(Unpooled.buffer(buffer.readableBytes()));
this.keepBuffer.writeBytes(buffer);
}
}
else {
// keepBuffer is released when releasing the composite buffer in the finally block
this.keepBuffer = null;
}
}
catch (Throwable e) {
this.emitter.error(e);
this.cancel();
}
finally {
buffer.release();
}
}
@Override
protected void hookOnError(Throwable throwable) {
this.emitter.error(throwable);
}
@Override
protected void hookOnComplete() {
this.emitter.complete();
}
@Override
protected void hookFinally(SignalType type) {
if(this.keepBuffer != null) {
this.keepBuffer.release();
this.keepBuffer = null;
}
}
}
}