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