1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */

17 package org.apache.coyote.http11.filters;
18
19 import java.io.EOFException;
20 import java.io.IOException;
21 import java.nio.ByteBuffer;
22 import java.nio.charset.StandardCharsets;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.apache.coyote.InputBuffer;
28 import org.apache.coyote.Request;
29 import org.apache.coyote.http11.Constants;
30 import org.apache.coyote.http11.InputFilter;
31 import org.apache.tomcat.util.buf.ByteChunk;
32 import org.apache.tomcat.util.buf.HexUtils;
33 import org.apache.tomcat.util.net.ApplicationBufferHandler;
34 import org.apache.tomcat.util.res.StringManager;
35
36 /**
37  * Chunked input filter. Parses chunked data according to
38  * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1</a><br>
39  *
40  * @author Remy Maucherat
41  */

42 public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler {
43
44     private static final StringManager sm = StringManager.getManager(
45             ChunkedInputFilter.class.getPackage().getName());
46
47
48     // -------------------------------------------------------------- Constants
49
50     protected static final String ENCODING_NAME = "chunked";
51     protected static final ByteChunk ENCODING = new ByteChunk();
52
53
54     // ----------------------------------------------------- Static Initializer
55
56     static {
57         ENCODING.setBytes(ENCODING_NAME.getBytes(StandardCharsets.ISO_8859_1),
58                 0, ENCODING_NAME.length());
59     }
60
61
62     // ----------------------------------------------------- Instance Variables
63
64     /**
65      * Next buffer in the pipeline.
66      */

67     protected InputBuffer buffer;
68
69
70     /**
71      * Number of bytes remaining in the current chunk.
72      */

73     protected int remaining = 0;
74
75
76     /**
77      * Byte chunk used to read bytes.
78      */

79     protected ByteBuffer readChunk;
80
81
82     /**
83      * Flag set to true when the end chunk has been read.
84      */

85     protected boolean endChunk = false;
86
87
88     /**
89      * Byte chunk used to store trailing headers.
90      */

91     protected final ByteChunk trailingHeaders = new ByteChunk();
92
93
94     /**
95      * Flag set to true if the next call to doRead() must parse a CRLF pair
96      * before doing anything else.
97      */

98     protected boolean needCRLFParse = false;
99
100
101     /**
102      * Request being parsed.
103      */

104     private Request request;
105
106
107     /**
108      * Limit for extension size.
109      */

110     private final long maxExtensionSize;
111
112
113     /**
114      * Limit for trailer size.
115      */

116     private final int maxTrailerSize;
117
118
119     /**
120      * Size of extensions processed for this request.
121      */

122     private long extensionSize;
123
124
125     private final int maxSwallowSize;
126
127
128     /**
129      * Flag that indicates if an error has occurred.
130      */

131     private boolean error;
132
133
134     private final Set<String> allowedTrailerHeaders;
135
136     // ----------------------------------------------------------- Constructors
137
138     public ChunkedInputFilter(int maxTrailerSize, Set<String> allowedTrailerHeaders,
139             int maxExtensionSize, int maxSwallowSize) {
140         this.trailingHeaders.setLimit(maxTrailerSize);
141         this.allowedTrailerHeaders = allowedTrailerHeaders;
142         this.maxExtensionSize = maxExtensionSize;
143         this.maxTrailerSize = maxTrailerSize;
144         this.maxSwallowSize = maxSwallowSize;
145     }
146
147
148     // ---------------------------------------------------- InputBuffer Methods
149
150     @Override
151     public int doRead(ApplicationBufferHandler handler) throws IOException {
152         if (endChunk) {
153             return -1;
154         }
155
156         checkError();
157
158         if(needCRLFParse) {
159             needCRLFParse = false;
160             parseCRLF(false);
161         }
162
163         if (remaining <= 0) {
164             if (!parseChunkHeader()) {
165                 throwIOException(sm.getString("chunkedInputFilter.invalidHeader"));
166             }
167             if (endChunk) {
168                 parseEndChunk();
169                 return -1;
170             }
171         }
172
173         int result = 0;
174
175         if (readChunk == null || readChunk.position() >= readChunk.limit()) {
176             if (readBytes() < 0) {
177                 throwIOException(sm.getString("chunkedInputFilter.eos"));
178             }
179         }
180
181         if (remaining > readChunk.remaining()) {
182             result = readChunk.remaining();
183             remaining = remaining - result;
184             if (readChunk != handler.getByteBuffer()) {
185                 handler.setByteBuffer(readChunk.duplicate());
186             }
187             readChunk.position(readChunk.limit());
188         } else {
189             result = remaining;
190             if (readChunk != handler.getByteBuffer()) {
191                 handler.setByteBuffer(readChunk.duplicate());
192                 handler.getByteBuffer().limit(readChunk.position() + remaining);
193             }
194             readChunk.position(readChunk.position() + remaining);
195             remaining = 0;
196             //we need a CRLF
197             if ((readChunk.position() + 1) >= readChunk.limit()) {
198                 //if we call parseCRLF we overrun the buffer here
199                 //so we defer it to the next call BZ 11117
200                 needCRLFParse = true;
201             } else {
202                 parseCRLF(false); //parse the CRLF immediately
203             }
204         }
205
206         return result;
207     }
208
209
210     // ---------------------------------------------------- InputFilter Methods
211
212     /**
213      * Read the content length from the request.
214      */

215     @Override
216     public void setRequest(Request request) {
217         this.request = request;
218     }
219
220
221     /**
222      * End the current request.
223      */

224     @Override
225     public long end() throws IOException {
226         long swallowed = 0;
227         int read = 0;
228         // Consume extra bytes : parse the stream until the end chunk is found
229         while ((read = doRead(this)) >= 0) {
230             swallowed += read;
231             if (maxSwallowSize > -1 && swallowed > maxSwallowSize) {
232                 throwIOException(sm.getString("inputFilter.maxSwallow"));
233             }
234         }
235
236         // Return the number of extra bytes which were consumed
237         return readChunk.remaining();
238     }
239
240
241     /**
242      * Amount of bytes still available in a buffer.
243      */

244     @Override
245     public int available() {
246         return readChunk != null ? readChunk.remaining() : 0;
247     }
248
249
250     /**
251      * Set the next buffer in the filter pipeline.
252      */

253     @Override
254     public void setBuffer(InputBuffer buffer) {
255         this.buffer = buffer;
256     }
257
258
259     /**
260      * Make the filter ready to process the next request.
261      */

262     @Override
263     public void recycle() {
264         remaining = 0;
265         if (readChunk != null) {
266             readChunk.position(0).limit(0);
267         }
268         endChunk = false;
269         needCRLFParse = false;
270         trailingHeaders.recycle();
271         trailingHeaders.setLimit(maxTrailerSize);
272         extensionSize = 0;
273         error = false;
274     }
275
276
277     /**
278      * Return the name of the associated encoding; Here, the value is
279      * "identity".
280      */

281     @Override
282     public ByteChunk getEncodingName() {
283         return ENCODING;
284     }
285
286
287     @Override
288     public boolean isFinished() {
289         return endChunk;
290     }
291
292
293     // ------------------------------------------------------ Protected Methods
294
295     /**
296      * Read bytes from the previous buffer.
297      * @return The byte count which has been read
298      * @throws IOException Read error
299      */

300     protected int readBytes() throws IOException {
301         return buffer.doRead(this);
302     }
303
304
305     /**
306      * Parse the header of a chunk.
307      * A chunk header can look like one of the following:<br>
308      * A10CRLF<br>
309      * F23;chunk-extension to be ignoredCRLF
310      *
311      * <p>
312      * The letters before CRLF or ';' (whatever comes first) must be valid hex
313      * digits. We should not parse F23IAMGONNAMESSTHISUP34CRLF as a valid
314      * header according to the spec.
315      * @return <code>true</code> if the chunk header has been
316      *  successfully parsed
317      * @throws IOException Read error
318      */

319     protected boolean parseChunkHeader() throws IOException {
320
321         int result = 0;
322         boolean eol = false;
323         int readDigit = 0;
324         boolean extension = false;
325
326         while (!eol) {
327
328             if (readChunk == null || readChunk.position() >= readChunk.limit()) {
329                 if (readBytes() <= 0)
330                     return false;
331             }
332
333             byte chr = readChunk.get(readChunk.position());
334             if (chr == Constants.CR || chr == Constants.LF) {
335                 parseCRLF(false);
336                 eol = true;
337             } else if (chr == Constants.SEMI_COLON && !extension) {
338                 // First semi-colon marks the start of the extension. Further
339                 // semi-colons may appear to separate multiple chunk-extensions.
340                 // These need to be processed as part of parsing the extensions.
341                 extension = true;
342                 extensionSize++;
343             } else if (!extension) {
344                 //don't read data after the trailer
345                 int charValue = HexUtils.getDec(chr);
346                 if (charValue != -1 && readDigit < 8) {
347                     readDigit++;
348                     result = (result << 4) | charValue;
349                 } else {
350                     //we shouldn't allow invalid, non hex characters
351                     //in the chunked header
352                     return false;
353                 }
354             } else {
355                 // Extension 'parsing'
356                 // Note that the chunk-extension is neither parsed nor
357                 // validated. Currently it is simply ignored.
358                 extensionSize++;
359                 if (maxExtensionSize > -1 && extensionSize > maxExtensionSize) {
360                     throwIOException(sm.getString("chunkedInputFilter.maxExtension"));
361                 }
362             }
363
364             // Parsing the CRLF increments pos
365             if (!eol) {
366                 readChunk.position(readChunk.position() + 1);
367             }
368         }
369
370         if (readDigit == 0 || result < 0) {
371             return false;
372         }
373
374         if (result == 0) {
375             endChunk = true;
376         }
377
378         remaining = result;
379         return true;
380     }
381
382
383     /**
384      * Parse CRLF at end of chunk.
385      *
386      * @param   tolerant    Should tolerant parsing (LF and CRLF) be used? This
387      *                      is recommended (RFC2616, section 19.3) for message
388      *                      headers.
389      * @throws IOException An error occurred parsing CRLF
390      */

391     protected void parseCRLF(boolean tolerant) throws IOException {
392
393         boolean eol = false;
394         boolean crfound = false;
395
396         while (!eol) {
397             if (readChunk == null || readChunk.position() >= readChunk.limit()) {
398                 if (readBytes() <= 0) {
399                     throwIOException(sm.getString("chunkedInputFilter.invalidCrlfNoData"));
400                 }
401             }
402
403             byte chr = readChunk.get(readChunk.position());
404             if (chr == Constants.CR) {
405                 if (crfound) {
406                     throwIOException(sm.getString("chunkedInputFilter.invalidCrlfCRCR"));
407                 }
408                 crfound = true;
409             } else if (chr == Constants.LF) {
410                 if (!tolerant && !crfound) {
411                     throwIOException(sm.getString("chunkedInputFilter.invalidCrlfNoCR"));
412                 }
413                 eol = true;
414             } else {
415                 throwIOException(sm.getString("chunkedInputFilter.invalidCrlf"));
416             }
417
418             readChunk.position(readChunk.position() + 1);
419         }
420     }
421
422
423     /**
424      * Parse end chunk data.
425      * @throws IOException Error propagation
426      */

427     protected void parseEndChunk() throws IOException {
428         // Handle optional trailer headers
429         while (parseHeader()) {
430             // Loop until we run out of headers
431         }
432     }
433
434
435     private boolean parseHeader() throws IOException {
436
437         Map<String,String> headers = request.getTrailerFields();
438
439         byte chr = 0;
440
441         // Read new bytes if needed
442         if (readChunk == null || readChunk.position() >= readChunk.limit()) {
443             if (readBytes() <0) {
444                throwEOFException(sm.getString("chunkedInputFilter.eosTrailer"));
445             }
446         }
447
448         // readBytes() above will set readChunk unless it returns a value < 0
449         chr = readChunk.get(readChunk.position());
450
451         // CRLF terminates the request
452         if (chr == Constants.CR || chr == Constants.LF) {
453             parseCRLF(false);
454             return false;
455         }
456
457         // Mark the current buffer position
458         int startPos = trailingHeaders.getEnd();
459
460         //
461         // Reading the header name
462         // Header name is always US-ASCII
463         //
464
465         boolean colon = false;
466         while (!colon) {
467
468             // Read new bytes if needed
469             if (readChunk == null || readChunk.position() >= readChunk.limit()) {
470                 if (readBytes() <0) {
471                     throwEOFException(sm.getString("chunkedInputFilter.eosTrailer"));
472                 }
473             }
474
475             // readBytes() above will set readChunk unless it returns a value < 0
476             chr = readChunk.get(readChunk.position());
477             if ((chr >= Constants.A) && (chr <= Constants.Z)) {
478                 chr = (byte) (chr - Constants.LC_OFFSET);
479             }
480
481             if (chr == Constants.COLON) {
482                 colon = true;
483             } else {
484                 trailingHeaders.append(chr);
485             }
486
487             readChunk.position(readChunk.position() + 1);
488
489         }
490         int colonPos = trailingHeaders.getEnd();
491
492         //
493         // Reading the header value (which can be spanned over multiple lines)
494         //
495
496         boolean eol = false;
497         boolean validLine = true;
498         int lastSignificantChar = 0;
499
500         while (validLine) {
501
502             boolean space = true;
503
504             // Skipping spaces
505             while (space) {
506
507                 // Read new bytes if needed
508                 if (readChunk == null || readChunk.position() >= readChunk.limit()) {
509                     if (readBytes() <0) {
510                         throwEOFException(sm.getString("chunkedInputFilter.eosTrailer"));
511                     }
512                 }
513
514                 chr = readChunk.get(readChunk.position());
515                 if ((chr == Constants.SP) || (chr == Constants.HT)) {
516                     readChunk.position(readChunk.position() + 1);
517                     // If we swallow whitespace, make sure it counts towards the
518                     // limit placed on trailing header size
519                     int newlimit = trailingHeaders.getLimit() -1;
520                     if (trailingHeaders.getEnd() > newlimit) {
521                         throwIOException(sm.getString("chunkedInputFilter.maxTrailer"));
522                     }
523                     trailingHeaders.setLimit(newlimit);
524                 } else {
525                     space = false;
526                 }
527
528             }
529
530             // Reading bytes until the end of the line
531             while (!eol) {
532
533                 // Read new bytes if needed
534                 if (readChunk == null || readChunk.position() >= readChunk.limit()) {
535                     if (readBytes() <0) {
536                         throwEOFException(sm.getString("chunkedInputFilter.eosTrailer"));
537                     }
538                 }
539
540                 chr = readChunk.get(readChunk.position());
541                 if (chr == Constants.CR || chr == Constants.LF) {
542                     parseCRLF(true);
543                     eol = true;
544                 } else if (chr == Constants.SP) {
545                     trailingHeaders.append(chr);
546                 } else {
547                     trailingHeaders.append(chr);
548                     lastSignificantChar = trailingHeaders.getEnd();
549                 }
550
551                 if (!eol) {
552                     readChunk.position(readChunk.position() + 1);
553                 }
554             }
555
556             // Checking the first character of the new line. If the character
557             // is a LWS, then it's a multiline header
558
559             // Read new bytes if needed
560             if (readChunk == null || readChunk.position() >= readChunk.limit()) {
561                 if (readBytes() <0) {
562                     throwEOFException(sm.getString("chunkedInputFilter.eosTrailer"));
563                 }
564             }
565
566             chr = readChunk.get(readChunk.position());
567             if ((chr != Constants.SP) && (chr != Constants.HT)) {
568                 validLine = false;
569             } else {
570                 eol = false;
571                 // Copying one extra space in the buffer (since there must
572                 // be at least one space inserted between the lines)
573                 trailingHeaders.append(chr);
574             }
575
576         }
577
578         String headerName = new String(trailingHeaders.getBytes(), startPos,
579                 colonPos - startPos, StandardCharsets.ISO_8859_1);
580
581         headerName = headerName.toLowerCase(Locale.ENGLISH);
582
583         if (allowedTrailerHeaders.contains(headerName)) {
584
585             String value = new String(trailingHeaders.getBytes(), colonPos,
586                     lastSignificantChar - colonPos, StandardCharsets.ISO_8859_1);
587
588             headers.put(headerName, value);
589         }
590
591         return true;
592     }
593
594
595     private void throwIOException(String msg) throws IOException {
596         error = true;
597         throw new IOException(msg);
598     }
599
600
601     private void throwEOFException(String msg) throws IOException {
602         error = true;
603         throw new EOFException(msg);
604     }
605
606
607     private void checkError() throws IOException {
608         if (error) {
609             throw new IOException(sm.getString("chunkedInputFilter.error"));
610         }
611     }
612
613
614     @Override
615     public void setByteBuffer(ByteBuffer buffer) {
616         readChunk = buffer;
617     }
618
619
620     @Override
621     public ByteBuffer getByteBuffer() {
622         return readChunk;
623     }
624
625
626     @Override
627     public void expand(int size) {
628         // no-op
629     }
630 }
631