/*
* 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 org.jboss.netty.handler.codec.http.multipart;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpConstants;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.util.internal.StringUtil;
import java.nio.charset.Charset;
import java.util.List;
/**
* This decoder will decode Body and can handle POST BODY (both multipart and standard).
*/
@SuppressWarnings("deprecation")
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
/**
* Does this request is a Multipart request
*/
private final InterfaceHttpPostRequestDecoder decoder;
/**
*
* @param request the request to decode
* @throws NullPointerException for request
* @throws IncompatibleDataDecoderException if the request has no body to decode
* @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
*/
public HttpPostRequestDecoder(HttpRequest request)
throws ErrorDataDecoderException, IncompatibleDataDecoderException {
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 IncompatibleDataDecoderException if the request has no body to decode
* @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
*/
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request)
throws ErrorDataDecoderException, IncompatibleDataDecoderException {
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 IncompatibleDataDecoderException if the request has no body to decode
* @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
*/
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request,
Charset charset) throws ErrorDataDecoderException,
IncompatibleDataDecoderException {
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 status 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 status 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) throws ErrorDataDecoderException {
if (request.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) {
return getMultipartDataBoundary(request.headers().get(HttpHeaders.Names.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)
throws ErrorDataDecoderException {
// Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
String[] headerContentType = splitHeaderContentType(contentType);
if (headerContentType[0].toLowerCase().startsWith(
HttpHeaders.Values.MULTIPART_FORM_DATA)) {
int mrank = 1, crank = 2;
if (headerContentType[1].toLowerCase().startsWith(
HttpHeaders.Values.BOUNDARY.toString())) {
mrank = 1;
crank = 2;
} else if (headerContentType[2].toLowerCase().startsWith(
HttpHeaders.Values.BOUNDARY.toString())) {
mrank = 2;
crank = 1;
} else {
return null;
}
String[] boundary = StringUtil.split(headerContentType[mrank], '=');
if (boundary.length != 2) {
throw new ErrorDataDecoderException("Needs a boundary value");
}
if (headerContentType[crank].toLowerCase().startsWith(
HttpHeaders.Values.CHARSET.toString())) {
String[] charset = StringUtil.split(headerContentType[crank], '=');
if (charset.length > 1) {
return new String[] {"--" + boundary[1], charset[1]};
}
}
return new String[] {"--" + boundary[1]};
}
return null;
}
/**
* True if this request is a Multipart request
* @return True if this request is a Multipart request
*/
public boolean isMultipart() {
return decoder.isMultipart();
}
/**
* This method returns a List of all HttpDatas from body.<br>
*
* If chunked, all chunks must have been offered using offer() method.
* If not, NotEnoughDataDecoderException will be raised.
*
* @return the list of HttpDatas from Body part for POST method
* @throws NotEnoughDataDecoderException Need more chunks
*/
public List<InterfaceHttpData> getBodyHttpDatas()
throws NotEnoughDataDecoderException {
return decoder.getBodyHttpDatas();
}
/**
* This method returns a List of all HttpDatas with the given name from body.<br>
*
* If chunked, all chunks must have been offered using offer() method.
* If not, NotEnoughDataDecoderException will be raised.
* @return All Body HttpDatas with the given name (ignore case)
* @throws NotEnoughDataDecoderException need more chunks
*/
public List<InterfaceHttpData> getBodyHttpDatas(String name)
throws NotEnoughDataDecoderException {
return decoder.getBodyHttpDatas(name);
}
/**
* This method returns the first InterfaceHttpData with the given name from body.<br>
*
* If chunked, all chunks must have been offered using offer() method.
* If not, NotEnoughDataDecoderException will be raised.
*
* @return The first Body InterfaceHttpData with the given name (ignore case)
* @throws NotEnoughDataDecoderException need more chunks
*/
public InterfaceHttpData getBodyHttpData(String name)
throws NotEnoughDataDecoderException {
return decoder.getBodyHttpData(name);
}
/**
* Initialized the internals from a new chunk
* @param chunk the new received chunk
* @throws ErrorDataDecoderException if there is a problem with the charset decoding or
* other errors
*/
public void offer(HttpChunk chunk) throws ErrorDataDecoderException {
decoder.offer(chunk);
}
/**
* True if at current status, there is an available decoded InterfaceHttpData from the Body.
*
* This method works for chunked and not chunked request.
*
* @return True if at current status, there is a decoded InterfaceHttpData
* @throws EndOfDataDecoderException No more data will be available
*/
public boolean hasNext() throws EndOfDataDecoderException {
return decoder.hasNext();
}
/**
* Returns the next available InterfaceHttpData or null if, at the time it is called, there is no more
* available InterfaceHttpData. A subsequent call to offer(httpChunk) could enable more data.
*
* @return the next available InterfaceHttpData or null if none
* @throws EndOfDataDecoderException No more data will be available
*/
public InterfaceHttpData next() throws EndOfDataDecoderException {
return decoder.next();
}
/**
* Clean all HttpDatas (on Disk) for the current request.
*/
public void cleanFiles() {
decoder.cleanFiles();
}
/**
* Remove the given FileUpload from the list of FileUploads to clean
*/
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 Exception {
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 Exception {
private static final long serialVersionUID = 1336267941020800769L;
}
/**
* Exception when an error occurs while decoding
*/
public static class ErrorDataDecoderException extends Exception {
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);
}
}
/**
* Exception when an unappropriated method was called on a request
*/
@Deprecated
public static class IncompatibleDataDecoderException extends Exception {
private static final long serialVersionUID = -953268047926250267L;
public IncompatibleDataDecoderException() {
}
public IncompatibleDataDecoderException(String msg) {
super(msg);
}
public IncompatibleDataDecoderException(Throwable cause) {
super(cause);
}
public IncompatibleDataDecoderException(String msg, Throwable cause) {
super(msg, cause);
}
}
}