Answer WHOIS IDLE for our pseudo-clients
All checks were successful
CI / check (push) Successful in 3m54s
All checks were successful
CI / check (push) Successful in 3m54s
A two-parameter WHOIS (/whois x x — what mIRC sends when you double-click a nick, to fetch idle time) routes the query to the target's server. For a service that server is echo, which was dropping the resulting IDLE request as an unknown command, so the WHOIS produced no output at all: InspIRCd only emits the whois numerics once the IDLE reply arrives (m_spanningtree/idle.cpp), so no reply meant total silence. Handle the inbound IDLE request and answer it for our own services and bots: ":<target> IDLE <requester> <signon> <idle>", signon = burst time, idle 0 (a pseudo-client is always active). A routed WHOIS of NickServ/ChanServ/etc. now completes.
This commit is contained in:
parent
4e12531815
commit
3f11f77a81
4 changed files with 59 additions and 0 deletions
|
|
@ -15,6 +15,10 @@ pub enum NetEvent {
|
||||||
Registered,
|
Registered,
|
||||||
EndBurst,
|
EndBurst,
|
||||||
Ping { token: String, from: Option<String> },
|
Ping { token: String, from: Option<String> },
|
||||||
|
// A remote server asks for one of our pseudo-clients' idle/signon time — the
|
||||||
|
// idle half of a routed WHOIS (`WHOIS x x` / mIRC's double-click). Must be
|
||||||
|
// answered or that WHOIS produces no output at all.
|
||||||
|
Idle { requester: String, target: String },
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
UserConnect { uid: String, nick: String, host: String, ip: String },
|
UserConnect { uid: String, nick: String, host: String, ip: String },
|
||||||
NickChange { uid: String, nick: String },
|
NickChange { uid: String, nick: String },
|
||||||
|
|
@ -66,6 +70,9 @@ pub enum NetAction {
|
||||||
Burst,
|
Burst,
|
||||||
EndBurst,
|
EndBurst,
|
||||||
Pong { token: String, from: Option<String> },
|
Pong { token: String, from: Option<String> },
|
||||||
|
// Answer a remote IDLE request for one of our pseudo-clients so a routed
|
||||||
|
// WHOIS completes: `:<target> IDLE <requester> <signon> <idle>`.
|
||||||
|
IdleReply { target: String, requester: String, signon: u64, idle: u64 },
|
||||||
IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String },
|
IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String },
|
||||||
Privmsg { from: String, to: String, text: String },
|
Privmsg { from: String, to: String, text: String },
|
||||||
Notice { from: String, to: String, text: String },
|
Notice { from: String, to: String, text: String },
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,17 @@ impl Protocol for InspIrcd {
|
||||||
.to_string();
|
.to_string();
|
||||||
vec![NetEvent::Ping { token, from: source }]
|
vec![NetEvent::Ping { token, from: source }]
|
||||||
}
|
}
|
||||||
|
"IDLE" => {
|
||||||
|
// `:<requester> IDLE <target>` — the idle half of a routed WHOIS.
|
||||||
|
// Multi-param IDLE is a reply (never sent to us); ignore it.
|
||||||
|
let target = tokens.next().unwrap_or("").to_string();
|
||||||
|
match source {
|
||||||
|
Some(requester) if !target.is_empty() && tokens.next().is_none() => {
|
||||||
|
vec![NetEvent::Idle { requester, target }]
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
"PRIVMSG" => {
|
"PRIVMSG" => {
|
||||||
let to = tokens.next().unwrap_or("").to_string();
|
let to = tokens.next().unwrap_or("").to_string();
|
||||||
vec![NetEvent::Privmsg {
|
vec![NetEvent::Privmsg {
|
||||||
|
|
@ -279,6 +290,11 @@ impl Protocol for InspIrcd {
|
||||||
let dest = from.clone().unwrap_or_else(|| self.sid.clone());
|
let dest = from.clone().unwrap_or_else(|| self.sid.clone());
|
||||||
vec![self.sourced(format!("PONG {} {}", dest, token))]
|
vec![self.sourced(format!("PONG {} {}", dest, token))]
|
||||||
}
|
}
|
||||||
|
// Reply to a routed-WHOIS idle request, sourced from the target client:
|
||||||
|
// `:<target> IDLE <requester> <signon> <idle>` (m_spanningtree/idle.cpp).
|
||||||
|
NetAction::IdleReply { target, requester, signon, idle } => {
|
||||||
|
vec![format!(":{target} IDLE {requester} {signon} {idle}")]
|
||||||
|
}
|
||||||
// insp4 UID: uuid nickts nick realhost disphost realuser dispuser ip signonts +modes :gecos
|
// insp4 UID: uuid nickts nick realhost disphost realuser dispuser ip signonts +modes :gecos
|
||||||
// (both a real AND a displayed user field — see m_spanningtree/uid.cpp Builder).
|
// (both a real AND a displayed user field — see m_spanningtree/uid.cpp Builder).
|
||||||
NetAction::IntroduceUser { uid, nick, ident, host, gecos } => vec![self.sourced(format!(
|
NetAction::IntroduceUser { uid, nick, ident, host, gecos } => vec![self.sourced(format!(
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ pub struct Engine {
|
||||||
enforce_seq: u32, // counter appended to guest_nick
|
enforce_seq: u32, // counter appended to guest_nick
|
||||||
pending_enforce: Vec<PendingEnforce>, // registered nicks awaiting identify-or-rename
|
pending_enforce: Vec<PendingEnforce>, // registered nicks awaiting identify-or-rename
|
||||||
bot_uids: HashMap<String, String>, // casefolded bot nick -> live uid (per connection)
|
bot_uids: HashMap<String, String>, // casefolded bot nick -> live uid (per connection)
|
||||||
|
services_signon: u64, // burst time our pseudo-clients signed on, for WHOIS IDLE replies
|
||||||
bot_idents: HashMap<String, u64>, // casefolded bot nick -> hash of user/host/gecos (to spot BOT CHANGE)
|
bot_idents: HashMap<String, u64>, // casefolded bot nick -> hash of user/host/gecos (to spot BOT CHANGE)
|
||||||
next_bot_index: u32,
|
next_bot_index: u32,
|
||||||
bot_channels: std::collections::HashSet<(String, String)>, // (bot nick lc, channel) the bot has joined
|
bot_channels: std::collections::HashSet<(String, String)>, // (bot nick lc, channel) the bot has joined
|
||||||
|
|
@ -240,6 +241,7 @@ impl Engine {
|
||||||
enforce_seq: 0,
|
enforce_seq: 0,
|
||||||
pending_enforce: Vec::new(),
|
pending_enforce: Vec::new(),
|
||||||
bot_uids: HashMap::new(),
|
bot_uids: HashMap::new(),
|
||||||
|
services_signon: 0,
|
||||||
bot_idents: HashMap::new(),
|
bot_idents: HashMap::new(),
|
||||||
next_bot_index: 0,
|
next_bot_index: 0,
|
||||||
bot_channels: std::collections::HashSet::new(),
|
bot_channels: std::collections::HashSet::new(),
|
||||||
|
|
@ -902,7 +904,15 @@ impl Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||||
|
// Whether `uid` is one of our own pseudo-clients — a service or a bot — so we
|
||||||
|
// answer a WHOIS IDLE request for it (and only it).
|
||||||
|
fn is_own_client(&self, uid: &str) -> bool {
|
||||||
|
self.services.iter().any(|s| s.uid() == uid) || self.bot_uids.values().any(|v| v == uid)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn startup_actions(&mut self) -> Vec<NetAction> {
|
pub fn startup_actions(&mut self) -> Vec<NetAction> {
|
||||||
|
// Fresh link: our pseudo-clients (re)sign on now — remember it for IDLE.
|
||||||
|
self.services_signon = self.now_secs();
|
||||||
// Fresh link: forget bot uids minted on any previous connection.
|
// Fresh link: forget bot uids minted on any previous connection.
|
||||||
self.bot_uids.clear();
|
self.bot_uids.clear();
|
||||||
self.bot_idents.clear();
|
self.bot_idents.clear();
|
||||||
|
|
@ -955,6 +965,16 @@ impl Engine {
|
||||||
let mut out = self.sweep_unbans();
|
let mut out = self.sweep_unbans();
|
||||||
let evout = match event {
|
let evout = match event {
|
||||||
NetEvent::Ping { token, from } => vec![NetAction::Pong { token, from }],
|
NetEvent::Ping { token, from } => vec![NetAction::Pong { token, from }],
|
||||||
|
NetEvent::Idle { requester, target } => {
|
||||||
|
// A routed WHOIS (2-param / mIRC double-click) of one of our
|
||||||
|
// pseudo-clients asks us for its idle/signon; answer or that WHOIS
|
||||||
|
// shows nothing at all. Services are always active (idle 0).
|
||||||
|
if self.is_own_client(&target) {
|
||||||
|
vec![NetAction::IdleReply { target, requester, signon: self.services_signon, idle: 0 }]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
NetEvent::UserConnect { uid, nick, host, ip } => {
|
NetEvent::UserConnect { uid, nick, host, ip } => {
|
||||||
let arriving_nick = nick.clone();
|
let arriving_nick = nick.clone();
|
||||||
self.network.user_connect(uid.clone(), nick, host, ip.clone());
|
self.network.user_connect(uid.clone(), nick, host, ip.clone());
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,22 @@
|
||||||
assert!(is_success(&out), "{out:?}");
|
assert!(is_success(&out), "{out:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A routed WHOIS (mIRC double-click) asks the service's server for its idle
|
||||||
|
// time; we must answer for our own pseudo-clients, and only them.
|
||||||
|
#[test]
|
||||||
|
fn idle_request_is_answered_for_our_clients_only() {
|
||||||
|
let mut e = engine_with("idle", "foo", "sesame");
|
||||||
|
e.set_sid("42S".into());
|
||||||
|
let ours = e.handle(NetEvent::Idle { requester: "000AAAAAB".into(), target: "42SAAAAAA".into() });
|
||||||
|
assert!(
|
||||||
|
ours.iter().any(|a| matches!(a, NetAction::IdleReply { target, requester, idle, .. }
|
||||||
|
if target == "42SAAAAAA" && requester == "000AAAAAB" && *idle == 0)),
|
||||||
|
"expected an IDLE reply for NickServ, got {ours:?}"
|
||||||
|
);
|
||||||
|
let foreign = e.handle(NetEvent::Idle { requester: "000AAAAAB".into(), target: "000AAAAAZ".into() });
|
||||||
|
assert!(!foreign.iter().any(|a| matches!(a, NetAction::IdleReply { .. })), "{foreign:?}");
|
||||||
|
}
|
||||||
|
|
||||||
// DebugServ streams auth outcomes into the log channel, and stays silent when
|
// DebugServ streams auth outcomes into the log channel, and stays silent when
|
||||||
// it isn't running.
|
// it isn't running.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue