Add targetted pylint disables
[ganeti-local] / lib / http / __init__.py
index e8a355d..c98fa58 100644 (file)
@@ -61,17 +61,23 @@ HTTP_CONTENT_TYPE = "Content-Type"
 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):
   """Internal exception for HTTP errors.
 
@@ -80,8 +86,17 @@ 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.
 
@@ -100,45 +115,187 @@ class HttpException(Exception):
   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 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):
+  """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 HttpGatewayTimeout(HttpException):
+  """504 Gateway Timeout
+
+  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.
+
+  """
+  code = 504
+
+
 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
 
 
@@ -152,13 +309,11 @@ class HttpJsonConverter:
     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
@@ -174,6 +329,7 @@ def WaitForSocketCondition(poller, sock, event, timeout):
     # Poller object expects milliseconds
     timeout *= 1000
 
+  poller = select.poll()
   poller.register(sock, event)
   try:
     while True:
@@ -191,36 +347,32 @@ def WaitForSocketCondition(poller, sock, event, timeout):
     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
 
   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.
@@ -229,18 +381,23 @@ def SocketOperation(poller, sock, op, arg1, timeout):
   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()
 
@@ -269,6 +426,9 @@ def SocketOperation(poller, sock, op, arg1, timeout):
           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
@@ -282,6 +442,21 @@ def SocketOperation(poller, sock, op, arg1, timeout):
       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
@@ -290,9 +465,13 @@ def SocketOperation(poller, sock, op, arg1, timeout):
             # 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)
 
@@ -307,19 +486,30 @@ def SocketOperation(poller, sock, op, arg1, timeout):
       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
@@ -327,12 +517,32 @@ def ShutdownConnection(poller, sock, close_timeout, write_timeout, msgreader,
 
   # 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):
@@ -345,7 +555,8 @@ 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)
@@ -360,12 +571,12 @@ class HttpSslParams(object):
                                            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
@@ -376,8 +587,8 @@ class HttpSocketBase(object):
     @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
@@ -385,9 +596,9 @@ class HttpSocketBase(object):
     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()
@@ -462,22 +673,29 @@ class HttpMessageWriter(object):
 
   """
   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()
-
     pos = 0
     end = len(buf)
     while pos < end:
       # Send only SOCK_BUF_SIZE bytes at a time
-      data = buf[pos:pos+SOCK_BUF_SIZE]
+      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
@@ -523,7 +741,7 @@ class HttpMessageWriter(object):
   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)
@@ -544,10 +762,19 @@ class HttpMessageReader(object):
   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()
@@ -558,8 +785,9 @@ class HttpMessageReader(object):
     buf = ""
     eof = False
     while self.parser_status != self.PS_COMPLETE:
-      data = SocketOperation(self.poller, sock, SOCKOP_RECV, SOCK_BUF_SIZE,
-                             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
@@ -604,6 +832,7 @@ class HttpMessageReader(object):
     @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:
@@ -615,7 +844,7 @@ class HttpMessageReader(object):
         # 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
 
@@ -669,6 +898,8 @@ class HttpMessageReader(object):
       # [...] 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
@@ -703,7 +934,7 @@ class HttpMessageReader(object):
   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
@@ -755,9 +986,9 @@ class HttpMessageReader(object):
 
     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