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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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> ¶ms) 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)
|
||||
|
||||
Reference in New Issue
Block a user