View Javadoc

1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.http.impl.nio.codecs;
29  
30  import java.io.IOException;
31  import java.nio.ByteBuffer;
32  import java.nio.channels.ReadableByteChannel;
33  import java.util.ArrayList;
34  import java.util.List;
35  
36  import org.apache.http.ConnectionClosedException;
37  import org.apache.http.Header;
38  import org.apache.http.MalformedChunkCodingException;
39  import org.apache.http.MessageConstraintException;
40  import org.apache.http.ParseException;
41  import org.apache.http.TruncatedChunkException;
42  import org.apache.http.config.MessageConstraints;
43  import org.apache.http.impl.io.HttpTransportMetricsImpl;
44  import org.apache.http.message.BufferedHeader;
45  import org.apache.http.nio.reactor.SessionInputBuffer;
46  import org.apache.http.util.Args;
47  import org.apache.http.util.CharArrayBuffer;
48  
49  /**
50   * Implements chunked transfer coding. The content is received in small chunks.
51   * Entities transferred using this encoder can be of unlimited length.
52   *
53   * @since 4.0
54   */
55  public class ChunkDecoder extends AbstractContentDecoder {
56  
57      private static final int READ_CONTENT   = 0;
58      private static final int READ_FOOTERS  = 1;
59      private static final int COMPLETED      = 2;
60  
61      private int state;
62      private boolean endOfChunk;
63      private boolean endOfStream;
64  
65      private CharArrayBuffer lineBuf;
66      private long chunkSize;
67      private long pos;
68  
69      private final MessageConstraints constraints;
70      private final List<CharArrayBuffer> trailerBufs;
71  
72      private Header[] footers;
73  
74      /**
75       * @since 4.4
76       */
77      public ChunkDecoder(
78              final ReadableByteChannel channel,
79              final SessionInputBuffer buffer,
80              final MessageConstraints constraints,
81              final HttpTransportMetricsImpl metrics) {
82          super(channel, buffer, metrics);
83          this.state = READ_CONTENT;
84          this.chunkSize = -1L;
85          this.pos = 0L;
86          this.endOfChunk = false;
87          this.endOfStream = false;
88          this.constraints = constraints != null ? constraints : MessageConstraints.DEFAULT;
89          this.trailerBufs = new ArrayList<CharArrayBuffer>();
90      }
91  
92      public ChunkDecoder(
93              final ReadableByteChannel channel,
94              final SessionInputBuffer buffer,
95              final HttpTransportMetricsImpl metrics) {
96          this(channel, buffer, null, metrics);
97      }
98  
99      private void readChunkHead() throws IOException {
100         if (this.lineBuf == null) {
101             this.lineBuf = new CharArrayBuffer(32);
102         } else {
103             this.lineBuf.clear();
104         }
105         if (this.endOfChunk) {
106             if (this.buffer.readLine(this.lineBuf, this.endOfStream)) {
107                 if (!this.lineBuf.isEmpty()) {
108                     throw new MalformedChunkCodingException("CRLF expected at end of chunk");
109                 }
110             } else {
111                 if (this.buffer.length() > 2 || this.endOfStream) {
112                     throw new MalformedChunkCodingException("CRLF expected at end of chunk");
113                 }
114                 return;
115             }
116             this.endOfChunk = false;
117         }
118         final boolean lineComplete = this.buffer.readLine(this.lineBuf, this.endOfStream);
119         final int maxLineLen = this.constraints.getMaxLineLength();
120         if (maxLineLen > 0 &&
121                 (this.lineBuf.length() > maxLineLen ||
122                         (!lineComplete && this.buffer.length() > maxLineLen))) {
123             throw new MessageConstraintException("Maximum line length limit exceeded");
124         }
125         if (lineComplete) {
126             int separator = this.lineBuf.indexOf(';');
127             if (separator < 0) {
128                 separator = this.lineBuf.length();
129             }
130             final String s = this.lineBuf.substringTrimmed(0, separator);
131             try {
132                 this.chunkSize = Long.parseLong(s, 16);
133             } catch (final NumberFormatException e) {
134                 throw new MalformedChunkCodingException("Bad chunk header: " + s);
135             }
136             this.pos = 0L;
137         } else if (this.endOfStream) {
138             throw new ConnectionClosedException("Premature end of chunk coded message body: " +
139                     "closing chunk expected");
140         }
141     }
142 
143     private void parseHeader() throws IOException {
144         final CharArrayBuffer current = this.lineBuf;
145         final int count = this.trailerBufs.size();
146         if ((this.lineBuf.charAt(0) == ' ' || this.lineBuf.charAt(0) == '\t') && count > 0) {
147             // Handle folded header line
148             final CharArrayBuffer previous = this.trailerBufs.get(count - 1);
149             int i = 0;
150             while (i < current.length()) {
151                 final char ch = current.charAt(i);
152                 if (ch != ' ' && ch != '\t') {
153                     break;
154                 }
155                 i++;
156             }
157             final int maxLineLen = this.constraints.getMaxLineLength();
158             if (maxLineLen > 0 && previous.length() + 1 + current.length() - i > maxLineLen) {
159                 throw new MessageConstraintException("Maximum line length limit exceeded");
160             }
161             previous.append(' ');
162             previous.append(current, i, current.length() - i);
163         } else {
164             this.trailerBufs.add(current);
165             this.lineBuf = null;
166         }
167     }
168 
169     private void processFooters() throws IOException {
170         final int count = this.trailerBufs.size();
171         if (count > 0) {
172             this.footers = new Header[this.trailerBufs.size()];
173             for (int i = 0; i < this.trailerBufs.size(); i++) {
174                 try {
175                     this.footers[i] = new BufferedHeader(this.trailerBufs.get(i));
176                 } catch (final ParseException ex) {
177                     throw new IOException(ex.getMessage());
178                 }
179             }
180         }
181         this.trailerBufs.clear();
182     }
183 
184     @Override
185     public int read(final ByteBuffer dst) throws IOException {
186         Args.notNull(dst, "Byte buffer");
187         if (this.state == COMPLETED) {
188             return -1;
189         }
190 
191         int totalRead = 0;
192         while (this.state != COMPLETED) {
193 
194             if (!this.buffer.hasData() || this.chunkSize == -1L) {
195                 final int bytesRead = fillBufferFromChannel();
196                 if (bytesRead == -1) {
197                     this.endOfStream = true;
198                 }
199             }
200 
201             switch (this.state) {
202             case READ_CONTENT:
203 
204                 if (this.chunkSize == -1L) {
205                     readChunkHead();
206                     if (this.chunkSize == -1L) {
207                         // Unable to read a chunk head
208                         return totalRead;
209                     }
210                     if (this.chunkSize == 0L) {
211                         // Last chunk. Read footers
212                         this.chunkSize = -1L;
213                         this.state = READ_FOOTERS;
214                         break;
215                     }
216                 }
217                 final long maxLen = this.chunkSize - this.pos;
218                 final int len = this.buffer.read(dst, (int) Math.min(maxLen, Integer.MAX_VALUE));
219                 if (len > 0) {
220                     this.pos += len;
221                     totalRead += len;
222                 } else {
223                     if (!this.buffer.hasData() && this.endOfStream) {
224                         this.state = COMPLETED;
225                         this.completed = true;
226                         throw new TruncatedChunkException("Truncated chunk "
227                                 + "( expected size: " + this.chunkSize
228                                 + "; actual size: " + this.pos + ")");
229                     }
230                 }
231 
232                 if (this.pos == this.chunkSize) {
233                     // At the end of the chunk
234                     this.chunkSize = -1L;
235                     this.pos = 0L;
236                     this.endOfChunk = true;
237                     break;
238                 }
239                 return totalRead;
240             case READ_FOOTERS:
241                 if (this.lineBuf == null) {
242                     this.lineBuf = new CharArrayBuffer(32);
243                 } else {
244                     this.lineBuf.clear();
245                 }
246                 if (!this.buffer.readLine(this.lineBuf, this.endOfStream)) {
247                     // Unable to read a footer
248                     if (this.endOfStream) {
249                         this.state = COMPLETED;
250                         this.completed = true;
251                     }
252                     return totalRead;
253                 }
254                 if (this.lineBuf.length() > 0) {
255                     final int maxHeaderCount = this.constraints.getMaxHeaderCount();
256                     if (maxHeaderCount > 0 && trailerBufs.size() >= maxHeaderCount) {
257                         throw new MessageConstraintException("Maximum header count exceeded");
258                     }
259                     parseHeader();
260                 } else {
261                     this.state = COMPLETED;
262                     this.completed = true;
263                     processFooters();
264                 }
265                 break;
266             }
267 
268         }
269         return totalRead;
270     }
271 
272     public Header[] getFooters() {
273         if (this.footers != null) {
274             return this.footers.clone();
275         } else {
276             return new Header[] {};
277         }
278     }
279 
280     @Override
281     public String toString() {
282         final StringBuilder sb = new StringBuilder();
283         sb.append("[chunk-coded; completed: ");
284         sb.append(this.completed);
285         sb.append("]");
286         return sb.toString();
287     }
288 
289 }