From e12fe9e2b4eb0a2d356abe8276c9e7ac209a719d Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Tue, 29 Sep 2020 10:47:07 +0200 Subject: [PATCH 1/5] Add simple REST server. Extend tauriond so that it can run a REST server. The server has just one endpoint (at least for now), /bootstrap.json.gz, which returns the getbootstrapdata JSON (as per the RPC method) with gzip compression. --- configure.ac | 1 + src/Makefile.am | 6 ++++-- src/main.cpp | 18 ++++++++++++++++++ src/rest.cpp | 31 +++++++++++++++++++++++++++++++ src/rest.hpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/rest.cpp create mode 100644 src/rest.hpp diff --git a/configure.ac b/configure.ac index 45eb8e8c..17bbebf3 100644 --- a/configure.ac +++ b/configure.ac @@ -47,6 +47,7 @@ PKG_CHECK_MODULES([XAYAGAME], [libxayautil libxayagame]) PKG_CHECK_MODULES([CHARON], [charon]) PKG_CHECK_MODULES([SQLITE3], [sqlite3]) PKG_CHECK_MODULES([JSON], [jsoncpp]) +PKG_CHECK_MODULES([MHD], [libmicrohttpd]) PKG_CHECK_MODULES([GLOG], [libglog]) PKG_CHECK_MODULES([GFLAGS], [gflags]) PKG_CHECK_MODULES([GTEST], [gmock gtest]) diff --git a/src/Makefile.am b/src/Makefile.am index 31e09a05..bb663df9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -74,22 +74,24 @@ libtaurionheaders = \ tauriond_CXXFLAGS = \ -I$(top_srcdir) \ $(XAYAGAME_CFLAGS) $(CHARON_CFLAGS) \ - $(JSON_CFLAGS) \ + $(JSON_CFLAGS) $(MHD_CLFGAS) \ $(GLOG_CFLAGS) $(GFLAGS_CFLAGS) $(PROTOBUF_CFLAGS) tauriond_LDADD = \ $(builddir)/libtaurion.la \ $(top_builddir)/mapdata/libmapdata.la \ $(top_builddir)/database/libdatabase.la \ $(XAYAGAME_LIBS) $(CHARON_LIBS) \ - $(JSON_LIBS) \ + $(JSON_LIBS) $(MHD_LIBS) \ $(GLOG_LIBS) $(GFLAGS_LIBS) $(PROTOBUF_LIBS) tauriond_SOURCES = main.cpp \ charon.cpp \ pxrpcserver.cpp \ + rest.cpp \ version.cpp tauriondheaders = \ charon.hpp \ pxrpcserver.hpp \ + rest.hpp \ version.hpp \ \ rpc-stubs/nonstaterpcserverstub.h \ diff --git a/src/main.cpp b/src/main.cpp index 0fbae3da..d97397c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,7 @@ #include "logic.hpp" #include "pending.hpp" #include "pxrpcserver.hpp" +#include "rest.hpp" #include "version.hpp" #include @@ -49,6 +50,9 @@ DEFINE_int32 (game_rpc_port, 0, DEFINE_bool (game_rpc_listen_locally, true, "whether the game's JSON-RPC server should listen locally"); +DEFINE_int32 (rest_port, 0, + "if non-zero, the port at which the REST interface should run"); + DEFINE_int32 (enable_pruning, -1, "if non-negative (including zero), old undo data will be pruned" " and only as many blocks as specified will be kept"); @@ -71,12 +75,21 @@ class PXInstanceFactory : public xaya::CustomisedInstanceFactory */ pxd::PXLogic& rules; + /** The REST API port. */ + int restPort = 0; + public: explicit PXInstanceFactory (pxd::PXLogic& r) : rules(r) {} + void + EnableRest (const int p) + { + restPort = p; + } + std::unique_ptr BuildRpcServer (xaya::Game& game, jsonrpc::AbstractServerConnector& conn) override @@ -96,6 +109,9 @@ class PXInstanceFactory : public xaya::CustomisedInstanceFactory if (charonSrv != nullptr) res.push_back (std::move (charonSrv)); + if (restPort != 0) + res.push_back (std::make_unique (game, rules, restPort)); + return res; } @@ -175,6 +191,8 @@ main (int argc, char** argv) pxd::PXLogic rules; PXInstanceFactory instanceFact(rules); + if (FLAGS_rest_port != 0) + instanceFact.EnableRest (FLAGS_rest_port); config.InstanceFactory = &instanceFact; pxd::PendingMoves pending(rules); diff --git a/src/rest.cpp b/src/rest.cpp new file mode 100644 index 00000000..5bc6feb6 --- /dev/null +++ b/src/rest.cpp @@ -0,0 +1,31 @@ +// Copyright (C) 2020 The Xaya developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "rest.hpp" + +#include "gamestatejson.hpp" + +#include + +namespace pxd +{ + +RestApi::SuccessResult +RestApi::Process (const std::string& url) +{ + std::string remainder; + if (MatchEndpoint (url, "/bootstrap.json.gz", remainder) && remainder == "") + { + const Json::Value res = logic.GetCustomStateData (game, + [] (GameStateJson& gsj) + { + return gsj.BootstrapData (); + }); + return SuccessResult (res).Gzip (); + } + + throw HttpError (MHD_HTTP_NOT_FOUND, "invalid API endpoint"); +} + +} // namespace pxd diff --git a/src/rest.hpp b/src/rest.hpp new file mode 100644 index 00000000..0cc6db07 --- /dev/null +++ b/src/rest.hpp @@ -0,0 +1,44 @@ +// Copyright (C) 2020 The Xaya developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef PXD_REST_HPP +#define PXD_REST_HPP + +#include "logic.hpp" + +#include +#include + +namespace pxd +{ + +/** + * HTTP server providing a REST API for tauriond. + */ +class RestApi : public xaya::RestApi +{ + +private: + + /** The underlying Game instance that manages everything. */ + xaya::Game& game; + + /** The game logic implementation. */ + PXLogic& logic; + +protected: + + SuccessResult Process (const std::string& url) override; + +public: + + explicit RestApi (xaya::Game& g, PXLogic& l, const int p) + : xaya::RestApi(p), game(g), logic(l) + {} + +}; + +} // namespace pxd + +#endif // PXD_REST_HPP From 1089d83fe684125a3a26a97cac21048f85fc5682 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Tue, 29 Sep 2020 11:38:19 +0200 Subject: [PATCH 2/5] Cache bootstrap data for REST API. Keep a copy of the bootstrap data for the REST API in memory, and only refresh it with a certain interval. (If we are still catching up, no cache is kept.) --- src/rest.cpp | 94 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/rest.hpp | 36 ++++++++++++++++++++ 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/src/rest.cpp b/src/rest.cpp index 5bc6feb6..f4003025 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -8,24 +8,106 @@ #include +#include +#include + +#include + namespace pxd { +namespace +{ + +DEFINE_int32 (rest_bootstrap_refresh_seconds, 60 * 60, + "the refresh interval for bootstrap data in seconds"); + +} // anonymous namespace + +std::shared_ptr +RestApi::ComputeBootstrapData () +{ + const Json::Value val = logic.GetCustomStateData (game, + [] (GameStateJson& gsj) + { + return gsj.BootstrapData (); + }); + auto res = std::make_shared (SuccessResult (val).Gzip ()); + + if (val["state"].asString () == "up-to-date") + { + LOG (INFO) << "Refreshing bootstrap-data cache"; + std::lock_guard lock(mutBootstrap); + bootstrapData = res; + } + else + LOG (WARNING) << "We are still catching up, not caching bootstrap data"; + + return res; +} + RestApi::SuccessResult RestApi::Process (const std::string& url) { std::string remainder; if (MatchEndpoint (url, "/bootstrap.json.gz", remainder) && remainder == "") { - const Json::Value res = logic.GetCustomStateData (game, - [] (GameStateJson& gsj) - { - return gsj.BootstrapData (); - }); - return SuccessResult (res).Gzip (); + std::shared_ptr res; + { + std::lock_guard lock(mutBootstrap); + res = bootstrapData; + } + if (res == nullptr) + res = ComputeBootstrapData (); + CHECK (res != nullptr); + return *res; } throw HttpError (MHD_HTTP_NOT_FOUND, "invalid API endpoint"); } +void +RestApi::Start () +{ + xaya::RestApi::Start (); + + std::lock_guard lock(mutStop); + shouldStop = false; + CHECK (bootstrapRefresher == nullptr); + bootstrapRefresher = std::make_unique ([this] () + { + const auto intv + = std::chrono::seconds (FLAGS_rest_bootstrap_refresh_seconds); + while (true) + { + ComputeBootstrapData (); + + std::unique_lock lock(mutStop); + if (shouldStop) + break; + cvStop.wait_for (lock, intv); + if (shouldStop) + break; + } + }); +} + +void +RestApi::Stop () +{ + { + std::lock_guard lock(mutStop); + shouldStop = true; + cvStop.notify_all (); + } + + if (bootstrapRefresher != nullptr) + { + bootstrapRefresher->join (); + bootstrapRefresher.reset (); + } + + xaya::RestApi::Stop (); +} + } // namespace pxd diff --git a/src/rest.hpp b/src/rest.hpp index 0cc6db07..228fbad6 100644 --- a/src/rest.hpp +++ b/src/rest.hpp @@ -10,6 +10,11 @@ #include #include +#include +#include +#include +#include + namespace pxd { @@ -27,6 +32,34 @@ class RestApi : public xaya::RestApi /** The game logic implementation. */ PXLogic& logic; + /** + * The current bootstrap data payload, if we have one cached. This is a + * shared pointer, so that we can create copies quickly (while holding the + * lock) and then release it again while we send the result to a client. + */ + std::shared_ptr bootstrapData; + + /** Lock for the bootstrap data cache. */ + std::mutex mutBootstrap; + + /** Set to true if we should stop. */ + bool shouldStop; + + /** Condition variable that is signaled if shouldStop is set. */ + std::condition_variable cvStop; + + /** Mutex for the stop flag and condition variable. */ + std::mutex mutStop; + + /** Thread running the bootstrap data update. */ + std::unique_ptr bootstrapRefresher; + + /** + * Computes the bootstrap data and returns it. This may fill in the + * cache (if we are up-to-date), but does not use an existing cache. + */ + std::shared_ptr ComputeBootstrapData (); + protected: SuccessResult Process (const std::string& url) override; @@ -37,6 +70,9 @@ class RestApi : public xaya::RestApi : xaya::RestApi(p), game(g), logic(l) {} + void Start () override; + void Stop () override; + }; } // namespace pxd From fc4bc537fdaa8f48a7f2f98895c35d8ba850c820 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Wed, 30 Sep 2020 11:48:23 +0200 Subject: [PATCH 3/5] Use REST API for getbootstrapdata. Instead of passing getbootstrapdata through Charon, use the new REST API to get the data from tauriond in client mode. --- src/charon.cpp | 39 ++++++++++++++++++++++++++++++++------- src/rest.cpp | 13 +++++++++++++ src/rest.hpp | 18 ++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/charon.cpp b/src/charon.cpp index 428ba27a..9f02105d 100644 --- a/src/charon.cpp +++ b/src/charon.cpp @@ -21,6 +21,7 @@ #include "config.h" #include "pxrpcserver.hpp" +#include "rest.hpp" #include #include @@ -62,6 +63,12 @@ DEFINE_int32 (charon_timeout_ms, 3000, "Timeout in ms that the Charon client will wait" " for a server response"); +DEFINE_string (rest_endpoint, "https://rest.taurion.io", + "URL for the REST API that is used in the Charon client"); +DEFINE_string (cafile, "", + "if set, trust these certificates for TLS" + " instead of the cURL default"); + /** Interval for Charon server reconnects. */ const auto RECONNECT_INTERVAL = std::chrono::seconds (5); @@ -115,12 +122,6 @@ const std::map CHARON_METHODS = { {"getserviceinfo", &PXRpcServer::getserviceinfoI}, {"getversion", &PXRpcServer::getversionI}, - - /* FIXME: Instead of handling that through Charon, use an HTTP server to - download the bootstrap data. - - See also https://github.com/xaya/taurion_gsp/issues/162. */ - {"getbootstrapdata", &PXRpcServer::getbootstrapdataI}, }; /** @@ -377,6 +378,9 @@ class RealCharonClient : public CharonClient /** The Charon client. */ charon::Client client; + /** The REST client. */ + RestClient rest; + /** The RPC server, if one has been started / set up. */ std::unique_ptr rpc; @@ -394,11 +398,15 @@ class RealCharonClient : public CharonClient explicit RealCharonClient (const std::string& serverJid, const std::string& clientJid, const std::string& password) - : client(serverJid, GetBackendVersion (), clientJid, password) + : client(serverJid, GetBackendVersion (), clientJid, password), + rest(FLAGS_rest_endpoint) { LOG (INFO) << "Using " << serverJid << " as Charon server," << " requiring backend version " << GetBackendVersion (); + LOG (INFO) + << "REST endpoint: " << FLAGS_rest_endpoint; + rest.SetCaFile (FLAGS_cafile); } /** @@ -435,6 +443,8 @@ RealCharonClient::RpcServer::RpcServer (RealCharonClient& p, jsonrpc::Procedure stopProc("stop", jsonrpc::PARAMS_BY_POSITION, nullptr); bindAndAddNotification (stopProc, &RpcServer::stop); + AddMethod ("getbootstrapdata"); + for (const auto& entry : CHARON_METHODS) AddMethod (entry.first); for (const auto& entry : NONSTATE_METHODS) @@ -451,6 +461,21 @@ RealCharonClient::RpcServer::HandleMethodCall (jsonrpc::Procedure& proc, { const auto& method = proc.GetProcedureName (); + if (method == "getbootstrapdata") + { + VLOG (1) << "Getting bootstrap data through REST..."; + try + { + result = parent.rest.GetBootstrapData (); + return; + } + catch (const std::runtime_error& exc) + { + throw jsonrpc::JsonRpcException ( + jsonrpc::Errors::ERROR_RPC_INTERNAL_ERROR, exc.what ()); + } + } + if (CHARON_METHODS.find (method) != CHARON_METHODS.end ()) { VLOG (1) << "Forwarding method " << method << " through Charon"; diff --git a/src/rest.cpp b/src/rest.cpp index f4003025..897c9581 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -110,4 +110,17 @@ RestApi::Stop () xaya::RestApi::Stop (); } +Json::Value +RestClient::GetBootstrapData () +{ + Request req(*this); + if (!req.Send ("/bootstrap.json.gz")) + throw std::runtime_error (req.GetError ()); + + if (req.GetType () != "application/json") + throw std::runtime_error ("response is not JSON"); + + return req.GetJson (); +} + } // namespace pxd diff --git a/src/rest.hpp b/src/rest.hpp index 228fbad6..482f5ced 100644 --- a/src/rest.hpp +++ b/src/rest.hpp @@ -75,6 +75,24 @@ class RestApi : public xaya::RestApi }; +/** + * REST client for the Taurion API. + */ +class RestClient : public xaya::RestClient +{ + +public: + + using xaya::RestClient::RestClient; + + /** + * Queries for the bootstrap data. May throw a std::runtime_error + * if the request fails. + */ + Json::Value GetBootstrapData (); + +}; + } // namespace pxd #endif // PXD_REST_HPP From 3e1f5107b3241fb11f02d5bab4f91d0d71f6a092 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Wed, 30 Sep 2020 12:49:12 +0200 Subject: [PATCH 4/5] Check in CA file for production. Add in letsencrypt.pem, which holds the --cafile that can be used with the Taurion REST client on the production server (rest.taurion.io). --- Makefile.am | 2 +- configure.ac | 1 + data/Makefile.am | 1 + data/letsencrypt.pem | 31 +++++++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 data/Makefile.am create mode 100644 data/letsencrypt.pem diff --git a/Makefile.am b/Makefile.am index 3f8a8bde..06943144 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,3 +1,3 @@ ACLOCAL_AMFLAGS = ${ACLOCAL_FLAGS} -Im4 -SUBDIRS = hexagonal proto database mapdata src gametest +SUBDIRS = data hexagonal proto database mapdata src gametest diff --git a/configure.ac b/configure.ac index 17bbebf3..c5c8f75f 100644 --- a/configure.ac +++ b/configure.ac @@ -69,6 +69,7 @@ AC_SUBST(GMP_LIBS, -lgmp) AC_CONFIG_FILES([ Makefile \ + data/Makefile \ database/Makefile \ gametest/Makefile \ hexagonal/Makefile \ diff --git a/data/Makefile.am b/data/Makefile.am new file mode 100644 index 00000000..24866c37 --- /dev/null +++ b/data/Makefile.am @@ -0,0 +1 @@ +EXTRA_DIST = letsencrypt.pem diff --git a/data/letsencrypt.pem b/data/letsencrypt.pem new file mode 100644 index 00000000..b85c8037 --- /dev/null +++ b/data/letsencrypt.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- From b3c83b8fb4071fce3c8d0a123cb42439ae460e2c Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Wed, 30 Sep 2020 12:55:33 +0200 Subject: [PATCH 5/5] Integration test REST bootstrap data. Extend the charon.py integration test to also exercise getbootstrapdata via the new REST interface between client and server. --- gametest/charon.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gametest/charon.py b/gametest/charon.py index ab86184a..f510fbe0 100755 --- a/gametest/charon.py +++ b/gametest/charon.py @@ -46,6 +46,11 @@ PUBSUB = "pubsub.chat.xaya.io" +# Port and URL for the local REST API used for bootstrap data. +REST_PORT = 18_042 +REST_URL = "http://localhost:%d" % REST_PORT + + def testAccountJid (acc): return "%s@%s" % (acc[0], XMPP_SERVER) @@ -79,6 +84,7 @@ def __enter__ (self): args = [self.binary] args.extend (["--datadir", self.datadir]) args.append ("--game_rpc_port=%d" % self.rpcport) + args.extend (["--rest_endpoint", REST_URL]) args.extend (["--charon", "client"]) args.extend (["--charon_server_jid", testAccountJid (TEST_ACCOUNTS[0])]) args.extend (["--charon_client_jid", testAccountJid (TEST_ACCOUNTS[1])]) @@ -142,6 +148,7 @@ def run (self): args.extend (["--charon_pubsub_service", PUBSUB]) args.extend (["--charon_server_jid", testAccountJid (TEST_ACCOUNTS[0])]) args.extend (["--charon_password", TEST_ACCOUNTS[0][1]]) + args.extend (["--rest_port", str (REST_PORT)]) self.startGameDaemon (extraArgs=args) self.mainLogger.info ("Starting tauriond as Charon client...") @@ -163,6 +170,10 @@ def run (self): del res["server"] self.assertEqual (res, srv) + self.mainLogger.info ("Testing bootstrap data via REST...") + res = client.rpc.getbootstrapdata () + self.assertEqual (res, self.rpc.game.getbootstrapdata ()) + self.mainLogger.info ("Testing invalid Charon method call...") self.expectError (-32602, ".*Invalid method parameters.*", client.rpc.getregions, 42)