1
0
mirror of https://github.com/anope/anope.git synced 2026-06-12 19:14:47 +02:00

Add support for self-service vhost validation via DNS.

This commit is contained in:
Sadie Powell
2025-09-06 17:46:01 +01:00
parent 4021c0bb68
commit eccb338cdd
4 changed files with 254 additions and 5 deletions
+20 -1
View File
@@ -168,7 +168,13 @@ command { service = "HostServ"; name = "ON"; command = "hostserv/on"; }
/*
* hs_request
*
* Provides the commands hostserv/request, hostserv/activate, hostserv/reject, and hostserv/waiting.
* Provides the commands:
* hostserv/request - Requests a vhost.
* hostserv/activate - Approves a requested vhost.
* hostserv/reject - Rejects a requested vhost.
* hostserv/waiting - Lists pending vhost requests.
* hostserv/validate - Allows self-service approval of vhosts using DNS
* validation (requires the dns module).
*
* Used to manage vhosts requested by users.
*/
@@ -186,11 +192,24 @@ module
* If set, Anope will send a memo to all services staff when a new vhost is requested.
*/
#memooper = yes
/*
* If DNS validation is enabled, how long should users have to wait between
* attempts at DNS validation. Defaults to 5 minutes.
*/
#validationcooldown = 5m
/*
* If DNS validation is enabled, the TXT record to look for when determining
* if the requester controls the domain. Defaults to anope-dns-validation.
*/
#validationcooldown = "anope-dns-validation"
}
command { service = "HostServ"; name = "REQUEST"; command = "hostserv/request"; }
command { service = "HostServ"; name = "ACTIVATE"; command = "hostserv/activate"; permission = "hostserv/set"; }
command { service = "HostServ"; name = "REJECT"; command = "hostserv/reject"; permission = "hostserv/set"; }
command { service = "HostServ"; name = "WAITING"; command = "hostserv/waiting"; permission = "hostserv/set"; }
#command { service = "HostServ"; name = "VALIDATE"; command = "hostserv/validate"; }
/*
* hs_set
+2
View File
@@ -18,6 +18,8 @@ public:
Anope::string ident;
Anope::string host;
time_t time = 0;
Anope::string validation_token;
time_t last_validation = 0;
virtual ~HostRequest() = default;
};
+26 -3
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Anope\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-30 15:30+0100\n"
"PO-Revision-Date: 2025-07-30 15:30+0100\n"
"POT-Creation-Date: 2025-09-06 17:45+0100\n"
"PO-Revision-Date: 2025-09-06 17:45+0100\n"
"Last-Translator: Sadie Powell <sadie@witchery.services>\n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -5609,6 +5609,10 @@ msgstr ""
msgid "Unable to find regex engine %s."
msgstr ""
#, c-format
msgid "Unable to find the DNS record required to validate %s. If you have not already done this add a TXT record for %s with the value %s and re-execute this command."
msgstr ""
#, c-format
msgid "Unable to load module %s."
msgstr ""
@@ -5767,6 +5771,10 @@ msgstr ""
msgid "VHost for %s has been rejected."
msgstr ""
#, c-format
msgid "VHost for %s has been validated using DNS."
msgstr ""
#, c-format
msgid "VHost for account %s set to %s."
msgstr ""
@@ -5784,6 +5792,9 @@ msgstr ""
msgid "VIEW [mask | list]"
msgstr ""
msgid "Validates a previously requested vhost using DNS"
msgstr ""
msgid "Value"
msgstr ""
@@ -6107,6 +6118,10 @@ msgstr ""
msgid "You must wait %s before registering your nick."
msgstr ""
#, c-format
msgid "You must wait for %s before trying DNS validation again."
msgstr ""
msgid "You need to be identified to use this command."
msgstr ""
@@ -6269,7 +6284,15 @@ msgstr ""
msgid "Your requested vhost has been rejected. Reason: %s"
msgstr ""
msgid "Your vhost has been requested."
msgid "Your requested vhost has been validated via DNS."
msgstr ""
#, c-format
msgid "Your vhost %s has been requested."
msgstr ""
#, c-format
msgid "Your vhost %s has been requested. If the requested vhost is for a valid DNS name you can add a TXT record for %s with the value %s and automatically approve your vhost using %s."
msgstr ""
#, c-format
+206 -1
View File
@@ -15,12 +15,20 @@
*/
#include "module.h"
#include "modules/dns.h"
#include "modules/hostserv/request.h"
static ServiceReference<DNS::Manager> dnsmanager("DNS::Manager", "dns/manager");
static ServiceReference<MemoServService> memoserv("MemoServService", "MemoServ");
static void req_send_memos(Module *me, CommandSource &source, const Anope::string &vident, const Anope::string &vhost);
namespace
{
// The name of the DNS record used for validation.
Anope::string validation_record;
}
struct HostRequestImpl final
: HostRequest
, Serializable
@@ -29,6 +37,23 @@ struct HostRequestImpl final
: Serializable("HostRequest")
{
}
static HostRequestImpl *Get(NickAlias *na)
{
return na ? na->GetExt<HostRequestImpl>("hostrequest") : nullptr;
}
Anope::string Mask() const
{
if (ident.empty())
return host;
return ident + "@" + host;
}
Anope::string GetValidationRecord() const
{
return Anope::Format("%s=%s", validation_record.c_str(), this->validation_token.c_str());
}
};
struct HostRequestTypeImpl final
@@ -46,6 +71,8 @@ struct HostRequestTypeImpl final
data.Store("ident", req->ident);
data.Store("host", req->host);
data.Store("time", req->time);
data.Store("validation_token", req->validation_token);
data.Store("last_validation", req->last_validation);
}
Serializable *Unserialize(Serializable *obj, Serialize::Data &data) const override
@@ -68,12 +95,96 @@ struct HostRequestTypeImpl final
data["ident"] >> req->ident;
data["host"] >> req->host;
data["time"] >> req->time;
data["validation_token"] >> req->validation_token;
data["last_validation"] >> req->last_validation;
}
return req;
}
};
class DNSHostResolver final
: public DNS::Request
{
private:
Command *command;
Reference<NickAlias> nickalias;
CommandSource source;
void HandleError(HostRequestImpl *hr)
{
source.Reply(_(
"Unable to find the DNS record required to validate \002%s\002. If you have not already "
"done this add a TXT record for %s with the value %s and re-execute this command."
),
hr->Mask().c_str(),
hr->host.c_str(),
hr->GetValidationRecord().c_str()
);
}
public:
DNSHostResolver(Command *cmd, HostRequest *hr, NickAlias *na, const CommandSource &src)
: Request(dnsmanager, cmd->module, hr->host, DNS::QUERY_TXT, false)
, command(cmd)
, nickalias(na)
, source(src)
{
hr->last_validation = Anope::CurTime;
Log(LOG_DEBUG) << "Checking " << hr->host << " for " << hr->validation_token;
}
void OnError(const DNS::Query *record) override
{
NickAlias *na = nickalias;
if (!na)
return; // Nick has been dropped.
auto *hr = HostRequestImpl::Get(na);
if (!hr)
{
source.Reply(_("No request for nick %s found."), source.GetNick().c_str());
return;
}
HandleError(hr);
}
void OnLookupComplete(const DNS::Query *record) override
{
NickAlias *na = nickalias;
if (!na)
return; // Nick has been dropped.
auto *hr = HostRequestImpl::Get(na);
if (!hr)
{
source.Reply(_("No request for nick %s found."), source.GetNick().c_str());
return;
}
for (const auto &answer : record->answers)
{
if (answer.rdata != hr->GetValidationRecord())
continue; // Not for us.
na->SetVHost(hr->ident, hr->host, source.GetNick(), hr->time);
FOREACH_MOD(OnSetVHost, (na));
if (Config->GetModule(command->module).Get<bool>("memouser") && memoserv)
memoserv->Send(source.service->nick, na->nick, _("Your requested vhost has been validated via DNS."), true);
source.Reply(_("VHost for %s has been validated using DNS."), na->nick.c_str());
Log(LOG_COMMAND, source, command) << "for " << na->nick << " for vhost " << hr->Mask();
na->Shrink<HostRequestImpl>("hostrequest");
return; // We're done.
}
HandleError(hr);
}
};
class CommandHSRequest final
: public Command
{
@@ -171,9 +282,28 @@ public:
req.ident = user;
req.host = host;
req.time = Anope::CurTime;
req.validation_token = Anope::Random(Config->GetBlock("options").Get<size_t>("codelength", "15"));
na->Extend<HostRequestImpl>("hostrequest", req);
source.Reply(_("Your vhost has been requested."));
BotInfo *bi;
Anope::string cmd;
if (dnsmanager && Command::FindCommandFromService("hostserv/validate", bi, cmd))
{
source.Reply(_(
"Your vhost \002%s\002 has been requested. If the requested vhost is for a valid "
"DNS name you can add a TXT record for %s with the value %s and automatically "
"approve your vhost using \002%s\002."
),
req.Mask().c_str(),
req.host.c_str(),
req.GetValidationRecord().c_str(),
bi->GetQueryCommand("hostserv/validate").c_str()
);
}
else
source.Reply(_("Your vhost \002%s\002 has been requested."), req.Mask().c_str());
req_send_memos(owner, source, user, host);
Log(LOG_COMMAND, source, this) << "to request new vhost " << (!user.empty() ? user + "@" : "") << host;
}
@@ -357,6 +487,72 @@ public:
}
};
class CommandHSValidate final
: public Command
{
public:
time_t cooldown;
CommandHSValidate(Module *creator)
: Command(creator, "hostserv/validate", 0)
{
this->SetDesc(_("Validates a previously requested vhost using DNS"));
}
void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
{
if (Anope::ReadOnly)
{
source.Reply(READ_ONLY_MODE);
return;
}
auto *na = NickAlias::Find(source.GetNick());
if (!na || na->nc != source.GetAccount())
{
source.Reply(ACCESS_DENIED);
return;
}
auto *req = HostRequestImpl::Get(na);
if (!req)
{
source.Reply(_("No request for nick %s found."), source.GetNick().c_str());
return;
}
auto next_validation = req->last_validation + cooldown;
if (req->last_validation && next_validation > Anope::CurTime)
{
source.Reply(_("You must wait for %s before trying DNS validation again."),
Anope::Duration(next_validation - Anope::CurTime).c_str());
return;
}
DNSHostResolver *res = nullptr;
try
{
if (!dnsmanager)
throw SocketException("DNS is not available");
res = new DNSHostResolver(this, req, na, source);
dnsmanager->Process(res);
}
catch (const SocketException &ex)
{
Log(this->module) << ex.GetReason();
source.Reply("Unable to validate vhosts right now. Please try again later.");
delete res;
}
}
bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
{
// TODO
return true;
}
};
class HSRequest final
: public Module
{
@@ -364,6 +560,7 @@ class HSRequest final
CommandHSActivate commandhsactive;
CommandHSReject commandhsreject;
CommandHSWaiting commandhswaiting;
CommandHSValidate commandhsvalidate;
ExtensibleItem<HostRequestImpl> hostrequest;
HostRequestTypeImpl request_type;
@@ -374,11 +571,19 @@ public:
, commandhsactive(this)
, commandhsreject(this)
, commandhswaiting(this)
, commandhsvalidate(this)
, hostrequest(this, "hostrequest")
{
if (!IRCD || !IRCD->CanSetVHost)
throw ModuleException("Your IRCd does not support vhosts");
}
void OnReload(Configuration::Conf &conf) override
{
const auto &block = conf.GetModule(this);
commandhsvalidate.cooldown = block.Get<time_t>("validationcooldown", "5m");
validation_record = block.Get<const Anope::string>("dnsrecord", "anope-dns-validation");
}
};
static void req_send_memos(Module *me, CommandSource &source, const Anope::string &vident, const Anope::string &vhost)