diff --git a/Makefile.am b/Makefile.am index d06fc2885bf4d902aa0ce7e90b883fffd156de39..7b8bae0b33c083938095d76a85a177fff51a4cbf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -67,6 +67,7 @@ pkgpython_PYTHON = \ lib/config.py \ lib/constants.py \ lib/errors.py \ + lib/http.py \ lib/jqueue.py \ lib/locking.py \ lib/logger.py \ diff --git a/lib/http.py b/lib/http.py new file mode 100644 index 0000000000000000000000000000000000000000..a9febda66b7b881900e99c92a48009c8c0b56916 --- /dev/null +++ b/lib/http.py @@ -0,0 +1,277 @@ +# +# +# 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. + +"""HTTP server module. + +""" + +import socket +import BaseHTTPServer +import OpenSSL +import time +import logging + +from ganeti import errors +from ganeti import logger +from ganeti import serializer + + +class HTTPException(Exception): + code = None + message = None + + def __init__(self, message=None): + if message is not None: + self.message = message + + +class HTTPBadRequest(HTTPException): + code = 400 + + +class HTTPForbidden(HTTPException): + code = 403 + + +class HTTPNotFound(HTTPException): + code = 404 + + +class HTTPGone(HTTPException): + code = 410 + + +class HTTPLengthRequired(HTTPException): + code = 411 + + +class HTTPInternalError(HTTPException): + code = 500 + + +class HTTPNotImplemented(HTTPException): + code = 501 + + +class HTTPServiceUnavailable(HTTPException): + code = 503 + + +class ApacheLogfile: + """Utility class to write HTTP server log files. + + The written format is the "Common Log Format" as defined by Apache: + http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples + + """ + MONTHNAME = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + def __init__(self, fd): + """Constructor for ApacheLogfile class. + + Args: + - fd: Open file object + + """ + self._fd = fd + + def LogRequest(self, request, format, *args): + self._fd.write("%s %s %s [%s] %s\n" % ( + # Remote host address + request.address_string(), + + # RFC1413 identity (identd) + "-", + + # Remote user + "-", + + # Request time + self._FormatCurrentTime(), + + # Message + format % args, + )) + + def _FormatCurrentTime(self): + """Formats current time in Common Log Format. + + """ + return self._FormatLogTime(time.time()) + + def _FormatLogTime(self, seconds): + """Formats time for Common Log Format. + + All timestamps are logged in the UTC timezone. + + Args: + - seconds: Time in seconds since the epoch + + """ + (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds) + format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000" + return time.strftime(format, tm) + + +class HTTPServer(BaseHTTPServer.HTTPServer, object): + """Class to provide an HTTP/HTTPS server. + + """ + allow_reuse_address = True + + def __init__(self, server_address, HandlerClass, httplog=None, + enable_ssl=False, ssl_key=None, ssl_cert=None): + """Server constructor. + + Args: + server_address: a touple containing: + ip: a string with IP address, localhost if empty string + port: port number, integer + HandlerClass: HTTPRequestHandler object + httplog: Access log object + enable_ssl: Whether to enable SSL + ssl_key: SSL key file + ssl_cert: SSL certificate key + + """ + BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass) + + self.httplog = httplog + + if enable_ssl: + # Set up SSL + context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + context.use_privatekey_file(ssl_key) + context.use_certificate_file(ssl_cert) + self.socket = OpenSSL.SSL.Connection(context, + socket.socket(self.address_family, + self.socket_type)) + else: + self.socket = socket.socket(self.address_family, self.socket_type) + + self.server_bind() + self.server_activate() + + +class HTTPJsonConverter: + CONTENT_TYPE = "application/json" + + def Encode(self, data): + return serializer.DumpJson(data) + + def Decode(self, data): + return serializer.LoadJson(data) + + +class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object): + """Request handler class. + + """ + def setup(self): + """Setup secure read and write file objects. + + """ + self.connection = self.request + self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) + self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) + + def handle_one_request(self): + """Parses a request and calls the handler function. + + """ + self.raw_requestline = None + try: + self.raw_requestline = self.rfile.readline() + except OpenSSL.SSL.Error, ex: + logger.Error("Error in SSL: %s" % str(ex)) + if not self.raw_requestline: + self.close_connection = 1 + return + if not self.parse_request(): # An error code has been sent, just exit + return + logging.debug("HTTP request: %s", self.raw_requestline.rstrip("\r\n")) + + try: + self._ReadPostData() + + result = self.HandleRequest() + + # TODO: Content-type + encoder = HTTPJsonConverter() + encoded_result = encoder.Encode(result) + + self.send_response(200) + self.send_header("Content-Type", encoder.CONTENT_TYPE) + self.send_header("Content-Length", str(len(encoded_result))) + self.end_headers() + + self.wfile.write(encoded_result) + + except HTTPException, err: + self.send_error(err.code, message=err.message) + + except Exception, err: + self.send_error(HTTPInternalError.code, message=str(err)) + + except: + self.send_error(HTTPInternalError.code, message="Unknown error") + + def _ReadPostData(self): + if self.command.upper() not in ("POST", "PUT"): + self.post_data = None + return + + # TODO: Decide what to do when Content-Length header was not sent + try: + content_length = int(self.headers.get('Content-Length', 0)) + except ValueError: + raise HTTPBadRequest("No Content-Length header or invalid format") + + try: + data = self.rfile.read(content_length) + except socket.error, err: + logger.Error("Socket error while reading: %s" % str(err)) + return + + # TODO: Content-type, error handling + self.post_data = HTTPJsonConverter().Decode(data) + + logging.debug("HTTP POST data: %s", self.post_data) + + def HandleRequest(self): + """Handles a request. + + """ + raise NotImplementedError() + + def log_message(self, format, *args): + """Log an arbitrary message. + + This is used by all other logging functions. + + The first argument, FORMAT, is a format string for the + message to be logged. If the format string contains + any % escapes requiring parameters, they should be + specified as subsequent arguments (it's just like + printf!). + + """ + logging.debug("Handled request: %s", format % args) + if self.server.httplog: + self.server.httplog.LogRequest(self, format, *args)