/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * 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 io.undertow.util; import io.undertow.UndertowLogger; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Represents a byte range for a range request * * * @author Stuart Douglas */ public class ByteRange { private final List<Range> ranges; public ByteRange(List<Range> ranges) { this.ranges = ranges; } public int getRanges() { return ranges.size(); } /** * Gets the start of the specified range segment, of -1 if this is a suffix range segment * @param range The range segment to get * @return The range start */ public long getStart(int range) { return ranges.get(range).getStart(); } /** * Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment * @param range The range segment to get * @return The range end */ public long getEnd(int range) { return ranges.get(range).getEnd(); } /** * Attempts to parse a range request. If the range request is invalid it will just return null so that * it may be ignored. * * * @param rangeHeader The range spec * @return A range spec, or null if the range header could not be parsed */ public static ByteRange parse(String rangeHeader) { if(rangeHeader == null || rangeHeader.length() < 7) { return null; } if(!rangeHeader.startsWith("bytes=")) { return null; } List<Range> ranges = new ArrayList<>(); String[] parts = rangeHeader.substring(6).split(","); for(String part : parts) { try { int index = part.indexOf('-'); if (index == 0) { //suffix range spec //represents the last N bytes //internally we represent this using a -1 as the start position long val = Long.parseLong(part.substring(1)); if(val < 0) { UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader); return null; } ranges.add(new Range(-1, val)); } else { if(index == -1) { UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader); return null; } long start = Long.parseLong(part.substring(0, index)); if(start < 0) { UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader); return null; } long end; if (index + 1 < part.length()) { end = Long.parseLong(part.substring(index + 1)); } else { end = -1; } ranges.add(new Range(start, end)); } } catch (NumberFormatException e) { UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader); return null; } } if(ranges.isEmpty()) { return null; } return new ByteRange(ranges); } /** * Returns a representation of the range result. If this returns null then a 200 response should be sent instead * @param resourceContentLength * @return */ public RangeResponseResult getResponseResult(final long resourceContentLength, String ifRange, Date lastModified, String eTag) { if(ranges.isEmpty()) { return null; } long start = getStart(0); long end = getEnd(0); long rangeLength; if(ifRange != null && !ifRange.isEmpty()) { if(ifRange.charAt(0) == '"') { //entity tag if(eTag != null && !eTag.equals(ifRange)) { return null; } } else { Date ifDate = DateUtils.parseDate(ifRange); if(ifDate != null && lastModified != null && ifDate.getTime() < lastModified.getTime()) { return null; } } } if(start == -1 ) { //suffix range if(end < 0){ //ignore the range request return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE); } start = Math.max(resourceContentLength - end, 0); end = resourceContentLength - 1; rangeLength = resourceContentLength - start; } else if(end == -1) { //prefix range long toWrite = resourceContentLength - start; if(toWrite >= 0) { rangeLength = toWrite; } else { //ignore the range request return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE); } end = resourceContentLength - 1; } else { end = Math.min(end, resourceContentLength - 1); if(start >= resourceContentLength || start > end) { return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE); } rangeLength = end - start + 1; } return new RangeResponseResult(start, end, rangeLength, "bytes " + start + "-" + end + "/" + resourceContentLength, StatusCodes.PARTIAL_CONTENT); } public static class RangeResponseResult { private final long start; private final long end; private final long contentLength; private final String contentRange; private final int statusCode; public RangeResponseResult(long start, long end, long contentLength, String contentRange, int statusCode) { this.start = start; this.end = end; this.contentLength = contentLength; this.contentRange = contentRange; this.statusCode = statusCode; } public long getStart() { return start; } public long getEnd() { return end; } public long getContentLength() { return contentLength; } public String getContentRange() { return contentRange; } public int getStatusCode() { return statusCode; } } public static class Range { private final long start, end; public Range(long start, long end) { this.start = start; this.end = end; } public long getStart() { return start; } public long getEnd() { return end; } } }