io.netty.handler.codec.http.multipart.HttpPostRequestEncoder Maven / Gradle / Ivy
/*
* 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.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest;
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.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedInput;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.regex.Pattern;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.util.AbstractMap.SimpleImmutableEntry;
/**
* This encoder will help to encode Request for a FORM as POST.
*
* According to RFC 7231, POST, PUT and OPTIONS allow to have a body.
* This encoder will support widely all methods except TRACE since the RFC notes
* for GET, DELETE, HEAD and CONNECT: (replaces XXX by one of these methods)
* "A payload within a XXX request message has no defined semantics;
* sending a payload body on a XXX request might cause some existing
* implementations to reject the request."
* On the contrary, for TRACE method, RFC says:
* "A client MUST NOT send a message body in a TRACE request."
*/
public class HttpPostRequestEncoder implements ChunkedInput {
/**
* Different modes to use to encode form data.
*/
public enum EncoderMode {
/**
* Legacy mode which should work for most. It is known to not work with OAUTH. For OAUTH use
* {@link EncoderMode#RFC3986}. The W3C form recommendations this for submitting post form data.
*/
RFC1738,
/**
* Mode which is more new and is used for OAUTH
*/
RFC3986,
/**
* The HTML5 spec disallows mixed mode in multipart/form-data
* requests. More concretely this means that more files submitted
* under the same name will not be encoded using mixed mode, but
* will be treated as distinct fields.
*
* Reference:
* https://www.w3.org/TR/html5/forms.html#multipart-form-data
*/
HTML5
}
@SuppressWarnings("rawtypes")
private static final Map.Entry[] percentEncodings;
static {
percentEncodings = new Map.Entry[] {
new SimpleImmutableEntry(Pattern.compile("\\*"), "%2A"),
new SimpleImmutableEntry(Pattern.compile("\\+"), "%20"),
new SimpleImmutableEntry(Pattern.compile("~"), "%7E")
};
}
/**
* Factory used to create InterfaceHttpData
*/
private final HttpDataFactory factory;
/**
* Request to encode
*/
private final HttpRequest request;
/**
* Default charset to use
*/
private final Charset charset;
/**
* Chunked false by default
*/
private boolean isChunked;
/**
* InterfaceHttpData for Body (without encoding)
*/
private final List bodyListDatas;
/**
* The final Multipart List of InterfaceHttpData including encoding
*/
final List multipartHttpDatas;
/**
* Does this request is a Multipart request
*/
private final boolean isMultipart;
/**
* If multipart, this is the boundary for the flobal multipart
*/
String multipartDataBoundary;
/**
* If multipart, there could be internal multiparts (mixed) to the global multipart. Only one level is allowed.
*/
String multipartMixedBoundary;
/**
* To check if the header has been finalized
*/
private boolean headerFinalized;
private final EncoderMode encoderMode;
/**
*
* @param request
* the request to encode
* @param multipart
* True if the FORM is a ENCTYPE="multipart/form-data"
* @throws NullPointerException
* for request
* @throws ErrorDataEncoderException
* if the request is a TRACE
*/
public HttpPostRequestEncoder(HttpRequest request, boolean multipart) throws ErrorDataEncoderException {
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, multipart,
HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
}
/**
*
* @param factory
* the factory used to create InterfaceHttpData
* @param request
* the request to encode
* @param multipart
* True if the FORM is a ENCTYPE="multipart/form-data"
* @throws NullPointerException
* for request and factory
* @throws ErrorDataEncoderException
* if the request is a TRACE
*/
public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart)
throws ErrorDataEncoderException {
this(factory, request, multipart, HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
}
/**
*
* @param factory
* the factory used to create InterfaceHttpData
* @param request
* the request to encode
* @param multipart
* True if the FORM is a ENCTYPE="multipart/form-data"
* @param charset
* the charset to use as default
* @param encoderMode
* the mode for the encoder to use. See {@link EncoderMode} for the details.
* @throws NullPointerException
* for request or charset or factory
* @throws ErrorDataEncoderException
* if the request is a TRACE
*/
public HttpPostRequestEncoder(
HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset,
EncoderMode encoderMode)
throws ErrorDataEncoderException {
this.request = checkNotNull(request, "request");
this.charset = checkNotNull(charset, "charset");
this.factory = checkNotNull(factory, "factory");
if (HttpMethod.TRACE.equals(request.method())) {
throw new ErrorDataEncoderException("Cannot create a Encoder if request is a TRACE");
}
// Fill default values
bodyListDatas = new ArrayList();
// default mode
isLastChunk = false;
isLastChunkSent = false;
isMultipart = multipart;
multipartHttpDatas = new ArrayList();
this.encoderMode = encoderMode;
if (isMultipart) {
initDataMultipart();
}
}
/**
* Clean all HttpDatas (on Disk) for the current request.
*/
public void cleanFiles() {
factory.cleanRequestHttpData(request);
}
/**
* Does the last non empty chunk already encoded so that next chunk will be empty (last chunk)
*/
private boolean isLastChunk;
/**
* Last chunk already sent
*/
private boolean isLastChunkSent;
/**
* The current FileUpload that is currently in encode process
*/
private FileUpload currentFileUpload;
/**
* While adding a FileUpload, is the multipart currently in Mixed Mode
*/
private boolean duringMixedMode;
/**
* Global Body size
*/
private long globalBodySize;
/**
* Global Transfer progress
*/
private long globalProgress;
/**
* True if this request is a Multipart request
*
* @return True if this request is a Multipart request
*/
public boolean isMultipart() {
return isMultipart;
}
/**
* Init the delimiter for Global Part (Data).
*/
private void initDataMultipart() {
multipartDataBoundary = getNewMultipartDelimiter();
}
/**
* Init the delimiter for Mixed Part (Mixed).
*/
private void initMixedMultipart() {
multipartMixedBoundary = getNewMultipartDelimiter();
}
/**
*
* @return a newly generated Delimiter (either for DATA or MIXED)
*/
private static String getNewMultipartDelimiter() {
// construct a generated delimiter
return Long.toHexString(PlatformDependent.threadLocalRandom().nextLong());
}
/**
* This getMethod returns a List of all InterfaceHttpData from body part.
* @return the list of InterfaceHttpData from Body part
*/
public List getBodyListAttributes() {
return bodyListDatas;
}
/**
* Set the Body HttpDatas list
*
* @throws NullPointerException
* for datas
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public void setBodyHttpDatas(List datas) throws ErrorDataEncoderException {
ObjectUtil.checkNotNull(datas, "datas");
globalBodySize = 0;
bodyListDatas.clear();
currentFileUpload = null;
duringMixedMode = false;
multipartHttpDatas.clear();
for (InterfaceHttpData data : datas) {
addBodyHttpData(data);
}
}
/**
* Add a simple attribute in the body as Name=Value
*
* @param name
* name of the parameter
* @param value
* the value of the parameter
* @throws NullPointerException
* for name
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public void addBodyAttribute(String name, String value) throws ErrorDataEncoderException {
String svalue = value != null? value : StringUtil.EMPTY_STRING;
Attribute data = factory.createAttribute(request, checkNotNull(name, "name"), svalue);
addBodyHttpData(data);
}
/**
* Add a file as a FileUpload
*
* @param name
* the name of the parameter
* @param file
* the file to be uploaded (if not Multipart mode, only the filename will be included)
* @param contentType
* the associated contentType for the File
* @param isText
* True if this file should be transmitted in Text format (else binary)
* @throws NullPointerException
* for name and file
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public void addBodyFileUpload(String name, File file, String contentType, boolean isText)
throws ErrorDataEncoderException {
addBodyFileUpload(name, file.getName(), file, contentType, isText);
}
/**
* Add a file as a FileUpload
*
* @param name
* the name of the parameter
* @param file
* the file to be uploaded (if not Multipart mode, only the filename will be included)
* @param filename
* the filename to use for this File part, empty String will be ignored by
* the encoder
* @param contentType
* the associated contentType for the File
* @param isText
* True if this file should be transmitted in Text format (else binary)
* @throws NullPointerException
* for name and file
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public void addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText)
throws ErrorDataEncoderException {
checkNotNull(name, "name");
checkNotNull(file, "file");
if (filename == null) {
filename = StringUtil.EMPTY_STRING;
}
String scontentType = contentType;
String contentTransferEncoding = null;
if (contentType == null) {
if (isText) {
scontentType = HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE;
} else {
scontentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
}
}
if (!isText) {
contentTransferEncoding = HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value();
}
FileUpload fileUpload = factory.createFileUpload(request, name, filename, scontentType,
contentTransferEncoding, null, file.length());
try {
fileUpload.setContent(file);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
addBodyHttpData(fileUpload);
}
/**
* Add a series of Files associated with one File parameter
*
* @param name
* the name of the parameter
* @param file
* the array of files
* @param contentType
* the array of content Types associated with each file
* @param isText
* the array of isText attribute (False meaning binary mode) for each file
* @throws IllegalArgumentException
* also throws if array have different sizes
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public void addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)
throws ErrorDataEncoderException {
if (file.length != contentType.length && file.length != isText.length) {
throw new IllegalArgumentException("Different array length");
}
for (int i = 0; i < file.length; i++) {
addBodyFileUpload(name, file[i], contentType[i], isText[i]);
}
}
/**
* Add the InterfaceHttpData to the Body list
*
* @throws NullPointerException
* for data
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException {
if (headerFinalized) {
throw new ErrorDataEncoderException("Cannot add value once finalized");
}
bodyListDatas.add(checkNotNull(data, "data"));
if (!isMultipart) {
if (data instanceof Attribute) {
Attribute attribute = (Attribute) data;
try {
// name=value& with encoded name and attribute
String key = encodeAttribute(attribute.getName(), charset);
String value = encodeAttribute(attribute.getValue(), charset);
Attribute newattribute = factory.createAttribute(request, key, value);
multipartHttpDatas.add(newattribute);
globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
} else if (data instanceof FileUpload) {
// since not Multipart, only name=filename => Attribute
FileUpload fileUpload = (FileUpload) data;
// name=filename& with encoded name and filename
String key = encodeAttribute(fileUpload.getName(), charset);
String value = encodeAttribute(fileUpload.getFilename(), charset);
Attribute newattribute = factory.createAttribute(request, key, value);
multipartHttpDatas.add(newattribute);
globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
}
return;
}
/*
* Logic:
* if not Attribute:
* add Data to body list
* if (duringMixedMode)
* add endmixedmultipart delimiter
* currentFileUpload = null
* duringMixedMode = false;
* add multipart delimiter, multipart body header and Data to multipart list
* reset currentFileUpload, duringMixedMode
* if FileUpload: take care of multiple file for one field => mixed mode
* if (duringMixedMode)
* if (currentFileUpload.name == data.name)
* add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
* else
* add endmixedmultipart delimiter, multipart body header and Data to multipart list
* currentFileUpload = data
* duringMixedMode = false;
* else
* if (currentFileUpload.name == data.name)
* change multipart body header of previous file into multipart list to
* mixedmultipart start, mixedmultipart body header
* add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
* duringMixedMode = true
* else
* add multipart delimiter, multipart body header and Data to multipart list
* currentFileUpload = data
* duringMixedMode = false;
* Do not add last delimiter! Could be:
* if duringmixedmode: endmixedmultipart + endmultipart
* else only endmultipart
*/
if (data instanceof Attribute) {
if (duringMixedMode) {
InternalAttribute internal = new InternalAttribute(charset);
internal.addValue("\r\n--" + multipartMixedBoundary + "--");
multipartHttpDatas.add(internal);
multipartMixedBoundary = null;
currentFileUpload = null;
duringMixedMode = false;
}
InternalAttribute internal = new InternalAttribute(charset);
if (!multipartHttpDatas.isEmpty()) {
// previously a data field so CRLF
internal.addValue("\r\n");
}
internal.addValue("--" + multipartDataBoundary + "\r\n");
// content-disposition: form-data; name="field1"
Attribute attribute = (Attribute) data;
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
+ HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n");
// Add Content-Length: xxx
internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
attribute.length() + "\r\n");
Charset localcharset = attribute.getCharset();
if (localcharset != null) {
// Content-Type: text/plain; charset=charset
internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " +
HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE + "; " +
HttpHeaderValues.CHARSET + '='
+ localcharset.name() + "\r\n");
}
// CRLF between body header and data
internal.addValue("\r\n");
multipartHttpDatas.add(internal);
multipartHttpDatas.add(data);
globalBodySize += attribute.length() + internal.size();
} else if (data instanceof FileUpload) {
FileUpload fileUpload = (FileUpload) data;
InternalAttribute internal = new InternalAttribute(charset);
if (!multipartHttpDatas.isEmpty()) {
// previously a data field so CRLF
internal.addValue("\r\n");
}
boolean localMixed;
if (duringMixedMode) {
if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) {
// continue a mixed mode
localMixed = true;
} else {
// end a mixed mode
// add endmixedmultipart delimiter, multipart body header
// and
// Data to multipart list
internal.addValue("--" + multipartMixedBoundary + "--");
multipartHttpDatas.add(internal);
multipartMixedBoundary = null;
// start a new one (could be replaced if mixed start again
// from here
internal = new InternalAttribute(charset);
internal.addValue("\r\n");
localMixed = false;
// new currentFileUpload and no more in Mixed mode
currentFileUpload = fileUpload;
duringMixedMode = false;
}
} else {
if (encoderMode != EncoderMode.HTML5 && currentFileUpload != null
&& currentFileUpload.getName().equals(fileUpload.getName())) {
// create a new mixed mode (from previous file)
// change multipart body header of previous file into
// multipart list to
// mixedmultipart start, mixedmultipart body header
// change Internal (size()-2 position in multipartHttpDatas)
// from (line starting with *)
// --AaB03x
// * Content-Disposition: form-data; name="files";
// filename="file1.txt"
// Content-Type: text/plain
// to (lines starting with *)
// --AaB03x
// * Content-Disposition: form-data; name="files"
// * Content-Type: multipart/mixed; boundary=BbC04y
// *
// * --BbC04y
// * Content-Disposition: attachment; filename="file1.txt"
// Content-Type: text/plain
initMixedMultipart();
InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas.get(multipartHttpDatas
.size() - 2);
// remove past size
globalBodySize -= pastAttribute.size();
StringBuilder replacement = new StringBuilder(
139 + multipartDataBoundary.length() + multipartMixedBoundary.length() * 2 +
fileUpload.getFilename().length() + fileUpload.getName().length())
.append("--")
.append(multipartDataBoundary)
.append("\r\n")
.append(HttpHeaderNames.CONTENT_DISPOSITION)
.append(": ")
.append(HttpHeaderValues.FORM_DATA)
.append("; ")
.append(HttpHeaderValues.NAME)
.append("=\"")
.append(fileUpload.getName())
.append("\"\r\n")
.append(HttpHeaderNames.CONTENT_TYPE)
.append(": ")
.append(HttpHeaderValues.MULTIPART_MIXED)
.append("; ")
.append(HttpHeaderValues.BOUNDARY)
.append('=')
.append(multipartMixedBoundary)
.append("\r\n\r\n")
.append("--")
.append(multipartMixedBoundary)
.append("\r\n")
.append(HttpHeaderNames.CONTENT_DISPOSITION)
.append(": ")
.append(HttpHeaderValues.ATTACHMENT);
if (!fileUpload.getFilename().isEmpty()) {
replacement.append("; ")
.append(HttpHeaderValues.FILENAME)
.append("=\"")
.append(currentFileUpload.getFilename())
.append('"');
}
replacement.append("\r\n");
pastAttribute.setValue(replacement.toString(), 1);
pastAttribute.setValue("", 2);
// update past size
globalBodySize += pastAttribute.size();
// now continue
// add mixedmultipart delimiter, mixedmultipart body header
// and
// Data to multipart list
localMixed = true;
duringMixedMode = true;
} else {
// a simple new multipart
// add multipart delimiter, multipart body header and Data
// to multipart list
localMixed = false;
currentFileUpload = fileUpload;
duringMixedMode = false;
}
}
if (localMixed) {
// add mixedmultipart delimiter, mixedmultipart body header and
// Data to multipart list
internal.addValue("--" + multipartMixedBoundary + "\r\n");
if (fileUpload.getFilename().isEmpty()) {
// Content-Disposition: attachment
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
+ HttpHeaderValues.ATTACHMENT + "\r\n");
} else {
// Content-Disposition: attachment; filename="file1.txt"
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
+ HttpHeaderValues.ATTACHMENT + "; "
+ HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
}
} else {
internal.addValue("--" + multipartDataBoundary + "\r\n");
if (fileUpload.getFilename().isEmpty()) {
// Content-Disposition: form-data; name="files";
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
+ HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"\r\n");
} else {
// Content-Disposition: form-data; name="files";
// filename="file1.txt"
internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
+ HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; "
+ HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
}
}
// Add Content-Length: xxx
internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
fileUpload.length() + "\r\n");
// Content-Type: image/gif
// Content-Type: text/plain; charset=ISO-8859-1
// Content-Transfer-Encoding: binary
internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType());
String contentTransferEncoding = fileUpload.getContentTransferEncoding();
if (contentTransferEncoding != null
&& contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": "
+ HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() + "\r\n\r\n");
} else if (fileUpload.getCharset() != null) {
internal.addValue("; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n");
} else {
internal.addValue("\r\n\r\n");
}
multipartHttpDatas.add(internal);
multipartHttpDatas.add(data);
globalBodySize += fileUpload.length() + internal.size();
}
}
/**
* Iterator to be used when encoding will be called chunk after chunk
*/
private ListIterator iterator;
/**
* Finalize the request by preparing the Header in the request and returns the request ready to be sent.
* Once finalized, no data must be added.
* If the request does not need chunk (isChunked() == false), this request is the only object to send to the remote
* server.
*
* @return the request object (chunked or not according to size of body)
* @throws ErrorDataEncoderException
* if the encoding is in error or if the finalize were already done
*/
public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
// Finalize the multipartHttpDatas
if (!headerFinalized) {
if (isMultipart) {
InternalAttribute internal = new InternalAttribute(charset);
if (duringMixedMode) {
internal.addValue("\r\n--" + multipartMixedBoundary + "--");
}
internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
multipartHttpDatas.add(internal);
multipartMixedBoundary = null;
currentFileUpload = null;
duringMixedMode = false;
globalBodySize += internal.size();
}
headerFinalized = true;
} else {
throw new ErrorDataEncoderException("Header already encoded");
}
HttpHeaders headers = request.headers();
List contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE);
List transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING);
if (contentTypes != null) {
headers.remove(HttpHeaderNames.CONTENT_TYPE);
for (String contentType : contentTypes) {
// "multipart/form-data; boundary=--89421926422648"
String lowercased = contentType.toLowerCase();
if (lowercased.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString()) ||
lowercased.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
// ignore
} else {
headers.add(HttpHeaderNames.CONTENT_TYPE, contentType);
}
}
}
if (isMultipart) {
String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '='
+ multipartDataBoundary;
headers.add(HttpHeaderNames.CONTENT_TYPE, value);
} else {
// Not multipart
headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED);
}
// Now consider size for chunk or not
long realSize = globalBodySize;
if (!isMultipart) {
realSize -= 1; // last '&' removed
}
iterator = multipartHttpDatas.listIterator();
headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize));
if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) {
isChunked = true;
if (transferEncoding != null) {
headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
for (CharSequence v : transferEncoding) {
if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) {
// ignore
} else {
headers.add(HttpHeaderNames.TRANSFER_ENCODING, v);
}
}
}
HttpUtil.setTransferEncodingChunked(request, true);
// wrap to hide the possible content
return new WrappedHttpRequest(request);
} else {
// get the only one body and set it to the request
HttpContent chunk = nextChunk();
if (request instanceof FullHttpRequest) {
FullHttpRequest fullRequest = (FullHttpRequest) request;
ByteBuf chunkContent = chunk.content();
if (fullRequest.content() != chunkContent) {
fullRequest.content().clear().writeBytes(chunkContent);
chunkContent.release();
}
return fullRequest;
} else {
return new WrappedFullHttpRequest(request, chunk);
}
}
}
/**
* @return True if the request is by Chunk
*/
public boolean isChunked() {
return isChunked;
}
/**
* Encode one attribute
*
* @return the encoded attribute
* @throws ErrorDataEncoderException
* if the encoding is in error
*/
@SuppressWarnings("unchecked")
private String encodeAttribute(String s, Charset charset) throws ErrorDataEncoderException {
if (s == null) {
return "";
}
try {
String encoded = URLEncoder.encode(s, charset.name());
if (encoderMode == EncoderMode.RFC3986) {
for (Map.Entry entry : percentEncodings) {
String replacement = entry.getValue();
encoded = entry.getKey().matcher(encoded).replaceAll(replacement);
}
}
return encoded;
} catch (UnsupportedEncodingException e) {
throw new ErrorDataEncoderException(charset.name(), e);
}
}
/**
* The ByteBuf currently used by the encoder
*/
private ByteBuf currentBuffer;
/**
* The current InterfaceHttpData to encode (used if more chunks are available)
*/
private InterfaceHttpData currentData;
/**
* If not multipart, does the currentBuffer stands for the Key or for the Value
*/
private boolean isKey = true;
/**
*
* @return the next ByteBuf to send as an HttpChunk and modifying currentBuffer accordingly
*/
private ByteBuf fillByteBuf() {
int length = currentBuffer.readableBytes();
if (length > HttpPostBodyUtil.chunkSize) {
return currentBuffer.readRetainedSlice(HttpPostBodyUtil.chunkSize);
} else {
// to continue
ByteBuf slice = currentBuffer;
currentBuffer = null;
return slice;
}
}
/**
* From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
* sizeleft bytes more into the currentBuffer. This is the Multipart version.
*
* @param sizeleft
* the number of bytes to try to get from currentData
* @return the next HttpChunk or null if not enough bytes were found
* @throws ErrorDataEncoderException
* if the encoding is in error
*/
private HttpContent encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException {
if (currentData == null) {
return null;
}
ByteBuf buffer;
if (currentData instanceof InternalAttribute) {
buffer = ((InternalAttribute) currentData).toByteBuf();
currentData = null;
} else {
try {
buffer = ((HttpData) currentData).getChunk(sizeleft);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
if (buffer.capacity() == 0) {
// end for current InterfaceHttpData, need more data
currentData = null;
return null;
}
}
if (currentBuffer == null) {
currentBuffer = buffer;
} else {
currentBuffer = wrappedBuffer(currentBuffer, buffer);
}
if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
currentData = null;
return null;
}
buffer = fillByteBuf();
return new DefaultHttpContent(buffer);
}
/**
* From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
* sizeleft bytes more into the currentBuffer. This is the UrlEncoded version.
*
* @param sizeleft
* the number of bytes to try to get from currentData
* @return the next HttpChunk or null if not enough bytes were found
* @throws ErrorDataEncoderException
* if the encoding is in error
*/
private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException {
if (currentData == null) {
return null;
}
int size = sizeleft;
ByteBuf buffer;
// Set name=
if (isKey) {
String key = currentData.getName();
buffer = wrappedBuffer(key.getBytes(charset));
isKey = false;
if (currentBuffer == null) {
currentBuffer = wrappedBuffer(buffer, wrappedBuffer("=".getBytes(charset)));
} else {
currentBuffer = wrappedBuffer(currentBuffer, buffer, wrappedBuffer("=".getBytes(charset)));
}
// continue
size -= buffer.readableBytes() + 1;
if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
buffer = fillByteBuf();
return new DefaultHttpContent(buffer);
}
}
// Put value into buffer
try {
buffer = ((HttpData) currentData).getChunk(size);
} catch (IOException e) {
throw new ErrorDataEncoderException(e);
}
// Figure out delimiter
ByteBuf delimiter = null;
if (buffer.readableBytes() < size) {
isKey = true;
delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes(charset)) : null;
}
// End for current InterfaceHttpData, need potentially more data
if (buffer.capacity() == 0) {
currentData = null;
if (currentBuffer == null) {
if (delimiter == null) {
return null;
} else {
currentBuffer = delimiter;
}
} else {
if (delimiter != null) {
currentBuffer = wrappedBuffer(currentBuffer, delimiter);
}
}
if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
buffer = fillByteBuf();
return new DefaultHttpContent(buffer);
}
return null;
}
// Put it all together: name=value&
if (currentBuffer == null) {
if (delimiter != null) {
currentBuffer = wrappedBuffer(buffer, delimiter);
} else {
currentBuffer = buffer;
}
} else {
if (delimiter != null) {
currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
} else {
currentBuffer = wrappedBuffer(currentBuffer, buffer);
}
}
// end for current InterfaceHttpData, need more data
if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
currentData = null;
isKey = true;
return null;
}
buffer = fillByteBuf();
return new DefaultHttpContent(buffer);
}
@Override
public void close() throws Exception {
// NO since the user can want to reuse (broadcast for instance)
// cleanFiles();
}
@Deprecated
@Override
public HttpContent readChunk(ChannelHandlerContext ctx) throws Exception {
return readChunk(ctx.alloc());
}
/**
* Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
* in order to stop calling this getMethod.
*
* @return the next available HttpChunk
* @throws ErrorDataEncoderException
* if the encoding is in error
*/
@Override
public HttpContent readChunk(ByteBufAllocator allocator) throws Exception {
if (isLastChunkSent) {
return null;
} else {
HttpContent nextChunk = nextChunk();
globalProgress += nextChunk.content().readableBytes();
return nextChunk;
}
}
/**
* Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
* in order to stop calling this getMethod.
*
* @return the next available HttpChunk
* @throws ErrorDataEncoderException
* if the encoding is in error
*/
private HttpContent nextChunk() throws ErrorDataEncoderException {
if (isLastChunk) {
isLastChunkSent = true;
return LastHttpContent.EMPTY_LAST_CONTENT;
}
// first test if previous buffer is not empty
int size = calculateRemainingSize();
if (size <= 0) {
// NextChunk from buffer
ByteBuf buffer = fillByteBuf();
return new DefaultHttpContent(buffer);
}
// size > 0
if (currentData != null) {
// continue to read data
HttpContent chunk;
if (isMultipart) {
chunk = encodeNextChunkMultipart(size);
} else {
chunk = encodeNextChunkUrlEncoded(size);
}
if (chunk != null) {
// NextChunk from data
return chunk;
}
size = calculateRemainingSize();
}
if (!iterator.hasNext()) {
return lastChunk();
}
while (size > 0 && iterator.hasNext()) {
currentData = iterator.next();
HttpContent chunk;
if (isMultipart) {
chunk = encodeNextChunkMultipart(size);
} else {
chunk = encodeNextChunkUrlEncoded(size);
}
if (chunk == null) {
// not enough
size = calculateRemainingSize();
continue;
}
// NextChunk from data
return chunk;
}
// end since no more data
return lastChunk();
}
private int calculateRemainingSize() {
int size = HttpPostBodyUtil.chunkSize;
if (currentBuffer != null) {
size -= currentBuffer.readableBytes();
}
return size;
}
private HttpContent lastChunk() {
isLastChunk = true;
if (currentBuffer == null) {
isLastChunkSent = true;
// LastChunk with no more data
return LastHttpContent.EMPTY_LAST_CONTENT;
}
// NextChunk as last non empty from buffer
ByteBuf buffer = currentBuffer;
currentBuffer = null;
return new DefaultHttpContent(buffer);
}
@Override
public boolean isEndOfInput() throws Exception {
return isLastChunkSent;
}
@Override
public long length() {
return isMultipart? globalBodySize : globalBodySize - 1;
}
@Override
public long progress() {
return globalProgress;
}
/**
* Exception when an error occurs while encoding
*/
public static class ErrorDataEncoderException extends Exception {
private static final long serialVersionUID = 5020247425493164465L;
public ErrorDataEncoderException() {
}
public ErrorDataEncoderException(String msg) {
super(msg);
}
public ErrorDataEncoderException(Throwable cause) {
super(cause);
}
public ErrorDataEncoderException(String msg, Throwable cause) {
super(msg, cause);
}
}
private static class WrappedHttpRequest implements HttpRequest {
private final HttpRequest request;
WrappedHttpRequest(HttpRequest request) {
this.request = request;
}
@Override
public HttpRequest setProtocolVersion(HttpVersion version) {
request.setProtocolVersion(version);
return this;
}
@Override
public HttpRequest setMethod(HttpMethod method) {
request.setMethod(method);
return this;
}
@Override
public HttpRequest setUri(String uri) {
request.setUri(uri);
return this;
}
@Override
public HttpMethod getMethod() {
return request.method();
}
@Override
public HttpMethod method() {
return request.method();
}
@Override
public String getUri() {
return request.uri();
}
@Override
public String uri() {
return request.uri();
}
@Override
public HttpVersion getProtocolVersion() {
return request.protocolVersion();
}
@Override
public HttpVersion protocolVersion() {
return request.protocolVersion();
}
@Override
public HttpHeaders headers() {
return request.headers();
}
@Override
public DecoderResult decoderResult() {
return request.decoderResult();
}
@Override
@Deprecated
public DecoderResult getDecoderResult() {
return request.getDecoderResult();
}
@Override
public void setDecoderResult(DecoderResult result) {
request.setDecoderResult(result);
}
}
private static final class WrappedFullHttpRequest extends WrappedHttpRequest implements FullHttpRequest {
private final HttpContent content;
private WrappedFullHttpRequest(HttpRequest request, HttpContent content) {
super(request);
this.content = content;
}
@Override
public FullHttpRequest setProtocolVersion(HttpVersion version) {
super.setProtocolVersion(version);
return this;
}
@Override
public FullHttpRequest setMethod(HttpMethod method) {
super.setMethod(method);
return this;
}
@Override
public FullHttpRequest setUri(String uri) {
super.setUri(uri);
return this;
}
@Override
public FullHttpRequest copy() {
return replace(content().copy());
}
@Override
public FullHttpRequest duplicate() {
return replace(content().duplicate());
}
@Override
public FullHttpRequest retainedDuplicate() {
return replace(content().retainedDuplicate());
}
@Override
public FullHttpRequest replace(ByteBuf content) {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(protocolVersion(), method(), uri(), content);
duplicate.headers().set(headers());
duplicate.trailingHeaders().set(trailingHeaders());
return duplicate;
}
@Override
public FullHttpRequest retain(int increment) {
content.retain(increment);
return this;
}
@Override
public FullHttpRequest retain() {
content.retain();
return this;
}
@Override
public FullHttpRequest touch() {
content.touch();
return this;
}
@Override
public FullHttpRequest touch(Object hint) {
content.touch(hint);
return this;
}
@Override
public ByteBuf content() {
return content.content();
}
@Override
public HttpHeaders trailingHeaders() {
if (content instanceof LastHttpContent) {
return ((LastHttpContent) content).trailingHeaders();
} else {
return EmptyHttpHeaders.INSTANCE;
}
}
@Override
public int refCnt() {
return content.refCnt();
}
@Override
public boolean release() {
return content.release();
}
@Override
public boolean release(int decrement) {
return content.release(decrement);
}
}
}