//! core_user — the client registration & session commands: CAP, NICK, USER, //! PING, PONG, QUIT. use std::net::{IpAddr, SocketAddr}; use crate::channels::glob_match; use crate::command::{CmdResult, Command}; use crate::numeric::*; use crate::server::Server; use crate::users::{ident_of, valid_nick, Caps}; use crate::Uid; pub fn commands() -> Vec> { vec![ Box::new(Cap), Box::new(Authenticate), Box::new(Nick), Box::new(UserCmd), Box::new(Ping), Box::new(Pong), Box::new(Quit), Box::new(Away), Box::new(SetName), Box::new(WebIrc), ] } /// WEBIRC — a trusted web gateway declares the real client's host + IP, so users /// behind it don't all share the gateway's address. `WEBIRC /// [:flags]`; must precede registration and the password must /// match a `webirc` config block. (Password-only trust for now — restricting it /// to the gateway's own source IP is a TODO.) struct WebIrc; impl Command for WebIrc { fn name(&self) -> &'static str { "WEBIRC" } fn min_params(&self) -> usize { 4 } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if s.users.get(&uid).map(|u| u.registered).unwrap_or(false) { return CmdResult::Fail; // can't re-spoof a registered session } let (pass, host, ip) = (¶ms[0], ¶ms[2], ¶ms[3]); // the gateway's own connecting IP (before we spoof it below) let from = s .users .get(&uid) .map(|u| u.addr.ip().to_string()) .unwrap_or_default(); let Some(gw) = s .webirc .iter() .find(|(p, _, mask)| p == pass && (mask.is_empty() || glob_match(mask, &from))) .map(|(_, g, _)| g.clone()) else { s.notice_star(uid, "WEBIRC: invalid credentials"); return CmdResult::Fail; }; let newip = ip.parse::().ok(); if let Some(u) = s.users.get_mut(&uid) { u.host = host.clone(); if let Some(a) = newip { u.addr = SocketAddr::new(a, u.addr.port()); } } s.notice_star(uid, &format!("WEBIRC identity accepted via {gw}")); CmdResult::Ok } } struct Away; impl Command for Away { fn name(&self) -> &'static str { "AWAY" } fn before_reg(&self) -> bool { true // draft/pre-away: clients may set AWAY during CAP negotiation } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let msg = params.first().cloned().filter(|m| !m.is_empty()); let now_away = msg.is_some(); let prefix = match s.users.get_mut(&uid) { Some(u) => { u.flags.away = msg.clone(); u.prefix() } None => return CmdResult::Fail, }; // away-notify: tell capable peers we went away / came back let line = match &msg { Some(m) => format!(":{prefix} AWAY :{m}"), None => format!(":{prefix} AWAY"), }; s.notify_peers(uid, &line, |c| c.away_notify); if now_away { s.numeric(uid, RPL_NOWAWAY, ":You have been marked as being away"); } else { s.numeric(uid, RPL_UNAWAY, ":You are no longer marked as being away"); } CmdResult::Ok } } struct Cap; impl Command for Cap { fn name(&self) -> &'static str { "CAP" } fn min_params(&self) -> usize { 1 } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let who = cap_target(s, uid); match params[0].to_ascii_uppercase().as_str() { "LS" => { let cap302 = params.get(1).map(|v| v == "302").unwrap_or(false); let secure = s.users.get(&uid).map(|u| u.secure).unwrap_or(false); if let Some(u) = s.users.get_mut(&uid) { u.cap = true; // hold registration until CAP END u.cap_302 |= cap302; } s.send( uid, format!( ":{} CAP {who} LS :{}", s.name, Caps::ls_line(cap302, secure) ), ); } "REQ" => { if let Some(u) = s.users.get_mut(&uid) { u.cap = true; } let req = params.get(1).cloned().unwrap_or_default(); let wanted: Vec<(&str, bool)> = req .split_whitespace() .map(|t| match t.strip_prefix('-') { Some(rest) => (rest, false), None => (t, true), }) .collect(); // CAP REQ is atomic: ACK the whole set or NAK the whole set if !wanted.is_empty() && wanted.iter().all(|(n, _)| Caps::is_known(n)) { for (name, on) in &wanted { if let Some(u) = s.users.get_mut(&uid) { u.caps.set(name, *on); } } s.send(uid, format!(":{} CAP {who} ACK :{req}", s.name)); } else { s.send(uid, format!(":{} CAP {who} NAK :{req}", s.name)); } } "LIST" => { let list = s .users .get(&uid) .map(|u| u.caps.enabled()) .unwrap_or_default(); s.send(uid, format!(":{} CAP {who} LIST :{list}", s.name)); } "END" => { if let Some(u) = s.users.get_mut(&uid) { u.cap = false; } } _ => {} } CmdResult::Ok } } /// CAP reply target: the nick, or `*` before one is set. fn cap_target(s: &Server, uid: Uid) -> String { s.users .get(&uid) .map(|u| { if u.nick.is_empty() { "*".to_string() } else { u.nick.clone() } }) .unwrap_or_else(|| "*".to_string()) } /// AUTHENTICATE — the SASL handshake. echoIRCd verifies nothing itself (it has no /// accounts); once a services server is linked over S2S the payload is relayed to /// it and `set_login` applied on success. Until then — exactly like InspIRCd with /// no services — SASL fails cleanly. struct Authenticate; impl Command for Authenticate { fn name(&self) -> &'static str { "AUTHENTICATE" } fn min_params(&self) -> usize { 1 } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if !s.users.get(&uid).map(|u| u.caps.sasl).unwrap_or(false) { s.numeric( uid, ERR_SASLFAIL, ":You must request the sasl capability first", ); return CmdResult::Fail; } let arg = ¶ms[0]; let mech = s.users.get(&uid).and_then(|u| u.sasl_mech.clone()); // SASL is relayed to a linked services server (see `Server::sasl_relay`); // with none configured/linked it fails cleanly, exactly like InspIRCd. let have_services = s.sasl_link().is_some(); match mech { // step 1 — the client picks a mechanism None => { if arg == "*" { s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted"); CmdResult::Ok } else if arg.eq_ignore_ascii_case("PLAIN") { if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = Some("PLAIN".to_string()); } if have_services { s.sasl_relay(uid, "S PLAIN"); // start the exchange at services } s.send(uid, "AUTHENTICATE +".to_string()); CmdResult::Ok } else if arg.eq_ignore_ascii_case("EXTERNAL") { // CertFP: only works on TLS with a client cert; the fingerprint // goes to services, which map it to an account. let certfp = s.users.get(&uid).and_then(|u| u.certfp.clone()); match certfp { Some(fp) if have_services => { if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = Some("EXTERNAL".to_string()); } s.sasl_relay(uid, &format!("S EXTERNAL {fp}")); s.send(uid, "AUTHENTICATE +".to_string()); CmdResult::Ok } _ => { s.numeric( uid, ERR_SASLFAIL, ":SASL EXTERNAL requires a client certificate", ); CmdResult::Fail } } } else { s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms"); s.numeric(uid, ERR_SASLFAIL, ":Unsupported SASL mechanism"); CmdResult::Fail } } // step 2 — the client sends the base64 payload (or aborts with `*`) Some(_) => { if arg == "*" { if have_services { s.sasl_relay(uid, "D A"); } if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = None; } s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted"); return CmdResult::Ok; } if arg.len() > 400 { if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = None; } s.numeric(uid, ERR_SASLTOOLONG, ":SASL message too long"); return CmdResult::Fail; } if have_services { // relay the response; the verdict (900/903 or 904) comes back // over S2S in `Server::link_sasl`, which clears `sasl_mech`. s.sasl_relay(uid, &format!("C {arg}")); CmdResult::Ok } else { if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = None; } s.numeric( uid, ERR_SASLFAIL, ":SASL authentication failed (services are not available)", ); CmdResult::Fail } } } } } /// SETNAME — change your realname (IRCv3). Broadcast to `setname`-capable peers. struct SetName; impl Command for SetName { fn name(&self) -> &'static str { "SETNAME" } fn min_params(&self) -> usize { 1 } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let realname = params[0].clone(); let prefix = match s.users.get_mut(&uid) { Some(u) => { u.realname = realname.clone(); u.prefix() } None => return CmdResult::Fail, }; let line = format!(":{prefix} SETNAME :{realname}"); if s.users.get(&uid).map(|u| u.caps.setname).unwrap_or(false) { s.send(uid, line.clone()); } s.notify_peers(uid, &line, |c| c.setname); CmdResult::Ok } } struct Nick; impl Command for Nick { fn name(&self) -> &'static str { "NICK" } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let Some(newnick) = params.first() else { s.numeric(uid, ERR_NONICKNAMEGIVEN, ":No nickname given"); return CmdResult::Fail; }; if !valid_nick(newnick) { s.numeric( uid, ERR_ERRONEUSNICKNAME, &format!("{newnick} :Erroneous nickname"), ); return CmdResult::Fail; } // Q-line: a reserved nick is refused (opers and services bypass) if !s.is_oper(uid) { if let Some(reason) = s.matched_qline(newnick) { s.numeric( uid, ERR_ERRONEUSNICKNAME, &format!("{newnick} :Nickname is reserved: {reason}"), ); return CmdResult::Fail; } } if let Some(other) = s.find_nick(newnick) { if other != uid { s.numeric( uid, ERR_NICKNAMEINUSE, &format!("{newnick} :Nickname is already in use"), ); return CmdResult::Fail; } return CmdResult::Ok; // same nick, no-op } // a nick already held by a user on a linked server is taken too if s.remote_nick.contains_key(&newnick.to_ascii_lowercase()) { s.numeric( uid, ERR_NICKNAMEINUSE, &format!("{newnick} :Nickname is already in use"), ); return CmdResult::Fail; } // +N — can't change nick while on a no-nick-change channel (opers bypass) if !s.is_oper(uid) { let blocked = s .users .get(&uid) .map(|u| u.channels.clone()) .unwrap_or_default() .iter() .find_map(|k| { s.channels .get(k) .filter(|c| c.modes.no_nick) .map(|c| c.name.clone()) }); if let Some(cn) = blocked { s.numeric( uid, ERR_CANTCHANGENICK, &format!("{cn} :Cannot change nick while on this channel (+N is set)"), ); return CmdResult::Fail; } // extban `n:` — a matched user can't change nick on that channel let chans = s .users .get(&uid) .map(|u| u.channels.clone()) .unwrap_or_default(); if let Some(k) = chans.into_iter().find(|k| s.extban_active(uid, k, 'n')) { let cn = s.channels.get(&k).map(|c| c.name.clone()).unwrap_or(k); s.numeric( uid, ERR_CANTCHANGENICK, &format!("{cn} :Cannot change nick here (+b n:)"), ); return CmdResult::Fail; } // +F nick-change flood — locks nick changes on the channel for 60s if let Some(cn) = s.nickflood_blocked(uid) { s.numeric( uid, ERR_CANTCHANGENICK, &format!("{cn} :Too many nick changes, try later (+F is set)"), ); return CmdResult::Fail; } } s.set_nick(uid, newnick); CmdResult::Ok } } struct UserCmd; impl Command for UserCmd { fn name(&self) -> &'static str { "USER" } fn min_params(&self) -> usize { 4 } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { if s.users.get(&uid).map(|u| u.registered).unwrap_or(false) { s.numeric(uid, ERR_ALREADYREGISTERED, ":You may not reregister"); return CmdResult::Fail; } let ident = ident_of(¶ms[0]); let realname = params[3].clone(); if let Some(u) = s.users.get_mut(&uid) { u.ident = format!("~{ident}"); u.realname = realname; } CmdResult::Ok } } struct Ping; impl Command for Ping { fn name(&self) -> &'static str { "PING" } fn min_params(&self) -> usize { 1 } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { s.send(uid, format!(":{} PONG {} :{}", s.name, s.name, params[0])); CmdResult::Ok } } struct Pong; impl Command for Pong { fn name(&self) -> &'static str { "PONG" } fn before_reg(&self) -> bool { true } fn handle(&self, _s: &mut Server, _uid: Uid, _params: &[String]) -> CmdResult { CmdResult::Ok // keepalive; nothing to do yet } } struct Quit; impl Command for Quit { fn name(&self) -> &'static str { "QUIT" } fn before_reg(&self) -> bool { true } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let reason = params .first() .cloned() .unwrap_or_else(|| "Client quit".to_string()); s.mark_quit(uid, format!("Quit: {reason}")); CmdResult::Ok } }