diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ad2ea72 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: {} + push: + branches: + - develop + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: ["1.60", stable] + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.rust}} + - run: cargo build --workspace --all-targets + - run: cargo build --workspace --all-targets --no-default-features + - run: cargo build --workspace --all-targets --features tls-native + - run: cargo build --workspace --all-targets --features tls-rust + # runs all tests for all targets, including examples and benchmarks. Only on + # stable, since we don't care about tests running on MSRV. + - run: cargo test --workspace --all-targets + if: matrix.rust == 'stable' + # runs all documentation tests separately, since those are not picked up by + # `--all-targets`. + - run: cargo test --workspace --doc + if: matrix.rust == 'stable' + + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all --check diff --git a/Cargo.toml b/Cargo.toml index 947d1d0..23398b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,16 @@ [package] name = "irc" version = "0.15.0" -description = "the irc crate – usable, async IRC for Rust " authors = ["Aaron Weiss "] +edition = "2018" +rust-version = "1.60" +description = "the irc crate – usable, async IRC for Rust " +documentation = "https://docs.rs/irc/" +readme = "README.md" +repository = "https://github.com/aatxe/irc" license = "MPL-2.0" keywords = ["irc", "client", "thread-safe", "async", "tokio"] categories = ["asynchronous", "network-programming"] -documentation = "https://docs.rs/irc/" -repository = "https://github.com/aatxe/irc" -readme = "README.md" -edition = "2018" [badges] @@ -37,46 +38,47 @@ yaml = ["yaml_config"] proxy = ["tokio-socks"] tls-native = ["native-tls", "tokio-native-tls"] -tls-rust = ["tokio-rustls", "webpki-roots"] +tls-rust = ["tokio-rustls", "webpki-roots", "rustls-pemfile"] [dependencies] -chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } -encoding = "0.2.0" -futures-util = { version = "0.3.0", default-features = false, features = ["alloc", "sink"] } +chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } +encoding = "0.2.33" +futures-util = { version = "0.3.28", default-features = false, features = ["alloc", "sink"] } irc-proto = { version = "0.15.0", path = "irc-proto" } -log = "0.4.0" -parking_lot = "0.11.0" -thiserror = "1.0.0" -pin-project = "1.0.2" -tokio = { version = "1.0.0", features = ["net", "time", "sync"] } -tokio-stream = "0.1.0" -tokio-util = { version = "0.6.0", features = ["codec"] } +log = "0.4.17" +parking_lot = "0.12.1" +thiserror = "1.0.40" +pin-project = "1.0.12" +tokio = { version = "1.27.0", features = ["net", "time", "sync"] } +tokio-stream = "0.1.12" +tokio-util = { version = "0.7.7", features = ["codec"] } # Feature - Config -serde = { version = "1.0.0", optional = true } -serde_derive = { version = "1.0.0", optional = true } -serde_json = { version = "1.0.0", optional = true } -serde_yaml = { version = "0.8.0", optional = true } -toml = { version = "0.5.0", optional = true } +serde = { version = "1.0.160", optional = true } +serde_derive = { version = "1.0.160", 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 } # Feature - Proxy tokio-socks = { version = "0.5.1", optional = true } # Feature - TLS -native-tls = { version = "0.2.0", optional = true } -tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"], optional = true } -tokio-native-tls = { version = "0.3.0", optional = true } -webpki-roots = { version = "0.20.0", optional = true } +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 } [dev-dependencies] -anyhow = "1.0.0" -args = "2.0.0" -env_logger = "0.7.0" -futures = "0.3.0" -getopts = "0.2.0" -tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } +anyhow = "1.0.70" +args = "2.2.0" +env_logger = "0.10.0" +futures = "0.3.28" +getopts = "0.2.21" +tokio = { version = "1.27.0", features = ["rt", "rt-multi-thread", "macros", "net", "time"] } [[example]] diff --git a/irc-proto/Cargo.toml b/irc-proto/Cargo.toml index 8df1116..af4089a 100644 --- a/irc-proto/Cargo.toml +++ b/irc-proto/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "irc-proto" version = "0.15.0" -description = "The IRC protocol distilled." authors = ["Aaron Weiss "] +edition = "2018" +rust-version = "1.60" +description = "The IRC protocol distilled." +documentation = "https://docs.rs/irc-proto/" +repository = "https://github.com/aatxe/irc" license = "MPL-2.0" keywords = ["irc", "protocol", "tokio"] categories = ["network-programming"] -documentation = "https://docs.rs/irc-proto/" -repository = "https://github.com/aatxe/irc" -edition = "2018" [badges] travis-ci = { repository = "aatxe/irc" } @@ -17,9 +18,9 @@ travis-ci = { repository = "aatxe/irc" } default = ["bytes", "tokio", "tokio-util"] [dependencies] -encoding = "0.2.0" -thiserror = "1.0.0" +encoding = "0.2.33" +thiserror = "1.0.40" -bytes = { version = "1.0.0", optional = true } -tokio = { version = "1.0.0", optional = true } -tokio-util = { version = "0.6.0", features = ["codec"], optional = true } +bytes = { version = "1.4.0", optional = true } +tokio = { version = "1.27.0", optional = true } +tokio-util = { version = "0.7.7", features = ["codec"], optional = true } diff --git a/irc-proto/src/message.rs b/irc-proto/src/message.rs index 6ef9d8f..18dbf41 100644 --- a/irc-proto/src/message.rs +++ b/irc-proto/src/message.rs @@ -550,13 +550,11 @@ mod test { #[test] fn to_message_with_colon_in_suffix() { - let msg = "PRIVMSG #test ::test" - .parse::() - .unwrap(); + let msg = "PRIVMSG #test ::test".parse::().unwrap(); let message = Message { tags: None, prefix: None, - command: PRIVMSG("#test".to_string(), ":test".to_string()) + command: PRIVMSG("#test".to_string(), ":test".to_string()), }; assert_eq!(msg, message); } diff --git a/src/client/conn.rs b/src/client/conn.rs index 6db62d5..403c18a 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -16,30 +16,32 @@ use tokio_socks::tcp::Socks5Stream; #[cfg(feature = "proxy")] use crate::client::data::ProxyType; -#[cfg(feature = "tls-native")] +#[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] use std::{fs::File, io::Read}; -#[cfg(feature = "tls-native")] +#[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] use native_tls::{Certificate, Identity, TlsConnector}; -#[cfg(feature = "tls-native")] +#[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, fs::File, io::{BufReader, Error, ErrorKind}, sync::Arc, }; - #[cfg(feature = "tls-rust")] -use webpki_roots::TLS_SERVER_ROOTS; - +use tokio_rustls::client::TlsStream; #[cfg(feature = "tls-rust")] use tokio_rustls::{ - client::TlsStream, - rustls::{self, internal::pemfile::certs, ClientConfig, PrivateKey}, - webpki::DNSNameRef, + rustls::client::{ServerCertVerified, ServerCertVerifier}, + rustls::{ + self, Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerName, + }, TlsConnector, }; @@ -157,7 +159,7 @@ impl Connection { Ok(Transport::new(&config, framed, tx)) } - #[cfg(feature = "tls-native")] + #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] async fn new_secured_transport( config: &Config, tx: UnboundedSender, @@ -221,44 +223,46 @@ impl Connection { config: &Config, tx: UnboundedSender, ) -> error::Result>> { - let mut builder = ClientConfig::default(); - builder - .root_store - .add_server_trust_anchors(&TLS_SERVER_ROOTS); + struct DangerousAcceptAllVerifier; - if let Some(cert_path) = config.cert_path() { - if let Ok(mut file) = File::open(cert_path) { - let mut cert_data = BufReader::new(file); - builder - .root_store - .add_pem_file(&mut cert_data) - .map_err(|_| { - error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert")) - })?; - log::info!("Added {} to trusted certificates.", cert_path); - } else { - return Err(error::Error::InvalidConfig { - path: config.path(), - cause: error::ConfigError::FileMissing { - file: cert_path.to_string(), - }, - }); + impl ServerCertVerifier for DangerousAcceptAllVerifier { + fn verify_server_cert( + &self, + _: &Certificate, + _: &[Certificate], + _: &ServerName, + _: &mut dyn Iterator, + _: &[u8], + _: std::time::SystemTime, + ) -> Result { + return Ok(ServerCertVerified::assertion()); } } - if let Some(client_cert_path) = config.client_cert_path() { - if let Ok(mut file) = File::open(client_cert_path) { + enum ClientAuth { + SingleCert(Vec, PrivateKey), + 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 = client_cert_data + .into_iter() + .map(Certificate) + .collect::>(); + let client_cert_pass = PrivateKey(Vec::from(config.client_cert_pass())); - builder - .set_single_client_cert(client_cert_data, client_cert_pass) - .map_err(|err| error::Error::Io(Error::new(ErrorKind::InvalidInput, err)))?; + log::info!( "Using {} for client certificate authentication.", client_cert_path ); + + ClientAuth::SingleCert(client_cert_data, client_cert_pass) } else { return Err(error::Error::InvalidConfig { path: config.path(), @@ -267,15 +271,68 @@ impl Connection { }, }); } + } else { + ClientAuth::NoClientAuth + }; + + macro_rules! make_client_auth { + ($builder:expr) => { + match client_auth { + ClientAuth::SingleCert(data, pass) => { + $builder.with_single_cert(data, pass).map_err(|err| { + error::Error::Io(Error::new(ErrorKind::InvalidInput, err)) + })? + } + ClientAuth::NoClientAuth => $builder.with_no_client_auth(), + } + }; } - if config.dangerously_accept_invalid_certs() { - builder.dangerous().set_certificate_verifier(Arc::new(DangerousAcceptAllVerifier)); - } + let builder = ClientConfig::builder() + .with_safe_default_cipher_suites() + .with_safe_default_kx_groups() + .with_safe_default_protocol_versions()?; - let connector = TlsConnector::from(Arc::new(builder)); - let domain = DNSNameRef::try_from_ascii_str(config.server()?)?; + let tls_config = if config.dangerously_accept_invalid_certs() { + let builder = + builder.with_custom_certificate_verifier(Arc::new(DangerousAcceptAllVerifier)); + 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, + ) + }, + )); + + 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")) + })?; + + log::info!("Added {} to trusted certificates.", cert_path); + } else { + return Err(error::Error::InvalidConfig { + path: config.path(), + cause: error::ConfigError::FileMissing { + file: cert_path.to_string(), + }, + }); + } + } + + let builder = builder.with_root_certificates(root_store); + make_client_auth!(builder) + }; + + let connector = TlsConnector::from(Arc::new(tls_config)); + let domain = ServerName::try_from(config.server()?)?; let stream = Self::new_stream(config).await?; let stream = connector.connect(domain, stream).await?; let framed = Framed::new(stream, IrcCodec::new(config.encoding())?); @@ -371,19 +428,3 @@ impl Sink for Connection { } } } - -#[cfg(feature = "tls-rust")] -struct DangerousAcceptAllVerifier; - -#[cfg(feature = "tls-rust")] -impl rustls::ServerCertVerifier for DangerousAcceptAllVerifier { - fn verify_server_cert( - &self, - _: &rustls::RootCertStore, - _: &[rustls::Certificate], - _: DNSNameRef, - _: &[u8] - ) -> Result { - return Ok(rustls::ServerCertVerified::assertion()); - } -} diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 7fe13a2..3a85413 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -523,7 +523,10 @@ impl Config { /// Gets whether or not to dangerously accept invalid certificates. /// This defaults to `false` when not specified. pub fn dangerously_accept_invalid_certs(&self) -> bool { - self.dangerously_accept_invalid_certs.as_ref().cloned().unwrap_or(false) + self.dangerously_accept_invalid_certs + .as_ref() + .cloned() + .unwrap_or(false) } /// Gets the path to the client authentication certificate in DER format if specified. diff --git a/src/error.rs b/src/error.rs index 0f4b8c1..c33ab42 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,9 +5,8 @@ use std::sync::mpsc::RecvError; use thiserror::Error; use tokio::sync::mpsc::error::{SendError, TrySendError}; - #[cfg(feature = "tls-rust")] -use tokio_rustls::webpki::InvalidDNSNameError; +use tokio_rustls::rustls::client::InvalidDnsNameError; use crate::proto::error::{MessageParseError, ProtocolError}; @@ -19,26 +18,51 @@ pub type Result = std::result::Result; pub enum Error { /// An internal I/O error. #[error("an io error occurred")] - Io(#[source] IoError), + Io( + #[source] + #[from] + IoError, + ), /// An internal proxy error. #[cfg(feature = "proxy")] #[error("a proxy error occurred")] - Proxy(tokio_socks::Error), + Proxy(#[from] tokio_socks::Error), /// An internal TLS error. - #[cfg(feature = "tls-native")] + #[cfg(all(feature = "tls-native", not(feature = "tls-rust")))] #[error("a TLS error occurred")] - Tls(#[source] native_tls::Error), + Tls( + #[source] + #[from] + native_tls::Error, + ), - /// An internal DNS error. + /// An internal TLS error. #[cfg(feature = "tls-rust")] - #[error("a DNS error occurred")] - Dns(#[source] InvalidDNSNameError), + #[error("a TLS error occurred")] + Tls( + #[source] + #[from] + tokio_rustls::rustls::Error, + ), + + /// An invalid DNS name was specified. + #[cfg(feature = "tls-rust")] + #[error("invalid DNS name")] + InvalidDnsNameError( + #[source] + #[from] + InvalidDnsNameError, + ), /// An internal synchronous channel closed. #[error("a sync channel closed")] - SyncChannelClosed(#[source] RecvError), + SyncChannelClosed( + #[source] + #[from] + RecvError, + ), /// An internal asynchronous channel closed. #[error("an async channel closed")] @@ -176,39 +200,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: IoError) -> Error { - Error::Io(e) - } -} - -#[cfg(feature = "proxy")] -impl From for Error { - fn from(e: tokio_socks::Error) -> Error { - Error::Proxy(e) - } -} - -#[cfg(feature = "tls-native")] -impl From for Error { - fn from(e: native_tls::Error) -> Error { - Error::Tls(e) - } -} - -#[cfg(feature = "tls-rust")] -impl From for Error { - fn from(e: InvalidDNSNameError) -> Error { - Error::Dns(e) - } -} - -impl From for Error { - fn from(e: RecvError) -> Error { - Error::SyncChannelClosed(e) - } -} - impl From> for Error { fn from(_: SendError) -> Error { Error::AsyncChannelClosed