diff --git a/Cargo.toml b/Cargo.toml index bc63221..938026f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ nochanlists = [] json_config = ["serde", "serde_derive", "serde_json"] toml_config = ["serde", "serde_derive", "toml"] yaml_config = ["serde", "serde_derive", "serde_yaml"] +proxy = ["tokio-socks"] # Temporary transitionary features json = ["json_config"] @@ -44,6 +45,7 @@ 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", "stream"] } tokio-util = { version = "0.2.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 } @@ -59,3 +61,8 @@ anyhow = "1.0.13" args = "2.0" getopts = "0.2" env_logger = "0.7" + +[[example]] +name = "proxy" +path = "examples/proxy.rs" +required-features = ["proxy"] diff --git a/README.md b/README.md index c1d8e26..aee773d 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,11 @@ realname = "Test User" server = "chat.freenode.net" port = 6697 password = "" +proxy_type = "None" +proxy_server = "127.0.0.1" +proxy_port = "1080" +proxy_username = "" +proxy_password = "" use_ssl = true cert_path = "cert.der" client_cert_path = "client.der" diff --git a/examples/build-bot.rs b/examples/build-bot.rs index e3d808c..2c00e42 100644 --- a/examples/build-bot.rs +++ b/examples/build-bot.rs @@ -17,7 +17,6 @@ async fn main() -> irc::error::Result<()> { nickname: Some("irc-crate-ci".to_owned()), server: Some("irc.pdgn.co".to_owned()), alt_nicks: vec!["[irc-crate-ci]".to_owned()], - use_ssl: true, ..Default::default() }; diff --git a/examples/proxy.rs b/examples/proxy.rs new file mode 100644 index 0000000..b65cbf4 --- /dev/null +++ b/examples/proxy.rs @@ -0,0 +1,28 @@ +use futures::prelude::*; +use irc::client::data::ProxyType; +use irc::client::prelude::*; + +#[tokio::main] +async fn main() -> irc::error::Result<()> { + let config = Config { + nickname: Some("rust-irc-bot".to_owned()), + alt_nicks: vec!["bananas".to_owned(), "apples".to_owned()], + server: Some("irc.oftc.net".to_owned()), + channels: vec!["#rust-spam".to_owned()], + proxy_type: Some(ProxyType::Socks5), + proxy_server: Some("127.0.0.1".to_owned()), + proxy_port: Some(9050), + ..Default::default() + }; + + let mut client = Client::from_config(config).await?; + client.identify()?; + + let mut stream = client.stream()?; + + while let Some(message) = stream.next().await.transpose()? { + print!("{}", message); + } + + Ok(()) +} diff --git a/examples/repeater.rs b/examples/repeater.rs index 3342c7d..134d577 100644 --- a/examples/repeater.rs +++ b/examples/repeater.rs @@ -7,7 +7,6 @@ async fn main() -> irc::error::Result<()> { nickname: Some("repeater".to_owned()), alt_nicks: vec!["blaster".to_owned(), "smg".to_owned()], server: Some("irc.mozilla.org".to_owned()), - use_ssl: true, channels: vec!["#rust-spam".to_owned()], burst_window_length: Some(4), max_messages_in_burst: Some(4), diff --git a/examples/simple_ssl.rs b/examples/simple_plaintext.rs similarity index 96% rename from examples/simple_ssl.rs rename to examples/simple_plaintext.rs index 0b9133b..826e8bc 100644 --- a/examples/simple_ssl.rs +++ b/examples/simple_plaintext.rs @@ -7,7 +7,7 @@ async fn main() -> irc::error::Result<()> { nickname: Some("pickles".to_owned()), server: Some("irc.mozilla.org".to_owned()), channels: vec!["#rust-spam".to_owned()], - use_ssl: true, + use_ssl: Some(false), ..Default::default() }; diff --git a/src/client/conn.rs b/src/client/conn.rs index 9bcedf1..87727e6 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -13,6 +13,12 @@ use tokio::net::TcpStream; use tokio_tls::{self, TlsStream}; use tokio_util::codec::Decoder; +#[cfg(feature = "proxy")] +use tokio_socks::tcp::Socks5Stream; + +#[cfg(feature = "proxy")] +use crate::client::data::ProxyType; + use crate::{ client::{ data::Config, @@ -76,8 +82,8 @@ impl Connection { } if config.use_ssl() { - let domain = format!("{}", config.server()?); - log::info!("Connecting via SSL to {}.", domain); + log::info!("Building SSL connection."); + let mut builder = TlsConnector::builder(); if let Some(cert_path) = config.cert_path() { @@ -103,15 +109,14 @@ impl Connection { } let connector: tokio_tls::TlsConnector = builder.build()?.into(); - let socket = TcpStream::connect(config.to_socket_addrs()?).await?; - let stream = connector.connect(&domain, socket).await?; + 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 { - log::info!("Connecting to {}.", config.server()?); - let stream = TcpStream::connect(config.to_socket_addrs()?).await?; + let stream = Self::new_conn(config).await?; let framed = IrcCodec::new(config.encoding())?.framed(stream); let transport = Transport::new(&config, framed, tx); @@ -119,6 +124,60 @@ impl Connection { } } + #[cfg(not(feature = "proxy"))] + async fn new_conn(config: &Config) -> error::Result { + let server = config.server()?; + 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")] + async fn new_conn(config: &Config) -> error::Result { + let server = config.server()?; + let port = config.port(); + let address = (server, port); + + log::info!( + "Connecting to {:?} using SSL: {}", + address, + config.use_ssl() + ); + + match config.proxy_type() { + ProxyType::None => Ok(TcpStream::connect(address).await?), + _ => { + let proxy_server = config.proxy_server(); + let proxy_port = config.proxy_port(); + let proxy_username = config.proxy_username(); + let proxy_password = config.proxy_password(); + let proxy = (proxy_server, proxy_port); + + log::info!("Setup proxy {:?}.", proxy); + + if !proxy_username.is_empty() || !proxy_password.is_empty() { + return Ok(Socks5Stream::connect_with_password( + proxy, + address, + proxy_username, + proxy_password, + ) + .await? + .into_inner()); + } + + Ok(Socks5Stream::connect(proxy, address).await?.into_inner()) + } + } + } + /// 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. pub fn log_view(&self) -> Option { diff --git a/src/client/data/client_config.json b/src/client/data/client_config.json index 9d820d5..150b323 100644 --- a/src/client/data/client_config.json +++ b/src/client/data/client_config.json @@ -8,7 +8,6 @@ "password": "", "server": "irc.test.net", "port": 6667, - "use_ssl": false, "encoding": "UTF-8", "channels": [ "#test", diff --git a/src/client/data/client_config.toml b/src/client/data/client_config.toml index f253250..a99da73 100644 --- a/src/client/data/client_config.toml +++ b/src/client/data/client_config.toml @@ -5,7 +5,6 @@ realname = "test" server = "irc.test.net" port = 6667 password = "" -use_ssl = false encoding = "UTF-8" channels = ["#test", "#test2"] umodes = "+BR" diff --git a/src/client/data/client_config.yaml b/src/client/data/client_config.yaml index aaeef08..d252db1 100644 --- a/src/client/data/client_config.yaml +++ b/src/client/data/client_config.yaml @@ -7,7 +7,6 @@ realname: test server: irc.test.net port: 6667 password: "" -use_ssl: false encoding: UTF-8 channels: - "#test" diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 1e09175..931762b 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -7,7 +7,6 @@ use std::{ io::prelude::*, path::{Path, PathBuf}, }; -use tokio::net::ToSocketAddrs; #[cfg(feature = "json_config")] use serde_json; @@ -16,6 +15,9 @@ use serde_yaml; #[cfg(feature = "toml_config")] use toml; +#[cfg(feature = "proxy")] +use crate::client::data::proxy::ProxyType; + use crate::error::Error::InvalidConfig; #[cfg(feature = "toml_config")] use crate::error::TomlError; @@ -98,11 +100,30 @@ pub struct Config { /// The password to connect to the server. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub password: Option, + /// The proxy type to connect to. + #[cfg(feature = "proxy")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub proxy_type: Option, + /// The proxy server to connect to. + #[cfg(feature = "proxy")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub proxy_server: Option, + /// The proxy port to connect on. + #[cfg(feature = "proxy")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub proxy_port: Option, + /// The username to connect to the proxy server. + #[cfg(feature = "proxy")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub proxy_username: Option, + /// The password to connect to the proxy server. + #[cfg(feature = "proxy")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub proxy_password: Option, /// Whether or not to use SSL. /// Clients will automatically panic if this is enabled without SSL support. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))] - #[cfg_attr(feature = "serde", serde(default))] - pub use_ssl: bool, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub use_ssl: Option, /// The path to the SSL certificate for this server in DER format. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub cert_path: Option, @@ -415,31 +436,64 @@ impl Config { } /// Gets the port of the server specified in the configuration. - /// This defaults to 6667 (or 6697 if use_ssl is specified as true) when not specified. + /// This defaults to 6697 (or 6667 if use_ssl is specified as false) when not specified. pub fn port(&self) -> u16 { self.port .as_ref() .cloned() - .unwrap_or(if self.use_ssl() { 6697 } else { 6667 }) - } - - /// Return something that can be converted into a socket address by tokio. - pub(crate) fn to_socket_addrs(&self) -> Result { - let server = self.server()?; - let port = self.port(); - Ok((server, port)) + .unwrap_or(match self.use_ssl() { + true => 6697, + false => 6667 + }) } /// Gets the server password specified in the configuration. - /// This defaults to a blank string when not specified. + /// This defaults to an empty string when not specified. pub fn password(&self) -> &str { self.password.as_ref().map_or("", String::as_str) } + /// Gets the type of the proxy specified in the configuration. + /// This defaults to a None ProxyType when not specified. + #[cfg(feature = "proxy")] + pub fn proxy_type(&self) -> ProxyType { + self.proxy_type.as_ref().cloned().unwrap_or(ProxyType::None) + } + + /// Gets the address of the proxy specified in the configuration. + /// This defaults to "localhost" string when not specified. + #[cfg(feature = "proxy")] + pub fn proxy_server(&self) -> &str { + self.proxy_server + .as_ref() + .map_or("localhost", String::as_str) + } + + /// Gets the port of the proxy specified in the configuration. + /// This defaults to 1080 when not specified. + #[cfg(feature = "proxy")] + pub fn proxy_port(&self) -> u16 { + self.proxy_port.as_ref().cloned().unwrap_or(1080) + } + + /// Gets the username of the proxy specified in the configuration. + /// This defaults to an empty string when not specified. + #[cfg(feature = "proxy")] + pub fn proxy_username(&self) -> &str { + self.proxy_username.as_ref().map_or("", String::as_str) + } + + /// Gets the password of the proxy specified in the configuration. + /// This defaults to an empty string when not specified. + #[cfg(feature = "proxy")] + pub fn proxy_password(&self) -> &str { + self.proxy_password.as_ref().map_or("", String::as_str) + } + /// Gets whether or not to use SSL with this connection. - /// This defaults to false when not specified. + /// This defaults to true when not specified. pub fn use_ssl(&self) -> bool { - self.use_ssl + self.use_ssl.as_ref().cloned().map_or(true, |s| s) } /// Gets the path to the SSL certificate in DER format if specified. diff --git a/src/client/data/mod.rs b/src/client/data/mod.rs index e9f4849..5306394 100644 --- a/src/client/data/mod.rs +++ b/src/client/data/mod.rs @@ -1,7 +1,11 @@ //! Data related to IRC functionality. pub use crate::client::data::config::Config; +#[cfg(feature = "proxy")] +pub use crate::client::data::proxy::ProxyType; pub use crate::client::data::user::{AccessLevel, User}; pub mod config; +#[cfg(feature = "proxy")] +pub mod proxy; pub mod user; diff --git a/src/client/data/proxy.rs b/src/client/data/proxy.rs new file mode 100644 index 0000000..dc80f87 --- /dev/null +++ b/src/client/data/proxy.rs @@ -0,0 +1,33 @@ +//! A feature which allow us to connect to IRC via a proxy. +//! +//! ``` +//! use irc::client::prelude::Config; +//! use irc::client::data::ProxyType; +//! +//! # fn main() { +//! let config = Config { +//! nickname: Some("test".to_owned()), +//! server: Some("irc.example.com".to_owned()), +//! proxy_type: Some(ProxyType::Socks5), +//! proxy_server: Some("127.0.0.1".to_owned()), +//! proxy_port: Some(9050), +//! ..Config::default() +//! }; +//! # } +//! ``` + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// An enum which defines which type of proxy should be in use. +#[cfg(feature = "proxy")] +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ProxyType { + /// Does not use any proxy. + None, + + /// Use a SOCKS5 proxy. + /// DNS queries are also sent via the proxy. + Socks5, +} diff --git a/src/error.rs b/src/error.rs index ff78b49..4ac68fd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,11 @@ pub enum Error { #[error("an io error occurred")] Io(#[source] IoError), + /// An internal proxy error. + #[cfg(feature = "proxy")] + #[error("a proxy error occurred")] + Proxy(tokio_socks::Error), + /// An internal TLS error. #[error("a TLS error occurred")] Tls(#[source] native_tls::Error), @@ -164,6 +169,13 @@ impl From for Error { } } +#[cfg(feature = "proxy")] +impl From for Error { + fn from(e: tokio_socks::Error) -> Error { + Error::Proxy(e) + } +} + impl From for Error { fn from(e: native_tls::Error) -> Error { Error::Tls(e)