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.annotation.NotThreadSafe;
43  import org.apache.http.config.MessageConstraints;
44  import org.apache.http.impl.io.HttpTransportMetricsImpl;
45  import org.apache.http.message.BufferedHeader;
46  import org.apache.http.nio.reactor.SessionInputBuffer;
47  import org.apache.http.util.Args;
48  import org.apache.http.util.CharArrayBuffer;
49  
50  /**
51   * Implements chunked transfer coding. The content is received in small chunks.
52   * Entities transferred using this encoder can be of unlimited length.
53   *
54   * @since 4.0
55   */
56  @NotThreadSafe
57  public class ChunkDecoder extends AbstractContentDecoder {
58  
59      private static final int READ_CONTENT   = 0;
60      private static final int READ_FOOTERS  = 1;
61      private static final int COMPLETED      = 2;
62  
63      private int state;
64      private boolean endOfChunk;
65      private boolean endOfStream;
66  
67      private CharArrayBuffer lineBuf;
68      private int chunkSize;
69      private int pos;
70  
71      private final MessageConstraints constraints;
72      private final List<CharArrayBuffer> trailerBufs;
73  
74      private Header[] footers;
75  
76      /**
77       * @since 4.4
78       */
79      public ChunkDecoder(
80              final ReadableByteChannel channel,
81              final SessionInputBuffer buffer,
82              final MessageConstraints constraints,
83              final HttpTransportMetricsImpl metrics) {
84          super(channel, buffer, metrics);
85          this.state = READ_CONTENT;
86          this.chunkSize = -1;
87          this.pos = 0;
88          this.endOfChunk = false;
89          this.endOfStream = false;
90          this.constraints = constraints != null ? constraints : MessageConstraints.DEFAULT;
91          this.trailerBufs = new ArrayList<CharArrayBuffer>();
92      }
93  
94      public ChunkDecoder(
95              final ReadableByteChannel channel,
96              final SessionInputBuffer buffer,
97              final HttpTransportMetricsImpl metrics) {
98          this(channel, buffer, null, metrics);
99      }
100 
101     private void readChunkHead() throws IOException {
102         if (this.lineBuf == null) {
103             this.lineBuf = new CharArrayBuffer(32);
104         } else {
105             this.lineBuf.clear();
106         }
107         if (this.endOfChunk) {
108             if (this.buffer.readLine(this.lineBuf, this.endOfStream)) {
109                 if (!this.lineBuf.isEmpty()) {
110                     throw new MalformedChunkCodingException("CRLF expected at end of chunk");
111                 }
112             } else {
113                 if (this.buffer.length() > 2 || this.endOfStream) {
114                     throw new MalformedChunkCodingException("CRLF expected at end of chunk");
115                 }
116                 return;
117             }
118             this.endOfChunk = false;
119         }
120         final boolean lineComplete = this.buffer.readLine(this.lineBuf, this.endOfStream);
121         final int maxLineLen = this.constraints.getMaxLineLength();
122         if (maxLineLen > 0 &&
123                 (this.lineBuf.length() > maxLineLen ||
124                         (!lineComplete && this.buffer.length() > maxLineLen))) {
125             throw new MessageConstraintException("Maximum line length limit exceeded");
126         }
127         if (lineComplete) {
128             int separator = this.lineBuf.indexOf(';');
129             if (separator < 0) {
130                 separator = this.lineBuf.length();
131             }
132             try {
133                 final String s = this.lineBuf.substringTrimmed(0, separator);
134                 this.chunkSize = Integer.parseInt(s, 16);
135             } catch (final NumberFormatException e) {
136                 throw new MalformedChunkCodingException("Bad chunk header");
137             }
138             this.pos = 0;
139         } else if (this.endOfStream) {
140             throw new ConnectionClosedException("Premature end of chunk coded message body: " +
141                     "closing chunk expected");
142         }
143     }
144 
145     private void parseHeader() throws IOException {
146         final CharArrayBuffer current = this.lineBuf;
147         final int count = this.trailerBufs.size();
148         if ((this.lineBuf.charAt(0) == ' ' || this.lineBuf.charAt(0) == '\t') && count > 0) {
149             // Handle folded header line
150             final CharArrayBuffer previous = this.trailerBufs.get(count - 1);
151             int i = 0;
152             while (i < current.length()) {
153                 final char ch = current.charAt(i);
154                 if (ch != ' ' && ch != '\t') {
155                     break;
156                 }
157                 i++;
158             }
159             final int maxLineLen = this.constraints.getMaxLineLength();
160             if (maxLineLen > 0 && previous.length() + 1 + current.length() - i > maxLineLen) {
161                 throw new MessageConstraintException("Maximum line length limit exceeded");
162             }
163             previous.append(' ');
164             previous.append(current, i, current.length() - i);
165         } else {
166             this.trailerBufs.add(current);
167             this.lineBuf = null;
168         }
169     }
170 
171     private void processFooters() throws IOException {
172         final int count = this.trailerBufs.size();
173         if (count > 0) {
174             this.footers = new Header[this.trailerBufs.size()];
175             for (int i = 0; i < this.trailerBufs.size(); i++) {
176                 try {
177                     this.footers[i] = new BufferedHeader(this.trailerBufs.get(i));
178                 } catch (final ParseException ex) {
179                     throw new IOException(ex.getMessage());
180                 }
181             }
182         }
183         this.trailerBufs.clear();
184     }
185 
186     @Override
187     public int read(final ByteBuffer dst) throws IOException {
188         Args.notNull(dst, "Byte buffer");
189         if (this.state == COMPLETED) {
190             return -1;
191         }
192 
193         int totalRead = 0;
194         while (this.state != COMPLETED) {
195 
196             if (!this.buffer.hasData() || this.chunkSize == -1) {
197                 final int bytesRead = fillBufferFromChannel();
198                 if (bytesRead == -1) {
199                     this.endOfStream = true;
200                 }
201             }
202 
203             switch (this.state) {
204             case READ_CONTENT:
205 
206                 if (this.chunkSize == -1) {
207                     readChunkHead();
208                     if (this.chunkSize == -1) {
209                         // Unable to read a chunk head
210                         return totalRead;
211                     }
212                     if (this.chunkSize == 0) {
213                         // Last chunk. Read footers
214                         this.chunkSize = -1;
215                         this.state = READ_FOOTERS;
216                         break;
217                     }
218                 }
219                 final int maxLen = this.chunkSize - this.pos;
220                 final int len = this.buffer.read(dst, maxLen);
221                 if (len > 0) {
222                     this.pos += len;
223                     totalRead += len;
224                 } else {
225                     if (!this.buffer.hasData() && this.endOfStream) {
226                         this.state = COMPLETED;
227                         this.completed = true;
228                         throw new TruncatedChunkException("Truncated chunk "
229                                 + "( expected size: " + this.chunkSize
230                                 + "; actual size: " + this.pos + ")");
231                     }
232                 }
233 
234                 if (this.pos == this.chunkSize) {
235                     // At the end of the chunk
236                     this.chunkSize = -1;
237                     this.pos = 0;
238                     this.endOfChunk = true;
239                     break;
240                 }
241                 return totalRead;
242             case READ_FOOTERS:
243                 if (this.lineBuf == null) {
244                     this.lineBuf = new CharArrayBuffer(32);
245                 } else {
246                     this.lineBuf.clear();
247                 }
248                 if (!this.buffer.readLine(this.lineBuf, this.endOfStream)) {
249                     // Unable to read a footer
250                     if (this.endOfStream) {
251                         this.state = COMPLETED;
252                         this.completed = true;
253                     }
254                     return totalRead;
255                 }
256                 if (this.lineBuf.length() > 0) {
257                     final int maxHeaderCount = this.constraints.getMaxHeaderCount();
258                     if (maxHeaderCount > 0 && trailerBufs.size() >= maxHeaderCount) {
259                         throw new MessageConstraintException("Maximum header count exceeded");
260                     }
261                     parseHeader();
262                 } else {
263                     this.state = COMPLETED;
264                     this.completed = true;
265                     processFooters();
266                 }
267                 break;
268             }
269 
270         }
271         return totalRead;
272     }
273 
274     public Header[] getFooters() {
275         if (this.footers != null) {
276             return this.footers.clone();
277         } else {
278             return new Header[] {};
279         }
280     }
281 
282     @Override
283     public String toString() {
284         final StringBuilder sb = new StringBuilder();
285         sb.append("[chunk-coded; completed: ");
286         sb.append(this.completed);
287         sb.append("]");
288         return sb.toString();
289     }
290 
291 }