1"""WSGI Paste wrapper for mod_python. Requires Python 2.2 or greater.
2
3
4Example httpd.conf section for a Paste app with an ini file::
5
6    <Location />
7        SetHandler python-program
8        PythonHandler paste.modpython
9        PythonOption paste.ini /some/location/your/pasteconfig.ini
10    </Location>
11
12Or if you want to load a WSGI application under /your/homedir in the module
13``startup`` and the WSGI app is ``app``::
14
15    <Location />
16        SetHandler python-program
17        PythonHandler paste.modpython
18        PythonPath "['/virtual/project/directory'] + sys.path"
19        PythonOption wsgi.application startup::app
20    </Location>
21
22
23If you'd like to use a virtual installation, make sure to add it in the path
24like so::
25
26    <Location />
27        SetHandler python-program
28        PythonHandler paste.modpython
29        PythonPath "['/virtual/project/directory', '/virtual/lib/python2.4/'] + sys.path"
30        PythonOption paste.ini /virtual/project/directory/pasteconfig.ini
31    </Location>
32
33Some WSGI implementations assume that the SCRIPT_NAME environ variable will
34always be equal to "the root URL of the app"; Apache probably won't act as
35you expect in that case. You can add another PythonOption directive to tell
36modpython_gateway to force that behavior:
37
38    PythonOption SCRIPT_NAME /mcontrol
39
40Some WSGI applications need to be cleaned up when Apache exits. You can
41register a cleanup handler with yet another PythonOption directive:
42
43    PythonOption wsgi.cleanup module::function
44
45The module.function will be called with no arguments on server shutdown,
46once for each child process or thread.
47
48This module highly based on Robert Brewer's, here:
49http://projects.amor.org/misc/svn/modpython_gateway.py
50"""
51
52import six
53import traceback
54
55try:
56    from mod_python import apache
57except:
58    pass
59from paste.deploy import loadapp
60
61class InputWrapper(object):
62
63    def __init__(self, req):
64        self.req = req
65
66    def close(self):
67        pass
68
69    def read(self, size=-1):
70        return self.req.read(size)
71
72    def readline(self, size=-1):
73        return self.req.readline(size)
74
75    def readlines(self, hint=-1):
76        return self.req.readlines(hint)
77
78    def __iter__(self):
79        line = self.readline()
80        while line:
81            yield line
82            # Notice this won't prefetch the next line; it only
83            # gets called if the generator is resumed.
84            line = self.readline()
85
86
87class ErrorWrapper(object):
88
89    def __init__(self, req):
90        self.req = req
91
92    def flush(self):
93        pass
94
95    def write(self, msg):
96        self.req.log_error(msg)
97
98    def writelines(self, seq):
99        self.write(''.join(seq))
100
101
102bad_value = ("You must provide a PythonOption '%s', either 'on' or 'off', "
103             "when running a version of mod_python < 3.1")
104
105
106class Handler(object):
107
108    def __init__(self, req):
109        self.started = False
110
111        options = req.get_options()
112
113        # Threading and forking
114        try:
115            q = apache.mpm_query
116            threaded = q(apache.AP_MPMQ_IS_THREADED)
117            forked = q(apache.AP_MPMQ_IS_FORKED)
118        except AttributeError:
119            threaded = options.get('multithread', '').lower()
120            if threaded == 'on':
121                threaded = True
122            elif threaded == 'off':
123                threaded = False
124            else:
125                raise ValueError(bad_value % "multithread")
126
127            forked = options.get('multiprocess', '').lower()
128            if forked == 'on':
129                forked = True
130            elif forked == 'off':
131                forked = False
132            else:
133                raise ValueError(bad_value % "multiprocess")
134
135        env = self.environ = dict(apache.build_cgi_env(req))
136
137        if 'SCRIPT_NAME' in options:
138            # Override SCRIPT_NAME and PATH_INFO if requested.
139            env['SCRIPT_NAME'] = options['SCRIPT_NAME']
140            env['PATH_INFO'] = req.uri[len(options['SCRIPT_NAME']):]
141        else:
142            env['SCRIPT_NAME'] = ''
143            env['PATH_INFO'] = req.uri
144
145        env['wsgi.input'] = InputWrapper(req)
146        env['wsgi.errors'] = ErrorWrapper(req)
147        env['wsgi.version'] = (1, 0)
148        env['wsgi.run_once'] = False
149        if env.get("HTTPS") in ('yes', 'on', '1'):
150            env['wsgi.url_scheme'] = 'https'
151        else:
152            env['wsgi.url_scheme'] = 'http'
153        env['wsgi.multithread']  = threaded
154        env['wsgi.multiprocess'] = forked
155
156        self.request = req
157
158    def run(self, application):
159        try:
160            result = application(self.environ, self.start_response)
161            for data in result:
162                self.write(data)
163            if not self.started:
164                self.request.set_content_length(0)
165            if hasattr(result, 'close'):
166                result.close()
167        except:
168            traceback.print_exc(None, self.environ['wsgi.errors'])
169            if not self.started:
170                self.request.status = 500
171                self.request.content_type = 'text/plain'
172                data = "A server error occurred. Please contact the administrator."
173                self.request.set_content_length(len(data))
174                self.request.write(data)
175
176    def start_response(self, status, headers, exc_info=None):
177        if exc_info:
178            try:
179                if self.started:
180                    six.reraise(exc_info[0], exc_info[1], exc_info[2])
181            finally:
182                exc_info = None
183
184        self.request.status = int(status[:3])
185
186        for key, val in headers:
187            if key.lower() == 'content-length':
188                self.request.set_content_length(int(val))
189            elif key.lower() == 'content-type':
190                self.request.content_type = val
191            else:
192                self.request.headers_out.add(key, val)
193
194        return self.write
195
196    def write(self, data):
197        if not self.started:
198            self.started = True
199        self.request.write(data)
200
201
202startup = None
203cleanup = None
204wsgiapps = {}
205
206def handler(req):
207    options = req.get_options()
208    # Run a startup function if requested.
209    global startup
210    if 'wsgi.startup' in options and not startup:
211        func = options['wsgi.startup']
212        if func:
213            module_name, object_str = func.split('::', 1)
214            module = __import__(module_name, globals(), locals(), [''])
215            startup = apache.resolve_object(module, object_str)
216            startup(req)
217
218    # Register a cleanup function if requested.
219    global cleanup
220    if 'wsgi.cleanup' in options and not cleanup:
221        func = options['wsgi.cleanup']
222        if func:
223            module_name, object_str = func.split('::', 1)
224            module = __import__(module_name, globals(), locals(), [''])
225            cleanup = apache.resolve_object(module, object_str)
226            def cleaner(data):
227                cleanup()
228            try:
229                # apache.register_cleanup wasn't available until 3.1.4.
230                apache.register_cleanup(cleaner)
231            except AttributeError:
232                req.server.register_cleanup(req, cleaner)
233
234    # Import the wsgi 'application' callable and pass it to Handler.run
235    global wsgiapps
236    appini = options.get('paste.ini')
237    app = None
238    if appini:
239        if appini not in wsgiapps:
240            wsgiapps[appini] = loadapp("config:%s" % appini)
241        app = wsgiapps[appini]
242
243    # Import the wsgi 'application' callable and pass it to Handler.run
244    appwsgi = options.get('wsgi.application')
245    if appwsgi and not appini:
246        modname, objname = appwsgi.split('::', 1)
247        module = __import__(modname, globals(), locals(), [''])
248        app = getattr(module, objname)
249
250    Handler(req).run(app)
251
252    # status was set in Handler; always return apache.OK
253    return apache.OK
254