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 },
|
ForceNick { uid: String, nick: String },
|
||||||
// Set a user's displayed host (a vhost), or restore it.
|
// Set a user's displayed host (a vhost), or restore it.
|
||||||
SetHost { uid: String, host: String },
|
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
|
// Force a user into a channel (SVSJOIN), e.g. applying an account's auto-join
|
||||||
// list on identify. `key` is empty for keyless channels.
|
// list on identify. `key` is empty for keyless channels.
|
||||||
ForceJoin { uid: String, channel: String, key: String },
|
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.
|
// 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) {
|
pub fn force_join(&mut self, uid: &str, channel: &str, key: &str) {
|
||||||
self.actions.push(NetAction::ForceJoin {
|
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) {
|
match db.set_vhost(account, &host, from.nick) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
for uid in net.uids_logged_into(account) {
|
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."));
|
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
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether `host` is a syntactically valid vhost: a hostname of dot-separated
|
// Whether `spec` is a valid vhost: an optional `ident@` (letters, digits, a few
|
||||||
// labels using letters, digits and hyphens, not too long.
|
// punctuation) followed by a hostname of dot-separated alphanumeric/hyphen labels.
|
||||||
pub(crate) fn valid_vhost(host: &str) -> bool {
|
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('.') {
|
if host.is_empty() || host.len() > 64 || !host.contains('.') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
|
||||||
};
|
};
|
||||||
match db.vhost(account) {
|
match db.vhost(account) {
|
||||||
Some(v) => {
|
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));
|
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."),
|
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) {
|
match db.set_vhost(account, host, from.nick) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
for uid in net.uids_logged_into(account) {
|
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."));
|
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());
|
let target = uid.get(..3).unwrap_or(uid.as_str());
|
||||||
vec![self.from_us(format!("ENCAP {} CHGHOST {} {}", target, uid, host))]
|
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 } => {
|
NetAction::ForceNick { uid, nick } => {
|
||||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(self.ts);
|
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))]
|
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:?}");
|
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]
|
#[test]
|
||||||
fn serializes_set_host() {
|
fn serializes_set_host() {
|
||||||
let lines = proto().serialize(&NetAction::SetHost { uid: "0IRAAAAAB".into(), host: "cool.example".into() });
|
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()]);
|
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]
|
#[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.
|
// Apply the account's vhost (HostServ), if it has one.
|
||||||
if let Some(v) = db.vhost(&account) {
|
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 them know about waiting memos.
|
||||||
let unread = db.unread_memos(&account);
|
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");
|
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
|
// HostServ REQUEST -> WAITING -> ACTIVATE assigns and applies the vhost; a
|
||||||
// REJECT clears the request without assigning anything.
|
// REJECT clears the request without assigning anything.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue