Skip to content
Snippets Groups Projects
Commit a43f68dc authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Add generic HTTP server classes

Some of the code is adopted from the 1.2 branch
(lib/rapi/RESTHTTPServer.py). This code can be used as a base for the
various HTTP servers in Ganeti.

Reviewed-by: iustinp
parent af30b2fd
No related branches found
No related tags found
No related merge requests found
...@@ -67,6 +67,7 @@ pkgpython_PYTHON = \ ...@@ -67,6 +67,7 @@ pkgpython_PYTHON = \
lib/config.py \ lib/config.py \
lib/constants.py \ lib/constants.py \
lib/errors.py \ lib/errors.py \
lib/http.py \
lib/jqueue.py \ lib/jqueue.py \
lib/locking.py \ lib/locking.py \
lib/logger.py \ lib/logger.py \
......
#
#
# 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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment