From 4d57bff5266dfc0ea5db9d76e647c88bd00b3c4f Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 2 Feb 2023 01:32:46 +0000 Subject: [PATCH] Add support for userhost-in-names capability --- Cargo.lock | 17 ++++++++++++----- Cargo.toml | 1 + src/channel.rs | 9 +++++++-- src/channel/response.rs | 32 +++++++++++++++++++++----------- src/client.rs | 9 +++++++-- src/connection.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++++------- 6 files changed, 97 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98a5cac..4918ac9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" dependencies = [ "actix-rt", "actix_derive", - "bitflags", + "bitflags 1.3.2", "bytes", "crossbeam-channel", "futures-core", @@ -135,6 +135,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "bitflags" +version = "2.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e196f430604fcd7503056c5aaa7dfc5bc570582b928240622b075b5cad3b90dd" + +[[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -203,7 +209,7 @@ version = "4.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" dependencies = [ - "bitflags", + "bitflags 1.3.2", "clap_derive", "clap_lex", "is-terminal", @@ -1151,7 +1157,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1199,7 +1205,7 @@ version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -1395,7 +1401,7 @@ checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" dependencies = [ "ahash", "atoi", - "bitflags", + "bitflags 1.3.2", "byteorder", "bytes", "crc", @@ -1569,6 +1575,7 @@ dependencies = [ "anyhow", "argon2", "base64 0.21.0-rc.1", + "bitflags 2.0.0-rc.1", "bytes", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5f9fb3f..83ced0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ actix-rt = "2.7" anyhow = "1.0" argon2 = "0.4" base64 = "0.21.0-rc.1" +bitflags = "2.0.0-rc.1" bytes = "1.3" const_format = "0.2" chrono = "0.4" diff --git a/src/channel.rs b/src/channel.rs index a914628..ba38a8d 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -20,7 +20,7 @@ use crate::{ }, }, client::Client, - connection::{InitiatedConnection, UserId}, + connection::{Capability, InitiatedConnection, UserId}, messages::{ Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, ChannelUpdateTopic, @@ -381,7 +381,12 @@ impl Handler for Channel { } // send the user list to the user - for message in ChannelNamesList::new(self).into_messages(msg.connection.nick.to_string()) { + for message in ChannelNamesList::new(self).into_messages( + msg.connection.nick.to_string(), + msg.connection + .capabilities + .contains(Capability::USERHOST_IN_NAMES), + ) { msg.client.do_send(Broadcast { message, span: Span::current(), diff --git a/src/channel/response.rs b/src/channel/response.rs index b0d2357..2bec2e8 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -1,7 +1,9 @@ use irc_proto::{Command, Message, Prefix, Response}; +use itertools::Itertools; use crate::{ - channel::{Channel, CurrentChannelTopic}, + channel::{permissions::Permission, Channel, CurrentChannelTopic}, + connection::InitiatedConnection, SERVER_NAME, }; @@ -66,7 +68,7 @@ impl ChannelTopic { pub struct ChannelNamesList { pub channel_name: String, - pub nick_list: Vec, + pub nick_list: Vec<(Permission, InitiatedConnection)>, } impl ChannelNamesList { @@ -77,13 +79,7 @@ impl ChannelNamesList { nick_list: channel .clients .values() - .map(|v| { - format!( - "{}{}", - channel.get_user_permissions(v.user_id).into_prefix(), - v.nick - ) - }) + .map(|v| (channel.get_user_permissions(v.user_id), v.clone())) .collect(), } } @@ -97,7 +93,21 @@ impl ChannelNamesList { } #[must_use] - pub fn into_messages(self, for_user: String) -> Vec { + pub fn into_messages(self, for_user: String, with_hostnames: bool) -> Vec { + let nick_list = self + .nick_list + .into_iter() + .map(|(permission, connection)| { + let permission = permission.into_prefix(); + + if with_hostnames { + format!("{permission}{}", connection.to_nick()) + } else { + format!("{permission}{}", connection.nick) + } + }) + .join(" "); + vec![ Message { tags: None, @@ -108,7 +118,7 @@ impl ChannelNamesList { for_user.to_string(), "=".to_string(), self.channel_name, - self.nick_list.join(" "), + nick_list, ], ), }, diff --git a/src/client.rs b/src/client.rs index 60665d0..0f76b8a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ use tracing::{debug, error, info_span, instrument, warn, Instrument, Span}; use crate::{ channel::Channel, - connection::{sasl::SaslAlreadyAuthenticated, InitiatedConnection, MessageSink}, + connection::{sasl::SaslAlreadyAuthenticated, Capability, InitiatedConnection, MessageSink}, messages::{ Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelList, ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, ChannelUpdateTopic, @@ -281,7 +281,12 @@ impl Handler for Client { for list in result { let list = list.unwrap(); - for message in list.into_messages(this.connection.nick.clone()) { + for message in list.into_messages( + this.connection.nick.clone(), + this.connection + .capabilities + .contains(Capability::USERHOST_IN_NAMES), + ) { this.writer.write(message); } } diff --git a/src/connection.rs b/src/connection.rs index b1d707c..ef5abca 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -4,9 +4,11 @@ pub mod sasl; use std::{ io::{Error, ErrorKind}, net::SocketAddr, + str::FromStr, }; use actix::{io::FramedWrite, Actor, Addr}; +use bitflags::bitflags; use const_format::concatcp; use futures::{SinkExt, TryStreamExt}; use irc_proto::{ @@ -30,8 +32,6 @@ use crate::{ pub type MessageStream = FramedRead, irc_proto::IrcCodec>; pub type MessageSink = FramedWrite, irc_proto::IrcCodec>; -pub const SUPPORTED_CAPABILITIES: &[&str] = &[concatcp!("sasl=", AuthStrategy::SUPPORTED)]; - #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, sqlx::Type)] #[sqlx(transparent)] pub struct UserId(pub i64); @@ -44,6 +44,7 @@ pub struct ConnectionRequest { mode: Option, real_name: Option, user_id: Option, + capabilities: Capability, } #[derive(Clone)] @@ -54,6 +55,7 @@ pub struct InitiatedConnection { pub mode: String, pub real_name: String, pub user_id: UserId, + pub capabilities: Capability, } impl InitiatedConnection { @@ -78,6 +80,7 @@ impl TryFrom for InitiatedConnection { mode: Some(mode), real_name: Some(real_name), user_id: Some(user_id), + capabilities, } = value else { return Err(value); }; @@ -89,6 +92,7 @@ impl TryFrom for InitiatedConnection { mode, real_name, user_id, + capabilities, }) } } @@ -140,15 +144,24 @@ pub async fn negotiate_client_connection( Some("*".to_string()), CapSubCommand::LS, None, - Some(SUPPORTED_CAPABILITIES.join(" ")), + Some(Capability::SUPPORTED.join(" ")), ), }) .await .unwrap(); } Command::CAP(_, CapSubCommand::REQ, Some(arguments), None) => { + let acked = if arguments == "sasl" { + true + } else if let Ok(capability) = Capability::from_str(&arguments) { + request.capabilities |= capability; + true + } else { + false + }; + write - .send(AcknowledgedCapabilities(arguments).into_message()) + .send(AcknowledgedCapabilities(arguments, acked).into_message()) .await?; } Command::AUTHENTICATE(msg) => { @@ -229,8 +242,8 @@ impl NickNotOwnedByUser { } } -/// Return an acknowledgement to the client for their requested capabilities. -pub struct AcknowledgedCapabilities(String); +/// Return an ACK (or NAK) to the client for their requested capabilities. +pub struct AcknowledgedCapabilities(String, bool); impl AcknowledgedCapabilities { #[must_use] @@ -240,10 +253,39 @@ impl AcknowledgedCapabilities { prefix: None, command: Command::CAP( Some("*".to_string()), - CapSubCommand::ACK, + if self.1 { + CapSubCommand::ACK + } else { + CapSubCommand::NAK + }, None, Some(self.0), ), } } } + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] + pub struct Capability: u32 { + const USERHOST_IN_NAMES = 0b0000_0000_0000_0000_0000_0000_0000_0001; + } +} + +impl Capability { + pub const SUPPORTED: &'static [&'static str] = &[ + "userhost-in-names", + concatcp!("sasl=", AuthStrategy::SUPPORTED), + ]; +} + +impl FromStr for Capability { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "userhost-in-names" => Ok(Self::USERHOST_IN_NAMES), + _ => Err(()), + } + } +} -- libgit2 1.7.2