3519cdf4090ef1d7da669353d347d0068fb22fb7
[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 from wsgiref.simple_server import make_server
29 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
30 import httplib
31 import urllib
32 import xmlrpclib
33 import StringIO
34
35 import os
36 import signal
37 import sys
38 import time
39
40 import openerp
41 import openerp.tools.config as config
42 import openerp.service.websrv_lib as websrv_lib
43
44 def xmlrpc_return(start_response, service, method, params):
45     """ Helper to call a service's method with some params, using a
46     wsgi-supplied ``start_response`` callback."""
47     # This mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for exception
48     # handling.
49     try:
50         result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth
51         response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None)
52     except openerp.netsvc.OpenERPDispatcherException, e:
53         fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback)
54         response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
55     except:
56         exc_type, exc_value, exc_tb = sys.exc_info()
57         fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
58         response = xmlrpclib.dumps(fault, allow_none=None, encoding=None)
59     start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))])
60     return [response]
61
62 def wsgi_xmlrpc(environ, start_response):
63     """ The main OpenERP WSGI handler."""
64     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/xmlrpc'):
65         length = int(environ['CONTENT_LENGTH'])
66         data = environ['wsgi.input'].read(length)
67
68         params, method = xmlrpclib.loads(data)
69
70         path = environ['PATH_INFO'][len('/openerp/xmlrpc'):]
71         if path.startswith('/'): path = path[1:]
72         if path.endswith('/'): p = path[:-1]
73         path = path.split('/')
74
75         # All routes are hard-coded. Need a way to register addons-supplied handlers.
76
77         # No need for a db segment.
78         if len(path) == 1:
79             service = path[0]
80
81             if service == 'common':
82                 if method in ('create_database', 'list', 'server_version'):
83                     return xmlrpc_return(start_response, 'db', method, params)
84                 else:
85                     return xmlrpc_return(start_response, 'common', method, params)
86         # A db segment must be given.
87         elif len(path) == 2:
88             service, db_name = path
89             params = (db_name,) + params
90
91             if service == 'model':
92                 return xmlrpc_return(start_response, 'object', method, params)
93             elif service == 'report':
94                 return xmlrpc_return(start_response, 'report', method, params)
95
96         # TODO the body has been read, need to raise an exception (not return None).
97
98 def legacy_wsgi_xmlrpc(environ, start_response):
99     if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'):
100         length = int(environ['CONTENT_LENGTH'])
101         data = environ['wsgi.input'].read(length)
102         path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ...
103
104         params, method = xmlrpclib.loads(data)
105         return xmlrpc_return(start_response, path, method, params)
106
107 def wsgi_jsonrpc(environ, start_response):
108     pass
109
110 def wsgi_modules(environ, start_response):
111     """ WSGI handler dispatching to addons-provided entry points."""
112     pass
113
114 def wsgi_webdav(environ, start_response):
115     if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*':
116         return return_options(start_response)
117
118     # Make sure the addons are loaded in the registry, so they have a chance
119     # to register themselves in the 'service' layer.
120     openerp.pooler.get_db_and_pool('xx', update_module=[], pooljobs=False)
121
122     http_dir = websrv_lib.find_http_service(environ['PATH_INFO'])
123     if http_dir:
124         environ['PATH_INFO'] = '/' + environ['PATH_INFO'][len(http_dir.path):]
125         return http_to_wsgi(http_dir)(environ, start_response)
126
127 def return_options(start_response):
128     # TODO Microsoft specifi header, see websrv_lib do_OPTIONS 
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 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(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 provided by modules loaded with the --load command-line option.
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_modules,
280         wsgi_webdav
281         ] #+ module_handlers
282     for handler in wsgi_handlers:
283         result = handler(environ, start_response)
284         if result is None:
285             continue
286         return result
287
288     # We never returned from the loop. Needs something else than 200 OK.
289     response = 'No handler found.\n'
290     start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
291     return [response]
292
293 def serve():
294     """ Serve XMLRPC requests via wsgiref's simple_server.
295
296     Blocking, should probably be called in its own process.
297     """
298     httpd = make_server('localhost', config['xmlrpc_port'], application)
299     httpd.serve_forever()
300
301 # Master process id, can be used for signaling.
302 arbiter_pid = None
303
304 # Application setup before we can spawn any worker process.
305 # This is suitable for e.g. gunicorn's on_starting hook.
306 def on_starting(server):
307     global arbiter_pid
308     arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable
309     config = openerp.tools.config
310     config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc' # need a config file
311     #config['log_level'] = 10 # debug
312     #openerp.tools.cache = kill_workers_cache
313     openerp.netsvc.init_logger()
314     openerp.osv.osv.start_object_proxy()
315     openerp.service.web_services.start_web_services()
316     test_in_thread()
317
318 def test_in_thread():
319     def f():
320         import time
321         time.sleep(2)
322         print ">>>> test thread"
323         cr = openerp.sql_db.db_connect('xx').cursor()
324         module_name = 'document_webdav'
325         fp = openerp.tools.file_open('/home/openerp/repos/addons/trunk-xmlrpc/document_webdav/test/webdav_test1.yml')
326         openerp.tools.convert_yaml_import(cr, module_name, fp, {}, 'update', True)
327         cr.close()
328         print "<<<< test thread"
329     import threading
330     threading.Thread(target=f).start()
331
332 # Install our own signal handler on the master process.
333 def when_ready(server):
334     # Hijack gunicorn's SIGWINCH handling; we can choose another one.
335     signal.signal(signal.SIGWINCH, make_winch_handler(server))
336
337 # Our signal handler will signal a SGIQUIT to all workers.
338 def make_winch_handler(server):
339     def handle_winch(sig, fram):
340         server.kill_workers(signal.SIGQUIT) # This is gunicorn specific.
341     return handle_winch
342
343 # Kill gracefuly the workers (e.g. because we want to clear their cache).
344 # This is done by signaling a SIGWINCH to the master process, so it can be
345 # called by the workers themselves.
346 def kill_workers():
347     try:
348         os.kill(arbiter_pid, signal.SIGWINCH)
349     except OSError, e:
350         if e.errno == errno.ESRCH: # no such pid
351             return
352         raise            
353
354 class kill_workers_cache(openerp.tools.ormcache):
355     def clear(self, dbname, *args, **kwargs):
356         kill_workers()
357
358 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: