summaryrefslogtreecommitdiff
path: root/paste
diff options
context:
space:
mode:
authorcce <devnull@localhost>2005-12-29 18:16:59 +0000
committercce <devnull@localhost>2005-12-29 18:16:59 +0000
commit5c466f7cef6a16ab03067dbbc61d940f26a186a5 (patch)
treee73906c72aad99fd11fa9f2361eb0f5949e7c407 /paste
parentc4a7ac89559ee520845b7a1652c85e363fb0e66a (diff)
downloadpaste-5c466f7cef6a16ab03067dbbc61d940f26a186a5.tar.gz
- added Range.parse to httpheaders
- renamed Expires.time to Expires.parse for consistency - updated FileApp/DataApp to return 206 on Partial Content - all HttpHeader(environ) return strings (empty string when not found) so that checks like 'if header-part in HttpHeader(collection)' works without having to check for None - updated FileApp to use Range header (instead of having its own copy)
Diffstat (limited to 'paste')
-rw-r--r--paste/fileapp.py38
-rw-r--r--paste/httpheaders.py116
2 files changed, 96 insertions, 58 deletions
diff --git a/paste/fileapp.py b/paste/fileapp.py
index 1c62ed4..77baa0c 100644
--- a/paste/fileapp.py
+++ b/paste/fileapp.py
@@ -8,8 +8,9 @@ if-modified-since request header.
"""
import os, time, mimetypes
-from httpexceptions import HTTPBadRequest, HTTPForbidden
-from httpheaders import get_header, Expires, \
+from httpexceptions import HTTPBadRequest, HTTPForbidden, \
+ HTTPRequestRangeNotSatisfiable
+from httpheaders import get_header, Expires, Range, \
ContentType, AcceptRanges, CacheControl, ContentDisposition, \
ContentLength, ContentRange, LastModified, IfModifiedSince
@@ -70,7 +71,7 @@ class DataApp(object):
self.set_content(content)
def cache_control(self, **kwargs):
- self.expires = CacheControl.apply(self.headers, **kwargs)
+ self.expires = CacheControl.apply(self.headers, **kwargs) or None
return self
def set_content(self, content):
@@ -91,7 +92,7 @@ class DataApp(object):
Expires.update(headers, delta=self.expires)
try:
- client_clock = IfModifiedSince.time(environ)
+ client_clock = IfModifiedSince.parse(environ)
if client_clock >= int(self.last_modified):
# the client has a recent copy
#@@: all entity headers should be removed, not just these
@@ -103,19 +104,13 @@ class DataApp(object):
return exce.wsgi_application(environ, start_response)
(lower,upper) = (0, self.content_length - 1)
- if 'HTTP_RANGE' in environ:
- range = environ['HTTP_RANGE'].split(",")[0]
- range = range.strip().lower().replace(" ","")
- if not range.startswith("bytes=") or 1 != range.count("-"):
- return HTTPBadRequest((
- "A malformed range request was given.\r\n"
- " Range: %s\r\n") % range
- ).wsgi_application(environ, start_response)
- (lower,upper) = range[len("bytes="):].split("-")
- upper = upper and int(upper) or (self.content_length - 1)
- lower = lower and int(lower) or 0
- if upper >= self.content_length or lower >= self.content_length:
- return HTTPBadRequest((
+ range = Range.parse(environ)
+ print range
+ if range and 'bytes' == range[0] and 1 == len(range[1]):
+ (lower,upper) = range[1][0]
+ upper = upper or (self.content_length - 1)
+ if upper >= self.content_length or lower > upper:
+ return HTTPRequestRangeNotSatisfiable((
"Range request was made beyond the end of the content,\r\n"
"which is %s long.\r\n Range: %s\r\n") % (
self.content_length, range)
@@ -126,8 +121,10 @@ class DataApp(object):
ContentRange.update(headers,
# @@: make parameterized version
"%d-%d/%d" % (lower, upper, self.content_length))
-
- start_response('200 OK',headers)
+ if lower == 0 and upper == self.content_length - 1:
+ start_response('200 OK', headers)
+ else:
+ start_response('206 Partial Content', headers)
if self.content is not None:
return [self.content[lower:upper+1]]
assert self.__class__ != DataApp, "DataApp must call set_content"
@@ -163,7 +160,8 @@ class FileApp(DataApp):
self.last_modified = stat.st_mtime
def __call__(self, environ, start_response):
- if 'max-age=0' in environ.get("HTTP_CACHE_CONTROL",''):
+ print CacheControl(environ), "\n"
+ if 'max-age=0' in CacheControl(environ):
self.update(force=True) # RFC 2616 13.2.6
else:
self.update()
diff --git a/paste/httpheaders.py b/paste/httpheaders.py
index 3623704..c65f83a 100644
--- a/paste/httpheaders.py
+++ b/paste/httpheaders.py
@@ -230,7 +230,8 @@ class HTTPHeader(object):
'response': 3, 'entity': 4 }[self.category]
self.extensions = {}
_headers[self.name.lower()] = self
- self._environ_name = 'HTTP_'+ self.name.upper().replace("-","_")
+ self._environ_name = getattr(self, '_environ_name',
+ 'HTTP_'+ self.name.upper().replace("-","_"))
self._headers_name = self.name.lower()
assert self.version in ('1.1','1.0','0.9')
assert isinstance(self,(SingleValueHeader,MultiValueHeader,
@@ -263,7 +264,7 @@ class HTTPHeader(object):
def format(self, *values):
""" produce a return value appropriate for this kind of header """
if not values:
- return None
+ return ''
raise NotImplementedError()
def __call__(self, *args, **kwargs):
@@ -305,8 +306,8 @@ class HTTPHeader(object):
if dict == type(args[0]):
assert 1 == len(args) and 'wsgi.version' in args[0]
value = args[0].get(self._environ_name)
- if value is None:
- return None
+ if not value:
+ return ''
return self.format(value)
for item in args:
assert not type(item) in (dict, list)
@@ -340,7 +341,7 @@ class HTTPHeader(object):
could be a list for ``MultiEntryHeader`` instances).
"""
value = self.__call__(*args, **kwargs)
- if value is None:
+ if not value:
self.remove(connection)
return
if type(collection) == dict:
@@ -385,7 +386,7 @@ class SingleValueHeader(HTTPHeader):
def format(self, *values):
if not values:
- return None
+ return ''
assert len(values) == 1, "more than one value: %s" % repr(values)
return str(values[0]).strip()
@@ -397,7 +398,7 @@ class MultiValueHeader(HTTPHeader):
def format(self, *values):
if not values:
- return None
+ return []
return ", ".join([str(v).strip() for v in values])
class MultiEntryHeader(HTTPHeader):
@@ -417,7 +418,7 @@ class MultiEntryHeader(HTTPHeader):
def format(self, *values):
if not values:
- return None
+ return ''
return list([str(v).strip() for v in values])
def tuples(self, *args, **kwargs):
@@ -499,17 +500,16 @@ class DateHeader(SingleValueHeader):
time += delta
return (formatdate(time),)
- def time(self, *args, **kwargs):
+ def parse(self, *args, **kwargs):
""" return the time value (in seconds since 1970) """
value = self.__call__(*args, **kwargs)
- if value is None:
- return None
- try:
- return mktime_tz(parsedate_tz(value))
- except TypeError:
- raise HTTPBadRequest((
- "Received an ill-formed timestamp for %s: %s\r\n") %
- (self.name, value))
+ if value:
+ try:
+ return mktime_tz(parsedate_tz(value))
+ except TypeError:
+ raise HTTPBadRequest((
+ "Received an ill-formed timestamp for %s: %s\r\n") %
+ (self.name, value))
#
# Following are specific HTTP headers. Since these classes are mostly
@@ -629,28 +629,14 @@ class CacheControl(MultiValueHeader):
CacheControl = CacheControl('Cache-Control','general')
-class SingleValueCGIHeader(SingleValueHeader):
- """
- This is a base class for Content-Type and Content-Length headers,
- which besides their HTTP_ entries may also have a CGI version.
- The logic is to only use the CGI version when the HTTP_ version is
- missing. Hopefully this can be removed.
- """
- def __call__(self, *args, **kwargs):
- if args and dict == type(args[0]):
- if not args[0].get(self._environ_name):
- cgi_name = self._environ_name[5:]
- return self.format(args[0].get(cgi_name))
- return SingleValueHeader.__call__(self, *args, **kwargs)
-
-class ContentType(SingleValueCGIHeader):
+class ContentType(SingleValueHeader):
"""
Content-Type, RFC 2616 section 14.17
- If the 'Content-Type' does not appear in the ``environ`` the
- corresponding CGI variable is searched.
+ Unlike other headers, use the CGI variable instead.
"""
version = '1.0'
+ _environ_name = 'CONTENT_TYPE'
# common mimetype constants
UNKNOWN = 'application/octet-stream'
@@ -673,11 +659,15 @@ class ContentType(SingleValueCGIHeader):
return (result,)
ContentType = ContentType('Content-Type','entity')
-class ContentLength(SingleValueCGIHeader):
+class ContentLength(SingleValueHeader):
"""
Content-Length, RFC 2616 section 14.13
+
+ Unlike other headers, use the CGI variable instead.
"""
version = "1.0"
+ _environ_name = 'CONTENT_LENGTH'
+
ContentLength = ContentLength('Content-Length','entity')
class ContentDisposition(SingleValueHeader):
@@ -749,8 +739,8 @@ class IfModifiedSince(DateHeader):
If-Modified-Since, RFC 2616 section 14.25
"""
version = '1.0'
- def time(self, *args, **kwargs):
- value = DateHeader.time(self, *args, **kwargs)
+ def parse(self, *args, **kwargs):
+ value = DateHeader.parse(self, *args, **kwargs)
if value and value > now():
raise HTTPBadRequest((
"Please check your system clock.\r\n"
@@ -759,6 +749,56 @@ class IfModifiedSince(DateHeader):
return value
IfModifiedSince = IfModifiedSince('If-Modified-Since','request')
+class Range(MultiValueHeader):
+ """
+ Range, RFC 2616 section 14.35
+
+ According to section 14.16, the response to this message should be a
+ 206 Partial Content and that if multiple non-overlapping byte ranges
+ are requested (it is an error to request multiple overlapping
+ ranges) the result should be sent as multipart/byteranges mimetype.
+
+ The server should respond with '416 Requested Range Not Satisifiable'
+ if the requested ranges are out-of-bounds. The specification also
+ indicates that a syntax error in the Range request should result in
+ the header being ignored rather than a '400 Bad Request'.
+ """
+ version = '1.1'
+ def parse(self, *args, **kwargs):
+ """
+ Returns a tuple (units, list), where list is a sequence of
+ (begin, end) tuples; and end is None if it was not provided.
+ """
+ value = self.__call__(*args, **kwargs)
+ if not value:
+ return None
+ ranges = []
+ last_end = -1
+ try:
+ (units, range) = value.split("=")
+ units = units.strip().lower()
+ for item in range.split(","):
+ (begin, end) = item.split("-")
+ if not begin.strip():
+ begin = 0
+ else:
+ begin = int(begin)
+ if begin <= last_end:
+ raise ValueError()
+ if not end.strip():
+ end = None
+ else:
+ end = int(end)
+ last_end = end
+ ranges.append((begin,end))
+ except ValueError:
+ # In this case where the Range header is malformed,
+ # section 14.16 says to treat the request as if the
+ # Range header was not present. How do I log this?
+ return None
+ return (units, ranges)
+Range = Range('Range','request')
+
#
# For now, construct a minimalistic version of the field-names; at a
# later date more complicated headers may sprout content constructors.
@@ -800,7 +840,7 @@ for (name, category, version, style, comment) in \
,("Pragma" ,'general' ,'1.0','multi-value','RFC 2616 $14.32')
,("Proxy-Authenticate" ,'response','1.1','multi-value','RFC 2616 $14.33')
,("Proxy-Authorization",'request' ,'1.1','singular' ,'RFC 2616 $14.34')
-,("Range" ,'request' ,'1.1','multi-value','RFC 2616 $14.35')
+#,("Range" ,'request' ,'1.1','multi-value','RFC 2616 $14.35')
,("Referer" ,'request' ,'1.0','singular' ,'RFC 2616 $14.36')
,("Retry-After" ,'response','1.1','singular' ,'RFC 2616 $14.37')
,("Server" ,'response','1.0','singular' ,'RFC 2616 $14.38')