feature: add TLS as feature and support multiples TLS backends (currently native-tls and rustls)

This commit is contained in:
Théo Gaillard 2020-03-07 13:17:35 +08:00
parent 751c56e85b
commit 67e61e0606
No known key found for this signature in database
GPG key ID: 9C6AAAF893B070FC
16 changed files with 355 additions and 179 deletions

View file

@ -36,6 +36,9 @@ jobs:
# nochanlists on w/toml # nochanlists on w/toml
- <<: *test - <<: *test
env: FEATURES=nochanlists toml_config env: FEATURES=nochanlists toml_config
# tls-rust
- <<: *test
env: FEATURES=tls-rust
# Beta # Beta
- <<: *test - <<: *test

View file

@ -11,58 +11,81 @@ repository = "https://github.com/aatxe/irc"
readme = "README.md" readme = "README.md"
edition = "2018" edition = "2018"
[badges] [badges]
travis-ci = { repository = "aatxe/irc" } travis-ci = { repository = "aatxe/irc" }
is-it-maintained-issue-resolution = { repository = "aatxe/irc" } is-it-maintained-issue-resolution = { repository = "aatxe/irc" }
is-it-maintained-open-issues = { repository = "aatxe/irc" } is-it-maintained-open-issues = { repository = "aatxe/irc" }
[workspace] [workspace]
members = [ "./", "irc-proto" ] members = [ "./", "irc-proto/" ]
[features] [features]
default = ["ctcp", "toml_config"] default = ["ctcp", "tls-native", "toml_config"]
ctcp = [] ctcp = []
nochanlists = [] nochanlists = []
json_config = ["serde", "serde_derive", "serde_json"]
toml_config = ["serde", "serde_derive", "toml"]
yaml_config = ["serde", "serde_derive", "serde_yaml"]
proxy = ["tokio-socks"]
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"]
# Temporary transitionary features # Temporary transitionary features
json = ["json_config"] json = ["json_config"]
yaml = ["yaml_config"] yaml = ["yaml_config"]
proxy = ["tokio-socks"]
tls-native = ["native-tls", "tokio-tls"]
tls-rust = ["tokio-rustls", "webpki-roots"]
[dependencies] [dependencies]
thiserror = "1.0.2" bufstream = "0.1.0"
bufstream = "0.1" bytes = "0.5.0"
bytes = "0.5" chrono = "0.4.0"
chrono = "0.4" encoding = "0.2.0"
encoding = "0.2" futures-channel = "0.3.0"
irc-proto = { version = "*", path = "irc-proto" } futures-util = { version = "0.3.0", features = ["sink"] }
log = "0.4" irc-proto = { path = "irc-proto" }
native-tls = "0.2" log = "0.4.0"
serde = { version = "1.0", features = ["derive"], optional = true }
serde_derive = { version = "1.0", optional = true }
tokio = { version = "0.2.4", features = ["time", "net", "stream", "macros"] }
tokio-util = { version = "0.3.0", features = ["codec"] }
tokio-socks = { version = "0.2.0", optional = true }
tokio-tls = "0.3.0"
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.8", optional = true }
toml = { version = "0.5", optional = true }
pin-utils = "0.1.0-alpha.4"
parking_lot = "0.10.0" parking_lot = "0.10.0"
futures-channel = "0.3.1" pin-utils = "0.1.0-alpha.4"
futures-util = { version = "0.3.1", features = ["sink"] } thiserror = "1.0.0"
tokio = { version = "0.2.0", features = ["macros", "net", "stream", "time"] }
tokio-util = { version = "0.3.0", 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 }
# Feature - Proxy
tokio-socks = { version = "0.2.0", optional = true }
# Feature - TLS
native-tls = { version = "0.2.0", optional = true }
tokio-rustls = { version = "0.13.0", optional = true }
tokio-tls = { version = "0.3.0", optional = true }
webpki-roots = { version = "0.19.0", optional = true }
[dev-dependencies] [dev-dependencies]
futures = "0.3.1" anyhow = "1.0.0"
anyhow = "1.0.13" args = "2.0.0"
args = "2.0" env_logger = "0.7.0"
getopts = "0.2" futures = "0.3.0"
env_logger = "0.7" getopts = "0.2.0"
[[example]] [[example]]
name = "proxy" name = "simple_proxy"
path = "examples/proxy.rs" path = "examples/simple_proxy.rs"
required-features = ["proxy"] required-features = ["proxy"]
[[example]]
name = "simple_plaintext"
path = "examples/simple_plaintext.rs"
required-features = ["tls-native"]

