[MERGE] merged long-polling branch:
[odoo/odoo.git] / openerp / service / wsgi_server.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2011-2012 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 """
23
24 WSGI stack, common code.
25
26 """
27
28 import httplib
29 import urllib
30 import xmlrpclib
31 import StringIO
32
33 import errno
34 import logging
35 import os
36 import signal
37 import socket
38 import sys
39 import threading
40 import traceback
41
42 import werkzeug.serving
43 import werkzeug.contrib.fixers
44
45 import openerp
46 import openerp.modules
47 import openerp.tools.config as config
48 import websrv_lib
49
50 _logger = logging.getLogger(__name__)
51
52 # XML-RPC fault codes. Some care must be taken when changing these: the
53 # constants are also defined client-side and must remain in sync.
54 # User code must use the exceptions defined in ``openerp.exceptions`` (not
55 # create directly ``xmlrpclib.Fault`` objects).
56 RPC_FAULT_CODE_CLIENT_ERROR = 1 # indistinguishable from app. error.
57 RPC_FAULT_CODE_APPLICATION_ERROR = 1
58 RPC_FAULT_CODE_WARNING = 2
59 RPC_FAULT_CODE_ACCESS_DENIED = 3
60 RPC_FAULT_CODE_ACCESS_ERROR = 4
61
62 # The new (6.1) versioned RPC paths.
63 XML_RPC_PATH = '/openerp/xmlrpc'
64 XML_RPC_PATH_1 = '/openerp/xmlrpc/1'
65 JSON_RPC_PATH = '/openerp/jsonrpc'
66 JSON_RPC_PATH_1 = '/openerp/jsonrpc/1'
67
68 def xmlrpc_return(start_response, service, method, params, legacy_exceptions=False):
69     """
70     Helper to call a service's method with some params, using a wsgi-supplied
71     ``start_response`` callback.
72
73     This is the place to look at to see the mapping between core exceptions
74     and XML-RPC fault codes.
75     """
76     # Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions
77     # defined in ``openerp.exceptions`` are mapped to specific fault codes;
78     # all the other exceptions are mapped to the generic
79     # RPC_FAULT_CODE_APPLICATION_ERROR value.
80     # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for
81     # exception handling.
82     try:
83         result = openerp.netsvc.dispatch_rpc(service, method, params)
84         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
85     except Exception, e:
86         if legacy_exceptions:
87             response = xmlrpc_handle_exception_legacy(e)
88         else:
89             response = xmlrpc_handle_exception(e)
90     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
91     return [response]
92
93 def xmlrpc_handle_exception(e):
94     if isinstance(e, openerp.osv.orm.except_orm): # legacy
95         fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, openerp.tools.ustr(e.value))
96         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
97     elif isinstance(e, openerp.exceptions.Warning) or isinstance(e, openerp.exceptions.RedirectWarning):
98         fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
99         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
100     elif isinstance (e, openerp.exceptions.AccessError):
101         fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e))
102         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
103     elif isinstance(e, openerp.exceptions.AccessDenied):
104         fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
105         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
106     elif isinstance(e, openerp.exceptions.DeferredException):
107         info = e.traceback
108         # Which one is the best ?
109         formatted_info = "".join(traceback.format_exception(*info))
110         #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
111         fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
112         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
113     else:
114         if hasattr(e, 'message') and e.message == 'AccessDenied': # legacy
115             fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
116             response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
117         else:
118             info = sys.exc_info()
119             # Which one is the best ?
120             formatted_info = "".join(traceback.format_exception(*info))
121             #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
122             fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
123             response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
124     return response
125
126 def xmlrpc_handle_exception_legacy(e):
127     if isinstance(e, openerp.osv.orm.except_orm):
128         fault = xmlrpclib.Fault('warning -- ' + e.name + '\n\n' + e.value, '')
129         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
130     elif isinstance(e, openerp.exceptions.Warning):
131         fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '')
132         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
133     elif isinstance(e, openerp.exceptions.AccessError):
134         fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '')
135         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
136     elif isinstance(e, openerp.exceptions.AccessDenied):
137         fault = xmlrpclib.Fault('AccessDenied', str(e))
138         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
139     elif isinstance(e, openerp.exceptions.DeferredException):
140         info = e.traceback
141         formatted_info = "".join(traceback.format_exception(*info))
142         fault = xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info)
143         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
144     else:
145         info = sys.exc_info()
146         formatted_info = "".join(traceback.format_exception(*info))
147         fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info)
148         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
149     return response
150
151 def wsgi_xmlrpc_1(environ, start_response):
152     """ The main OpenERP WSGI handler."""
153     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith(XML_RPC_PATH_1):
154         length = int(environ['CONTENT_LENGTH'])
155         data = environ['wsgi.input'].read(length)
156
157         params, method = xmlrpclib.loads(data)
158
159         path = environ['PATH_INFO'][len(XML_RPC_PATH_1):]
160         if path.startswith('/'): path = path[1:]
161         if path.endswith('/'): path = path[:-1]
162         path = path.split('/')
163
164         # All routes are hard-coded.
165
166         # No need for a db segment.
167         if len(path) == 1:
168             service = path[0]
169
170             if service == 'common':
171                 if method in ('server_version',):
172                     service = 'db'
173             return xmlrpc_return(start_response, service, method, params)
174
175         # A db segment must be given.
176         elif len(path) == 2:
177             service, db_name = path
178             params = (db_name,) + params
179
180             return xmlrpc_return(start_response, service, method, params)
181
182         # A db segment and a model segment must be given.
183         elif len(path) == 3 and path[0] == 'model':
184             service, db_name, model_name = path
185             params = (db_name,) + params[:2] + (model_name,) + params[2:]
186             service = 'object'
187             return xmlrpc_return(start_response, service, method, params)
188
189         # The body has been read, need to raise an exception (not return None).
190         fault = xmlrpclib.Fault(RPC_FAULT_CODE_CLIENT_ERROR, '')
191         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
192         start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
193         return [response]
194
195 def wsgi_xmlrpc(environ, start_response):
196     """ WSGI handler to return the versions."""
197     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith(XML_RPC_PATH):
198         length = int(environ['CONTENT_LENGTH'])
199         data = environ['wsgi.input'].read(length)
200
201         params, method = xmlrpclib.loads(data)
202
203         path = environ['PATH_INFO'][len(XML_RPC_PATH):]
204         if path.startswith('/'): path = path[1:]
205         if path.endswith('/'): path = path[:-1]
206         path = path.split('/')
207
208         # All routes are hard-coded.
209
210         if len(path) == 1 and path[0] == '' and method in ('version',):
211             return xmlrpc_return(start_response, 'common', method, ())
212
213         # The body has been read, need to raise an exception (not return None).
214         fault = xmlrpclib.Fault(RPC_FAULT_CODE_CLIENT_ERROR, '')
215         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
216         start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
217         return [response]
218
219 def wsgi_xmlrpc_legacy(environ, start_response):
220     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
221         length = int(environ['CONTENT_LENGTH'])
222         data = environ['wsgi.input'].read(length)
223         path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
224
225         params, method = xmlrpclib.loads(data)
226         return xmlrpc_return(start_response, path, method, params, True)
227
228 def wsgi_webdav(environ, start_response):
229     pi = environ['PATH_INFO']
230     if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']:
231         return return_options(environ, start_response)
232     elif pi.startswith('/webdav'):
233         http_dir = websrv_lib.find_http_service(pi)
234         if http_dir:
235             path = pi[len(http_dir.path):]
236             if path.startswith('/'):
237                 environ['PATH_INFO'] = path
238             else:
239                 environ['PATH_INFO'] = '/' + path
240             return http_to_wsgi(http_dir)(environ, start_response)
241
242 def return_options(environ, start_response):
243     # Microsoft specific header, see
244     # http://www.ibm.com/developerworks/rational/library/2089.html
245     if 'Microsoft' in environ.get('User-Agent', ''):
246         options = [('MS-Author-Via', 'DAV')]
247     else:
248         options = []
249     options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
250     start_response("200 OK", [('Content-Length', str(0))] + options)
251     return []
252
253 def http_to_wsgi(http_dir):
254     """
255     Turn a BaseHTTPRequestHandler into a WSGI entry point.
256
257     Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
258     (as a class, so it needs to be instanciated) in a HTTPDir.
259
260     This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
261     It is a temporary solution: the HTTP sub-handlers (in particular the
262     document_webdav addon) have to be WSGIfied.
263     """
264     def wsgi_handler(environ, start_response):
265
266         headers = {}
267         for key, value in environ.items():
268             if key.startswith('HTTP_'):
269                 key = key[5:].replace('_', '-').title()
270                 headers[key] = value
271             if key == 'CONTENT_LENGTH':
272                 key = key.replace('_', '-').title()
273                 headers[key] = value
274         if environ.get('Content-Type'):
275             headers['Content-Type'] = environ['Content-Type']
276
277         path = urllib.quote(environ.get('PATH_INFO', ''))
278         if environ.get('QUERY_STRING'):
279             path += '?' + environ['QUERY_STRING']
280
281         request_version = 'HTTP/1.1' # TODO
282         request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
283
284         class Dummy(object):
285             pass
286
287         # Let's pretend we have a server to hand to the handler.
288         server = Dummy()
289         server.server_name = environ['SERVER_NAME']
290         server.server_port = int(environ['SERVER_PORT'])
291
292         # Initialize the underlying handler and associated auth. provider.
293         con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
294         handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
295
296         # Populate the handler as if it is called by a regular HTTP server
297         # and the request is already parsed.
298         handler.wfile = StringIO.StringIO()
299         handler.rfile = environ['wsgi.input']
300         handler.headers = headers
301         handler.command = environ['REQUEST_METHOD']
302         handler.path = path
303         handler.request_version = request_version
304         handler.close_connection = 1
305         handler.raw_requestline = request_line
306         handler.requestline = request_line
307
308         # Handle authentication if there is an auth. provider associated to
309         # the handler.
310         if hasattr(handler, 'auth_provider'):
311             try:
312                 handler.auth_provider.checkRequest(handler, path)
313             except websrv_lib.AuthRequiredExc, ae:
314                 # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
315                 # authorisation features of HTTP/1.1
316                 if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
317                     start_response("403 Forbidden", [])
318                     return []
319                 start_response("401 Authorization required", [
320                     ('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
321                     # ('Connection', 'keep-alive'),
322                     ('Content-Type', 'text/html'),
323                     ('Content-Length', 4), # len(self.auth_required_msg)
324                     ])
325                 return ['Blah'] # self.auth_required_msg
326             except websrv_lib.AuthRejectedExc,e:
327                 start_response("403 %s" % (e.args[0],), [])
328                 return []
329
330         method_name = 'do_' + handler.command
331
332         # Support the OPTIONS method even when not provided directly by the
333         # handler. TODO I would prefer to remove it and fix the handler if
334         # needed.
335         if not hasattr(handler, method_name):
336             if handler.command == 'OPTIONS':
337                 return return_options(environ, start_response)
338             start_response("501 Unsupported method (%r)" % handler.command, [])
339             return []
340
341         # Finally, call the handler's method.
342         try:
343             method = getattr(handler, method_name)
344             method()
345             # The DAV handler buffers its output and provides a _flush()
346             # method.
347             getattr(handler, '_flush', lambda: None)()
348             response = parse_http_response(handler.wfile.getvalue())
349             response_headers = response.getheaders()
350             body = response.read()
351             start_response(str(response.status) + ' ' + response.reason, response_headers)
352             return [body]
353         except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
354             raise
355         except Exception, e:
356             start_response("500 Internal error", [])
357             return []
358
359     return wsgi_handler
360
361 def parse_http_response(s):
362     """ Turn a HTTP response string into a httplib.HTTPResponse object."""
363     class DummySocket(StringIO.StringIO):
364         """
365         This is used to provide a StringIO to httplib.HTTPResponse
366         which, instead of taking a file object, expects a socket and
367         uses its makefile() method.
368         """
369         def makefile(self, *args, **kw):
370             return self
371     response = httplib.HTTPResponse(DummySocket(s))
372     response.begin()
373     return response
374
375 # WSGI handlers registered through the register_wsgi_handler() function below.
376 module_handlers = []
377 # RPC endpoints registered through the register_rpc_endpoint() function below.
378 rpc_handlers = {}
379
380 def register_wsgi_handler(handler):
381     """ Register a WSGI handler.
382
383     Handlers are tried in the order they are added. We might provide a way to
384     register a handler for specific routes later.
385     """
386     module_handlers.append(handler)
387
388 def register_rpc_endpoint(endpoint, handler):
389     """ Register a handler for a given RPC enpoint.
390     """
391     rpc_handlers[endpoint] = handler
392
393 def application_unproxied(environ, start_response):
394     """ WSGI entry point."""
395     openerp.service.start_internal()
396
397     # Try all handlers until one returns some result (i.e. not None).
398     wsgi_handlers = [wsgi_xmlrpc_1, wsgi_xmlrpc, wsgi_xmlrpc_legacy, wsgi_webdav]
399     wsgi_handlers += module_handlers
400     for handler in wsgi_handlers:
401         result = handler(environ, start_response)
402         if result is None:
403             continue
404         return result
405
406
407     # We never returned from the loop.
408     response = 'No handler found.\n'
409     start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
410     return [response]
411
412 def application(environ, start_response):
413     if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
414         return werkzeug.contrib.fixers.ProxyFix(application_unproxied)(environ, start_response)
415     else:
416         return application_unproxied(environ, start_response)
417
418 # The WSGI server, started by start_server(), stopped by stop_server().
419 httpd = None
420
421 def serve(interface, port, threaded):
422     """ Serve HTTP requests via werkzeug development server.
423
424     Calling this function is blocking, you might want to call it in its own
425     thread.
426     """
427
428     global httpd
429     if not openerp.evented:
430         httpd = werkzeug.serving.make_server(interface, port, application, threaded=threaded)
431     else:
432         from gevent.wsgi import WSGIServer
433         httpd = WSGIServer((interface, port), application)
434     httpd.serve_forever()
435
436 def start_service():
437     """ Call serve() in its own thread.
438
439     The WSGI server can be shutdown with stop_server() below.
440     """
441     # TODO Change the xmlrpc_* options to http_*
442     interface = config['xmlrpc_interface'] or '0.0.0.0'
443     port = config['xmlrpc_port']
444     _logger.info('HTTP service (werkzeug) running on %s:%s', interface, port)
445     threading.Thread(target=serve, args=(interface, port, True)).start()
446
447 def stop_service():
448     """ Initiate the shutdown of the WSGI server.
449
450     The server is supposed to have been started by start_server() above.
451     """
452     if httpd:
453         if not openerp.evented:
454             httpd.shutdown()
455             close_socket(httpd.socket)
456         else:
457             import gevent
458             httpd.stop()
459             gevent.shutdown()
460
461 def close_socket(sock):
462     """ Closes a socket instance cleanly
463
464     :param sock: the network socket to close
465     :type sock: socket.socket
466     """
467     try:
468         sock.shutdown(socket.SHUT_RDWR)
469     except socket.error, e:
470         # On OSX, socket shutdowns both sides if any side closes it
471         # causing an error 57 'Socket is not connected' on shutdown
472         # of the other side (or something), see
473         # http://bugs.python.org/issue4397
474         # note: stdlib fixed test, not behavior
475         if e.errno != errno.ENOTCONN or platform.system() not in ['Darwin', 'Windows']:
476             raise
477     sock.close()
478
479
480 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: