from ganeti import utils
-HTTP_CLIENT_THREADS = 10
-
HTTP_GANETI_VERSION = "Ganeti %s" % constants.RELEASE_VERSION
HTTP_OK = 200
HTTP_HEAD = "HEAD"
HTTP_POST = "POST"
HTTP_PUT = "PUT"
+HTTP_DELETE = "DELETE"
HTTP_ETAG = "ETag"
HTTP_HOST = "Host"
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"
_SSL_UNEXPECTED_EOF = "Unexpected EOF"
# Socket operations
(SOCKOP_SEND,
SOCKOP_RECV,
- SOCKOP_SHUTDOWN) = range(3)
+ SOCKOP_SHUTDOWN,
+ SOCKOP_HANDSHAKE) = range(4)
+
+# send/receive quantum
+SOCK_BUF_SIZE = 32768
class HttpError(Exception):
"""
-class _HttpClientError(Exception):
- """Internal exception for HTTP client errors.
+class HttpConnectionClosed(Exception):
+ """Internal exception for a closed connection.
+
+ This should only be used for internal error reporting. Only use
+ it if there's no other way to report this condition.
+
+ """
+
+
+class HttpSessionHandshakeUnexpectedEOF(HttpError):
+ """Internal exception for errors during SSL handshake.
This should only be used for internal error reporting.
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 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 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 HttpServiceUnavailable(HttpException):
+ """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.
+
+ """
code = 503
class HttpVersionNotSupported(HttpException):
+ """505 HTTP Version Not Supported
+
+ RFC2616, 10.5.6: The server does not support, or refuses to support,
+ the HTTP protocol version that was used in the request message.
+
+ """
code = 505
return serializer.LoadJson(data)
-def WaitForSocketCondition(poller, sock, event, timeout):
+def WaitForSocketCondition(sock, event, timeout):
"""Waits for a condition to occur on the socket.
- @type poller: select.Poller
- @param poller: Poller object as created by select.poll()
@type sock: socket
- @param socket: Wait for events on this socket
+ @param sock: Wait for events on this socket
@type event: int
@param event: ORed condition (see select module)
@type timeout: float or None
# Poller object expects milliseconds
timeout *= 1000
+ poller = select.poll()
poller.register(sock, event)
try:
while True:
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 socket: Socket for the operation
+ @param sock: Socket for the operation
@type op: int
@param op: Operation to execute (SOCKOP_* constants)
@type arg1: any
@param arg1: Parameter for function (if needed)
@type timeout: None or float
@param timeout: Timeout in seconds or None
+ @return: Return value of socket function
"""
# TODO: event_poll/event_check/override
- if op == SOCKOP_SEND:
+ if op in (SOCKOP_SEND, SOCKOP_HANDSHAKE):
event_poll = select.POLLOUT
event_check = select.POLLOUT
else:
raise AssertionError("Invalid socket operation")
+ # Handshake is only supported by SSL sockets
+ if (op == SOCKOP_HANDSHAKE and
+ not isinstance(sock, OpenSSL.SSL.ConnectionType)):
+ return
+
# No override by default
event_override = 0
while True:
# Poll only for certain operations and when asked for by an override
- if event_override or op in (SOCKOP_SEND, SOCKOP_RECV):
+ if event_override or op in (SOCKOP_SEND, SOCKOP_RECV, SOCKOP_HANDSHAKE):
if event_override:
wait_for_event = event_override
else:
wait_for_event = event_poll
- event = WaitForSocketCondition(poller, sock, wait_for_event, timeout)
+ event = WaitForSocketCondition(sock, wait_for_event, timeout)
if event is None:
raise HttpSocketTimeout()
else:
return sock.shutdown(arg1)
+ elif op == SOCKOP_HANDSHAKE:
+ return sock.do_handshake()
+
except OpenSSL.SSL.WantWriteError:
# OpenSSL wants to write, poll for POLLOUT
event_override = select.POLLOUT
except OpenSSL.SSL.WantX509LookupError:
continue
+ except OpenSSL.SSL.ZeroReturnError, err:
+ # SSL Connection has been closed. In SSL 3.0 and TLS 1.0, this only
+ # occurs if a closure alert has occurred in the protocol, i.e. the
+ # connection has been closed cleanly. Note that this does not
+ # necessarily mean that the transport layer (e.g. a socket) has been
+ # closed.
+ if op == SOCKOP_SEND:
+ # Can happen during a renegotiation
+ raise HttpConnectionClosed(err.args)
+ elif op == SOCKOP_RECV:
+ return ""
+
+ # SSL_shutdown shouldn't return SSL_ERROR_ZERO_RETURN
+ raise socket.error(err.args)
+
except OpenSSL.SSL.SysCallError, err:
if op == SOCKOP_SEND:
# arg1 is the data when writing
# and can be ignored
return 0
- elif op == SOCKOP_RECV:
- if err.args == (-1, _SSL_UNEXPECTED_EOF):
+ if err.args == (-1, _SSL_UNEXPECTED_EOF):
+ if op == SOCKOP_RECV:
return ""
+ elif op == SOCKOP_HANDSHAKE:
+ # Can happen if peer disconnects directly after the connection is
+ # opened.
+ raise HttpSessionHandshakeUnexpectedEOF(err.args)
raise socket.error(err.args)
raise
-def ShutdownConnection(poller, sock, close_timeout, write_timeout, msgreader,
- force):
+def ShutdownConnection(sock, close_timeout, write_timeout, msgreader, force):
"""Closes the connection.
- """
- poller = 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
+ @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
+ @type force: bool
+ @param force: Whether to forcibly close the connection without
+ waiting for peer
+ """
#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:
- SocketOperation(poller, sock, SOCKOP_SHUTDOWN, socket.SHUT_RDWR,
+ # We don't care about the return value, see NOTES in SSL_shutdown(3).
+ 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(sock, write_timeout):
+ """Shakes peer's hands.
+
+ @type sock: socket
+ @param sock: Socket to be shut down
+ @type write_timeout: float
+ @param write_timeout: Write timeout for handshake
+
+ """
+ try:
+ 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)
class HttpSslParams(object):
@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)
-class HttpSocketBase(object):
+class HttpBase(object):
"""Base class for HTTP server and client.
"""
def __init__(self):
- self._using_ssl = None
+ self.using_ssl = None
self._ssl_params = None
self._ssl_key = None
self._ssl_cert = None
@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
"""
self._ssl_params = ssl_params
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Should we enable SSL?
- self._using_ssl = ssl_params is not None
+ self.using_ssl = ssl_params is not None
- if not self._using_ssl:
+ if not self.using_ssl:
return sock
self._ssl_key = ssl_params.GetKey()
"""
def __init__(self, sock, msg, write_timeout):
+ """Initializes this class and writes an HTTP message to a socket.
+
+ @type sock: socket
+ @param sock: Socket to be written to
+ @type msg: http.HttpMessage
+ @param msg: HTTP message to be written
+ @type write_timeout: float
+ @param write_timeout: Write timeout for socket
+
+ """
self._msg = msg
self._PrepareMessage()
buf = self._FormatMessage()
- poller = select.poll()
- while buf:
- # Send only 4 KB at a time
- data = buf[:4096]
+ 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
- buf = buf[sent:]
+ pos += sent
- assert not buf, "Message wasn't sent completely"
+ assert pos == end, "Message wasn't sent completely"
def _PrepareMessage(self):
"""Prepares the HTTP message by setting mandatory headers.
PS_COMPLETE = "complete"
def __init__(self, sock, msg, read_timeout):
+ """Reads an HTTP message from a socket.
+
+ @type sock: socket
+ @param sock: Socket to be read from
+ @type msg: http.HttpMessage
+ @param msg: Object for the read message
+ @type read_timeout: float
+ @param read_timeout: Read timeout for socket
+
+ """
self.sock = sock
self.msg = msg
- self.poller = select.poll()
self.start_line_buffer = None
self.header_buffer = StringIO()
self.body_buffer = StringIO()
buf = ""
eof = False
while self.parser_status != self.PS_COMPLETE:
- data = SocketOperation(self.poller, sock, SOCKOP_RECV, 4096,
- read_timeout)
+ # TODO: Don't read more than necessary (Content-Length), otherwise
+ # data might be lost and/or an error could occur
+ data = SocketOperation(sock, SOCKOP_RECV, SOCK_BUF_SIZE, read_timeout)
if data:
buf += data
@return: Updated receive buffer
"""
+ # TODO: Use offset instead of slicing when possible
if self.parser_status == self.PS_START_LINE:
# Expect start line
while True:
# beginning of a message and receives a CRLF first, it should ignore
# the CRLF."
if idx == 0:
- # TODO: Limit number of CRLFs for safety?
+ # TODO: Limit number of CRLFs/empty lines for safety?
buf = buf[:2]
continue
# [...] 5. By the server closing the connection. (Closing the connection
# cannot be used to indicate the end of a request body, since that would
# leave no possibility for the server to send back a response.)"
+ #
+ # TODO: Error when buffer length > Content-Length header
if (eof or
self.content_length is None or
(self.content_length is not None and
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