org.eclipse.jetty.http.MultiPartParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ehcache Show documentation
Show all versions of ehcache Show documentation
Ehcache is an open source, standards-based cache used to boost performance,
offload the database and simplify scalability. Ehcache is robust, proven and full-featured and
this has made it the most widely-used Java-based cache.
//
// ========================================================================
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import org.eclipse.jetty.http.HttpParser.RequestHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.SearchPattern;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/* ------------------------------------------------------------ */
/**
* A parser for MultiPart content type.
*
* @see https://tools.ietf.org/html/rfc2046#section-5.1
* @see https://tools.ietf.org/html/rfc2045
*/
public class MultiPartParser
{
public static final Logger LOG = Log.getLogger(MultiPartParser.class);
// States
public enum FieldState
{
FIELD,
IN_NAME,
AFTER_NAME,
VALUE,
IN_VALUE
}
// States
public enum State
{
PREAMBLE,
DELIMITER,
DELIMITER_PADDING,
DELIMITER_CLOSE,
BODY_PART,
FIRST_OCTETS,
OCTETS,
EPILOGUE,
END
}
private final static EnumSet __delimiterStates = EnumSet.of(State.DELIMITER, State.DELIMITER_CLOSE, State.DELIMITER_PADDING);
private final static int MAX_HEADER_LINE_LENGTH = 998;
private final boolean DEBUG = LOG.isDebugEnabled();
private final Handler _handler;
private final SearchPattern _delimiterSearch;
private String _fieldName;
private String _fieldValue;
private State _state = State.PREAMBLE;
private FieldState _fieldState = FieldState.FIELD;
private int _partialBoundary = 2; // No CRLF if no preamble
private boolean _cr;
private ByteBuffer _patternBuffer;
private final Utf8StringBuilder _string = new Utf8StringBuilder();
private int _length;
private int _totalHeaderLineLength = -1;
/* ------------------------------------------------------------------------------- */
public MultiPartParser(Handler handler, String boundary)
{
_handler = handler;
String delimiter = "\r\n--" + boundary;
_patternBuffer = ByteBuffer.wrap(delimiter.getBytes(StandardCharsets.US_ASCII));
_delimiterSearch = SearchPattern.compile(_patternBuffer.array());
}
public void reset()
{
_state = State.PREAMBLE;
_fieldState = FieldState.FIELD;
_partialBoundary = 2; // No CRLF if no preamble
}
/* ------------------------------------------------------------------------------- */
public Handler getHandler()
{
return _handler;
}
/* ------------------------------------------------------------------------------- */
public State getState()
{
return _state;
}
/* ------------------------------------------------------------------------------- */
public boolean isState(State state)
{
return _state == state;
}
/* ------------------------------------------------------------------------------- */
private static boolean hasNextByte(ByteBuffer buffer)
{
return BufferUtil.hasContent(buffer);
}
/* ------------------------------------------------------------------------------- */
private HttpTokens.Token next(ByteBuffer buffer)
{
byte ch = buffer.get();
HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch];
if (DEBUG)
LOG.debug("token={}",t);
switch(t.getType())
{
case CNTL:
throw new IllegalCharacterException(_state,t,buffer);
case LF:
_cr=false;
break;
case CR:
if (_cr)
throw new BadMessageException("Bad EOL");
_cr=true;
return null;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case HTAB:
case SPACE:
case OTEXT:
case COLON:
if (_cr)
throw new BadMessageException("Bad EOL");
break;
default:
break;
}
return t;
}
/* ------------------------------------------------------------------------------- */
private void setString(String s)
{
_string.reset();
_string.append(s);
_length = s.length();
}
/* ------------------------------------------------------------------------------- */
/*
* Mime Field strings are treated as UTF-8 as per https://tools.ietf.org/html/rfc7578#section-5.1
*/
private String takeString()
{
String s = _string.toString();
// trim trailing whitespace.
if (s.length() > _length)
s = s.substring(0, _length);
_string.reset();
_length = -1;
return s;
}
/* ------------------------------------------------------------------------------- */
/**
* Parse until next Event.
*
* @param buffer the buffer to parse
* @param last whether this buffer contains last bit of content
* @return True if an {@link RequestHandler} method was called and it returned true;
*/
public boolean parse(ByteBuffer buffer, boolean last)
{
boolean handle = false;
while (!handle && BufferUtil.hasContent(buffer))
{
switch (_state)
{
case PREAMBLE:
parsePreamble(buffer);
continue;
case DELIMITER:
case DELIMITER_PADDING:
case DELIMITER_CLOSE:
parseDelimiter(buffer);
continue;
case BODY_PART:
handle = parseMimePartHeaders(buffer);
break;
case FIRST_OCTETS:
case OCTETS:
handle = parseOctetContent(buffer);
break;
case EPILOGUE:
BufferUtil.clear(buffer);
break;
case END:
handle = true;
break;
default:
throw new IllegalStateException();
}
}
if (last && BufferUtil.isEmpty(buffer))
{
if (_state == State.EPILOGUE)
{
_state = State.END;
if (LOG.isDebugEnabled())
LOG.debug("messageComplete {}", this);
return _handler.messageComplete();
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("earlyEOF {}", this);
_handler.earlyEOF();
return true;
}
}
return handle;
}
/* ------------------------------------------------------------------------------- */
private void parsePreamble(ByteBuffer buffer)
{
if (_partialBoundary > 0)
{
int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
if (partial > 0)
{
if (partial == _delimiterSearch.getLength())
{
buffer.position(buffer.position() + partial - _partialBoundary);
_partialBoundary = 0;
setState(State.DELIMITER);
return;
}
_partialBoundary = partial;
BufferUtil.clear(buffer);
return;
}
_partialBoundary = 0;
}
int delimiter = _delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
if (delimiter >= 0)
{
buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
setState(State.DELIMITER);
return;
}
_partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
BufferUtil.clear(buffer);
}
/* ------------------------------------------------------------------------------- */
private void parseDelimiter(ByteBuffer buffer)
{
while (__delimiterStates.contains(_state) && hasNextByte(buffer))
{
HttpTokens.Token t = next(buffer);
if (t == null)
return;
if (t.getType()==HttpTokens.Type.LF)
{
setState(State.BODY_PART);
if (LOG.isDebugEnabled())
LOG.debug("startPart {}", this);
_handler.startPart();
return;
}
switch (_state)
{
case DELIMITER:
if (t.getChar() == '-')
setState(State.DELIMITER_CLOSE);
else
setState(State.DELIMITER_PADDING);
continue;
case DELIMITER_CLOSE:
if (t.getChar() == '-')
{
setState(State.EPILOGUE);
return;
}
setState(State.DELIMITER_PADDING);
continue;
case DELIMITER_PADDING:
default:
}
}
}
/* ------------------------------------------------------------------------------- */
/*
* Parse the message headers and return true if the handler has signaled for a return
*/
protected boolean parseMimePartHeaders(ByteBuffer buffer)
{
// Process headers
while (_state == State.BODY_PART && hasNextByte(buffer))
{
// process each character
HttpTokens.Token t = next(buffer);
if (t == null)
break;
if (t.getType() != HttpTokens.Type.LF)
_totalHeaderLineLength++;
if (_totalHeaderLineLength > MAX_HEADER_LINE_LENGTH)
throw new IllegalStateException("Header Line Exceeded Max Length");
switch (_fieldState)
{
case FIELD:
switch (t.getType())
{
case SPACE:
case HTAB:
{
// Folded field value!
if (_fieldName == null)
throw new IllegalStateException("First field folded");
if (_fieldValue == null)
{
_string.reset();
_length = 0;
}
else
{
setString(_fieldValue);
_string.append(' ');
_length++;
_fieldValue = null;
}
setState(FieldState.VALUE);
break;
}
case LF:
handleField();
setState(State.FIRST_OCTETS);
_partialBoundary = 2; // CRLF is option for empty parts
if (LOG.isDebugEnabled())
LOG.debug("headerComplete {}", this);
if (_handler.headerComplete())
return true;
break;
case ALPHA:
case DIGIT:
case TCHAR:
// process previous header
handleField();
// New header
setState(FieldState.IN_NAME);
_string.reset();
_string.append(t.getChar());
_length = 1;
break;
default:
throw new IllegalCharacterException(_state,t,buffer);
}
break;
case IN_NAME:
switch(t.getType())
{
case COLON:
_fieldName = takeString();
_length = -1;
setState(FieldState.VALUE);
break;
case SPACE:
// Ignore trailing whitespaces
setState(FieldState.AFTER_NAME);
break;
case LF:
{
if (LOG.isDebugEnabled())
LOG.debug("Line Feed in Name {}", this);
handleField();
setState(FieldState.FIELD);
break;
}
case ALPHA:
case DIGIT:
case TCHAR:
_string.append(t.getChar());
_length = _string.length();
break;
default:
throw new IllegalCharacterException(_state,t,buffer);
}
break;
case AFTER_NAME:
switch(t.getType())
{
case COLON:
_fieldName = takeString();
_length = -1;
setState(FieldState.VALUE);
break;
case LF:
_fieldName = takeString();
_string.reset();
_fieldValue = "";
_length = -1;
break;
case SPACE:
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case VALUE:
switch(t.getType())
{
case LF:
_string.reset();
_fieldValue = "";
_length = -1;
setState(FieldState.FIELD);
break;
case SPACE:
case HTAB:
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
case OTEXT:
_string.append(t.getByte());
_length = _string.length();
setState(FieldState.IN_VALUE);
break;
default:
throw new IllegalCharacterException(_state,t,buffer);
}
break;
case IN_VALUE:
switch(t.getType())
{
case SPACE:
case HTAB:
_string.append(' ');
break;
case LF:
if (_length > 0)
{
_fieldValue = takeString();
_length = -1;
_totalHeaderLineLength = -1;
}
setState(FieldState.FIELD);
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
case OTEXT:
_string.append(t.getByte());
_length=_string.length();
break;
default:
throw new IllegalCharacterException(_state,t,buffer);
}
break;
default:
throw new IllegalStateException(_state.toString());
}
}
return false;
}
/* ------------------------------------------------------------------------------- */
private void handleField()
{
if (LOG.isDebugEnabled())
LOG.debug("parsedField: _fieldName={} _fieldValue={} {}", _fieldName, _fieldValue, this);
if (_fieldName != null && _fieldValue != null)
_handler.parsedField(_fieldName, _fieldValue);
_fieldName = _fieldValue = null;
}
/* ------------------------------------------------------------------------------- */
protected boolean parseOctetContent(ByteBuffer buffer)
{
// Starts With
if (_partialBoundary > 0)
{
int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
if (partial > 0)
{
if (partial == _delimiterSearch.getLength())
{
buffer.position(buffer.position() + _delimiterSearch.getLength() - _partialBoundary);
setState(State.DELIMITER);
_partialBoundary = 0;
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(BufferUtil.EMPTY_BUFFER), true, this);
return _handler.content(BufferUtil.EMPTY_BUFFER, true);
}
_partialBoundary = partial;
BufferUtil.clear(buffer);
return false;
}
else
{
// output up to _partialBoundary of the search pattern
ByteBuffer content = _patternBuffer.slice();
if (_state == State.FIRST_OCTETS)
{
setState(State.OCTETS);
content.position(2);
}
content.limit(_partialBoundary);
_partialBoundary = 0;
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
if (_handler.content(content, false))
return true;
}
}
// Contains
int delimiter = _delimiterSearch.match(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
if (delimiter >= 0)
{
ByteBuffer content = buffer.slice();
content.limit(delimiter - buffer.arrayOffset() - buffer.position());
buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
setState(State.DELIMITER);
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), true, this);
return _handler.content(content, true);
}
// Ends With
_partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
if (_partialBoundary > 0)
{
ByteBuffer content = buffer.slice();
content.limit(content.limit() - _partialBoundary);
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
BufferUtil.clear(buffer);
return _handler.content(content, false);
}
// There is normal content with no delimiter
ByteBuffer content = buffer.slice();
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
BufferUtil.clear(buffer);
return _handler.content(content, false);
}
/* ------------------------------------------------------------------------------- */
private void setState(State state)
{
if (DEBUG)
LOG.debug("{} --> {}", _state, state);
_state = state;
}
/* ------------------------------------------------------------------------------- */
private void setState(FieldState state)
{
if (DEBUG)
LOG.debug("{}:{} --> {}", _state, _fieldState, state);
_fieldState = state;
}
/* ------------------------------------------------------------------------------- */
@Override
public String toString()
{
return String.format("%s{s=%s}", getClass().getSimpleName(), _state);
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/*
* Event Handler interface These methods return true if the caller should process the events so far received (eg return from parseNext and call
* HttpChannel.handle). If multiple callbacks are called in sequence (eg headerComplete then messageComplete) from the same point in the parsing then it is
* sufficient for the caller to process the events only once.
*/
public interface Handler
{
default void startPart()
{
}
@SuppressWarnings("unused")
default void parsedField(String name, String value)
{
}
default boolean headerComplete()
{
return false;
}
@SuppressWarnings("unused")
default boolean content(ByteBuffer item, boolean last)
{
return false;
}
default boolean messageComplete()
{
return false;
}
default void earlyEOF()
{
}
}
/* ------------------------------------------------------------------------------- */
@SuppressWarnings("serial")
private static class IllegalCharacterException extends BadMessageException
{
private IllegalCharacterException(State state,HttpTokens.Token token,ByteBuffer buffer)
{
super(400,String.format("Illegal character %s",token));
if (LOG.isDebugEnabled())
LOG.debug(String.format("Illegal character %s in state=%s for buffer %s",token,state,BufferUtil.toDetailString(buffer)));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy