/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.http.server; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import jj.util.StringUtils; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; /** * Helper to determine, from request headers and prepared response, * what content to actually serve to the client * @author jason * */ class RangeHeaderReader { private static final String HEADER_PREFIX = "bytes="; private static final Pattern SPLITTER = Pattern.compile(","); private static final Pattern RANGE = Pattern.compile("(\\d*)-(\\d*)"); static class Range implements Comparable<Range> { final long start; final long end; private Range(final long start, final long end) { this.start = start; this.end = end; } @Override public String toString() { return start + "-" + end; } @Override public int compareTo(Range o) { return (int)Math.signum(start - o.start); } } private final long responseSize; private final List<Range> ranges = new ArrayList<>(2); private final List<Range> originalranges = new ArrayList<>(2); private final long overlapDistance; private final int maxRanges; private boolean badRequest = false; /** * * @param requestHeaders The headers from the request * @param resource The resource being served * @param overlapDistance The amount of distance between consecutive ranges that should be coalesced */ RangeHeaderReader( final HttpHeaders requestHeaders, final long responseSize, final long overlapDistance ) { this(requestHeaders, responseSize, overlapDistance, 5); } /** * * @param requestHeaders The headers from the request * @param resource The resource being served * @param overlapDistance The amount of distance between consecutive ranges that should be coalesced * @param maxRanges The maximum number of ranges that can be requested before bailing */ RangeHeaderReader( final HttpHeaders requestHeaders, final long responseSize, final long overlapDistance, final int maxRanges ) { this.responseSize = responseSize; this.overlapDistance = overlapDistance; this.maxRanges = maxRanges; parseRanges(requestHeaders.get(HttpHeaderNames.RANGE).toString()); } private void parseRanges(String headerValue) { if (headerValue == null) { ranges.add(new Range(0, responseSize - 1)); } else if (headerValue.startsWith(HEADER_PREFIX)) { String[] candidates = SPLITTER.split(headerValue.substring(HEADER_PREFIX.length())); if (candidates.length > maxRanges) { badRequest = true; } else { tryCandidates(candidates); } } else { badRequest = true; } if (!badRequest && !ranges.isEmpty()) { originalranges.addAll(ranges); coalesceRanges(); } else { ranges.clear(); } } private void tryCandidates(String[] candidates) { for (String candidate : candidates) { Matcher matcher = RANGE.matcher(candidate); if (matcher.matches()) { String group1 = matcher.group(1); String group2 = matcher.group(2); if (StringUtils.isEmpty(group1) && StringUtils.isEmpty(group2)) { badRequest = true; break; } Range range; if (StringUtils.isEmpty(group1)) { // it's a range from the end range = new Range(responseSize - (Long.parseLong(group2) + 1), responseSize - 1); } else if (StringUtils.isEmpty(group2)) { // it specifies a start to the end range = new Range(Long.parseLong(group1), responseSize - 1); } else { // it's a normal range range = new Range(Long.parseLong(group1), Long.parseLong(group2)); } if (range.start >= 0 && range.start <= range.end && range.start <= responseSize - 1) { ranges.add(range.end > responseSize - 1 ? new Range(range.start, responseSize - 1) : range); } } else { badRequest = true; break; } } } /** * */ private void coalesceRanges() { Collections.sort(ranges); List<Range> adjustedRanges = makeAdjustedRanges(); // now work through the adjusted ranges, removing anything that is redundant cleanOverlaps(adjustedRanges); ranges.clear(); ranges.addAll(adjustedRanges); } private List<Range> makeAdjustedRanges() { List<Range> adjustedRanges = new ArrayList<>(ranges.size()); for (Range current : ranges) { Range working = current; for (Range nextRange : ranges) { if (nextRange == working) continue; if (working.start < nextRange.start && working.end > nextRange.end) { continue; } if ( working.start < nextRange.start && working.end > (nextRange.start - overlapDistance) ) { working = new Range(working.start, nextRange.end); } } adjustedRanges.add(working); } return adjustedRanges; } private void cleanOverlaps(List<Range> adjustedRanges) { long hwm = 0; for (Iterator<Range> i = adjustedRanges.iterator(); i.hasNext();) { Range range = i.next(); if (range.end > hwm) { hwm = range.end; } else { i.remove(); } } } public List<Range> ranges() { return Collections.unmodifiableList(ranges); } public boolean isBadRequest() { return badRequest; } }