Ticket #14903: wsgiref_usage.diff

File wsgiref_usage.diff, 23.7 KB (added by Maxim Bublis, 14 years ago)

Patch replacing current development server with wsgiref

  • django/core/servers/basehttp.py

     
    77been reviewed for security issues. Don't use it for production use.
    88"""
    99
    10 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
    1110import os
    12 import re
    1311import socket
    1412import sys
    1513import urllib
    16 import warnings
     14from wsgiref.simple_server import (
     15        ServerHandler as OriginServerHandler,
     16        WSGIServer as OriginWSGIServer,
     17        WSGIRequestHandler as OriginWSGIRequestHandler)
    1718
    1819from django.core.management.color import color_style
    19 from django.utils.http import http_date
    2020from django.utils._os import safe_join
    2121
    2222from django.contrib.staticfiles import handlers, views as static
    2323
    24 __version__ = "0.1"
    25 __all__ = ['WSGIServer','WSGIRequestHandler']
     24__all__ = ['WSGIServer', 'WSGIRequestHandler']
    2625
    27 server_version = "WSGIServer/" + __version__
    28 sys_version = "Python/" + sys.version.split()[0]
    29 software_version = server_version + ' ' + sys_version
    3026
    3127class WSGIServerException(Exception):
    3228    pass
    3329
    34 class FileWrapper(object):
    35     """Wrapper to convert file-like objects to iterables"""
    3630
    37     def __init__(self, filelike, blksize=8192):
    38         self.filelike = filelike
    39         self.blksize = blksize
    40         if hasattr(filelike,'close'):
    41             self.close = filelike.close
     31class ServerHandler(OriginServerHandler):
    4232
    43     def __getitem__(self,key):
    44         data = self.filelike.read(self.blksize)
    45         if data:
    46             return data
    47         raise IndexError
    48 
    49     def __iter__(self):
    50         return self
    51 
    52     def next(self):
    53         data = self.filelike.read(self.blksize)
    54         if data:
    55             return data
    56         raise StopIteration
    57 
    58 # Regular expression that matches `special' characters in parameters, the
    59 # existence of which force quoting of the parameter value.
    60 tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
    61 
    62 def _formatparam(param, value=None, quote=1):
    63     """Convenience function to format and return a key=value pair.
    64 
    65     This will quote the value if needed or if quote is true.
    66     """
    67     if value is not None and len(value) > 0:
    68         if quote or tspecials.search(value):
    69             value = value.replace('\\', '\\\\').replace('"', r'\"')
    70             return '%s="%s"' % (param, value)
    71         else:
    72             return '%s=%s' % (param, value)
    73     else:
    74         return param
    75 
    76 class Headers(object):
    77     """Manage a collection of HTTP response headers"""
    78     def __init__(self,headers):
    79         if not isinstance(headers, list):
    80             raise TypeError("Headers must be a list of name/value tuples")
    81         self._headers = headers
    82 
    83     def __len__(self):
    84         """Return the total number of headers, including duplicates."""
    85         return len(self._headers)
    86 
    87     def __setitem__(self, name, val):
    88         """Set the value of a header."""
    89         del self[name]
    90         self._headers.append((name, val))
    91 
    92     def __delitem__(self,name):
    93         """Delete all occurrences of a header, if present.
    94 
    95         Does *not* raise an exception if the header is missing.
    96         """
    97         name = name.lower()
    98         self._headers[:] = [kv for kv in self._headers if kv[0].lower()<>name]
    99 
    100     def __getitem__(self,name):
    101         """Get the first header value for 'name'
    102 
    103         Return None if the header is missing instead of raising an exception.
    104 
    105         Note that if the header appeared multiple times, the first exactly which
    106         occurrance gets returned is undefined.  Use getall() to get all
    107         the values matching a header field name.
    108         """
    109         return self.get(name)
    110 
    111     def has_key(self, name):
    112         """Return true if the message contains the header."""
    113         return self.get(name) is not None
    114 
    115     __contains__ = has_key
    116 
    117     def get_all(self, name):
    118         """Return a list of all the values for the named field.
    119 
    120         These will be sorted in the order they appeared in the original header
    121         list or were added to this instance, and may contain duplicates.  Any
    122         fields deleted and re-inserted are always appended to the header list.
    123         If no fields exist with the given name, returns an empty list.
    124         """
    125         name = name.lower()
    126         return [kv[1] for kv in self._headers if kv[0].lower()==name]
    127 
    128 
    129     def get(self,name,default=None):
    130         """Get the first header value for 'name', or return 'default'"""
    131         name = name.lower()
    132         for k,v in self._headers:
    133             if k.lower()==name:
    134                 return v
    135         return default
    136 
    137     def keys(self):
    138         """Return a list of all the header field names.
    139 
    140         These will be sorted in the order they appeared in the original header
    141         list, or were added to this instance, and may contain duplicates.
    142         Any fields deleted and re-inserted are always appended to the header
    143         list.
    144         """
    145         return [k for k, v in self._headers]
    146 
    147     def values(self):
    148         """Return a list of all header values.
    149 
    150         These will be sorted in the order they appeared in the original header
    151         list, or were added to this instance, and may contain duplicates.
    152         Any fields deleted and re-inserted are always appended to the header
    153         list.
    154         """
    155         return [v for k, v in self._headers]
    156 
    157     def items(self):
    158         """Get all the header fields and values.
    159 
    160         These will be sorted in the order they were in the original header
    161         list, or were added to this instance, and may contain duplicates.
    162         Any fields deleted and re-inserted are always appended to the header
    163         list.
    164         """
    165         return self._headers[:]
    166 
    167     def __repr__(self):
    168         return "Headers(%s)" % `self._headers`
    169 
    170     def __str__(self):
    171         """str() returns the formatted headers, complete with end line,
    172         suitable for direct HTTP transmission."""
    173         return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['',''])
    174 
    175     def setdefault(self,name,value):
    176         """Return first matching header value for 'name', or 'value'
    177 
    178         If there is no header named 'name', add a new header with name 'name'
    179         and value 'value'."""
    180         result = self.get(name)
    181         if result is None:
    182             self._headers.append((name,value))
    183             return value
    184         else:
    185             return result
    186 
    187     def add_header(self, _name, _value, **_params):
    188         """Extended header setting.
    189 
    190         _name is the header field to add.  keyword arguments can be used to set
    191         additional parameters for the header field, with underscores converted
    192         to dashes.  Normally the parameter will be added as key="value" unless
    193         value is None, in which case only the key will be added.
    194 
    195         Example:
    196 
    197         h.add_header('content-disposition', 'attachment', filename='bud.gif')
    198 
    199         Note that unlike the corresponding 'email.Message' method, this does
    200         *not* handle '(charset, language, value)' tuples: all values must be
    201         strings or None.
    202         """
    203         parts = []
    204         if _value is not None:
    205             parts.append(_value)
    206         for k, v in _params.items():
    207             if v is None:
    208                 parts.append(k.replace('_', '-'))
    209             else:
    210                 parts.append(_formatparam(k.replace('_', '-'), v))
    211         self._headers.append((_name, "; ".join(parts)))
    212 
    213 def guess_scheme(environ):
    214     """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https'
    215     """
    216     if environ.get("HTTPS") in ('yes','on','1'):
    217         return 'https'
    218     else:
    219         return 'http'
    220 
    221 _hop_headers = {
    222     'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    223     'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    224     'upgrade':1
    225 }
    226 
    227 def is_hop_by_hop(header_name):
    228     """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header"""
    229     return header_name.lower() in _hop_headers
    230 
    231 class ServerHandler(object):
    232     """Manage the invocation of a WSGI application"""
    233 
    234     # Configuration parameters; can override per-subclass or per-instance
    235     wsgi_version = (1,0)
    236     wsgi_multithread = True
    237     wsgi_multiprocess = True
    238     wsgi_run_once = False
    239 
    240     origin_server = True    # We are transmitting direct to client
    241     http_version  = "1.0"   # Version that should be used for response
    242     server_software = software_version
    243 
    244     # os_environ is used to supply configuration from the OS environment:
    245     # by default it's a copy of 'os.environ' as of import time, but you can
    246     # override this in e.g. your __init__ method.
    247     os_environ = dict(os.environ.items())
    248 
    249     # Collaborator classes
    250     wsgi_file_wrapper = FileWrapper     # set to None to disable
    251     headers_class = Headers             # must be a Headers-like class
    252 
    253     # Error handling (also per-subclass or per-instance)
    254     traceback_limit = None  # Print entire traceback to self.get_stderr()
    255     error_status = "500 INTERNAL SERVER ERROR"
    256     error_headers = [('Content-Type','text/plain')]
    257 
    258     # State variables (don't mess with these)
    259     status = result = None
    260     headers_sent = False
    261     headers = None
    262     bytes_sent = 0
    263 
    264     def __init__(self, stdin, stdout, stderr, environ, multithread=True,
    265         multiprocess=False):
    266         self.stdin = stdin
    267         self.stdout = stdout
    268         self.stderr = stderr
    269         self.base_env = environ
    270         self.wsgi_multithread = multithread
    271         self.wsgi_multiprocess = multiprocess
    272 
    273     def run(self, application):
    274         """Invoke the application"""
    275         # Note to self: don't move the close()!  Asynchronous servers shouldn't
    276         # call close() from finish_response(), so if you close() anywhere but
    277         # the double-error branch here, you'll break asynchronous servers by
    278         # prematurely closing.  Async servers must return from 'run()' without
    279         # closing if there might still be output to iterate over.
    280         try:
    281             self.setup_environ()
    282             self.result = application(self.environ, self.start_response)
    283             self.finish_response()
    284         except:
    285             try:
    286                 self.handle_error()
    287             except:
    288                 # If we get an error handling an error, just give up already!
    289                 self.close()
    290                 raise   # ...and let the actual server figure it out.
    291 
    292     def setup_environ(self):
    293         """Set up the environment for one request"""
    294 
    295         env = self.environ = self.os_environ.copy()
    296         self.add_cgi_vars()
    297 
    298         env['wsgi.input']        = self.get_stdin()
    299         env['wsgi.errors']       = self.get_stderr()
    300         env['wsgi.version']      = self.wsgi_version
    301         env['wsgi.run_once']     = self.wsgi_run_once
    302         env['wsgi.url_scheme']   = self.get_scheme()
    303         env['wsgi.multithread']  = self.wsgi_multithread
    304         env['wsgi.multiprocess'] = self.wsgi_multiprocess
    305 
    306         if self.wsgi_file_wrapper is not None:
    307             env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
    308 
    309         if self.origin_server and self.server_software:
    310             env.setdefault('SERVER_SOFTWARE',self.server_software)
    311 
    312     def finish_response(self):
    313         """
    314         Send any iterable data, then close self and the iterable
    315 
    316         Subclasses intended for use in asynchronous servers will want to
    317         redefine this method, such that it sets up callbacks in the event loop
    318         to iterate over the data, and to call 'self.close()' once the response
    319         is finished.
    320         """
    321         if not self.result_is_file() or not self.sendfile():
    322             for data in self.result:
    323                 self.write(data)
    324             self.finish_content()
    325         self.close()
    326 
    327     def get_scheme(self):
    328         """Return the URL scheme being used"""
    329         return guess_scheme(self.environ)
    330 
    331     def set_content_length(self):
    332         """Compute Content-Length or switch to chunked encoding if possible"""
    333         try:
    334             blocks = len(self.result)
    335         except (TypeError, AttributeError, NotImplementedError):
    336             pass
    337         else:
    338             if blocks==1:
    339                 self.headers['Content-Length'] = str(self.bytes_sent)
    340                 return
    341         # XXX Try for chunked encoding if origin server and client is 1.1
    342 
    343     def cleanup_headers(self):
    344         """Make any necessary header changes or defaults
    345 
    346         Subclasses can extend this to add other defaults.
    347         """
    348         if 'Content-Length' not in self.headers:
    349             self.set_content_length()
    350 
    351     def start_response(self, status, headers,exc_info=None):
    352         """'start_response()' callable as specified by PEP 333"""
    353 
    354         if exc_info:
    355             try:
    356                 if self.headers_sent:
    357                     # Re-raise original exception if headers sent
    358                     raise exc_info[0], exc_info[1], exc_info[2]
    359             finally:
    360                 exc_info = None        # avoid dangling circular ref
    361         elif self.headers is not None:
    362             raise AssertionError("Headers already set!")
    363 
    364         assert isinstance(status, str),"Status must be a string"
    365         assert len(status)>=4,"Status must be at least 4 characters"
    366         assert int(status[:3]),"Status message must begin w/3-digit code"
    367         assert status[3]==" ", "Status message must have a space after code"
    368         if __debug__:
    369             for name,val in headers:
    370                 assert isinstance(name, str),"Header names must be strings"
    371                 assert isinstance(val, str),"Header values must be strings"
    372                 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
    373         self.status = status
    374         self.headers = self.headers_class(headers)
    375         return self.write
    376 
    377     def send_preamble(self):
    378         """Transmit version/status/date/server, via self._write()"""
    379         if self.origin_server:
    380             if self.client_is_modern():
    381                 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
    382                 if 'Date' not in self.headers:
    383                     self._write(
    384                         'Date: %s\r\n' % http_date()
    385                     )
    386                 if self.server_software and 'Server' not in self.headers:
    387                     self._write('Server: %s\r\n' % self.server_software)
    388         else:
    389             self._write('Status: %s\r\n' % self.status)
    390 
    39133    def write(self, data):
    39234        """'write()' callable as specified by PEP 333"""
    39335
     
    40345        else:
    40446            self.bytes_sent += len(data)
    40547
    406         # XXX check Content-Length and truncate if too many bytes written?
    407 
    40848        # If data is too large, socket will choke, so write chunks no larger
    40949        # than 32MB at a time.
    41050        length = len(data)
     
    41252            offset = 0
    41353            while offset < length:
    41454                chunk_size = min(33554432, length)
    415                 self._write(data[offset:offset+chunk_size])
     55                self._write(data[offset:offset + chunk_size])
    41656                self._flush()
    41757                offset += chunk_size
    41858        else:
    41959            self._write(data)
    42060            self._flush()
    42161
    422     def sendfile(self):
    423         """Platform-specific file transmission
    424 
    425         Override this method in subclasses to support platform-specific
    426         file transmission.  It is only called if the application's
    427         return iterable ('self.result') is an instance of
    428         'self.wsgi_file_wrapper'.
    429 
    430         This method should return a true value if it was able to actually
    431         transmit the wrapped file-like object using a platform-specific
    432         approach.  It should return a false value if normal iteration
    433         should be used instead.  An exception can be raised to indicate
    434         that transmission was attempted, but failed.
    435 
    436         NOTE: this method should call 'self.send_headers()' if
    437         'self.headers_sent' is false and it is going to attempt direct
    438         transmission of the file1.
    439         """
    440         return False   # No platform-specific transmission by default
    441 
    442     def finish_content(self):
    443         """Ensure headers and content have both been sent"""
    444         if not self.headers_sent:
    445             self.headers['Content-Length'] = "0"
    446             self.send_headers()
    447         else:
    448             pass # XXX check if content-length was too short?
    449 
    450     def close(self):
    451         try:
    452             self.request_handler.log_request(self.status.split(' ',1)[0], self.bytes_sent)
    453         finally:
    454             try:
    455                 if hasattr(self.result,'close'):
    456                     self.result.close()
    457             finally:
    458                 self.result = self.headers = self.status = self.environ = None
    459                 self.bytes_sent = 0; self.headers_sent = False
    460 
    461     def send_headers(self):
    462         """Transmit headers to the client, via self._write()"""
    463         self.cleanup_headers()
    464         self.headers_sent = True
    465         if not self.origin_server or self.client_is_modern():
    466             self.send_preamble()
    467             self._write(str(self.headers))
    468 
    469     def result_is_file(self):
    470         """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
    471         wrapper = self.wsgi_file_wrapper
    472         return wrapper is not None and isinstance(self.result,wrapper)
    473 
    474     def client_is_modern(self):
    475         """True if client can accept status and headers"""
    476         return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
    477 
    478     def log_exception(self,exc_info):
    479         """Log the 'exc_info' tuple in the server log
    480 
    481         Subclasses may override to retarget the output or change its format.
    482         """
    483         try:
    484             from traceback import print_exception
    485             stderr = self.get_stderr()
    486             print_exception(
    487                 exc_info[0], exc_info[1], exc_info[2],
    488                 self.traceback_limit, stderr
    489             )
    490             stderr.flush()
    491         finally:
    492             exc_info = None
    493 
    494     def handle_error(self):
    495         """Log current error, and send error output to client if possible"""
    496         self.log_exception(sys.exc_info())
    497         if not self.headers_sent:
    498             self.result = self.error_output(self.environ, self.start_response)
    499             self.finish_response()
    500         # XXX else: attempt advanced recovery techniques for HTML or text?
    501 
    50262    def error_output(self, environ, start_response):
     63        OriginServerHandler.error_output(environ, start_response)
    50364        import traceback
    504         start_response(self.error_status, self.error_headers[:], sys.exc_info())
    50565        return ['\n'.join(traceback.format_exception(*sys.exc_info()))]
    50666
    507     # Pure abstract methods; *must* be overridden in subclasses
    50867
    509     def _write(self,data):
    510         self.stdout.write(data)
    511         self._write = self.stdout.write
     68class WSGIServer(OriginWSGIServer):
    51269
    513     def _flush(self):
    514         self.stdout.flush()
    515         self._flush = self.stdout.flush
    516 
    517     def get_stdin(self):
    518         return self.stdin
    519 
    520     def get_stderr(self):
    521         return self.stderr
    522 
    523     def add_cgi_vars(self):
    524         self.environ.update(self.base_env)
    525 
    526 class WSGIServer(HTTPServer):
    527     """BaseHTTPServer that implements the Python WSGI protocol"""
    528     application = None
    529 
    53070    def __init__(self, *args, **kwargs):
    53171        if kwargs.pop('ipv6', False):
    53272            self.address_family = socket.AF_INET6
    533         HTTPServer.__init__(self, *args, **kwargs)
     73        OriginWSGIServer.__init__(self, *args, **kwargs)
    53474
    53575    def server_bind(self):
    536         """Override server_bind to store the server name."""
    53776        try:
    538             HTTPServer.server_bind(self)
     77            OriginWSGIServer.server_bind(self)
    53978        except Exception, e:
    54079            raise WSGIServerException(e)
    541         self.setup_environ()
    54280
    543     def setup_environ(self):
    544         # Set up base environment
    545         env = self.base_environ = {}
    546         env['SERVER_NAME'] = self.server_name
    547         env['GATEWAY_INTERFACE'] = 'CGI/1.1'
    548         env['SERVER_PORT'] = str(self.server_port)
    549         env['REMOTE_HOST']=''
    550         env['CONTENT_LENGTH']=''
    551         env['SCRIPT_NAME'] = ''
    55281
    553     def get_app(self):
    554         return self.application
     82class WSGIRequestHandler(OriginWSGIRequestHandler):
    55583
    556     def set_app(self,application):
    557         self.application = application
    558 
    559 class WSGIRequestHandler(BaseHTTPRequestHandler):
    560     server_version = "WSGIServer/" + __version__
    561 
    56284    def __init__(self, *args, **kwargs):
    56385        from django.conf import settings
    56486        self.admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
     
    56688        # requests (like "OPTIONS").
    56789        self.path = ''
    56890        self.style = color_style()
    569         BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
     91        OriginWSGIRequestHandler.__init__(self, *args, **kwargs)
    57092
    571     def get_environ(self):
    572         env = self.server.base_environ.copy()
    573         env['SERVER_PROTOCOL'] = self.request_version
    574         env['REQUEST_METHOD'] = self.command
    575         if '?' in self.path:
    576             path,query = self.path.split('?',1)
    577         else:
    578             path,query = self.path,''
    579 
    580         env['PATH_INFO'] = urllib.unquote(path)
    581         env['QUERY_STRING'] = query
    582         env['REMOTE_ADDR'] = self.client_address[0]
    583 
    584         if self.headers.typeheader is None:
    585             env['CONTENT_TYPE'] = self.headers.type
    586         else:
    587             env['CONTENT_TYPE'] = self.headers.typeheader
    588 
    589         length = self.headers.getheader('content-length')
    590         if length:
    591             env['CONTENT_LENGTH'] = length
    592 
    593         for h in self.headers.headers:
    594             k,v = h.split(':',1)
    595             k=k.replace('-','_').upper(); v=v.strip()
    596             if k in env:
    597                 continue                    # skip content length, type,etc.
    598             if 'HTTP_'+k in env:
    599                 env['HTTP_'+k] += ','+v     # comma-separate multiple headers
    600             else:
    601                 env['HTTP_'+k] = v
    602         return env
    603 
    604     def get_stderr(self):
    605         return sys.stderr
    606 
    607     def handle(self):
    608         """Handle a single HTTP request"""
    609         self.raw_requestline = self.rfile.readline()
    610         if not self.parse_request(): # An error code has been sent, just exit
    611             return
    612         handler = ServerHandler(self.rfile, self.wfile, self.get_stderr(), self.get_environ())
    613         handler.request_handler = self      # backpointer for logging
    614         handler.run(self.server.get_app())
    615 
    61693    def log_message(self, format, *args):
    61794        # Don't bother logging requests for admin images or the favicon.
    618         if self.path.startswith(self.admin_media_prefix) or self.path == '/favicon.ico':
     95        if (self.path.startswith(self.admin_media_prefix) or
     96            self.path == '/favicon.ico'):
    61997            return
    62098
    62199        msg = "[%s] %s\n" % (self.log_date_time_string(), format % args)
     
    640118
    641119        sys.stderr.write(msg)
    642120
     121    def handle(self):
     122        """Handle a single HTTP request"""
     123        self.raw_requestline = self.rfile.readline()
     124        if not self.parse_request():  # An error code has been sent, just exit
     125            return
    643126
     127        handler = ServerHandler(self.rfile, self.wfile, self.get_stderr(),
     128                                self.get_environ())
     129        handler.request_handler = self      # backpointer for logging
     130        handler.run(self.server.get_app())
     131
     132
    644133class AdminMediaHandler(handlers.StaticFilesHandler):
    645134    """
    646135    WSGI middleware that intercepts calls to the admin media directory, as
     
    689178        """
    690179        return path.startswith(self.base_url[2]) and not self.base_url[1]
    691180
     181
    692182def run(addr, port, wsgi_handler, ipv6=False):
    693183    server_address = (addr, port)
    694184    httpd = WSGIServer(server_address, WSGIRequestHandler, ipv6=ipv6)
Back to Top