summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSadie Powell <sadie@witchery.services>2025-05-04 14:14:19 +0100
committerSadie Powell <sadie@witchery.services>2025-05-04 14:14:19 +0100
commit4b854d39357cbcfa43eb833949d48a49b0420ad6 (patch)
treebdc625b02f2366a64c68ed33826250894e1fa9c0
parent0b2b00b37dd2e713129a04bdae4c5b8aab858126 (diff)
Add support for bearer tokens for authorising with RPC.
-rw-r--r--data/modules.example.conf46
-rw-r--r--docs/RPC/jsonrpc.js9
-rw-r--r--include/modules/rpc.h25
-rw-r--r--modules/extra/xmlrpc.cpp26
-rw-r--r--modules/rpc/jsonrpc.cpp28
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);
}
};