diff options
author | Sadie Powell <sadie@witchery.services> | 2025-05-04 14:14:19 +0100 |
---|---|---|
committer | Sadie Powell <sadie@witchery.services> | 2025-05-04 14:14:19 +0100 |
commit | 4b854d39357cbcfa43eb833949d48a49b0420ad6 (patch) | |
tree | bdc625b02f2366a64c68ed33826250894e1fa9c0 | |
parent | 0b2b00b37dd2e713129a04bdae4c5b8aab858126 (diff) |
Add support for bearer tokens for authorising with RPC.
-rw-r--r-- | data/modules.example.conf | 46 | ||||
-rw-r--r-- | docs/RPC/jsonrpc.js | 9 | ||||
-rw-r--r-- | include/modules/rpc.h | 25 | ||||
-rw-r--r-- | modules/extra/xmlrpc.cpp | 26 | ||||
-rw-r--r-- | modules/rpc/jsonrpc.cpp | 28 |
5 files changed, 124 insertions, 10 deletions
diff --git a/data/modules.example.conf b/data/modules.example.conf index a017e54fe..bb9e6f57a 100644 --- a/data/modules.example.conf +++ b/data/modules.example.conf @@ -672,7 +672,7 @@ module /* * If your database uses a password hashing algorithm that can not be compared using a simple - * comparison function then you can specify it here to compare locally. + * comparison method then you can specify it here to compare locally. * * You will need to have the appropriate encryption module (e.g. enc_bcrypt) loaded in order * for this to work. @@ -795,8 +795,11 @@ module /* * 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. + * Allows remote applications to execute methods within Anope using the JSON-RPC + * protocol. See https://www.jsonrpc.org/specification for more information. + * + * By itself this module does nothing. You should load a RPC method module like + * rpc_data which actually provides RPC methods. * * IMPORTANT: this can not be loaded at the same time as the xmlrpc module. */ @@ -806,13 +809,30 @@ module /* Web service to use. Requires httpd. */ server = "httpd/main" + + /* + * You can also specify one or more authorization tokens to protect access + * to the JSON-RPC interface. These tokens should be sent using the Bearer + * authorization header as defined in RFC 6750. + */ + #token + { + /* The token used for authentication. */ + token = "BmcxTaiYjoBtayfnxCFq" + + /** A list of glob patterns for methods the token can execute. */ + methods = "~anope.message* anope.*" + } } /* * [EXTRA] 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. + * Allows remote applications to execute methods within Anope using the XML-RPC + * protocol. See https://xmlrpc.com/spec.md for more information. + * + * By itself this module does nothing. You should load a RPC method module like + * rpc_data which actually provides RPC methods. * * IMPORTANT: this can not be loaded at the same time as the jsonrpc module. */ @@ -836,6 +856,20 @@ module */ #enable_i8 = no #enable_nil = no + + /* + * You can also specify one or more authorization tokens to protect access + * to the XML-RPC interface. These tokens should be sent using the Bearer + * authorization header as defined in RFC 6750. + */ + #token + { + /* The token used for authentication. */ + token = "BmcxTaiYjoBtayfnxCFq" + + /** A list of glob patterns for methods the token can execute. */ + methods = "~anope.message* anope.*" + } } /* @@ -858,7 +892,7 @@ module /* * rpc_main * - * Adds the main RPC core functions. + * Adds the main RPC core methods. * Requires xmlrpc. */ #module { name = "rpc_main" } diff --git a/docs/RPC/jsonrpc.js b/docs/RPC/jsonrpc.js index 8f15a4ee8..76c284553 100644 --- a/docs/RPC/jsonrpc.js +++ b/docs/RPC/jsonrpc.js @@ -8,9 +8,11 @@ class AnopeRPC { * Initializes a new AnopeRPC instance with the specified RPC host. * * @param {string} The RPC host base URL. + * @param {token} The bearer token for authorizing with the RPC token. */ - constructor(host) { + constructor(host, token = "") { this.host = host; + this.token = token; } /** @@ -27,8 +29,13 @@ class AnopeRPC { "params": params, "id": Math.random().toString(36).slice(2) }); + const headers = new Headers(); + if (this.token) { + headers.append("Authorization", `Bearer ${btoa(this.token)}`); + } const response = await fetch(this.host, { method: 'POST', + headers: headers, body: request }); if (!response.ok) { diff --git a/include/modules/rpc.h b/include/modules/rpc.h index 99be7c076..41a95fc14 100644 --- a/include/modules/rpc.h +++ b/include/modules/rpc.h @@ -193,11 +193,36 @@ class RPC::ServiceInterface : public Service { public: + Anope::map<std::vector<Anope::string>> tokens; + ServiceInterface(Module *creator) : Service(creator, "RPC::ServiceInterface", "rpc") { } + bool CanExecute(const Anope::string &header, const Anope::string &method) const + { + if (header.compare(0, 7, "Bearer ", 7) != 0) + return false; // No token provided. + + Anope::string token; + Anope::B64Decode(header.substr(7), token); + + auto it = tokens.find(token); + if (it == tokens.end()) + return false; // No valid token. + + for (const auto &glob : it->second) + { + if (glob[0] == '~' && Anope::Match(method, glob.substr(1))) + return false; // Negative match. + + if (Anope::Match(method, glob)) + return true; // Positive match. + } + return false; // No match. + } + virtual void Reply(Request &request) = 0; }; diff --git a/modules/extra/xmlrpc.cpp b/modules/extra/xmlrpc.cpp index de23f0dc1..24f5f5f0e 100644 --- a/modules/extra/xmlrpc.cpp +++ b/modules/extra/xmlrpc.cpp @@ -93,6 +93,17 @@ public: request.name = method; delete method; + if (!tokens.empty()) + { + auto it = message.headers.find("Authorization"); + if (it == message.headers.end() || !CanExecute(it->second, request.name)) + { + xmlrpc_env_set_fault(&env, RPC::ERR_METHOD_NOT_FOUND, "No authorization for method"); + SendError(reply, env); + return true; + } + } + ServiceReference<RPC::Event> event(RPC_EVENT, request.name); if (!event) { @@ -308,8 +319,21 @@ public: this->httpref = ServiceReference<HTTPProvider>("HTTPProvider", modconf.Get<const Anope::string>("server", "httpd/main")); if (!httpref) throw ConfigException("Unable to find http reference, is httpd loaded?"); - httpref->RegisterPage(&xmlrpcinterface); + xmlrpcinterface.tokens.clear(); + for (int i = 0; i < modconf.CountBlock("token"); ++i) + { + const auto &block = modconf.GetBlock("token", i); + const auto &token = block.Get<const Anope::string>("token"); + if (!token.empty()) + { + std::vector<Anope::string> methods; + spacesepstream(block.Get<const Anope::string>("methods")).GetTokens(methods); + xmlrpcinterface.tokens.emplace(token, methods); + } + } + + httpref->RegisterPage(&xmlrpcinterface); } }; diff --git a/modules/rpc/jsonrpc.cpp b/modules/rpc/jsonrpc.cpp index cb6835d47..8069ed8eb 100644 --- a/modules/rpc/jsonrpc.cpp +++ b/modules/rpc/jsonrpc.cpp @@ -99,7 +99,7 @@ public: auto *doc = yyjson_read_opts(const_cast<char *>(message.content.c_str()), message.content.length(), flags, nullptr, &error); if (!doc) { - SendError(reply, RPC::ERR_PARSE_ERROR, Anope::printf("JSON parse error #%u: %s", error.code, error.msg)); + SendError(reply, RPC::ERR_PARSE_ERROR, Anope::printf("JSON parse error #%u: %s", error.code, error.msg)); return true; } @@ -131,6 +131,16 @@ public: return true; } + if (!tokens.empty()) + { + auto it = message.headers.find("Authorization"); + if (it == message.headers.end() || !CanExecute(it->second, request.name)) + { + SendError(reply, RPC::ERR_METHOD_NOT_FOUND, "No authorization for method: " + request.name, id); + return true; + } + } + auto *params = yyjson_obj_get(root, "params"); size_t idx, max; yyjson_val *val; @@ -262,10 +272,24 @@ public: if (httpref) httpref->UnregisterPage(&jsonrpcinterface); - this->httpref = ServiceReference<HTTPProvider>("HTTPProvider", conf.GetModule(this).Get<const Anope::string>("server", "httpd/main")); + const auto &modconf = conf.GetModule(this); + this->httpref = ServiceReference<HTTPProvider>("HTTPProvider", modconf.Get<const Anope::string>("server", "httpd/main")); if (!httpref) throw ConfigException("Unable to find http reference, is httpd loaded?"); + jsonrpcinterface.tokens.clear(); + for (int i = 0; i < modconf.CountBlock("token"); ++i) + { + const auto &block = modconf.GetBlock("token", i); + const auto &token = block.Get<const Anope::string>("token"); + if (!token.empty()) + { + std::vector<Anope::string> methods; + spacesepstream(block.Get<const Anope::string>("methods")).GetTokens(methods); + jsonrpcinterface.tokens.emplace(token, methods); + } + } + httpref->RegisterPage(&jsonrpcinterface); } }; |