http.server: No longer enfore JSON encoding for body
authorMichael Hanselmann <hansmi@google.com>
Wed, 2 Dec 2009 16:48:55 +0000 (17:48 +0100)
committerMichael Hanselmann <hansmi@google.com>
Fri, 22 Jan 2010 14:17:44 +0000 (15:17 +0100)
The HTTP layer shouldn't care about the contents of the request data or
responses. This requires further changes in the RAPI code to handle client
requests correctly.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

NEWS
daemons/ganeti-noded
daemons/ganeti-rapi
lib/http/__init__.py
lib/http/server.py
lib/rapi/baserlib.py
lib/rapi/rlib2.py

diff --git a/NEWS b/NEWS
index ec98376..a0eae07 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,13 @@
 News
 ====
 
+Version 2.2.0
+-------------
+
+- RAPI now requires a Content-Type header for requests with a body (e.g.
+  ``PUT`` or ``POST``) which must be set to ``application/json`` (see
+  RFC2616 (HTTP/1.1), section 7.2.1)
+
 
 Version 2.1.0
 -------------
index d91eef4..4d27e18 100755 (executable)
@@ -45,6 +45,7 @@ from ganeti import daemon
 from ganeti import http
 from ganeti import utils
 from ganeti import storage
+from ganeti import serializer
 
 import ganeti.http.server # pylint: disable-msg=W0611
 
@@ -99,14 +100,13 @@ class NodeHttpServer(http.server.HttpServer):
       raise http.HttpNotFound()
 
     try:
-      rvalue = method(req.request_body)
-      return True, rvalue
+      result = (True, method(serializer.LoadJson(req.request_body)))
 
     except backend.RPCFail, err:
       # our custom failure exception; str(err) works fine if the
       # exception was constructed with a single argument, and in
       # this case, err.message == err.args[0] == str(err)
-      return (False, str(err))
+      result = (False, str(err))
     except errors.QuitGanetiException, err:
       # Tell parent to quit
       logging.info("Shutting down the node daemon, arguments: %s",
@@ -114,10 +114,12 @@ class NodeHttpServer(http.server.HttpServer):
       os.kill(self.noded_pid, signal.SIGTERM)
       # And return the error's arguments, which must be already in
       # correct tuple format
-      return err.args
+      result = err.args
     except Exception, err:
       logging.exception("Error in RPC call")
-      return False, "Error while executing backend function: %s" % str(err)
+      result = (False, "Error while executing backend function: %s" % str(err))
+
+    return serializer.DumpJson(result, indent=False)
 
   # the new block devices  --------------------------
 
index 00654e1..b2a2753 100755 (executable)
@@ -52,13 +52,14 @@ class RemoteApiRequestContext(object):
     self.handler = None
     self.handler_fn = None
     self.handler_access = None
+    self.body_data = None
 
 
 class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
   """Custom Request Executor class that formats HTTP errors in JSON.
 
   """
-  error_content_type = "application/json"
+  error_content_type = http.HttpJsonConverter.CONTENT_TYPE
 
   def _FormatErrorMessage(self, values):
     """Formats the body of an error message.
@@ -116,6 +117,9 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
       if ctx.handler_access is None:
         raise AssertionError("Permissions definition missing")
 
+      # This is only made available in HandleRequest
+      ctx.body_data = None
+
       req.private = ctx
 
     return req.private
@@ -162,6 +166,25 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
     """
     ctx = self._GetRequestContext(req)
 
+    # Deserialize request parameters
+    if req.request_body:
+      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
+      # include a Content-Type header field defining the media type of that
+      # body. [...] If the media type remains unknown, the recipient SHOULD
+      # treat it as type "application/octet-stream".
+      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
+                                                 http.HTTP_APP_OCTET_STREAM)
+      if (req_content_type.lower() !=
+          http.HttpJsonConverter.CONTENT_TYPE.lower()):
+        raise http.HttpUnsupportedMediaType()
+
+      try:
+        ctx.body_data = serializer.LoadJson(req.request_body)
+      except Exception:
+        raise http.HttpBadRequest(message="Unable to parse JSON data")
+    else:
+      ctx.body_data = None
+
     try:
       result = ctx.handler_fn()
     except luxi.TimeoutError:
@@ -173,7 +196,10 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
       logging.exception("Error while handling the %s request", method)
       raise
 
-    return result
+    req.resp_headers[http.HTTP_CONTENT_TYPE] = \
+      http.HttpJsonConverter.CONTENT_TYPE
+
+    return serializer.DumpJson(result)
 
 
 def CheckRapi(options, args):
index 272cff0..b291715 100644 (file)
@@ -680,7 +680,6 @@ class HttpMessage(object):
     self.start_line = None
     self.headers = None
     self.body = None
-    self.decoded_body = None
 
 
 class HttpClientToServerStartLine(object):
@@ -851,17 +850,9 @@ class HttpMessageReader(object):
     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.
 
index fbc7ec7..e395030 100644 (file)
@@ -78,7 +78,7 @@ class _HttpServerRequest(object):
     self.request_method = request_msg.start_line.method
     self.request_path = request_msg.start_line.path
     self.request_headers = request_msg.headers
-    self.request_body = request_msg.decoded_body
+    self.request_body = request_msg.body
 
     # Response attributes
     self.resp_headers = {}
@@ -333,12 +333,12 @@ class HttpServerRequestExecutor(object):
         logging.exception("Unknown exception")
         raise http.HttpInternalServerError(message="Unknown error")
 
-      # TODO: Content-type
-      encoder = http.HttpJsonConverter()
+      if not isinstance(result, basestring):
+        raise http.HttpError("Handler function didn't return string type")
+
       self.response_msg.start_line.code = http.HTTP_OK
-      self.response_msg.body = encoder.Encode(result)
       self.response_msg.headers = handler_context.resp_headers
-      self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
+      self.response_msg.body = result
     finally:
       # No reason to keep this any longer, even for exceptions
       handler_context.private = None
index 1f23f43..09d594c 100644 (file)
@@ -272,13 +272,13 @@ class R_Generic(object):
     @param name: the required parameter
 
     """
-    if name in self.req.request_body:
-      return self.req.request_body[name]
-    elif args:
-      return args[0]
-    else:
-      raise http.HttpBadRequest("Required parameter '%s' is missing" %
-                                name)
+    try:
+      return self.req.private.body_data[name]
+    except KeyError:
+      if args:
+        return args[0]
+
+    raise http.HttpBadRequest("Required parameter '%s' is missing" % name)
 
   def useLocking(self):
     """Check if the request specifies locking.
index f475277..d215736 100644 (file)
@@ -256,11 +256,11 @@ class R_2_nodes_name_role(baserlib.R_Generic):
     @return: a job id
 
     """
-    if not isinstance(self.req.request_body, basestring):
+    if not isinstance(self.req.private.body_data, basestring):
       raise http.HttpBadRequest("Invalid body contents, not a string")
 
     node_name = self.items[0]
-    role = self.req.request_body
+    role = self.req.private.body_data
 
     if role == _NR_REGULAR:
       candidate = False
@@ -431,12 +431,12 @@ class R_2_instances(baserlib.R_Generic):
     @return: a job id
 
     """
-    if not isinstance(self.req.request_body, dict):
+    if not isinstance(self.req.private.body_data, dict):
       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
 
-    beparams = baserlib.MakeParamsDict(self.req.request_body,
+    beparams = baserlib.MakeParamsDict(self.req.private.body_data,
                                        constants.BES_PARAMETERS)
-    hvparams = baserlib.MakeParamsDict(self.req.request_body,
+    hvparams = baserlib.MakeParamsDict(self.req.private.body_data,
                                        constants.HVS_PARAMETERS)
     fn = self.getBodyParameter