From 5bf3909d461edace8c1ffa1100cd8fb07799e26f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9o=20Gaillard?= <theo.gaillard@protonmail.com>
Date: Wed, 4 Mar 2020 14:38:01 +0800
Subject: [PATCH] feature: add proxy config

---
 Cargo.toml                                    |  7 ++
 README.md                                     |  5 ++
 examples/build-bot.rs                         |  1 -
 examples/proxy.rs                             | 28 ++++++
 examples/repeater.rs                          |  1 -
 .../{simple_ssl.rs => simple_plaintext.rs}    |  2 +-
 src/client/conn.rs                            | 71 +++++++++++++--
 src/client/data/client_config.json            |  1 -
 src/client/data/client_config.toml            |  1 -
 src/client/data/client_config.yaml            |  1 -
 src/client/data/config.rs                     | 86 +++++++++++++++----
 src/client/data/mod.rs                        |  4 +
 src/client/data/proxy.rs                      | 33 +++++++
 src/error.rs                                  | 12 +++
 14 files changed, 225 insertions(+), 28 deletions(-)
 create mode 100644 examples/proxy.rs
 rename examples/{simple_ssl.rs => simple_plaintext.rs} (96%)
 create mode 100644 src/client/data/proxy.rs

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<TcpStream> {
+        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<TcpStream> {
+        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<LogView> {
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<String>,
+    /// The proxy type to connect to.
+    #[cfg(feature = "proxy")]
+    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
+    pub proxy_type: Option<ProxyType>,
+    /// The proxy server to connect to.
+    #[cfg(feature = "proxy")]
+    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
+    pub proxy_server: Option<String>,
+    /// The proxy port to connect on.
+    #[cfg(feature = "proxy")]
+    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
+    pub proxy_port: Option<u16>,
+    /// 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<String>,
+    /// 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<String>,
     /// 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<bool>,
     /// 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<String>,
@@ -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<impl ToSocketAddrs + '_> {
-        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<IoError> for Error {
     }
 }
 
+#[cfg(feature = "proxy")]
+impl From<tokio_socks::Error> for Error {
+    fn from(e: tokio_socks::Error) -> Error {
+        Error::Proxy(e)
+    }
+}
+
 impl From<native_tls::Error> for Error {
     fn from(e: native_tls::Error) -> Error {
         Error::Tls(e)