summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpje <pje@571e12c6-e1fa-0310-aee7-ff1267fa46bd>2004-10-06 21:35:47 +0000
committerpje <pje@571e12c6-e1fa-0310-aee7-ff1267fa46bd>2004-10-06 21:35:47 +0000
commitb60e8cd5700a11062b3a4a46fe212932e18cc2c3 (patch)
tree10426dc31f6ecf69581b0abd94fae87e1555a7b0
parentbda7b7fbd6ace503470b12e572a1cc001a7b0e82 (diff)
downloadwsgiref-b60e8cd5700a11062b3a4a46fe212932e18cc2c3.tar.gz
Improved support for different client and server HTTP versions,
including optional Date: and Server: header generation, and support for SERVER_PROTOCOL. Added hop-by-hop header check (and utility function). Headers() objects also now have a 'setdefault()' method. The BaseHandler.send_status() method has been replaced by a send_preamble() method, and you probably no longer need to override it. git-svn-id: svn://svn.eby-sarna.com/svnroot/wsgiref@252 571e12c6-e1fa-0310-aee7-ff1267fa46bd
-rw-r--r--src/wsgiref/handlers.py76
-rw-r--r--src/wsgiref/headers.py22
-rw-r--r--src/wsgiref/tests/test_handlers.py42
-rw-r--r--src/wsgiref/tests/test_headers.py8
-rw-r--r--src/wsgiref/tests/test_util.py30
-rw-r--r--src/wsgiref/util.py41
6 files changed, 150 insertions, 69 deletions
diff --git a/src/wsgiref/handlers.py b/src/wsgiref/handlers.py
index 287e3bb..2cc8fed 100644
--- a/src/wsgiref/handlers.py
+++ b/src/wsgiref/handlers.py
@@ -1,10 +1,10 @@
"""Base classes for server/gateway implementations"""
from types import StringType
-from util import FileWrapper, guess_scheme
+from util import FileWrapper, guess_scheme, is_hop_by_hop
from headers import Headers
-import sys, os
+import sys, os, time
try:
dict
@@ -48,6 +48,10 @@ class BaseHandler:
wsgi_multiprocess = True
wsgi_run_once = False
+ origin_server = True # We are transmitting direct to client
+ http_version = "1.0" # Version that should be used for response
+ server_software = None # String name of server software, if any
+
# os_environ is used to supply configuration from the OS environment:
# by default it's a copy of 'os.environ' as of import time, but you can
# override this in e.g. your __init__ method.
@@ -76,10 +80,6 @@ class BaseHandler:
-
-
-
-
def run(self, application):
"""Invoke the application"""
# Note to self: don't move the close()! Asynchronous servers shouldn't
@@ -151,17 +151,17 @@ class BaseHandler:
if blocks==1:
self.headers['Content-Length'] = str(self.bytes_sent)
return
- # XXX Try for chunked encoding if enabled
+ # XXX Try for chunked encoding if origin server and client is 1.1
def cleanup_headers(self):
- """Make any necessary header changes or defaults"""
+ """Make any necessary header changes or defaults
+
+ Subclasses can extend this to add other defaults.
+ """
if not self.headers.has_key('Content-Length'):
self.set_content_length()
- # XXX set up Date, Server headers
-
-
def start_response(self, status, headers,exc_info=None):
"""'start_response()' callable as specified by PEP 333"""
@@ -183,25 +183,25 @@ class BaseHandler:
for name,val in headers:
assert type(name) is StringType,"Header names must be strings"
assert type(val) is StringType,"Header values must be strings"
-
+ assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
self.status = status
self.headers = self.headers_class(headers)
return self.write
-
-
-
-
-
-
-
-
-
-
-
-
-
+ def send_preamble(self):
+ """Transmit version/status/date/server, via self._write()"""
+ if self.origin_server:
+ if self.client_is_modern():
+ self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
+ if not self.headers.has_key('Date'):
+ self._write(
+ 'Date: %s\r\n' % time.asctime(time.gmtime(time.time()))
+ )
+ if self.server_software and not self.headers.has_key('Server'):
+ self._write('Server: %s\r\n' % self.server_software)
+ else:
+ self._write('Status: %s\r\n' % self.status)
def write(self, data):
"""'write()' callable as specified by PEP 333"""
@@ -265,18 +265,13 @@ class BaseHandler:
self.bytes_sent = 0; self.headers_sent = False
- def send_status(self):
- """Transmit the status to the client, via self._write()
-
- (BaseCGIHandler overrides this to use a "Status:" prefix.)"""
- self._write('%s\r\n' % status)
-
def send_headers(self):
"""Transmit headers to the client, via self._write()"""
self.cleanup_headers()
self.headers_sent = True
- self.send_status()
- self._write(str(self.headers))
+ if not self.origin_server or self.client_is_modern():
+ self.send_preamble()
+ self._write(str(self.headers))
def result_is_file(self):
@@ -285,6 +280,11 @@ class BaseHandler:
return wrapper is not None and isinstance(self.result,wrapper)
+ def client_is_modern(self):
+ """True if client can accept status and headers"""
+ return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
+
+
def log_exception(self,exc_info):
"""Log the 'exc_info' tuple in the server log
@@ -308,6 +308,7 @@ class BaseHandler:
self.finish_response()
# XXX else: attempt advanced recovery techniques for HTML or text?
+
def error_output(self, environ, start_response):
"""WSGI mini-app to create error output
@@ -325,7 +326,6 @@ class BaseHandler:
return [self.error_body]
-
# Pure abstract methods; *must* be overridden in subclasses
def _write(self,data):
@@ -384,7 +384,7 @@ class BaseCGIHandler(BaseHandler):
'multiprocess' (defaulting to 'True' and 'False' respectively) to control
the configuration sent to the application.
"""
-
+ origin_server = False
wsgi_multithread = False
wsgi_multiprocess = True
@@ -416,9 +416,6 @@ class BaseCGIHandler(BaseHandler):
self.stdout.flush()
self._flush = self.stdout.flush
- def send_status(self):
- self._write('Status: %s\r\n' % self.status)
-
class CGIHandler(BaseCGIHandler):
"""CGI-based invocation via sys.stdin/stdout/stderr and os.environ
@@ -449,3 +446,6 @@ class CGIHandler(BaseCGIHandler):
+
+
+
diff --git a/src/wsgiref/headers.py b/src/wsgiref/headers.py
index b2d5de4..fa9b829 100644
--- a/src/wsgiref/headers.py
+++ b/src/wsgiref/headers.py
@@ -149,17 +149,17 @@ class Headers:
suitable for direct HTTP transmission."""
return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['',''])
-
-
-
-
-
-
-
-
-
-
-
+ def setdefault(self,name,value):
+ """Return first matching header value for 'name', or 'value'
+
+ If there is no header named 'name', add a new header with name 'name'
+ and value 'value'."""
+ result = self.get(name)
+ if result is None:
+ self._headers.append((name,value))
+ return value
+ else:
+ return result
def add_header(self, _name, _value, **_params):
diff --git a/src/wsgiref/tests/test_handlers.py b/src/wsgiref/tests/test_handlers.py
index 7d0863f..d56c337 100644
--- a/src/wsgiref/tests/test_handlers.py
+++ b/src/wsgiref/tests/test_handlers.py
@@ -4,7 +4,7 @@ from wsgiref.util import setup_testing_defaults
from wsgiref.headers import Headers
from wsgiref.handlers import BaseHandler, BaseCGIHandler
from StringIO import StringIO
-
+import re
class ErrorHandler(BaseCGIHandler):
"""Simple handler subclass for testing BaseHandler"""
@@ -162,6 +162,46 @@ class HandlerTests(TestCase):
self.failUnless(h.stderr.getvalue().find("AssertionError")<>-1)
+ def testHeaderFormats(self):
+
+ def non_error_app(e,s):
+ s('200 OK',[])
+ return []
+
+ stdpat = (
+ r"HTTP/%s 200 OK\r\n"
+ r"Date: \w{3} \w{3} \d{2} \d\d:\d\d:\d\d \d{4}\r\n"
+ r"%s" r"Content-Length: 0\r\n" r"\r\n"
+ )
+ shortpat = (
+ "Status: 200 OK\r\n" "Content-Length: 0\r\n" "\r\n"
+ )
+
+ for ssw in "FooBar/1.0", None:
+ sw = ssw and "Server: %s\r\n" % ssw or ""
+
+ for version in "1.0", "1.1":
+ for proto in "HTTP/0.9", "HTTP/1.0", "HTTP/1.1":
+
+ h = TestHandler(SERVER_PROTOCOL=proto)
+ h.origin_server = False
+ h.http_version = version
+ h.server_software = ssw
+ h.run(non_error_app)
+ self.assertEqual(shortpat,h.stdout.getvalue())
+
+ h = TestHandler(SERVER_PROTOCOL=proto)
+ h.origin_server = True
+ h.http_version = version
+ h.server_software = ssw
+ h.run(non_error_app)
+ if proto=="HTTP/0.9":
+ self.assertEqual(h.stdout.getvalue(),"")
+ else:
+ self.failUnless(
+ re.match(stdpat%(version,sw), h.stdout.getvalue()),
+ (stdpat%(version,sw), h.stdout.getvalue())
+ )
TestClasses = (
HandlerTests,
diff --git a/src/wsgiref/tests/test_headers.py b/src/wsgiref/tests/test_headers.py
index c671dc0..db9afc3 100644
--- a/src/wsgiref/tests/test_headers.py
+++ b/src/wsgiref/tests/test_headers.py
@@ -2,7 +2,6 @@ from unittest import TestCase, TestSuite, makeSuite
from wsgiref.headers import Headers
from wsgiref.tests import compare_generic_iter
-
class HeaderTests(TestCase):
def testMappingInterface(self):
@@ -31,14 +30,15 @@ class HeaderTests(TestCase):
self.assertEqual(h.get("foo","whee"), "baz")
self.assertEqual(h.get("zoo","whee"), "whee")
+ self.assertEqual(h.setdefault("foo","whee"), "baz")
+ self.assertEqual(h.setdefault("zoo","whee"), "whee")
+ self.assertEqual(h["foo"],"baz")
+ self.assertEqual(h["zoo"],"whee")
def testRequireList(self):
self.assertRaises(TypeError, Headers, "foo")
-
-
-
def testExtras(self):
h = Headers([])
self.assertEqual(str(h),'\r\n')
diff --git a/src/wsgiref/tests/test_util.py b/src/wsgiref/tests/test_util.py
index 91caac6..b406841 100644
--- a/src/wsgiref/tests/test_util.py
+++ b/src/wsgiref/tests/test_util.py
@@ -84,6 +84,7 @@ class UtilityTests(TestCase):
for key, value in [
('SERVER_NAME','127.0.0.1'),
('SERVER_PORT', '80'),
+ ('SERVER_PROTOCOL','HTTP/1.0'),
('HTTP_HOST','127.0.0.1'),
('REQUEST_METHOD','GET'),
('SCRIPT_NAME',''),
@@ -120,7 +121,6 @@ class UtilityTests(TestCase):
-
def testAppURIs(self):
self.checkAppURI("http://127.0.0.1/")
self.checkAppURI("http://127.0.0.1/spam", SCRIPT_NAME="/spam")
@@ -146,20 +146,20 @@ class UtilityTests(TestCase):
def testFileWrapper(self):
self.checkFW("xyz"*50, 120, ["xyz"*40,"xyz"*10])
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ def testHopByHop(self):
+ for hop in (
+ "Connection Keep-Alive Proxy-Authenticate Proxy-Authorization "
+ "TE Trailers Transfer-Encoding Upgrade"
+ ).split():
+ for alt in hop, hop.title(), hop.upper(), hop.lower():
+ self.failUnless(util.is_hop_by_hop(alt))
+
+ # Not comprehensive, just a few random header names
+ for hop in (
+ "Accept Cache-Control Date Pragma Trailer Via Warning"
+ ).split():
+ for alt in hop, hop.title(), hop.upper(), hop.lower():
+ self.failIf(util.is_hop_by_hop(alt))
TestClasses = (
diff --git a/src/wsgiref/util.py b/src/wsgiref/util.py
index dd6f79c..1a8bd9a 100644
--- a/src/wsgiref/util.py
+++ b/src/wsgiref/util.py
@@ -135,6 +135,8 @@ def setup_testing_defaults(environ):
"""
environ.setdefault('SERVER_NAME','127.0.0.1')
+ environ.setdefault('SERVER_PROTOCOL','HTTP/1.0')
+
environ.setdefault('HTTP_HOST',environ['SERVER_NAME'])
environ.setdefault('REQUEST_METHOD','GET')
@@ -160,5 +162,44 @@ def setup_testing_defaults(environ):
+_hoppish = {
+ 'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
+ 'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
+ 'upgrade':1
+}.has_key
+
+def is_hop_by_hop(header_name):
+ """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header"""
+ return _hoppish(header_name.lower())
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+