diff mbox

[14/17] Add implementation of JSON-RPC

Message ID 1500926714-56988-15-git-send-email-dmalcolm@redhat.com
State New
Headers show

Commit Message

David Malcolm July 24, 2017, 8:05 p.m. UTC
This patch adds an abstract base class for implementing
JSON-RPC 2.0 servers, along with a class for wiring them
up to the HTTP server class implemented in the previous
patch, supporting the serving of JSON-RPC over HTTP on a port.

gcc/ChangeLog:
	* Makefile.in (OBJS): Add json-rpc.o.
	* json-rpc.c: New file.
	* json-rpc.h: New file.
	* selftest-run-tests.c (selftest::run_tests): Call
	selftest::json_rpc_c_tests.
	* selftest.h (selftest::json_rpc_c_tests.): New decl.
---
 gcc/Makefile.in          |   1 +
 gcc/json-rpc.c           | 486 +++++++++++++++++++++++++++++++++++++++++++++++
 gcc/json-rpc.h           |  94 +++++++++
 gcc/selftest-run-tests.c |   1 +
 gcc/selftest.h           |   1 +
 5 files changed, 583 insertions(+)
 create mode 100644 gcc/json-rpc.c
 create mode 100644 gcc/json-rpc.h
diff mbox

Patch

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index 0c361f1..1f9050c 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1371,6 +1371,7 @@  OBJS = \
 	ira-emit.o \
 	ira-lives.o \
 	json.o \
+	json-rpc.o \
 	jump.o \
 	langhooks.o \
 	lcm.o \
diff --git a/gcc/json-rpc.c b/gcc/json-rpc.c
new file mode 100644
index 0000000..1cc0049
--- /dev/null
+++ b/gcc/json-rpc.c
@@ -0,0 +1,486 @@ 
+/* JSON-RPC server implementation.
+   Copyright (C) 2017 Free Software Foundation, Inc.
+
+This file is part of GCC.
+
+GCC 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 3, or (at your option) any later
+version.
+
+GCC 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 GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "json.h"
+#include "http-server.h"
+#include "json-rpc.h"
+#include "selftest.h"
+
+/* Create a JSON-RPC 2.0 response object, setting "id" to a copy
+   of ID if it is non-NULL.  */
+
+json::object *
+jsonrpc::make_response (const json::value *id)
+{
+  json::object *response = new json::object ();
+  response->set ("jsonrpc", new json::string ("2.0"));
+  if (id)
+    response->set ("id", id->clone ());
+  return response;
+}
+
+/* Create a JSON-RPC 2.0 error object, setting:
+   - "id" to a copy of ID if it is non-NULL,
+   - error.code to CODE,
+   - error.message to MESSAGE.  */
+
+json::object *
+jsonrpc::make_failure (int code, const char *message, const json::value *id)
+{
+  json::object *failure = make_response (id);
+  json::object *error = new json::object ();
+  failure->set ("error", error);
+
+  error->set ("code", new json::number (code));
+  error->set ("message", new json::string (message));
+  return failure;
+}
+
+/* Create a JSON-RPC 2.0 "parse error" object with error.message as MSG.  */
+
+json::value *
+jsonrpc::make_parse_error (const char *msg)
+{
+  return make_failure (PARSE_ERROR, msg, NULL);
+}
+
+/* Create a JSON-RPC 2.0 "invalid request" error object with the given ID.  */
+
+json::value *
+jsonrpc::make_invalid_request (const json::value *id)
+{
+  return make_failure (INVALID_REQUEST, "invalid request", id);
+}
+
+/* Create a JSON-RPC 2.0 "method not found" error object for a method named
+   METHOD.  */
+
+json::value *
+jsonrpc::make_method_not_found (const json::value *id, const char *method)
+{
+  const char *msg = ACONCAT (("method not found: ", method, NULL));
+  return make_failure (METHOD_NOT_FOUND, msg, id);
+}
+
+/* Create a JSON-RPC 2.0 "invalid params" error object with error.message as MSG
+   if non-NULL, or "invalid params" otherwise.  */
+
+json::value *
+jsonrpc::make_invalid_params (const json::value *id, const char *msg)
+{
+  if (msg == NULL)
+    msg = "invalid params";
+  return make_failure (INVALID_PARAMS, msg, id);
+}
+
+/* Create a JSON-RPC 2.0 response object for request ID, with result
+   as RESULT, taking ownership of RESULT.  */
+
+json::value *
+jsonrpc::make_success (const json::value *id, json::value *result)
+{
+  json::object *success = make_response (id);
+  success->set ("result", result);
+  return success;
+}
+
+/* Public interface for handling JSON-RPC requests.
+   Takes a borrowed pointer to REQUEST.
+   Returns a new pointer to a result object.
+   Wrapper around handle_request_1, adding support for dumping.  */
+
+json::value *
+jsonrpc::server::handle_request (json::value *request)
+{
+  if (m_verbose)
+    {
+      fprintf (stderr, "request: ");
+      request->dump (stderr);
+      fprintf (stderr, "\n");
+    }
+
+  json::value *response = handle_request_1 (request);
+
+  if (m_verbose)
+    {
+      fprintf (stderr, "response: ");
+      if (response)
+	response->dump (stderr);
+      else
+	fprintf (stderr, "NULL");
+      fprintf (stderr, "\n");
+    }
+
+  return response;
+}
+
+/* Private subroutine of handle_request.
+   Takes a borrowed pointer to REQUEST.
+   Returns a new pointer to a result object.
+   Performs various error-checking, then calls the "dispatch" vfunc.  */
+
+json::value *
+jsonrpc::server::handle_request_1 (json::value *request)
+{
+  // FIXME: handle batch requests
+  if (request->get_kind () != json::JSON_OBJECT)
+    return make_parse_error ("not an object");
+
+  json::object *reqobj = static_cast <json::object *> (request);
+
+  /* Lookup id.  */
+  json::value *id = reqobj->get ("id");
+
+  /* If no id, then it's a notification.  */
+
+  /* TODO: validate non-NULL id.  */
+
+  /* Check version.  */
+  json::value *version = reqobj->get ("jsonrpc");
+  if (!version)
+    return make_invalid_request (id);
+  const json::string *version_string = version->as_string ();
+  if (!version_string)
+    return make_failure (INVALID_REQUEST, "jsonrpc was not 2.0", id);
+  if (0 != strcmp (version_string->get_string (), "2.0"))
+    return make_failure (INVALID_REQUEST, "jsonrpc was not 2.0", id);
+
+  json::value *method = reqobj->get ("method");
+  if (!method)
+    return make_failure (INVALID_REQUEST, "no method", id);
+  const json::string *method_str = method->as_string ();
+  if (!method_str)
+    return make_failure (INVALID_REQUEST, "method was not a string", id);
+
+  const json::value *params = reqobj->get ("params");
+  return dispatch (method_str->get_string (), params, id);
+}
+
+/* Parse the given buffer as JSON-RPC and attempt to dispatch the call.
+   Returns a new pointer to a result object.  */
+
+json::value *
+jsonrpc::server::handle_request (size_t sz, const char *buf)
+{
+  char *err = NULL;
+  json::value *request = json::parse_utf8_string (sz, buf, &err);
+  if (!request)
+    {
+      gcc_assert (err);
+      json::value *response = make_parse_error (err);
+      free (err);
+      return response;
+    }
+  gcc_assert (err == NULL);
+  json::value *response = handle_request (request);
+  delete request;
+  return response;
+}
+
+/* Parse the given UTF-8 encoded 0-terminated string as JSON-RPC and
+   attempt to dispatch the call.
+   Returns a new pointer to a result object.
+
+   Helper function for writing selftests.  */
+
+json::value *
+jsonrpc::server::handle_request_string (const char *utf8)
+{
+  return handle_request (strlen (utf8), utf8);
+}
+
+/* class ::jsonrpc::http_server : public ::http::server.  */
+
+/* jsonrpc::http_server's ctor.  */
+
+jsonrpc::http_server::http_server (jsonrpc::server &json_handler)
+: ::http::server (true), m_json_handler (json_handler)
+{}
+
+/* Handle HTTP requests by parsing the content as JSON-RPC, and delegating
+   them to a ::jsonrpc::server.  */
+
+void
+jsonrpc::http_server::on_http_request (const http::request &http_request,
+				       http::response &http_response)
+{
+  size_t length = http_request.get_content_length ();
+  const char *buf = http_request.get_content ();
+  if (0)
+    fprintf (stderr, "got content: '%.*s'\n", (int)length, buf);
+  json::value *json_response = m_json_handler.handle_request (length, buf);
+  char *response_str = json_response->to_str ();
+  delete json_response;
+  http_response.set_content (strlen (response_str), response_str);
+  free (response_str);
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Selftests.  */
+
+/* Assert that RESPONSE is a non-NULL response for ID.
+   Verify that the jsonrpc value is "2.0".
+   Return RESPONSE, cast to an object *.  */
+
+static const json::object *
+assert_is_response (const json::value *response, int id)
+{
+  ASSERT_TRUE (response != NULL);
+  const json::object *obj = response->as_object ();
+  ASSERT_TRUE (obj != NULL);
+  json::value *jsonrpc = obj->get ("jsonrpc");
+  ASSERT_TRUE (jsonrpc != NULL);
+  ASSERT_STREQ ("2.0", jsonrpc->as_string ()->get_string ());
+  ASSERT_EQ (id, obj->get ("id")->as_number ()->get ());
+  return obj;
+}
+
+/* Assert that RESPONSE is a non-NULL response, with no ID.
+   Verify that the jsonrpc value is "2.0".
+   Return RESPONSE, cast to an object *.  */
+
+static const json::object *
+assert_is_response (const json::value *response)
+{
+  ASSERT_TRUE (response != NULL);
+  const json::object *obj = response->as_object ();
+  ASSERT_TRUE (obj != NULL);
+  json::value *jsonrpc = obj->get ("jsonrpc");
+  ASSERT_TRUE (jsonrpc != NULL);
+  ASSERT_STREQ ("2.0", jsonrpc->as_string ()->get_string ());
+  return obj;
+}
+
+/* Assert that RESPONSE is a non-NULL successful response for ID.
+   Verify that the jsonrpc value is "2.0".
+   Return the "result" value.  */
+
+static const json::value *
+assert_is_success (const json::value *response, int id)
+{
+  const json::object *obj = assert_is_response (response, id);
+  ASSERT_EQ (NULL, obj->get ("error"));
+  return obj->get ("result");
+}
+
+/* Assert that RESPONSE is a non-NULL failure response for ID.
+   Verify that the jsonrpc value is "2.0".
+   Verify that CODE and MESSAGE match the given values.  */
+
+static void
+assert_is_failure (const json::value *response, int code, const char *message,
+		   int id)
+{
+  const json::object *obj = assert_is_response (response, id);
+  ASSERT_EQ (NULL, obj->get ("result"));
+  const json::object *err = obj->get ("error")->as_object ();
+  ASSERT_EQ (code, err->get ("code")->as_number ()->get ());
+  ASSERT_STREQ (message, err->get ("message")->as_string ()->get_string ());
+}
+
+/* Assert that RESPONSE is a non-NULL failure response, with no ID.
+   Verify that the jsonrpc value is "2.0".
+   Verify that CODE and MESSAGE match the given values.  */
+
+static void
+assert_is_failure (const json::value *response, int code, const char *message)
+{
+  const json::object *obj = assert_is_response (response);
+  ASSERT_EQ (NULL, obj->get ("result"));
+  const json::object *err = obj->get ("error")->as_object ();
+  ASSERT_EQ (code, err->get ("code")->as_number ()->get ());
+  ASSERT_STREQ (message, err->get ("message")->as_string ()->get_string ());
+}
+
+using namespace jsonrpc;
+
+namespace {
+
+/* A concrete subclass of jsonrpc::server for use by the selftests.  */
+
+class test_server : public jsonrpc::server
+{
+ public:
+  test_server (bool verbose) : server (verbose) {}
+
+  json::value *
+  dispatch (const char *method, const json::value *params,
+	    const json::value *id) FINAL OVERRIDE
+  {
+    if (0 == strcmp (method, "subtract"))
+      return do_subtract (params, id);
+    return make_method_not_found (id, method);
+  }
+
+  json::value *
+  do_subtract (const json::value *params, const json::value *id)
+  {
+    const json::array *positional = params->as_array ();
+    if (!positional)
+      return make_invalid_params (id, "params was not an array");
+    if (positional->get_length () != 2)
+      return make_invalid_params (id, "length of params was not 2");
+
+    const json::number *arg0 = positional->get (0)->as_number ();
+    if (!arg0)
+      return make_invalid_params (id, "param 0 was not a number");
+    const json::number *arg1 = positional->get (1)->as_number ();
+    if (!arg1)
+      return make_invalid_params (id, "param 1 was not a number");
+
+    double result = arg0->get () - arg1->get ();
+
+    return make_success (id, new json::number (result));
+  }
+};
+
+} // anonymous namespace
+
+/* Verify that a simple JSON-RPC call (of "subtract") works.  */
+
+static void
+test_simple ()
+{
+  test_server s (false);
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": \"subtract\","
+       " \"params\": [42, 23], \"id\": 1}");
+  json::value *response = s.handle_request_string (request);
+  const json::value *result = assert_is_success (response, 1);
+  ASSERT_EQ (19, result->as_number ()->get ());
+  delete response;
+}
+
+/* Verify that jsonrpc::server::make_invalid_params works.  */
+
+static void
+test_bad_params ()
+{
+  test_server s (false);
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": \"subtract\","
+       " \"params\": [42, \"0\"], \"id\": 1}");
+  json::value *response = s.handle_request_string (request);
+  assert_is_failure (response, INVALID_PARAMS, "param 1 was not a number", 1);
+  delete response;
+}
+
+/* Verify that jsonrpc::server enforces JSON-RPC 2.0.  */
+
+static void
+test_bad_version ()
+{
+  test_server s (false);
+  const char *request
+    = ("{\"jsonrpc\": \"1.0\", \"method\": \"subtract\","
+       " \"params\": [42, 23], \"id\": 1}");
+  json::value *response = s.handle_request_string (request);
+  assert_is_failure (response, INVALID_REQUEST, "jsonrpc was not 2.0", 1);
+  delete response;
+}
+
+/* Verify that jsonrpc::server handles non-string methods.  */
+
+static void
+test_method_not_a_string ()
+{
+  test_server s (false);
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": 1,"
+       " \"params\": \"bar\", \"id\": 1}");
+  json::value *response = s.handle_request_string (request);
+  assert_is_failure (response, INVALID_REQUEST, "method was not a string", 1);
+  delete response;
+}
+
+/* Verify that jsonrpc::server::make_method_not_found works.  */
+
+static void
+test_method_not_found ()
+{
+  test_server s (false);
+  const char *request
+    = ("{\"jsonrpc\": \"2.0\", \"method\": \"does_not_exist\","
+       " \"params\": [42, 23], \"id\": 1}");
+  json::value *response = s.handle_request_string (request);
+  assert_is_failure (response, METHOD_NOT_FOUND,
+    "method not found: does_not_exist", 1);
+  delete response;
+}
+
+/* Verify that jsonrpc::server::handle_request_string gracefully
+   handles malformed JSON.  */
+
+static void
+test_malformed_json ()
+{
+  test_server s (false);
+  const char *request = "{";
+  json::value *response = s.handle_request_string (request);
+  assert_is_failure (response, PARSE_ERROR,
+    "error at index 1: expected string for object key");
+  delete response;
+}
+
+/* Verify that jsonrpc::http_server can handle JSON-RPC within HTTP,
+   delegating to a jsonrpc::server.  */
+
+static void
+test_http_server ()
+{
+  http::request req;
+  const char *in = ("POST /jsonrpc HTTP/1.1\r\n"
+		    "Content-Length: 69\r\n"
+		    "\r\n"
+		    "{\"jsonrpc\": \"2.0\", \"method\": \"subtract\","
+		    " \"params\": [42, 23], \"id\": 1}");
+  size_t consumed = req.parse_buffer (strlen (in), in);
+  ASSERT_EQ (strlen (in), consumed);
+
+  test_server s (false);
+  http_server http_server (s);
+  http::response resp;
+  http_server.on_http_request (req, resp);
+  ASSERT_STREQ ("{\"jsonrpc\": \"2.0\", \"id\": 1, \"result\": 19}",
+		resp.get_content ()); // FIXME: length?
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+json_rpc_c_tests ()
+{
+  test_simple ();
+  test_bad_params ();
+  test_bad_version ();
+  test_method_not_a_string ();
+  test_method_not_found ();
+  test_malformed_json ();
+  test_http_server ();
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/json-rpc.h b/gcc/json-rpc.h
new file mode 100644
index 0000000..85f7370
--- /dev/null
+++ b/gcc/json-rpc.h
@@ -0,0 +1,94 @@ 
+/* JSON-RPC.
+   Copyright (C) 2017 Free Software Foundation, Inc.
+
+This file is part of GCC.
+
+GCC 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 3, or (at your option) any later
+version.
+
+GCC 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 GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_JSON_RPC_H
+#define GCC_JSON_RPC_H
+
+namespace jsonrpc
+{
+
+/* jsonrpc::server is an abstract base class for implementing
+   a JSON-RPC 2.0 server.  */
+
+class server
+{
+ public:
+  server (bool verbose) : m_verbose (verbose) {}
+  virtual ~server () {}
+
+  json::value *handle_request (json::value *);
+  json::value *handle_request (size_t sz, const char *buf);
+  json::value *handle_request_string (const char *utf8);
+
+  virtual json::value *dispatch (const char *method,
+				 const json::value *params,
+				 const json::value *id) = 0;
+
+ private:
+  json::value *handle_request_1 (json::value *);
+
+  bool m_verbose;
+};
+
+/* The standard JSON-RPC error codes.  */
+
+const int PARSE_ERROR = -32700;
+const int INVALID_REQUEST = -32600;
+const int METHOD_NOT_FOUND = -32601;
+const int INVALID_PARAMS = -32602;
+const int INTERNAL_ERROR = -32603;
+
+/* Helper functions for creating responses.  */
+
+extern json::object *make_response (const json::value *id);
+extern json::object *make_failure (int code, const char *message,
+				   const json::value *id);
+
+extern json::value *make_parse_error (const char *msg);
+
+extern json::value *make_invalid_request (const json::value *id);
+
+extern json::value *make_method_not_found (const json::value *id,
+					   const char *method);
+
+extern json::value *make_invalid_params (const json::value *id,
+					 const char *msg);
+
+extern json::value *make_success (const json::value *id, json::value *result);
+
+
+/* A subclass of ::http::server that handles HTTP requests
+   by parsing the content as JSON-RPC, and delegating them to
+   a ::jsonrpc::server.  */
+
+class http_server : public ::http::server
+{
+ public:
+  http_server (::jsonrpc::server &json_handler);
+
+  void on_http_request (const http::request &http_request,
+			http::response &http_response) FINAL OVERRIDE;
+
+ private:
+  ::jsonrpc::server &m_json_handler;
+};
+
+} // namespace jsonrpc
+
+#endif  /* GCC_JSON_RPC_H  */
diff --git a/gcc/selftest-run-tests.c b/gcc/selftest-run-tests.c
index 7e75680..35ab965 100644
--- a/gcc/selftest-run-tests.c
+++ b/gcc/selftest-run-tests.c
@@ -69,6 +69,7 @@  selftest::run_tests ()
   blt_c_tests ();
   json_c_tests ();
   http_server_c_tests ();
+  json_rpc_c_tests ();
 
   /* Mid-level data structures.  */
   input_c_tests ();
diff --git a/gcc/selftest.h b/gcc/selftest.h
index ad4e957..2312fb2 100644
--- a/gcc/selftest.h
+++ b/gcc/selftest.h
@@ -186,6 +186,7 @@  extern void hash_set_tests_c_tests ();
 extern void http_server_c_tests ();
 extern void input_c_tests ();
 extern void json_c_tests ();
+extern void json_rpc_c_tests ();
 extern void pretty_print_c_tests ();
 extern void read_rtl_function_c_tests ();
 extern void rtl_tests_c_tests ();