1"""
2SCGI-->WSGI application proxy, "SWAP".
3
4(Originally written by Titus Brown.)
5
6This lets an SCGI front-end like mod_scgi be used to execute WSGI
7application 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
15where 'TestAppClass' is the application object from WSGI and '/canal'
16is the prefix for what is served by the SCGI Web-server-side process.
17
18Then execute the SCGI handler "as usual" by doing something like this::
19
20   scgi_server.SCGIServer(TestAppHandler, port=4000).serve()
21
22and point mod_scgi (or whatever your SCGI front end is) at port 4000.
23
24Kudos to the WSGI folk for writing a nice PEP & the Quixote folk for
25writing a nice extensible SCGI server for Python!
26"""
27
28import six
29import sys
30import time
31from scgi import scgi_server
32
33def 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
38class 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
139def 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