diff --git a/data/modules.example.conf b/data/modules.example.conf index 71ed05ab5..1acbf9b8c 100644 --- a/data/modules.example.conf +++ b/data/modules.example.conf @@ -906,6 +906,29 @@ module } } +/* + * rpc_user + * + * Adds support for the following RPC methods: + * + * anope.checkCredentials anope.identify + * anope.listCommands anope.command + * + * Requires either the jsonrpc or xmlrpc module. + * + * See docs/RPC/rpc_user.md for API documentation. + */ +#module +{ + name = "rpc_user" + + /* + * Some commands can only be executed by a real IRC user. You can work around + * this executing them as an IRC user logged into the account if one exists. + */ + pretenduser = no +} + /* * rpc_data * diff --git a/docs/RPC/jsonrpc.js b/docs/RPC/jsonrpc.js index 935344675..6e5869229 100644 --- a/docs/RPC/jsonrpc.js +++ b/docs/RPC/jsonrpc.js @@ -206,6 +206,58 @@ class AnopeRPC { messageUser(source, target, ...messages) { return this.run("anope.messageUser", source, target, ...messages); } + + /** + * Checks whether the specified credentials are valid. + * + * Requires the rpc_user module to be loaded. + * + * @param {string} account A nickname belonging to the account to check. + * @param {string} password The password for the specified account. + * @returns {object} An object containing basic information about the account. + */ + checkCredentials(account, password) { + return this.run("anope.checkCredentials", account, password); + } + + /** + * Identifies an IRC user to the specified account. + * + * Requires the rpc_user module to be loaded. + * + * @param {string} account Either an account identifier or nickname belonging to the account to + * identify to. + * @param {string} password The nickname of the IRC user to identify to the account. + */ + identify(account, user) { + return this.run("anope.identify", account, user); + } + + /** + * Lists all commands that exist on the network. + * + * Requires the rpc_user module to be loaded. + * + * @param {...*} services The nicknames of the services to list commands for. + * @returns {object} An object containing information about the available commands. + */ + listCommands(...services) { + return this.run("anope.listCommands", ...services); + } + + /** + * Lists all commands that exist on the network. + * + * Requires the rpc_user module to be loaded. + * + * @param {string} account If non-empty then the account to execute the command as. + * @param {string} service The service which the command exists on. + * @param {...*} command The the command to execute and any parameters to pass to it. + * @returns {object} An object containing information about the available commands. + */ + command(account, service, ...command) { + return this.run("anope.command", account, service, ...command); + } } /* diff --git a/docs/RPC/jsonrpc.rb b/docs/RPC/jsonrpc.rb index af8ea0fa7..54a3a6358 100644 --- a/docs/RPC/jsonrpc.rb +++ b/docs/RPC/jsonrpc.rb @@ -186,6 +186,50 @@ class AnopeRPC def message_user(source, target, *messages) self.run("anope.messageUser", source, target, *messages) end + + # Checks whether the specified credentials are valid. + # + # Requires the rpc_user module to be loaded. + # + # @param account [String] A nickname belonging to the account to check. + # @param password [String] The password for the specified account. + # @return [Hash] A hash containing basic information about the account. + def check_credentials(account, password) + self.run("anope.checkCredentials", account, password) + end + + # Identifies an IRC user to the specified account. + # + # Requires the rpc_user module to be loaded. + # + # @param account [String] Either an account identifier or nickname belonging to the account to + # identify to. + # @param password [String] The nickname of the IRC user to identify to the account. + def identify(account, user) + self.run("anope.identify", account, user) + end + + # Lists all commands that exist on the network. + # + # Requires the rpc_user module to be loaded. + # + # @param services [Array] The nicknames of the services to list commands for. + # @return [Hash] A hash containing information about the available commands. + def list_commands(*services) + self.run("anope.listCommands", *services) + end + + # Lists all commands that exist on the network. + # + # Requires the rpc_user module to be loaded. + # + # @param account [String] If non-empty then the account to execute the command as. + # @param service [String] The service which the command exists on. + # @param command [Array] The the command to execute and any parameters to pass to it. + # @return [Hash] A hash containing information about the available commands. + def command(account, service, *command) + self.run("anope.command", account, service, *command) + end end =begin diff --git a/docs/RPC/rpc_user.md b/docs/RPC/rpc_user.md new file mode 100644 index 000000000..c468d15db --- /dev/null +++ b/docs/RPC/rpc_user.md @@ -0,0 +1,175 @@ +# Anope `rpc_user` RPC interface + +## `anope.checkCredentials` + +Checks whether the specified credentials are valid. + +### Parameters + +Index | Description +----- | ----------- +0 | A nickname belonging to the account to check. +1 | The password for the specified account. + +### Errors + +Code | Description +------ | ----------- +-32099 | The specified account does not exist. +-32098 | The specified password is not correct. +-32097 | The specified account is suspended. + +### Result + +Returns a map containing basic information about the account. More information about the account can be found by calling [the `anope.account` event using the value from the `uniqueid` field (requires the rpc_data module)](./rpc_user.md). + +Key | Type | Description +--- | ---- | ----------- +account | string | The display nickname of the account. +confirmed | boolean | Whether the account has been confirmed. +uniqueid | uint | The unique immutable identifier of the account. + +#### Example + +```json +{ + "account": "foo", + "confirmed": true, + "uniqueid": 11085415958920757000, +} +``` + +## `anope.identify` + +Identifies an IRC user to the specified account. + +### Parameters + +Index | Description +----- | ----------- +0 | Either an account identifier or nickname belonging to the account to identify to. +1 | The nickname of the IRC user to identify to the account. + +### Errors + +Code | Description +------ | ----------- +-32099 | The specified account does not exist. +-32098 | The specified IRC user does not exist. + +### Result + +This procedure returns no result. + +## `anope.listCommands` + +Lists all commands that exist on the network. + +### Parameters + +Index | Description +----- | ----------- +0...n | The nicknames of the services to list commands for. If none are specified then all commands are returned. + +### Errors + +Code | Description +------ | ----------- +-32098 | The specified service does not exist. + +### Result + +Returns a map containing information about the available commands. + +Key | Type | Description +--- | ---- | ----------- +\* | map | A key-value map of services to the commands that exist on them. +\*.\* | string | A key-value map of commands to information about the commands. +\*.\*.group | string or null | The group that the command belongs to or null if the command is not grouped. +\*.\*.hidden | boolean | Whether the command is visible in the help output. +\*.\*.maxparams | uint or null | The maximum number of parameters that the command accepts or null if there is no limit. +\*.\*.minparams | uint | The minimum number of parameters that the command accepts. +\*.\*.permission | string or null | The services operator permission required to execute the command or null if no permissions are required. +\*.\*.requiresaccount | boolean | Whether a caller must be logged into an account to execute the command. +\*.\*.requiresuser | boolean | Whether an IRC user is required to execute the command. + +#### Example + +```json +{ + "Global": { + "GLOBAL": { + "group": null, + "hidden": false, + "maxparams": 1, + "minparams": 0, + "permission": "global/global", + "requiresaccount": true, + "requiresuser": false + }, + "HELP": { + "group": null, + "hidden": false, + "maxparams": null, + "minparams": 0, + "permission": null, + "requireaccount": false, + "requireuser": false + }, + "QUEUE": { + "group": null, + "hidden": false, + "maxparams": 2, + "minparams": 1, + "permission": "global/queue", + "requireaccount": true, + "requireuser": false + }, + "SERVER": { + "group": null, + "hidden": false, + "maxparams": 2, + "minparams": 1, + "permission": "global/server", + "requireaccount": true, + "requireuser": false + } + } +} +``` + +## `anope.commands` + +Executes the specified command. + +### Parameters + +Index | Description +----- | ----------- +0 | If non-empty then the account to execute the command as. +1 | The service which the command exists on. +2...n | The the command to execute and any parameters to pass to it. + +### Errors + +Code | Description +------ | ----------- +-32099 | The specified account does not exist. +-32098 | The specified service does not exist. +-32097 | The specified command does not exist. + +### Result + +Returns an array of messages returned by the command. + +#### Example + +```json +[ + "Global commands:", + " GLOBAL Send a message to all users", + " HELP Displays this list and give information about commands", + " QUEUE Manages your pending message queue.", + " SERVER Send a message to all users on a server" +] +``` diff --git a/modules/rpc/rpc_user.cpp b/modules/rpc/rpc_user.cpp new file mode 100644 index 000000000..00e099f92 --- /dev/null +++ b/modules/rpc/rpc_user.cpp @@ -0,0 +1,335 @@ +/* + * + * (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" + +enum +{ + // Used by anope.checkCredentials, anope.identify, and anope.command. + ERR_INVALID_ACCOUNT = RPC::ERR_CUSTOM_START, + + // Used by anope.checkCredentials + ERR_INVALID_PASSWORD = RPC::ERR_CUSTOM_START + 1, + ERR_ACCOUNT_SUSPENDED = RPC::ERR_CUSTOM_START + 2, + + // Used by anope.identify + ERR_INVALID_USER = RPC::ERR_CUSTOM_START + 1, + + // Used by anope.listCommands, and anope.command + ERR_INVALID_SERVICE = RPC::ERR_CUSTOM_START + 1, + + // Used by anope.command + ERR_INVALID_COMMAND = RPC::ERR_CUSTOM_START + 2, +}; + + +class AnopeCheckCredentialsRPCEvent final + : public RPC::Event +{ +private: + class RPCIdentifyRequest final + : public IdentifyRequest + { + private: + RPC::Request request; + Reference client; + Reference rpcinterface; + + public: + RPCIdentifyRequest(Module *m, RPC::Request &r, HTTP::Client *c, RPC::ServiceInterface *i, const Anope::string &a, const Anope::string &p) + : IdentifyRequest(m, a, p, c->GetIP()) + , request(r) + , client(c) + , rpcinterface(i) + { + } + + void OnSuccess() override + { + if (!rpcinterface || !client) + return; + + auto *na = NickAlias::Find(GetAccount()); + if (!na) + return; // Should never happen. + + if (na->nc->HasExt("NS_SUSPENDED")) + { + request.Error(ERR_ACCOUNT_SUSPENDED, "Account suspended"); + rpcinterface->Reply(request); + client->SendReply(&request.reply); + return; + } + + auto &root = request.Root(); + root.Reply("account", na->nc->display) + .Reply("confirmed", !na->nc->HasExt("UNCONFIRMED")) + .Reply("uniqueid", na->nc->GetId()); + + rpcinterface->Reply(request); + client->SendReply(&request.reply); + } + + void OnFail() override + { + if (!rpcinterface || !client) + return; + + if (NickAlias::Find(GetAccount())) + request.Error(ERR_INVALID_ACCOUNT, "Invalid account"); + else + request.Error(ERR_INVALID_PASSWORD, "Invalid password"); + + rpcinterface->Reply(request); + client->SendReply(&request.reply); + } + }; + +public: + AnopeCheckCredentialsRPCEvent(Module *o) + : RPC::Event(o, "anope.checkCredentials", 2) + { + } + + bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override + { + const auto &username = request.data[0]; + const auto &password = request.data[1]; + if (username.empty() || password.empty()) + { + request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters"); + return true; + } + + auto *req = new RPCIdentifyRequest(this->owner, request, client, iface, username, password); + FOREACH_MOD(OnCheckAuthentication, (nullptr, req)); + req->Dispatch(); + return false; + } +}; + +class AnopeIdentifyRPCEvent final + : public RPC::Event +{ +public: + AnopeIdentifyRPCEvent(Module *o) + : RPC::Event(o, "anope.identify", 2) + { + } + + bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override + { + auto *na = request.data[0].is_pos_number_only() + ? NickAlias::FindId(Anope::Convert(request.data[0], 0)) + : NickAlias::Find(request.data[0]); + if (!na) + { + request.Error(ERR_INVALID_ACCOUNT, "No such account"); + return true; + } + + auto *u = User::Find(request.data[1]); + if (!u) + { + request.Error(ERR_INVALID_USER, "No such user"); + return true; + } + + u->Identify(na); + return true; + } +}; + +class AnopeListCommandsRPCEvent final + : public RPC::Event +{ +public: + AnopeListCommandsRPCEvent(Module *o) + : RPC::Event(o, "anope.listCommands") + { + } + + bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override + { + std::vector bots; + if (request.data.empty()) + { + for (const auto &[_, bi] : *BotListByNick) + bots.push_back(bi); + } + else + { + for (const auto &bot : request.data) + { + auto *bi = BotInfo::Find(bot); + if (!bi) + { + request.Error(ERR_INVALID_SERVICE, "No such service"); + return true; + } + bots.push_back(bi); + } + } + + auto &root = request.Root(); + for (const auto *bi : bots) + { + if (bi->commands.empty()) + continue; + + auto &commands = root.ReplyMap(bi->nick); + for (const auto &[command, info] : bi->commands) + { + ServiceReference cmdref("Command", info.name); + if (!cmdref) + continue; + + auto &cmdinfo = commands.ReplyMap(command); + cmdinfo.Reply("hidden", info.hide) + .Reply("minparams", cmdref->min_params) + .Reply("requiresaccount", !cmdref->AllowUnregistered()) + .Reply("requiresuser", cmdref->RequireUser()); + + if (info.group.empty()) + cmdinfo.Reply("group", nullptr); + else + cmdinfo.Reply("group", info.group); + + if (cmdref->max_params) + cmdinfo.Reply("maxparams", cmdref->max_params); + else + cmdinfo.Reply("maxparams", nullptr); + + if (info.permission.empty()) + cmdinfo.Reply("permission", nullptr); + else + cmdinfo.Reply("permission", info.permission); + } + } + + return true; + } +}; + +class AnopeCommandRPCEvent final + : public RPC::Event +{ +private: + class RPCCommandReply final + : public CommandReply + { + private: + RPC::Array &root; + + public: + RPCCommandReply(RPC::Array &r) + : root(r) + { + } + + void SendMessage(BotInfo *source, const Anope::string &msg) override + { + root.Reply(NormalizeBuffer(msg.replace_all_cs("\x1A", "\x20"))); + }; + }; + +public: + static bool pretenduser; + + AnopeCommandRPCEvent(Module *o) + : RPC::Event(o, "anope.command", 3) + { + } + + bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override + { + NickAlias *na = nullptr; + if (!request.data[0].empty()) + { + na = request.data[0].is_pos_number_only() + ? NickAlias::FindId(Anope::Convert(request.data[0], 0)) + : NickAlias::Find(request.data[0]); + if (!na) + { + request.Error(ERR_INVALID_ACCOUNT, "No such account"); + return true; + } + } + + auto *bi = BotInfo::Find(request.data[1], true); + if (!bi) + { + request.Error(ERR_INVALID_SERVICE, "No such service"); + return true; + } + + Anope::string command; + for (size_t i = 2; i < request.data.size(); ++i) + { + if (!command.empty()) + command.push_back(' '); + command.append(request.data[i]); + } + + User *u = nullptr; + if (pretenduser && na && !na->nc->users.empty()) + { + // Try and find the nick user first. + for (auto *user : na->nc->users) + { + if (user->nick.equals_ci(na->nick)) + { + u = user; + break; + } + } + + // No nick user, fallback to the first. + if (!u) + u = na->nc->users.front(); + } + + RPCCommandReply reply(request.Root()); + CommandSource source(na ? na->nick : "RPC", u, na ? *na->nc : nullptr, &reply, bi, request.id); + + if (!Command::Run(source, command)) + request.Error(ERR_INVALID_COMMAND, "No such command"); + + return true; + } +}; + +bool AnopeCommandRPCEvent::pretenduser = false; + +class ModuleRPCAccount final + : public Module +{ +private: + AnopeCheckCredentialsRPCEvent anopecheckcredentialsrpcevent; + AnopeIdentifyRPCEvent anopeidentifyrpcevent; + AnopeListCommandsRPCEvent anopelistcommandsrpcevent; + AnopeCommandRPCEvent anopecommandrpcevent; + +public: + ModuleRPCAccount(const Anope::string &modname, const Anope::string &creator) + : Module(modname, creator, EXTRA | VENDOR) + , anopecheckcredentialsrpcevent(this) + , anopeidentifyrpcevent(this) + , anopelistcommandsrpcevent(this) + , anopecommandrpcevent(this) + { + } + + void OnReload(Configuration::Conf &conf) override + { + AnopeCommandRPCEvent::pretenduser = conf.GetModule(this).Get("pretenduser"); + } +}; + +MODULE_INIT(ModuleRPCAccount)