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    *   https://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 io.netty.handler.codec.http.multipart;
17  
18  import io.netty.handler.codec.DecoderException;
19  import io.netty.handler.codec.http.HttpConstants;
20  import io.netty.handler.codec.http.HttpContent;
21  import io.netty.handler.codec.http.HttpHeaderNames;
22  import io.netty.handler.codec.http.HttpHeaderValues;
23  import io.netty.handler.codec.http.HttpRequest;
24  import io.netty.util.internal.ObjectUtil;
25  import io.netty.util.internal.StringUtil;
26  
27  import java.nio.charset.Charset;
28  import java.util.List;
29  
30  /**
31   * This decoder will decode Body and can handle POST BODY.
32   *
33   * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
34   *
35   */
36  public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
37  
38      static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;
39  
40      private final InterfaceHttpPostRequestDecoder decoder;
41  
42      /**
43       *
44       * @param request
45       *            the request to decode
46       * @throws NullPointerException
47       *             for request
48       * @throws ErrorDataDecoderException
49       *             if the default charset was wrong when decoding or other
50       *             errors
51       */
52      public HttpPostRequestDecoder(HttpRequest request) {
53          this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
54      }
55  
56      /**
57       *
58       * @param factory
59       *            the factory used to create InterfaceHttpData
60       * @param request
61       *            the request to decode
62       * @throws NullPointerException
63       *             for request or factory
64       * @throws ErrorDataDecoderException
65       *             if the default charset was wrong when decoding or other
66       *             errors
67       */
68      public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) {
69          this(factory, request, HttpConstants.DEFAULT_CHARSET);
70      }
71  
72      /**
73       *
74       * @param factory
75       *            the factory used to create InterfaceHttpData
76       * @param request
77       *            the request to decode
78       * @param charset
79       *            the charset to use as default
80       * @throws NullPointerException
81       *             for request or charset or factory
82       * @throws ErrorDataDecoderException
83       *             if the default charset was wrong when decoding or other
84       *             errors
85       */
86      public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
87          ObjectUtil.checkNotNull(factory, "factory");
88          ObjectUtil.checkNotNull(request, "request");
89          ObjectUtil.checkNotNull(charset, "charset");
90  
91          // Fill default values
92          if (isMultipart(request)) {
93              decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
94          } else {
95              decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
96          }
97      }
98  
99      /**
100      * states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD |
101      * FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER
102      * MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+
103      * EPILOGUE
104      *
105      * First getStatus is: NOSTARTED
106      *
107      * Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header
108      *
109      * --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1"
110      * => DISPOSITION
111      *
112      * Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition:
113      * form-data; name="pics" => DISPOSITION Content-type: multipart/mixed,
114      * boundary=BbC04y
115      *
116      * --BbC04y => MIXEDDELIMITER Content-disposition: attachment;
117      * filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain
118      *
119      * ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y =>
120      * MIXEDDELIMITER Content-disposition: file; filename="file2.gif" =>
121      * MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding:
122      * binary
123      *
124      * ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- =>
125      * MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER
126      *
127      * Once CLOSEDELIMITER is found, last getStatus is EPILOGUE
128      */
129     protected enum MultiPartStatus {
130         NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER,
131         MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE
132     }
133 
134     /**
135      * Check if the given request is a multipart request
136      * @return True if the request is a Multipart request
137      */
138     public static boolean isMultipart(HttpRequest request) {
139         String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
140         if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
141             return getMultipartDataBoundary(mimeType) != null;
142         }
143         return false;
144     }
145 
146     /**
147      * Check from the request ContentType if this request is a Multipart request.
148      * @return an array of String if multipartDataBoundary exists with the multipartDataBoundary
149      * as first element, charset if any as second (missing if not set), else null
150      */
151     protected static String[] getMultipartDataBoundary(String contentType) {
152         // Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
153         String[] headerContentType = splitHeaderContentType(contentType);
154         final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString();
155         if (headerContentType[0].regionMatches(true, 0, multiPartHeader, 0 , multiPartHeader.length())) {
156             int mrank;
157             int crank;
158             final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString();
159             if (headerContentType[1].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {
160                 mrank = 1;
161                 crank = 2;
162             } else if (headerContentType[2].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) {
163                 mrank = 2;
164                 crank = 1;
165             } else {
166                 return null;
167             }
168             String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
169             if (boundary == null) {
170                 throw new ErrorDataDecoderException("Needs a boundary value");
171             }
172             if (boundary.charAt(0) == '"') {
173                 String bound = boundary.trim();
174                 int index = bound.length() - 1;
175                 if (bound.charAt(index) == '"') {
176                     boundary = bound.substring(1, index);
177                 }
178             }
179             final String charsetHeader = HttpHeaderValues.CHARSET.toString();
180             if (headerContentType[crank].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
181                 String charset = StringUtil.substringAfter(headerContentType[crank], '=');
182                 if (charset != null) {
183                     return new String[] {"--" + boundary, charset};
184                 }
185             }
186             return new String[] {"--" + boundary};
187         }
188         return null;
189     }
190 
191     @Override
192     public boolean isMultipart() {
193         return decoder.isMultipart();
194     }
195 
196     @Override
197     public void setDiscardThreshold(int discardThreshold) {
198         decoder.setDiscardThreshold(discardThreshold);
199     }
200 
201     @Override
202     public int getDiscardThreshold() {
203         return decoder.getDiscardThreshold();
204     }
205 
206     @Override
207     public List<InterfaceHttpData> getBodyHttpDatas() {
208         return decoder.getBodyHttpDatas();
209     }
210 
211     @Override
212     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
213         return decoder.getBodyHttpDatas(name);
214     }
215 
216     @Override
217     public InterfaceHttpData getBodyHttpData(String name) {
218         return decoder.getBodyHttpData(name);
219     }
220 
221     @Override
222     public InterfaceHttpPostRequestDecoder offer(HttpContent content) {
223         return decoder.offer(content);
224     }
225 
226     @Override
227     public boolean hasNext() {
228         return decoder.hasNext();
229     }
230 
231     @Override
232     public InterfaceHttpData next() {
233         return decoder.next();
234     }
235 
236     @Override
237     public InterfaceHttpData currentPartialHttpData() {
238         return decoder.currentPartialHttpData();
239     }
240 
241     @Override
242     public void destroy() {
243         decoder.destroy();
244     }
245 
246     @Override
247     public void cleanFiles() {
248         decoder.cleanFiles();
249     }
250 
251     @Override
252     public void removeHttpDataFromClean(InterfaceHttpData data) {
253         decoder.removeHttpDataFromClean(data);
254     }
255 
256     /**
257      * Split the very first line (Content-Type value) in 3 Strings
258      *
259      * @return the array of 3 Strings
260      */
261     private static String[] splitHeaderContentType(String sb) {
262         int aStart;
263         int aEnd;
264         int bStart;
265         int bEnd;
266         int cStart;
267         int cEnd;
268         aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
269         aEnd =  sb.indexOf(';');
270         if (aEnd == -1) {
271             return new String[] { sb, "", "" };
272         }
273         bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
274         if (sb.charAt(aEnd - 1) == ' ') {
275             aEnd--;
276         }
277         bEnd =  sb.indexOf(';', bStart);
278         if (bEnd == -1) {
279             bEnd = HttpPostBodyUtil.findEndOfString(sb);
280             return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), "" };
281         }
282         cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
283         if (sb.charAt(bEnd - 1) == ' ') {
284             bEnd--;
285         }
286         cEnd = HttpPostBodyUtil.findEndOfString(sb);
287         return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd) };
288     }
289 
290     /**
291      * Exception when try reading data from request in chunked format, and not
292      * enough data are available (need more chunks)
293      */
294     public static class NotEnoughDataDecoderException extends DecoderException {
295         private static final long serialVersionUID = -7846841864603865638L;
296 
297         public NotEnoughDataDecoderException() {
298         }
299 
300         public NotEnoughDataDecoderException(String msg) {
301             super(msg);
302         }
303 
304         public NotEnoughDataDecoderException(Throwable cause) {
305             super(cause);
306         }
307 
308         public NotEnoughDataDecoderException(String msg, Throwable cause) {
309             super(msg, cause);
310         }
311     }
312 
313     /**
314      * Exception when the body is fully decoded, even if there is still data
315      */
316     public static class EndOfDataDecoderException extends DecoderException {
317         private static final long serialVersionUID = 1336267941020800769L;
318     }
319 
320     /**
321      * Exception when an error occurs while decoding
322      */
323     public static class ErrorDataDecoderException extends DecoderException {
324         private static final long serialVersionUID = 5020247425493164465L;
325 
326         public ErrorDataDecoderException() {
327         }
328 
329         public ErrorDataDecoderException(String msg) {
330             super(msg);
331         }
332 
333         public ErrorDataDecoderException(Throwable cause) {
334             super(cause);
335         }
336 
337         public ErrorDataDecoderException(String msg, Throwable cause) {
338             super(msg, cause);
339         }
340     }
341 }