def parse_content_range(content_range: str) -> tuple: """ Args: content_range: the content-range or range header to parse. Returns: Offset and limit arguments for the command. """ try: range_type, range_count = content_range.split(' ', 1) range_count_arr = range_count.split('/') range_begin, range_end = range_count_arr[0].split('-', 1) offset = int(range_begin) limit = int(range_end) - offset if range_type != 'items' or range_end < range_begin or limit < 0 or offset < 0: raise Exception except Exception: raise RequestedRangeNotSatisfiable( description=f'Range header: {content_range}') return offset, limit
def _extract_range(cls, req, blocks): """Convert byte range into block an performs validity check""" # accept only single part range val = req.headers['Range'] match = RANGE_RE.match(val) if match is None: raise RequestedRangeNotSatisfiable() start = int(match.group(1)) end = int(match.group(2)) if start >= end: raise RequestedRangeNotSatisfiable() def check_range(value): block, remainder = divmod(value, BLOCKSIZE) if remainder or block < 0 or (blocks and block > blocks): raise RequestedRangeNotSatisfiable() return block block_start = check_range(start) block_end = check_range(end + 1) # Check Range RFC return start, end, block_start, block_end
def _process_range_request(self, environ, complete_length=None, accept_ranges=None): """Handle Range Request related headers (RFC7233). If `Accept-Ranges` header is valid, and Range Request is processable, we set the headers as described by the RFC, and wrap the underlying response in a RangeWrapper. Returns ``True`` if Range Request can be fulfilled, ``False`` otherwise. :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable` if `Range` header could not be parsed or satisfied. """ from werkzeug.exceptions import RequestedRangeNotSatisfiable if accept_ranges is None: return False self.headers['Accept-Ranges'] = accept_ranges if not self._is_range_request_processable( environ) or complete_length is None: return False parsed_range = parse_range_header(environ.get('HTTP_RANGE')) if parsed_range is None: raise RequestedRangeNotSatisfiable(complete_length) range_tuple = parsed_range.range_for_length(complete_length) content_range_header = parsed_range.to_content_range_header( complete_length) if range_tuple is None or content_range_header is None: raise RequestedRangeNotSatisfiable(complete_length) content_length = range_tuple[1] - range_tuple[0] # Be sure not to send 206 response # if requested range is the full content. if content_length != complete_length: self.headers['Content-Length'] = content_length self.content_range = content_range_header self.status_code = 206 self._wrap_response(range_tuple[0], content_length) return True return False
async def make_conditional(self, request_range: Optional[Range], max_partial_size: Optional[int] = None) -> None: """Make the response conditional to the Arguments: request_range: The range as requested by the request. max_partial_size: The maximum length the server is willing to serve in a single response. Defaults to unlimited. """ self.accept_ranges = "bytes" # Advertise this ability if request_range is None or len( request_range.ranges) == 0: # Not a conditional request return if request_range.units != "bytes" or len(request_range.ranges) > 1: raise RequestedRangeNotSatisfiable() begin, end = request_range.ranges[0] try: complete_length = await self.response.make_conditional( # type: ignore begin, end, max_partial_size) except AttributeError: self.response = self.data_body_class( await self.response.convert_to_sequence()) return await self.make_conditional(request_range, max_partial_size) else: self.content_length = self.response.end - self.response.begin # type: ignore if self.content_length != complete_length: self.content_range = ContentRange( request_range.units, self.response.begin, # type: ignore self.response.end - 1, # type: ignore complete_length, ) self.status_code = 206
def check_range(value): block, remainder = divmod(value, BLOCKSIZE) if remainder or block < 0 or (blocks and block > blocks): raise RequestedRangeNotSatisfiable() return block
def serve_fileobj(fileobj, headers, content_length): status_code = 200 headers["Accept-Ranges"] = "bytes" r = httputil.get_ranges(headers.get('Range'), content_length) if r == []: headers['Content-Range'] = "bytes */{0}".format(content_length) message = "Invalid Range (first-byte-pos greater than Content-Length)" raise RequestedRangeNotSatisfiable(message) if not r: headers['Content-Length'] = content_length return fileobj, headers, status_code # Return a multipart/byteranges response. status_code = 206 if len(r) == 1: # Return a single-part response. start, stop = r[0] if stop > content_length: stop = content_length r_len = stop - start headers['Content-Range'] = "bytes {0}-{1}/{2}".format( start, stop - 1, content_length) headers['Content-Length'] = r_len fileobj.seek(start) body = file_generator_limited(fileobj, r_len) return body, headers, status_code try: # Python 3 from email.generator import _make_boundary as make_boundary except ImportError: # Python 2 from mimetools import choose_boundary as make_boundary boundary = make_boundary() content_type = "multipart/byteranges; boundary={0}".format(boundary) headers['Content-Type'] = content_type if "Content-Length" in headers: del headers["Content-Length"] def file_ranges(): for start, stop in r: yield to_unicode("--" + boundary) yield to_unicode("\r\nContent-type: {0}".format(content_type)) yield to_unicode( "\r\nContent-range: bytes {0}-{1}/{2}\r\n\r\n".format( start, stop - 1, content_length)) fileobj.seek(start) gen = file_generator_limited(fileobj, stop - start) for chunk in gen: yield chunk yield to_unicode("\r\n") yield to_unicode("--" + boundary + "--") body = file_ranges() return body, headers, status_code
def _raise_if_invalid_range(begin: int, end: int, size: int) -> None: if begin >= end or abs(begin) > size or end > size: raise RequestedRangeNotSatisfiable()