diff options
-rw-r--r-- | data/modules.example.conf | 18 | ||||
-rw-r--r-- | docs/RPC/RPC (renamed from docs/RPC/XMLRPC) | 13 | ||||
-rw-r--r-- | modules/rpc/jsonrpc.cpp | 206 |
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) |