Home | History | Annotate | Download | only in wsgiref
      1 """Base classes for server/gateway implementations"""
      2 
      3 from types import StringType
      4 from util import FileWrapper, guess_scheme, is_hop_by_hop
      5 from headers import Headers
      6 
      7 import sys, os, time
      8 
      9 __all__ = ['BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler']
     10 
     11 try:
     12     dict
     13 except NameError:
     14     def dict(items):
     15         d = {}
     16         for k,v in items:
     17             d[k] = v
     18         return d
     19 
     20 # Uncomment for 2.2 compatibility.

     21 #try:

     22 #    True

     23 #    False

     24 #except NameError:

     25 #    True = not None

     26 #    False = not True

     27 
     28 
     29 # Weekday and month names for HTTP date/time formatting; always English!

     30 _weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
     31 _monthname = [None, # Dummy so we can use 1-based month numbers

     32               "Jan", "Feb", "Mar", "Apr", "May", "Jun",
     33               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
     34 
     35 def format_date_time(timestamp):
     36     year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
     37     return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
     38         _weekdayname[wd], day, _monthname[month], year, hh, mm, ss
     39     )
     40 
     41 
     42 class BaseHandler:
     43     """Manage the invocation of a WSGI application"""
     44 
     45     # Configuration parameters; can override per-subclass or per-instance

     46     wsgi_version = (1,0)
     47     wsgi_multithread = True
     48     wsgi_multiprocess = True
     49     wsgi_run_once = False
     50 
     51     origin_server = True    # We are transmitting direct to client

     52     http_version  = "1.0"   # Version that should be used for response

     53     server_software = None  # String name of server software, if any

     54 
     55     # os_environ is used to supply configuration from the OS environment:

     56     # by default it's a copy of 'os.environ' as of import time, but you can

     57     # override this in e.g. your __init__ method.

     58     os_environ = dict(os.environ.items())
     59 
     60     # Collaborator classes

     61     wsgi_file_wrapper = FileWrapper     # set to None to disable

     62     headers_class = Headers             # must be a Headers-like class

     63 
     64     # Error handling (also per-subclass or per-instance)

     65     traceback_limit = None  # Print entire traceback to self.get_stderr()

     66     error_status = "500 Internal Server Error"
     67     error_headers = [('Content-Type','text/plain')]
     68     error_body = "A server error occurred.  Please contact the administrator."
     69 
     70     # State variables (don't mess with these)

     71     status = result = None
     72     headers_sent = False
     73     headers = None
     74     bytes_sent = 0
     75 
     76     def run(self, application):
     77         """Invoke the application"""
     78         # Note to self: don't move the close()!  Asynchronous servers shouldn't

     79         # call close() from finish_response(), so if you close() anywhere but

     80         # the double-error branch here, you'll break asynchronous servers by

     81         # prematurely closing.  Async servers must return from 'run()' without

     82         # closing if there might still be output to iterate over.

     83         try:
     84             self.setup_environ()
     85             self.result = application(self.environ, self.start_response)
     86             self.finish_response()
     87         except:
     88             try:
     89                 self.handle_error()
     90             except:
     91                 # If we get an error handling an error, just give up already!

     92                 self.close()
     93                 raise   # ...and let the actual server figure it out.

     94 
     95 
     96     def setup_environ(self):
     97         """Set up the environment for one request"""
     98 
     99         env = self.environ = self.os_environ.copy()
    100         self.add_cgi_vars()
    101 
    102         env['wsgi.input']        = self.get_stdin()
    103         env['wsgi.errors']       = self.get_stderr()
    104         env['wsgi.version']      = self.wsgi_version
    105         env['wsgi.run_once']     = self.wsgi_run_once
    106         env['wsgi.url_scheme']   = self.get_scheme()
    107         env['wsgi.multithread']  = self.wsgi_multithread
    108         env['wsgi.multiprocess'] = self.wsgi_multiprocess
    109 
    110         if self.wsgi_file_wrapper is not None:
    111             env['wsgi.file_wrapper'] = self.wsgi_file_wrapper
    112 
    113         if self.origin_server and self.server_software:
    114             env.setdefault('SERVER_SOFTWARE',self.server_software)
    115 
    116 
    117     def finish_response(self):
    118         """Send any iterable data, then close self and the iterable
    119 
    120         Subclasses intended for use in asynchronous servers will
    121         want to redefine this method, such that it sets up callbacks
    122         in the event loop to iterate over the data, and to call
    123         'self.close()' once the response is finished.
    124         """
    125         if not self.result_is_file() or not self.sendfile():
    126             for data in self.result:
    127                 self.write(data)
    128             self.finish_content()
    129         self.close()
    130 
    131 
    132     def get_scheme(self):
    133         """Return the URL scheme being used"""
    134         return guess_scheme(self.environ)
    135 
    136 
    137     def set_content_length(self):
    138         """Compute Content-Length or switch to chunked encoding if possible"""
    139         try:
    140             blocks = len(self.result)
    141         except (TypeError,AttributeError,NotImplementedError):
    142             pass
    143         else:
    144             if blocks==1:
    145                 self.headers['Content-Length'] = str(self.bytes_sent)
    146                 return
    147         # XXX Try for chunked encoding if origin server and client is 1.1

    148 
    149 
    150     def cleanup_headers(self):
    151         """Make any necessary header changes or defaults
    152 
    153         Subclasses can extend this to add other defaults.
    154         """
    155         if 'Content-Length' not in self.headers:
    156             self.set_content_length()
    157 
    158     def start_response(self, status, headers,exc_info=None):
    159         """'start_response()' callable as specified by PEP 333"""
    160 
    161         if exc_info:
    162             try:
    163                 if self.headers_sent:
    164                     # Re-raise original exception if headers sent

    165                     raise exc_info[0], exc_info[1], exc_info[2]
    166             finally:
    167                 exc_info = None        # avoid dangling circular ref

    168         elif self.headers is not None:
    169             raise AssertionError("Headers already set!")
    170 
    171         assert type(status) is StringType,"Status must be a string"
    172         assert len(status)>=4,"Status must be at least 4 characters"
    173         assert int(status[:3]),"Status message must begin w/3-digit code"
    174         assert status[3]==" ", "Status message must have a space after code"
    175         if __debug__:
    176             for name,val in headers:
    177                 assert type(name) is StringType,"Header names must be strings"
    178                 assert type(val) is StringType,"Header values must be strings"
    179                 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
    180         self.status = status
    181         self.headers = self.headers_class(headers)
    182         return self.write
    183 
    184 
    185     def send_preamble(self):
    186         """Transmit version/status/date/server, via self._write()"""
    187         if self.origin_server:
    188             if self.client_is_modern():
    189                 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
    190                 if 'Date' not in self.headers:
    191                     self._write(
    192                         'Date: %s\r\n' % format_date_time(time.time())
    193                     )
    194                 if self.server_software and 'Server' not in self.headers:
    195                     self._write('Server: %s\r\n' % self.server_software)
    196         else:
    197             self._write('Status: %s\r\n' % self.status)
    198 
    199     def write(self, data):
    200         """'write()' callable as specified by PEP 333"""
    201 
    202         assert type(data) is StringType,"write() argument must be string"
    203 
    204         if not self.status:
    205             raise AssertionError("write() before start_response()")
    206 
    207         elif not self.headers_sent:
    208             # Before the first output, send the stored headers

    209             self.bytes_sent = len(data)    # make sure we know content-length

    210             self.send_headers()
    211         else:
    212             self.bytes_sent += len(data)
    213 
    214         # XXX check Content-Length and truncate if too many bytes written?

    215         self._write(data)
    216         self._flush()
    217 
    218 
    219     def sendfile(self):
    220         """Platform-specific file transmission
    221 
    222         Override this method in subclasses to support platform-specific
    223         file transmission.  It is only called if the application's
    224         return iterable ('self.result') is an instance of
    225         'self.wsgi_file_wrapper'.
    226 
    227         This method should return a true value if it was able to actually
    228         transmit the wrapped file-like object using a platform-specific
    229         approach.  It should return a false value if normal iteration
    230         should be used instead.  An exception can be raised to indicate
    231         that transmission was attempted, but failed.
    232 
    233         NOTE: this method should call 'self.send_headers()' if
    234         'self.headers_sent' is false and it is going to attempt direct
    235         transmission of the file.
    236         """
    237         return False   # No platform-specific transmission by default

    238 
    239 
    240     def finish_content(self):
    241         """Ensure headers and content have both been sent"""
    242         if not self.headers_sent:
    243             # Only zero Content-Length if not set by the application (so

    244             # that HEAD requests can be satisfied properly, see #3839)

    245             self.headers.setdefault('Content-Length', "0")
    246             self.send_headers()
    247         else:
    248             pass # XXX check if content-length was too short?

    249 
    250     def close(self):
    251         """Close the iterable (if needed) and reset all instance vars
    252 
    253         Subclasses may want to also drop the client connection.
    254         """
    255         try:
    256             if hasattr(self.result,'close'):
    257                 self.result.close()
    258         finally:
    259             self.result = self.headers = self.status = self.environ = None
    260             self.bytes_sent = 0; self.headers_sent = False
    261 
    262 
    263     def send_headers(self):
    264         """Transmit headers to the client, via self._write()"""
    265         self.cleanup_headers()
    266         self.headers_sent = True
    267         if not self.origin_server or self.client_is_modern():
    268             self.send_preamble()
    269             self._write(str(self.headers))
    270 
    271 
    272     def result_is_file(self):
    273         """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
    274         wrapper = self.wsgi_file_wrapper
    275         return wrapper is not None and isinstance(self.result,wrapper)
    276 
    277 
    278     def client_is_modern(self):
    279         """True if client can accept status and headers"""
    280         return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
    281 
    282 
    283     def log_exception(self,exc_info):
    284         """Log the 'exc_info' tuple in the server log
    285 
    286         Subclasses may override to retarget the output or change its format.
    287         """
    288         try:
    289             from traceback import print_exception
    290             stderr = self.get_stderr()
    291             print_exception(
    292                 exc_info[0], exc_info[1], exc_info[2],
    293                 self.traceback_limit, stderr
    294             )
    295             stderr.flush()
    296         finally:
    297             exc_info = None
    298 
    299     def handle_error(self):
    300         """Log current error, and send error output to client if possible"""
    301         self.log_exception(sys.exc_info())
    302         if not self.headers_sent:
    303             self.result = self.error_output(self.environ, self.start_response)
    304             self.finish_response()
    305         # XXX else: attempt advanced recovery techniques for HTML or text?

    306 
    307     def error_output(self, environ, start_response):
    308         """WSGI mini-app to create error output
    309 
    310         By default, this just uses the 'error_status', 'error_headers',
    311         and 'error_body' attributes to generate an output page.  It can
    312         be overridden in a subclass to dynamically generate diagnostics,
    313         choose an appropriate message for the user's preferred language, etc.
    314 
    315         Note, however, that it's not recommended from a security perspective to
    316         spit out diagnostics to any old user; ideally, you should have to do
    317         something special to enable diagnostic output, which is why we don't
    318         include any here!
    319         """
    320         start_response(self.error_status,self.error_headers[:],sys.exc_info())
    321         return [self.error_body]
    322 
    323 
    324     # Pure abstract methods; *must* be overridden in subclasses

    325 
    326     def _write(self,data):
    327         """Override in subclass to buffer data for send to client
    328 
    329         It's okay if this method actually transmits the data; BaseHandler
    330         just separates write and flush operations for greater efficiency
    331         when the underlying system actually has such a distinction.
    332         """
    333         raise NotImplementedError
    334 
    335     def _flush(self):
    336         """Override in subclass to force sending of recent '_write()' calls
    337 
    338         It's okay if this method is a no-op (i.e., if '_write()' actually
    339         sends the data.
    340         """
    341         raise NotImplementedError
    342 
    343     def get_stdin(self):
    344         """Override in subclass to return suitable 'wsgi.input'"""
    345         raise NotImplementedError
    346 
    347     def get_stderr(self):
    348         """Override in subclass to return suitable 'wsgi.errors'"""
    349         raise NotImplementedError
    350 
    351     def add_cgi_vars(self):
    352         """Override in subclass to insert CGI variables in 'self.environ'"""
    353         raise NotImplementedError
    354 
    355 
    356 class SimpleHandler(BaseHandler):
    357     """Handler that's just initialized with streams, environment, etc.
    358 
    359     This handler subclass is intended for synchronous HTTP/1.0 origin servers,
    360     and handles sending the entire response output, given the correct inputs.
    361 
    362     Usage::
    363 
    364         handler = SimpleHandler(
    365             inp,out,err,env, multithread=False, multiprocess=True
    366         )
    367         handler.run(app)"""
    368 
    369     def __init__(self,stdin,stdout,stderr,environ,
    370         multithread=True, multiprocess=False
    371     ):
    372         self.stdin = stdin
    373         self.stdout = stdout
    374         self.stderr = stderr
    375         self.base_env = environ
    376         self.wsgi_multithread = multithread
    377         self.wsgi_multiprocess = multiprocess
    378 
    379     def get_stdin(self):
    380         return self.stdin
    381 
    382     def get_stderr(self):
    383         return self.stderr
    384 
    385     def add_cgi_vars(self):
    386         self.environ.update(self.base_env)
    387 
    388     def _write(self,data):
    389         self.stdout.write(data)
    390         self._write = self.stdout.write
    391 
    392     def _flush(self):
    393         self.stdout.flush()
    394         self._flush = self.stdout.flush
    395 
    396 
    397 class BaseCGIHandler(SimpleHandler):
    398 
    399     """CGI-like systems using input/output/error streams and environ mapping
    400 
    401     Usage::
    402 
    403         handler = BaseCGIHandler(inp,out,err,env)
    404         handler.run(app)
    405 
    406     This handler class is useful for gateway protocols like ReadyExec and
    407     FastCGI, that have usable input/output/error streams and an environment
    408     mapping.  It's also the base class for CGIHandler, which just uses
    409     sys.stdin, os.environ, and so on.
    410 
    411     The constructor also takes keyword arguments 'multithread' and
    412     'multiprocess' (defaulting to 'True' and 'False' respectively) to control
    413     the configuration sent to the application.  It sets 'origin_server' to
    414     False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
    415     False.
    416     """
    417 
    418     origin_server = False
    419 
    420 
    421 class CGIHandler(BaseCGIHandler):
    422 
    423     """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
    424 
    425     Usage::
    426 
    427         CGIHandler().run(app)
    428 
    429     The difference between this class and BaseCGIHandler is that it always
    430     uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
    431     'wsgi.multiprocess' of 'True'.  It does not take any initialization
    432     parameters, but always uses 'sys.stdin', 'os.environ', and friends.
    433 
    434     If you need to override any of these parameters, use BaseCGIHandler
    435     instead.
    436     """
    437 
    438     wsgi_run_once = True
    439     # Do not allow os.environ to leak between requests in Google App Engine

    440     # and other multi-run CGI use cases.  This is not easily testable.

    441     # See http://bugs.python.org/issue7250

    442     os_environ = {}
    443 
    444     def __init__(self):
    445         BaseCGIHandler.__init__(
    446             self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
    447             multithread=False, multiprocess=True
    448         )
    449