Jetty 9.4.x 2550 coalesce ranges (#2551)
Issue #2550 coalesce ranges Author: Lachlan Roberts <lachlan@webtide.com> Signed-off-by: olivier lamy <oliver.lamy@gmail.com> Signed-off-by: Greg Wilkins <gregw@webtide.com> Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
parent
07b3367519
commit
252ab9facf
|
@ -18,11 +18,12 @@
|
|||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.eclipse.jetty.util.LazyList;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
|
@ -54,8 +55,8 @@ public class InclusiveByteRange
|
|||
{
|
||||
private static final Logger LOG = Log.getLogger(InclusiveByteRange.class);
|
||||
|
||||
long first = 0;
|
||||
long last = 0;
|
||||
private long first;
|
||||
private long last;
|
||||
|
||||
public InclusiveByteRange(long first, long last)
|
||||
{
|
||||
|
@ -74,143 +75,62 @@ public class InclusiveByteRange
|
|||
}
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param headers Enumeration of Range header fields.
|
||||
* @param size Size of the resource.
|
||||
* @return LazyList of satisfiable ranges
|
||||
*/
|
||||
public static List<InclusiveByteRange> satisfiableRanges(Enumeration<String> headers, long size)
|
||||
private void coalesce(InclusiveByteRange r)
|
||||
{
|
||||
Object satRanges=null;
|
||||
|
||||
// walk through all Range headers
|
||||
headers:
|
||||
while (headers.hasMoreElements())
|
||||
{
|
||||
String header = headers.nextElement();
|
||||
StringTokenizer tok = new StringTokenizer(header,"=,",false);
|
||||
String t=null;
|
||||
try
|
||||
{
|
||||
// read all byte ranges for this header
|
||||
while (tok.hasMoreTokens())
|
||||
{
|
||||
try
|
||||
{
|
||||
t = tok.nextToken().trim();
|
||||
|
||||
long first = -1;
|
||||
long last = -1;
|
||||
int d = t.indexOf('-');
|
||||
if (d < 0 || t.indexOf("-",d + 1) >= 0)
|
||||
{
|
||||
if ("bytes".equals(t))
|
||||
continue;
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
continue headers;
|
||||
}
|
||||
else if (d == 0)
|
||||
{
|
||||
if (d + 1 < t.length())
|
||||
last = Long.parseLong(t.substring(d + 1).trim());
|
||||
else
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (d + 1 < t.length())
|
||||
{
|
||||
first = Long.parseLong(t.substring(0,d).trim());
|
||||
last = Long.parseLong(t.substring(d + 1).trim());
|
||||
}
|
||||
else
|
||||
first = Long.parseLong(t.substring(0,d).trim());
|
||||
|
||||
if (first == -1 && last == -1)
|
||||
continue headers;
|
||||
|
||||
if (first != -1 && last != -1 && (first > last))
|
||||
continue headers;
|
||||
|
||||
if (first < size)
|
||||
{
|
||||
InclusiveByteRange range = new InclusiveByteRange(first,last);
|
||||
satRanges = LazyList.add(satRanges,range);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
LOG.ignore(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
LOG.ignore(e);
|
||||
}
|
||||
}
|
||||
return LazyList.getList(satRanges,true);
|
||||
first = Math.min(first,r.first);
|
||||
last = Math.max(last,r.last);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public long getFirst(long size)
|
||||
private boolean overlaps(InclusiveByteRange range)
|
||||
{
|
||||
if (first<0)
|
||||
{
|
||||
long tf=size-last;
|
||||
if (tf<0)
|
||||
tf=0;
|
||||
return tf;
|
||||
}
|
||||
return first;
|
||||
return (range.first>=this.first && range.first<=this.last)
|
||||
||
|
||||
(range.last>=this.first && range.last<=this.last)
|
||||
||
|
||||
(range.first<this.first && range.last>this.last);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public long getLast(long size)
|
||||
public long getSize()
|
||||
{
|
||||
if (first<0)
|
||||
return size-1;
|
||||
|
||||
if (last<0 ||last>=size)
|
||||
return size-1;
|
||||
return last;
|
||||
return last-first+1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public long getSize(long size)
|
||||
{
|
||||
return getLast(size)-getFirst(size)+1;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public String toHeaderRangeString(long size)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(40);
|
||||
sb.append("bytes ");
|
||||
sb.append(getFirst(size));
|
||||
sb.append(first);
|
||||
sb.append('-');
|
||||
sb.append(getLast(size));
|
||||
sb.append(last);
|
||||
sb.append("/");
|
||||
sb.append(size);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public static String to416HeaderRangeString(long size)
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(40);
|
||||
sb.append("bytes */");
|
||||
sb.append(size);
|
||||
return sb.toString();
|
||||
return (int)(first ^ last);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
@Override
|
||||
public boolean equals( Object obj )
|
||||
{
|
||||
if(obj == null)
|
||||
return false;
|
||||
|
||||
if(!(obj instanceof InclusiveByteRange))
|
||||
return false;
|
||||
|
||||
return ((InclusiveByteRange)obj).first == this.first &&
|
||||
((InclusiveByteRange)obj).last == this.last;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
@Override
|
||||
|
@ -224,6 +144,137 @@ public class InclusiveByteRange
|
|||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/**
|
||||
* @param headers Enumeration of Range header fields.
|
||||
* @param size Size of the resource.
|
||||
* @return List of satisfiable ranges
|
||||
*/
|
||||
public static List<InclusiveByteRange> satisfiableRanges(Enumeration<String> headers, long size)
|
||||
{
|
||||
List<InclusiveByteRange> ranges = null;
|
||||
final long end = size-1;
|
||||
|
||||
// walk through all Range headers
|
||||
while (headers.hasMoreElements())
|
||||
{
|
||||
String header = headers.nextElement();
|
||||
StringTokenizer tok = new StringTokenizer(header,"=,",false);
|
||||
String t=null;
|
||||
try
|
||||
{
|
||||
// read all byte ranges for this header
|
||||
while (tok.hasMoreTokens())
|
||||
{
|
||||
try
|
||||
{
|
||||
t = tok.nextToken().trim();
|
||||
if ("bytes".equals(t))
|
||||
continue;
|
||||
|
||||
long first = -1;
|
||||
long last = -1;
|
||||
int dash = t.indexOf('-');
|
||||
if (dash < 0 || t.indexOf("-",dash + 1) >= 0)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
break;
|
||||
}
|
||||
|
||||
if (dash>0)
|
||||
first = Long.parseLong(t.substring(0,dash).trim());
|
||||
if (dash<(t.length()-1))
|
||||
last = Long.parseLong(t.substring(dash + 1).trim());
|
||||
|
||||
if (first==-1)
|
||||
{
|
||||
if (last==-1)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
break;
|
||||
}
|
||||
|
||||
if (last==0)
|
||||
continue;
|
||||
|
||||
// This is a suffix range
|
||||
first = Math.max(0,size-last);
|
||||
last = end;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Range starts after end
|
||||
if (first>=size)
|
||||
continue;
|
||||
|
||||
if (last==-1)
|
||||
last = end;
|
||||
else if (last>=end)
|
||||
last = end;
|
||||
}
|
||||
|
||||
if (last<first)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
break;
|
||||
}
|
||||
|
||||
InclusiveByteRange range = new InclusiveByteRange(first,last);
|
||||
if (ranges==null)
|
||||
ranges = new ArrayList<>();
|
||||
|
||||
boolean coalesced = false;
|
||||
for (Iterator<InclusiveByteRange> i = ranges.listIterator(); i.hasNext();)
|
||||
{
|
||||
InclusiveByteRange r = i.next();
|
||||
if (range.overlaps(r))
|
||||
{
|
||||
coalesced = true;
|
||||
r.coalesce(range);
|
||||
while(i.hasNext())
|
||||
{
|
||||
InclusiveByteRange r2 = i.next();
|
||||
|
||||
if (r2.overlaps(r))
|
||||
{
|
||||
r.coalesce(r2);
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!coalesced)
|
||||
ranges.add(range);
|
||||
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
LOG.ignore(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Bad range format: {}",t);
|
||||
LOG.ignore(e);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
public static String to416HeaderRangeString(long size)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(40);
|
||||
sb.append("bytes */");
|
||||
sb.append(size);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -735,7 +735,7 @@ public class ResourceService
|
|||
else
|
||||
{
|
||||
// Parse the satisfiable ranges
|
||||
List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges(reqRanges,content_length);
|
||||
List<InclusiveByteRange> ranges =InclusiveByteRange.satisfiableRanges( reqRanges, content_length);
|
||||
|
||||
// if there are no satisfiable ranges, send 416 response
|
||||
if (ranges==null || ranges.size()==0)
|
||||
|
@ -752,15 +752,15 @@ public class ResourceService
|
|||
// since were here now), send that range with a 216 response
|
||||
if ( ranges.size()== 1)
|
||||
{
|
||||
InclusiveByteRange singleSatisfiableRange = ranges.get(0);
|
||||
long singleLength = singleSatisfiableRange.getSize(content_length);
|
||||
InclusiveByteRange singleSatisfiableRange = ranges.iterator().next();
|
||||
long singleLength = singleSatisfiableRange.getSize();
|
||||
putHeaders(response,content,singleLength);
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
if (!response.containsHeader(HttpHeader.DATE.asString()))
|
||||
response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis());
|
||||
response.setHeader(HttpHeader.CONTENT_RANGE.asString(),
|
||||
singleSatisfiableRange.toHeaderRangeString(content_length));
|
||||
content.getResource().writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength);
|
||||
content.getResource().writeTo(out,singleSatisfiableRange.getFirst(),singleLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -793,9 +793,9 @@ public class ResourceService
|
|||
// calculate the content-length
|
||||
int length=0;
|
||||
String[] header = new String[ranges.size()];
|
||||
for (int i=0;i<ranges.size();i++)
|
||||
int i = 0;
|
||||
for (InclusiveByteRange ibr:ranges)
|
||||
{
|
||||
InclusiveByteRange ibr = ranges.get(i);
|
||||
header[i]=ibr.toHeaderRangeString(content_length);
|
||||
length+=
|
||||
((i>0)?2:0)+
|
||||
|
@ -803,18 +803,19 @@ public class ResourceService
|
|||
(mimetype==null?0:HttpHeader.CONTENT_TYPE.asString().length()+2+mimetype.length())+2+
|
||||
HttpHeader.CONTENT_RANGE.asString().length()+2+header[i].length()+2+
|
||||
2+
|
||||
(ibr.getLast(content_length)-ibr.getFirst(content_length))+1;
|
||||
(ibr.getLast()-ibr.getFirst())+1;
|
||||
i++;
|
||||
}
|
||||
length+=2+2+multi.getBoundary().length()+2+2;
|
||||
response.setContentLength(length);
|
||||
|
||||
for (int i=0;i<ranges.size();i++)
|
||||
i=0;
|
||||
for (InclusiveByteRange ibr:ranges)
|
||||
{
|
||||
InclusiveByteRange ibr = ranges.get(i);
|
||||
multi.startPart(mimetype,new String[]{HttpHeader.CONTENT_RANGE+": "+header[i]});
|
||||
|
||||
long start=ibr.getFirst(content_length);
|
||||
long size=ibr.getSize(content_length);
|
||||
long start=ibr.getFirst();
|
||||
long size=ibr.getSize();
|
||||
if (in!=null)
|
||||
{
|
||||
// Handle non cached resource
|
||||
|
@ -836,6 +837,8 @@ public class ResourceService
|
|||
else
|
||||
// Handle cached resource
|
||||
content.getResource().writeTo(multi,start,size);
|
||||
|
||||
i++;
|
||||
}
|
||||
if (in!=null)
|
||||
in.close();
|
||||
|
|
|
@ -18,34 +18,35 @@
|
|||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.eclipse.jetty.toolchain.test.AdvancedRunner;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AdvancedRunner.class)
|
||||
public class InclusiveByteRangeTest
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
private void assertInvalidRange(String rangeString)
|
||||
{
|
||||
Vector strings = new Vector();
|
||||
Vector<String> strings = new Vector<>();
|
||||
strings.add(rangeString);
|
||||
|
||||
List ranges = InclusiveByteRange.satisfiableRanges(strings.elements(),200);
|
||||
List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges( strings.elements(), 200);
|
||||
assertNull("Invalid Range [" + rangeString + "] should result in no satisfiable ranges",ranges);
|
||||
}
|
||||
|
||||
private void assertRange(String msg, int expectedFirst, int expectedLast, int size, InclusiveByteRange actualRange)
|
||||
{
|
||||
assertEquals(msg + " - first",expectedFirst,actualRange.getFirst(size));
|
||||
assertEquals(msg + " - last",expectedLast,actualRange.getLast(size));
|
||||
assertEquals(msg + " - first",expectedFirst,actualRange.getFirst());
|
||||
assertEquals(msg + " - last",expectedLast,actualRange.getLast());
|
||||
String expectedHeader = String.format("bytes %d-%d/%d",expectedFirst,expectedLast,size);
|
||||
assertEquals(msg + " - header range string",expectedHeader,actualRange.toHeaderRangeString(size));
|
||||
}
|
||||
|
@ -54,32 +55,30 @@ public class InclusiveByteRangeTest
|
|||
{
|
||||
InclusiveByteRange range = parseRange(rangeId,size);
|
||||
|
||||
assertEquals("Range [" + rangeId + "] - first",expectedFirst,range.getFirst(size));
|
||||
assertEquals("Range [" + rangeId + "] - last",expectedLast,range.getLast(size));
|
||||
assertEquals("Range [" + rangeId + "] - first",expectedFirst,range.getFirst());
|
||||
assertEquals("Range [" + rangeId + "] - last",expectedLast,range.getLast());
|
||||
String expectedHeader = String.format("bytes %d-%d/%d",expectedFirst,expectedLast,size);
|
||||
assertEquals("Range [" + rangeId + "] - header range string",expectedHeader,range.toHeaderRangeString(size));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private InclusiveByteRange parseRange(String rangeString, int size)
|
||||
{
|
||||
Vector strings = new Vector();
|
||||
Vector<String> strings = new Vector<>();
|
||||
strings.add(rangeString);
|
||||
|
||||
List ranges = InclusiveByteRange.satisfiableRanges(strings.elements(),size);
|
||||
List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(),size);
|
||||
assertNotNull("Satisfiable Ranges should not be null",ranges);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",1,ranges.size());
|
||||
return (InclusiveByteRange)ranges.get(0);
|
||||
return (InclusiveByteRange)ranges.iterator().next();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<InclusiveByteRange> parseRanges(String rangeString, int size)
|
||||
private List<InclusiveByteRange> parseRanges(int size, String... rangeString)
|
||||
{
|
||||
Vector strings = new Vector();
|
||||
strings.add(rangeString);
|
||||
Vector<String> strings = new Vector<>();
|
||||
for (String range : rangeString)
|
||||
strings.add(range);
|
||||
|
||||
List<InclusiveByteRange> ranges;
|
||||
ranges = InclusiveByteRange.satisfiableRanges(strings.elements(),size);
|
||||
List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(),size);
|
||||
assertNotNull("Satisfiable Ranges should not be null",ranges);
|
||||
return ranges;
|
||||
}
|
||||
|
@ -112,10 +111,26 @@ public class InclusiveByteRangeTest
|
|||
|
||||
rangeString = "bytes=5-20,35-65";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(rangeString,size);
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
assertRange("Range [" + rangeString + "]",5,20,size,ranges.get(0));
|
||||
assertRange("Range [" + rangeString + "]",35,49,size,ranges.get(1));
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",5,20,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",35,49,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranges have a multiple ranges, all absolutely defined.
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleAbsoluteRangesSplit()
|
||||
{
|
||||
int size = 50;
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,"bytes=5-20","bytes=35-65");
|
||||
assertEquals(2,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("testMultipleAbsoluteRangesSplit[0]",5,20,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("testMultipleAbsoluteRangesSplit[1]",35,49,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,40 +139,120 @@ public class InclusiveByteRangeTest
|
|||
@Test
|
||||
public void testMultipleRangesClipped()
|
||||
{
|
||||
int size = 50;
|
||||
String rangeString;
|
||||
|
||||
rangeString = "bytes=5-20,35-65,-5";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(rangeString,50);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",3,ranges.size());
|
||||
assertRange("Range [" + rangeString + "]",5,20,50,ranges.get(0));
|
||||
assertRange("Range [" + rangeString + "]",35,49,50,ranges.get(1));
|
||||
assertRange("Range [" + rangeString + "]",45,49,50,ranges.get(2));
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",5,20,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",35,49,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleRangesOverlapping()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
|
||||
rangeString = "bytes=5-20,15-25";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(rangeString,200);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
assertRange("Range [" + rangeString + "]",5,20,200,ranges.get(0));
|
||||
assertRange("Range [" + rangeString + "]",15,25,200,ranges.get(1));
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",1,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",5,25,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleRangesSplit()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
rangeString = "bytes=5-10,15-20";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(rangeString,200);
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
assertRange("Range [" + rangeString + "]",5,10,200,ranges.get(0));
|
||||
assertRange("Range [" + rangeString + "]",15,20,200,ranges.get(1));
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",5,10,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",15,20,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleSameRangesSplit()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
rangeString = "bytes=5-10,15-20,5-10,15-20,5-10,5-10,5-10,5-10,5-10,5-10";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",5,10,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",15,20,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleOverlappingRanges()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
rangeString = "bytes=5-15,20-30,10-25";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",1,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",5,30,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleOverlappingRangesOrdered()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
rangeString = "bytes=20-30,5-15,0-5,25-35";
|
||||
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",20,35,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",0,15,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleOverlappingRangesOrderedSplit()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
rangeString = "bytes=20-30,5-15,0-5,25-35";
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,"bytes=20-30","bytes=5-15","bytes=0-5,25-35");
|
||||
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",20,35,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",0,15,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNasty()
|
||||
{
|
||||
int size = 200;
|
||||
String rangeString;
|
||||
|
||||
rangeString = "bytes=90-100, 10-20, 30-40, -161";
|
||||
List<InclusiveByteRange> ranges = parseRanges(size,rangeString);
|
||||
|
||||
assertEquals("Satisfiable Ranges of [" + rangeString + "] count",2,ranges.size());
|
||||
Iterator<InclusiveByteRange> inclusiveByteRangeIterator = ranges.iterator();
|
||||
assertRange("Range [" + rangeString + "]",30,199,size,inclusiveByteRangeIterator.next());
|
||||
assertRange("Range [" + rangeString + "]",10,20,size,inclusiveByteRangeIterator.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRange_OpenEnded()
|
||||
{
|
||||
assertSimpleRange(50, 499, "bytes=50-", 500);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -167,5 +262,93 @@ public class InclusiveByteRangeTest
|
|||
assertSimpleRange(195,199,"bytes=-5",200);
|
||||
assertSimpleRange(50,119,"bytes=50-150",120);
|
||||
assertSimpleRange(50,119,"bytes=50-",120);
|
||||
|
||||
assertSimpleRange(1,50,"bytes= 1 - 50",120);
|
||||
}
|
||||
|
||||
// TODO: evaluate this vs assertInvalidRange() above, which behavior is correct? null? or empty list?
|
||||
private void assertBadRangeList(int size, String badRange)
|
||||
{
|
||||
Vector<String> strings = new Vector<>();
|
||||
strings.add(badRange);
|
||||
|
||||
List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(strings.elements(),size);
|
||||
// if one part is bad, the entire set of ranges should be treated as bad, per RFC7233
|
||||
assertThat("Should have no ranges", ranges, is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testBadRange_SetPartiallyBad()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=1-50,1-b,a-50");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_NoNumbers()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=a-b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_Empty()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testBadRange_ZeroPrefixed()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=01-050");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_Hex()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=0F-FF");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testBadRange_TabWhitespace()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=\t1\t-\t50");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_TabDelim()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=1-50\t90-101\t200-250");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_SemiColonDelim()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=1-50;90-101;200-250");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_NegativeSize()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=50-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_DoubleDash()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=1--20");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_TrippleDash()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=1---");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRange_ZeroedNegativeSize()
|
||||
{
|
||||
assertBadRangeList(500, "bytes=050-001");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,15 @@
|
|||
|
||||
package org.eclipse.jetty.servlet;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
|
@ -32,7 +35,6 @@ import org.eclipse.jetty.toolchain.test.FS;
|
|||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.OS;
|
||||
import org.eclipse.jetty.toolchain.test.TestingDir;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
|
@ -161,6 +163,59 @@ public class DefaultServletRangesTest
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMultipleSameRangeRequests() throws Exception
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder( );
|
||||
for(int i = 0; i < 1000; i++)
|
||||
{
|
||||
stringBuilder.append( "10-60," );
|
||||
}
|
||||
|
||||
|
||||
String response;
|
||||
response = connector.getResponse(
|
||||
"GET /context/data.txt HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Connection: close\r\n"+
|
||||
"Range: bytes=" + stringBuilder.toString() +"0-2\r\n" +
|
||||
"\r\n");
|
||||
int start = response.indexOf("--jetty");
|
||||
String body = response.substring(start);
|
||||
String boundary = body.substring(0, body.indexOf("\r\n"));
|
||||
assertResponseContains("206 Partial", response);
|
||||
assertResponseContains("Content-Type: multipart/byteranges; boundary=", response);
|
||||
|
||||
assertResponseContains("Content-Range: bytes 10-60/80", response);
|
||||
assertResponseContains("Content-Range: bytes 0-2/80", response);
|
||||
Assert.assertEquals( "Content range 0-60/80 in response not only 1:" + response , //
|
||||
2, response.split( "Content-Range: bytes 10-60/80" ).length);
|
||||
assertTrue(body.endsWith(boundary + "--\r\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleSameRangeRequestsTooLargeHeader() throws Exception
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder( );
|
||||
for(int i = 0; i < 2000; i++)
|
||||
{
|
||||
stringBuilder.append( "10-60," );
|
||||
}
|
||||
|
||||
|
||||
String response;
|
||||
response = connector.getResponse(
|
||||
"GET /context/data.txt HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Connection: close\r\n"+
|
||||
"Range: bytes=" + stringBuilder.toString() +"0-2\r\n" +
|
||||
"\r\n");
|
||||
int start = response.indexOf("--jetty");
|
||||
assertEquals( -1, start );
|
||||
assertResponseContains("HTTP/1.1 431 Request Header Fields Too Large", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenEndRange() throws Exception
|
||||
{
|
||||
|
@ -211,17 +266,11 @@ public class DefaultServletRangesTest
|
|||
|
||||
private void createFile(File file, String str) throws IOException
|
||||
{
|
||||
FileOutputStream out = null;
|
||||
try
|
||||
try(OutputStream out = Files.newOutputStream( file.toPath()))
|
||||
{
|
||||
out = new FileOutputStream(file);
|
||||
out.write(str.getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IO.close(out);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertResponseNotContains(String forbidden, String response)
|
||||
|
|
|
@ -1567,75 +1567,6 @@ public abstract class RFC2616BaseTest
|
|||
assertEquals("--"+boundary+"--",lines.get(i++));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Range (Header Field)
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.35">RFC 2616 (section 14.35)</a>
|
||||
*/
|
||||
@Test
|
||||
public void test14_35_Range_Multipart2() throws Exception
|
||||
{
|
||||
// Request the last 1 byte, last 2 bytes, and last 3 bytes.
|
||||
// This is an example of overlapping ranges
|
||||
|
||||
String rangedef = "-1,-2,-3";
|
||||
|
||||
StringBuffer req1 = new StringBuffer();
|
||||
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
|
||||
req1.append("Host: localhost\n");
|
||||
req1.append("Range: ").append(rangedef).append("\n");
|
||||
req1.append("Connection: close\n");
|
||||
req1.append("\n");
|
||||
|
||||
HttpTester.Response response = http.request(req1);
|
||||
|
||||
String msg = "Partial (Byte) Range: '" + rangedef + "'";
|
||||
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206,response.getStatus());
|
||||
|
||||
String contentType = response.get("Content-Type");
|
||||
// RFC states that multiple parts should result in multipart/byteranges Content type.
|
||||
StringAssert.assertContains(msg + " Content-Type",contentType,"multipart/byteranges");
|
||||
|
||||
// Collect 'boundary' string
|
||||
String boundary = null;
|
||||
String parts[] = StringUtil.split(contentType,';');
|
||||
for (int i = 0; i < parts.length; i++)
|
||||
{
|
||||
if (parts[i].trim().startsWith("boundary="))
|
||||
{
|
||||
String boundparts[] = StringUtil.split(parts[i],'=');
|
||||
Assert.assertEquals(msg + " Boundary parts.length",2,boundparts.length);
|
||||
boundary = boundparts[1];
|
||||
}
|
||||
}
|
||||
|
||||
Assert.assertNotNull(msg + " Should have found boundary in Content-Type header",boundary);
|
||||
|
||||
|
||||
List<String> lines = StringUtil.asLines(response.getContent().trim());
|
||||
int i=0;
|
||||
assertEquals("--"+boundary,lines.get(i++));
|
||||
assertEquals("Content-Type: text/plain",lines.get(i++));
|
||||
assertEquals("Content-Range: bytes 26-26/27",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("--"+boundary,lines.get(i++));
|
||||
assertEquals("Content-Type: text/plain",lines.get(i++));
|
||||
assertEquals("Content-Range: bytes 25-26/27",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("Z",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("--"+boundary,lines.get(i++));
|
||||
assertEquals("Content-Type: text/plain",lines.get(i++));
|
||||
assertEquals("Content-Range: bytes 24-26/27",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("YZ",lines.get(i++));
|
||||
assertEquals("",lines.get(i++));
|
||||
assertEquals("--"+boundary+"--",lines.get(i++));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Range (Header Field)
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue