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.io;
29  
30  import java.io.ByteArrayInputStream;
31  import java.io.ByteArrayOutputStream;
32  import java.io.IOException;
33  import java.io.InterruptedIOException;
34  import java.nio.charset.StandardCharsets;
35  import java.util.Arrays;
36  
37  import org.apache.hc.core5.http.ConnectionClosedException;
38  import org.apache.hc.core5.http.Header;
39  import org.apache.hc.core5.http.MalformedChunkCodingException;
40  import org.apache.hc.core5.http.MessageConstraintException;
41  import org.apache.hc.core5.http.StreamClosedException;
42  import org.apache.hc.core5.http.io.SessionInputBuffer;
43  import org.apache.hc.core5.http.io.SessionOutputBuffer;
44  import org.apache.hc.core5.http.message.BasicHeader;
45  import org.junit.jupiter.api.Assertions;
46  import org.junit.jupiter.api.Test;
47  
48  public class TestChunkCoding {
49  
50      private final static String CHUNKED_INPUT
51          = "10;key=\"value\"\r\n1234567890123456\r\n5\r\n12345\r\n0\r\nFooter1: abcde\r\nFooter2: fghij\r\n";
52  
53      private final static String CHUNKED_RESULT
54          = "123456789012345612345";
55  
56      @Test
57      public void testChunkedInputStreamLargeBuffer() throws IOException {
58          final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
59          final ByteArrayInputStream inputStream = new ByteArrayInputStream(CHUNKED_INPUT.getBytes(StandardCharsets.ISO_8859_1));
60          final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
61          final byte[] buffer = new byte[300];
62          final ByteArrayOutputStream out = new ByteArrayOutputStream();
63          int len;
64          while ((len = in.read(buffer)) > 0) {
65              out.write(buffer, 0, len);
66          }
67          Assertions.assertEquals(-1, in.read(buffer));
68          Assertions.assertEquals(-1, in.read(buffer));
69  
70          in.close();
71  
72          final String result = new String(out.toByteArray(), StandardCharsets.ISO_8859_1);
73          Assertions.assertEquals(result, CHUNKED_RESULT);
74  
75          final Header[] footers = in.getFooters();
76          Assertions.assertNotNull(footers);
77          Assertions.assertEquals(2, footers.length);
78          Assertions.assertEquals("Footer1", footers[0].getName());
79          Assertions.assertEquals("abcde", footers[0].getValue());
80          Assertions.assertEquals("Footer2", footers[1].getName());
81          Assertions.assertEquals("fghij", footers[1].getValue());
82      }
83  
84      //Test for when buffer is smaller than chunk size.
85      @Test
86      public void testChunkedInputStreamSmallBuffer() throws IOException {
87          final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
88          final ByteArrayInputStream inputStream = new ByteArrayInputStream(CHUNKED_INPUT.getBytes(StandardCharsets.ISO_8859_1));
89          final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
90  
91          final byte[] buffer = new byte[7];
92          final ByteArrayOutputStream out = new ByteArrayOutputStream();
93          int len;
94          while ((len = in.read(buffer)) > 0) {
95              out.write(buffer, 0, len);
96          }
97          Assertions.assertEquals(-1, in.read(buffer));
98          Assertions.assertEquals(-1, in.read(buffer));
99  
100         in.close();
101 
102         final Header[] footers = in.getFooters();
103         Assertions.assertNotNull(footers);
104         Assertions.assertEquals(2, footers.length);
105         Assertions.assertEquals("Footer1", footers[0].getName());
106         Assertions.assertEquals("abcde", footers[0].getValue());
107         Assertions.assertEquals("Footer2", footers[1].getName());
108         Assertions.assertEquals("fghij", footers[1].getValue());
109     }
110 
111     // One byte read
112     @Test
113     public void testChunkedInputStreamOneByteRead() throws IOException {
114         final String s = "5\r\n01234\r\n5\r\n56789\r\n0\r\n";
115         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
116         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
117         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
118         int ch;
119         int i = '0';
120         while ((ch = in.read()) != -1) {
121             Assertions.assertEquals(i, ch);
122             i++;
123         }
124         Assertions.assertEquals(-1, in.read());
125         Assertions.assertEquals(-1, in.read());
126 
127         in.close();
128     }
129 
130     @Test
131     public void testAvailable() throws IOException {
132         final String s = "5\r\n12345\r\n0\r\n";
133         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
134         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
135         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
136         Assertions.assertEquals(0, in.available());
137         in.read();
138         Assertions.assertEquals(4, in.available());
139         in.close();
140     }
141 
142     @Test
143     public void testChunkedInputStreamClose() throws IOException {
144         final String s = "5\r\n01234\r\n5\r\n56789\r\n0\r\n";
145         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
146         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
147         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
148         in.close();
149         in.close();
150         Assertions.assertThrows(StreamClosedException.class, () -> in.read());
151         final byte[] tmp = new byte[10];
152         Assertions.assertThrows(StreamClosedException.class, () -> in.read(tmp));
153         Assertions.assertThrows(StreamClosedException.class, () -> in.read(tmp, 0, tmp.length));
154     }
155 
156     // Missing closing chunk
157     @Test
158     public void testChunkedInputStreamNoClosingChunk() throws IOException {
159         final String s = "5\r\n01234\r\n";
160         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
161         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
162         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
163         final byte[] tmp = new byte[5];
164         Assertions.assertEquals(5, in.read(tmp));
165         Assertions.assertThrows(ConnectionClosedException.class, () -> in.read());
166         Assertions.assertThrows(ConnectionClosedException.class, () -> in.close());
167     }
168 
169     // Truncated stream (missing closing CRLF)
170     @Test
171     public void testCorruptChunkedInputStreamTruncatedCRLF() throws IOException {
172         final String s = "5\r\n01234";
173         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
174         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
175         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
176         final byte[] tmp = new byte[5];
177         Assertions.assertEquals(5, in.read(tmp));
178         Assertions.assertThrows(MalformedChunkCodingException.class, () -> in.read());
179         in.close();
180     }
181 
182     // Missing \r\n at the end of the first chunk
183     @Test
184     public void testCorruptChunkedInputStreamMissingCRLF() throws IOException {
185         final String s = "5\r\n012345\r\n56789\r\n0\r\n";
186         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
187         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
188         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
189         final byte[] buffer = new byte[300];
190         final ByteArrayOutputStream out = new ByteArrayOutputStream();
191         Assertions.assertThrows(MalformedChunkCodingException.class, () -> {
192             int len;
193             while ((len = in.read(buffer)) > 0) {
194                 out.write(buffer, 0, len);
195             }
196         });
197         in.close();
198     }
199 
200     // Missing LF
201     @Test
202     public void testCorruptChunkedInputStreamMissingLF() throws IOException {
203         final String s = "5\r01234\r\n5\r\n56789\r\n0\r\n";
204         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
205         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
206         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
207         Assertions.assertThrows(MalformedChunkCodingException.class, in::read);
208         in.close();
209     }
210 
211     // Invalid chunk size
212     @Test
213     public void testCorruptChunkedInputStreamInvalidSize() throws IOException {
214         final String s = "whatever\r\n01234\r\n5\r\n56789\r\n0\r\n";
215         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
216         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
217         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
218         Assertions.assertThrows(MalformedChunkCodingException.class, in::read);
219         in.close();
220     }
221 
222     // Negative chunk size
223     @Test
224     public void testCorruptChunkedInputStreamNegativeSize() throws IOException {
225         final String s = "-5\r\n01234\r\n5\r\n56789\r\n0\r\n";
226         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
227         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
228         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
229         Assertions.assertThrows(MalformedChunkCodingException.class, in::read);
230         in.close();
231     }
232 
233     // Truncated chunk
234     @Test
235     public void testCorruptChunkedInputStreamTruncatedChunk() throws IOException {
236         final String s = "3\r\n12";
237         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
238         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
239         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
240         final byte[] buffer = new byte[300];
241         Assertions.assertEquals(2, in.read(buffer));
242         Assertions.assertThrows(MalformedChunkCodingException.class, () -> in.read(buffer));
243         in.close();
244     }
245 
246     // Invalid footer
247     @Test
248     public void testCorruptChunkedInputStreamInvalidFooter() throws IOException {
249         final String s = "1\r\n0\r\n0\r\nstuff\r\n";
250         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
251         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
252         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
253         in.read();
254         Assertions.assertThrows(MalformedChunkCodingException.class, in::read);
255         in.close();
256     }
257 
258     @Test
259     public void testCorruptChunkedInputStreamClose() throws IOException {
260         final String s = "whatever\r\n01234\r\n5\r\n56789\r\n0\r\n";
261         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
262         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
263         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
264         Assertions.assertThrows(MalformedChunkCodingException.class, in::read);
265     }
266 
267     @Test
268     public void testEmptyChunkedInputStream() throws IOException {
269         final String s = "0\r\n";
270         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
271         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
272         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
273         final byte[] buffer = new byte[300];
274         final ByteArrayOutputStream out = new ByteArrayOutputStream();
275         int len;
276         while ((len = in.read(buffer)) > 0) {
277             out.write(buffer, 0, len);
278         }
279         Assertions.assertEquals(0, out.size());
280         in.close();
281     }
282 
283     @Test
284     public void testTooLongChunkHeader() throws IOException {
285         final String s = "5; and some very looooong commend\r\n12345\r\n0\r\n";
286         final SessionInputBuffer inBuffer1 = new SessionInputBufferImpl(16);
287         final ByteArrayInputStream inputStream1 = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
288         final ChunkedInputStream in1 = new ChunkedInputStream(inBuffer1, inputStream1);
289         final byte[] buffer = new byte[300];
290         Assertions.assertEquals(5, in1.read(buffer));
291         in1.close();
292 
293         final SessionInputBuffer inBuffer2 = new SessionInputBufferImpl(16, 10);
294         final ByteArrayInputStream inputStream2 = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
295         final ChunkedInputStream in2 = new ChunkedInputStream(inBuffer2, inputStream2);
296         Assertions.assertThrows(MessageConstraintException.class, () -> in2.read(buffer));
297     }
298 
299     @Test
300     public void testChunkedOutputStreamClose() throws IOException {
301         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
302         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
303         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2048);
304         out.close();
305         out.close();
306         Assertions.assertThrows(IOException.class, () -> out.write(new byte[] {1,2,3}));
307         Assertions.assertThrows(IOException.class, () -> out.write(1));
308     }
309 
310     @Test
311     public void testChunkedConsistence() throws IOException {
312         final String input = "76126;27823abcd;:q38a-\nkjc\rk%1ad\tkh/asdui\r\njkh+?\\suweb";
313         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
314         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
315         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2048);
316         out.write(input.getBytes(StandardCharsets.ISO_8859_1));
317         out.flush();
318         out.close();
319         out.close();
320         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
321         final ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
322         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
323 
324         final byte[] d = new byte[10];
325         final ByteArrayOutputStream result = new ByteArrayOutputStream();
326         int len = 0;
327         while ((len = in.read(d)) > 0) {
328             result.write(d, 0, len);
329         }
330 
331         final String output = new String(result.toByteArray(), StandardCharsets.ISO_8859_1);
332         Assertions.assertEquals(input, output);
333         in.close();
334     }
335 
336     @Test
337     public void testChunkedOutputStreamWithTrailers() throws IOException {
338         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
339         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
340         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2, () -> Arrays.asList(
341                 new BasicHeader("E", ""),
342                 new BasicHeader("Y", "Z"))
343         );
344         out.write('x');
345         out.finish();
346         out.close();
347 
348         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
349         Assertions.assertEquals("1\r\nx\r\n0\r\nE: \r\nY: Z\r\n\r\n", content);
350     }
351 
352     @Test
353     public void testChunkedOutputStream() throws IOException {
354         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
355         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
356         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2);
357         out.write('1');
358         out.write('2');
359         out.write('3');
360         out.write('4');
361         out.finish();
362         out.close();
363 
364         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
365         Assertions.assertEquals("2\r\n12\r\n2\r\n34\r\n0\r\n\r\n", content);
366     }
367 
368     @Test
369     public void testChunkedOutputStreamLargeChunk() throws IOException {
370         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
371         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
372         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2);
373         out.write(new byte[] {'1', '2', '3', '4'});
374         out.finish();
375         out.close();
376 
377         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
378         Assertions.assertEquals("4\r\n1234\r\n0\r\n\r\n", content);
379     }
380 
381     @Test
382     public void testChunkedOutputStreamSmallChunk() throws IOException {
383         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
384         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
385         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2);
386         out.write('1');
387         out.finish();
388         out.close();
389 
390         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
391         Assertions.assertEquals("1\r\n1\r\n0\r\n\r\n", content);
392     }
393 
394     @Test
395     public void testResumeOnSocketTimeoutInData() throws IOException {
396         final String s = "5\r\n01234\r\n5\r\n5\0006789\r\na\r\n0123\000456789\r\n0\r\n";
397         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
398         final TimeoutByteArrayInputStream inputStream = new TimeoutByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
399         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
400 
401         final byte[] tmp = new byte[3];
402 
403         int bytesRead = 0;
404         int timeouts = 0;
405 
406         int i = 0;
407         while (i != -1) {
408             try {
409                 i = in.read(tmp);
410                 if (i > 0) {
411                     bytesRead += i;
412                 }
413             } catch (final InterruptedIOException ex) {
414                 timeouts++;
415             }
416         }
417         Assertions.assertEquals(20, bytesRead);
418         Assertions.assertEquals(2, timeouts);
419         in.close();
420 }
421 
422     @Test
423     public void testResumeOnSocketTimeoutInChunk() throws IOException {
424         final String s = "5\000\r\000\n\00001234\r\n\0005\r\n56789\r\na\r\n0123456789\r\n\0000\r\n";
425         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
426         final TimeoutByteArrayInputStream inputStream = new TimeoutByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
427         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
428 
429         final byte[] tmp = new byte[3];
430 
431         int bytesRead = 0;
432         int timeouts = 0;
433 
434         int i = 0;
435         while (i != -1) {
436             try {
437                 i = in.read(tmp);
438                 if (i > 0) {
439                     bytesRead += i;
440                 }
441             } catch (final InterruptedIOException ex) {
442                 timeouts++;
443             }
444         }
445         Assertions.assertEquals(20, bytesRead);
446         Assertions.assertEquals(5, timeouts);
447         in.close();
448     }
449 
450     // Test for when buffer is larger than chunk size
451     @Test
452     public void testHugeChunk() throws IOException {
453 
454         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
455         final ByteArrayInputStream inputStream = new ByteArrayInputStream("1234567890abcdef\r\n01234567".getBytes(
456                 StandardCharsets.ISO_8859_1));
457         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
458 
459         final ByteArrayOutputStream out = new ByteArrayOutputStream();
460         for (int i = 0; i < 8; ++i) {
461             out.write(in.read());
462         }
463 
464         final String result = new String(out.toByteArray(), StandardCharsets.ISO_8859_1);
465         Assertions.assertEquals("01234567", result);
466     }
467 
468 }
469