1 /*
2 * Copyright 2012 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package org.jboss.netty.handler.codec.http.multipart;
17
18 import org.jboss.netty.handler.codec.http.HttpChunk;
19 import org.jboss.netty.handler.codec.http.HttpConstants;
20 import org.jboss.netty.handler.codec.http.HttpHeaders;
21 import org.jboss.netty.handler.codec.http.HttpRequest;
22 import org.jboss.netty.util.internal.StringUtil;
23
24 import java.nio.charset.Charset;
25 import java.util.List;
26
27 /**
28 * This decoder will decode Body and can handle POST BODY (both multipart and standard).
29 */
30 public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
31 /**
32 * Does this request is a Multipart request
33 */
34 private final InterfaceHttpPostRequestDecoder decoder;
35
36 /**
37 *
38 * @param request the request to decode
39 * @throws NullPointerException for request
40 * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
41 */
42 public HttpPostRequestDecoder(HttpRequest request) throws ErrorDataDecoderException {
43 this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
44 request, HttpConstants.DEFAULT_CHARSET);
45 }
46
47 /**
48 *
49 * @param factory the factory used to create InterfaceHttpData
50 * @param request the request to decode
51 * @throws NullPointerException for request or factory
52 * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
53 */
54 public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request)
55 throws ErrorDataDecoderException {
56 this(factory, request, HttpConstants.DEFAULT_CHARSET);
57 }
58
59 /**
60 *
61 * @param factory the factory used to create InterfaceHttpData
62 * @param request the request to decode
63 * @param charset the charset to use as default
64 * @throws NullPointerException for request or charset or factory
65 * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
66 */
67 public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request,
68 Charset charset) throws ErrorDataDecoderException {
69 if (factory == null) {
70 throw new NullPointerException("factory");
71 }
72 if (request == null) {
73 throw new NullPointerException("request");
74 }
75 if (charset == null) {
76 throw new NullPointerException("charset");
77 }
78 // Fill default values
79 if (isMultipart(request)) {
80 decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
81 } else {
82 decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
83 }
84 }
85
86 /**
87 * states follow
88 * NOTSTARTED PREAMBLE (
89 * (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
90 * (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
91 * (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
92 * MIXEDCLOSEDELIMITER)*
93 * CLOSEDELIMITER)+ EPILOGUE
94 *
95 * First status is: NOSTARTED
96
97 Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header
98
99 --AaB03x => HEADERDELIMITER
100 content-disposition: form-data; name="field1" => DISPOSITION
101
102 Joe Blow => FIELD
103 --AaB03x => HEADERDELIMITER
104 content-disposition: form-data; name="pics" => DISPOSITION
105 Content-type: multipart/mixed, boundary=BbC04y
106
107 --BbC04y => MIXEDDELIMITER
108 Content-disposition: attachment; filename="file1.txt" => MIXEDDISPOSITION
109 Content-Type: text/plain
110
111 ... contents of file1.txt ... => MIXEDFILEUPLOAD
112 --BbC04y => MIXEDDELIMITER
113 Content-disposition: file; filename="file2.gif" => MIXEDDISPOSITION
114 Content-type: image/gif
115 Content-Transfer-Encoding: binary
116
117 ...contents of file2.gif... => MIXEDFILEUPLOAD
118 --BbC04y-- => MIXEDCLOSEDELIMITER
119 --AaB03x-- => CLOSEDELIMITER
120
121 Once CLOSEDELIMITER is found, last status is EPILOGUE
122 */
123 protected enum MultiPartStatus {
124 NOTSTARTED,
125 PREAMBLE,
126 HEADERDELIMITER,
127 DISPOSITION,
128 FIELD,
129 FILEUPLOAD,
130 MIXEDPREAMBLE,
131 MIXEDDELIMITER,
132 MIXEDDISPOSITION,
133 MIXEDFILEUPLOAD,
134 MIXEDCLOSEDELIMITER,
135 CLOSEDELIMITER,
136 PREEPILOGUE,
137 EPILOGUE
138 }
139
140 /**
141 * Check if the given request is a multipart request
142 *
143 * @return True if the request is a Multipart request
144 */
145 public static boolean isMultipart(HttpRequest request) throws ErrorDataDecoderException {
146 if (request.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) {
147 return getMultipartDataBoundary(request.headers().get(HttpHeaders.Names.CONTENT_TYPE)) != null;
148 } else {
149 return false;
150 }
151 }
152
153 /**
154 * Check from the request ContentType if this request is a Multipart request.
155 * @return an array of String if multipartDataBoundary exists with the multipartDataBoundary
156 * as first element, charset if any as second (missing if not set), else null
157 */
158 protected static String[] getMultipartDataBoundary(String contentType)
159 throws ErrorDataDecoderException {
160 // Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
161 String[] headerContentType = splitHeaderContentType(contentType);
162 if (headerContentType[0].toLowerCase().startsWith(
163 HttpHeaders.Values.MULTIPART_FORM_DATA)) {
164 int mrank = 1, crank = 2;
165 if (headerContentType[1].toLowerCase().startsWith(
166 HttpHeaders.Values.BOUNDARY.toString())) {
167 mrank = 1;
168 crank = 2;
169 } else if (headerContentType[2].toLowerCase().startsWith(
170 HttpHeaders.Values.BOUNDARY.toString())) {
171 mrank = 2;
172 crank = 1;
173 } else {
174 return null;
175 }
176 String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
177 if (boundary == null) {
178 throw new ErrorDataDecoderException("Needs a boundary value");
179 }
180 if (boundary.charAt(0) == '"') {
181 String bound = boundary.trim();
182 int index = bound.length() - 1;
183 if (bound.charAt(index) == '"') {
184 boundary = bound.substring(1, index);
185 }
186 }
187 if (headerContentType[crank].toLowerCase().startsWith(
188 HttpHeaders.Values.CHARSET.toString())) {
189 String charset = StringUtil.substringAfter(headerContentType[crank], '=');
190 if (charset != null) {
191 return new String[] {"--" + boundary, charset};
192 }
193 }
194 return new String[] {"--" + boundary};
195 }
196 return null;
197 }
198
199 /**
200 * True if this request is a Multipart request
201 * @return True if this request is a Multipart request
202 */
203 public boolean isMultipart() {
204 return decoder.isMultipart();
205 }
206
207 /**
208 * This method returns a List of all HttpDatas from body.<br>
209 *
210 * If chunked, all chunks must have been offered using offer() method.
211 * If not, NotEnoughDataDecoderException will be raised.
212 *
213 * @return the list of HttpDatas from Body part for POST method
214 * @throws NotEnoughDataDecoderException Need more chunks
215 */
216 public List<InterfaceHttpData> getBodyHttpDatas()
217 throws NotEnoughDataDecoderException {
218 return decoder.getBodyHttpDatas();
219 }
220
221 /**
222 * This method returns a List of all HttpDatas with the given name from body.<br>
223 *
224 * If chunked, all chunks must have been offered using offer() method.
225 * If not, NotEnoughDataDecoderException will be raised.
226
227 * @return All Body HttpDatas with the given name (ignore case)
228 * @throws NotEnoughDataDecoderException need more chunks
229 */
230 public List<InterfaceHttpData> getBodyHttpDatas(String name)
231 throws NotEnoughDataDecoderException {
232 return decoder.getBodyHttpDatas(name);
233 }
234
235 /**
236 * This method returns the first InterfaceHttpData with the given name from body.<br>
237 *
238 * If chunked, all chunks must have been offered using offer() method.
239 * If not, NotEnoughDataDecoderException will be raised.
240 *
241 * @return The first Body InterfaceHttpData with the given name (ignore case)
242 * @throws NotEnoughDataDecoderException need more chunks
243 */
244 public InterfaceHttpData getBodyHttpData(String name)
245 throws NotEnoughDataDecoderException {
246 return decoder.getBodyHttpData(name);
247 }
248
249 /**
250 * Initialized the internals from a new chunk
251 * @param chunk the new received chunk
252 * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
253 * other errors
254 */
255 public void offer(HttpChunk chunk) throws ErrorDataDecoderException {
256 decoder.offer(chunk);
257 }
258
259 /**
260 * True if at current status, there is an available decoded InterfaceHttpData from the Body.
261 *
262 * This method works for chunked and not chunked request.
263 *
264 * @return True if at current status, there is a decoded InterfaceHttpData
265 * @throws EndOfDataDecoderException No more data will be available
266 */
267 public boolean hasNext() throws EndOfDataDecoderException {
268 return decoder.hasNext();
269 }
270
271 /**
272 * Returns the next available InterfaceHttpData or null if, at the time it is called, there is no more
273 * available InterfaceHttpData. A subsequent call to offer(httpChunk) could enable more data.
274 *
275 * @return the next available InterfaceHttpData or null if none
276 * @throws EndOfDataDecoderException No more data will be available
277 */
278 public InterfaceHttpData next() throws EndOfDataDecoderException {
279 return decoder.next();
280 }
281
282 /**
283 * Clean all HttpDatas (on Disk) for the current request.
284 */
285 public void cleanFiles() {
286 decoder.cleanFiles();
287 }
288
289 /**
290 * Remove the given FileUpload from the list of FileUploads to clean
291 */
292 public void removeHttpDataFromClean(InterfaceHttpData data) {
293 decoder.removeHttpDataFromClean(data);
294 }
295
296 /**
297 * Split the very first line (Content-Type value) in 3 Strings
298 * @return the array of 3 Strings
299 */
300 private static String[] splitHeaderContentType(String sb) {
301 int aStart;
302 int aEnd;
303 int bStart;
304 int bEnd;
305 int cStart;
306 int cEnd;
307 aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
308 aEnd = sb.indexOf(';');
309 if (aEnd == -1) {
310 return new String[] { sb, "", "" };
311 }
312 bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
313 if (sb.charAt(aEnd - 1) == ' ') {
314 aEnd--;
315 }
316 bEnd = sb.indexOf(';', bStart);
317 if (bEnd == -1) {
318 bEnd = HttpPostBodyUtil.findEndOfString(sb);
319 return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" };
320 }
321 cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
322 if (sb.charAt(bEnd - 1) == ' ') {
323 bEnd--;
324 }
325 cEnd = HttpPostBodyUtil.findEndOfString(sb);
326 return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd) };
327 }
328
329 /**
330 * Exception when try reading data from request in chunked format, and not enough
331 * data are available (need more chunks)
332 */
333 public static class NotEnoughDataDecoderException extends Exception {
334 private static final long serialVersionUID = -7846841864603865638L;
335
336 public NotEnoughDataDecoderException() {
337 }
338
339 public NotEnoughDataDecoderException(String msg) {
340 super(msg);
341 }
342
343 public NotEnoughDataDecoderException(Throwable cause) {
344 super(cause);
345 }
346
347 public NotEnoughDataDecoderException(String msg, Throwable cause) {
348 super(msg, cause);
349 }
350 }
351
352 /**
353 * Exception when the body is fully decoded, even if there is still data
354 */
355 public static class EndOfDataDecoderException extends Exception {
356 private static final long serialVersionUID = 1336267941020800769L;
357 }
358
359 /**
360 * Exception when an error occurs while decoding
361 */
362 public static class ErrorDataDecoderException extends Exception {
363 private static final long serialVersionUID = 5020247425493164465L;
364
365 public ErrorDataDecoderException() {
366 }
367
368 public ErrorDataDecoderException(String msg) {
369 super(msg);
370 }
371
372 public ErrorDataDecoderException(Throwable cause) {
373 super(cause);
374 }
375
376 public ErrorDataDecoderException(String msg, Throwable cause) {
377 super(msg, cause);
378 }
379 }
380 }