summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSadie Powell <sadie@witchery.services>2025-02-13 04:05:18 +0000
committerSadie Powell <sadie@witchery.services>2025-02-13 04:19:39 +0000
commit9e621cca11ae3cf1d54356a8910d9205fadbf0ee (patch)
tree4790d82665a31765d3299338aa29864e1cfdd3d2
parent1e82b4075e6982d32a6ed5d29ee79ab8fd81fe2e (diff)
Add the new JSON-RPC module to compliment the XMLRPC one.
-rw-r--r--data/modules.example.conf18
-rw-r--r--docs/RPC/RPC (renamed from docs/RPC/XMLRPC)13
-rw-r--r--modules/rpc/jsonrpc.cpp206
3 files changed, 231 insertions, 6 deletions
diff --git a/data/modules.example.conf b/data/modules.example.conf
index 604f05c43..bfc1643ac 100644
--- a/data/modules.example.conf
+++ b/data/modules.example.conf
@@ -786,10 +786,28 @@ module { name = "sasl" }
}
/*
+ * jsonrpc
+ *
+ * Allows remote applications (websites) to execute queries in real time to retrieve data from Anope.
+ * By itself this module does nothing, but allows other modules (rpc_main) to receive and send JSON-RPC queries.
+ *
+ * IMPORTANT: this can not be loaded at the same time as the xmlrpc module.
+ */
+#module
+{
+ name = "jsonrpc"
+
+ /* Web service to use. Requires httpd. */
+ server = "httpd/main"
+}
+
+/*
* xmlrpc
*
* Allows remote applications (websites) to execute queries in real time to retrieve data from Anope.
* By itself this module does nothing, but allows other modules (rpc_main) to receive and send XMLRPC queries.
+ *
+ * IMPORTANT: this can not be loaded at the same time as the jsonrpc module.
*/
#module
{
diff --git a/docs/RPC/XMLRPC b/docs/RPC/RPC
index ac3f1059b..a93d61af8 100644
--- a/docs/RPC/XMLRPC
+++ b/docs/RPC/RPC
@@ -1,14 +1,15 @@
-XMLRPC using PHP's xmlrpc_encode_request and xmlrpc_decode functions is supported.
+RPC using JSON-RPC and XML-RPC (using PECL's xmlrpc_encode_request and xmlrpc_decode functions) is supported.
+
This allows external applications, such as websites, to execute remote procedure calls to Anope in real time.
-Currently there are 5 supported XMLRPC calls, provided by rpc_main:
+Currently there are 5 supported RPC calls, provided by rpc_main:
checkAuthentication - Takes two parameters, an account name and a password. Checks if the account name is valid and the password
is correct for the account name, useful for making login pages on websites.
command - Takes three parameters, a service name (BotServ, ChanServ, NickServ), a user name (whether online or not), and the command
to execute. This will execute the given command to Anope using the given service name. If the user given is online, the
- command reply will go to them, if not it is returned by XMLRPC.
+ command reply will go to them, if not it is returned by RPC.
stats - Takes no parameters, returns miscellaneous stats that can be found in the /operserv stats command.
@@ -21,9 +22,9 @@ opers - Takes no parameters, returns opertypes, their privileges and commands.
notice - Takes three parameters, source user, target user, and message. Sends a message to the user.
-XMLRPC was designed to be used with db_sql, and will not return any information that can be pulled from the SQL
+RPC was designed to be used with db_sql, and will not return any information that can be pulled from the SQL
database, such as accounts and registered channel information. It is instead used for pulling realtime data such
-as users and channels currently online. For examples on how to use these calls in PHP, see xmlrpc.php in docs/XMLRPC.
+as users and channels currently online. For examples on how to use these calls in PHP, see xmlrpc.php in docs/RPC.
-Also note that the parameter named "id" is reserved for query ID. If you pass a query to Anope containing a value for id. it will
+Also note that when using XMLRPC the parameter named "id" is reserved for query ID. If you pass a query to Anope containing a value for id. it will
be stored by Anope and the same id will be passed back in the result.
diff --git a/modules/rpc/jsonrpc.cpp b/modules/rpc/jsonrpc.cpp
new file mode 100644
index 000000000..0afc0c8e3
--- /dev/null
+++ b/modules/rpc/jsonrpc.cpp
@@ -0,0 +1,206 @@
+/*
+ *
+ * (C) 2010-2025 Anope Team
+ * Contact us at team@anope.org
+ *
+ * Please read COPYING and README for further details.
+ */
+
+#include "module.h"
+#include "modules/rpc.h"
+#include "modules/httpd.h"
+
+#include "yyjson/yyjson.c"
+
+inline Anope::string yyjson_get_astr(yyjson_val *val, const char *key)
+{
+ const auto *str = yyjson_get_str(yyjson_obj_get(val, key));
+ return str ? str : "";
+}
+
+class MyJSONRPCServiceInterface final
+ : public RPCServiceInterface
+ , public HTTPPage
+{
+private:
+ std::deque<RPCEvent *> events;
+
+ void SendError(HTTPReply &reply, int64_t code, const char *message, const Anope::string &id)
+ {
+ Log(LOG_DEBUG) << "JSON-RPC error " << code << ": " << message;
+
+ // {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
+ auto* doc = yyjson_mut_doc_new(nullptr);
+
+ auto* root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+
+ auto *error = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_sint(doc, error, "code", code);
+ yyjson_mut_obj_add_str(doc, error, "message", message);
+
+ yyjson_mut_obj_add_val(doc, root, "error", error);
+ yyjson_mut_obj_add_str(doc, root, "jsonrpc", "2.0");
+
+ if (id.empty())
+ yyjson_mut_obj_add_null(doc, root, "id");
+ else
+ yyjson_mut_obj_add_strn(doc, root, "id", id.c_str(), id.length());
+
+ auto *json = yyjson_mut_write(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE | YYJSON_WRITE_NEWLINE_AT_END, nullptr);
+ if (json)
+ {
+ reply.Write(json);
+ free(json);
+ }
+ yyjson_mut_doc_free(doc);
+ }
+
+public:
+ MyJSONRPCServiceInterface(Module *creator, const Anope::string &sname)
+ : RPCServiceInterface(creator, sname)
+ , HTTPPage("/jsonrpc", "application/json")
+ {
+ }
+
+ void Register(RPCEvent *event) override
+ {
+ this->events.push_back(event);
+ }
+
+ void Unregister(RPCEvent *event) override
+ {
+ auto it = std::find(this->events.begin(), this->events.end(), event);
+ if (it != this->events.end())
+ this->events.erase(it);
+ }
+
+ bool OnRequest(HTTPProvider *provider, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) override
+ {
+ auto *doc = yyjson_read(message.content.c_str(), message.content.length(), YYJSON_READ_ALLOW_TRAILING_COMMAS | YYJSON_READ_ALLOW_INVALID_UNICODE);
+ if (!doc)
+ {
+ SendError(reply, -32700, "JSON parse error", "");
+ return true;
+ }
+
+ auto *root = yyjson_doc_get_root(doc);
+ if (!yyjson_is_obj(root))
+ {
+ // TODO: handle an array of JSON-RPC requests
+ yyjson_doc_free(doc);
+ SendError(reply, -32600, "Wrong JSON root element", "");
+ return true;
+ }
+
+ const auto id = yyjson_get_astr(root, "id");
+ const auto jsonrpc = yyjson_get_astr(root, "jsonrpc");
+ if (!jsonrpc.empty() && jsonrpc != "2.0")
+ {
+ yyjson_doc_free(doc);
+ SendError(reply, -32600, "Unsupported JSON-RPC version", id);
+ return true;
+ }
+
+ RPCRequest request(reply);
+ request.id = id;
+ request.name = yyjson_get_astr(root, "method");
+
+ auto *params = yyjson_obj_get(root, "params");
+ size_t idx, max;
+ yyjson_val *val;
+ yyjson_arr_foreach(params, idx, max, val)
+ {
+ const auto *str = yyjson_get_str(val);
+ request.data.push_back(str ? str : "");
+ }
+
+ Log() << "id " << request.id;
+ Log() << "name " << request.name;
+
+ yyjson_doc_free(doc);
+
+ for (auto *e : this->events)
+ {
+ if (!e->Run(this, client, request))
+ return false;
+
+ else if (!request.get_replies().empty())
+ {
+ this->Reply(request);
+ return true;
+ }
+ }
+
+ // If we reached this point nobody handled the event.
+ SendError(reply, -32601, "Method not found", id);
+ return true;
+ }
+
+ void Reply(RPCRequest &request) override
+ {
+ // {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
+ auto* doc = yyjson_mut_doc_new(nullptr);
+
+ auto* root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+
+ if (request.id.empty())
+ yyjson_mut_obj_add_null(doc, root, "id");
+ else
+ yyjson_mut_obj_add_strn(doc, root, "id", request.id.c_str(), request.id.length());
+
+ if (!request.get_replies().empty())
+ {
+ auto *result = yyjson_mut_obj(doc);
+ for (const auto &[k, v] : request.get_replies())
+ yyjson_mut_obj_add_strn(doc, result, k.c_str(), v.c_str(), v.length());
+ yyjson_mut_obj_add_val(doc, root, "result", result);
+ }
+
+ yyjson_mut_obj_add_str(doc, root, "jsonrpc", "2.0");
+
+ auto *json = yyjson_mut_write(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE | YYJSON_WRITE_NEWLINE_AT_END, nullptr);
+ if (json)
+ {
+ request.r.Write(json);
+ free(json);
+ }
+ yyjson_mut_doc_free(doc);
+ }
+};
+
+class ModuleJSONRPC final
+ : public Module
+{
+private:
+ ServiceReference<HTTPProvider> httpref;
+ MyJSONRPCServiceInterface jsonrpcinterface;
+
+public:
+ ModuleJSONRPC(const Anope::string &modname, const Anope::string &creator)
+ : Module(modname, creator, EXTRA | VENDOR)
+ , jsonrpcinterface(this, "rpc")
+ {
+ }
+
+ ~ModuleJSONRPC() override
+ {
+ if (httpref)
+ httpref->UnregisterPage(&jsonrpcinterface);
+ }
+
+ void OnReload(Configuration::Conf *conf) override
+ {
+ if (httpref)
+ httpref->UnregisterPage(&jsonrpcinterface);
+
+ this->httpref = ServiceReference<HTTPProvider>("HTTPProvider", conf->GetModule(this)->Get<const Anope::string>("server", "httpd/main"));
+ if (!httpref)
+ throw ConfigException("Unable to find http reference, is httpd loaded?");
+
+ httpref->RegisterPage(&jsonrpcinterface);
+ }
+};
+
+MODULE_INIT(ModuleJSONRPC)