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     @Override
342     public InterfaceHttpData currentPartialHttpData() {
343         return currentAttribute;
344     }
345 
346     /**
347      * This getMethod will parse as much as possible data and fill the list and map
348      *
349      * @throws ErrorDataDecoderException
350      *             if there is a problem with the charset decoding or other
351      *             errors
352      */
353     private void parseBody() {
354         if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
355             if (isLastChunk) {
356                 currentStatus = MultiPartStatus.EPILOGUE;
357             }
358             return;
359         }
360         parseBodyAttributes();
361     }
362 
363     /**
364      * Utility function to add a new decoded data
365      */
366     protected void addHttpData(InterfaceHttpData data) {
367         if (data == null) {
368             return;
369         }
370         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
371         if (datas == null) {
372             datas = new ArrayList<InterfaceHttpData>(1);
373             bodyMapHttpData.put(data.getName(), datas);
374         }
375         datas.add(data);
376         bodyListHttpData.add(data);
377     }
378 
379     /**
380      * This getMethod fill the map and list with as much Attribute as possible from
381      * Body in not Multipart mode.
382      *
383      * @throws ErrorDataDecoderException
384      *             if there is a problem with the charset decoding or other
385      *             errors
386      */
387     private void parseBodyAttributesStandard() {
388         int firstpos = undecodedChunk.readerIndex();
389         int currentpos = firstpos;
390         int equalpos;
391         int ampersandpos;
392         if (currentStatus == MultiPartStatus.NOTSTARTED) {
393             currentStatus = MultiPartStatus.DISPOSITION;
394         }
395         boolean contRead = true;
396         try {
397             while (undecodedChunk.isReadable() && contRead) {
398                 char read = (char) undecodedChunk.readUnsignedByte();
399                 currentpos++;
400                 switch (currentStatus) {
401                 case DISPOSITION:// search '='
402                     if (read == '=') {
403                         currentStatus = MultiPartStatus.FIELD;
404                         equalpos = currentpos - 1;
405                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
406                                 charset);
407                         currentAttribute = factory.createAttribute(request, key);
408                         firstpos = currentpos;
409                     } else if (read == '&') { // special empty FIELD
410                         currentStatus = MultiPartStatus.DISPOSITION;
411                         ampersandpos = currentpos - 1;
412                         String key = decodeAttribute(
413                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
414                         currentAttribute = factory.createAttribute(request, key);
415                         currentAttribute.setValue(""); // empty
416                         addHttpData(currentAttribute);
417                         currentAttribute = null;
418                         firstpos = currentpos;
419                         contRead = true;
420                     }
421                     break;
422                 case FIELD:// search '&' or end of line
423                     if (read == '&') {
424                         currentStatus = MultiPartStatus.DISPOSITION;
425                         ampersandpos = currentpos - 1;
426                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
427                         firstpos = currentpos;
428                         contRead = true;
429                     } else if (read == HttpConstants.CR) {
430                         if (undecodedChunk.isReadable()) {
431                             read = (char) undecodedChunk.readUnsignedByte();
432                             currentpos++;
433                             if (read == HttpConstants.LF) {
434                                 currentStatus = MultiPartStatus.PREEPILOGUE;
435                                 ampersandpos = currentpos - 2;
436                                 setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
437                                 firstpos = currentpos;
438                                 contRead = false;
439                             } else {
440                                 // Error
441                                 throw new ErrorDataDecoderException("Bad end of line");
442                             }
443                         } else {
444                             currentpos--;
445                         }
446                     } else if (read == HttpConstants.LF) {
447                         currentStatus = MultiPartStatus.PREEPILOGUE;
448                         ampersandpos = currentpos - 1;
449                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
450                         firstpos = currentpos;
451                         contRead = false;
452                     }
453                     break;
454                 default:
455                     // just stop
456                     contRead = false;
457                 }
458             }
459             if (isLastChunk && currentAttribute != null) {
460                 // special case
461                 ampersandpos = currentpos;
462                 if (ampersandpos > firstpos) {
463                     setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
464                 } else if (!currentAttribute.isCompleted()) {
465                     setFinalBuffer(EMPTY_BUFFER);
466                 }
467                 firstpos = currentpos;
468                 currentStatus = MultiPartStatus.EPILOGUE;
469                 undecodedChunk.readerIndex(firstpos);
470                 return;
471             }
472             if (contRead && currentAttribute != null) {
473                 // reset index except if to continue in case of FIELD getStatus
474                 if (currentStatus == MultiPartStatus.FIELD) {
475                     currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos),
476                                                 false);
477                     firstpos = currentpos;
478                 }
479                 undecodedChunk.readerIndex(firstpos);
480             } else {
481                 // end of line or end of block so keep index to last valid position
482                 undecodedChunk.readerIndex(firstpos);
483             }
484         } catch (ErrorDataDecoderException e) {
485             // error while decoding
486             undecodedChunk.readerIndex(firstpos);
487             throw e;
488         } catch (IOException e) {
489             // error while decoding
490             undecodedChunk.readerIndex(firstpos);
491             throw new ErrorDataDecoderException(e);
492         }
493     }
494 
495     /**
496      * This getMethod fill the map and list with as much Attribute as possible from
497      * Body in not Multipart mode.
498      *
499      * @throws ErrorDataDecoderException
500      *             if there is a problem with the charset decoding or other
501      *             errors
502      */
503     private void parseBodyAttributes() {
504         if (!undecodedChunk.hasArray()) {
505             parseBodyAttributesStandard();
506             return;
507         }
508         SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
509         int firstpos = undecodedChunk.readerIndex();
510         int currentpos = firstpos;
511         int equalpos;
512         int ampersandpos;
513         if (currentStatus == MultiPartStatus.NOTSTARTED) {
514             currentStatus = MultiPartStatus.DISPOSITION;
515         }
516         boolean contRead = true;
517         try {
518             loop: while (sao.pos < sao.limit) {
519                 char read = (char) (sao.bytes[sao.pos++] & 0xFF);
520                 currentpos++;
521                 switch (currentStatus) {
522                 case DISPOSITION:// search '='
523                     if (read == '=') {
524                         currentStatus = MultiPartStatus.FIELD;
525                         equalpos = currentpos - 1;
526                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
527                                 charset);
528                         currentAttribute = factory.createAttribute(request, key);
529                         firstpos = currentpos;
530                     } else if (read == '&') { // special empty FIELD
531                         currentStatus = MultiPartStatus.DISPOSITION;
532                         ampersandpos = currentpos - 1;
533                         String key = decodeAttribute(
534                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
535                         currentAttribute = factory.createAttribute(request, key);
536                         currentAttribute.setValue(""); // empty
537                         addHttpData(currentAttribute);
538                         currentAttribute = null;
539                         firstpos = currentpos;
540                         contRead = true;
541                     }
542                     break;
543                 case FIELD:// search '&' or end of line
544                     if (read == '&') {
545                         currentStatus = MultiPartStatus.DISPOSITION;
546                         ampersandpos = currentpos - 1;
547                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
548                         firstpos = currentpos;
549                         contRead = true;
550                     } else if (read == HttpConstants.CR) {
551                         if (sao.pos < sao.limit) {
552                             read = (char) (sao.bytes[sao.pos++] & 0xFF);
553                             currentpos++;
554                             if (read == HttpConstants.LF) {
555                                 currentStatus = MultiPartStatus.PREEPILOGUE;
556                                 ampersandpos = currentpos - 2;
557                                 sao.setReadPosition(0);
558                                 setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
559                                 firstpos = currentpos;
560                                 contRead = false;
561                                 break loop;
562                             } else {
563                                 // Error
564                                 sao.setReadPosition(0);
565                                 throw new ErrorDataDecoderException("Bad end of line");
566                             }
567                         } else {
568                             if (sao.limit > 0) {
569                                 currentpos--;
570                             }
571                         }
572                     } else if (read == HttpConstants.LF) {
573                         currentStatus = MultiPartStatus.PREEPILOGUE;
574                         ampersandpos = currentpos - 1;
575                         sao.setReadPosition(0);
576                         setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
577                         firstpos = currentpos;
578                         contRead = false;
579                         break loop;
580                     }
581                     break;
582                 default:
583                     // just stop
584                     sao.setReadPosition(0);
585                     contRead = false;
586                     break loop;
587                 }
588             }
589             if (isLastChunk && currentAttribute != null) {
590                 // special case
591                 ampersandpos = currentpos;
592                 if (ampersandpos > firstpos) {
593                     setFinalBuffer(undecodedChunk.copy(firstpos, ampersandpos - firstpos));
594                 } else if (!currentAttribute.isCompleted()) {
595                     setFinalBuffer(EMPTY_BUFFER);
596                 }
597                 firstpos = currentpos;
598                 currentStatus = MultiPartStatus.EPILOGUE;
599                 undecodedChunk.readerIndex(firstpos);
600                 return;
601             }
602             if (contRead && currentAttribute != null) {
603                 // reset index except if to continue in case of FIELD getStatus
604                 if (currentStatus == MultiPartStatus.FIELD) {
605                     currentAttribute.addContent(undecodedChunk.copy(firstpos, currentpos - firstpos),
606                                                 false);
607                     firstpos = currentpos;
608                 }
609                 undecodedChunk.readerIndex(firstpos);
610             } else {
611                 // end of line or end of block so keep index to last valid position
612                 undecodedChunk.readerIndex(firstpos);
613             }
614         } catch (ErrorDataDecoderException e) {
615             // error while decoding
616             undecodedChunk.readerIndex(firstpos);
617             throw e;
618         } catch (IOException e) {
619             // error while decoding
620             undecodedChunk.readerIndex(firstpos);
621             throw new ErrorDataDecoderException(e);
622         } catch (IllegalArgumentException e) {
623             // error while decoding
624             undecodedChunk.readerIndex(firstpos);
625             throw new ErrorDataDecoderException(e);
626         }
627     }
628 
629     private void setFinalBuffer(ByteBuf buffer) throws IOException {
630         currentAttribute.addContent(buffer, true);
631         String value = decodeAttribute(currentAttribute.getByteBuf().toString(charset), charset);
632         currentAttribute.setValue(value);
633         addHttpData(currentAttribute);
634         currentAttribute = null;
635     }
636 
637     /**
638      * Decode component
639      *
640      * @return the decoded component
641      */
642     private static String decodeAttribute(String s, Charset charset) {
643         try {
644             return QueryStringDecoder.decodeComponent(s, charset);
645         } catch (IllegalArgumentException e) {
646             throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e);
647         }
648     }
649 
650     /**
651      * Destroy the {@link HttpPostStandardRequestDecoder} and release all it resources. After this method
652      * was called it is not possible to operate on it anymore.
653      */
654     @Override
655     public void destroy() {
656         checkDestroyed();
657         cleanFiles();
658         destroyed = true;
659 
660         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
661             undecodedChunk.release();
662             undecodedChunk = null;
663         }
664 
665         // release all data which was not yet pulled
666         for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
667             bodyListHttpData.get(i).release();
668         }
669     }
670 
671     /**
672      * Clean all HttpDatas (on Disk) for the current request.
673      */
674     @Override
675     public void cleanFiles() {
676         checkDestroyed();
677 
678         factory.cleanRequestHttpData(request);
679     }
680 
681     /**
682      * Remove the given FileUpload from the list of FileUploads to clean
683      */
684     @Override
685     public void removeHttpDataFromClean(InterfaceHttpData data) {
686         checkDestroyed();
687 
688         factory.removeHttpDataFromClean(request, data);
689     }
690 }