Home | History | Annotate | Download | only in util
      1 """
      2 SCGI-->WSGI application proxy, "SWAP".
      3 
      4 (Originally written by Titus Brown.)
      5 
      6 This lets an SCGI front-end like mod_scgi be used to execute WSGI
      7 application objects.  To use it, subclass the SWAP class like so::
      8 
      9    class TestAppHandler(swap.SWAP):
     10        def __init__(self, *args, **kwargs):
     11            self.prefix = '/canal'
     12            self.app_obj = TestAppClass
     13            swap.SWAP.__init__(self, *args, **kwargs)
     14 
     15 where 'TestAppClass' is the application object from WSGI and '/canal'
     16 is the prefix for what is served by the SCGI Web-server-side process.
     17 
     18 Then execute the SCGI handler "as usual" by doing something like this::
     19 
     20    scgi_server.SCGIServer(TestAppHandler, port=4000).serve()
     21 
     22 and point mod_scgi (or whatever your SCGI front end is) at port 4000.
     23 
     24 Kudos to the WSGI folk for writing a nice PEP & the Quixote folk for
     25 writing a nice extensible SCGI server for Python!
     26 """
     27 
     28 import six
     29 import sys
     30 import time
     31 from scgi import scgi_server
     32 
     33 def debug(msg):
     34     timestamp = time.strftime("%Y-%m-%d %H:%M:%S",
     35                               time.localtime(time.time()))
     36     sys.stderr.write("[%s] %s\n" % (timestamp, msg))
     37 
     38 class SWAP(scgi_server.SCGIHandler):
     39     """
     40     SCGI->WSGI application proxy: let an SCGI server execute WSGI
     41     application objects.
     42     """
     43     app_obj = None
     44     prefix = None
     45 
     46     def __init__(self, *args, **kwargs):
     47         assert self.app_obj, "must set app_obj"
     48         assert self.prefix is not None, "must set prefix"
     49         args = (self,) + args
     50         scgi_server.SCGIHandler.__init__(*args, **kwargs)
     51 
     52     def handle_connection(self, conn):
     53         """
     54         Handle an individual connection.
     55         """
     56         input = conn.makefile("r")
     57         output = conn.makefile("w")
     58 
     59         environ = self.read_env(input)
     60         environ['wsgi.input']        = input
     61         environ['wsgi.errors']       = sys.stderr
     62         environ['wsgi.version']      = (1, 0)
     63         environ['wsgi.multithread']  = False
     64         environ['wsgi.multiprocess'] = True
     65         environ['wsgi.run_once']     = False
     66 
     67         # dunno how SCGI does HTTPS signalling; can't test it myself... @CTB
     68         if environ.get('HTTPS','off') in ('on','1'):
     69             environ['wsgi.url_scheme'] = 'https'
     70         else:
     71             environ['wsgi.url_scheme'] = 'http'
     72 
     73         ## SCGI does some weird environ manglement.  We need to set
     74         ## SCRIPT_NAME from 'prefix' and then set PATH_INFO from
     75         ## REQUEST_URI.
     76 
     77         prefix = self.prefix
     78         path = environ['REQUEST_URI'][len(prefix):].split('?', 1)[0]
     79 
     80         environ['SCRIPT_NAME'] = prefix
     81         environ['PATH_INFO'] = path
     82 
     83         headers_set = []
     84         headers_sent = []
     85         chunks = []
     86         def write(data):
     87             chunks.append(data)
     88 
     89         def start_response(status, response_headers, exc_info=None):
     90             if exc_info:
     91                 try:
     92                     if headers_sent:
     93                         # Re-raise original exception if headers sent
     94                         six.reraise(exc_info[0], exc_info[1], exc_info[2])
     95                 finally:
     96                     exc_info = None     # avoid dangling circular ref
     97             elif headers_set:
     98                 raise AssertionError("Headers already set!")
     99 
    100             headers_set[:] = [status, response_headers]
    101             return write
    102 
    103         ###
    104 
    105         result = self.app_obj(environ, start_response)
    106         try:
    107             for data in result:
    108                 chunks.append(data)
    109 
    110             # Before the first output, send the stored headers
    111             if not headers_set:
    112                 # Error -- the app never called start_response
    113                 status = '500 Server Error'
    114                 response_headers = [('Content-type', 'text/html')]
    115                 chunks = ["XXX start_response never called"]
    116             else:
    117                 status, response_headers = headers_sent[:] = headers_set
    118 
    119             output.write('Status: %s\r\n' % status)
    120             for header in response_headers:
    121                 output.write('%s: %s\r\n' % header)
    122             output.write('\r\n')
    123 
    124             for data in chunks:
    125                 output.write(data)
    126         finally:
    127             if hasattr(result,'close'):
    128                 result.close()
    129 
    130         # SCGI backends use connection closing to signal 'fini'.
    131         try:
    132             input.close()
    133             output.close()
    134             conn.close()
    135         except IOError as err:
    136             debug("IOError while closing connection ignored: %s" % err)
    137 
    138 
    139 def serve_application(application, prefix, port=None, host=None, max_children=None):
    140     """
    141     Serve the specified WSGI application via SCGI proxy.
    142 
    143     ``application``
    144         The WSGI application to serve.
    145 
    146     ``prefix``
    147         The prefix for what is served by the SCGI Web-server-side process.
    148 
    149     ``port``
    150         Optional port to bind the SCGI proxy to. Defaults to SCGIServer's
    151         default port value.
    152 
    153     ``host``
    154         Optional host to bind the SCGI proxy to. Defaults to SCGIServer's
    155         default host value.
    156 
    157     ``host``
    158         Optional maximum number of child processes the SCGIServer will
    159         spawn. Defaults to SCGIServer's default max_children value.
    160     """
    161     class SCGIAppHandler(SWAP):
    162         def __init__ (self, *args, **kwargs):
    163             self.prefix = prefix
    164             self.app_obj = application
    165             SWAP.__init__(self, *args, **kwargs)
    166 
    167     kwargs = dict(handler_class=SCGIAppHandler)
    168     for kwarg in ('host', 'port', 'max_children'):
    169         if locals()[kwarg] is not None:
    170             kwargs[kwarg] = locals()[kwarg]
    171 
    172     scgi_server.SCGIServer(**kwargs).serve()
    173