From d19b109f3b1203723e2ada4e3ed060064732082a Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 19 Apr 2024 16:57:02 +0200 Subject: [PATCH] WHO: Add support for nick masks And some random rustfmt changes to `matchers.rs` because it was not imported before. --- sable_ircd/src/command/handlers/who.rs | 14 +++ sable_network/src/lib.rs | 3 + .../src/network/network/accessors.rs | 20 +++++ sable_network/src/policy/error.rs | 2 + .../src/policy/standard_user_policy.rs | 28 ++++++ sable_network/src/policy/user_policy.rs | 3 + sable_network/src/types/matchers.rs | 85 +++++++++---------- 7 files changed, 111 insertions(+), 44 deletions(-) diff --git a/sable_ircd/src/command/handlers/who.rs b/sable_ircd/src/command/handlers/who.rs index 4af78350..6087cee6 100644 --- a/sable_ircd/src/command/handlers/who.rs +++ b/sable_ircd/src/command/handlers/who.rs @@ -2,6 +2,8 @@ use super::*; use crate::capability::ClientCapability; use crate::utils::make_numeric; +const MAX_RESULTS: usize = 10; + #[command_handler("WHO")] fn handle_who( server: &ClientServer, @@ -31,6 +33,18 @@ fn handle_who( None, // membership ); } + } else { + let nick_pattern = NicknameMatcher::new(Pattern::new(target.to_owned())); + network + .users_by_nick_pattern(&nick_pattern) + .filter(|user| server.policy().can_list_user(&source, user).is_ok()) + .take(MAX_RESULTS) + .for_each(|user| { + send_who_reply( + response, &user, None, // channel + None, // membership + ); + }); } // If nick/channel is not found, EndOfWho should be the only numeric we send diff --git a/sable_network/src/lib.rs b/sable_network/src/lib.rs index 10e5899f..18903b4f 100644 --- a/sable_network/src/lib.rs +++ b/sable_network/src/lib.rs @@ -26,6 +26,9 @@ pub mod config; pub mod audit; pub mod types { + mod matchers; + pub use matchers::*; + mod pattern; pub use pattern::*; } diff --git a/sable_network/src/network/network/accessors.rs b/sable_network/src/network/network/accessors.rs index 070c693a..31996bac 100644 --- a/sable_network/src/network/network/accessors.rs +++ b/sable_network/src/network/network/accessors.rs @@ -106,6 +106,26 @@ impl Network { }) } + /// Look up the user currently using the given nickname pattern + pub fn users_by_nick_pattern<'a>( + &'a self, + pattern: &'a NicknameMatcher, + ) -> impl Iterator + 'a { + let pattern = std::rc::Rc::new(pattern); + let pattern2 = pattern.clone(); + let alias_users = self + .get_alias_users() + .iter() + .filter(move |(nick, _)| pattern2.matches(nick)) + .map(move |(_, user)| User::wrap(self, user)); + let users = self + .nick_bindings + .iter() + .filter(move |(nick, _)| pattern.matches(nick)) + .map(move |(_, user)| self.user(user.user).unwrap()); + alias_users.chain(users) + } + /// Remove a user from nick bindings and add it to historical users for that nick /// Return a nickname binding for the given nick. diff --git a/sable_network/src/policy/error.rs b/sable_network/src/policy/error.rs index 37a86c8f..4511666a 100644 --- a/sable_network/src/policy/error.rs +++ b/sable_network/src/policy/error.rs @@ -32,6 +32,8 @@ pub enum UserPermissionError { ReadOnlyUmode, /// User isn't logged in (and needs to be) NotLoggedIn, + /// That user is invisible, and does not share any channel with the requested + Invisible, } /// A permission error for a registration-related operation diff --git a/sable_network/src/policy/standard_user_policy.rs b/sable_network/src/policy/standard_user_policy.rs index 85bf2183..09f033b0 100644 --- a/sable_network/src/policy/standard_user_policy.rs +++ b/sable_network/src/policy/standard_user_policy.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use super::*; use UserPermissionError::*; @@ -24,4 +26,30 @@ impl UserPolicyService for StandardUserPolicy { fn can_unset_umode(&self, _user: &wrapper::User, _mode: UserModeFlag) -> PermissionResult { Ok(()) } + + fn can_list_user(&self, to_user: &User, user: &User) -> PermissionResult { + if !user.mode().has_mode(UserModeFlag::Invisible) { + return Ok(()); + } + + // If the target user is invisible, check whether they share any channel + let mut channels1: HashSet<_> = to_user + .channels() + .flat_map(|membership| membership.channel().map(|chan| chan.id())) + .collect(); + let mut channels2: HashSet<_> = user + .channels() + .flat_map(|membership| membership.channel().map(|chan| chan.id())) + .collect(); + if channels1.len() <= channels2.len() { + std::mem::swap(&mut channels1, &mut channels2); + } + for chan in channels2 { + if channels1.contains(&chan) { + return Ok(()); + } + } + + Err(PermissionError::User(Invisible)) + } } diff --git a/sable_network/src/policy/user_policy.rs b/sable_network/src/policy/user_policy.rs index b39c8d2f..bd76243f 100644 --- a/sable_network/src/policy/user_policy.rs +++ b/sable_network/src/policy/user_policy.rs @@ -7,4 +7,7 @@ pub trait UserPolicyService { fn can_set_umode(&self, user: &wrapper::User, mode: UserModeFlag) -> PermissionResult; /// Determine whether a given user can unset a given user mode on themselves fn can_unset_umode(&self, user: &wrapper::User, mode: UserModeFlag) -> PermissionResult; + /// Determine whether one user can discover another without knowing their nick + /// (eg. with `WHO *`) + fn can_list_user(&self, touser: &User, user: &User) -> PermissionResult; } diff --git a/sable_network/src/types/matchers.rs b/sable_network/src/types/matchers.rs index b936094f..c08fc1c0 100644 --- a/sable_network/src/types/matchers.rs +++ b/sable_network/src/types/matchers.rs @@ -1,94 +1,91 @@ -use crate::prelude::*; use std::net::IpAddr; +use std::ops::Deref; + use ipnet::IpNet; +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; -#[derive(Debug,Clone,Serialize,Deserialize)] -pub enum HostMatcher -{ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HostMatcher { Hostname(Pattern), Ip(IpNet), } -impl HostMatcher -{ - pub fn is_host(&self) -> bool - { +impl HostMatcher { + pub fn is_host(&self) -> bool { matches!(self, Self::Hostname(_)) } - pub fn is_ip(&self) -> bool - { + pub fn is_ip(&self) -> bool { matches!(self, Self::Ip(_)) } - pub fn matches_host(&self, hostname: &str) -> bool - { - match self - { + pub fn matches_host(&self, hostname: &str) -> bool { + match self { Self::Hostname(pat) => pat.matches(hostname), - _ => false + _ => false, } } - pub fn matches_ip(&self, addr: &IpAddr) -> bool - { - match self - { + pub fn matches_ip(&self, addr: &IpAddr) -> bool { + match self { Self::Ip(mask) => mask.contains(addr), - _ => false + _ => false, } } - pub fn matches(&self, hostname: &str, addr: &IpAddr) -> bool - { - match self - { + pub fn matches(&self, hostname: &str, addr: &IpAddr) -> bool { + match self { Self::Hostname(pat) => pat.matches(hostname), Self::Ip(mask) => mask.contains(addr), } } } -pub struct UserHostMatcher -{ +pub struct UserHostMatcher { user: Pattern, - host: HostMatcher + host: HostMatcher, } -impl UserHostMatcher -{ - pub fn matches(&self, username: &str, hostname: &str, addr: &IpAddr) -> bool - { +impl UserHostMatcher { + pub fn matches(&self, username: &str, hostname: &str, addr: &IpAddr) -> bool { self.user.matches(username) && self.host.matches(hostname, addr) } } pub struct IpMatcher(IpNet); -impl IpMatcher -{ - pub fn matches(&self, addr: &IpAddr) -> bool - { +impl IpMatcher { + pub fn matches(&self, addr: &IpAddr) -> bool { self.0.contains(addr) } } pub struct ExactIpMatcher(IpAddr); -impl ExactIpMatcher -{ - pub fn matches(&self, addr: &IpAddr) -> bool - { +impl ExactIpMatcher { + pub fn matches(&self, addr: &IpAddr) -> bool { &self.0 == addr } } pub struct NicknameMatcher(Pattern); -impl NicknameMatcher -{ - pub fn matches(&self, nick: &Nickname) -> bool - { +impl Deref for NicknameMatcher { + type Target = Pattern; + + fn deref(&self) -> &Pattern { + &self.0 + } +} + +impl NicknameMatcher { + pub fn new(pattern: Pattern) -> NicknameMatcher { + NicknameMatcher(pattern) + } + + pub fn matches(&self, nick: &Nickname) -> bool { self.0.matches(nick.as_ref()) } -} \ No newline at end of file +}