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.hc.core5.http.impl.nio;
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.hc.core5.http.ConnectionClosedException;
37  import org.apache.hc.core5.http.Header;
38  import org.apache.hc.core5.http.MalformedChunkCodingException;
39  import org.apache.hc.core5.http.MessageConstraintException;
40  import org.apache.hc.core5.http.ParseException;
41  import org.apache.hc.core5.http.TruncatedChunkException;
42  import org.apache.hc.core5.http.config.Http1Config;
43  import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
44  import org.apache.hc.core5.http.message.BufferedHeader;
45  import org.apache.hc.core5.http.nio.SessionInputBuffer;
46  import org.apache.hc.core5.util.Args;
47  import org.apache.hc.core5.util.CharArrayBuffer;
48  
49  /**
50   * Implements chunked transfer decoding. 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 enum State {
58          READ_CONTENT, READ_FOOTERS, COMPLETED
59      }
60  
61      private State 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 Http1Config http1Config;
70      private final List<CharArrayBuffer> trailerBufs;
71      private final List<Header> trailers;
72  
73      /**
74       * @since 4.4
75       */
76      public ChunkDecoder(
77              final ReadableByteChannel channel,
78              final SessionInputBuffer buffer,
79              final Http1Config http1Config,
80              final BasicHttpTransportMetrics metrics) {
81          super(channel, buffer, metrics);
82          this.state = State.READ_CONTENT;
83          this.chunkSize = -1L;
84          this.pos = 0L;
85          this.endOfChunk = false;
86          this.endOfStream = false;
87          this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT;
88          this.trailerBufs = new ArrayList<>();
89          this.trailers = new ArrayList<>();
90      }
91  
92      public ChunkDecoder(
93              final ReadableByteChannel channel,
94              final SessionInputBuffer buffer,
95              final BasicHttpTransportMetrics 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.http1Config.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(
139                             "Premature end of chunk coded message body: 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.http1Config.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.trailers.clear();
173             for (int i = 0; i < this.trailerBufs.size(); i++) {
174                 try {
175                     this.trailers.add(new BufferedHeader(this.trailerBufs.get(i)));
176                 } catch (final ParseException ex) {
177                     throw new IOException(ex);
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 == State.COMPLETED) {
188             return -1;
189         }
190 
191         int totalRead = 0;
192         while (this.state != 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 = 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 = State.COMPLETED;
225                         setCompleted();
226                         throw new TruncatedChunkException(
227                                         "Truncated chunk (expected size: %d; actual size: %d)",
228                                         chunkSize, 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 = State.COMPLETED;
250                         setCompleted();
251                     }
252                     return totalRead;
253                 }
254                 if (this.lineBuf.length() > 0) {
255                     final int maxHeaderCount = this.http1Config.getMaxHeaderCount();
256                     if (maxHeaderCount > 0 && trailerBufs.size() >= maxHeaderCount) {
257                         throw new MessageConstraintException("Maximum header count exceeded");
258                     }
259                     parseHeader();
260                 } else {
261                     this.state = State.COMPLETED;
262                     setCompleted();
263                     processFooters();
264                 }
265                 break;
266             }
267 
268         }
269         return totalRead;
270     }
271 
272     @Override
273     public List<? extends Header> getTrailers() {
274         return this.trailers.isEmpty() ? null : new ArrayList<>(this.trailers);
275     }
276 
277     @Override
278     public String toString() {
279         final StringBuilder sb = new StringBuilder();
280         sb.append("[chunk-coded; completed: ");
281         sb.append(this.completed);
282         sb.append("]");
283         return sb.toString();
284     }
285 
286 }