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}