diff --git a/include/defs.h b/include/defs.h index 62e164436..5f35fa988 100644 --- a/include/defs.h +++ b/include/defs.h @@ -42,6 +42,7 @@ struct ModeData; class Module; class NickAlias; class NickCore; +namespace NickServ { struct Cert; } struct Oper; namespace OperServ { struct Exception; } class OperType; diff --git a/include/modules.h b/include/modules.h index c39e2fe62..5e7774f5e 100644 --- a/include/modules.h +++ b/include/modules.h @@ -793,13 +793,13 @@ public: * @param nc The nick * @param entry The entry */ - virtual void OnNickAddCert(NickCore *nc, const Anope::string &entry) ATTR_NOT_NULL(2) { throw NotImplementedException(); } + virtual void OnNickAddCert(NickCore *nc, const NickServ::Cert *entry) ATTR_NOT_NULL(2, 3) { throw NotImplementedException(); } /** Called from NickCore::EraseCert() * @param nc pointer to the NickCore * @param entry The fingerprint */ - virtual void OnNickEraseCert(NickCore *nc, const Anope::string &entry) ATTR_NOT_NULL(2) { throw NotImplementedException(); } + virtual void OnNickEraseCert(NickCore *nc, const NickServ::Cert *entry) ATTR_NOT_NULL(2, 3) { throw NotImplementedException(); } /** Called when a user requests info for a nick * @param source The user requesting info diff --git a/include/modules/nickserv/cert.h b/include/modules/nickserv/cert.h index cdf158502..70e981606 100644 --- a/include/modules/nickserv/cert.h +++ b/include/modules/nickserv/cert.h @@ -19,12 +19,31 @@ namespace NickServ { + struct Cert; class CertList; class CertService; ServiceReference cert_service(NICKSERV_CERT_SERVICE, NICKSERV_CERT_SERVICE); } +struct NickServ::Cert +{ + /** The account this cert is for. */ + Serialize::Reference account; + + /** The time at which this certificate was created. */ + time_t created = 0; + + /** The user who created this certificate. */ + Anope::string creator; + + /** If non-empty then a description of the certificate. */ + Anope::string description; + + /** The TLS fingerprint for the certificate. */ + Anope::string fingerprint; +}; + class NickServ::CertList { protected: @@ -39,7 +58,7 @@ public: * * Adds a new entry into the cert list. */ - virtual void AddCert(const Anope::string &entry) = 0; + virtual NickServ::Cert *AddCert(const Anope::string &entry) = 0; /** Get an entry from the nick's cert list by index * @@ -48,7 +67,7 @@ public: * * Retrieves an entry from the certificate list corresponding to the given index. */ - virtual Anope::string GetCert(unsigned entry) const = 0; + virtual NickServ::Cert *GetCert(unsigned entry) const = 0; virtual unsigned GetCertCount() const = 0; diff --git a/language/anope.en_US.po b/language/anope.en_US.po index 40b5aac4c..686120ef6 100644 --- a/language/anope.en_US.po +++ b/language/anope.en_US.po @@ -16,8 +16,8 @@ msgid "" msgstr "" "Project-Id-Version: Anope\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-22 16:58+0000\n" -"PO-Revision-Date: 2026-02-22 16:58+0000\n" +"POT-Creation-Date: 2026-02-22 17:02+0000\n" +"PO-Revision-Date: 2026-02-22 17:03+0000\n" "Last-Translator: Sadie Powell \n" "Language-Team: English\n" "Language: en_US\n" @@ -363,6 +363,18 @@ msgstr "" msgid "[auto-memo] The memo you sent to %s has been viewed." msgstr "" +msgid "{fingerprint}" +msgstr "" + +msgid "{fingerprint} ({description})" +msgstr "" + +msgid "{fingerprint} -- created by {creator} at {created}" +msgstr "" + +msgid "{fingerprint} -- created by {creator} at {created} ({description})" +msgstr "" + msgid "{host}: {session} sessions" msgstr "" @@ -3040,6 +3052,9 @@ msgstr "" msgid "Find a user's status on a channel" msgstr "" +msgid "Fingerprint" +msgstr "" + #, c-format msgid "Fingerprint %s already present on %s's certificate list." msgstr "" @@ -6070,6 +6085,9 @@ msgstr "" msgid "VIEW [mask | list]" msgstr "" +msgid "VIEW [nickname]" +msgstr "" + msgid "VIEW [vhost-mask | entry-num | list]" msgstr "" diff --git a/modules/nickserv/ns_cert.cpp b/modules/nickserv/ns_cert.cpp index 387c62508..72b045a28 100644 --- a/modules/nickserv/ns_cert.cpp +++ b/modules/nickserv/ns_cert.cpp @@ -15,7 +15,20 @@ #include "module.h" #include "modules/nickserv/cert.h" -static Anope::unordered_map certmap; +#define NICKSERV_CERT_TYPE "NSCert" + +struct NSCertInfo final + : NickServ::Cert + , Serializable +{ + NSCertInfo(Extensible *ext) + : Serializable(NICKSERV_CERT_TYPE) + { + account = anope_dynamic_static_cast(ext); + } +}; + +static Anope::unordered_map certmap; struct CertServiceImpl final : NickServ::CertService @@ -27,9 +40,9 @@ struct CertServiceImpl final NickCore *FindAccountFromCert(const Anope::string &cert) override { - Anope::unordered_map::iterator it = certmap.find(cert); + auto it = certmap.find(cert); if (it != certmap.end()) - return it->second; + return it->second->account; return NULL; } @@ -48,8 +61,10 @@ struct CertServiceImpl final struct NSCertListImpl final : NickServ::CertList { + friend class NSCertInfoType; + Serialize::Reference nc; - std::vector certs; + std::vector certs; public: NSCertListImpl(Extensible *obj) : nc(anope_dynamic_static_cast(obj)) { } @@ -65,11 +80,15 @@ public: * * Adds a new entry into the cert list. */ - void AddCert(const Anope::string &entry) override + NickServ::Cert *AddCert(const Anope::string &entry) override { - this->certs.push_back(entry); - certmap[entry] = nc; - FOREACH_MOD(OnNickAddCert, (this->nc, entry)); + auto *cert = new NSCertInfo(nc); + cert->fingerprint = entry; + + this->certs.push_back(cert); + certmap[entry] = cert; + FOREACH_MOD(OnNickAddCert, (this->nc, cert)); + return cert; } /** Get an entry from the nick's cert list by index @@ -79,10 +98,11 @@ public: * * Retrieves an entry from the certificate list corresponding to the given index. */ - Anope::string GetCert(unsigned entry) const override + NickServ::Cert *GetCert(unsigned entry) const override { if (entry >= this->certs.size()) - return ""; + return nullptr; + return this->certs[entry]; } @@ -100,7 +120,10 @@ public: */ bool FindCert(const Anope::string &entry) const override { - return std::find(this->certs.begin(), this->certs.end(), entry) != this->certs.end(); + auto it = std::find_if(this->certs.begin(), this->certs.end(), [&entry](const NSCertInfo *cert) { + return cert->fingerprint == entry; + }); + return it != this->certs.end(); } /** Erase a fingerprint from the nick's certificate list @@ -111,34 +134,45 @@ public: */ void EraseCert(const Anope::string &entry) override { - std::vector::iterator it = std::find(this->certs.begin(), this->certs.end(), entry); + auto it = std::find_if(this->certs.begin(), this->certs.end(), [&entry](const NSCertInfo *cert) { + return cert->fingerprint == entry; + }); if (it != this->certs.end()) { - FOREACH_MOD(OnNickEraseCert, (this->nc, entry)); + FOREACH_MOD(OnNickEraseCert, (this->nc, *it)); certmap.erase(entry); + + delete *it; this->certs.erase(it); } } void ReplaceCert(const Anope::string &oldentry, const Anope::string &newentry) override { - auto it = std::find(this->certs.begin(), this->certs.end(), oldentry); - if (it == this->certs.end()) + auto oldit = std::find_if(this->certs.begin(), this->certs.end(), [&oldentry](const NSCertInfo *cert) { + return cert->fingerprint == oldentry; + }); + if (oldit == this->certs.end()) return; // We can't replace a non-existent cert. - FOREACH_MOD(OnNickEraseCert, (this->nc, oldentry)); + FOREACH_MOD(OnNickEraseCert, (this->nc, *oldit)); certmap.erase(oldentry); - if (std::find(this->certs.begin(), this->certs.end(), newentry) != this->certs.end()) + auto newit = std::find_if(this->certs.begin(), this->certs.end(), [&newentry](const NSCertInfo *cert) { + return cert->fingerprint == newentry; + }); + if (newit != this->certs.end()) { // The cert we're upgrading to already exists. - this->certs.erase(it); + delete *newit; + this->certs.erase(newit); return; } - *it = newentry; - certmap[newentry] = nc; - FOREACH_MOD(OnNickAddCert, (this->nc, newentry)); + auto *cert = *newit; + cert->fingerprint = newentry; + certmap[newentry] = cert; + FOREACH_MOD(OnNickAddCert, (this->nc, cert)); } /** Clears the entire nick's cert list @@ -148,8 +182,11 @@ public: void ClearCert() override { FOREACH_MOD(OnNickClearCert, (this->nc)); - for (const auto &cert : certs) - certmap.erase(cert); + for (const auto *cert : certs) + { + delete cert; + certmap.erase(cert->fingerprint); + } this->certs.clear(); } @@ -164,45 +201,89 @@ public: { ExtensibleItem(Module *m, const Anope::string &ename) : ::ExtensibleItem(m, ename) { } - void ExtensibleSerialize(const Extensible *e, const Serializable *s, Serialize::Data &data) const override - { - if (s->GetSerializableType()->GetName() != NICKCORE_TYPE) - return; - - const NickCore *n = anope_dynamic_static_cast(e); - auto *c = this->Get(n); - if (c == NULL || !c->GetCertCount()) - return; - - std::ostringstream oss; - for (unsigned i = 0; i < c->GetCertCount(); ++i) - oss << c->GetCert(i) << " "; - data.Store("cert", oss.str()); - } - void ExtensibleUnserialize(Extensible *e, Serializable *s, Serialize::Data &data) override { + // Begin 2.0 compatibility. if (s->GetSerializableType()->GetName() != NICKCORE_TYPE) return; - NickCore *n = anope_dynamic_static_cast(e); - auto *c = this->Require(n); + auto *nc = anope_dynamic_static_cast(e); + auto *cl = this->Require(nc); + // Delete the old cert list. + for (const auto *cert : cl->certs) + { + delete cert; + certmap.erase(cert->fingerprint); + } + cl->certs.clear(); + + // Add the new cert list Anope::string buf; data["cert"] >> buf; - spacesepstream sep(buf); - for (const auto &cert : c->certs) - certmap.erase(cert); - c->certs.clear(); - while (sep.GetToken(buf)) + for (spacesepstream sep(buf); sep.GetToken(buf); ) { - c->certs.push_back(buf); - certmap[buf] = n; + auto *cert = new NSCertInfo(e); + cert->fingerprint = buf; + cl->certs.push_back(cert); + certmap[buf] = cert; } + // End 2.0 compatibility. } }; }; + +class NSCertInfoType final + : public Serialize::Type +{ +public: + NSCertInfoType() + : Serialize::Type(NICKSERV_CERT_TYPE) + { + } + + void Serialize(Serializable *obj, Serialize::Data &data) const override + { + const auto *cert = static_cast(obj); + data.Store("account", cert->account->GetId()); + data.Store("created", cert->created); + data.Store("creator", cert->creator); + data.Store("description", cert->description); + data.Store("fingerprint", cert->fingerprint); + } + + Serializable *Unserialize(Serializable *obj, Serialize::Data &data) const override + { + uint64_t account = 0; + data["account"] >> account; + + auto *nc = NickCore::FindId(account); + if (!nc) + return nullptr; // Missing user. + + NSCertInfo *cert; + if (obj) + cert = anope_dynamic_static_cast(obj); + else + cert = new NSCertInfo(nc); + + data["created"] >> cert->created; + data["creator"] >> cert->creator; + data["description"] >> cert->description; + data["fingerprint"] >> cert->fingerprint; + + if (!obj) + { + auto *cl = nc->Require(NICKSERV_CERT_EXT); + cl->certs.push_back(cert); + certmap[cert->fingerprint] = cert; + } + + return cert; + } +}; + class CommandNSCert final : public Command { @@ -244,7 +325,10 @@ private: return; } - cl->AddCert(certfp); + auto *cert = cl->AddCert(certfp); + cert->created = Anope::CurTime; + cert->creator = source.GetNick(); + Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to ADD certificate fingerprint " << certfp << " to " << nc->display; source.Reply(_("\002%s\002 added to %s's certificate list."), certfp.c_str(), nc->display.c_str()); } @@ -278,7 +362,7 @@ private: source.Reply(_("\002%s\002 deleted from %s's certificate list."), certfp.c_str(), nc->display.c_str()); } - static void DoList(CommandSource &source, const NickCore *nc) + static void DoList(CommandSource &source, const NickCore *nc, bool full) { auto *cl = nc->GetExt(NICKSERV_CERT_EXT); @@ -288,12 +372,51 @@ private: return; } - source.Reply(_("Certificate list for %s:"), nc->display.c_str()); + ListFormatter list(source.GetAccount()); + list.AddColumn(_("Fingerprint")); + if (full) + { + list.AddColumn(_("Creator")).AddColumn(_("Created")); + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("\002{fingerprint}\002 -- created by {creator} at {created}") + : _("\002{fingerprint}\002 -- created by {creator} at {created} ({description})"); + }); + + } + else + { + list.SetFlexible([](ListFormatter::ListEntry &row) + { + return row["Description"].empty() + ? _("\002{fingerprint}\002") + : _("\002{fingerprint}\002 ({description})"); + }); + } + list.AddColumn(_("Description")); + for (unsigned i = 0; i < cl->GetCertCount(); ++i) { - Anope::string fingerprint = cl->GetCert(i); - source.Reply(" %s", fingerprint.c_str()); + auto *cert = cl->GetCert(i); + ListFormatter::ListEntry entry; + entry["Fingerprint"] = cert->fingerprint; + entry["Description"] = cert->description; + if (full) + { + entry["Created"] = cert->created + ? Anope::strftime(cert->created, nullptr, true) + : TIME_UNKNOWN; + + entry["Creator"] = cert->creator.empty() + ? TIME_UNKNOWN + : cert->creator; + } + list.AddEntry(entry); } + + source.Reply(_("Certificate list for %s:"), nc->display.c_str()); + list.SendTo(source); } public: @@ -303,6 +426,7 @@ public: this->SetSyntax(_("ADD [\037nickname\037] [\037fingerprint\037]")); this->SetSyntax(_("DEL [\037nickname\037] \037fingerprint\037")); this->SetSyntax(_("LIST [\037nickname\037]")); + this->SetSyntax(_("VIEW [\037nickname\037]")); } void Execute(CommandSource &source, const std::vector ¶ms) override @@ -310,7 +434,7 @@ public: const Anope::string &cmd = params[0]; Anope::string nick, certfp; - if (cmd.equals_ci("LIST")) + if (cmd.equals_ci("LIST") || cmd.equals_ci("VIEW")) nick = params.size() > 1 ? params[1] : ""; else { @@ -344,7 +468,9 @@ public: nc = source.nc; if (cmd.equals_ci("LIST")) - return this->DoList(source, nc); + return this->DoList(source, nc, false); + if (cmd.equals_ci("VIEW")) + return this->DoList(source, nc, true); else if (nc->HasExt("NS_SUSPENDED")) source.Reply(NICK_X_SUSPENDED, nc->display.c_str()); else if (Anope::ReadOnly) @@ -498,6 +624,7 @@ private: CommandNSSASetAutologin commandnssasetautologin; NSCertListImpl::ExtensibleItem certs; CertServiceImpl cs; + NSCertInfoType cert_type; bool CanLogin(User *u, NickCore *nc) { @@ -558,7 +685,9 @@ public: return; auto *cl = certs.Require(na->nc); - cl->AddCert(u->fingerprint); + auto *cert = cl->AddCert(u->fingerprint); + cert->created = Anope::CurTime; + cert->creator = u->nick; auto *NickServ = Config->GetClient("NickServ"); u->SendMessage(NickServ, _("Your SSL certificate fingerprint \002%s\002 has been automatically added to your certificate list."), u->fingerprint.c_str()); diff --git a/modules/webcpanel/pages/nickserv/cert.cpp b/modules/webcpanel/pages/nickserv/cert.cpp index 5ede15da1..2edc5c0ba 100644 --- a/modules/webcpanel/pages/nickserv/cert.cpp +++ b/modules/webcpanel/pages/nickserv/cert.cpp @@ -41,7 +41,7 @@ bool WebCPanel::NickServ::Cert::OnRequest(HTTP::Provider *server, const Anope::s auto *cl = na->nc->GetExt<::NickServ::CertList>(NICKSERV_CERT_EXT); if (cl) for (unsigned i = 0; i < cl->GetCertCount(); ++i) - replacements["CERTS"] = cl->GetCert(i); + replacements["CERTS"] = cl->GetCert(i)->fingerprint; TemplateFileServer page("nickserv/cert.html"); page.Serve(server, page_name, client, message, reply, replacements);