From d36999db6d9c0cd635345975b17c8feafc14edb1 Mon Sep 17 00:00:00 2001 From: Aram Peres Date: Sat, 31 Jul 2021 08:43:26 -0400 Subject: [PATCH] Add SSL support (#7) Fixes #1 --- .github/workflows/ci.yml | 7 +- Cargo.lock | 222 +++++++++++++++++++++++++++++- README.md | 12 +- nut-client/Cargo.toml | 5 +- nut-client/examples/blocking.rs | 2 +- nut-client/src/blocking/filter.rs | 47 +++++++ nut-client/src/blocking/mod.rs | 151 ++++++++++++-------- nut-client/src/cmd.rs | 8 ++ nut-client/src/config.rs | 13 +- nut-client/src/error.rs | 7 + nut-client/src/lib.rs | 2 + nut-client/src/ssl/mod.rs | 40 ++++++ rupsc/Cargo.toml | 1 + rupsc/README.md | 4 + rupsc/src/cmd.rs | 38 ++--- rupsc/src/main.rs | 39 ++++-- 16 files changed, 494 insertions(+), 104 deletions(-) create mode 100644 nut-client/src/blocking/filter.rs create mode 100644 nut-client/src/ssl/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39bdb91..85a7f3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,12 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --features env-file + + - name: Run cargo test with ssl + uses: actions-rs/cargo@v1 + with: + command: test + args: --features ssl lints: name: Lints diff --git a/Cargo.lock b/Cargo.lock index a662538..4058d68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,12 +28,36 @@ dependencies = [ "winapi", ] +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "2.33.3" @@ -49,12 +73,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -64,18 +82,82 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "nut-client" version = "0.1.0" dependencies = [ - "dotenv", + "rustls", "shell-words", + "webpki", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] @@ -87,18 +169,58 @@ dependencies = [ "nut-client", ] +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "shell-words" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -114,12 +236,98 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "wasm-bindgen" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" + +[[package]] +name = "web-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/README.md b/README.md index 4b4c713..dbf4c28 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A [Network UPS Tools](https://github.com/networkupstools/nut) (NUT) client libra - Login with username and password - List UPS devices - List variables for a UPS device +- Connect securely with SSL (optional feature) ## Getting Started @@ -60,7 +61,7 @@ fn main() -> nut_client::Result<()> { .with_debug(false) // Turn this on for debugging network chatter .build(); - let mut conn = Connection::new(config)?; + let mut conn = Connection::new(&config)?; // Print a list of all UPS devices println!("Connected UPS devices:"); @@ -78,3 +79,12 @@ fn main() -> nut_client::Result<()> { Ok(()) } ``` + +## SSL + +You can turn on SSL support by adding `.with_ssl(true)` in the `ConfigBuilder`. +This requires the `ssl` feature, which uses `rustls` under the hood. + +Note that this crate turns off all certificate validation at the moment, effectively +giving a false sense of security. If you'd like to contribute to this, see issue #8. + diff --git a/nut-client/Cargo.toml b/nut-client/Cargo.toml index 9c4c2ab..db90830 100644 --- a/nut-client/Cargo.toml +++ b/nut-client/Cargo.toml @@ -15,7 +15,8 @@ license = "MIT" [dependencies] shell-words = "1.0.0" -dotenv = { version = "0.15.0", optional = true } +rustls = { version = "0.19", optional = true, features = ["dangerous_configuration"] } +webpki = { version = "0.21", optional = true } [features] -env-file = ["dotenv"] +ssl = ["rustls", "webpki"] diff --git a/nut-client/examples/blocking.rs b/nut-client/examples/blocking.rs index 06cef65..d7d415d 100644 --- a/nut-client/examples/blocking.rs +++ b/nut-client/examples/blocking.rs @@ -22,7 +22,7 @@ fn main() -> nut_client::Result<()> { .with_debug(false) // Turn this on for debugging network chatter .build(); - let mut conn = Connection::new(config)?; + let mut conn = Connection::new(&config)?; // Print a list of all UPS devices println!("Connected UPS devices:"); diff --git a/nut-client/src/blocking/filter.rs b/nut-client/src/blocking/filter.rs new file mode 100644 index 0000000..e11035b --- /dev/null +++ b/nut-client/src/blocking/filter.rs @@ -0,0 +1,47 @@ +use std::io::{Read, Write}; +use std::net::TcpStream; + +#[allow(clippy::large_enum_variant)] +pub enum ConnectionPipeline { + Tcp(TcpStream), + + #[cfg(feature = "ssl")] + Ssl(rustls::StreamOwned), +} + +impl ConnectionPipeline { + pub fn tcp(&self) -> Option { + match self { + Self::Tcp(stream) => Some(stream.try_clone().ok()).flatten(), + _ => None, + } + } +} + +impl Read for ConnectionPipeline { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + Self::Tcp(stream) => stream.read(buf), + #[cfg(feature = "ssl")] + Self::Ssl(stream) => stream.read(buf), + } + } +} + +impl Write for ConnectionPipeline { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + Self::Tcp(stream) => stream.write(buf), + #[cfg(feature = "ssl")] + Self::Ssl(stream) => stream.write(buf), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match self { + Self::Tcp(stream) => stream.flush(), + #[cfg(feature = "ssl")] + Self::Ssl(stream) => stream.flush(), + } + } +} diff --git a/nut-client/src/blocking/mod.rs b/nut-client/src/blocking/mod.rs index b5d5d7a..4d377d9 100644 --- a/nut-client/src/blocking/mod.rs +++ b/nut-client/src/blocking/mod.rs @@ -1,9 +1,11 @@ -use std::io; use std::io::{BufRead, BufReader, Write}; use std::net::{SocketAddr, TcpStream}; +use crate::blocking::filter::ConnectionPipeline; use crate::cmd::{Command, Response}; -use crate::{Config, Host, NutError, Variable}; +use crate::{ClientError, Config, Host, NutError, Variable}; + +mod filter; /// A blocking NUT client connection. pub enum Connection { @@ -13,7 +15,7 @@ pub enum Connection { impl Connection { /// Initializes a connection to a NUT server (upsd). - pub fn new(config: Config) -> crate::Result { + pub fn new(config: &Config) -> crate::Result { match &config.host { Host::Tcp(socket_addr) => { Ok(Self::Tcp(TcpConnection::new(config.clone(), socket_addr)?)) @@ -58,17 +60,22 @@ impl Connection { } /// A blocking TCP NUT client connection. -#[derive(Debug)] pub struct TcpConnection { config: Config, - tcp_stream: TcpStream, + pipeline: ConnectionPipeline, } impl TcpConnection { fn new(config: Config, socket_addr: &SocketAddr) -> crate::Result { // Create the TCP connection let tcp_stream = TcpStream::connect_timeout(socket_addr, config.timeout)?; - let mut connection = Self { config, tcp_stream }; + let mut connection = Self { + config, + pipeline: ConnectionPipeline::Tcp(tcp_stream), + }; + + // Initialize SSL connection + connection.enable_ssl()?; // Attempt login using `config.auth` connection.login()?; @@ -76,84 +83,114 @@ impl TcpConnection { Ok(connection) } + #[cfg(feature = "ssl")] + fn enable_ssl(&mut self) -> crate::Result<()> { + if self.config.ssl { + // Send TLS request and check for 'OK' + self.write_cmd(Command::StartTLS)?; + self.read_response() + .map_err(|e| { + if let ClientError::Nut(NutError::FeatureNotConfigured) = e { + ClientError::Nut(NutError::SslNotSupported) + } else { + e + } + })? + .expect_ok()?; + + let mut config = rustls::ClientConfig::new(); + config + .dangerous() + .set_certificate_verifier(std::sync::Arc::new( + crate::ssl::NutCertificateValidator::new(&self.config), + )); + + // todo: this DNS name is temporary; should get from connection hostname? (#8) + let dns_name = webpki::DNSNameRef::try_from_ascii_str("www.google.com").unwrap(); + let sess = rustls::ClientSession::new(&std::sync::Arc::new(config), dns_name); + + // Wrap and override the TCP stream + let tcp = self + .pipeline + .tcp() + .ok_or_else(|| ClientError::from(NutError::SslNotSupported))?; + let tls = rustls::StreamOwned::new(sess, tcp); + self.pipeline = ConnectionPipeline::Ssl(tls); + + // Send a test command + self.get_network_version()?; + } + Ok(()) + } + + #[cfg(not(feature = "ssl"))] + fn enable_ssl(&mut self) -> crate::Result<()> { + Ok(()) + } + fn login(&mut self) -> crate::Result<()> { - if let Some(auth) = &self.config.auth { + if let Some(auth) = self.config.auth.clone() { // Pass username and check for 'OK' - Self::write_cmd( - &mut self.tcp_stream, - Command::SetUsername(&auth.username), - self.config.debug, - )?; - Self::read_response(&mut self.tcp_stream, self.config.debug)?.expect_ok()?; + self.write_cmd(Command::SetUsername(&auth.username))?; + self.read_response()?.expect_ok()?; // Pass password and check for 'OK' if let Some(password) = &auth.password { - Self::write_cmd( - &mut self.tcp_stream, - Command::SetPassword(password), - self.config.debug, - )?; - Self::read_response(&mut self.tcp_stream, self.config.debug)?.expect_ok()?; + self.write_cmd(Command::SetPassword(password))?; + self.read_response()?.expect_ok()?; } } Ok(()) } fn list_ups(&mut self) -> crate::Result> { - Self::write_cmd( - &mut self.tcp_stream, - Command::List(&["UPS"]), - self.config.debug, - )?; - let list = Self::read_list(&mut self.tcp_stream, &["UPS"], self.config.debug)?; + let query = &["UPS"]; + self.write_cmd(Command::List(query))?; + let list = self.read_list(query)?; list.into_iter().map(|row| row.expect_ups()).collect() } fn list_clients(&mut self, ups_name: &str) -> crate::Result> { let query = &["CLIENT", ups_name]; - Self::write_cmd( - &mut self.tcp_stream, - Command::List(query), - self.config.debug, - )?; - let list = Self::read_list(&mut self.tcp_stream, query, self.config.debug)?; + self.write_cmd(Command::List(query))?; + let list = self.read_list(query)?; list.into_iter().map(|row| row.expect_client()).collect() } fn list_vars(&mut self, ups_name: &str) -> crate::Result> { let query = &["VAR", ups_name]; - Self::write_cmd( - &mut self.tcp_stream, - Command::List(query), - self.config.debug, - )?; - let list = Self::read_list(&mut self.tcp_stream, query, self.config.debug)?; + self.write_cmd(Command::List(query))?; + let list = self.read_list(query)?; list.into_iter().map(|row| row.expect_var()).collect() } fn get_var(&mut self, ups_name: &str, variable: &str) -> crate::Result<(String, String)> { let query = &["VAR", ups_name, variable]; - Self::write_cmd(&mut self.tcp_stream, Command::Get(query), self.config.debug)?; + self.write_cmd(Command::Get(query))?; - let resp = Self::read_response(&mut self.tcp_stream, self.config.debug)?; - resp.expect_var() + self.read_response()?.expect_var() } - fn write_cmd(stream: &mut TcpStream, line: Command, debug: bool) -> crate::Result<()> { + fn get_network_version(&mut self) -> crate::Result { + self.write_cmd(Command::NetworkVersion)?; + self.read_plain_response() + } + + fn write_cmd(&mut self, line: Command) -> crate::Result<()> { let line = format!("{}\n", line); - if debug { + if self.config.debug { eprint!("DEBUG -> {}", line); } - stream.write_all(line.as_bytes())?; - stream.flush()?; + self.pipeline.write_all(line.as_bytes())?; + self.pipeline.flush()?; Ok(()) } fn parse_line( - reader: &mut BufReader<&mut TcpStream>, + reader: &mut BufReader<&mut ConnectionPipeline>, debug: bool, ) -> crate::Result> { let mut raw = String::new(); @@ -170,25 +207,27 @@ impl TcpConnection { Ok(args) } - fn read_response(stream: &mut TcpStream, debug: bool) -> crate::Result { - let mut reader = io::BufReader::new(stream); - let args = Self::parse_line(&mut reader, debug)?; + fn read_response(&mut self) -> crate::Result { + let mut reader = BufReader::new(&mut self.pipeline); + let args = Self::parse_line(&mut reader, self.config.debug)?; Response::from_args(args) } - fn read_list( - stream: &mut TcpStream, - query: &[&str], - debug: bool, - ) -> crate::Result> { - let mut reader = io::BufReader::new(stream); - let args = Self::parse_line(&mut reader, debug)?; + fn read_plain_response(&mut self) -> crate::Result { + let mut reader = BufReader::new(&mut self.pipeline); + let args = Self::parse_line(&mut reader, self.config.debug)?; + Ok(args.join(" ")) + } + + fn read_list(&mut self, query: &[&str]) -> crate::Result> { + let mut reader = BufReader::new(&mut self.pipeline); + let args = Self::parse_line(&mut reader, self.config.debug)?; Response::from_args(args)?.expect_begin_list(query)?; let mut lines: Vec = Vec::new(); loop { - let args = Self::parse_line(&mut reader, debug)?; + let args = Self::parse_line(&mut reader, self.config.debug)?; let resp = Response::from_args(args)?; match resp { diff --git a/nut-client/src/cmd.rs b/nut-client/src/cmd.rs index 45b92d1..28dc2e7 100644 --- a/nut-client/src/cmd.rs +++ b/nut-client/src/cmd.rs @@ -11,6 +11,10 @@ pub enum Command<'a> { SetPassword(&'a str), /// Queries for a list. Allows for any number of arguments, which forms a single query. List(&'a [&'a str]), + /// Tells upsd to switch to TLS, so all future communications will be encrypted. + StartTLS, + /// Queries the network version. + NetworkVersion, } impl<'a> Command<'a> { @@ -21,6 +25,8 @@ impl<'a> Command<'a> { Self::SetUsername(_) => "USERNAME", Self::SetPassword(_) => "PASSWORD", Self::List(_) => "LIST", + Self::StartTLS => "STARTTLS", + Self::NetworkVersion => "NETVER", } } @@ -31,6 +37,7 @@ impl<'a> Command<'a> { Self::SetUsername(username) => vec![username], Self::SetPassword(password) => vec![password], Self::List(query) => query.to_vec(), + _ => Vec::new(), } } } @@ -83,6 +90,7 @@ impl Response { match err_type.as_str() { "ACCESS-DENIED" => Err(NutError::AccessDenied.into()), "UNKNOWN-UPS" => Err(NutError::UnknownUps.into()), + "FEATURE-NOT-CONFIGURED" => Err(NutError::FeatureNotConfigured.into()), _ => Err(NutError::Generic(format!( "Server error: {} {}", err_type, diff --git a/nut-client/src/config.rs b/nut-client/src/config.rs index 89d6d0b..5fe9fc8 100644 --- a/nut-client/src/config.rs +++ b/nut-client/src/config.rs @@ -58,16 +58,18 @@ pub struct Config { pub(crate) host: Host, pub(crate) auth: Option, pub(crate) timeout: Duration, + pub(crate) ssl: bool, pub(crate) debug: bool, } impl Config { /// Creates a connection configuration. - pub fn new(host: Host, auth: Option, timeout: Duration, debug: bool) -> Self { + pub fn new(host: Host, auth: Option, timeout: Duration, ssl: bool, debug: bool) -> Self { Config { host, auth, timeout, + ssl, debug, } } @@ -79,6 +81,7 @@ pub struct ConfigBuilder { host: Option, auth: Option, timeout: Option, + ssl: Option, debug: Option, } @@ -107,6 +110,13 @@ impl ConfigBuilder { self } + /// Enables SSL on the connection. + #[cfg(feature = "ssl")] + pub fn with_ssl(mut self, ssl: bool) -> Self { + self.ssl = Some(ssl); + self + } + /// Enables debugging network calls by printing to stderr. pub fn with_debug(mut self, debug: bool) -> Self { self.debug = Some(debug); @@ -119,6 +129,7 @@ impl ConfigBuilder { self.host.unwrap_or_default(), self.auth, self.timeout.unwrap_or_else(|| Duration::from_secs(5)), + self.ssl.unwrap_or(false), self.debug.unwrap_or(false), ) } diff --git a/nut-client/src/error.rs b/nut-client/src/error.rs index 1f83939..2939c71 100644 --- a/nut-client/src/error.rs +++ b/nut-client/src/error.rs @@ -12,6 +12,11 @@ pub enum NutError { UnexpectedResponse, /// Occurs when the response type is not recognized by the client. UnknownResponseType(String), + /// Occurs when attempting to use SSL in a transport that doesn't support it, or + /// if the server is not configured for it. + SslNotSupported, + /// Occurs when the client used a feature that is disabled by the server. + FeatureNotConfigured, /// Generic (usually internal) client error. Generic(String), } @@ -23,6 +28,8 @@ impl fmt::Display for NutError { Self::UnknownUps => write!(f, "Unknown UPS device name"), Self::UnexpectedResponse => write!(f, "Unexpected server response content"), Self::UnknownResponseType(ty) => write!(f, "Unknown response type: {}", ty), + Self::SslNotSupported => write!(f, "SSL not supported by server or transport"), + Self::FeatureNotConfigured => write!(f, "Feature not configured by server"), Self::Generic(msg) => write!(f, "Internal client error: {}", msg), } } diff --git a/nut-client/src/lib.rs b/nut-client/src/lib.rs index 4fc0220..3c3ff02 100644 --- a/nut-client/src/lib.rs +++ b/nut-client/src/lib.rs @@ -15,4 +15,6 @@ pub mod blocking; mod cmd; mod config; mod error; +#[cfg(feature = "ssl")] +mod ssl; mod var; diff --git a/nut-client/src/ssl/mod.rs b/nut-client/src/ssl/mod.rs new file mode 100644 index 0000000..ed243dd --- /dev/null +++ b/nut-client/src/ssl/mod.rs @@ -0,0 +1,40 @@ +use crate::Config; + +/// The certificate validation mechanism for NUT. +pub struct NutCertificateValidator { + debug: bool, +} + +impl NutCertificateValidator { + /// Initialize a new instance. + pub fn new(config: &Config) -> Self { + NutCertificateValidator { + debug: config.debug, + } + } +} + +impl rustls::ServerCertVerifier for NutCertificateValidator { + fn verify_server_cert( + &self, + _roots: &rustls::RootCertStore, + presented_certs: &[rustls::Certificate], + _dns_name: webpki::DNSNameRef<'_>, + _ocsp: &[u8], + ) -> Result { + // todo: verify certificates, but not hostnames + + if self.debug { + let parsed = webpki::EndEntityCert::from(presented_certs[0].0.as_slice()).ok(); + if let Some(_parsed) = parsed { + eprintln!("DEBUG <- Certificate received and parsed"); + // todo: reading values here... https://github.com/briansmith/webpki/pull/103 + } else { + eprintln!("DEBUG <- Certificate not-parseable"); + } + } + + // trust everything for now + Ok(rustls::ServerCertVerified::assertion()) + } +} diff --git a/rupsc/Cargo.toml b/rupsc/Cargo.toml index 04acc69..87ebb2f 100644 --- a/rupsc/Cargo.toml +++ b/rupsc/Cargo.toml @@ -19,3 +19,4 @@ anyhow = "1" [dependencies.nut-client] version = "0.1.0" path = "../nut-client" +features = ["ssl"] diff --git a/rupsc/README.md b/rupsc/README.md index 145af59..e48e708 100644 --- a/rupsc/README.md +++ b/rupsc/README.md @@ -15,6 +15,7 @@ Written using the [nut-client](https://github.com/aramperes/nut-client-rs) crate - List variables for a UPS device - Get variable value of a UPS device - List clients connected to a UPS device +- Connect securely with SSL ## Installation @@ -58,6 +59,9 @@ However, there are also some additions: ```bash # Enable network debugging (global flag). ruspc -D + +# Enable SSL +rupsc -S ``` ## Pronunciation diff --git a/rupsc/src/cmd.rs b/rupsc/src/cmd.rs index 1d44f5b..6f8e6c5 100644 --- a/rupsc/src/cmd.rs +++ b/rupsc/src/cmd.rs @@ -1,11 +1,11 @@ -use crate::parser::UpsdName; use anyhow::Context; -use core::convert::TryInto; + use nut_client::blocking::Connection; +use nut_client::Config; /// Lists each UPS on the upsd server, one per line. -pub fn list_devices(server: UpsdName, with_description: bool, debug: bool) -> anyhow::Result<()> { - let mut conn = connect(server, debug)?; +pub fn list_devices(config: Config, with_description: bool) -> anyhow::Result<()> { + let mut conn = connect(config)?; for (name, description) in conn.list_ups()? { if with_description { @@ -18,11 +18,8 @@ pub fn list_devices(server: UpsdName, with_description: bool, debug: bool) -> an Ok(()) } -pub fn print_variable(server: UpsdName, variable: &str, debug: bool) -> anyhow::Result<()> { - let ups_name = server - .upsname - .with_context(|| "ups name must be specified: [@[:]]")?; - let mut conn = connect(server, debug)?; +pub fn print_variable(config: Config, ups_name: &str, variable: &str) -> anyhow::Result<()> { + let mut conn = connect(config)?; let variable = conn.get_var(ups_name, variable)?; println!("{}", variable.value()); @@ -30,11 +27,8 @@ pub fn print_variable(server: UpsdName, variable: &str, debug: bool) -> anyhow:: Ok(()) } -pub fn list_variables(server: UpsdName, debug: bool) -> anyhow::Result<()> { - let ups_name = server - .upsname - .with_context(|| "ups name must be specified: [@[:]]")?; - let mut conn = connect(server, debug)?; +pub fn list_variables(config: Config, ups_name: &str) -> anyhow::Result<()> { + let mut conn = connect(config)?; for var in conn.list_vars(ups_name)? { println!("{}", var); @@ -43,11 +37,8 @@ pub fn list_variables(server: UpsdName, debug: bool) -> anyhow::Result<()> { Ok(()) } -pub fn list_clients(server: UpsdName, debug: bool) -> anyhow::Result<()> { - let ups_name = server - .upsname - .with_context(|| "ups name must be specified: [@[:]]")?; - let mut conn = connect(server, debug)?; +pub fn list_clients(config: Config, ups_name: &str) -> anyhow::Result<()> { + let mut conn = connect(config)?; for client_ip in conn.list_clients(ups_name)? { println!("{}", client_ip); @@ -56,11 +47,6 @@ pub fn list_clients(server: UpsdName, debug: bool) -> anyhow::Result<()> { Ok(()) } -fn connect(server: UpsdName, debug: bool) -> anyhow::Result { - let host = server.try_into()?; - let config = nut_client::ConfigBuilder::new() - .with_host(host) - .with_debug(debug) - .build(); - Connection::new(config).with_context(|| format!("Failed to connect to upsd: {}", server)) +fn connect(config: Config) -> anyhow::Result { + Connection::new(&config).with_context(|| format!("Failed to connect to upsd: {:?}", &config)) } diff --git a/rupsc/src/main.rs b/rupsc/src/main.rs index d367f6e..17f5598 100644 --- a/rupsc/src/main.rs +++ b/rupsc/src/main.rs @@ -3,13 +3,14 @@ ///! This a Rust clone of [upsc](https://github.com/networkupstools/nut/blob/master/clients/upsc.c). ///! ///! P.S.: pronounced "r-oopsie". -mod cmd; -mod parser; +use core::convert::TryInto; -use crate::parser::UpsdName; use anyhow::Context; use clap::{App, Arg}; -use core::convert::TryInto; + +use crate::parser::UpsdName; +mod cmd; +mod parser; fn main() -> anyhow::Result<()> { let args = App::new(clap::crate_name!()) @@ -43,6 +44,12 @@ fn main() -> anyhow::Result<()> { .takes_value(false) .help("Enables debug mode (logs network commands to stderr)."), ) + .arg( + Arg::with_name("ssl") + .short("S") + .takes_value(false) + .help("Enables SSL on the connection with upsd."), + ) .arg( Arg::with_name("upsd-server") .required(false) @@ -63,23 +70,37 @@ fn main() -> anyhow::Result<()> { )?; let debug = args.is_present("debug"); + let ssl = args.is_present("ssl"); + + let host = server.try_into()?; + let config = nut_client::ConfigBuilder::new() + .with_host(host) + .with_debug(debug) + .with_ssl(ssl) + .build(); if args.is_present("list") { - return cmd::list_devices(server, false, debug); + return cmd::list_devices(config, false); } if args.is_present("list-full") { - return cmd::list_devices(server, true, debug); + return cmd::list_devices(config, true); } if args.is_present("clients") { - return cmd::list_clients(server, debug); + return cmd::list_clients(config, get_ups_name(&server)?); } // Fallback: prints one variable (or all of them) if let Some(variable) = args.value_of("variable") { - cmd::print_variable(server, variable, debug) + cmd::print_variable(config, get_ups_name(&server)?, variable) } else { - cmd::list_variables(server, debug) + cmd::list_variables(config, get_ups_name(&server)?) } } + +fn get_ups_name<'a>(server: &'a UpsdName) -> anyhow::Result<&'a str> { + server + .upsname + .with_context(|| "ups name must be specified: [@[:]]") +}