diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f6290f4..751e24d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - rust: ["1.71", stable] + rust: ["1.80", stable] steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master diff --git a/Cargo.toml b/Cargo.toml index 6d20f66d..fbea1f38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "irc" -version = "1.0.0" -authors = ["Aaron Weiss "] +version = "1.1.0" +authors = ["Ariel Weiss "] edition = "2018" -rust-version = "1.71" +rust-version = "1.80" description = "the irc crate – usable, async IRC for Rust" documentation = "https://docs.rs/irc/" readme = "README.md" @@ -20,17 +20,17 @@ is-it-maintained-open-issues = { repository = "aatxe/irc" } [workspace] -members = [ "./", "irc-proto/" ] +members = ["./", "irc-proto/"] [features] -default = ["ctcp", "tls-native", "channel-lists", "toml_config"] +default = ["ctcp", "tls-native", "channel-lists", "toml_config", "encoding"] ctcp = [] channel-lists = [] -json_config = ["serde", "serde/derive", "serde_derive", "serde_json"] -toml_config = ["serde", "serde/derive", "serde_derive", "toml"] -yaml_config = ["serde", "serde/derive", "serde_derive", "serde_yaml"] +json_config = ["serde", "serde_json"] +toml_config = ["serde", "toml"] +yaml_config = ["serde", "serde_yaml"] # Temporary transitionary features json = ["json_config"] yaml = ["yaml_config"] @@ -38,13 +38,24 @@ yaml = ["yaml_config"] proxy = ["tokio-socks"] tls-native = ["native-tls", "tokio-native-tls"] -tls-rust = ["tokio-rustls", "webpki-roots", "rustls-pemfile"] - +tls-rust = [ + "rustls-native-certs", + "rustls-pemfile", + "tokio-rustls", + "webpki-roots", +] +encoding = ["dep:encoding", "irc-proto/encoding"] [dependencies] -chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } -encoding = "0.2.33" -futures-util = { version = "0.3.30", default-features = false, features = ["alloc", "sink"] } +chrono = { version = "0.4.24", default-features = false, features = [ + "clock", + "std", +] } +encoding = { version = "0.2.33", optional = true } +futures-util = { version = "0.3.30", default-features = false, features = [ + "alloc", + "sink", +] } irc-proto = { version = "1.0.0", path = "irc-proto" } log = "0.4.21" parking_lot = "0.12.1" @@ -55,8 +66,7 @@ tokio-stream = "0.1.12" tokio-util = { version = "0.7.7", features = ["codec"] } # Feature - Config -serde = { version = "1.0.160", optional = true } -serde_derive = { version = "1.0.160", optional = true } +serde = { version = "1.0.160", features = ["derive"], optional = true } serde_json = { version = "1.0.95", optional = true } serde_yaml = { version = "0.9.21", optional = true } toml = { version = "0.7.3", optional = true } @@ -66,10 +76,11 @@ tokio-socks = { version = "0.5.1", optional = true } # Feature - TLS native-tls = { version = "0.2.11", optional = true } -tokio-rustls = { version = "0.24.0", features = ["dangerous_configuration"], optional = true } -rustls-pemfile = { version = "1.0.2", optional = true } tokio-native-tls = { version = "0.3.1", optional = true } -webpki-roots = { version = "0.23.0", optional = true } +rustls-native-certs = { version = "0.8", optional = true } +rustls-pemfile = { version = "2", optional = true } +tokio-rustls = { version = "0.26.0", optional = true } +webpki-roots = { version = "0.26.0", optional = true } [dev-dependencies] @@ -78,7 +89,13 @@ args = "2.2.0" env_logger = "0.11.0" futures = "0.3.30" getopts = "0.2.21" -tokio = { version = "1.27.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } +tokio = { version = "1.27.0", features = [ + "rt", + "rt-multi-thread", + "macros", + "net", + "time", +] } [[example]] diff --git a/README.md b/README.md index db4458a9..1a34303d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pu ## Getting Started -To start using the irc crate with cargo, you can add `irc = "0.15"` to your dependencies in +To start using the irc crate with cargo, you can add `irc = "1.0.0"` to your dependencies in or you can use the comman `cargo add irc`. your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude]. You'll find a number of examples to help you get started in `examples/`, throughout the documentation, and below. @@ -52,12 +52,12 @@ documentation, and below. The release of v0.14 replaced all existing APIs with one based on async/await. -```rust,no_run,edition2018 -use irc::client::prelude::*; +```rust,no_run,edition2021 use futures::prelude::*; +use irc::client::prelude::*; #[tokio::main] -async fn main() -> Result<(), failure::Error> { +async fn main() -> Result<(), anyhow::Error> { // We can also load the Config at runtime via Config::load("path/to/config.toml") let config = Config { nickname: Some("the-irc-crate".to_owned()), @@ -65,7 +65,6 @@ async fn main() -> Result<(), failure::Error> { channels: vec!["#test".to_owned()], ..Config::default() }; - let mut client = Client::from_config(config).await?; client.identify()?; @@ -80,19 +79,25 @@ async fn main() -> Result<(), failure::Error> { ``` Example Cargo.toml file: -```rust,no_run,edition2018 +```rust,no_run,edition2021 [package] name = "example" version = "0.1.0" -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -irc = "0.15.0" -tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } -futures = "0.3.0" -failure = "0.1.8" +anyhow = "1.0" +futures = "0.3" +irc = "1.0.0" +tokio = { version = "1.0", features = [ + "rt", + "rt-multi-thread", + "macros", + "net", + "time", +] } ``` ## Configuring IRC Clients @@ -122,7 +127,7 @@ port = 6697 password = "" proxy_type = "None" proxy_server = "127.0.0.1" -proxy_port = "1080" +proxy_port = 1080 proxy_username = "" proxy_password = "" use_tls = true @@ -162,7 +167,7 @@ tool should make it easier for users to migrate their old configurations to TOML ## Contributing the irc crate is a free, open source library that relies on contributions from its maintainers, -Aaron Weiss ([@aatxe][awe]) and Peter Atashian ([@retep998][bun]), as well as the broader Rust +Ariel Weiss ([@aatxe][awe]) and Peter Atashian ([@retep998][bun]), as well as the broader Rust community. It's licensed under the Mozilla Public License 2.0 whose text can be found in `LICENSE.md`. To foster an inclusive community around the irc crate, we have adopted a Code of Conduct whose text can be found in `CODE_OF_CONDUCT.md`. You can find details about how to diff --git a/irc-proto/Cargo.toml b/irc-proto/Cargo.toml index be9c457d..c95ce15e 100644 --- a/irc-proto/Cargo.toml +++ b/irc-proto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "irc-proto" -version = "1.0.0" -authors = ["Aaron Weiss "] +version = "1.1.0" +authors = ["Ariel Weiss "] edition = "2018" rust-version = "1.60" description = "The IRC protocol distilled." @@ -15,10 +15,11 @@ categories = ["network-programming"] travis-ci = { repository = "aatxe/irc" } [features] -default = ["bytes", "tokio", "tokio-util"] +default = ["tokio"] +tokio = ["bytes", "dep:tokio", "tokio-util"] [dependencies] -encoding = "0.2.33" +encoding = { version = "0.2.33", optional = true } thiserror = "1.0.40" bytes = { version = "1.4.0", optional = true } diff --git a/irc-proto/src/command.rs b/irc-proto/src/command.rs index 8f96a8bc..098b8781 100644 --- a/irc-proto/src/command.rs +++ b/irc-proto/src/command.rs @@ -227,15 +227,13 @@ impl<'a> From<&'a Command> for String { Command::NICK(ref n) => stringify("NICK", &[n]), Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*", r]), Command::OPER(ref u, ref p) => stringify("OPER", &[u, p]), - Command::UserMODE(ref u, ref m) => format!( - "MODE {}{}", - u, - m.iter().fold(String::new(), |mut acc, mode| { - acc.push(' '); - acc.push_str(&mode.to_string()); + Command::UserMODE(ref u, ref m) => { + // User modes never have arguments. + m.iter().fold(format!("MODE {u} "), |mut acc, m| { + acc.push_str(&m.flag()); acc }) - ), + } Command::SERVICE(ref nick, ref r0, ref dist, ref typ, ref r1, ref info) => { stringify("SERVICE", &[nick, r0, dist, typ, r1, info]) } @@ -248,15 +246,17 @@ impl<'a> From<&'a Command> for String { Command::JOIN(ref c, None, None) => stringify("JOIN", &[c]), Command::PART(ref c, Some(ref m)) => stringify("PART", &[c, m]), Command::PART(ref c, None) => stringify("PART", &[c]), - Command::ChannelMODE(ref u, ref m) => format!( - "MODE {}{}", - u, - m.iter().fold(String::new(), |mut acc, mode| { + Command::ChannelMODE(ref c, ref m) => { + let cmd = m.iter().fold(format!("MODE {c} "), |mut acc, m| { + acc.push_str(&m.flag()); + acc + }); + m.iter().filter_map(|m| m.arg()).fold(cmd, |mut acc, arg| { acc.push(' '); - acc.push_str(&mode.to_string()); + acc.push_str(arg); acc }) - ), + } Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c, t]), Command::TOPIC(ref c, None) => stringify("TOPIC", &[c]), Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c, t]), diff --git a/irc-proto/src/line.rs b/irc-proto/src/line.rs index 8c83cdd7..3953fef0 100644 --- a/irc-proto/src/line.rs +++ b/irc-proto/src/line.rs @@ -3,7 +3,9 @@ use std::io; use bytes::BytesMut; +#[cfg(feature = "encoding")] use encoding::label::encoding_from_whatwg_label; +#[cfg(feature = "encoding")] use encoding::{DecoderTrap, EncoderTrap, EncodingRef}; use tokio_util::codec::{Decoder, Encoder}; @@ -11,6 +13,7 @@ use crate::error; /// A line-based codec parameterized by an encoding. pub struct LineCodec { + #[cfg(feature = "encoding")] encoding: EncodingRef, next_index: usize, } @@ -18,18 +21,19 @@ pub struct LineCodec { impl LineCodec { /// Creates a new instance of LineCodec from the specified encoding. pub fn new(label: &str) -> error::Result { - encoding_from_whatwg_label(label) - .map(|enc| LineCodec { - encoding: enc, - next_index: 0, - }) - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Attempted to use unknown codec {}.", label)[..], - ) - .into() - }) + Ok(LineCodec { + #[cfg(feature = "encoding")] + encoding: match encoding_from_whatwg_label(label) { + Some(x) => x, + None => { + return Err(error::ProtocolError::Io(io::Error::new( + io::ErrorKind::InvalidInput, + &format!("Attempted to use unknown codec {}.", label)[..], + ))); + } + }, + next_index: 0, + }) } } @@ -45,14 +49,29 @@ impl Decoder for LineCodec { // Set the search start index back to 0 since we found a newline. self.next_index = 0; - // Decode the line using the codec's encoding. - match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { - Ok(data) => Ok(Some(data)), - Err(data) => Err(io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Failed to decode {} as {}.", data, self.encoding.name())[..], - ) - .into()), + #[cfg(feature = "encoding")] + { + // Decode the line using the codec's encoding. + match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { + Ok(data) => Ok(Some(data)), + Err(data) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + &format!("Failed to decode {} as {}.", data, self.encoding.name())[..], + ) + .into()), + } + } + + #[cfg(not(feature = "encoding"))] + { + match String::from_utf8(line.to_vec()) { + Ok(data) => Ok(Some(data)), + Err(data) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + &format!("Failed to decode {} as UTF-8.", data)[..], + ) + .into()), + } } } else { // Set the search start index to the current length since we know that none of the @@ -67,20 +86,27 @@ impl Encoder for LineCodec { type Error = error::ProtocolError; fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> { - // Encode the message using the codec's encoding. - let data: error::Result> = self - .encoding - .encode(&msg, EncoderTrap::Replace) - .map_err(|data| { - io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Failed to encode {} as {}.", data, self.encoding.name())[..], - ) - .into() - }); + #[cfg(feature = "encoding")] + { + // Encode the message using the codec's encoding. + let data: error::Result> = self + .encoding + .encode(&msg, EncoderTrap::Replace) + .map_err(|data| { + io::Error::new( + io::ErrorKind::InvalidInput, + &format!("Failed to encode {} as {}.", data, self.encoding.name())[..], + ) + .into() + }); + // Write the encoded message to the output buffer. + dst.extend(&data?); + } - // Write the encoded message to the output buffer. - dst.extend(&data?); + #[cfg(not(feature = "encoding"))] + { + dst.extend(msg.into_bytes()); + } Ok(()) } diff --git a/irc-proto/src/mode.rs b/irc-proto/src/mode.rs index 778e0a76..07ea335a 100644 --- a/irc-proto/src/mode.rs +++ b/irc-proto/src/mode.rs @@ -238,6 +238,24 @@ where pub fn no_prefix(inner: T) -> Mode { Mode::NoPrefix(inner) } + + /// Gets the mode flag associated with this mode with a + or - prefix as needed. + pub fn flag(&self) -> String { + match self { + Mode::Plus(mode, _) => format!("+{}", mode), + Mode::Minus(mode, _) => format!("-{}", mode), + Mode::NoPrefix(mode) => mode.to_string(), + } + } + + /// Gets the arg associated with this mode, if any. Only some channel modes support arguments, + /// e.g. b (ban) or o (oper). + pub fn arg(&self) -> Option<&str> { + match self { + Mode::Plus(_, arg) | Mode::Minus(_, arg) => arg.as_deref(), + _ => None, + } + } } impl fmt::Display for Mode @@ -246,11 +264,10 @@ where { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Mode::Plus(ref mode, Some(ref arg)) => write!(f, "+{} {}", mode, arg), - Mode::Minus(ref mode, Some(ref arg)) => write!(f, "-{} {}", mode, arg), - Mode::Plus(ref mode, None) => write!(f, "+{}", mode), - Mode::Minus(ref mode, None) => write!(f, "-{}", mode), - Mode::NoPrefix(ref mode) => write!(f, "{}", mode), + Mode::Plus(_, Some(ref arg)) | Mode::Minus(_, Some(ref arg)) => { + write!(f, "{} {}", self.flag(), arg) + } + _ => write!(f, "{}", self.flag()), } } } diff --git a/src/client/conn.rs b/src/client/conn.rs index a90f2b83..7fd2cbbe 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -25,8 +25,6 @@ use native_tls::{Certificate, Identity, TlsConnector}; #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] use tokio_native_tls::{self, TlsStream}; -#[cfg(feature = "tls-rust")] -use rustls_pemfile::certs; #[cfg(feature = "tls-rust")] use std::{ convert::TryFrom, @@ -38,10 +36,12 @@ use std::{ use tokio_rustls::client::TlsStream; #[cfg(feature = "tls-rust")] use tokio_rustls::{ - rustls::client::{ServerCertVerified, ServerCertVerifier}, - rustls::{ - self, Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerName, + rustls::client::danger::{ServerCertVerified, ServerCertVerifier}, + rustls::crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, + rustls::pki_types::{ + CertificateDer as Certificate, PrivateKeyDer as PrivateKey, ServerName, UnixTime, }, + rustls::{self, ClientConfig, RootCertStore}, TlsConnector, }; @@ -223,39 +223,84 @@ impl Connection { config: &Config, tx: UnboundedSender, ) -> error::Result>> { - struct DangerousAcceptAllVerifier; + #[derive(Debug)] + struct DangerousAcceptAllVerifier(Arc); + + impl DangerousAcceptAllVerifier { + fn new() -> Self { + DangerousAcceptAllVerifier(CryptoProvider::get_default() + .expect("no process default crypto provider has been set - application must call CryptoProvider::install_default()") + .clone()) + } + } impl ServerCertVerifier for DangerousAcceptAllVerifier { fn verify_server_cert( &self, - _: &Certificate, - _: &[Certificate], - _: &ServerName, - _: &mut dyn Iterator, - _: &[u8], - _: std::time::SystemTime, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _oscp: &[u8], + _now: UnixTime, ) -> Result { return Ok(ServerCertVerified::assertion()); } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &Certificate<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result + { + verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &Certificate<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result + { + verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } } enum ClientAuth { - SingleCert(Vec, PrivateKey), + SingleCert(Vec>, PrivateKey<'static>), NoClientAuth, } let client_auth = if let Some(client_cert_path) = config.client_cert_path() { if let Ok(file) = File::open(client_cert_path) { - let client_cert_data = certs(&mut BufReader::new(file)).map_err(|_| { - error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert")) - })?; + let client_cert_data = + rustls_pemfile::certs(&mut BufReader::new(file)).collect::>()?; - let client_cert_data = client_cert_data - .into_iter() - .map(Certificate) - .collect::>(); - - let client_cert_pass = PrivateKey(Vec::from(config.client_cert_pass())); + let client_cert_pass = config.client_cert_pass(); + let client_cert_pass = rustls_pemfile::private_key( + &mut client_cert_pass.as_bytes(), + )? + .ok_or_else(|| error::Error::InvalidConfig { + path: config.path(), + cause: error::ConfigError::UnknownConfigFormat { + format: "Failed to parse private key".to_string(), + }, + })?; log::info!( "Using {} for client certificate authentication.", @@ -279,7 +324,7 @@ impl Connection { ($builder:expr) => { match client_auth { ClientAuth::SingleCert(data, pass) => { - $builder.with_single_cert(data, pass).map_err(|err| { + $builder.with_client_auth_cert(data, pass).map_err(|err| { error::Error::Io(Error::new(ErrorKind::InvalidInput, err)) })? } @@ -288,35 +333,37 @@ impl Connection { }; } - let builder = ClientConfig::builder() - .with_safe_default_cipher_suites() - .with_safe_default_kx_groups() - .with_safe_default_protocol_versions()?; + let builder = ClientConfig::builder(); let tls_config = if config.dangerously_accept_invalid_certs() { - let builder = - builder.with_custom_certificate_verifier(Arc::new(DangerousAcceptAllVerifier)); + let builder = builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(DangerousAcceptAllVerifier::new())); make_client_auth!(builder) } else { let mut root_store = RootCertStore::empty(); - root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map( - |ta| { - OwnedTrustAnchor::from_subject_spki_name_constraints( - ta.subject, - ta.spki, - ta.name_constraints, - ) - }, - )); + #[cfg(feature = "webpki-roots")] + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let native_certs = rustls_native_certs::load_native_certs(); + for cert in native_certs.certs { + root_store.add(cert.into())?; + } if let Some(cert_path) = config.cert_path() { - if let Ok(data) = std::fs::read(cert_path) { - root_store.add(&Certificate(data)).map_err(|_| { - error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert")) - })?; + if let Ok(file) = File::open(cert_path) { + let certificates = rustls_pemfile::certs(&mut BufReader::new(file)) + .collect::, _>>()?; + let (added, ignored) = root_store.add_parsable_certificates(certificates); - log::info!("Added {} to trusted certificates.", cert_path); + if ignored > 0 { + log::warn!("Failed to parse some certificates in {}", cert_path); + } + + if added > 0 { + log::info!("Added {} to trusted certificates.", cert_path); + } } else { return Err(error::Error::InvalidConfig { path: config.path(), @@ -332,7 +379,7 @@ impl Connection { }; let connector = TlsConnector::from(Arc::new(tls_config)); - let domain = ServerName::try_from(config.server()?)?; + let domain = ServerName::try_from(config.server()?)?.to_owned(); let stream = Self::new_stream(config).await?; let stream = connector.connect(domain, stream).await?; let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); @@ -344,22 +391,31 @@ impl Connection { config: &Config, tx: UnboundedSender, ) -> error::Result> { - use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; + let init_str = config.mock_initial_value(); - let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| { - error::Error::UnknownCodec { - codec: config.encoding().to_owned(), - } - })?; + let initial = { + #[cfg(feature = "encoding")] + { + use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; - let init_str = config.mock_initial_value(); - let initial = encoding - .encode(init_str, EncoderTrap::Replace) - .map_err(|data| error::Error::CodecFailed { - codec: encoding.name(), - data: data.into_owned(), - })?; + let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| { + error::Error::UnknownCodec { + codec: config.encoding().to_owned(), + } + })?; + encoding + .encode(init_str, EncoderTrap::Replace) + .map_err(|data| error::Error::CodecFailed { + codec: encoding.name(), + data: data.into_owned(), + })? + } + #[cfg(not(feature = "encoding"))] + { + init_str.as_bytes() + } + }; let stream = MockStream::new(&initial); let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); diff --git a/src/client/mod.rs b/src/client/mod.rs index 0fffaee7..8c3f08c6 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1101,7 +1101,7 @@ mod test { error::Error, proto::{ command::Command::{Raw, PRIVMSG}, - ChannelMode, IrcCodec, Mode, + ChannelMode, IrcCodec, Mode, UserMode, }, }; use anyhow::Result; @@ -1807,10 +1807,31 @@ mod test { let mut client = Client::from_config(test_config()).await?; client.send_mode( "#test", - &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))], + &[ + Mode::Plus(ChannelMode::Oper, Some("test".to_owned())), + Mode::Minus(ChannelMode::Oper, Some("test2".to_owned())), + ], )?; client.stream()?.collect().await?; - assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n"); + assert_eq!( + &get_client_value(client)[..], + "MODE #test +o-o test test2\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_umode() -> Result<()> { + let mut client = Client::from_config(test_config()).await?; + client.send_mode( + "test", + &[ + Mode::Plus(UserMode::Invisible, None), + Mode::Plus(UserMode::MaskedHost, None), + ], + )?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "MODE test +i+x\r\n"); Ok(()) } diff --git a/src/error.rs b/src/error.rs index 391340df..21b25b86 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,7 +6,7 @@ use std::sync::mpsc::RecvError; use thiserror::Error; use tokio::sync::mpsc::error::{SendError, TrySendError}; #[cfg(feature = "tls-rust")] -use tokio_rustls::rustls::client::InvalidDnsNameError; +use tokio_rustls::rustls::pki_types::InvalidDnsNameError; use crate::proto::error::{MessageParseError, ProtocolError};