io.netty.handler.codec.http.multipart.HttpPostRequestDecoder 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).
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you 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 io.netty.handler.codec.http.multipart;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.StringUtil;
import java.nio.charset.Charset;
import java.util.List;
/**
* This decoder will decode Body and can handle POST BODY.
*
* You MUST call {@link #destroy()} after completion to release all resources.
*
*/
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;
private final InterfaceHttpPostRequestDecoder decoder;
/**
*
* @param request
* the request to decode
* @throws NullPointerException
* for request
* @throws ErrorDataDecoderException
* if the default charset was wrong when decoding or other
* errors
*/
public HttpPostRequestDecoder(HttpRequest request) {
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
}
/**
*
* @param factory
* the factory used to create InterfaceHttpData
* @param request
* the request to decode
* @throws NullPointerException
* for request or factory
* @throws ErrorDataDecoderException
* if the default charset was wrong when decoding or other
* errors
*/
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) {
this(factory, request, HttpConstants.DEFAULT_CHARSET);
}
/**
*
* @param factory
* the factory used to create InterfaceHttpData
* @param request
* the request to decode
* @param charset
* the charset to use as default
* @throws NullPointerException
* for request or charset or factory
* @throws ErrorDataDecoderException
* if the default charset was wrong when decoding or other
* errors
*/
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
ObjectUtil.checkNotNull(factory, "factory");
ObjectUtil.checkNotNull(request, "request");
ObjectUtil.checkNotNull(charset, "charset");
// Fill default values
if (isMultipart(request)) {
decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
} else {
decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
}
}
/**
* states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD |
* FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER
* MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+
* EPILOGUE
*
* First getStatus is: NOSTARTED
*
* Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header
*
* --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1"
* => DISPOSITION
*
* Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition:
* form-data; name="pics" => DISPOSITION Content-type: multipart/mixed,
* boundary=BbC04y
*
* --BbC04y => MIXEDDELIMITER Content-disposition: attachment;
* filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain
*
* ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y =>
* MIXEDDELIMITER Content-disposition: file; filename="file2.gif" =>
* MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding:
* binary
*
* ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- =>
* MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER
*
* Once CLOSEDELIMITER is found, last getStatus is EPILOGUE
*/
protected enum MultiPartStatus {
NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER,
MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE
}
/**
* Check if the given request is a multipart request
* @return True if the request is a Multipart request
*/
public static boolean isMultipart(HttpRequest request) {
String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
return getMultipartDataBoundary(mimeType) != null;
}
return false;
}
/**
* Check from the request ContentType if this request is a Multipart request.
* @return an array of String if multipartDataBoundary exists with the multipartDataBoundary
* as first element, charset if any as second (missing if not set), else null
*/
protected static String[] getMultipartDataBoundary(String contentType) {
// Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
String[] headerContentType = splitHeaderContentType(contentType);
final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString();
if (headerContentType[0].regionMatches(true, 0, multiPartHeader, 0 , multiPartHeader.length())) {
int mrank;
int crank;
final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString();
if (headerContentType[1].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {
mrank = 1;
crank = 2;
} else if (headerContentType[2].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {
mrank = 2;
crank = 1;
} else {
return null;
}
String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
if (boundary == null) {
throw new ErrorDataDecoderException("Needs a boundary value");
}
if (boundary.charAt(0) == '"') {
String bound = boundary.trim();
int index = bound.length() - 1;
if (bound.charAt(index) == '"') {
boundary = bound.substring(1, index);
}
}
final String charsetHeader = HttpHeaderValues.CHARSET.toString();
if (headerContentType[crank].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
String charset = StringUtil.substringAfter(headerContentType[crank], '=');
if (charset != null) {
return new String[] {"--" + boundary, charset};
}
}
return new String[] {"--" + boundary};
}
return null;
}
@Override
public boolean isMultipart() {
return decoder.isMultipart();
}
@Override
public void setDiscardThreshold(int discardThreshold) {
decoder.setDiscardThreshold(discardThreshold);
}
@Override
public int getDiscardThreshold() {
return decoder.getDiscardThreshold();
}
@Override
public List getBodyHttpDatas() {
return decoder.getBodyHttpDatas();
}
@Override
public List getBodyHttpDatas(String name) {
return decoder.getBodyHttpDatas(name);
}
@Override
public InterfaceHttpData getBodyHttpData(String name) {
return decoder.getBodyHttpData(name);
}
@Override
public InterfaceHttpPostRequestDecoder offer(HttpContent content) {
return decoder.offer(content);
}
@Override
public boolean hasNext() {
return decoder.hasNext();
}
@Override
public InterfaceHttpData next() {
return decoder.next();
}
@Override
public InterfaceHttpData currentPartialHttpData() {
return decoder.currentPartialHttpData();
}
@Override
public void destroy() {
decoder.destroy();
}
@Override
public void cleanFiles() {
decoder.cleanFiles();
}
@Override
public void removeHttpDataFromClean(InterfaceHttpData data) {
decoder.removeHttpDataFromClean(data);
}
/**
* Split the very first line (Content-Type value) in 3 Strings
*
* @return the array of 3 Strings
*/
private static String[] splitHeaderContentType(String sb) {
int aStart;
int aEnd;
int bStart;
int bEnd;
int cStart;
int cEnd;
aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
aEnd = sb.indexOf(';');
if (aEnd == -1) {
return new String[] { sb, "", "" };
}
bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
if (sb.charAt(aEnd - 1) == ' ') {
aEnd--;
}
bEnd = sb.indexOf(';', bStart);
if (bEnd == -1) {
bEnd = HttpPostBodyUtil.findEndOfString(sb);
return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" };
}
cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
if (sb.charAt(bEnd - 1) == ' ') {
bEnd--;
}
cEnd = HttpPostBodyUtil.findEndOfString(sb);
return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd) };
}
/**
* Exception when try reading data from request in chunked format, and not
* enough data are available (need more chunks)
*/
public static class NotEnoughDataDecoderException extends DecoderException {
private static final long serialVersionUID = -7846841864603865638L;
public NotEnoughDataDecoderException() {
}
public NotEnoughDataDecoderException(String msg) {
super(msg);
}
public NotEnoughDataDecoderException(Throwable cause) {
super(cause);
}
public NotEnoughDataDecoderException(String msg, Throwable cause) {
super(msg, cause);
}
}
/**
* Exception when the body is fully decoded, even if there is still data
*/
public static class EndOfDataDecoderException extends DecoderException {
private static final long serialVersionUID = 1336267941020800769L;
}
/**
* Exception when an error occurs while decoding
*/
public static class ErrorDataDecoderException extends DecoderException {
private static final long serialVersionUID = 5020247425493164465L;
public ErrorDataDecoderException() {
}
public ErrorDataDecoderException(String msg) {
super(msg);
}
public ErrorDataDecoderException(Throwable cause) {
super(cause);
}
public ErrorDataDecoderException(String msg, Throwable cause) {
super(msg, cause);
}
}
}