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