io.undertow.util.MultipartParser Maven / Gradle / Ivy
Go to download
This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including
all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and
JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up
with different versions on classes on the class path).
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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.undertow.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import io.undertow.connector.ByteBufferPool;
import io.undertow.connector.PooledByteBuffer;
/**
* @author Stuart Douglas
*/
public class MultipartParser {
/**
* The Horizontal Tab ASCII character value;
*/
public static final byte HTAB = 0x09;
/**
* The Carriage Return ASCII character value.
*/
public static final byte CR = 0x0D;
/**
* The Line Feed ASCII character value.
*/
public static final byte LF = 0x0A;
/**
* The Space ASCII character value;
*/
public static final byte SP = 0x20;
/**
* The dash (-) ASCII character value.
*/
public static final byte DASH = 0x2D;
/**
* A byte sequence that precedes a boundary (CRLF--
).
*/
private static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH};
public interface PartHandler {
void beginPart(final HeaderMap headers);
void data(final ByteBuffer buffer) throws IOException;
void endPart();
}
public static ParseState beginParse(final ByteBufferPool bufferPool, final PartHandler handler, final byte[] boundary, final String requestCharset) {
// We prepend CR/LF to the boundary to chop trailing CR/LF from
// body-data tokens.
byte[] boundaryToken = new byte[boundary.length + BOUNDARY_PREFIX.length];
System.arraycopy(BOUNDARY_PREFIX, 0, boundaryToken, 0, BOUNDARY_PREFIX.length);
System.arraycopy(boundary, 0, boundaryToken, BOUNDARY_PREFIX.length, boundary.length);
return new ParseState(bufferPool, handler, requestCharset, boundaryToken);
}
public static class ParseState {
private final ByteBufferPool bufferPool;
private final PartHandler partHandler;
private String requestCharset;
/**
* The boundary, complete with the initial CRLF--
*/
private final byte[] boundary;
//0=preamble
private int state = 0;
private int subState = Integer.MAX_VALUE; // used for preamble parsing
private ByteArrayOutputStream currentString = null;
private String currentHeaderName = null;
private HeaderMap headers;
private Encoding encodingHandler;
public ParseState(final ByteBufferPool bufferPool, final PartHandler partHandler, String requestCharset, final byte[] boundary) {
this.bufferPool = bufferPool;
this.partHandler = partHandler;
this.requestCharset = requestCharset;
this.boundary = boundary;
}
public void setCharacterEncoding(String encoding) {
requestCharset = encoding;
}
public void parse(ByteBuffer buffer) throws IOException {
while (buffer.hasRemaining()) {
switch (state) {
case 0: {
preamble(buffer);
break;
}
case 1: {
headerName(buffer);
break;
}
case 2: {
headerValue(buffer);
break;
}
case 3: {
entity(buffer);
break;
}
case -1: {
return;
}
default: {
throw new IllegalStateException("" + state);
}
}
}
}
private void preamble(final ByteBuffer buffer) {
while (buffer.hasRemaining()) {
final byte b = buffer.get();
if (subState >= 0) {
//handle the case of no preamble. In this case there is no CRLF
if (subState == Integer.MAX_VALUE) {
if (boundary[2] == b) {
subState = 2;
} else {
subState = 0;
}
}
if (b == boundary[subState]) {
subState++;
if (subState == boundary.length) {
subState = -1;
}
} else if (b == boundary[0]) {
subState = 1;
} else {
subState = 0;
}
} else if (subState == -1) {
if (b == CR) {
subState = -2;
}
} else if (subState == -2) {
if (b == LF) {
subState = 0;
state = 1;//preamble is done
headers = new HeaderMap();
return;
} else {
subState = -1;
}
}
}
}
private void headerName(final ByteBuffer buffer) throws MalformedMessageException, UnsupportedEncodingException {
while (buffer.hasRemaining()) {
final byte b = buffer.get();
if (b == ':') {
if (currentString == null || subState != 0) {
throw new MalformedMessageException();
} else {
currentHeaderName = new String(currentString.toByteArray(), requestCharset);
currentString.reset();
subState = 0;
state = 2;
return;
}
} else if (b == CR) {
if (currentString != null) {
throw new MalformedMessageException();
} else {
subState = 1;
}
} else if (b == LF) {
if (currentString != null || subState != 1) {
throw new MalformedMessageException();
}
state = 3;
subState = 0;
partHandler.beginPart(headers);
//select the appropriate encoding
String encoding = headers.getFirst(Headers.CONTENT_TRANSFER_ENCODING);
if (encoding == null) {
encodingHandler = new IdentityEncoding();
} else if (encoding.equalsIgnoreCase("base64")) {
encodingHandler = new Base64Encoding(bufferPool);
} else if (encoding.equalsIgnoreCase("quoted-printable")) {
encodingHandler = new QuotedPrintableEncoding(bufferPool);
} else {
encodingHandler = new IdentityEncoding();
}
headers = null;
return;
} else {
if (subState != 0) {
throw new MalformedMessageException();
} else if (currentString == null) {
currentString = new ByteArrayOutputStream();
}
currentString.write(b);
}
}
}
private void headerValue(final ByteBuffer buffer) throws MalformedMessageException, UnsupportedEncodingException {
while (buffer.hasRemaining()) {
final byte b = buffer.get();
if(subState == 2) {
if (b == CR) { //end of headers section
headers.put(new HttpString(currentHeaderName.trim()), new String(currentString.toByteArray(), requestCharset).trim());
//set state for headerName to verify end of headers section
state = 1;
subState = 1; //CR already encountered
currentString = null;
return;
} else if (b == SP || b == HTAB) { //multi-line header
currentString.write(b);
subState = 0;
} else { //next header name
headers.put(new HttpString(currentHeaderName.trim()), new String(currentString.toByteArray(), requestCharset).trim());
//set state for headerName to collect next header's name
state = 1;
subState = 0;
//start name collection for headerName to finish
currentString = new ByteArrayOutputStream();
currentString.write(b);
return;
}
} else if (b == CR) {
subState = 1;
} else if (b == LF) {
if (subState != 1) {
throw new MalformedMessageException();
}
subState = 2;
} else {
if (subState != 0) {
throw new MalformedMessageException();
}
currentString.write(b);
}
}
}
private void entity(final ByteBuffer buffer) throws IOException {
int startingSubState = subState;
int pos = buffer.position();
while (buffer.hasRemaining()) {
final byte b = buffer.get();
if (subState >= 0) {
if (b == boundary[subState]) {
//if we have a potential boundary match
subState++;
if (subState == boundary.length) {
startingSubState = 0;
//we have our data
ByteBuffer retBuffer = buffer.duplicate();
retBuffer.position(pos);
retBuffer.limit(Math.max(buffer.position() - boundary.length, 0));
encodingHandler.handle(partHandler, retBuffer);
partHandler.endPart();
subState = -1;
}
} else if (b == boundary[0]) {
//we started half way through a boundary, but it turns out we did not actually meet the boundary condition
//so we call the part handler with our copy of the boundary data
if (startingSubState > 0) {
encodingHandler.handle(partHandler, ByteBuffer.wrap(boundary, 0, startingSubState));
startingSubState = 0;
}
subState = 1;
} else {
//we started half way through a boundary, but it turns out we did not actually meet the boundary condition
//so we call the part handler with our copy of the boundary data
if (startingSubState > 0) {
encodingHandler.handle(partHandler, ByteBuffer.wrap(boundary, 0, startingSubState));
startingSubState = 0;
}
subState = 0;
}
} else if (subState == -1) {
if (b == CR) {
subState = -2;
} else if (b == DASH) {
subState = -3;
}
} else if (subState == -2) {
if (b == LF) {
//ok, we have our data
subState = 0;
state = 1;
headers = new HeaderMap();
return;
} else if (b == DASH) {
subState = -3;
} else {
subState = -1;
}
} else if (subState == -3) {
if (b == DASH) {
state = -1; //we are done
return;
} else {
subState = -1;
}
}
}
//handle the data we read so far
ByteBuffer retBuffer = buffer.duplicate();
retBuffer.position(pos);
if (subState == 0) {
//if we end partially through a boundary we do not handle the data
encodingHandler.handle(partHandler, retBuffer);
} else if (retBuffer.remaining() > subState && subState > 0) {
//we have some data to handle, and the end of the buffer might be a boundary match
retBuffer.limit(retBuffer.limit() - subState);
encodingHandler.handle(partHandler, retBuffer);
}
}
public boolean isComplete() {
return state == -1;
}
}
private interface Encoding {
void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException;
}
private static class IdentityEncoding implements Encoding {
@Override
public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException {
handler.data(rawData);
rawData.clear();
}
}
private static class Base64Encoding implements Encoding {
private final FlexBase64.Decoder decoder = FlexBase64.createDecoder();
private final ByteBufferPool bufferPool;
private Base64Encoding(final ByteBufferPool bufferPool) {
this.bufferPool = bufferPool;
}
@Override
public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException {
PooledByteBuffer resource = bufferPool.allocate();
ByteBuffer buf = resource.getBuffer();
try {
do {
buf.clear();
try {
decoder.decode(rawData, buf);
} catch (IOException e) {
throw new RuntimeException(e);
}
buf.flip();
handler.data(buf);
} while (rawData.hasRemaining());
} finally {
resource.close();
}
}
}
private static class QuotedPrintableEncoding implements Encoding {
private final ByteBufferPool bufferPool;
boolean equalsSeen;
byte firstCharacter;
private QuotedPrintableEncoding(final ByteBufferPool bufferPool) {
this.bufferPool = bufferPool;
}
@Override
public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException {
boolean equalsSeen = this.equalsSeen;
byte firstCharacter = this.firstCharacter;
PooledByteBuffer resource = bufferPool.allocate();
ByteBuffer buf = resource.getBuffer();
try {
while (rawData.hasRemaining()) {
byte b = rawData.get();
if (equalsSeen) {
if (firstCharacter == 0) {
if (b == '\n' || b == '\r') {
//soft line break
//ignore
equalsSeen = false;
} else {
firstCharacter = b;
}
} else {
int result = Character.digit((char) firstCharacter, 16);
result <<= 4; //shift it 4 bytes and then add the next value to the end
result += Character.digit((char) b, 16);
buf.put((byte) result);
equalsSeen = false;
firstCharacter = 0;
}
} else if (b == '=') {
equalsSeen = true;
} else {
buf.put(b);
if (!buf.hasRemaining()) {
buf.flip();
handler.data(buf);
buf.clear();
}
}
}
buf.flip();
handler.data(buf);
} finally {
resource.close();
this.equalsSeen = equalsSeen;
this.firstCharacter = firstCharacter;
}
}
}
}