346 lines
12 KiB
Java
346 lines
12 KiB
Java
/*
|
|
* 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:
|
|
*
|
|
* 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.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.StringUtil;
|
|
|
|
import java.nio.charset.Charset;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* This decoder will decode Body and can handle POST BODY.
|
|
*
|
|
* You <strong>MUST</strong> 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) {
|
|
if (factory == null) {
|
|
throw new NullPointerException("factory");
|
|
}
|
|
if (request == null) {
|
|
throw new NullPointerException("request");
|
|
}
|
|
if (charset == null) {
|
|
throw new NullPointerException("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) {
|
|
if (request.headers().contains(HttpHeaderNames.CONTENT_TYPE)) {
|
|
return getMultipartDataBoundary(request.headers().get(HttpHeaderNames.CONTENT_TYPE)) != null;
|
|
} else {
|
|
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<InterfaceHttpData> getBodyHttpDatas() {
|
|
return decoder.getBodyHttpDatas();
|
|
}
|
|
|
|
@Override
|
|
public List<InterfaceHttpData> 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);
|
|
}
|
|
}
|
|
}
|