/*
 * 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
/** * @author Stuart Douglas */
public class MultipartParser {
The Horizontal Tab ASCII character value;
/** * The Horizontal Tab ASCII character value; */
public static final byte HTAB = 0x09;
The Carriage Return ASCII character value.
/** * The Carriage Return ASCII character value. */
public static final byte CR = 0x0D;
The Line Feed ASCII character value.
/** * The Line Feed ASCII character value. */
public static final byte LF = 0x0A;
The Space ASCII character value;
/** * The Space ASCII character value; */
public static final byte SP = 0x20;
The dash (-) ASCII character value.
/** * The dash (-) ASCII character value. */
public static final byte DASH = 0x2D;
A byte sequence that precedes a boundary (CRLF--).
/** * A byte sequence that precedes a boundary (<code>CRLF--</code>). */
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--
/** * 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; } } } }