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         try (final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream)) {
264             Assertions.assertThrows(MalformedChunkCodingException.class, in::read);
265         }
266     }
267 
268     @Test
269     public void testEmptyChunkedInputStream() throws IOException {
270         final String s = "0\r\n";
271         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
272         final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
273         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
274         final byte[] buffer = new byte[300];
275         final ByteArrayOutputStream out = new ByteArrayOutputStream();
276         int len;
277         while ((len = in.read(buffer)) > 0) {
278             out.write(buffer, 0, len);
279         }
280         Assertions.assertEquals(0, out.size());
281         in.close();
282     }
283 
284     @Test
285     public void testTooLongChunkHeader() throws IOException {
286         final String s = "5; and some very looooong commend\r\n12345\r\n0\r\n";
287         final SessionInputBuffer inBuffer1 = new SessionInputBufferImpl(16);
288         final byte[] buffer = new byte[300];
289         final ByteArrayInputStream inputStream1 = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
290         try (final ChunkedInputStream in1 = new ChunkedInputStream(inBuffer1, inputStream1)) {
291             Assertions.assertEquals(5, in1.read(buffer));
292         }
293 
294         final SessionInputBuffer inBuffer2 = new SessionInputBufferImpl(16, 10);
295         final ByteArrayInputStream inputStream2 = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
296         final ChunkedInputStream in2 = new ChunkedInputStream(inBuffer2, inputStream2);
297         Assertions.assertThrows(MessageConstraintException.class, () -> in2.read(buffer));
298     }
299 
300     @Test
301     public void testChunkedOutputStreamClose() throws IOException {
302         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
303         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
304         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2048);
305         out.close();
306         out.close();
307         Assertions.assertThrows(IOException.class, () -> out.write(new byte[] {1,2,3}));
308         Assertions.assertThrows(IOException.class, () -> out.write(1));
309     }
310 
311     @Test
312     public void testChunkedConsistence() throws IOException {
313         final String input = "76126;27823abcd;:q38a-\nkjc\rk%1ad\tkh/asdui\r\njkh+?\\suweb";
314         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
315         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
316         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2048);
317         out.write(input.getBytes(StandardCharsets.ISO_8859_1));
318         out.flush();
319         out.close();
320         out.close();
321         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
322         final ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
323         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
324 
325         final byte[] d = new byte[10];
326         final ByteArrayOutputStream result = new ByteArrayOutputStream();
327         int len = 0;
328         while ((len = in.read(d)) > 0) {
329             result.write(d, 0, len);
330         }
331 
332         final String output = new String(result.toByteArray(), StandardCharsets.ISO_8859_1);
333         Assertions.assertEquals(input, output);
334         in.close();
335     }
336 
337     @Test
338     public void testChunkedOutputStreamWithTrailers() throws IOException {
339         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
340         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
341         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2, () -> Arrays.asList(
342                 new BasicHeader("E", ""),
343                 new BasicHeader("Y", "Z"))
344         );
345         out.write('x');
346         out.finish();
347         out.close();
348 
349         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
350         Assertions.assertEquals("1\r\nx\r\n0\r\nE: \r\nY: Z\r\n\r\n", content);
351     }
352 
353     @Test
354     public void testChunkedOutputStream() throws IOException {
355         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
356         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
357         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2);
358         out.write('1');
359         out.write('2');
360         out.write('3');
361         out.write('4');
362         out.finish();
363         out.close();
364 
365         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
366         Assertions.assertEquals("2\r\n12\r\n2\r\n34\r\n0\r\n\r\n", content);
367     }
368 
369     @Test
370     public void testChunkedOutputStreamLargeChunk() throws IOException {
371         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
372         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
373         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2);
374         out.write(new byte[] {'1', '2', '3', '4'});
375         out.finish();
376         out.close();
377 
378         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
379         Assertions.assertEquals("4\r\n1234\r\n0\r\n\r\n", content);
380     }
381 
382     @Test
383     public void testChunkedOutputStreamSmallChunk() throws IOException {
384         final SessionOutputBuffer outbuffer = new SessionOutputBufferImpl(16);
385         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
386         final ChunkedOutputStream out = new ChunkedOutputStream(outbuffer, outputStream, 2);
387         out.write('1');
388         out.finish();
389         out.close();
390 
391         final String content = new String(outputStream.toByteArray(), StandardCharsets.US_ASCII);
392         Assertions.assertEquals("1\r\n1\r\n0\r\n\r\n", content);
393     }
394 
395     @Test
396     public void testResumeOnSocketTimeoutInData() throws IOException {
397         final String s = "5\r\n01234\r\n5\r\n5\0006789\r\na\r\n0123\000456789\r\n0\r\n";
398         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
399         final TimeoutByteArrayInputStream inputStream = new TimeoutByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
400         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
401 
402         final byte[] tmp = new byte[3];
403 
404         int bytesRead = 0;
405         int timeouts = 0;
406 
407         int i = 0;
408         while (i != -1) {
409             try {
410                 i = in.read(tmp);
411                 if (i > 0) {
412                     bytesRead += i;
413                 }
414             } catch (final InterruptedIOException ex) {
415                 timeouts++;
416             }
417         }
418         Assertions.assertEquals(20, bytesRead);
419         Assertions.assertEquals(2, timeouts);
420         in.close();
421 }
422 
423     @Test
424     public void testResumeOnSocketTimeoutInChunk() throws IOException {
425         final String s = "5\000\r\000\n\00001234\r\n\0005\r\n56789\r\na\r\n0123456789\r\n\0000\r\n";
426         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
427         final TimeoutByteArrayInputStream inputStream = new TimeoutByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1));
428         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
429 
430         final byte[] tmp = new byte[3];
431 
432         int bytesRead = 0;
433         int timeouts = 0;
434 
435         int i = 0;
436         while (i != -1) {
437             try {
438                 i = in.read(tmp);
439                 if (i > 0) {
440                     bytesRead += i;
441                 }
442             } catch (final InterruptedIOException ex) {
443                 timeouts++;
444             }
445         }
446         Assertions.assertEquals(20, bytesRead);
447         Assertions.assertEquals(5, timeouts);
448         in.close();
449     }
450 
451     // Test for when buffer is larger than chunk size
452     @Test
453     public void testHugeChunk() throws IOException {
454 
455         final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16);
456         final ByteArrayInputStream inputStream = new ByteArrayInputStream("1234567890abcdef\r\n01234567".getBytes(
457                 StandardCharsets.ISO_8859_1));
458         final ChunkedInputStream in = new ChunkedInputStream(inBuffer, inputStream);
459 
460         final ByteArrayOutputStream out = new ByteArrayOutputStream();
461         for (int i = 0; i < 8; ++i) {
462             out.write(in.read());
463         }
464 
465         final String result = new String(out.toByteArray(), StandardCharsets.ISO_8859_1);
466         Assertions.assertEquals("01234567", result);
467     }
468 
469 }
470