#
#
-# Copyright (C) 2007, 2008 Google Inc.
+# Copyright (C) 2007, 2008, 2010 Google Inc.
#
# 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
from cStringIO import StringIO
from ganeti import constants
-from ganeti import serializer
from ganeti import utils
HTTP_CONTENT_LENGTH = "Content-Length"
HTTP_CONNECTION = "Connection"
HTTP_KEEP_ALIVE = "Keep-Alive"
+HTTP_WWW_AUTHENTICATE = "WWW-Authenticate"
+HTTP_AUTHORIZATION = "Authorization"
+HTTP_AUTHENTICATION_INFO = "Authentication-Info"
+HTTP_ALLOW = "Allow"
+
+HTTP_APP_OCTET_STREAM = "application/octet-stream"
+HTTP_APP_JSON = "application/json"
_SSL_UNEXPECTED_EOF = "Unexpected EOF"
code = None
message = None
- def __init__(self, message=None):
+ def __init__(self, message=None, headers=None):
Exception.__init__(self)
- if message is not None:
- self.message = message
+ self.message = message
+ self.headers = headers
class HttpBadRequest(HttpException):
+ """400 Bad Request
+
+ RFC2616, 10.4.1: The request could not be understood by the server
+ due to malformed syntax. The client SHOULD NOT repeat the request
+ without modifications.
+
+ """
code = 400
+class HttpUnauthorized(HttpException):
+ """401 Unauthorized
+
+ RFC2616, section 10.4.2: The request requires user
+ authentication. The response MUST include a WWW-Authenticate header
+ field (section 14.47) containing a challenge applicable to the
+ requested resource.
+
+ """
+ code = 401
+
+
class HttpForbidden(HttpException):
+ """403 Forbidden
+
+ RFC2616, 10.4.4: The server understood the request, but is refusing
+ to fulfill it. Authorization will not help and the request SHOULD
+ NOT be repeated.
+
+ """
code = 403
class HttpNotFound(HttpException):
+ """404 Not Found
+
+ RFC2616, 10.4.5: The server has not found anything matching the
+ Request-URI. No indication is given of whether the condition is
+ temporary or permanent.
+
+ """
code = 404
+class HttpMethodNotAllowed(HttpException):
+ """405 Method Not Allowed
+
+ RFC2616, 10.4.6: The method specified in the Request-Line is not
+ allowed for the resource identified by the Request-URI. The response
+ MUST include an Allow header containing a list of valid methods for
+ the requested resource.
+
+ """
+ code = 405
+
+
+class HttpNotAcceptable(HttpException):
+ """406 Not Acceptable
+
+ RFC2616, 10.4.7: The resource identified by the request is only capable of
+ generating response entities which have content characteristics not
+ acceptable according to the accept headers sent in the request.
+
+ """
+ code = 406
+
+
+class HttpRequestTimeout(HttpException):
+ """408 Request Timeout
+
+ RFC2616, 10.4.9: The client did not produce a request within the
+ time that the server was prepared to wait. The client MAY repeat the
+ request without modifications at any later time.
+
+ """
+ code = 408
+
+
+class HttpConflict(HttpException):
+ """409 Conflict
+
+ RFC2616, 10.4.10: The request could not be completed due to a
+ conflict with the current state of the resource. This code is only
+ allowed in situations where it is expected that the user might be
+ able to resolve the conflict and resubmit the request.
+
+ """
+ code = 409
+
+
class HttpGone(HttpException):
+ """410 Gone
+
+ RFC2616, 10.4.11: The requested resource is no longer available at
+ the server and no forwarding address is known. This condition is
+ expected to be considered permanent.
+
+ """
code = 410
class HttpLengthRequired(HttpException):
+ """411 Length Required
+
+ RFC2616, 10.4.12: The server refuses to accept the request without a
+ defined Content-Length. The client MAY repeat the request if it adds
+ a valid Content-Length header field containing the length of the
+ message-body in the request message.
+
+ """
code = 411
-class HttpInternalError(HttpException):
+class HttpPreconditionFailed(HttpException):
+ """412 Precondition Failed
+
+ RFC2616, 10.4.13: The precondition given in one or more of the
+ request-header fields evaluated to false when it was tested on the
+ server.
+
+ """
+ code = 412
+
+
+class HttpUnsupportedMediaType(HttpException):
+ """415 Unsupported Media Type
+
+ RFC2616, 10.4.16: The server is refusing to service the request because the
+ entity of the request is in a format not supported by the requested resource
+ for the requested method.
+
+ """
+ code = 415
+
+
+class HttpInternalServerError(HttpException):
+ """500 Internal Server Error
+
+ RFC2616, 10.5.1: The server encountered an unexpected condition
+ which prevented it from fulfilling the request.
+
+ """
code = 500
class HttpNotImplemented(HttpException):
+ """501 Not Implemented
+
+ RFC2616, 10.5.2: The server does not support the functionality
+ required to fulfill the request.
+
+ """
code = 501
+class HttpBadGateway(HttpException):
+ """502 Bad Gateway
+
+ RFC2616, 10.5.3: The server, while acting as a gateway or proxy,
+ received an invalid response from the upstream server it accessed in
+ attempting to fulfill the request.
+
+ """
+ code = 502
+
+
class HttpServiceUnavailable(HttpException):
- code = 503
+ """503 Service Unavailable
+ RFC2616, 10.5.4: The server is currently unable to handle the
+ request due to a temporary overloading or maintenance of the server.
-class HttpVersionNotSupported(HttpException):
- code = 505
+ """
+ code = 503
-class HttpJsonConverter:
- CONTENT_TYPE = "application/json"
+class HttpGatewayTimeout(HttpException):
+ """504 Gateway Timeout
- def Encode(self, data):
- return serializer.DumpJson(data)
+ RFC2616, 10.5.5: The server, while acting as a gateway or proxy, did
+ not receive a timely response from the upstream server specified by
+ the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server
+ (e.g. DNS) it needed to access in attempting to complete the
+ request.
- def Decode(self, data):
- return serializer.LoadJson(data)
+ """
+ code = 504
-def WaitForSocketCondition(poller, sock, event, timeout):
- """Waits for a condition to occur on the socket.
+class HttpVersionNotSupported(HttpException):
+ """505 HTTP Version Not Supported
- @type poller: select.Poller
- @param poller: Poller object as created by select.poll()
- @type sock: socket
- @param sock: Wait for events on this socket
- @type event: int
- @param event: ORed condition (see select module)
- @type timeout: float or None
- @param timeout: Timeout in seconds
- @rtype: int or None
- @return: None for timeout, otherwise occured conditions
+ RFC2616, 10.5.6: The server does not support, or refuses to support,
+ the HTTP protocol version that was used in the request message.
"""
- check = (event | select.POLLPRI |
- select.POLLNVAL | select.POLLHUP | select.POLLERR)
+ code = 505
- if timeout is not None:
- # Poller object expects milliseconds
- timeout *= 1000
- poller.register(sock, event)
- try:
- while True:
- # TODO: If the main thread receives a signal and we have no timeout, we
- # could wait forever. This should check a global "quit" flag or
- # something every so often.
- io_events = poller.poll(timeout)
- if not io_events:
- # Timeout
- return None
- for (evfd, evcond) in io_events:
- if evcond & check:
- return evcond
- finally:
- poller.unregister(sock)
-
-
-def SocketOperation(poller, sock, op, arg1, timeout):
+def SocketOperation(sock, op, arg1, timeout):
"""Wrapper around socket functions.
This function abstracts error handling for socket operations, especially
for the complicated interaction with OpenSSL.
- @type poller: select.Poller
- @param poller: Poller object as created by select.poll()
@type sock: socket
@param sock: Socket for the operation
@type op: int
# TODO: event_poll/event_check/override
if op in (SOCKOP_SEND, SOCKOP_HANDSHAKE):
event_poll = select.POLLOUT
- event_check = select.POLLOUT
elif op == SOCKOP_RECV:
event_poll = select.POLLIN
- event_check = select.POLLIN | select.POLLPRI
elif op == SOCKOP_SHUTDOWN:
event_poll = None
- event_check = None
# The timeout is only used when OpenSSL requests polling for a condition.
# It is not advisable to have no timeout for shutdown.
else:
wait_for_event = event_poll
- event = WaitForSocketCondition(poller, sock, wait_for_event, timeout)
+ event = utils.WaitForFdCondition(sock, wait_for_event, timeout)
if event is None:
raise HttpSocketTimeout()
- if (op == SOCKOP_RECV and
- event & (select.POLLNVAL | select.POLLHUP | select.POLLERR)):
- return ""
+ if event & (select.POLLNVAL | select.POLLHUP | select.POLLERR):
+ # Let the socket functions handle these
+ break
if not event & wait_for_event:
continue
raise
-def ShutdownConnection(poller, sock, close_timeout, write_timeout, msgreader,
- force):
+def ShutdownConnection(sock, close_timeout, write_timeout, msgreader, force):
"""Closes the connection.
- @type poller: select.Poller
- @param poller: Poller object as created by select.poll()
@type sock: socket
@param sock: Socket to be shut down
@type close_timeout: float
- @param close_timeout: How long to wait for the peer to close the connection
+ @param close_timeout: How long to wait for the peer to close
+ the connection
@type write_timeout: float
@param write_timeout: Write timeout for shutdown
@type msgreader: http.HttpMessageReader
- @param msgreader: Request message reader, used to determine whether peer
- should close connection
+ @param msgreader: Request message reader, used to determine whether
+ peer should close connection
@type force: bool
- @param force: Whether to forcibly close the connection without waiting
- for peer
+ @param force: Whether to forcibly close the connection without
+ waiting for peer
"""
- poller = select.poll()
-
#print msgreader.peer_will_close, force
if msgreader and msgreader.peer_will_close and not force:
# Wait for peer to close
try:
# Check whether it's actually closed
- if not SocketOperation(poller, sock, SOCKOP_RECV, 1, close_timeout):
+ if not SocketOperation(sock, SOCKOP_RECV, 1, close_timeout):
return
except (socket.error, HttpError, HttpSocketTimeout):
# Ignore errors at this stage
# Close the connection from our side
try:
# We don't care about the return value, see NOTES in SSL_shutdown(3).
- SocketOperation(poller, sock, SOCKOP_SHUTDOWN, socket.SHUT_RDWR,
+ SocketOperation(sock, SOCKOP_SHUTDOWN, socket.SHUT_RDWR,
write_timeout)
except HttpSocketTimeout:
raise HttpError("Timeout while shutting down connection")
except socket.error, err:
- raise HttpError("Error while shutting down connection: %s" % err)
+ # Ignore ENOTCONN
+ if not (err.args and err.args[0] == errno.ENOTCONN):
+ raise HttpError("Error while shutting down connection: %s" % err)
-def Handshake(poller, sock, write_timeout):
+def Handshake(sock, write_timeout):
"""Shakes peer's hands.
- @type poller: select.Poller
- @param poller: Poller object as created by select.poll()
@type sock: socket
@param sock: Socket to be shut down
@type write_timeout: float
"""
try:
- return SocketOperation(poller, sock, SOCKOP_HANDSHAKE, None, write_timeout)
+ return SocketOperation(sock, SOCKOP_HANDSHAKE, None, write_timeout)
except HttpSocketTimeout:
raise HttpError("Timeout during SSL handshake")
except socket.error, err:
raise HttpError("Error in SSL handshake: %s" % err)
+def InitSsl():
+ """Initializes the SSL infrastructure.
+
+ This function is idempotent.
+
+ """
+ if not OpenSSL.rand.status():
+ raise EnvironmentError("OpenSSL could not collect enough entropy"
+ " for the PRNG")
+
+ # TODO: Maybe add some additional seeding for OpenSSL's PRNG
+
+
class HttpSslParams(object):
"""Data class for SSL key and certificate.
@type ssl_key_path: string
@param ssl_key_path: Path to file containing SSL key in PEM format
@type ssl_cert_path: string
- @param ssl_cert_path: Path to file containing SSL certificate in PEM format
+ @param ssl_cert_path: Path to file containing SSL certificate
+ in PEM format
"""
self.ssl_key_pem = utils.ReadFile(ssl_key_path)
self.ssl_cert_pem = utils.ReadFile(ssl_cert_path)
+ self.ssl_cert_path = ssl_cert_path
def GetKey(self):
return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
self._ssl_key = None
self._ssl_cert = None
- def _CreateSocket(self, ssl_params, ssl_verify_peer):
+ def _CreateSocket(self, ssl_params, ssl_verify_peer, family):
"""Creates a TCP socket and initializes SSL if needed.
@type ssl_params: HttpSslParams
@param ssl_params: SSL key and certificate
@type ssl_verify_peer: bool
- @param ssl_verify_peer: Whether to require client certificate and compare
- it with our certificate
+ @param ssl_verify_peer: Whether to require client certificate
+ and compare it with our certificate
+ @type family: int
+ @param family: socket.AF_INET | socket.AF_INET6
"""
- self._ssl_params = ssl_params
+ assert family in (socket.AF_INET, socket.AF_INET6)
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._ssl_params = ssl_params
+ sock = socket.socket(family, socket.SOCK_STREAM)
# Should we enable SSL?
self.using_ssl = ssl_params is not None
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
+ ciphers = self.GetSslCiphers()
+ logging.debug("Setting SSL cipher string %s", ciphers)
+ ctx.set_cipher_list(ciphers)
+
ctx.use_privatekey(self._ssl_key)
ctx.use_certificate(self._ssl_cert)
ctx.check_privatekey()
OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
self._SSLVerifyCallback)
+ # Also add our certificate as a trusted CA to be sent to the client.
+ # This is required at least for GnuTLS clients to work.
+ try:
+ # This will fail for PyOpenssl versions before 0.10
+ ctx.add_client_ca(self._ssl_cert)
+ except AttributeError:
+ # Fall back to letting OpenSSL read the certificate file directly.
+ ctx.load_client_ca(ssl_params.ssl_cert_path)
+
return OpenSSL.SSL.Connection(ctx, sock)
+ def GetSslCiphers(self): # pylint: disable-msg=R0201
+ """Returns the ciphers string for SSL.
+
+ """
+ return constants.OPENSSL_CIPHERS
+
def _SSLVerifyCallback(self, conn, cert, errnum, errdepth, ok):
"""Verify the certificate provided by the peer
we do on our side.
"""
+ # some parameters are unused, but this is the API
+ # pylint: disable-msg=W0613
assert self._ssl_params, "SSL not initialized"
return (self._ssl_cert.digest("sha1") == cert.digest("sha1") and
self.start_line = None
self.headers = None
self.body = None
- self.decoded_body = None
class HttpClientToServerStartLine(object):
buf = self._FormatMessage()
- poller = select.poll()
-
pos = 0
end = len(buf)
while pos < end:
# Send only SOCK_BUF_SIZE bytes at a time
data = buf[pos:(pos + SOCK_BUF_SIZE)]
- sent = SocketOperation(poller, sock, SOCKOP_SEND, data,
- write_timeout)
+ sent = SocketOperation(sock, SOCKOP_SEND, data, write_timeout)
# Remove sent bytes
pos += sent
def HasMessageBody(self):
"""Checks whether the HTTP message contains a body.
- Can be overriden by subclasses.
+ Can be overridden by subclasses.
"""
return bool(self._msg.body)
self.sock = sock
self.msg = msg
- self.poller = select.poll()
self.start_line_buffer = None
self.header_buffer = StringIO()
self.body_buffer = StringIO()
while self.parser_status != self.PS_COMPLETE:
# TODO: Don't read more than necessary (Content-Length), otherwise
# data might be lost and/or an error could occur
- data = SocketOperation(self.poller, sock, SOCKOP_RECV, SOCK_BUF_SIZE,
- read_timeout)
+ data = SocketOperation(sock, SOCKOP_RECV, SOCK_BUF_SIZE, read_timeout)
if data:
buf += data
buf = self._ContinueParsing(buf, eof)
# Must be done only after the buffer has been evaluated
- # TODO: Connection-length < len(data read) and connection closed
+ # TODO: Content-Length < len(data read) and connection closed
if (eof and
self.parser_status in (self.PS_START_LINE,
self.PS_HEADERS)):
assert self.parser_status == self.PS_COMPLETE
assert not buf, "Parser didn't read full response"
+ # Body is complete
msg.body = self.body_buffer.getvalue()
- # TODO: Content-type, error handling
- if msg.body:
- msg.decoded_body = HttpJsonConverter().Decode(msg.body)
- else:
- msg.decoded_body = None
-
- if msg.decoded_body:
- logging.debug("Message body: %s", msg.decoded_body)
-
def _ContinueParsing(self, buf, eof):
"""Main function for HTTP message state machine.
# the CRLF."
if idx == 0:
# TODO: Limit number of CRLFs/empty lines for safety?
- buf = buf[:2]
+ buf = buf[2:]
continue
if idx > 0:
def ParseStartLine(self, start_line):
"""Parses the start line of a message.
- Must be overriden by subclass.
+ Must be overridden by subclass.
@type start_line: string
@param start_line: Start line string
This function also adjusts internal variables based on header values.
- RFC2616, section 4.3: "The presence of a message-body in a request is
+ RFC2616, section 4.3: The presence of a message-body in a request is
signaled by the inclusion of a Content-Length or Transfer-Encoding header
- field in the request's message-headers."
+ field in the request's message-headers.
"""
# Parse headers
if hdr_content_length:
try:
self.content_length = int(hdr_content_length)
- except ValueError:
+ except (TypeError, ValueError):
self.content_length = None
if self.content_length is not None and self.content_length < 0:
self.content_length = None