001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.nio.codecs;
029
030import java.io.IOException;
031import java.nio.ByteBuffer;
032import java.nio.channels.ReadableByteChannel;
033import java.util.ArrayList;
034import java.util.List;
035
036import org.apache.http.ConnectionClosedException;
037import org.apache.http.Header;
038import org.apache.http.MalformedChunkCodingException;
039import org.apache.http.MessageConstraintException;
040import org.apache.http.ParseException;
041import org.apache.http.TruncatedChunkException;
042import org.apache.http.config.MessageConstraints;
043import org.apache.http.impl.io.HttpTransportMetricsImpl;
044import org.apache.http.message.BufferedHeader;
045import org.apache.http.nio.reactor.SessionInputBuffer;
046import org.apache.http.util.Args;
047import org.apache.http.util.CharArrayBuffer;
048
049/**
050 * Implements chunked transfer coding. The content is received in small chunks.
051 * Entities transferred using this encoder can be of unlimited length.
052 *
053 * @since 4.0
054 */
055public class ChunkDecoder extends AbstractContentDecoder {
056
057    private static final int READ_CONTENT   = 0;
058    private static final int READ_FOOTERS  = 1;
059    private static final int COMPLETED      = 2;
060
061    private int state;
062    private boolean endOfChunk;
063    private boolean endOfStream;
064
065    private CharArrayBuffer lineBuf;
066    private long chunkSize;
067    private long pos;
068
069    private final MessageConstraints constraints;
070    private final List<CharArrayBuffer> trailerBufs;
071
072    private Header[] footers;
073
074    /**
075     * @since 4.4
076     */
077    public ChunkDecoder(
078            final ReadableByteChannel channel,
079            final SessionInputBuffer buffer,
080            final MessageConstraints constraints,
081            final HttpTransportMetricsImpl metrics) {
082        super(channel, buffer, metrics);
083        this.state = READ_CONTENT;
084        this.chunkSize = -1L;
085        this.pos = 0L;
086        this.endOfChunk = false;
087        this.endOfStream = false;
088        this.constraints = constraints != null ? constraints : MessageConstraints.DEFAULT;
089        this.trailerBufs = new ArrayList<CharArrayBuffer>();
090    }
091
092    public ChunkDecoder(
093            final ReadableByteChannel channel,
094            final SessionInputBuffer buffer,
095            final HttpTransportMetricsImpl metrics) {
096        this(channel, buffer, null, metrics);
097    }
098
099    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}