96651edf749f92a470d77725bd0f29ee6274dd25
[odoo/odoo.git] / openerp / wsgi.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ WSGI stuffs (proof of concept for now)
23
24 This module offers a WSGI interface to OpenERP.
25
26 """
27
28 import httplib
29 import urllib
30 import xmlrpclib
31 import StringIO
32
33 import logging
34 import os
35 import signal
36 import sys
37 import threading
38 import time
39
40 import openerp
41 import openerp.tools.config as config
42 import service.websrv_lib as websrv_lib
43
44 def xmlrpc_return(start_response, service, method, params):
45     """ Helper to call a service's method with some params, using a
46     wsgi-supplied ``start_response`` callback."""
47     # This mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for exception
48     # handling.
49     try:
50         result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth
51         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
52     except openerp.netsvc.OpenERPDispatcherException, e:
53         fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback)
54         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
55     except:
56         exc_type, exc_value, exc_tb = sys.exc_info()
57         fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
58         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
59     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
60     return [response]
61
62 def wsgi_xmlrpc(environ, start_response):
63     """ The main OpenERP WSGI handler."""
64     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/xmlrpc'):
65         length = int(environ['CONTENT_LENGTH'])
66         data = environ['wsgi.input'].read(length)
67
68         params, method = xmlrpclib.loads(data)
69
70         path = environ['PATH_INFO'][len('/openerp/xmlrpc'):]
71         if path.startswith('/'): path = path[1:]
72         if path.endswith('/'): p = path[:-1]
73         path = path.split('/')
74
75         # All routes are hard-coded. Need a way to register addons-supplied handlers.
76
77         # No need for a db segment.
78         if len(path) == 1:
79             service = path[0]
80
81             if service == 'common':
82                 if method in ('create_database', 'list', 'server_version'):
83                     return xmlrpc_return(start_response, 'db', method, params)
84                 else:
85                     return xmlrpc_return(start_response, 'common', method, params)
86         # A db segment must be given.
87         elif len(path) == 2:
88             service, db_name = path
89             params = (db_name,) + params
90
91             if service == 'model':
92                 return xmlrpc_return(start_response, 'object', method, params)
93             elif service == 'report':
94                 return xmlrpc_return(start_response, 'report', method, params)
95
96         # TODO the body has been read, need to raise an exception (not return None).
97
98 def legacy_wsgi_xmlrpc(environ, start_response):
99     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
100         length = int(environ['CONTENT_LENGTH'])
101         data = environ['wsgi.input'].read(length)
102         path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
103
104         params, method = xmlrpclib.loads(data)
105         return xmlrpc_return(start_response, path, method, params)
106
107 def wsgi_jsonrpc(environ, start_response):
108     pass
109
110 def wsgi_webdav(environ, start_response):
111     if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*':
112         return return_options(environ, start_response)
113
114     http_dir = websrv_lib.find_http_service(environ['PATH_INFO'])
115     if http_dir:
116         path = environ['PATH_INFO'][len(http_dir.path):]
117         if path.startswith('/'):
118             environ['PATH_INFO'] = path
119         else:
120             environ['PATH_INFO'] = '/' + path
121         return http_to_wsgi(http_dir)(environ, start_response)
122
123 def return_options(environ, start_response):
124     # Microsoft specific header, see
125     # http://www.ibm.com/developerworks/rational/library/2089.html
126     if 'Microsoft' in environ.get('User-Agent', ''):
127         option = [('MS-Author-Via', 'DAV')]
128     else:
129         option = []
130     options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
131     start_response("200 OK", [('Content-Length', str(0))] + options)
132     return []
133
134 def http_to_wsgi(http_dir):
135     """
136     Turn a BaseHTTPRequestHandler into a WSGI entry point.
137
138     Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
139     (as a class, so it needs to be instanciated) in a HTTPDir.
140
141     This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
142     It is a temporary solution: the HTTP sub-handlers (in particular the
143     document_webdav addon) have to be WSGIfied.
144     """
145     def wsgi_handler(environ, start_response):
146
147         # Extract from the WSGI environment the necessary data.
148         scheme = environ['wsgi.url_scheme']
149
150         headers = {}
151         for key, value in environ.items():
152             if key.startswith('HTTP_'):
153                 key = key[5:].replace('_', '-').title()
154                 headers[key] = value
155             if key == 'CONTENT_LENGTH':
156                 key = key.replace('_', '-').title()
157                 headers[key] = value
158         if environ.get('Content-Type'):
159             headers['Content-Type'] = environ['Content-Type']
160
161         path = urllib.quote(environ.get('PATH_INFO', ''))
162         if environ.get('QUERY_STRING'):
163             path += '?' + environ['QUERY_STRING']
164
165         request_version = 'HTTP/1.1' # TODO
166         request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
167
168         class Dummy(object):
169             pass
170
171         # Let's pretend we have a server to hand to the handler.
172         server = Dummy()
173         server.server_name = environ['SERVER_NAME']
174         server.server_port = int(environ['SERVER_PORT'])
175
176         # Initialize the underlying handler and associated auth. provider.
177         con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
178         handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
179
180         # Populate the handler as if it is called by a regular HTTP server
181         # and the request is already parsed.
182         handler.wfile = StringIO.StringIO()
183         handler.rfile = environ['wsgi.input']
184         handler.headers = headers
185         handler.command = environ['REQUEST_METHOD']
186         handler.path = path
187         handler.request_version = request_version
188         handler.close_connection = 1
189         handler.raw_requestline = request_line
190         handler.requestline = request_line
191
192         # Handle authentication if there is an auth. provider associated to
193         # the handler.
194         if hasattr(handler, 'auth_provider'):
195             try:
196                 handler.auth_provider.checkRequest(handler, path)
197             except websrv_lib.AuthRequiredExc, ae:
198                 # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
199                 # authorisation features of HTTP/1.1 
200                 if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
201                     start_response("403 Forbidden", [])
202                     return []
203                 start_response("401 Authorization required", [
204                     ('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
205                     # ('Connection', 'keep-alive'),
206                     ('Content-Type', 'text/html'),
207                     ('Content-Length', 4), # len(self.auth_required_msg)
208                     ])
209                 return ['Blah'] # self.auth_required_msg
210             except websrv_lib.AuthRejectedExc,e:
211                 start_response("403 %s" % (e.args[0],), [])
212                 return []
213
214         method_name = 'do_' + handler.command
215
216         # Support the OPTIONS method even when not provided directly by the
217         # handler. TODO I would prefer to remove it and fix the handler if
218         # needed.
219         if not hasattr(handler, method_name):
220             if handler.command == 'OPTIONS':
221                 return return_options(environ, start_response)
222             start_response("501 Unsupported method (%r)" % handler.command, [])
223             return []
224
225         # Finally, call the handler's method.
226         try:
227             method = getattr(handler, method_name)
228             method()
229             # The DAV handler buffers its output and provides a _flush()
230             # method.
231             getattr(handler, '_flush', lambda: None)()
232             response = parse_http_response(handler.wfile.getvalue())
233             response_headers = response.getheaders()
234             body = response.read()
235             start_response(str(response.status) + ' ' + response.reason, response_headers)
236             return [body]
237         except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
238             raise
239         except Exception, e:
240             start_response("500 Internal error", [])
241             return []
242
243     return wsgi_handler
244
245 def parse_http_response(s):
246     """ Turn a HTTP response string into a httplib.HTTPResponse object."""
247     class DummySocket(StringIO.StringIO):
248         """
249         This is used to provide a StringIO to httplib.HTTPResponse
250         which, instead of taking a file object, expects a socket and
251         uses its makefile() method.
252         """
253         def makefile(self, *args, **kw):
254             return self
255     response = httplib.HTTPResponse(DummySocket(s))
256     response.begin()
257     return response
258
259 # WSGI handlers registered through the register_wsgi_handler() function below.
260 module_handlers = []
261
262 def register_wsgi_handler(handler):
263     """ Register a WSGI handler.
264
265     Handlers are tried in the order they are added. We might provide a way to
266     register a handler for specific routes later.
267     """
268     module_handlers.append(handler)
269
270 def application(environ, start_response):
271     """ WSGI entry point."""
272
273     # Try all handlers until one returns some result (i.e. not None).
274     wsgi_handlers = [
275         wsgi_xmlrpc,
276         wsgi_jsonrpc,
277         legacy_wsgi_xmlrpc,
278         wsgi_webdav
279         ] + module_handlers
280     for handler in wsgi_handlers:
281         result = handler(environ, start_response)
282         if result is None:
283             continue
284         return result
285
286     # We never returned from the loop.
287     response = 'No handler found.\n'
288     start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
289     return [response]
290
291 # The WSGI server, started by start_server(), stopped by stop_server().
292 httpd = None
293
294 def serve():
295     """ Serve HTTP requests via werkzeug development server.
296
297     If werkzeug can not be imported, we fall back to wsgiref's simple_server.
298
299     Calling this function is blocking, you might want to call it in its own
300     thread.
301     """
302
303     global httpd
304
305     # TODO Change the xmlrpc_port option to http_port.
306     try:
307         import werkzeug.serving
308         httpd = werkzeug.serving.make_server('0.0.0.0', config['xmlrpc_port'], application, threaded=True)
309     except ImportError, e:
310         import wsgiref.simple_server
311         logging.getLogger('wsgi').warn('Can not import werkzeug, ' 'falling back to wsgiref.')
312         httpd = wsgiref.simple_server.make_server('0.0.0.0', config['xmlrpc_port'], application)
313
314     httpd.serve_forever()
315
316 def start_server():
317     """ Call serve() in its own thread.
318
319     The WSGI server can be shutdown with stop_server() below.
320     """
321     threading.Thread(target=openerp.wsgi.serve).start()
322
323 def stop_server():
324     """ Initiate the shutdown of the WSGI server.
325
326     The server is supposed to have been started by start_server() above.
327     """
328     if httpd:
329         httpd.shutdown()
330
331 # Master process id, can be used for signaling.
332 arbiter_pid = None
333
334 # Application setup before we can spawn any worker process.
335 # This is suitable for e.g. gunicorn's on_starting hook.
336 def on_starting(server):
337     global arbiter_pid
338     arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable
339     config = openerp.tools.config
340     #openerp.tools.cache = kill_workers_cache
341     openerp.netsvc.init_logger()
342     openerp.osv.osv.start_object_proxy()
343     openerp.service.web_services.start_web_services()
344
345 # Install our own signal handler on the master process.
346 def when_ready(server):
347     # Hijack gunicorn's SIGWINCH handling; we can choose another one.
348     signal.signal(signal.SIGWINCH, make_winch_handler(server))
349
350 # Our signal handler will signal a SGIQUIT to all workers.
351 def make_winch_handler(server):
352     def handle_winch(sig, fram):
353         server.kill_workers(signal.SIGQUIT) # This is gunicorn specific.
354     return handle_winch
355
356 # Kill gracefuly the workers (e.g. because we want to clear their cache).
357 # This is done by signaling a SIGWINCH to the master process, so it can be
358 # called by the workers themselves.
359 def kill_workers():
360     try:
361         os.kill(arbiter_pid, signal.SIGWINCH)
362     except OSError, e:
363         if e.errno == errno.ESRCH: # no such pid
364             return
365         raise            
366
367 class kill_workers_cache(openerp.tools.ormcache):
368     def clear(self, dbname, *args, **kwargs):
369         kill_workers()
370
371 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: