1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
51
52
53
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
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
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
208 return totalRead;
209 }
210 if (this.chunkSize == 0L) {
211
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
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
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 }