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         try:
    126             if not self.result_is_file() or not self.sendfile():
    127                 for data in self.result:
    128                     self.write(data)
    129                 self.finish_content()
    130         finally:
    131             self.close()
    132 
    133 
    134     def get_scheme(self):
    135         """Return the URL scheme being used"""
    136         return guess_scheme(self.environ)
    137 
    138 
    139     def set_content_length(self):
    140         """Compute Content-Length or switch to chunked encoding if possible"""
    141         try:
    142             blocks = len(self.result)
    143         except (TypeError,AttributeError,NotImplementedError):
    144             pass
    145         else:
    146             if blocks==1:
    147                 self.headers['Content-Length'] = str(self.bytes_sent)
    148                 return
    149         # XXX Try for chunked encoding if origin server and client is 1.1
    150 
    151 
    152     def cleanup_headers(self):
    153         """Make any necessary header changes or defaults
    154 
    155         Subclasses can extend this to add other defaults.
    156         """
    157         if 'Content-Length' not in self.headers:
    158             self.set_content_length()
    159 
    160     def start_response(self, status, headers,exc_info=None):
    161         """'start_response()' callable as specified by PEP 333"""
    162 
    163         if exc_info:
    164             try:
    165                 if self.headers_sent:
    166                     # Re-raise original exception if headers sent
    167                     raise exc_info[0], exc_info[1], exc_info[2]
    168             finally:
    169                 exc_info = None        # avoid dangling circular ref
    170         elif self.headers is not None:
    171             raise AssertionError("Headers already set!")
    172 
    173         assert type(status) is StringType,"Status must be a string"
    174         assert len(status)>=4,"Status must be at least 4 characters"
    175         assert int(status[:3]),"Status message must begin w/3-digit code"
    176         assert status[3]==" ", "Status message must have a space after code"
    177         if __debug__:
    178             for name,val in headers:
    179                 assert type(name) is StringType,"Header names must be strings"
    180                 assert type(val) is StringType,"Header values must be strings"
    181                 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
    182         self.status = status
    183         self.headers = self.headers_class(headers)
    184         return self.write
    185 
    186 
    187     def send_preamble(self):
    188         """Transmit version/status/date/server, via self._write()"""
    189         if self.origin_server:
    190             if self.client_is_modern():
    191                 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status))
    192                 if 'Date' not in self.headers:
    193                     self._write(
    194                         'Date: %s\r\n' % format_date_time(time.time())
    195                     )
    196                 if self.server_software and 'Server' not in self.headers:
    197                     self._write('Server: %s\r\n' % self.server_software)
    198         else:
    199             self._write('Status: %s\r\n' % self.status)
    200 
    201     def write(self, data):
    202         """'write()' callable as specified by PEP 333"""
    203 
    204         assert type(data) is StringType,"write() argument must be string"
    205 
    206         if not self.status:
    207             raise AssertionError("write() before start_response()")
    208 
    209         elif not self.headers_sent:
    210             # Before the first output, send the stored headers
    211             self.bytes_sent = len(data)    # make sure we know content-length
    212             self.send_headers()
    213         else:
    214             self.bytes_sent += len(data)
    215 
    216         # XXX check Content-Length and truncate if too many bytes written?
    217         self._write(data)
    218         self._flush()
    219 
    220 
    221     def sendfile(self):
    222         """Platform-specific file transmission
    223 
    224         Override this method in subclasses to support platform-specific
    225         file transmission.  It is only called if the application's
    226         return iterable ('self.result') is an instance of
    227         'self.wsgi_file_wrapper'.
    228 
    229         This method should return a true value if it was able to actually
    230         transmit the wrapped file-like object using a platform-specific
    231         approach.  It should return a false value if normal iteration
    232         should be used instead.  An exception can be raised to indicate
    233         that transmission was attempted, but failed.
    234 
    235         NOTE: this method should call 'self.send_headers()' if
    236         'self.headers_sent' is false and it is going to attempt direct
    237         transmission of the file.
    238         """
    239         return False   # No platform-specific transmission by default
    240 
    241 
    242     def finish_content(self):
    243         """Ensure headers and content have both been sent"""
    244         if not self.headers_sent:
    245             # Only zero Content-Length if not set by the application (so
    246             # that HEAD requests can be satisfied properly, see #3839)
    247             self.headers.setdefault('Content-Length', "0")
    248             self.send_headers()
    249         else:
    250             pass # XXX check if content-length was too short?
    251 
    252     def close(self):
    253         """Close the iterable (if needed) and reset all instance vars
    254 
    255         Subclasses may want to also drop the client connection.
    256         """
    257         try:
    258             if hasattr(self.result,'close'):
    259                 self.result.close()
    260         finally:
    261             self.result = self.headers = self.status = self.environ = None
    262             self.bytes_sent = 0; self.headers_sent = False
    263 
    264 
    265     def send_headers(self):
    266         """Transmit headers to the client, via self._write()"""
    267         self.cleanup_headers()
    268         self.headers_sent = True
    269         if not self.origin_server or self.client_is_modern():
    270             self.send_preamble()
    271             self._write(str(self.headers))
    272 
    273 
    274     def result_is_file(self):
    275         """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'"""
    276         wrapper = self.wsgi_file_wrapper
    277         return wrapper is not None and isinstance(self.result,wrapper)
    278 
    279 
    280     def client_is_modern(self):
    281         """True if client can accept status and headers"""
    282         return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9'
    283 
    284 
    285     def log_exception(self,exc_info):
    286         """Log the 'exc_info' tuple in the server log
    287 
    288         Subclasses may override to retarget the output or change its format.
    289         """
    290         try:
    291             from traceback import print_exception
    292             stderr = self.get_stderr()
    293             print_exception(
    294                 exc_info[0], exc_info[1], exc_info[2],
    295                 self.traceback_limit, stderr
    296             )
    297             stderr.flush()
    298         finally:
    299             exc_info = None
    300 
    301     def handle_error(self):
    302         """Log current error, and send error output to client if possible"""
    303         self.log_exception(sys.exc_info())
    304         if not self.headers_sent:
    305             self.result = self.error_output(self.environ, self.start_response)
    306             self.finish_response()
    307         # XXX else: attempt advanced recovery techniques for HTML or text?
    308 
    309     def error_output(self, environ, start_response):
    310         """WSGI mini-app to create error output
    311 
    312         By default, this just uses the 'error_status', 'error_headers',
    313         and 'error_body' attributes to generate an output page.  It can
    314         be overridden in a subclass to dynamically generate diagnostics,
    315         choose an appropriate message for the user's preferred language, etc.
    316 
    317         Note, however, that it's not recommended from a security perspective to
    318         spit out diagnostics to any old user; ideally, you should have to do
    319         something special to enable diagnostic output, which is why we don't
    320         include any here!
    321         """
    322         start_response(self.error_status,self.error_headers[:],sys.exc_info())
    323         return [self.error_body]
    324 
    325 
    326     # Pure abstract methods; *must* be overridden in subclasses
    327 
    328     def _write(self,data):
    329         """Override in subclass to buffer data for send to client
    330 
    331         It's okay if this method actually transmits the data; BaseHandler
    332         just separates write and flush operations for greater efficiency
    333         when the underlying system actually has such a distinction.
    334         """
    335         raise NotImplementedError
    336 
    337     def _flush(self):
    338         """Override in subclass to force sending of recent '_write()' calls
    339 
    340         It's okay if this method is a no-op (i.e., if '_write()' actually
    341         sends the data.
    342         """
    343         raise NotImplementedError
    344 
    345     def get_stdin(self):
    346         """Override in subclass to return suitable 'wsgi.input'"""
    347         raise NotImplementedError
    348 
    349     def get_stderr(self):
    350         """Override in subclass to return suitable 'wsgi.errors'"""
    351         raise NotImplementedError
    352 
    353     def add_cgi_vars(self):
    354         """Override in subclass to insert CGI variables in 'self.environ'"""
    355         raise NotImplementedError
    356 
    357 
    358 class SimpleHandler(BaseHandler):
    359     """Handler that's just initialized with streams, environment, etc.
    360 
    361     This handler subclass is intended for synchronous HTTP/1.0 origin servers,
    362     and handles sending the entire response output, given the correct inputs.
    363 
    364     Usage::
    365 
    366         handler = SimpleHandler(
    367             inp,out,err,env, multithread=False, multiprocess=True
    368         )
    369         handler.run(app)"""
    370 
    371     def __init__(self,stdin,stdout,stderr,environ,
    372         multithread=True, multiprocess=False
    373     ):
    374         self.stdin = stdin
    375         self.stdout = stdout
    376         self.stderr = stderr
    377         self.base_env = environ
    378         self.wsgi_multithread = multithread
    379         self.wsgi_multiprocess = multiprocess
    380 
    381     def get_stdin(self):
    382         return self.stdin
    383 
    384     def get_stderr(self):
    385         return self.stderr
    386 
    387     def add_cgi_vars(self):
    388         self.environ.update(self.base_env)
    389 
    390     def _write(self,data):
    391         self.stdout.write(data)
    392         self._write = self.stdout.write
    393 
    394     def _flush(self):
    395         self.stdout.flush()
    396         self._flush = self.stdout.flush
    397 
    398 
    399 class BaseCGIHandler(SimpleHandler):
    400 
    401     """CGI-like systems using input/output/error streams and environ mapping
    402 
    403     Usage::
    404 
    405         handler = BaseCGIHandler(inp,out,err,env)
    406         handler.run(app)
    407 
    408     This handler class is useful for gateway protocols like ReadyExec and
    409     FastCGI, that have usable input/output/error streams and an environment
    410     mapping.  It's also the base class for CGIHandler, which just uses
    411     sys.stdin, os.environ, and so on.
    412 
    413     The constructor also takes keyword arguments 'multithread' and
    414     'multiprocess' (defaulting to 'True' and 'False' respectively) to control
    415     the configuration sent to the application.  It sets 'origin_server' to
    416     False (to enable CGI-like output), and assumes that 'wsgi.run_once' is
    417     False.
    418     """
    419 
    420     origin_server = False
    421 
    422 
    423 class CGIHandler(BaseCGIHandler):
    424 
    425     """CGI-based invocation via sys.stdin/stdout/stderr and os.environ
    426 
    427     Usage::
    428 
    429         CGIHandler().run(app)
    430 
    431     The difference between this class and BaseCGIHandler is that it always
    432     uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and
    433     'wsgi.multiprocess' of 'True'.  It does not take any initialization
    434     parameters, but always uses 'sys.stdin', 'os.environ', and friends.
    435 
    436     If you need to override any of these parameters, use BaseCGIHandler
    437     instead.
    438     """
    439 
    440     wsgi_run_once = True
    441     # Do not allow os.environ to leak between requests in Google App Engine
    442     # and other multi-run CGI use cases.  This is not easily testable.
    443     # See http://bugs.python.org/issue7250
    444     os_environ = {}
    445 
    446     def __init__(self):
    447         BaseCGIHandler.__init__(
    448             self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()),
    449             multithread=False, multiprocess=True
    450         )
    451