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