Skip to content

Commit 276faa5

Browse files
committed
Improve parsing of Range request headers
1 parent d4509f4 commit 276faa5

File tree

6 files changed

+249
-68
lines changed

6 files changed

+249
-68
lines changed

TOMCAT-NEXT.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ New items for 10.0.x onwards:
5858
9. BZ 56966. Refactor internal request timing to use System.nanoTime()
5959

6060
10. BZ 63286. Make behaviour of %D and %T consistent with httpd.
61+
62+
11. Refactor DefaultServlet to use Ranges in parseRanges()
63+

java/org/apache/catalina/servlets/DefaultServlet.java

Lines changed: 25 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import org.apache.catalina.webresources.CachedResource;
8181
import org.apache.tomcat.util.buf.B2CConverter;
8282
import org.apache.tomcat.util.http.ResponseUtil;
83+
import org.apache.tomcat.util.http.parser.Ranges;
8384
import org.apache.tomcat.util.res.StringManager;
8485
import org.apache.tomcat.util.security.Escape;
8586
import org.apache.tomcat.util.security.PrivilegedGetTccl;
@@ -1480,87 +1481,51 @@ protected ArrayList<Range> parseRange(HttpServletRequest request,
14801481

14811482
long fileLength = resource.getContentLength();
14821483

1483-
if (fileLength == 0)
1484+
if (fileLength == 0) {
14841485
return null;
1486+
}
14851487

14861488
// Retrieving the range header (if any is specified
14871489
String rangeHeader = request.getHeader("Range");
14881490

1489-
if (rangeHeader == null)
1491+
if (rangeHeader == null) {
14901492
return null;
1493+
}
1494+
1495+
Ranges ranges = Ranges.parseRanges(new StringReader(rangeHeader));
1496+
14911497
// bytes is the only range unit supported (and I don't see the point
14921498
// of adding new ones).
1493-
if (!rangeHeader.startsWith("bytes")) {
1499+
if (ranges == null || !ranges.getUnits().equals("bytes")) {
14941500
response.addHeader("Content-Range", "bytes */" + fileLength);
1495-
response.sendError
1496-
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1501+
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
14971502
return null;
14981503
}
14991504

1500-
rangeHeader = rangeHeader.substring(6);
1501-
1502-
// Collection which will contain all the ranges which are successfully
1503-
// parsed.
1505+
// TODO: Remove the internal representation and use Ranges
1506+
// Convert to internal representation
15041507
ArrayList<Range> result = new ArrayList<>();
1505-
StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
1506-
1507-
// Parsing the range list
1508-
while (commaTokenizer.hasMoreTokens()) {
1509-
String rangeDefinition = commaTokenizer.nextToken().trim();
15101508

1509+
for (Ranges.Entry entry : ranges.getEntries()) {
15111510
Range currentRange = new Range();
1512-
currentRange.length = fileLength;
1513-
1514-
int dashPos = rangeDefinition.indexOf('-');
1515-
1516-
if (dashPos == -1) {
1517-
response.addHeader("Content-Range", "bytes */" + fileLength);
1518-
response.sendError
1519-
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1520-
return null;
1521-
}
1522-
1523-
if (dashPos == 0) {
1524-
1525-
try {
1526-
long offset = Long.parseLong(rangeDefinition);
1527-
currentRange.start = fileLength + offset;
1528-
currentRange.end = fileLength - 1;
1529-
} catch (NumberFormatException e) {
1530-
response.addHeader("Content-Range",
1531-
"bytes */" + fileLength);
1532-
response.sendError
1533-
(HttpServletResponse
1534-
.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1535-
return null;
1511+
if (entry.getStart() == -1) {
1512+
currentRange.start = fileLength - entry.getEnd();
1513+
if (currentRange.start < 0) {
1514+
currentRange.start = 0;
15361515
}
1537-
1516+
currentRange.end = fileLength - 1;
1517+
} else if (entry.getEnd() == -1) {
1518+
currentRange.start = entry.getStart();
1519+
currentRange.end = fileLength - 1;
15381520
} else {
1539-
1540-
try {
1541-
currentRange.start = Long.parseLong
1542-
(rangeDefinition.substring(0, dashPos));
1543-
if (dashPos < rangeDefinition.length() - 1)
1544-
currentRange.end = Long.parseLong
1545-
(rangeDefinition.substring
1546-
(dashPos + 1, rangeDefinition.length()));
1547-
else
1548-
currentRange.end = fileLength - 1;
1549-
} catch (NumberFormatException e) {
1550-
response.addHeader("Content-Range",
1551-
"bytes */" + fileLength);
1552-
response.sendError
1553-
(HttpServletResponse
1554-
.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1555-
return null;
1556-
}
1557-
1521+
currentRange.start = entry.getStart();
1522+
currentRange.end = entry.getEnd();
15581523
}
1524+
currentRange.length = fileLength;
15591525

15601526
if (!currentRange.validate()) {
15611527
response.addHeader("Content-Range", "bytes */" + fileLength);
1562-
response.sendError
1563-
(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
1528+
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
15641529
return null;
15651530
}
15661531

java/org/apache/tomcat/util/http/parser/HttpParser.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,43 @@ static String readToken(Reader input) throws IOException {
384384
}
385385
}
386386

387+
/**
388+
* @return the digits if any were found, the empty string if no data was
389+
* found or if data other than digits was found
390+
*/
391+
static String readDigits(Reader input) throws IOException {
392+
StringBuilder result = new StringBuilder();
393+
394+
skipLws(input);
395+
input.mark(1);
396+
int c = input.read();
397+
398+
while (c != -1 && isNumeric(c)) {
399+
result.append((char) c);
400+
input.mark(1);
401+
c = input.read();
402+
}
403+
// Use mark(1)/reset() rather than skip(-1) since skip() is a NOP
404+
// once the end of the String has been reached.
405+
input.reset();
406+
407+
return result.toString();
408+
}
409+
410+
/**
411+
* @return the number if digits were found, -1 if no data was found
412+
* or if data other than digits was found
413+
*/
414+
static long readLong(Reader input) throws IOException {
415+
String digits = readDigits(input);
416+
417+
if (digits.length() == 0) {
418+
return -1;
419+
}
420+
421+
return Long.parseLong(digits);
422+
}
423+
387424
/**
388425
* @return the quoted string if one was found, null if data other than a
389426
* quoted string was found or null if the end of data was reached
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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.tomcat.util.http.parser;
18+
19+
import java.io.IOException;
20+
import java.io.StringReader;
21+
import java.util.ArrayList;
22+
import java.util.Collections;
23+
import java.util.List;
24+
25+
public class Ranges {
26+
27+
private final String units;
28+
private final List<Entry> entries;
29+
30+
31+
private Ranges(String units, List<Entry> entries) {
32+
this.units = units;
33+
this.entries = Collections.unmodifiableList(entries);
34+
}
35+
36+
37+
public List<Entry> getEntries() {
38+
return entries;
39+
}
40+
41+
public String getUnits() {
42+
return units;
43+
}
44+
45+
46+
public static class Entry {
47+
48+
private final long start;
49+
private final long end;
50+
51+
52+
public Entry(long start, long end) {
53+
this.start = start;
54+
this.end = end;
55+
}
56+
57+
58+
public long getStart() {
59+
return start;
60+
}
61+
62+
63+
public long getEnd() {
64+
return end;
65+
}
66+
}
67+
68+
69+
/**
70+
* Parses a Range header from an HTTP header.
71+
*
72+
* @param input a reader over the header text
73+
*
74+
* @return a set of ranges parsed from the input, or null if not valid
75+
*
76+
* @throws IOException if there was a problem reading the input
77+
*/
78+
public static Ranges parseRanges(StringReader input) throws IOException {
79+
80+
// Units (required)
81+
String units = HttpParser.readToken(input);
82+
if (units == null || units.length() == 0) {
83+
return null;
84+
}
85+
86+
// Must be followed by '='
87+
if (HttpParser.skipConstant(input, "=") == SkipResult.NOT_FOUND) {
88+
return null;
89+
}
90+
91+
// Range entries
92+
List<Entry> entries = new ArrayList<>();
93+
94+
SkipResult skipResult;
95+
do {
96+
long start = HttpParser.readLong(input);
97+
// Must be followed by '-'
98+
if (HttpParser.skipConstant(input, "-") == SkipResult.NOT_FOUND) {
99+
return null;
100+
}
101+
long end = HttpParser.readLong(input);
102+
103+
if (start == -1 && end == -1) {
104+
// Invalid range
105+
return null;
106+
}
107+
108+
entries.add(new Entry(start, end));
109+
110+
skipResult = HttpParser.skipConstant(input, ",");
111+
if (skipResult == SkipResult.NOT_FOUND) {
112+
// Invalid range
113+
return null;
114+
}
115+
} while (skipResult == SkipResult.FOUND);
116+
117+
// There must be at least one entry
118+
if (entries.size() == 0) {
119+
return null;
120+
}
121+
122+
return new Ranges(units, entries);
123+
}
124+
}

test/org/apache/catalina/servlets/TestDefaultServletRangeRequests.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,50 @@
3737
@RunWith(Parameterized.class)
3838
public class TestDefaultServletRangeRequests extends TomcatBaseTest {
3939

40-
@Parameterized.Parameters
40+
// TODO: Add a check for response headers
41+
@Parameterized.Parameters(name = "{index} rangeHeader [{0}]")
4142
public static Collection<Object[]> parameters() {
43+
44+
// Note: The index page used by this test has a content-length of 934 bytes
4245
List<Object[]> parameterSets = new ArrayList<>();
4346

44-
parameterSets.add(new Object[] { null, Integer.valueOf(200)});
47+
parameterSets.add(new Object[] { "", Integer.valueOf(200), "934", "" });
4548
// Invalid
46-
// Commented out as these tests currently fail
47-
//parameterSets.add(new Object[] { buildRangeHeader("bytes"), Integer.valueOf(416)});
48-
//parameterSets.add(new Object[] { buildRangeHeader("bytes="), Integer.valueOf(416)});
49+
parameterSets.add(new Object[] { "bytes", Integer.valueOf(416), "", "*/934" });
50+
parameterSets.add(new Object[] { "bytes=", Integer.valueOf(416), "", "*/934" });
51+
// Invalid with unknown type
52+
parameterSets.add(new Object[] { "unknown", Integer.valueOf(416), "", "*/934" });
53+
parameterSets.add(new Object[] { "unknown=", Integer.valueOf(416), "", "*/934" });
54+
// Invalid ranges
55+
parameterSets.add(new Object[] { "bytes=-", Integer.valueOf(416), "", "*/934" });
56+
parameterSets.add(new Object[] { "bytes=10-b", Integer.valueOf(416), "", "*/934" });
57+
parameterSets.add(new Object[] { "bytes=b-10", Integer.valueOf(416), "", "*/934" });
58+
// Invalid no equals
59+
parameterSets.add(new Object[] { "bytes 1-10", Integer.valueOf(416), "", "*/934" });
60+
parameterSets.add(new Object[] { "bytes1-10", Integer.valueOf(416), "", "*/934" });
61+
parameterSets.add(new Object[] { "bytes10-", Integer.valueOf(416), "", "*/934" });
62+
parameterSets.add(new Object[] { "bytes-10", Integer.valueOf(416), "", "*/934" });
63+
// Unknown types
64+
parameterSets.add(new Object[] { "unknown=1-2", Integer.valueOf(416), "", "*/934" });
65+
parameterSets.add(new Object[] { "bytesX=1-2", Integer.valueOf(416), "", "*/934" });
66+
// Valid range
67+
parameterSets.add(new Object[] { "bytes=0-9", Integer.valueOf(206), "10", "0-9/934" });
68+
parameterSets.add(new Object[] { "bytes=-100", Integer.valueOf(206), "100", "834-933/934" });
69+
parameterSets.add(new Object[] { "bytes=100-", Integer.valueOf(206), "834", "100-933/934" });
70+
// Valid range (too much)
71+
parameterSets.add(new Object[] { "bytes=0-1000", Integer.valueOf(206), "934", "0-933/934" });
72+
parameterSets.add(new Object[] { "bytes=-1000", Integer.valueOf(206), "934", "0-933/934" });
4973
return parameterSets;
5074
}
5175

5276
@Parameter(0)
53-
public Map<String,List<String>> requestHeaders;
77+
public String rangeHeader;
5478
@Parameter(1)
5579
public int responseCodeExpected;
80+
@Parameter(2)
81+
public String contentLengthExpected;
82+
@Parameter(3)
83+
public String responseRangeExpected;
5684

5785
@Test
5886
public void testRange() throws Exception {
@@ -72,19 +100,36 @@ public void testRange() throws Exception {
72100
ByteChunk responseBody = new ByteChunk();
73101
Map<String,List<String>> responseHeaders = new HashMap<>();
74102

75-
int rc = getUrl(path, responseBody, requestHeaders, responseHeaders);
103+
int rc = getUrl(path, responseBody, buildRangeHeader(rangeHeader), responseHeaders);
76104

77105
// Check the result
78106
Assert.assertEquals(responseCodeExpected, rc);
107+
108+
if (contentLengthExpected.length() > 0) {
109+
String contentLength = responseHeaders.get("Content-Length").get(0);
110+
Assert.assertEquals(contentLengthExpected, contentLength);
111+
}
112+
113+
if (responseRangeExpected.length() > 0) {
114+
String responseRange = responseHeaders.get("Content-Range").get(0);
115+
Assert.assertEquals("bytes " + responseRangeExpected, responseRange);
116+
}
79117
}
80118

81119

82120
private static Map<String,List<String>> buildRangeHeader(String... headerValues) {
83121
Map<String,List<String>> requestHeaders = new HashMap<>();
84122
List<String> values = new ArrayList<>();
85123
for (String headerValue : headerValues) {
86-
values.add(headerValue);
124+
if (headerValue.length() > 0) {
125+
values.add(headerValue);
126+
}
127+
}
128+
129+
if (values.size() == 0) {
130+
return null;
87131
}
132+
88133
requestHeaders.put("range", values);
89134

90135
return requestHeaders;

0 commit comments

Comments
 (0)