diff --git a/spring-web/src/main/java/org/springframework/http/HttpRange.java b/spring-web/src/main/java/org/springframework/http/HttpRange.java index 3bd2485f3c1..32da802cce1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpRange.java +++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,9 @@ import org.springframework.util.StringUtils; */ public abstract class HttpRange { + /** Maximum ranges per request. */ + private static final int MAX_RANGES = 100; + private static final String BYTE_RANGE_PREFIX = "bytes="; @@ -59,16 +62,22 @@ public abstract class HttpRange { // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! Assert.isTrue(resource.getClass() != InputStreamResource.class, "Cannot convert an InputStreamResource to a ResourceRegion"); + long contentLength = getLengthFor(resource); + long start = getRangeStart(contentLength); + long end = getRangeEnd(contentLength); + return new ResourceRegion(resource, start, end - start + 1); + } + + private static long getLengthFor(Resource resource) { + long contentLength; try { - long contentLength = resource.contentLength(); + contentLength = resource.contentLength(); Assert.isTrue(contentLength > 0, "Resource content length should be > 0"); - long start = getRangeStart(contentLength); - long end = getRangeEnd(contentLength); - return new ResourceRegion(resource, start, end - start + 1); } catch (IOException ex) { - throw new IllegalArgumentException("Failed to convert Resource to ResourceRegion", ex); + throw new IllegalArgumentException("Failed to obtain Resource content length", ex); } + return contentLength; } /** @@ -122,7 +131,8 @@ public abstract class HttpRange { *

This method can be used to parse an {@code Range} header. * @param ranges the string to parse * @return the list of ranges - * @throws IllegalArgumentException if the string cannot be parsed + * @throws IllegalArgumentException if the string cannot be parsed, or if + * the number of ranges is greater than 100. */ public static List parseRanges(@Nullable String ranges) { if (!StringUtils.hasLength(ranges)) { @@ -134,6 +144,7 @@ public abstract class HttpRange { ranges = ranges.substring(BYTE_RANGE_PREFIX.length()); String[] tokens = StringUtils.tokenizeToStringArray(ranges, ","); + Assert.isTrue(tokens.length <= MAX_RANGES, () -> "Too many ranges " + tokens.length); List result = new ArrayList<>(tokens.length); for (String token : tokens) { result.add(parseRange(token)); @@ -169,6 +180,8 @@ public abstract class HttpRange { * @param ranges the list of ranges * @param resource the resource to select the regions from * @return the list of regions for the given resource + * @throws IllegalArgumentException if the sum of all ranges exceeds the + * resource length. * @since 4.3 */ public static List toResourceRegions(List ranges, Resource resource) { @@ -179,6 +192,13 @@ public abstract class HttpRange { for (HttpRange range : ranges) { regions.add(range.toResourceRegion(resource)); } + if (ranges.size() > 1) { + long length = getLengthFor(resource); + long total = regions.stream().map(ResourceRegion::getCount).reduce(0L, (count, sum) -> sum + count); + Assert.isTrue(total < length, + () -> "The sum of all ranges (" + total + ") " + + "should be less than the resource length (" + length + ")"); + } return regions; } diff --git a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java index cc8787de466..0f6d5da976a 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,9 @@ package org.springframework.http; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.junit.Test; @@ -100,6 +102,31 @@ public class HttpRangeTests { assertEquals(999, ranges.get(2).getRangeEnd(1000)); } + @Test + public void parseRangesValidations() { + + // 1. At limit.. + StringBuilder sb = new StringBuilder("bytes=0-0"); + for (int i=0; i < 99; i++) { + sb.append(",").append(i).append("-").append(i + 1); + } + List ranges = HttpRange.parseRanges(sb.toString()); + assertEquals(100, ranges.size()); + + // 2. Above limit.. + sb = new StringBuilder("bytes=0-0"); + for (int i=0; i < 100; i++) { + sb.append(",").append(i).append("-").append(i + 1); + } + try { + HttpRange.parseRanges(sb.toString()); + fail(); + } + catch (IllegalArgumentException ex) { + // Expected + } + } + @Test public void rangeToString() { List ranges = new ArrayList<>(); @@ -144,4 +171,25 @@ public class HttpRangeTests { range.toResourceRegion(resource); } + @Test + public void toResourceRegionsValidations() { + byte[] bytes = "12345".getBytes(StandardCharsets.UTF_8); + ByteArrayResource resource = new ByteArrayResource(bytes); + + // 1. Below length + List ranges = HttpRange.parseRanges("bytes=0-1,2-3"); + List regions = HttpRange.toResourceRegions(ranges, resource); + assertEquals(2, regions.size()); + + // 2. At length + ranges = HttpRange.parseRanges("bytes=0-1,2-4"); + try { + HttpRange.toResourceRegions(ranges, resource); + fail(); + } + catch (IllegalArgumentException ex) { + // Expected.. + } + } + }