View file

@ -109,7 +109,7 @@ proxy_server = "127.0.0.1"
proxy_port = "1080" proxy_port = "1080"
proxy_username = "" proxy_username = ""
proxy_password = "" proxy_password = ""
use_ssl = true use_tls = true
cert_path = "cert.der" cert_path = "cert.der"
client_cert_path = "client.der" client_cert_path = "client.der"
client_cert_pass = "password" client_cert_pass = "password"

View file

@ -1,5 +1,3 @@
extern crate irc;
use futures::prelude::*; use futures::prelude::*;
use irc::{client::prelude::*, error}; use irc::{client::prelude::*, error};
@ -9,21 +7,21 @@ async fn main() -> irc::error::Result<()> {
let cfg1 = Config { let cfg1 = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
..Default::default() ..Default::default()
}; };
let cfg2 = Config { let cfg2 = Config {
nickname: Some("bananas".to_owned()), nickname: Some("bananas".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
..Default::default() ..Default::default()
}; };
let configs = vec![cfg1, cfg2]; let configs = vec![cfg1, cfg2];
let mut senders = Vec::new();
let mut streams = Vec::new(); let mut streams = Vec::new();
let mut senders = Vec::new();
for config in configs { for config in configs {
// Immediate errors like failure to resolve the server's domain or to establish any connection will // Immediate errors like failure to resolve the server's domain or to establish any connection will
@ -31,8 +29,8 @@ async fn main() -> irc::error::Result<()> {
let mut client = Client::from_config(config).await?; let mut client = Client::from_config(config).await?;
client.identify()?; client.identify()?;
senders.push(client.sender());
streams.push(client.stream()?); streams.push(client.stream()?);
senders.push(client.sender());
} }
loop { loop {
@ -45,7 +43,7 @@ async fn main() -> irc::error::Result<()> {
} }
fn process_msg(sender: &Sender, message: Message) -> error::Result<()> { fn process_msg(sender: &Sender, message: Message) -> error::Result<()> {
// print!("{}", message); print!("{}", message);
match message.command { match message.command {
Command::PRIVMSG(ref target, ref msg) => { Command::PRIVMSG(ref target, ref msg) => {

View file

@ -4,9 +4,8 @@ use irc::client::prelude::*;
#[tokio::main] #[tokio::main]
async fn main() -> irc::error::Result<()> { async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("repeater".to_owned()), nickname: Some("pickles".to_owned()),
alt_nicks: vec!["blaster".to_owned(), "smg".to_owned()], server: Some("irc.pdgn.co".to_owned()),
server: Some("irc.mozilla.org".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
burst_window_length: Some(4), burst_window_length: Some(4),
max_messages_in_burst: Some(4), max_messages_in_burst: Some(4),
@ -17,6 +16,7 @@ async fn main() -> irc::error::Result<()> {
client.identify()?; client.identify()?;
let mut stream = client.stream()?; let mut stream = client.stream()?;
let sender = client.sender();
loop { loop {
let message = stream.select_next_some().await?; let message = stream.select_next_some().await?;
@ -28,7 +28,7 @@ async fn main() -> irc::error::Result<()> {
let n = tokens[0].len() + tokens[1].len() + 2; let n = tokens[0].len() + tokens[1].len() + 2;
if let Ok(count) = tokens[1].parse::<u8>() { if let Ok(count) = tokens[1].parse::<u8>() {
for _ in 0..count { for _ in 0..count {
client.send_privmsg( sender.send_privmsg(
message.response_target().unwrap_or(target), message.response_target().unwrap_or(target),
&msg[n..], &msg[n..],
)?; )?;

View file

@ -5,8 +5,7 @@ use irc::client::prelude::*;
async fn main() -> irc::error::Result<()> { async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
alt_nicks: vec!["bananas".to_owned(), "apples".to_owned()], server: Some("irc.pdgn.co".to_owned()),
server: Some("irc.mozilla.org".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
..Default::default() ..Default::default()
}; };
@ -15,14 +14,20 @@ async fn main() -> irc::error::Result<()> {
client.identify()?; client.identify()?;
let mut stream = client.stream()?; let mut stream = client.stream()?;
let sender = client.sender();
loop { while let Some(message) = stream.next().await.transpose()? {
let message = stream.select_next_some().await?; print!("{}", message);
if let Command::PRIVMSG(ref target, ref msg) = message.command { match message.command {
if msg.contains("pickles") { Command::PRIVMSG(ref target, ref msg) => {
client.send_privmsg(target, "Hi!").unwrap(); if msg.contains(client.current_nickname()) {
sender.send_privmsg(target, "Hi!")?;
}
} }
_ => (),
} }
} }
Ok(())
} }

View file

@ -5,23 +5,30 @@ use irc::client::prelude::*;
async fn main() -> irc::error::Result<()> { async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
use_ssl: Some(false), use_tls: Some(false),
..Default::default() ..Default::default()
}; };
let mut client = Client::from_config(config).await?; let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?; let mut stream = client.stream()?;
let sender = client.sender(); let sender = client.sender();
loop { while let Some(message) = stream.next().await.transpose()? {
let message = stream.select_next_some().await?; print!("{}", message);
if let Command::PRIVMSG(ref target, ref msg) = message.command { match message.command {
if msg.contains("pickles") { Command::PRIVMSG(ref target, ref msg) => {
sender.send_privmsg(target, "Hi!")?; if msg.contains(client.current_nickname()) {
sender.send_privmsg(target, "Hi!")?;
}
} }
_ => (),
} }
} }
Ok(())
} }

View file

@ -1,13 +1,11 @@
use futures::prelude::*; use futures::prelude::*;
use irc::client::data::ProxyType;
use irc::client::prelude::*; use irc::client::prelude::*;
#[tokio::main] #[tokio::main]
async fn main() -> irc::error::Result<()> { async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("rust-irc-bot".to_owned()), nickname: Some("pickles".to_owned()),
alt_nicks: vec!["bananas".to_owned(), "apples".to_owned()], server: Some("irc.pdgn.co".to_owned()),
server: Some("irc.oftc.net".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
proxy_type: Some(ProxyType::Socks5), proxy_type: Some(ProxyType::Socks5),
proxy_server: Some("127.0.0.1".to_owned()), proxy_server: Some("127.0.0.1".to_owned()),
@ -19,9 +17,19 @@ async fn main() -> irc::error::Result<()> {
client.identify()?; client.identify()?;
let mut stream = client.stream()?; let mut stream = client.stream()?;
let sender = client.sender();
while let Some(message) = stream.next().await.transpose()? { while let Some(message) = stream.next().await.transpose()? {
print!("{}", message); print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains(client.current_nickname()) {
sender.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
} }
Ok(()) Ok(())

View file

@ -7,7 +7,7 @@ use std::time::Duration;
async fn main() -> irc::error::Result<()> { async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("mastodon".to_owned()), nickname: Some("mastodon".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
..Default::default() ..Default::default()
}; };

View file

@ -7,7 +7,7 @@ use std::time::Duration;
async fn main() -> irc::error::Result<()> { async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()], channels: vec!["#rust-spam".to_owned()],
..Default::default() ..Default::default()
}; };

View file

@ -247,11 +247,9 @@ impl FromStr for Message {
args.push(suffix); args.push(suffix);
} }
Message::with_tags(tags, prefix, command, args).map_err(|e| { Message::with_tags(tags, prefix, command, args).map_err(|e| ProtocolError::InvalidMessage {
ProtocolError::InvalidMessage { string: s.to_owned(),
string: s.to_owned(), cause: e,
cause: e,
}
}) })
} }
} }

View file

@ -1,16 +1,12 @@
//! A module providing IRC connections for use by `IrcServer`s. //! A module providing IRC connections for use by `IrcServer`s.
use futures_channel::mpsc::UnboundedSender; use futures_channel::mpsc::UnboundedSender;
use futures_util::{sink::Sink, stream::Stream}; use futures_util::{sink::Sink, stream::Stream};
use native_tls::{Certificate, Identity, TlsConnector};
use std::{ use std::{
fmt, fmt,
fs::File,
io::Read,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_tls::{self, TlsStream};
use tokio_util::codec::Decoder; use tokio_util::codec::Decoder;
#[cfg(feature = "proxy")] #[cfg(feature = "proxy")]
@ -19,9 +15,37 @@ use tokio_socks::tcp::Socks5Stream;
#[cfg(feature = "proxy")] #[cfg(feature = "proxy")]
use crate::client::data::ProxyType; use crate::client::data::ProxyType;
#[cfg(feature = "tls-native")]
use std::{fs::File, io::Read};
#[cfg(feature = "tls-native")]
use native_tls::{Certificate, Identity, TlsConnector};
#[cfg(feature = "tls-native")]
use tokio_tls::{self, TlsStream};
#[cfg(feature = "tls-rust")]
use std::{
fs::File,
io::{BufReader, Error, ErrorKind},
sync::Arc,
};
#[cfg(feature = "tls-rust")]
use webpki_roots::TLS_SERVER_ROOTS;
#[cfg(feature = "tls-rust")]
use tokio_rustls::{
client::TlsStream,
rustls::{internal::pemfile::certs, ClientConfig, PrivateKey},
webpki::DNSNameRef,
TlsConnector,
};
use crate::{ use crate::{
client::{ client::{
data::Config, data::Config,
mock::MockStream,
transport::{LogView, Logged, Transport}, transport::{LogView, Logged, Transport},
}, },
error, error,
@ -33,9 +57,10 @@ pub enum Connection {
#[doc(hidden)] #[doc(hidden)]
Unsecured(Transport<TcpStream>), Unsecured(Transport<TcpStream>),
#[doc(hidden)] #[doc(hidden)]
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Secured(Transport<TlsStream<TcpStream>>), Secured(Transport<TlsStream<TcpStream>>),
#[doc(hidden)] #[doc(hidden)]
Mock(Logged<crate::client::mock::MockStream>), Mock(Logged<MockStream>),
} }
impl fmt::Debug for Connection { impl fmt::Debug for Connection {
@ -45,6 +70,7 @@ impl fmt::Debug for Connection {
"{}", "{}",
match *self { match *self {
Connection::Unsecured(_) => "Connection::Unsecured(...)", Connection::Unsecured(_) => "Connection::Unsecured(...)",
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(_) => "Connection::Secured(...)", Connection::Secured(_) => "Connection::Secured(...)",
Connection::Mock(_) => "Connection::Mock(...)", Connection::Mock(_) => "Connection::Mock(...)",
} }
@ -59,109 +85,50 @@ impl Connection {
tx: UnboundedSender<Message>, tx: UnboundedSender<Message>,
) -> error::Result<Connection> { ) -> error::Result<Connection> {
if config.use_mock_connection() { if config.use_mock_connection() {
use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; log::info!("Connecting via mock to {}.", config.server()?);
return Ok(Connection::Mock(Logged::wrap(
let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| { Self::new_mocked_transport(config, tx).await?,
error::Error::UnknownCodec { )));
codec: config.encoding().to_owned(),
}
})?;
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 stream = crate::client::mock::MockStream::new(&initial);
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = Transport::new(&config, framed, tx);
return Ok(Connection::Mock(Logged::wrap(transport)));
} }
if config.use_ssl() { #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
log::info!("Building SSL connection."); {
if config.use_tls() {
let mut builder = TlsConnector::builder(); log::info!("Connecting via TLS to {}.", config.server()?);
return Ok(Connection::Secured(
if let Some(cert_path) = config.cert_path() { Self::new_secured_transport(config, tx).await?,
let mut file = File::open(cert_path)?; ));
let mut cert_data = vec![];
file.read_to_end(&mut cert_data)?;
let cert = Certificate::from_der(&cert_data)?;
builder.add_root_certificate(cert);
log::info!("Added {} to trusted certificates.", cert_path);
} }
if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = config.client_cert_pass();
let mut file = File::open(client_cert_path)?;
let mut client_cert_data = vec![];
file.read_to_end(&mut client_cert_data)?;
let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
builder.identity(pkcs12_archive);
log::info!(
"Using {} for client certificate authentication.",
client_cert_path
);
}
let connector: tokio_tls::TlsConnector = builder.build()?.into();
let socket = Self::new_conn(config).await?;
let stream = connector.connect(config.server()?, socket).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = Transport::new(&config, framed, tx);
Ok(Connection::Secured(transport))
} else {
let stream = Self::new_conn(config).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = Transport::new(&config, framed, tx);
Ok(Connection::Unsecured(transport))
} }
log::info!("Connecting to {}.", config.server()?);
Ok(Connection::Unsecured(
Self::new_unsecured_transport(config, tx).await?,
))
} }
#[cfg(not(feature = "proxy"))] #[cfg(not(feature = "proxy"))]
async fn new_conn(config: &Config) -> error::Result<TcpStream> { async fn new_stream(config: &Config) -> error::Result<TcpStream> {
let server = config.server()?; Ok(TcpStream::connect((config.server()?, config.port())).await?)
let port = config.port();
let address = (server, port);
log::info!(
"Connecting to {:?} using SSL: {}",
address,
config.use_ssl()
);
Ok(TcpStream::connect(address).await?)
} }
#[cfg(feature = "proxy")] #[cfg(feature = "proxy")]
async fn new_conn(config: &Config) -> error::Result<TcpStream> { async fn new_stream(config: &Config) -> error::Result<TcpStream> {
let server = config.server()?; let server = config.server()?;
let port = config.port(); let port = config.port();
let address = (server, port); let address = (server, port);
log::info!(
"Connecting to {:?} using SSL: {}",
address,
config.use_ssl()
);
match config.proxy_type() { match config.proxy_type() {
ProxyType::None => Ok(TcpStream::connect(address).await?), ProxyType::None => Ok(TcpStream::connect(address).await?),
_ => { ProxyType::Socks5 => {
let proxy_server = config.proxy_server(); let proxy_server = config.proxy_server();
let proxy_port = config.proxy_port(); let proxy_port = config.proxy_port();
let proxy_username = config.proxy_username();
let proxy_password = config.proxy_password();
let proxy = (proxy_server, proxy_port); let proxy = (proxy_server, proxy_port);
log::info!("Setup proxy {:?}.", proxy); log::info!("Setup proxy {:?}.", proxy);
let proxy_username = config.proxy_username();
let proxy_password = config.proxy_password();
if !proxy_username.is_empty() || !proxy_password.is_empty() { if !proxy_username.is_empty() || !proxy_password.is_empty() {
return Ok(Socks5Stream::connect_with_password( return Ok(Socks5Stream::connect_with_password(
proxy, proxy,
@ -178,6 +145,128 @@ impl Connection {
} }
} }
async fn new_unsecured_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<TcpStream>> {
let stream = Self::new_stream(config).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
#[cfg(feature = "tls-native")]
async fn new_secured_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<TlsStream<TcpStream>>> {
let mut builder = TlsConnector::builder();
if let Some(cert_path) = config.cert_path() {
let mut file = File::open(cert_path)?;
let mut cert_data = vec![];
file.read_to_end(&mut cert_data)?;
let cert = Certificate::from_der(&cert_data)?;
builder.add_root_certificate(cert);
log::info!("Added {} to trusted certificates.", cert_path);
}
if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = config.client_cert_pass();
let mut file = File::open(client_cert_path)?;
let mut client_cert_data = vec![];
file.read_to_end(&mut client_cert_data)?;
let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
builder.identity(pkcs12_archive);
log::info!(
"Using {} for client certificate authentication.",
client_cert_path
);
}
let connector: tokio_tls::TlsConnector = builder.build()?.into();
let domain = config.server()?;
let stream = Self::new_stream(config).await?;
let stream = connector.connect(domain, stream).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
#[cfg(feature = "tls-rust")]
async fn new_secured_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<TlsStream<TcpStream>>> {
let mut builder = ClientConfig::default();
builder
.root_store
.add_server_trust_anchors(&TLS_SERVER_ROOTS);
if let Some(cert_path) = config.cert_path() {
let 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);
}
if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = PrivateKey(Vec::from(config.client_cert_pass()));
let 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"))
})?;
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
);
}
let connector = TlsConnector::from(Arc::new(builder));
let domain = DNSNameRef::try_from_ascii_str(config.server()?)?;
let stream = Self::new_stream(config).await?;
let stream = connector.connect(domain, stream).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
async fn new_mocked_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<MockStream>> {
use encoding::{label::encoding_from_whatwg_label, EncoderTrap};
let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| {
error::Error::UnknownCodec {
codec: config.encoding().to_owned(),
}
})?;
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 stream = MockStream::new(&initial);
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
/// Gets a view of the internal logging if and only if this connection is using a mock stream. /// Gets a view of the internal logging if and only if this connection is using a mock stream.
/// Otherwise, this will always return `None`. This is used for unit testing. /// Otherwise, this will always return `None`. This is used for unit testing.
pub fn log_view(&self) -> Option<LogView> { pub fn log_view(&self) -> Option<LogView> {
@ -194,6 +283,7 @@ impl Stream for Connection {
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match &mut *self { match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_next(cx), Connection::Unsecured(inner) => Pin::new(inner).poll_next(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_next(cx), Connection::Secured(inner) => Pin::new(inner).poll_next(cx),
Connection::Mock(inner) => Pin::new(inner).poll_next(cx), Connection::Mock(inner) => Pin::new(inner).poll_next(cx),
} }
@ -206,6 +296,7 @@ impl Sink<Message> for Connection {
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self { match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_ready(cx), Connection::Unsecured(inner) => Pin::new(inner).poll_ready(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_ready(cx), Connection::Secured(inner) => Pin::new(inner).poll_ready(cx),
Connection::Mock(inner) => Pin::new(inner).poll_ready(cx), Connection::Mock(inner) => Pin::new(inner).poll_ready(cx),
} }
@ -214,6 +305,7 @@ impl Sink<Message> for Connection {
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
match &mut *self { match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).start_send(item), Connection::Unsecured(inner) => Pin::new(inner).start_send(item),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).start_send(item), Connection::Secured(inner) => Pin::new(inner).start_send(item),
Connection::Mock(inner) => Pin::new(inner).start_send(item), Connection::Mock(inner) => Pin::new(inner).start_send(item),
} }
@ -222,6 +314,7 @@ impl Sink<Message> for Connection {
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self { match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_flush(cx), Connection::Unsecured(inner) => Pin::new(inner).poll_flush(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_flush(cx), Connection::Secured(inner) => Pin::new(inner).poll_flush(cx),
Connection::Mock(inner) => Pin::new(inner).poll_flush(cx), Connection::Mock(inner) => Pin::new(inner).poll_flush(cx),
} }
@ -230,6 +323,7 @@ impl Sink<Message> for Connection {
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self { match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_close(cx), Connection::Unsecured(inner) => Pin::new(inner).poll_close(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_close(cx), Connection::Secured(inner) => Pin::new(inner).poll_close(cx),
Connection::Mock(inner) => Pin::new(inner).poll_close(cx), Connection::Mock(inner) => Pin::new(inner).poll_close(cx),
} }

View file

@ -120,17 +120,21 @@ pub struct Config {
#[cfg(feature = "proxy")] #[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub proxy_password: Option<String>, pub proxy_password: Option<String>,
/// Whether or not to use SSL. /// Whether or not to use TLS.
/// Clients will automatically panic if this is enabled without SSL support. /// Clients will automatically panic if this is enabled without TLS support.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub use_ssl: Option<bool>, pub use_tls: Option<bool>,
/// The path to the SSL certificate for this server in DER format. /// The path to the TLS certificate for this server in DER format.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub cert_path: Option<String>, pub cert_path: Option<String>,
/// The path to a SSL certificate to use for CertFP client authentication in DER format. /// The path to a TLS certificate to use for CertFP client authentication in DER format.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub client_cert_path: Option<String>, pub client_cert_path: Option<String>,
/// The password for the certificate to use in CertFP authentication. /// The password for the certificate to use in CertFP authentication.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub client_cert_pass: Option<String>, pub client_cert_pass: Option<String>,
/// The encoding type used for this connection. /// The encoding type used for this connection.
@ -436,15 +440,20 @@ impl Config {
} }
/// Gets the port of the server specified in the configuration. /// Gets the port of the server specified in the configuration.
/// This defaults to 6697 (or 6667 if use_ssl is specified as false) when not specified. /// This defaults to 6697 (or 6667 if use_tls is specified as false) when not specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn port(&self) -> u16 { pub fn port(&self) -> u16 {
self.port self.port.as_ref().cloned().unwrap_or(match self.use_tls() {
.as_ref() true => 6697,
.cloned() false => 6667,
.unwrap_or(match self.use_ssl() { })
true => 6697, }
false => 6667
}) /// Gets the port of the server specified in the configuration.
/// This defaults to 6667 when not specified.
#[cfg(not(any(feature = "tls-native", feature = "tls-rust")))]
pub fn port(&self) -> u16 {
self.port.as_ref().cloned().unwrap_or(6667)
} }
/// Gets the server password specified in the configuration. /// Gets the server password specified in the configuration.
@ -490,23 +499,27 @@ impl Config {
self.proxy_password.as_ref().map_or("", String::as_str) self.proxy_password.as_ref().map_or("", String::as_str)
} }
/// Gets whether or not to use SSL with this connection. /// Gets whether or not to use TLS with this connection.
/// This defaults to true when not specified. /// This defaults to true when not specified.
pub fn use_ssl(&self) -> bool { #[cfg(any(feature = "tls-native", feature = "tls-rust"))]
self.use_ssl.as_ref().cloned().map_or(true, |s| s) pub fn use_tls(&self) -> bool {
self.use_tls.as_ref().cloned().map_or(true, |s| s)
} }
/// Gets the path to the SSL certificate in DER format if specified. /// Gets the path to the TLS certificate in DER format if specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn cert_path(&self) -> Option<&str> { pub fn cert_path(&self) -> Option<&str> {
self.cert_path.as_ref().map(String::as_str) self.cert_path.as_ref().map(String::as_str)
} }
/// Gets the path to the client authentication certificate in DER format if specified. /// Gets the path to the client authentication certificate in DER format if specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn client_cert_path(&self) -> Option<&str> { pub fn client_cert_path(&self) -> Option<&str> {
self.client_cert_path.as_ref().map(String::as_str) self.client_cert_path.as_ref().map(String::as_str)
} }
/// Gets the password to the client authentication certificate. /// Gets the password to the client authentication certificate.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn client_cert_pass(&self) -> &str { pub fn client_cert_pass(&self) -> &str {
self.client_cert_pass.as_ref().map_or("", String::as_str) self.client_cert_pass.as_ref().map_or("", String::as_str)
} }
@ -618,9 +631,16 @@ impl Config {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Config, Result}; use super::Config;
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(any(
feature = "json_config",
feature = "toml_config",
feature = "yaml_config"
))]
use super::Result;
#[allow(unused)] #[allow(unused)]
fn test_config() -> Config { fn test_config() -> Config {
Config { Config {

View file

@ -621,20 +621,20 @@ impl ClientState {
}; };
for s in seq { for s in seq {
self.send(NICKSERV(vec!( self.send(NICKSERV(vec![
s.to_string(), s.to_string(),
self.config().nickname()?.to_string(), self.config().nickname()?.to_string(),
self.config().nick_password().to_string(), self.config().nick_password().to_string(),
)))?; ]))?;
} }
*index = 0; *index = 0;
self.send(NICK(self.config().nickname()?.to_owned()))? self.send(NICK(self.config().nickname()?.to_owned()))?
} }
self.send(NICKSERV(vec!( self.send(NICKSERV(vec![
"IDENTIFY".to_string(), "IDENTIFY".to_string(),
self.config().nick_password().to_string() self.config().nick_password().to_string(),
))) ]))
} }
} }

View file

@ -21,6 +21,9 @@
//! dealing with IRC channel and user modes. They appear in methods for sending mode commands, //! dealing with IRC channel and user modes. They appear in methods for sending mode commands,
//! as well as in the parsed form of received mode commands. //! as well as in the parsed form of received mode commands.
#[cfg(feature = "proxy")]
pub use crate::client::data::ProxyType;
pub use crate::{ pub use crate::{
client::{data::Config, Client, Sender}, client::{data::Config, Client, Sender},
proto::{ proto::{

View file

@ -9,6 +9,9 @@ use futures_channel::{
}; };
use thiserror::Error; use thiserror::Error;
#[cfg(feature = "tls-rust")]
use tokio_rustls::webpki::InvalidDNSNameError;
use crate::proto::error::{MessageParseError, ProtocolError}; use crate::proto::error::{MessageParseError, ProtocolError};
/// A specialized `Result` type for the `irc` crate. /// A specialized `Result` type for the `irc` crate.
@ -27,9 +30,15 @@ pub enum Error {
Proxy(tokio_socks::Error), Proxy(tokio_socks::Error),
/// An internal TLS error. /// An internal TLS error.
#[cfg(feature = "tls-native")]
#[error("a TLS error occurred")] #[error("a TLS error occurred")]
Tls(#[source] native_tls::Error), Tls(#[source] native_tls::Error),
/// An internal DNS error.
#[cfg(feature = "tls-rust")]
#[error("a DNS error occurred")]
Dns(#[source] InvalidDNSNameError),
/// An internal synchronous channel closed. /// An internal synchronous channel closed.
#[error("a sync channel closed")] #[error("a sync channel closed")]
SyncChannelClosed(#[source] RecvError), SyncChannelClosed(#[source] RecvError),
@ -176,12 +185,20 @@ impl From<tokio_socks::Error> for Error {
} }
} }
#[cfg(feature = "tls-native")]
impl From<native_tls::Error> for Error { impl From<native_tls::Error> for Error {
fn from(e: native_tls::Error) -> Error { fn from(e: native_tls::Error) -> Error {
Error::Tls(e) Error::Tls(e)
} }
} }
#[cfg(feature = "tls-rust")]
impl From<InvalidDNSNameError> for Error {
fn from(e: InvalidDNSNameError) -> Error {
Error::Dns(e)
}
}
impl From<RecvError> for Error { impl From<RecvError> for Error {
fn from(e: RecvError) -> Error { fn from(e: RecvError) -> Error {
Error::SyncChannelClosed(e) Error::SyncChannelClosed(e)