Merge branch 'master' of https://github.com/odoo/odoo
[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 def xmlrpc_return(start_response, service, method, params, string_faultcode=False):
61     """
62     Helper to call a service's method with some params, using a wsgi-supplied
63     ``start_response`` callback.
64
65     This is the place to look at to see the mapping between core exceptions
66     and XML-RPC fault codes.
67     """
68     # Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions
69     # defined in ``openerp.exceptions`` are mapped to specific fault codes;
70     # all the other exceptions are mapped to the generic
71     # RPC_FAULT_CODE_APPLICATION_ERROR value.
72     # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for
73     # exception handling.
74     try:
75         result = openerp.http.dispatch_rpc(service, method, params)
76         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
77     except Exception, e:
78         if string_faultcode:
79             response = xmlrpc_handle_exception_string(e)
80         else:
81             response = xmlrpc_handle_exception_int(e)
82     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
83     return [response]
84
85 def xmlrpc_handle_exception_int(e):
86     if isinstance(e, openerp.osv.orm.except_orm): # legacy
87         fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, openerp.tools.ustr(e.value))
88         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
89     elif isinstance(e, openerp.exceptions.Warning) or isinstance(e, openerp.exceptions.RedirectWarning):
90         fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))
91         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
92     elif isinstance (e, openerp.exceptions.AccessError):
93         fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e))
94         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
95     elif isinstance(e, openerp.exceptions.AccessDenied):
96         fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
97         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
98     elif isinstance(e, openerp.exceptions.DeferredException):
99         info = e.traceback
100         # Which one is the best ?
101         formatted_info = "".join(traceback.format_exception(*info))
102         #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
103         fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
104         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
105     else:
106         if hasattr(e, 'message') and e.message == 'AccessDenied': # legacy
107             fault = xmlrpclib.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
108             response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
109         else:
110             info = sys.exc_info()
111             # Which one is the best ?
112             formatted_info = "".join(traceback.format_exception(*info))
113             #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info
114             fault = xmlrpclib.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
115             response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
116     return response
117
118 def xmlrpc_handle_exception_string(e):
119     if isinstance(e, openerp.osv.orm.except_orm):
120         fault = xmlrpclib.Fault('warning -- ' + e.name + '\n\n' + e.value, '')
121         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
122     elif isinstance(e, openerp.exceptions.Warning):
123         fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '')
124         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
125     elif isinstance(e, openerp.exceptions.AccessError):
126         fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '')
127         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
128     elif isinstance(e, openerp.exceptions.AccessDenied):
129         fault = xmlrpclib.Fault('AccessDenied', str(e))
130         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
131     elif isinstance(e, openerp.exceptions.DeferredException):
132         info = e.traceback
133         formatted_info = "".join(traceback.format_exception(*info))
134         fault = xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info)
135         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
136     else:
137         info = sys.exc_info()
138         formatted_info = "".join(traceback.format_exception(*info))
139         fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info)
140         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
141     return response
142
143 def wsgi_xmlrpc(environ, start_response):
144     """ Two routes are available for XML-RPC
145
146     /xmlrpc/<service> route returns faultCode as strings. This is a historic
147     violation of the protocol kept for compatibility.
148
149     /xmlrpc/2/<service> is a new route that returns faultCode as int and is
150     therefore fully compliant.
151     """
152     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
153         length = int(environ['CONTENT_LENGTH'])
154         data = environ['wsgi.input'].read(length)
155
156         # Distinguish betweed the 2 faultCode modes
157         string_faultcode = True
158         if environ['PATH_INFO'].startswith('/xmlrpc/2/'):
159             service = environ['PATH_INFO'][len('/xmlrpc/2/'):]
160             string_faultcode = False
161         else:
162             service = environ['PATH_INFO'][len('/xmlrpc/'):]
163
164         params, method = xmlrpclib.loads(data)
165         return xmlrpc_return(start_response, service, method, params, string_faultcode)
166
167 # WSGI handlers registered through the register_wsgi_handler() function below.
168 module_handlers = []
169 # RPC endpoints registered through the register_rpc_endpoint() function below.
170 rpc_handlers = {}
171
172 def register_wsgi_handler(handler):
173     """ Register a WSGI handler.
174
175     Handlers are tried in the order they are added. We might provide a way to
176     register a handler for specific routes later.
177     """
178     module_handlers.append(handler)
179
180 def register_rpc_endpoint(endpoint, handler):
181     """ Register a handler for a given RPC enpoint.
182     """
183     rpc_handlers[endpoint] = handler
184
185 def application_unproxied(environ, start_response):
186     """ WSGI entry point."""
187     # cleanup db/uid trackers - they're set at HTTP dispatch in
188     # web.session.OpenERPSession.send() and at RPC dispatch in
189     # openerp.service.web_services.objects_proxy.dispatch().
190     # /!\ The cleanup cannot be done at the end of this `application`
191     # method because werkzeug still produces relevant logging afterwards 
192     if hasattr(threading.current_thread(), 'uid'):
193         del threading.current_thread().uid
194     if hasattr(threading.current_thread(), 'dbname'):
195         del threading.current_thread().dbname
196
197     with openerp.api.Environment.manage():
198         # Try all handlers until one returns some result (i.e. not None).
199         wsgi_handlers = [wsgi_xmlrpc]
200         wsgi_handlers += module_handlers
201         for handler in wsgi_handlers:
202             result = handler(environ, start_response)
203             if result is None:
204                 continue
205             return result
206
207     # We never returned from the loop.
208     response = 'No handler found.\n'
209     start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
210     return [response]
211
212 def application(environ, start_response):
213     if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
214         return werkzeug.contrib.fixers.ProxyFix(application_unproxied)(environ, start_response)
215     else:
216         return application_unproxied(environ, start_response)
217
218
219 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: