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