View Javadoc
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 }