odoo/bin/service/websrv_lib.py

424 lines
16 KiB
Python

# -*- encoding: utf-8 -*-
#
# Copyright P. Christeas <p_christ@hol.gr> 2008,2009
#
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability 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
# garantees and support are strongly adviced 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
###############################################################################
""" Framework for generic http servers
"""
import socket
import base64
import SocketServer
from BaseHTTPServer import *
from SimpleHTTPServer import SimpleHTTPRequestHandler
class AuthRequiredExc(Exception):
def __init__(self,atype,realm):
Exception.__init__(self)
self.atype = atype
self.realm = realm
class AuthRejectedExc(Exception):
pass
class AuthProvider:
def __init__(self,realm):
self.realm = realm
def setupAuth(self, multi,handler):
""" Attach an AuthProxy object to handler
"""
pass
def authenticate(self, user, passwd, client_address):
return False
def log(self, msg):
print msg
class BasicAuthProvider(AuthProvider):
def setupAuth(self, multi, handler):
if not multi.sec_realms.has_key(self.realm):
multi.sec_realms[self.realm] = BasicAuthProxy(self)
class AuthProxy:
""" This class will hold authentication information for a handler,
i.e. a connection
"""
def __init__(self, provider):
self.provider = provider
def checkRequest(self,handler,path = '/'):
""" Check if we are allowed to process that request
"""
pass
class BasicAuthProxy(AuthProxy):
""" Require basic authentication..
"""
def __init__(self,provider):
AuthProxy.__init__(self,provider)
self.auth_creds = None
self.auth_tries = 0
def checkRequest(self,handler,path = '/'):
if self.auth_creds:
return True
auth_str = handler.headers.get('Authorization',False)
if auth_str and auth_str.startswith('Basic '):
auth_str=auth_str[len('Basic '):]
(user,passwd) = base64.decodestring(auth_str).split(':')
self.provider.log("Found user=\"%s\", passwd=\"%s\"" %(user,passwd))
self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address)
if self.auth_creds:
return True
if self.auth_tries > 5:
self.provider.log("Failing authorization after 5 requests w/o password")
raise AuthRejectedExc("Authorization failed.")
self.auth_tries += 1
raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
class HTTPHandler(SimpleHTTPRequestHandler):
def __init__(self,request, client_address, server):
SimpleHTTPRequestHandler.__init__(self,request,client_address,server)
# print "Handler for %s inited" % str(client_address)
self.protocol_version = 'HTTP/1.1'
self.connection = dummyconn()
def handle(self):
""" Classes here should NOT handle inside their constructor
"""
pass
def finish(self):
pass
def setup(self):
pass
class HTTPDir:
""" A dispatcher class, like a virtual folder in httpd
"""
def __init__(self,path,handler, auth_provider = None):
self.path = path
self.handler = handler
self.auth_provider = auth_provider
def matches(self, request):
""" Test if some request matches us. If so, return
the matched path. """
if request.startswith(self.path):
return self.path
return False
class noconnection:
""" a class to use instead of the real connection
"""
def makefile(self, mode, bufsize):
return None
class dummyconn:
def shutdown(self, tru):
pass
def _quote_html(html):
return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
class FixSendError:
#error_message_format = """ """
def send_error(self, code, message=None):
#overriden from BaseHTTPRequestHandler, we also send the content-length
try:
short, long = self.responses[code]
except KeyError:
short, long = '???', '???'
if message is None:
message = short
explain = long
self.log_error("code %d, message %s", code, message)
# using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
content = (self.error_message_format %
{'code': code, 'message': _quote_html(message), 'explain': explain})
self.send_response(code, message)
self.send_header("Content-Type", self.error_content_type)
self.send_header('Connection', 'close')
self.send_header('Content-Length', len(content) or 0)
self.end_headers()
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
self.wfile.write(content)
class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
""" this is a multiple handler, that will dispatch each request
to a nested handler, iff it matches
The handler will also have *one* dict of authentication proxies,
groupped by their realm.
"""
protocol_version = "HTTP/1.1"
default_request_version = "HTTP/0.9" # compatibility with py2.5
auth_required_msg = """ <html><head><title>Authorization required</title></head>
<body>You must authenticate to use this service</body><html>\r\r"""
def __init__(self, request, client_address, server):
self.in_handlers = {}
self.sec_realms = {}
SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
def _handle_one_foreign(self,fore, path, auth_provider):
""" This method overrides the handle_one_request for *children*
handlers. It is required, since the first line should not be
read again..
"""
fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
if not fore.parse_request(): # An error code has been sent, just exit
return
self.request_version = fore.request_version
if auth_provider and auth_provider.realm:
try:
self.sec_realms[auth_provider.realm].checkRequest(fore,path)
except AuthRequiredExc,ae:
if self.request_version != 'HTTP/1.1':
self.log_error("Cannot require auth at %s",self.request_version)
self.send_error(401)
return
self._get_ignore_body(fore) # consume any body that came, not loose sync with input
self.send_response(401,'Authorization required')
self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
self.send_header('Connection', 'keep-alive')
self.send_header('Content-Type','text/html')
self.send_header('Content-Length',len(self.auth_required_msg))
self.end_headers()
self.wfile.write(self.auth_required_msg)
return
except AuthRejectedExc,e:
self.log_error("Rejected auth: %s" % e.args[0])
self.send_error(401,e.args[0])
self.close_connection = 1
return
mname = 'do_' + fore.command
if not hasattr(fore, mname):
fore.send_error(501, "Unsupported method (%r)" % fore.command)
return
fore.close_connection = 0
method = getattr(fore, mname)
method()
if fore.close_connection:
# print "Closing connection because of handler"
self.close_connection = fore.close_connection
def parse_rawline(self):
"""Parse a request (internal).
The request should be stored in self.raw_requestline; the results
are in self.command, self.path, self.request_version and
self.headers.
Return True for success, False for failure; on failure, an
error is sent back.
"""
self.command = None # set in case of error on the first line
self.request_version = version = self.default_request_version
self.close_connection = 1
requestline = self.raw_requestline
if requestline[-2:] == '\r\n':
requestline = requestline[:-2]
elif requestline[-1:] == '\n':
requestline = requestline[:-1]
self.requestline = requestline
words = requestline.split()
if len(words) == 3:
[command, path, version] = words
if version[:5] != 'HTTP/':
self.send_error(400, "Bad request version (%r)" % version)
return False
try:
base_version_number = version.split('/', 1)[1]
version_number = base_version_number.split(".")
# RFC 2145 section 3.1 says there can be only one "." and
# - major and minor numbers MUST be treated as
# separate integers;
# - HTTP/2.4 is a lower version than HTTP/2.13, which in
# turn is lower than HTTP/12.3;
# - Leading zeros MUST be ignored by recipients.
if len(version_number) != 2:
raise ValueError
version_number = int(version_number[0]), int(version_number[1])
except (ValueError, IndexError):
self.send_error(400, "Bad request version (%r)" % version)
return False
if version_number >= (1, 1):
self.close_connection = 0
if version_number >= (2, 0):
self.send_error(505,
"Invalid HTTP Version (%s)" % base_version_number)
return False
elif len(words) == 2:
[command, path] = words
self.close_connection = 1
if command != 'GET':
self.send_error(400,
"Bad HTTP/0.9 request type (%r)" % command)
return False
elif not words:
return False
else:
self.send_error(400, "Bad request syntax (%r)" % requestline)
return False
self.request_version = version
self.command, self.path, self.version = command, path, version
return True
def handle_one_request(self):
"""Handle a single HTTP request.
Dispatch to the correct handler.
"""
self.request.setblocking(True)
self.raw_requestline = self.rfile.readline()
if not self.raw_requestline:
self.close_connection = 1
# self.log_message("no requestline, connection closed?")
return
if not self.parse_rawline():
self.log_message("Could not parse rawline.")
return
# self.parse_request(): # Do NOT parse here. the first line should be the only
for vdir in self.server.vdirs:
p = vdir.matches(self.path)
if p == False:
continue
npath = self.path[len(p):]
if not npath.startswith('/'):
npath = '/' + npath
if not self.in_handlers.has_key(p):
self.in_handlers[p] = vdir.handler(noconnection(),self.client_address,self.server)
if vdir.auth_provider:
vdir.auth_provider.setupAuth(self, self.in_handlers[p])
hnd = self.in_handlers[p]
hnd.rfile = self.rfile
hnd.wfile = self.wfile
self.rlpath = self.raw_requestline
self._handle_one_foreign(hnd,npath, vdir.auth_provider)
# print "Handled, closing = ", self.close_connection
return
# if no match:
self.send_error(404, "Path not found: %s" % self.path)
return
def _get_ignore_body(self,fore):
if not fore.headers.has_key("content-length"):
return
max_chunk_size = 10*1024*1024
size_remaining = int(fore.headers["content-length"])
got = ''
while size_remaining:
chunk_size = min(size_remaining, max_chunk_size)
got = fore.rfile.read(chunk_size)
size_remaining -= len(got)
class SecureMultiHTTPHandler(MultiHTTPHandler):
def getcert_fnames(self):
""" Return a pair with the filenames of ssl cert,key
Override this to direct to other filenames
"""
return ('server.cert','server.key')
def setup(self):
import ssl
certfile, keyfile = self.getcert_fnames()
try:
self.connection = ssl.wrap_socket(self.request,
server_side=True,
certfile=certfile,
keyfile=keyfile,
ssl_version=ssl.PROTOCOL_SSLv23)
self.rfile = self.connection.makefile('rb', self.rbufsize)
self.wfile = self.connection.makefile('wb', self.wbufsize)
self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
except:
self.request.shutdown(socket.SHUT_RDWR)
raise
def finish(self):
# With ssl connections, closing the filehandlers alone may not
# work because of ref counting. We explicitly tell the socket
# to shutdown.
MultiHTTPHandler.finish(self)
try:
self.connection.shutdown(socket.SHUT_RDWR)
except:
pass
import threading
class ConnThreadingMixIn:
"""Mix-in class to handle each _connection_ in a new thread.
This is necessary for persistent connections, where multiple
requests should be handled synchronously at each connection, but
multiple connections can run in parallel.
"""
# Decides how threads will act upon termination of the
# main process
daemon_threads = False
def _handle_request_noblock(self):
"""Start a new thread to process the request."""
t = threading.Thread(target = self._handle_request2)
if self.daemon_threads:
t.setDaemon (1)
t.start()
def _handle_request2(self):
"""Handle one request, without blocking.
I assume that select.select has returned that the socket is
readable before this function was called, so there should be
no risk of blocking in get_request().
"""
try:
request, client_address = self.get_request()
except socket.error:
return
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except:
self.handle_error(request, client_address)
self.close_request(request)
#eof