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.buffer.ChannelBuffer;
19  import org.jboss.netty.buffer.ChannelBuffers;
20  import org.jboss.netty.handler.codec.http.HttpChunk;
21  import org.jboss.netty.handler.codec.http.HttpConstants;
22  import org.jboss.netty.handler.codec.http.HttpRequest;
23  import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException;
24  import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
25  import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
26  import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
27  import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.IncompatibleDataDecoderException;
28  import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
29  import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
30  import org.jboss.netty.util.internal.CaseIgnoringComparator;
31  
32  import java.io.IOException;
33  import java.io.UnsupportedEncodingException;
34  import java.net.URLDecoder;
35  import java.nio.charset.Charset;
36  import java.util.ArrayList;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.TreeMap;
40  
41  /**
42   * This decoder will decode Body and can handle standard (non multipart) POST BODY.
43   */
44  @SuppressWarnings({ "deprecation", "RedundantThrowsDeclaration" })
45  public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestDecoder {
46      /**
47       * Factory used to create InterfaceHttpData
48       */
49      private final HttpDataFactory factory;
50  
51      /**
52       * Request to decode
53       */
54      private final HttpRequest request;
55  
56      /**
57       * Default charset to use
58       */
59      private final Charset charset;
60  
61      /**
62       * Does the last chunk already received
63       */
64      private boolean isLastChunk;
65  
66      /**
67       * HttpDatas from Body
68       */
69      private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
70  
71      /**
72       * HttpDatas as Map from Body
73       */
74      private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
75              CaseIgnoringComparator.INSTANCE);
76  
77      /**
78       * The current channelBuffer
79       */
80      private ChannelBuffer undecodedChunk;
81  
82      /**
83       * Body HttpDatas current position
84       */
85      private int bodyListHttpDataRank;
86  
87      /**
88       * Current status
89       */
90      private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
91  
92      /**
93       * The current Attribute that is currently in decode process
94       */
95      private Attribute currentAttribute;
96  
97      /**
98      *
99      * @param request the request to decode
100     * @throws NullPointerException for request
101     * @throws IncompatibleDataDecoderException if the request has no body to decode
102     * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
103     */
104     public HttpPostStandardRequestDecoder(HttpRequest request)
105             throws ErrorDataDecoderException, IncompatibleDataDecoderException {
106         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
107                 request, HttpConstants.DEFAULT_CHARSET);
108     }
109 
110     /**
111      *
112      * @param factory the factory used to create InterfaceHttpData
113      * @param request the request to decode
114      * @throws NullPointerException for request or factory
115      * @throws IncompatibleDataDecoderException if the request has no body to decode
116      * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
117      */
118     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request)
119             throws ErrorDataDecoderException, IncompatibleDataDecoderException {
120         this(factory, request, HttpConstants.DEFAULT_CHARSET);
121     }
122 
123     /**
124      *
125      * @param factory the factory used to create InterfaceHttpData
126      * @param request the request to decode
127      * @param charset the charset to use as default
128      * @throws NullPointerException for request or charset or factory
129      * @throws IncompatibleDataDecoderException if the request has no body to decode
130      * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
131      */
132     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request,
133             Charset charset) throws ErrorDataDecoderException,
134             IncompatibleDataDecoderException {
135         if (factory == null) {
136             throw new NullPointerException("factory");
137         }
138         if (request == null) {
139             throw new NullPointerException("request");
140         }
141         if (charset == null) {
142             throw new NullPointerException("charset");
143         }
144         this.request = request;
145         this.charset = charset;
146         this.factory = factory;
147         if (!this.request.isChunked()) {
148             undecodedChunk = this.request.getContent();
149             isLastChunk = true;
150             parseBody();
151         }
152     }
153 
154     public boolean isMultipart() {
155         return false;
156     }
157 
158     public List<InterfaceHttpData> getBodyHttpDatas()
159             throws NotEnoughDataDecoderException {
160         if (!isLastChunk) {
161             throw new NotEnoughDataDecoderException();
162         }
163         return bodyListHttpData;
164     }
165 
166     public List<InterfaceHttpData> getBodyHttpDatas(String name)
167             throws NotEnoughDataDecoderException {
168         if (!isLastChunk) {
169             throw new NotEnoughDataDecoderException();
170         }
171         return bodyMapHttpData.get(name);
172     }
173 
174     public InterfaceHttpData getBodyHttpData(String name)
175             throws NotEnoughDataDecoderException {
176         if (!isLastChunk) {
177             throw new NotEnoughDataDecoderException();
178         }
179         List<InterfaceHttpData> list = bodyMapHttpData.get(name);
180         if (list != null) {
181             return list.get(0);
182         }
183         return null;
184     }
185 
186     public void offer(HttpChunk chunk) throws ErrorDataDecoderException {
187         ChannelBuffer chunked = chunk.getContent();
188         if (undecodedChunk == null) {
189             undecodedChunk = chunked;
190         } else {
191             //undecodedChunk = ChannelBuffers.wrappedBuffer(undecodedChunk, chunk.getContent());
192             // less memory usage
193             undecodedChunk = ChannelBuffers.wrappedBuffer(
194                     undecodedChunk, chunked);
195         }
196         if (chunk.isLast()) {
197             isLastChunk = true;
198         }
199         parseBody();
200     }
201 
202     public boolean hasNext() throws EndOfDataDecoderException {
203         if (currentStatus == MultiPartStatus.EPILOGUE) {
204             // OK except if end of list
205             if (bodyListHttpDataRank >= bodyListHttpData.size()) {
206                 throw new EndOfDataDecoderException();
207             }
208         }
209         return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
210     }
211 
212     public InterfaceHttpData next() throws EndOfDataDecoderException {
213         if (hasNext()) {
214             return bodyListHttpData.get(bodyListHttpDataRank++);
215         }
216         return null;
217     }
218 
219     /**
220      * This method will parse as much as possible data and fill the list and map
221      * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
222      *          other errors
223      */
224     private void parseBody() throws ErrorDataDecoderException {
225         if (currentStatus == MultiPartStatus.PREEPILOGUE ||
226                 currentStatus == MultiPartStatus.EPILOGUE) {
227             if (isLastChunk) {
228                 currentStatus = MultiPartStatus.EPILOGUE;
229             }
230             return;
231         }
232         parseBodyAttributes();
233     }
234 
235     /**
236      * Utility function to add a new decoded data
237      */
238     private void addHttpData(InterfaceHttpData data) {
239         if (data == null) {
240             return;
241         }
242         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
243         if (datas == null) {
244             datas = new ArrayList<InterfaceHttpData>(1);
245             bodyMapHttpData.put(data.getName(), datas);
246         }
247         datas.add(data);
248         bodyListHttpData.add(data);
249     }
250 
251     /**
252       * This method fill the map and list with as much Attribute as possible from Body in
253       * not Multipart mode.
254       *
255       * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
256       *          other errors
257       */
258     private void parseBodyAttributesStandard() throws ErrorDataDecoderException {
259         int firstpos = undecodedChunk.readerIndex();
260         int currentpos = firstpos;
261         int equalpos;
262         int ampersandpos;
263         if (currentStatus == MultiPartStatus.NOTSTARTED) {
264             currentStatus = MultiPartStatus.DISPOSITION;
265         }
266         boolean contRead = true;
267         try {
268            while (undecodedChunk.readable() && contRead) {
269                 char read = (char) undecodedChunk.readUnsignedByte();
270                 currentpos++;
271                 switch (currentStatus) {
272                 case DISPOSITION:// search '='
273                     if (read == '=') {
274                         currentStatus = MultiPartStatus.FIELD;
275                         equalpos = currentpos - 1;
276                         String key = decodeAttribute(
277                                 undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
278                                 charset);
279                         currentAttribute = factory.createAttribute(request, key);
280                         firstpos = currentpos;
281                     } else if (read == '&') { // special empty FIELD
282                         currentStatus = MultiPartStatus.DISPOSITION;
283                         ampersandpos = currentpos - 1;
284                         String key = decodeAttribute(
285                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
286                         currentAttribute = factory.createAttribute(request, key);
287                         currentAttribute.setValue(""); // empty
288                         addHttpData(currentAttribute);
289                         currentAttribute = null;
290                         firstpos = currentpos;
291                         contRead = true;
292                     }
293                     break;
294                 case FIELD:// search '&' or end of line
295                     if (read == '&') {
296                         currentStatus = MultiPartStatus.DISPOSITION;
297                         ampersandpos = currentpos - 1;
298                         setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos));
299                         firstpos = currentpos;
300                         contRead = true;
301                     } else if (read == HttpConstants.CR) {
302                         if (undecodedChunk.readable()) {
303                             read = (char) undecodedChunk.readUnsignedByte();
304                             currentpos++;
305                             if (read == HttpConstants.LF) {
306                                 currentStatus = MultiPartStatus.PREEPILOGUE;
307                                 ampersandpos = currentpos - 2;
308                                 setFinalBuffer(
309                                         undecodedChunk.slice(firstpos, ampersandpos - firstpos));
310                                 firstpos = currentpos;
311                                 contRead = false;
312                             } else {
313                                 // Error
314                                 throw new ErrorDataDecoderException("Bad end of line");
315                             }
316                         } else {
317                             currentpos--;
318                         }
319                     } else if (read == HttpConstants.LF) {
320                         currentStatus = MultiPartStatus.PREEPILOGUE;
321                         ampersandpos = currentpos - 1;
322                         setFinalBuffer(
323                                 undecodedChunk.slice(firstpos, ampersandpos - firstpos));
324                         firstpos = currentpos;
325                         contRead = false;
326                     }
327                     break;
328                 default:
329                     // just stop
330                     contRead = false;
331                 }
332             }
333             if (isLastChunk && currentAttribute != null) {
334                 // special case
335                 ampersandpos = currentpos;
336                 if (ampersandpos > firstpos) {
337                     setFinalBuffer(
338                             undecodedChunk.slice(firstpos, ampersandpos - firstpos));
339                 } else if (! currentAttribute.isCompleted()) {
340                     setFinalBuffer(ChannelBuffers.EMPTY_BUFFER);
341                 }
342                 firstpos = currentpos;
343                 currentStatus = MultiPartStatus.EPILOGUE;
344                 undecodedChunk.readerIndex(firstpos);
345                 return;
346             }
347             if (contRead && currentAttribute != null) {
348                 // reset index except if to continue in case of FIELD status
349                 if (currentStatus == MultiPartStatus.FIELD) {
350                     currentAttribute.addContent(
351                             undecodedChunk.slice(firstpos, currentpos - firstpos),
352                             false);
353                     firstpos = currentpos;
354                 }
355                 undecodedChunk.readerIndex(firstpos);
356             } else {
357                 // end of line or end of block so keep index to last valid position
358                 undecodedChunk.readerIndex(firstpos);
359             }
360         } catch (ErrorDataDecoderException e) {
361             // error while decoding
362             undecodedChunk.readerIndex(firstpos);
363             throw e;
364         } catch (IOException e) {
365             // error while decoding
366             undecodedChunk.readerIndex(firstpos);
367             throw new ErrorDataDecoderException(e);
368         }
369     }
370 
371     /**
372      * This method fill the map and list with as much Attribute as possible from Body in
373      * not Multipart mode.
374      *
375      * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
376      * other errors
377      */
378     private void parseBodyAttributes() throws ErrorDataDecoderException {
379         SeekAheadOptimize sao;
380         try {
381             sao = new SeekAheadOptimize(undecodedChunk);
382         } catch (SeekAheadNoBackArrayException e1) {
383             parseBodyAttributesStandard();
384             return;
385         }
386         int firstpos = undecodedChunk.readerIndex();
387         int currentpos = firstpos;
388         int equalpos;
389         int ampersandpos;
390         if (currentStatus == MultiPartStatus.NOTSTARTED) {
391             currentStatus = MultiPartStatus.DISPOSITION;
392         }
393         boolean contRead = true;
394         try {
395             loop:
396             while (sao.pos < sao.limit) {
397                 char read = (char) (sao.bytes[sao.pos ++] & 0xFF);
398                 currentpos ++;
399                 switch (currentStatus) {
400                 case DISPOSITION:// search '='
401                     if (read == '=') {
402                         currentStatus = MultiPartStatus.FIELD;
403                         equalpos = currentpos - 1;
404                         String key = decodeAttribute(
405                                 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.slice(firstpos, ampersandpos - firstpos));
427                         firstpos = currentpos;
428                         contRead = true;
429                     } else if (read == HttpConstants.CR) {
430                         if (sao.pos < sao.limit) {
431                             read = (char) (sao.bytes[sao.pos ++] & 0xFF);
432                             currentpos++;
433                             if (read == HttpConstants.LF) {
434                                 currentStatus = MultiPartStatus.PREEPILOGUE;
435                                 ampersandpos = currentpos - 2;
436                                 sao.setReadPosition(0);
437                                 setFinalBuffer(
438                                         undecodedChunk.slice(firstpos, ampersandpos - firstpos));
439                                 firstpos = currentpos;
440                                 contRead = false;
441                                 break loop;
442                             } else {
443                                 // Error
444                                 sao.setReadPosition(0);
445                                 throw new ErrorDataDecoderException("Bad end of line");
446                             }
447                         } else {
448                             if (sao.limit > 0) {
449                                 currentpos --;
450                             }
451                         }
452                     } else if (read == HttpConstants.LF) {
453                         currentStatus = MultiPartStatus.PREEPILOGUE;
454                         ampersandpos = currentpos - 1;
455                         sao.setReadPosition(0);
456                         setFinalBuffer(
457                                 undecodedChunk.slice(firstpos, ampersandpos - firstpos));
458                         firstpos = currentpos;
459                         contRead = false;
460                         break loop;
461                     }
462                     break;
463                 default:
464                     // just stop
465                     sao.setReadPosition(0);
466                     contRead = false;
467                     break loop;
468                 }
469             }
470             if (isLastChunk && currentAttribute != null) {
471                 // special case
472                 ampersandpos = currentpos;
473                 if (ampersandpos > firstpos) {
474                     setFinalBuffer(
475                             undecodedChunk.slice(firstpos, ampersandpos - firstpos));
476                 } else if (! currentAttribute.isCompleted()) {
477                     setFinalBuffer(ChannelBuffers.EMPTY_BUFFER);
478                 }
479                 firstpos = currentpos;
480                 currentStatus = MultiPartStatus.EPILOGUE;
481                 undecodedChunk.readerIndex(firstpos);
482                 return;
483             }
484             if (contRead && currentAttribute != null) {
485                 // reset index except if to continue in case of FIELD status
486                 if (currentStatus == MultiPartStatus.FIELD) {
487                     currentAttribute.addContent(
488                             undecodedChunk.slice(firstpos, currentpos - firstpos),
489                             false);
490                     firstpos = currentpos;
491                 }
492                 undecodedChunk.readerIndex(firstpos);
493             } else {
494                 // end of line or end of block so keep index to last valid position
495                 undecodedChunk.readerIndex(firstpos);
496             }
497         } catch (ErrorDataDecoderException e) {
498             // error while decoding
499             undecodedChunk.readerIndex(firstpos);
500             throw e;
501         } catch (IOException e) {
502             // error while decoding
503             undecodedChunk.readerIndex(firstpos);
504             throw new ErrorDataDecoderException(e);
505         }
506     }
507 
508     private void setFinalBuffer(ChannelBuffer buffer) throws ErrorDataDecoderException, IOException {
509         currentAttribute.addContent(buffer, true);
510         String value = decodeAttribute(
511                 currentAttribute.getChannelBuffer().toString(charset),
512                 charset);
513         currentAttribute.setValue(value);
514         addHttpData(currentAttribute);
515         currentAttribute = null;
516     }
517 
518     /**
519      * Decode component
520      * @return the decoded component
521      */
522     private static String decodeAttribute(String s, Charset charset)
523             throws ErrorDataDecoderException {
524         if (s == null) {
525             return "";
526         }
527         try {
528             return URLDecoder.decode(s, charset.name());
529         } catch (UnsupportedEncodingException e) {
530             throw new ErrorDataDecoderException(charset.toString(), e);
531         } catch (IllegalArgumentException e) {
532             throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e);
533         }
534     }
535 
536     public void cleanFiles() {
537         factory.cleanRequestHttpDatas(request);
538     }
539 
540     public void removeHttpDataFromClean(InterfaceHttpData data) {
541         factory.removeHttpDataFromClean(request, data);
542     }
543 }