HostServ: ident@host vhosts (CHGIDENT)
A vhost may be ident@host, not just host: apply_vhost splits it and sends CHGIDENT for the ident plus CHGHOST for the host. valid_vhost validates the optional ident part; ON/SET/ACTIVATE/identify all route through apply_vhost, so a bare host still works unchanged. New SetIdent action.
This commit is contained in:
parent
1edf250952
commit
898461d1c2
8 changed files with 65 additions and 8 deletions
|
|
@ -66,6 +66,8 @@ pub enum NetAction {
|
|||
ForceNick { uid: String, nick: String },
|
||||
// Set a user's displayed host (a vhost), or restore it.
|
||||
SetHost { uid: String, host: String },
|
||||
// Set a user's displayed ident/username (the `ident@` part of a vhost).
|
||||
SetIdent { uid: String, ident: String },
|
||||
// Force a user into a channel (SVSJOIN), e.g. applying an account's auto-join
|
||||
// list on identify. `key` is empty for keyless channels.
|
||||
ForceJoin { uid: String, channel: String, key: String },
|
||||
|
|
@ -265,6 +267,18 @@ impl ServiceCtx {
|
|||
});
|
||||
}
|
||||
|
||||
// Apply a vhost spec to a user: `ident@host` sets both the ident and host,
|
||||
// a bare `host` just the host.
|
||||
pub fn apply_vhost(&mut self, uid: &str, spec: &str) {
|
||||
match spec.split_once('@') {
|
||||
Some((ident, host)) => {
|
||||
self.actions.push(NetAction::SetIdent { uid: uid.to_string(), ident: ident.to_string() });
|
||||
self.set_host(uid, host);
|
||||
}
|
||||
None => self.set_host(uid, spec),
|
||||
}
|
||||
}
|
||||
|
||||
// Force a user into a channel (SVSJOIN), e.g. an account's auto-join list.
|
||||
pub fn force_join(&mut self, uid: &str, channel: &str, key: &str) {
|
||||
self.actions.push(NetAction::ForceJoin {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
|||
match db.set_vhost(account, &host, from.nick) {
|
||||
Ok(()) => {
|
||||
for uid in net.uids_logged_into(account) {
|
||||
ctx.set_host(&uid, &host);
|
||||
ctx.apply_vhost(&uid, &host);
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("Activated vhost \x02{host}\x02 for \x02{account}\x02."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,18 @@ fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
// Whether `host` is a syntactically valid vhost: a hostname of dot-separated
|
||||
// labels using letters, digits and hyphens, not too long.
|
||||
pub(crate) fn valid_vhost(host: &str) -> bool {
|
||||
// Whether `spec` is a valid vhost: an optional `ident@` (letters, digits, a few
|
||||
// punctuation) followed by a hostname of dot-separated alphanumeric/hyphen labels.
|
||||
pub(crate) fn valid_vhost(spec: &str) -> bool {
|
||||
let (ident, host) = match spec.split_once('@') {
|
||||
Some((i, h)) => (Some(i), h),
|
||||
None => (None, spec),
|
||||
};
|
||||
if let Some(i) = ident {
|
||||
if i.is_empty() || i.len() > 12 || !i.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if host.is_empty() || host.len() > 64 || !host.contains('.') {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
|
|||
};
|
||||
match db.vhost(account) {
|
||||
Some(v) => {
|
||||
ctx.set_host(from.uid, &v.host);
|
||||
ctx.apply_vhost(from.uid, &v.host);
|
||||
ctx.notice(me, from.uid, format!("Your vhost \x02{}\x02 is now active.", v.host));
|
||||
}
|
||||
None => ctx.notice(me, from.uid, "You have no vhost assigned."),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
|||
match db.set_vhost(account, host, from.nick) {
|
||||
Ok(()) => {
|
||||
for uid in net.uids_logged_into(account) {
|
||||
ctx.set_host(&uid, host);
|
||||
ctx.apply_vhost(&uid, host);
|
||||
}
|
||||
ctx.notice(me, from.uid, format!("Vhost \x02{host}\x02 assigned to \x02{account}\x02."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,6 +275,10 @@ impl Protocol for InspIrcd {
|
|||
let target = uid.get(..3).unwrap_or(uid.as_str());
|
||||
vec![self.from_us(format!("ENCAP {} CHGHOST {} {}", target, uid, host))]
|
||||
}
|
||||
NetAction::SetIdent { uid, ident } => {
|
||||
let target = uid.get(..3).unwrap_or(uid.as_str());
|
||||
vec![self.from_us(format!("ENCAP {} CHGIDENT {} {}", target, uid, ident))]
|
||||
}
|
||||
NetAction::ForceNick { uid, nick } => {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(self.ts);
|
||||
vec![self.from_us(format!("SVSNICK {} {} {}", uid, nick, now))]
|
||||
|
|
@ -453,11 +457,13 @@ mod tests {
|
|||
assert!(ev.iter().any(|e| matches!(e, NetEvent::ChannelVoice { uid, voice: true, .. } if uid == "0IRAAAAAD")), "join voice: {ev:?}");
|
||||
}
|
||||
|
||||
// A vhost is set by ENCAP'ing the target's own server with CHGHOST.
|
||||
// A vhost is set by ENCAP'ing the target's own server with CHGHOST / CHGIDENT.
|
||||
#[test]
|
||||
fn serializes_set_host() {
|
||||
let lines = proto().serialize(&NetAction::SetHost { uid: "0IRAAAAAB".into(), host: "cool.example".into() });
|
||||
assert_eq!(lines, vec![":42S ENCAP 0IR CHGHOST 0IRAAAAAB cool.example".to_string()]);
|
||||
let lines = proto().serialize(&NetAction::SetIdent { uid: "0IRAAAAAB".into(), ident: "cool".into() });
|
||||
assert_eq!(lines, vec![":42S ENCAP 0IR CHGIDENT 0IRAAAAAB cool".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
|||
}
|
||||
// Apply the account's vhost (HostServ), if it has one.
|
||||
if let Some(v) = db.vhost(&account) {
|
||||
ctx.set_host(from.uid, &v.host);
|
||||
ctx.apply_vhost(from.uid, &v.host);
|
||||
}
|
||||
// Let them know about waiting memos.
|
||||
let unread = db.unread_memos(&account);
|
||||
|
|
|
|||
|
|
@ -2686,6 +2686,34 @@ mod tests {
|
|||
assert!(hs(&mut e, "000AAAAAB", "SET alice not a host").iter().any(|a| matches!(a, NetAction::Notice { text, .. } if text.contains("isn't a valid host"))), "host validated");
|
||||
}
|
||||
|
||||
// An ident@host vhost applies both the ident (CHGIDENT) and the host.
|
||||
#[test]
|
||||
fn hostserv_ident_at_host() {
|
||||
use fedserv_hostserv::HostServ;
|
||||
use fedserv_nickserv::NickServ;
|
||||
let path = std::env::temp_dir().join("fedserv-hsident.jsonl");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let mut db = Db::open(&path, "42S");
|
||||
db.scram_iterations = 4096;
|
||||
db.register("boss", "password1", None).unwrap();
|
||||
db.register("alice", "password1", None).unwrap();
|
||||
db.set_vhost("alice", "web@cloak.example", "system").unwrap();
|
||||
let mut e = Engine::new(
|
||||
vec![
|
||||
Box::new(NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }),
|
||||
Box::new(HostServ { uid: "42SAAAAAG".into() }),
|
||||
],
|
||||
db,
|
||||
);
|
||||
e.set_sid("42S".into());
|
||||
let ns = |e: &mut Engine, uid: &str, t: &str| e.handle(NetEvent::Privmsg { from: uid.into(), to: "42SAAAAAA".into(), text: t.into() });
|
||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAV".into(), nick: "alice".into(), host: "realhost".into() });
|
||||
|
||||
let out = ns(&mut e, "000AAAAAV", "IDENTIFY password1");
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::SetIdent { uid, ident } if uid == "000AAAAAV" && ident == "web")), "ident set: {out:?}");
|
||||
assert!(out.iter().any(|a| matches!(a, NetAction::SetHost { uid, host } if uid == "000AAAAAV" && host == "cloak.example")), "host set");
|
||||
}
|
||||
|
||||
// HostServ REQUEST -> WAITING -> ACTIVATE assigns and applies the vhost; a
|
||||
// REJECT clears the request without assigning anything.
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue