[IMP] wsgi and http cleanups, static http is now handled in http.py
bzr revid: al@openerp.com-20140326132057-scuiqvqma9dhyorl
This commit is contained in:
parent
e58f289aeb
commit
483bc96682
|
@ -1,178 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright P. Christeas <p_christ@hol.gr> 2008-2010
|
|
||||||
# Copyright 2010 OpenERP SA. (http://www.openerp.com)
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# WARNING: This program as such is intended to be used by professional
|
|
||||||
# programmers who take the whole responsibility of assessing all potential
|
|
||||||
# consequences resulting from its eventual inadequacies and bugs
|
|
||||||
# End users who are looking for a ready-to-use solution with commercial
|
|
||||||
# guarantees and support are strongly advised to contract a Free Software
|
|
||||||
# Service Company
|
|
||||||
#
|
|
||||||
# This program is Free Software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
""" This module offers the family of HTTP-based servers. These are not a single
|
|
||||||
class/functionality, but a set of network stack layers, implementing
|
|
||||||
extendable HTTP protocols.
|
|
||||||
|
|
||||||
The OpenERP server defines a single instance of a HTTP server, listening at
|
|
||||||
the standard 8069, 8071 ports (well, it is 2 servers, and ports are
|
|
||||||
configurable, of course). This "single" server then uses a `MultiHTTPHandler`
|
|
||||||
to dispatch requests to the appropriate channel protocol, like the XML-RPC,
|
|
||||||
static HTTP, DAV or other.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import posixpath
|
|
||||||
import urllib
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from websrv_lib import *
|
|
||||||
import openerp.tools as tools
|
|
||||||
|
|
||||||
try:
|
|
||||||
import fcntl
|
|
||||||
except ImportError:
|
|
||||||
fcntl = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ssl import SSLError
|
|
||||||
except ImportError:
|
|
||||||
class SSLError(Exception): pass
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# TODO delete this for 6.2, it is still needed for 6.1.
|
|
||||||
class HttpLogHandler:
|
|
||||||
""" helper class for uniform log handling
|
|
||||||
Please define self._logger at each class that is derived from this
|
|
||||||
"""
|
|
||||||
_logger = None
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
self._logger.debug(format % args) # todo: perhaps other level
|
|
||||||
|
|
||||||
def log_error(self, format, *args):
|
|
||||||
self._logger.error(format % args)
|
|
||||||
|
|
||||||
def log_exception(self, format, *args):
|
|
||||||
self._logger.exception(format, *args)
|
|
||||||
|
|
||||||
def log_request(self, code='-', size='-'):
|
|
||||||
self._logger.debug('"%s" %s %s',
|
|
||||||
self.requestline, str(code), str(size))
|
|
||||||
|
|
||||||
class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
|
|
||||||
|
|
||||||
def __init__(self,request, client_address, server):
|
|
||||||
HTTPHandler.__init__(self,request,client_address,server)
|
|
||||||
document_root = tools.config.get('static_http_document_root', False)
|
|
||||||
assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!"
|
|
||||||
self.__basepath = document_root
|
|
||||||
|
|
||||||
def translate_path(self, path):
|
|
||||||
"""Translate a /-separated PATH to the local filename syntax.
|
|
||||||
|
|
||||||
Components that mean special things to the local file system
|
|
||||||
(e.g. drive or directory names) are ignored. (XXX They should
|
|
||||||
probably be diagnosed.)
|
|
||||||
|
|
||||||
"""
|
|
||||||
# abandon query parameters
|
|
||||||
path = path.split('?',1)[0]
|
|
||||||
path = path.split('#',1)[0]
|
|
||||||
path = posixpath.normpath(urllib.unquote(path))
|
|
||||||
words = path.split('/')
|
|
||||||
words = filter(None, words)
|
|
||||||
path = self.__basepath
|
|
||||||
for word in words:
|
|
||||||
if word in (os.curdir, os.pardir): continue
|
|
||||||
path = os.path.join(path, word)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def init_static_http():
|
|
||||||
if not tools.config.get('static_http_enable', False):
|
|
||||||
return
|
|
||||||
|
|
||||||
document_root = tools.config.get('static_http_document_root', False)
|
|
||||||
assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)"
|
|
||||||
|
|
||||||
base_path = tools.config.get('static_http_url_prefix', '/')
|
|
||||||
|
|
||||||
reg_http_service(base_path, StaticHTTPHandler)
|
|
||||||
|
|
||||||
_logger.info("Registered HTTP dir %s for %s", document_root, base_path)
|
|
||||||
|
|
||||||
import security
|
|
||||||
|
|
||||||
class OpenERPAuthProvider(AuthProvider):
|
|
||||||
""" Require basic authentication."""
|
|
||||||
def __init__(self,realm='OpenERP User'):
|
|
||||||
self.realm = realm
|
|
||||||
self.auth_creds = {}
|
|
||||||
self.auth_tries = 0
|
|
||||||
self.last_auth = None
|
|
||||||
|
|
||||||
def authenticate(self, db, user, passwd, client_address):
|
|
||||||
try:
|
|
||||||
uid = security.login(db,user,passwd)
|
|
||||||
if uid is False:
|
|
||||||
return False
|
|
||||||
return user, passwd, db, uid
|
|
||||||
except Exception,e:
|
|
||||||
_logger.debug("Fail auth: %s" % e )
|
|
||||||
return False
|
|
||||||
|
|
||||||
def checkRequest(self,handler,path, db=False):
|
|
||||||
auth_str = handler.headers.get('Authorization',False)
|
|
||||||
try:
|
|
||||||
if not db:
|
|
||||||
db = handler.get_db_from_path(path)
|
|
||||||
except Exception:
|
|
||||||
if path.startswith('/'):
|
|
||||||
path = path[1:]
|
|
||||||
psp= path.split('/')
|
|
||||||
if len(psp)>1:
|
|
||||||
db = psp[0]
|
|
||||||
else:
|
|
||||||
#FIXME!
|
|
||||||
_logger.info("Wrong path: %s, failing auth" %path)
|
|
||||||
raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
|
|
||||||
if self.auth_creds.get(db):
|
|
||||||
return True
|
|
||||||
if auth_str and auth_str.startswith('Basic '):
|
|
||||||
auth_str=auth_str[len('Basic '):]
|
|
||||||
(user,passwd) = base64.decodestring(auth_str).split(':')
|
|
||||||
_logger.info("Found user=\"%s\", passwd=\"***\" for db=\"%s\"", user, db)
|
|
||||||
acd = self.authenticate(db,user,passwd,handler.client_address)
|
|
||||||
if acd != False:
|
|
||||||
self.auth_creds[db] = acd
|
|
||||||
self.last_auth = db
|
|
||||||
return True
|
|
||||||
if self.auth_tries > 5:
|
|
||||||
_logger.info("Failing authorization after 5 requests w/o password")
|
|
||||||
raise AuthRejectedExc("Authorization failed.")
|
|
||||||
self.auth_tries += 1
|
|
||||||
raise AuthRequiredExc(atype='Basic', realm=self.realm)
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
|
@ -164,153 +164,6 @@ def wsgi_xmlrpc(environ, start_response):
|
||||||
params, method = xmlrpclib.loads(data)
|
params, method = xmlrpclib.loads(data)
|
||||||
return xmlrpc_return(start_response, service, method, params, string_faultcode)
|
return xmlrpc_return(start_response, service, method, params, string_faultcode)
|
||||||
|
|
||||||
def wsgi_webdav(environ, start_response):
|
|
||||||
pi = environ['PATH_INFO']
|
|
||||||
if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']:
|
|
||||||
return return_options(environ, start_response)
|
|
||||||
elif pi.startswith('/webdav'):
|
|
||||||
http_dir = websrv_lib.find_http_service(pi)
|
|
||||||
if http_dir:
|
|
||||||
path = pi[len(http_dir.path):]
|
|
||||||
if path.startswith('/'):
|
|
||||||
environ['PATH_INFO'] = path
|
|
||||||
else:
|
|
||||||
environ['PATH_INFO'] = '/' + path
|
|
||||||
return http_to_wsgi(http_dir)(environ, start_response)
|
|
||||||
|
|
||||||
def return_options(environ, start_response):
|
|
||||||
# Microsoft specific header, see
|
|
||||||
# http://www.ibm.com/developerworks/rational/library/2089.html
|
|
||||||
if 'Microsoft' in environ.get('User-Agent', ''):
|
|
||||||
options = [('MS-Author-Via', 'DAV')]
|
|
||||||
else:
|
|
||||||
options = []
|
|
||||||
options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')]
|
|
||||||
start_response("200 OK", [('Content-Length', str(0))] + options)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def http_to_wsgi(http_dir):
|
|
||||||
"""
|
|
||||||
Turn a BaseHTTPRequestHandler into a WSGI entry point.
|
|
||||||
|
|
||||||
Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped
|
|
||||||
(as a class, so it needs to be instanciated) in a HTTPDir.
|
|
||||||
|
|
||||||
This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign().
|
|
||||||
It is a temporary solution: the HTTP sub-handlers (in particular the
|
|
||||||
document_webdav addon) have to be WSGIfied.
|
|
||||||
"""
|
|
||||||
def wsgi_handler(environ, start_response):
|
|
||||||
|
|
||||||
headers = {}
|
|
||||||
for key, value in environ.items():
|
|
||||||
if key.startswith('HTTP_'):
|
|
||||||
key = key[5:].replace('_', '-').title()
|
|
||||||
headers[key] = value
|
|
||||||
if key == 'CONTENT_LENGTH':
|
|
||||||
key = key.replace('_', '-').title()
|
|
||||||
headers[key] = value
|
|
||||||
if environ.get('Content-Type'):
|
|
||||||
headers['Content-Type'] = environ['Content-Type']
|
|
||||||
|
|
||||||
path = urllib.quote(environ.get('PATH_INFO', ''))
|
|
||||||
if environ.get('QUERY_STRING'):
|
|
||||||
path += '?' + environ['QUERY_STRING']
|
|
||||||
|
|
||||||
request_version = 'HTTP/1.1' # TODO
|
|
||||||
request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version)
|
|
||||||
|
|
||||||
class Dummy(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Let's pretend we have a server to hand to the handler.
|
|
||||||
server = Dummy()
|
|
||||||
server.server_name = environ['SERVER_NAME']
|
|
||||||
server.server_port = int(environ['SERVER_PORT'])
|
|
||||||
|
|
||||||
# Initialize the underlying handler and associated auth. provider.
|
|
||||||
con = openerp.service.websrv_lib.noconnection(environ['wsgi.input'])
|
|
||||||
handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server)
|
|
||||||
|
|
||||||
# Populate the handler as if it is called by a regular HTTP server
|
|
||||||
# and the request is already parsed.
|
|
||||||
handler.wfile = StringIO.StringIO()
|
|
||||||
handler.rfile = environ['wsgi.input']
|
|
||||||
handler.headers = headers
|
|
||||||
handler.command = environ['REQUEST_METHOD']
|
|
||||||
handler.path = path
|
|
||||||
handler.request_version = request_version
|
|
||||||
handler.close_connection = 1
|
|
||||||
handler.raw_requestline = request_line
|
|
||||||
handler.requestline = request_line
|
|
||||||
|
|
||||||
# Handle authentication if there is an auth. provider associated to
|
|
||||||
# the handler.
|
|
||||||
if hasattr(handler, 'auth_provider'):
|
|
||||||
try:
|
|
||||||
handler.auth_provider.checkRequest(handler, path)
|
|
||||||
except websrv_lib.AuthRequiredExc, ae:
|
|
||||||
# Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
|
|
||||||
# authorisation features of HTTP/1.1
|
|
||||||
if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')):
|
|
||||||
start_response("403 Forbidden", [])
|
|
||||||
return []
|
|
||||||
start_response("401 Authorization required", [
|
|
||||||
('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)),
|
|
||||||
# ('Connection', 'keep-alive'),
|
|
||||||
('Content-Type', 'text/html'),
|
|
||||||
('Content-Length', 4), # len(self.auth_required_msg)
|
|
||||||
])
|
|
||||||
return ['Blah'] # self.auth_required_msg
|
|
||||||
except websrv_lib.AuthRejectedExc,e:
|
|
||||||
start_response("403 %s" % (e.args[0],), [])
|
|
||||||
return []
|
|
||||||
|
|
||||||
method_name = 'do_' + handler.command
|
|
||||||
|
|
||||||
# Support the OPTIONS method even when not provided directly by the
|
|
||||||
# handler. TODO I would prefer to remove it and fix the handler if
|
|
||||||
# needed.
|
|
||||||
if not hasattr(handler, method_name):
|
|
||||||
if handler.command == 'OPTIONS':
|
|
||||||
return return_options(environ, start_response)
|
|
||||||
start_response("501 Unsupported method (%r)" % handler.command, [])
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Finally, call the handler's method.
|
|
||||||
try:
|
|
||||||
method = getattr(handler, method_name)
|
|
||||||
method()
|
|
||||||
# The DAV handler buffers its output and provides a _flush()
|
|
||||||
# method.
|
|
||||||
getattr(handler, '_flush', lambda: None)()
|
|
||||||
response = parse_http_response(handler.wfile.getvalue())
|
|
||||||
response_headers = response.getheaders()
|
|
||||||
body = response.read()
|
|
||||||
start_response(str(response.status) + ' ' + response.reason, response_headers)
|
|
||||||
return [body]
|
|
||||||
except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc):
|
|
||||||
raise
|
|
||||||
except Exception, e:
|
|
||||||
start_response("500 Internal error", [])
|
|
||||||
return []
|
|
||||||
|
|
||||||
return wsgi_handler
|
|
||||||
|
|
||||||
def parse_http_response(s):
|
|
||||||
""" Turn a HTTP response string into a httplib.HTTPResponse object."""
|
|
||||||
class DummySocket(StringIO.StringIO):
|
|
||||||
"""
|
|
||||||
This is used to provide a StringIO to httplib.HTTPResponse
|
|
||||||
which, instead of taking a file object, expects a socket and
|
|
||||||
uses its makefile() method.
|
|
||||||
"""
|
|
||||||
def makefile(self, *args, **kw):
|
|
||||||
return self
|
|
||||||
response = httplib.HTTPResponse(DummySocket(s))
|
|
||||||
response.begin()
|
|
||||||
return response
|
|
||||||
|
|
||||||
# WSGI handlers registered through the register_wsgi_handler() function below.
|
# WSGI handlers registered through the register_wsgi_handler() function below.
|
||||||
module_handlers = []
|
module_handlers = []
|
||||||
# RPC endpoints registered through the register_rpc_endpoint() function below.
|
# RPC endpoints registered through the register_rpc_endpoint() function below.
|
||||||
|
@ -342,7 +195,7 @@ def application_unproxied(environ, start_response):
|
||||||
del threading.current_thread().dbname
|
del threading.current_thread().dbname
|
||||||
|
|
||||||
# Try all handlers until one returns some result (i.e. not None).
|
# Try all handlers until one returns some result (i.e. not None).
|
||||||
wsgi_handlers = [wsgi_xmlrpc, wsgi_webdav]
|
wsgi_handlers = [wsgi_xmlrpc]
|
||||||
wsgi_handlers += module_handlers
|
wsgi_handlers += module_handlers
|
||||||
for handler in wsgi_handlers:
|
for handler in wsgi_handlers:
|
||||||
result = handler(environ, start_response)
|
result = handler(environ, start_response)
|
||||||
|
|
|
@ -152,19 +152,11 @@ class configmanager(object):
|
||||||
parser.add_option_group(group)
|
parser.add_option_group(group)
|
||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
# TODO move to web addons after MetaOption merge
|
|
||||||
group = optparse.OptionGroup(parser, "Web interface Configuration")
|
group = optparse.OptionGroup(parser, "Web interface Configuration")
|
||||||
group.add_option("--db-filter", dest="dbfilter", default='.*',
|
group.add_option("--db-filter", dest="dbfilter", default='.*',
|
||||||
help="Filter listed database", metavar="REGEXP")
|
help="Filter listed database", metavar="REGEXP")
|
||||||
parser.add_option_group(group)
|
parser.add_option_group(group)
|
||||||
|
|
||||||
# Static HTTP
|
|
||||||
group = optparse.OptionGroup(parser, "Static HTTP service")
|
|
||||||
group.add_option("--static-http-enable", dest="static_http_enable", action="store_true", my_default=False, help="enable static HTTP service for serving plain HTML files")
|
|
||||||
group.add_option("--static-http-document-root", dest="static_http_document_root", help="specify the directory containing your static HTML files (e.g '/var/www/')")
|
|
||||||
group.add_option("--static-http-url-prefix", dest="static_http_url_prefix", help="specify the URL root prefix where you want web browsers to access your static HTML files (e.g '/')")
|
|
||||||
parser.add_option_group(group)
|
|
||||||
|
|
||||||
# Testing Group
|
# Testing Group
|
||||||
group = optparse.OptionGroup(parser, "Testing Configuration")
|
group = optparse.OptionGroup(parser, "Testing Configuration")
|
||||||
group.add_option("--test-file", dest="test_file", my_default=False,
|
group.add_option("--test-file", dest="test_file", my_default=False,
|
||||||
|
@ -394,7 +386,6 @@ class configmanager(object):
|
||||||
'db_maxconn', 'import_partial', 'addons_path',
|
'db_maxconn', 'import_partial', 'addons_path',
|
||||||
'xmlrpc', 'syslog', 'without_demo', 'timezone',
|
'xmlrpc', 'syslog', 'without_demo', 'timezone',
|
||||||
'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs',
|
'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs',
|
||||||
'static_http_enable', 'static_http_document_root', 'static_http_url_prefix',
|
|
||||||
'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_handler', 'log_level', 'log_db'
|
'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_handler', 'log_level', 'log_db'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue