From b60e8cd5700a11062b3a4a46fe212932e18cc2c3 Mon Sep 17 00:00:00 2001 From: pje Date: Wed, 6 Oct 2004 21:35:47 +0000 Subject: 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 --- src/wsgiref/handlers.py | 76 +++++++++++++++++++------------------- src/wsgiref/headers.py | 22 +++++------ src/wsgiref/tests/test_handlers.py | 42 ++++++++++++++++++++- src/wsgiref/tests/test_headers.py | 8 ++-- src/wsgiref/tests/test_util.py | 30 +++++++-------- src/wsgiref/util.py | 41 ++++++++++++++++++++ 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') @@ -156,6 +158,45 @@ def setup_testing_defaults(environ): environ.setdefault('SERVER_PORT', '80') elif environ['wsgi.url_scheme']=='https': environ.setdefault('SERVER_PORT', '443') + + + + +_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()) + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.1