[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:
Antony Lesuisse 2014-03-26 14:20:57 +01:00
parent e58f289aeb
commit 483bc96682
3 changed files with 1 additions and 335 deletions

View File

@ -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:

View File

@ -164,153 +164,6 @@ def wsgi_xmlrpc(environ, start_response):
params, method = xmlrpclib.loads(data)
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.
module_handlers = []
# 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
# 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
for handler in wsgi_handlers:
result = handler(environ, start_response)

View File

@ -152,19 +152,11 @@ class configmanager(object):
parser.add_option_group(group)
# WEB
# TODO move to web addons after MetaOption merge
group = optparse.OptionGroup(parser, "Web interface Configuration")
group.add_option("--db-filter", dest="dbfilter", default='.*',
help="Filter listed database", metavar="REGEXP")
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
group = optparse.OptionGroup(parser, "Testing Configuration")
group.add_option("--test-file", dest="test_file", my_default=False,
@ -394,7 +386,6 @@ class configmanager(object):
'db_maxconn', 'import_partial', 'addons_path',
'xmlrpc', 'syslog', 'without_demo', 'timezone',
'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'
]