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 io.netty.handler.codec.http.multipart;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.handler.codec.http.HttpConstants;
20  import io.netty.handler.codec.http.HttpContent;
21  import io.netty.handler.codec.http.HttpRequest;
22  import io.netty.handler.codec.http.LastHttpContent;
23  import io.netty.handler.codec.http.QueryStringDecoder;
24  import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
25  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
26  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
27  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
28  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
29  
30  import java.io.IOException;
31  import java.nio.charset.Charset;
32  import java.util.ArrayList;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.TreeMap;
36  
37  import static io.netty.buffer.Unpooled.*;
38  import static io.netty.util.internal.ObjectUtil.*;
39  
40  /**
41   * This decoder will decode Body and can handle POST BODY.
42   *
43   * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
44   *
45   */
46  public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestDecoder {
47  
48      /**
49       * Factory used to create InterfaceHttpData
50       */
51      private final HttpDataFactory factory;
52  
53      /**
54       * Request to decode
55       */
56      private final HttpRequest request;
57  
58      /**
59       * Default charset to use
60       */
61      private final Charset charset;
62  
63      /**
64       * Does the last chunk already received
65       */
66      private boolean isLastChunk;
67  
68      /**
69       * HttpDatas from Body
70       */
71      private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
72  
73      /**
74       * HttpDatas as Map from Body
75       */
76      private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
77              CaseIgnoringComparator.INSTANCE);
78  
79      /**
80       * The current channelBuffer
81       */
82      private ByteBuf undecodedChunk;
83  
84      /**
85       * Body HttpDatas current position
86       */
87      private int bodyListHttpDataRank;
88  
89      /**
90       * Current getStatus
91       */
92      private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
93  
94      /**
95       * The current Attribute that is currently in decode process
96       */
97      private Attribute currentAttribute;
98  
99      private boolean destroyed;
100 
101     private int discardThreshold = HttpPostRequestDecoder.DEFAULT_DISCARD_THRESHOLD;
102 
103     /**
104      *
105      * @param request
106      *            the request to decode
107      * @throws NullPointerException
108      *             for request
109      * @throws ErrorDataDecoderException
110      *             if the default charset was wrong when decoding or other
111      *             errors
112      */
113     public HttpPostStandardRequestDecoder(HttpRequest request) {
114         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
115     }
116 
117     /**
118      *
119      * @param factory
120      *            the factory used to create InterfaceHttpData
121      * @param request
122      *            the request to decode
123      * @throws NullPointerException
124      *             for request or factory
125      * @throws ErrorDataDecoderException
126      *             if the default charset was wrong when decoding or other
127      *             errors
128      */
129     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) {
130         this(factory, request, HttpConstants.DEFAULT_CHARSET);
131     }
132 
133     /**
134      *
135      * @param factory
136      *            the factory used to create InterfaceHttpData
137      * @param request
138      *            the request to decode
139      * @param charset
140      *            the charset to use as default
141      * @throws NullPointerException
142      *             for request or charset or factory
143      * @throws ErrorDataDecoderException
144      *             if the default charset was wrong when decoding or other
145      *             errors
146      */
147     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
148         this.request = checkNotNull(request, "request");
149         this.charset = checkNotNull(charset, "charset");
150         this.factory = checkNotNull(factory, "factory");
151         if (request instanceof HttpContent) {
152             // Offer automatically if the given request is als type of HttpContent
153             // See #1089
154             offer((HttpContent) request);
155         } else {
156             undecodedChunk = buffer();
157             parseBody();
158         }
159     }
160 
161     private void checkDestroyed() {
162         if (destroyed) {
163             throw new IllegalStateException(HttpPostStandardRequestDecoder.class.getSimpleName()
164                     + " was destroyed already");
165         }
166     }
167 
168     /**
169      * True if this request is a Multipart request
170      *
171      * @return True if this request is a Multipart request
172      */
173     @Override
174     public boolean isMultipart() {
175         checkDestroyed();
176         return false;
177     }
178 
179     /**
180      * Set the amount of bytes after which read bytes in the buffer should be discarded.
181      * Setting this lower gives lower memory usage but with the overhead of more memory copies.
182      * Use {@code 0} to disable it.
183      */
184     @Override
185     public void setDiscardThreshold(int discardThreshold) {
186         this.discardThreshold = checkPositiveOrZero(discardThreshold, "discardThreshold");
187     }
188 
189     /**
190      * Return the threshold in bytes after which read data in the buffer should be discarded.
191      */
192     @Override
193     public int getDiscardThreshold() {
194         return discardThreshold;
195     }
196 
197     /**
198      * This getMethod returns a List of all HttpDatas from body.<br>
199      *
200      * If chunked, all chunks must have been offered using offer() getMethod. If
201      * not, NotEnoughDataDecoderException will be raised.
202      *
203      * @return the list of HttpDatas from Body part for POST getMethod
204      * @throws NotEnoughDataDecoderException
205      *             Need more chunks
206      */
207     @Override
208     public List<InterfaceHttpData> getBodyHttpDatas() {
209         checkDestroyed();
210 
211         if (!isLastChunk) {
212             throw new NotEnoughDataDecoderException();
213         }
214         return bodyListHttpData;
215     }
216 
217     /**
218      * This getMethod returns a List of all HttpDatas with the given name from
219      * body.<br>
220      *
221      * If chunked, all chunks must have been offered using offer() getMethod. If
222      * not, NotEnoughDataDecoderException will be raised.
223      *
224      * @return All Body HttpDatas with the given name (ignore case)
225      * @throws NotEnoughDataDecoderException
226      *             need more chunks
227      */
228     @Override
229     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
230         checkDestroyed();
231 
232         if (!isLastChunk) {
233             throw new NotEnoughDataDecoderException();
234         }
235         return bodyMapHttpData.get(name);
236     }
237 
238     /**
239      * This getMethod returns the first InterfaceHttpData with the given name from
240      * body.<br>
241      *
242      * If chunked, all chunks must have been offered using offer() getMethod. If
243      * not, NotEnoughDataDecoderException will be raised.
244      *
245      * @return The first Body InterfaceHttpData with the given name (ignore
246      *         case)
247      * @throws NotEnoughDataDecoderException
248      *             need more chunks
249      */
250     @Override
251     public InterfaceHttpData getBodyHttpData(String name) {
252         checkDestroyed();
253 
254         if (!isLastChunk) {
255             throw new NotEnoughDataDecoderException();
256         }
257         List<InterfaceHttpData> list = bodyMapHttpData.get(name);
258         if (list != null) {
259             return list.get(0);
260         }
261         return null;
262     }
263 
264     /**
265      * Initialized the internals from a new chunk
266      *
267      * @param content
268      *            the new received chunk
269      * @throws ErrorDataDecoderException
270      *             if there is a problem with the charset decoding or other
271      *             errors
272      */
273     @Override
274     public HttpPostStandardRequestDecoder offer(HttpContent content) {
275         checkDestroyed();
276 
277         // Maybe we should better not copy here for performance reasons but this will need
278         // more care by the caller to release the content in a correct manner later
279         // So maybe something to optimize on a later stage
280         ByteBuf buf = content.content();
281         if (undecodedChunk == null) {
282             undecodedChunk = buf.copy();
283         } else {
284             undecodedChunk.writeBytes(buf);
285         }
286         if (content instanceof LastHttpContent) {
287             isLastChunk = true;
288         }
289         parseBody();
290         if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
291             undecodedChunk.discardReadBytes();
292         }
293         return this;
294     }
295 
296     /**
297      * True if at current getStatus, there is an available decoded
298      * InterfaceHttpData from the Body.
299      *
300      * This getMethod works for chunked and not chunked request.
301      *
302      * @return True if at current getStatus, there is a decoded InterfaceHttpData
303      * @throws EndOfDataDecoderException
304      *             No more data will be available
305      */
306     @Override
307     public boolean hasNext() {
308         checkDestroyed();
309 
310         if (currentStatus == MultiPartStatus.EPILOGUE) {
311             // OK except if end of list
312             if (bodyListHttpDataRank >= bodyListHttpData.size()) {
313                 throw new EndOfDataDecoderException();
314             }
315         }
316         return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
317     }
318 
319     /**
320      * Returns the next available InterfaceHttpData or null if, at the time it
321      * is called, there is no more available InterfaceHttpData. A subsequent
322      * call to offer(httpChunk) could enable more data.
323      *
324      * Be sure to call {@link InterfaceHttpData#release()} after you are done
325      * with processing to make sure to not leak any resources
326      *
327      * @return the next available InterfaceHttpData or null if none
328      * @throws EndOfDataDecoderException
329      *             No more data will be available
330      */
331     @Override
332     public InterfaceHttpData next() {
333         checkDestroyed();
334 
335         if (hasNext()) {
336             return bodyListHttpData.get(bodyListHttpDataRank++);
337         }
338         return null;
339     }
340 
341     /**
342      * This getMethod will parse as much as possible data and fill the list and map
343      *
344      * @throws ErrorDataDecoderException
345      *             if there is a problem with the charset decoding or other
346      *             errors
347      */
348     private void parseBody() {
349         if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
350             if (isLastChunk) {
351                 currentStatus = MultiPartStatus.EPILOGUE;
352             }
353             return;
354         }
355         parseBodyAttributes();
356     }
357 
358     /**
359      * Utility function to add a new decoded data
360      */
361     protected void addHttpData(InterfaceHttpData data) {
362         if (data == null) {
363             return;
364         }
365         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
366         if (datas == null) {
367             datas = new ArrayList<InterfaceHttpData>(1);
368             bodyMapHttpData.put(data.getName(), datas);
369         }
370         datas.add(data);
371         bodyListHttpData.add(data);
372     }
373 
374     /**
375      * This getMethod fill the map and list with as much Attribute as possible from
376      * Body in not Multipart mode.
377      *
378      * @throws ErrorDataDecoderException
379      *             if there is a problem with the charset decoding or other
380      *             errors
381      */
382     private void parseBodyAttributesStandard() {
383         int firstpos = undecodedChunk.readerIndex();
384         int currentpos = firstpos;
385         int equalpos;
386         int ampersandpos;
387         if (currentStatus == MultiPartStatus.NOTSTARTED) {
388             currentStatus = MultiPartStatus.DISPOSITION;
389         }
390         boolean contRead = true;
391         try {
392             while (undecodedChunk.isReadable() && contRead) {
393                 char read = (char) undecodedChunk.readUnsignedByte();
394                 currentpos++;
395                 switch (currentStatus) {
396                 case DISPOSITION:// search '='
397                     if (read == '=') {
398                         currentStatus = MultiPartStatus.FIELD;
399                         equalpos = currentpos - 1;
400                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
401                                 charset);
402                         currentAttribute = factory.createAttribute(request, key);
403                         firstpos = currentpos;
404                     } else if (read == '&') { // special empty FIELD
405                         currentStatus = MultiPartStatus.DISPOSITION;
406                         ampersandpos = currentpos - 1;
407                         String key = decodeAttribute(
408                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
409                         currentAttribute = factory.createAttribute(request, key);
410                         currentAttribute.setValue(""); // empty
411                         addHttpData(currentAttribute);
412                         currentAttribute = null;
413                         firstpos = currentpos;
414                         contRead = true;
415                     }
416                     break;
417                 case FIELD:// search '&' or end of line
418                     if (read == '&') {
419                         currentStatus = MultiPartStatus.DISPOSITION;
420                         ampersandpos = currentpos - 1;
421                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
422                         firstpos = currentpos;
423                         contRead = true;
424                     } else if (read == HttpConstants.CR) {
425                         if (undecodedChunk.isReadable()) {
426                             read = (char) undecodedChunk.readUnsignedByte();
427                             currentpos++;
428                             if (read == HttpConstants.LF) {
429                                 currentStatus = MultiPartStatus.PREEPILOGUE;
430                                 ampersandpos = currentpos - 2;
431                                 setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
432                                 firstpos = currentpos;
433                                 contRead = false;
434                             } else {
435                                 // Error
436                                 throw new ErrorDataDecoderException("Bad end of line");
437                             }
438                         } else {
439                             currentpos--;
440                         }
441                     } else if (read == HttpConstants.LF) {
442                         currentStatus = MultiPartStatus.PREEPILOGUE;
443                         ampersandpos = currentpos - 1;
444                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
445                         firstpos = currentpos;
446                         contRead = false;
447                     }
448                     break;
449                 default:
450                     // just stop
451                     contRead = false;
452                 }
453             }
454             if (isLastChunk && currentAttribute != null) {
455                 // special case
456                 ampersandpos = currentpos;
457                 if (ampersandpos > firstpos) {
458                     setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
459                 } else if (!currentAttribute.isCompleted()) {
460                     setFinalBuffer(EMPTY_BUFFER);
461                 }
462                 firstpos = currentpos;
463                 currentStatus = MultiPartStatus.EPILOGUE;
464                 undecodedChunk.readerIndex(firstpos);
465                 return;
466             }
467             if (contRead && currentAttribute != null) {
468                 // reset index except if to continue in case of FIELD getStatus
469                 if (currentStatus == MultiPartStatus.FIELD) {
470                     currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos),
471                                                 false);
472                     firstpos = currentpos;
473                 }
474                 undecodedChunk.readerIndex(firstpos);
475             } else {
476                 // end of line or end of block so keep index to last valid position
477                 undecodedChunk.readerIndex(firstpos);
478             }
479         } catch (ErrorDataDecoderException e) {
480             // error while decoding
481             undecodedChunk.readerIndex(firstpos);
482             throw e;
483         } catch (IOException e) {
484             // error while decoding
485             undecodedChunk.readerIndex(firstpos);
486             throw new ErrorDataDecoderException(e);
487         }
488     }
489 
490     /**
491      * This getMethod fill the map and list with as much Attribute as possible from
492      * Body in not Multipart mode.
493      *
494      * @throws ErrorDataDecoderException
495      *             if there is a problem with the charset decoding or other
496      *             errors
497      */
498     private void parseBodyAttributes() {
499         if (!undecodedChunk.hasArray()) {
500             parseBodyAttributesStandard();
501             return;
502         }
503         SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
504         int firstpos = undecodedChunk.readerIndex();
505         int currentpos = firstpos;
506         int equalpos;
507         int ampersandpos;
508         if (currentStatus == MultiPartStatus.NOTSTARTED) {
509             currentStatus = MultiPartStatus.DISPOSITION;
510         }
511         boolean contRead = true;
512         try {
513             loop: while (sao.pos < sao.limit) {
514                 char read = (char) (sao.bytes[sao.pos++] & 0xFF);
515                 currentpos++;
516                 switch (currentStatus) {
517                 case DISPOSITION:// search '='
518                     if (read == '=') {
519                         currentStatus = MultiPartStatus.FIELD;
520                         equalpos = currentpos - 1;
521                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
522                                 charset);
523                         currentAttribute = factory.createAttribute(request, key);
524                         firstpos = currentpos;
525                     } else if (read == '&') { // special empty FIELD
526                         currentStatus = MultiPartStatus.DISPOSITION;
527                         ampersandpos = currentpos - 1;
528                         String key = decodeAttribute(
529                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
530                         currentAttribute = factory.createAttribute(request, key);
531                         currentAttribute.setValue(""); // empty
532                         addHttpData(currentAttribute);
533                         currentAttribute = null;
534                         firstpos = currentpos;
535                         contRead = true;
536                     }
537                     break;
538                 case FIELD:// search '&' or end of line
539                     if (read == '&') {
540                         currentStatus = MultiPartStatus.DISPOSITION;
541                         ampersandpos = currentpos - 1;
542                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
543                         firstpos = currentpos;
544                         contRead = true;
545                     } else if (read == HttpConstants.CR) {
546                         if (sao.pos < sao.limit) {
547                             read = (char) (sao.bytes[sao.pos++] & 0xFF);
548                             currentpos++;
549                             if (read == HttpConstants.LF) {
550                                 currentStatus = MultiPartStatus.PREEPILOGUE;
551                                 ampersandpos = currentpos - 2;
552                                 sao.setReadPosition(0);
553                                 setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
554                                 firstpos = currentpos;
555                                 contRead = false;
556                                 break loop;
557                             } else {
558                                 // Error
559                                 sao.setReadPosition(0);
560                                 throw new ErrorDataDecoderException("Bad end of line");
561                             }
562                         } else {
563                             if (sao.limit > 0) {
564                                 currentpos--;
565                             }
566                         }
567                     } else if (read == HttpConstants.LF) {
568                         currentStatus = MultiPartStatus.PREEPILOGUE;
569                         ampersandpos = currentpos - 1;
570                         sao.setReadPosition(0);
571                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
572                         firstpos = currentpos;
573                         contRead = false;
574                         break loop;
575                     }
576                     break;
577                 default:
578                     // just stop
579                     sao.setReadPosition(0);
580                     contRead = false;
581                     break loop;
582                 }
583             }
584             if (isLastChunk && currentAttribute != null) {
585                 // special case
586                 ampersandpos = currentpos;
587                 if (ampersandpos > firstpos) {
588                     setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
589                 } else if (!currentAttribute.isCompleted()) {
590                     setFinalBuffer(EMPTY_BUFFER);
591                 }
592                 firstpos = currentpos;
593                 currentStatus = MultiPartStatus.EPILOGUE;
594                 undecodedChunk.readerIndex(firstpos);
595                 return;
596             }
597             if (contRead && currentAttribute != null) {
598                 // reset index except if to continue in case of FIELD getStatus
599                 if (currentStatus == MultiPartStatus.FIELD) {
600                     currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos),
601                                                 false);
602                     firstpos = currentpos;
603                 }
604                 undecodedChunk.readerIndex(firstpos);
605             } else {
606                 // end of line or end of block so keep index to last valid position
607                 undecodedChunk.readerIndex(firstpos);
608             }
609         } catch (ErrorDataDecoderException e) {
610             // error while decoding
611             undecodedChunk.readerIndex(firstpos);
612             throw e;
613         } catch (IOException e) {
614             // error while decoding
615             undecodedChunk.readerIndex(firstpos);
616             throw new ErrorDataDecoderException(e);
617         } catch (IllegalArgumentException e) {
618             // error while decoding
619             undecodedChunk.readerIndex(firstpos);
620             throw new ErrorDataDecoderException(e);
621         }
622     }
623 
624     private void setFinalBuffer(ByteBuf buffer) throws IOException {
625         currentAttribute.addContent(buffer, true);
626         String value = decodeAttribute(currentAttribute.getByteBuf().toString(charset), charset);
627         currentAttribute.setValue(value);
628         addHttpData(currentAttribute);
629         currentAttribute = null;
630     }
631 
632     /**
633      * Decode component
634      *
635      * @return the decoded component
636      */
637     private static String decodeAttribute(String s, Charset charset) {
638         try {
639             return QueryStringDecoder.decodeComponent(s, charset);
640         } catch (IllegalArgumentException e) {
641             throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e);
642         }
643     }
644 
645     /**
646      * Destroy the {@link HttpPostStandardRequestDecoder} and release all it resources. After this method
647      * was called it is not possible to operate on it anymore.
648      */
649     @Override
650     public void destroy() {
651         checkDestroyed();
652         cleanFiles();
653         destroyed = true;
654 
655         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
656             undecodedChunk.release();
657             undecodedChunk = null;
658         }
659 
660         // release all data which was not yet pulled
661         for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
662             bodyListHttpData.get(i).release();
663         }
664     }
665 
666     /**
667      * Clean all HttpDatas (on Disk) for the current request.
668      */
669     @Override
670     public void cleanFiles() {
671         checkDestroyed();
672 
673         factory.cleanRequestHttpDatas(request);
674     }
675 
676     /**
677      * Remove the given FileUpload from the list of FileUploads to clean
678      */
679     @Override
680     public void removeHttpDataFromClean(InterfaceHttpData data) {
681         checkDestroyed();
682 
683         factory.removeHttpDataFromClean(request, data);
684     }
685 }