HostServ: normalise vhosts + enforce uniqueness

Canonicalise a requested vhost the way the ircd displays it (a disallowed
underscore becomes a hyphen) before storing and applying, so what we keep
matches what shows on the network. A vhost can no longer be assigned to two
accounts: prepare_vhost normalises, validates, and checks vhost_owner at
every assignment point (SET/REQUEST/ACTIVATE/TAKE/DEFAULT), so two inputs
that collapse to the same host are caught as one.

Approach recommended by siniStar (the ns_nethost normalisation logic).
This commit is contained in:
Jean Chevronnet 2026-07-13 23:05:41 +00:00
parent 271e06fa77
commit 31d23cf6b3
No known key found for this signature in database
9 changed files with 130 additions and 18 deletions

View file

@ -1356,6 +1356,15 @@ impl Db {
self.host_cfg.template.as_deref()
}
/// The account whose current (non-expired) vhost is `host`, if any — so a
/// vhost can't be assigned to two accounts and collide on the network.
pub fn vhost_owner(&self, host: &str) -> Option<String> {
self.accounts
.values()
.find(|a| a.vhost.as_ref().is_some_and(|v| v.host.eq_ignore_ascii_case(host) && v.expires.is_none_or(|e| e > now())))
.map(|a| a.name.clone())
}
/// Every account that has a vhost, as (account, host, setter, expires).
pub fn vhosts(&self) -> Vec<(String, String, String, Option<u64>)> {
let mut out: Vec<(String, String, String, Option<u64>)> = self
@ -2532,6 +2541,9 @@ impl Store for Db {
fn vhosts(&self) -> Vec<VhostView> {
Db::vhosts(self).into_iter().map(|(account, host, setter, expires)| VhostView { account, host, setter, expires }).collect()
}
fn vhost_owner(&self, host: &str) -> Option<String> {
Db::vhost_owner(self, host)
}
fn request_vhost(&mut self, account: &str, host: &str) -> Result<(), RegError> {
Db::request_vhost(self, account, host)
}