Release 0.5.0

Rename to rups
This commit is contained in:
Aram 🍐 2021-08-01 03:17:17 -04:00
parent 539d11848e
commit feef67255f
21 changed files with 51 additions and 47 deletions

37
rups/Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "rups"
version = "0.5.0"
authors = ["Aram Peres <aram.peres@wavy.fm>"]
edition = "2018"
description = "Network UPS Tools (NUT) client library"
categories = ["network-programming"]
keywords = ["ups", "nut", "tokio", "async"]
repository = "https://github.com/aramperes/nut-client-rs"
documentation = "https://docs.rs/rups"
readme = "../README.md"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
shell-words = "1.0.0"
rustls = { version = "0.19", optional = true }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.21", optional = true }
tokio = { version = "1", optional = true, features = ["net", "io-util", "rt"] }
tokio-rustls = { version = "0.22", optional = true }
[features]
default = []
ssl = ["rustls", "rustls/dangerous_configuration", "webpki", "webpki-roots"]
async = ["tokio"]
async-ssl = ["async", "tokio-rustls", "ssl"]
# a feature gate for examples
async-rt = ["async", "tokio/rt-multi-thread", "tokio/macros"]
[[example]]
name = "async"
required-features = ["async-rt"]
[package.metadata.docs.rs]
all-features = true

73
rups/examples/async.rs Normal file
View file

@ -0,0 +1,73 @@
use std::env;
use rups::tokio::Connection;
use rups::{Auth, ConfigBuilder};
use std::convert::TryInto;
#[tokio::main]
async fn main() -> rups::Result<()> {
let host = env::var("NUT_HOST").unwrap_or_else(|_| "localhost".into());
let port = env::var("NUT_PORT")
.ok()
.map(|s| s.parse::<u16>().ok())
.flatten()
.unwrap_or(3493);
let username = env::var("NUT_USER").ok();
let password = env::var("NUT_PASSWORD").ok();
let auth = username.map(|username| Auth::new(username, password));
let config = ConfigBuilder::new()
.with_host((host, port).try_into().unwrap_or_default())
.with_auth(auth)
.with_debug(false) // Turn this on for debugging network chatter
.build();
let mut conn = Connection::new(&config).await?;
// Get server information
println!("NUT server:");
println!("\tVersion: {}", conn.get_server_version().await?);
println!("\tNetwork Version: {}", conn.get_network_version().await?);
// Print a list of all UPS devices
println!("Connected UPS devices:");
for (name, description) in conn.list_ups().await? {
println!("\t- Name: {}", name);
println!("\t Description: {}", description);
println!(
"\t Number of logins: {}",
conn.get_num_logins(&name).await?
);
// Get list of mutable variables
let mutable_vars = conn.list_mutable_vars(&name).await?;
// List UPS variables (key = val)
println!("\t Mutable Variables:");
for var in mutable_vars.iter() {
println!("\t\t- {}", var);
println!("\t\t {:?}", conn.get_var_type(&name, var.name()).await?);
}
// List UPS immutable properties (key = val)
println!("\t Immutable Properties:");
for var in conn.list_vars(&name).await? {
if mutable_vars.iter().any(|v| v.name() == var.name()) {
continue;
}
println!("\t\t- {}", var);
println!("\t\t {:?}", conn.get_var_type(&name, var.name()).await?);
}
// List UPS commands
println!("\t Commands:");
for cmd in conn.list_commands(&name).await? {
let description = conn.get_command_description(&name, &cmd).await?;
println!("\t\t- {} ({})", cmd, description);
}
}
// Gracefully shut down the connection using the `LOGOUT` command
conn.close().await
}

69
rups/examples/blocking.rs Normal file
View file

@ -0,0 +1,69 @@
use std::convert::TryInto;
use std::env;
use rups::blocking::Connection;
use rups::{Auth, ConfigBuilder};
fn main() -> rups::Result<()> {
let host = env::var("NUT_HOST").unwrap_or_else(|_| "localhost".into());
let port = env::var("NUT_PORT")
.ok()
.map(|s| s.parse::<u16>().ok())
.flatten()
.unwrap_or(3493);
let username = env::var("NUT_USER").ok();
let password = env::var("NUT_PASSWORD").ok();
let auth = username.map(|username| Auth::new(username, password));
let config = ConfigBuilder::new()
.with_host((host, port).try_into().unwrap_or_default())
.with_auth(auth)
.with_debug(false) // Turn this on for debugging network chatter
.build();
let mut conn = Connection::new(&config)?;
// Get server information
println!("NUT server:");
println!("\tVersion: {}", conn.get_server_version()?);
println!("\tNetwork Version: {}", conn.get_network_version()?);
// Print a list of all UPS devices
println!("Connected UPS devices:");
for (name, description) in conn.list_ups()? {
println!("\t- Name: {}", name);
println!("\t Description: {}", description);
println!("\t Number of logins: {}", conn.get_num_logins(&name)?);
// Get list of mutable variables
let mutable_vars = conn.list_mutable_vars(&name)?;
// List UPS variables (key = val)
println!("\t Mutable Variables:");
for var in mutable_vars.iter() {
println!("\t\t- {}", var);
println!("\t\t {:?}", conn.get_var_type(&name, var.name())?);
}
// List UPS immutable properties (key = val)
println!("\t Immutable Properties:");
for var in conn.list_vars(&name)? {
if mutable_vars.iter().any(|v| v.name() == var.name()) {
continue;
}
println!("\t\t- {}", var);
println!("\t\t {:?}", conn.get_var_type(&name, var.name())?);
}
// List UPS commands
println!("\t Commands:");
for cmd in conn.list_commands(&name)? {
let description = conn.get_command_description(&name, &cmd)?;
println!("\t\t- {} ({})", cmd, description);
}
}
// Gracefully shut down the connection using the `LOGOUT` command
conn.close()
}

183
rups/src/blocking/mod.rs Normal file
View file

@ -0,0 +1,183 @@
use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpStream};
use crate::blocking::stream::ConnectionStream;
use crate::cmd::{Command, Response};
use crate::{ClientError, Config, Host, NutError};
mod stream;
/// A blocking NUT client connection.
pub enum Connection {
/// A TCP connection.
Tcp(TcpConnection),
}
impl Connection {
/// Initializes a connection to a NUT server (upsd).
pub fn new(config: &Config) -> crate::Result<Self> {
let mut conn = match &config.host {
Host::Tcp(host) => Self::Tcp(TcpConnection::new(config.clone(), &host.addr)?),
};
conn.get_network_version()?;
conn.login(config)?;
Ok(conn)
}
/// Gracefully closes the connection.
pub fn close(mut self) -> crate::Result<()> {
self.logout()?;
Ok(())
}
/// Sends username and password, as applicable.
fn login(&mut self, config: &Config) -> crate::Result<()> {
if let Some(auth) = config.auth.clone() {
// Pass username and check for 'OK'
self.set_username(&auth.username)?;
// Pass password and check for 'OK'
if let Some(password) = &auth.password {
self.set_password(password)?;
}
}
Ok(())
}
}
/// A blocking TCP NUT client connection.
pub struct TcpConnection {
config: Config,
stream: ConnectionStream,
}
impl TcpConnection {
fn new(config: Config, socket_addr: &SocketAddr) -> crate::Result<Self> {
// Create the TCP connection
let tcp_stream = TcpStream::connect_timeout(socket_addr, config.timeout)?;
let mut connection = Self {
config,
stream: ConnectionStream::Plain(tcp_stream),
};
connection = connection.enable_ssl()?;
Ok(connection)
}
#[cfg(feature = "ssl")]
fn enable_ssl(mut self) -> crate::Result<Self> {
if self.config.ssl {
self.write_cmd(Command::StartTLS)?;
self.read_response()
.map_err(|e| {
if let crate::ClientError::Nut(NutError::FeatureNotConfigured) = e {
crate::ClientError::Nut(NutError::SslNotSupported)
} else {
e
}
})?
.expect_ok()?;
let mut ssl_config = rustls::ClientConfig::new();
let sess = if self.config.ssl_insecure {
ssl_config
.dangerous()
.set_certificate_verifier(std::sync::Arc::new(
crate::ssl::InsecureCertificateValidator::new(&self.config),
));
let dns_name = webpki::DNSNameRef::try_from_ascii_str("www.google.com").unwrap();
rustls::ClientSession::new(&std::sync::Arc::new(ssl_config), dns_name)
} else {
// Try to get hostname as given (e.g. localhost can be used for strict SSL, but not 127.0.0.1)
let hostname = self
.config
.host
.hostname()
.ok_or(ClientError::Nut(NutError::SslInvalidHostname))?;
let dns_name = webpki::DNSNameRef::try_from_ascii_str(&hostname)
.map_err(|_| ClientError::Nut(NutError::SslInvalidHostname))?;
ssl_config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
rustls::ClientSession::new(&std::sync::Arc::new(ssl_config), dns_name)
};
// Wrap and override the TCP stream
self.stream = self.stream.upgrade_ssl(sess)?;
}
Ok(self)
}
#[cfg(not(feature = "ssl"))]
fn enable_ssl(self) -> crate::Result<Self> {
Ok(self)
}
pub(crate) fn write_cmd(&mut self, line: Command) -> crate::Result<()> {
let line = format!("{}\n", line);
if self.config.debug {
eprint!("DEBUG -> {}", line);
}
self.stream.write_all(line.as_bytes())?;
self.stream.flush()?;
Ok(())
}
fn parse_line(
reader: &mut BufReader<&mut ConnectionStream>,
debug: bool,
) -> crate::Result<Vec<String>> {
let mut raw = String::new();
reader.read_line(&mut raw)?;
if debug {
eprint!("DEBUG <- {}", raw);
}
raw = raw[..raw.len() - 1].to_string(); // Strip off \n
// Parse args by splitting whitespace, minding quotes for args with multiple words
let args = shell_words::split(&raw)
.map_err(|e| NutError::Generic(format!("Parsing server response failed: {}", e)))?;
Ok(args)
}
pub(crate) fn read_response(&mut self) -> crate::Result<Response> {
let mut reader = BufReader::new(&mut self.stream);
let args = Self::parse_line(&mut reader, self.config.debug)?;
Response::from_args(args)
}
pub(crate) fn read_plain_response(&mut self) -> crate::Result<String> {
let mut reader = BufReader::new(&mut self.stream);
let args = Self::parse_line(&mut reader, self.config.debug)?;
Ok(args.join(" "))
}
pub(crate) fn read_list(&mut self, query: &[&str]) -> crate::Result<Vec<Response>> {
let mut reader = BufReader::new(&mut self.stream);
let args = Self::parse_line(&mut reader, self.config.debug)?;
Response::from_args(args)?.expect_begin_list(query)?;
let mut lines: Vec<Response> = Vec::new();
loop {
let args = Self::parse_line(&mut reader, self.config.debug)?;
let resp = Response::from_args(args)?;
match resp {
Response::EndList(_) => {
break;
}
_ => lines.push(resp),
}
}
Ok(lines)
}
}

View file

@ -0,0 +1,50 @@
use std::io::{Read, Write};
use std::net::TcpStream;
/// A wrapper for various synchronous stream types.
pub enum ConnectionStream {
/// A plain TCP stream.
Plain(TcpStream),
/// A stream wrapped with SSL using `rustls`.
#[cfg(feature = "ssl")]
Ssl(Box<rustls::StreamOwned<rustls::ClientSession, ConnectionStream>>),
}
impl ConnectionStream {
/// Wraps the current stream with SSL using `rustls`.
#[cfg(feature = "ssl")]
pub fn upgrade_ssl(self, session: rustls::ClientSession) -> crate::Result<ConnectionStream> {
Ok(ConnectionStream::Ssl(Box::new(rustls::StreamOwned::new(
session, self,
))))
}
}
impl Read for ConnectionStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Plain(stream) => stream.read(buf),
#[cfg(feature = "ssl")]
Self::Ssl(stream) => stream.read(buf),
}
}
}
impl Write for ConnectionStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
Self::Plain(stream) => stream.write(buf),
#[cfg(feature = "ssl")]
Self::Ssl(stream) => stream.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Self::Plain(stream) => stream.flush(),
#[cfg(feature = "ssl")]
Self::Ssl(stream) => stream.flush(),
}
}
}

899
rups/src/cmd.rs Normal file
View file

@ -0,0 +1,899 @@
use core::fmt;
use std::convert::TryFrom;
use crate::{ClientError, NutError, Variable, VariableDefinition, VariableRange};
#[derive(Debug, Clone)]
pub enum Command<'a> {
Get(&'a [&'a str]),
/// Passes the login username.
SetUsername(&'a str),
/// Passes the login password.
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,
/// Queries the server version.
Version,
/// Gracefully shuts down the connection.
Logout,
}
impl<'a> Command<'a> {
/// The network identifier of the command.
pub fn name(&self) -> &'static str {
match self {
Self::Get(_) => "GET",
Self::SetUsername(_) => "USERNAME",
Self::SetPassword(_) => "PASSWORD",
Self::List(_) => "LIST",
Self::StartTLS => "STARTTLS",
Self::NetworkVersion => "NETVER",
Self::Version => "VER",
Self::Logout => "LOGOUT",
}
}
/// The arguments of the command to serialize.
pub fn args(&self) -> Vec<&str> {
match self {
Self::Get(cmd) => cmd.to_vec(),
Self::SetUsername(username) => vec![username],
Self::SetPassword(password) => vec![password],
Self::List(query) => query.to_vec(),
_ => Vec::new(),
}
}
}
impl<'a> fmt::Display for Command<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut args = self.args();
args.insert(0, self.name());
write!(f, "{}", shell_words::join(args))
}
}
#[derive(Debug, Clone)]
pub enum Response {
/// A successful response.
Ok,
/// Marks the beginning of a list response.
BeginList(String),
/// Marks the end of a list response.
EndList(String),
/// A variable (VAR) response.
///
/// Params: (var name, var value)
Var(String, String),
/// A UPS (UPS) response.
///
/// Params: (device name, device description)
Ups(String, String),
/// A client (CLIENT) response.
///
/// Params: (client IP)
Client(String),
/// A command (CMD) response.
///
/// Params: (command name)
Cmd(String),
/// A command description (CMDDESC) response.
///
/// Params: (command description)
CmdDesc(String),
/// A UPS description (UPSDESC) response.
///
/// Params: (UPS description)
UpsDesc(String),
/// A mutable variable (RW) response.
///
/// Params: (var name, var value)
Rw(String, String),
/// A variable description (DESC) response.
///
/// Params: (variable description)
Desc(String),
/// A NUMLOGINS response.
///
/// Params: (number of logins)
NumLogins(i32),
/// A variable type (TYPE) response.
///
/// Params: (variable name, variable types)
Type(String, Vec<String>),
/// A variable range (RANGE) response.
///
/// Params: (variable range)
Range(VariableRange),
/// A variable enum (ENUM) response.
///
/// Params: (enum value)
Enum(String),
}
impl Response {
pub fn from_args(mut args: Vec<String>) -> crate::Result<Response> {
if args.is_empty() {
return Err(
NutError::Generic("Parsing server response failed: empty line".into()).into(),
);
}
let cmd_name = args.remove(0);
match cmd_name.as_str() {
"OK" => Ok(Self::Ok),
"ERR" => {
if args.is_empty() {
Err(NutError::Generic("Unspecified server error".into()).into())
} else {
let err_type = args.remove(0);
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,
args.join(" ")
))
.into()),
}
}
}
"BEGIN" => {
if args.is_empty() {
Err(NutError::Generic("Unspecified BEGIN type".into()).into())
} else {
let begin_type = args.remove(0);
if &begin_type != "LIST" {
Err(
NutError::Generic(format!("Unexpected BEGIN type: {}", begin_type))
.into(),
)
} else {
let args = shell_words::join(args);
Ok(Response::BeginList(args))
}
}
}
"END" => {
if args.is_empty() {
Err(NutError::Generic("Unspecified END type".into()).into())
} else {
let begin_type = args.remove(0);
if &begin_type != "LIST" {
Err(
NutError::Generic(format!("Unexpected END type: {}", begin_type))
.into(),
)
} else {
let args = shell_words::join(args);
Ok(Response::EndList(args))
}
}
}
"VAR" => {
let _var_device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified VAR device name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified VAR name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_value = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified VAR value in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Var(var_name, var_value))
}
"RW" => {
let _var_device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RW device name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RW name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_value = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RW value in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Rw(var_name, var_value))
}
"UPS" => {
let name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified UPS name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let description = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified UPS description in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Ups(name, description))
}
"CLIENT" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CLIENT device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let ip_address = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CLIENT IP in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Client(ip_address))
}
"CMD" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMD device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMD name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Cmd(name))
}
"CMDDESC" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMDDESC device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let _name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMDDESC name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let desc = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMDDESC description in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::CmdDesc(desc))
}
"UPSDESC" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified UPSDESC device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let desc = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified UPSDESC description in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::UpsDesc(desc))
}
"DESC" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified DESC device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let _name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified DESC name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let desc = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified DESC description in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Desc(desc))
}
"NUMLOGINS" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified NUMLOGINS device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let num = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified NUMLOGINS number in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let num = num.parse::<i32>().map_err(|_| {
ClientError::from(NutError::Generic(
"Invalid NUMLOGINS number in response".into(),
))
})?;
Ok(Response::NumLogins(num))
}
"TYPE" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified TYPE device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified TYPE name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let types = args;
Ok(Response::Type(name, types))
}
"RANGE" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RANGE device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let _name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RANGE name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let min = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RANGE min in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let max = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RANGE max in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Range(VariableRange(min, max)))
}
"ENUM" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified ENUM device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let _name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified ENUM name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let val = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified ENUM value in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Enum(val))
}
_ => Err(NutError::UnknownResponseType(cmd_name).into()),
}
}
pub fn expect_ok(&self) -> crate::Result<&Response> {
match self {
Self::Ok => Ok(self),
_ => Err(NutError::UnexpectedResponse.into()),
}
}
pub fn expect_begin_list(self, expected_args: &[&str]) -> crate::Result<Response> {
let expected_args = shell_words::join(expected_args);
if let Self::BeginList(args) = &self {
if &expected_args == args {
Ok(self)
} else {
Err(NutError::UnexpectedResponse.into())
}
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_var(&self) -> crate::Result<Variable> {
if let Self::Var(name, value) = &self {
Ok(Variable::parse(name, value.to_owned()))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_rw(&self) -> crate::Result<Variable> {
if let Self::Rw(name, value) = &self {
Ok(Variable::parse(name, value.to_owned()))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_ups(&self) -> crate::Result<(String, String)> {
if let Self::Ups(name, description) = &self {
Ok((name.to_owned(), description.to_owned()))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_client(&self) -> crate::Result<String> {
if let Self::Client(client_ip) = &self {
Ok(client_ip.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_cmd(&self) -> crate::Result<String> {
if let Self::Cmd(name) = &self {
Ok(name.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_cmddesc(&self) -> crate::Result<String> {
if let Self::CmdDesc(description) = &self {
Ok(description.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_upsdesc(&self) -> crate::Result<String> {
if let Self::UpsDesc(description) = &self {
Ok(description.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_desc(&self) -> crate::Result<String> {
if let Self::Desc(description) = &self {
Ok(description.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_numlogins(&self) -> crate::Result<i32> {
if let Self::NumLogins(num) = &self {
Ok(*num)
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_type(&self) -> crate::Result<VariableDefinition> {
if let Self::Type(name, types) = &self {
VariableDefinition::try_from((
name.to_owned(),
types.iter().map(String::as_str).collect(),
))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_range(&self) -> crate::Result<VariableRange> {
if let Self::Range(range) = &self {
Ok(range.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_enum(&self) -> crate::Result<String> {
if let Self::Enum(value) = &self {
Ok(value.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
}
/// A macro for implementing `LIST` commands.
///
/// Each function should return a 2-tuple with
/// (1) the query to pass to `LIST`
/// (2) a closure for mapping each `Response` row to the return type
macro_rules! implement_list_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) -> $retty:ty {
(
$query:block,
$mapper:block,
)
}
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::List($query))?;
let list = conn.read_list($query)?;
list.into_iter().map($mapper).collect()
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::List($query)).await?;
let list = conn.read_list($query).await?;
list.into_iter().map($mapper).collect()
},
}
}
)*
}
};
}
/// A macro for implementing `GET` commands.
///
/// Each function should return a 2-tuple with
/// (1) the query to pass to `GET`
/// (2) a closure for mapping the `Response` row to the return type
macro_rules! implement_get_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) -> $retty:ty {
(
$query:block,
$mapper:block,
)
}
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::Get($query))?;
($mapper)(conn.read_response()?)
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::Get($query)).await?;
($mapper)(conn.read_response().await?)
},
}
}
)*
}
};
}
/// A macro for implementing simple/direct commands.
///
/// Each function should return a 2-tuple with
/// (1) the command to pass
/// (2) a closure for mapping the `String` row to the return type
macro_rules! implement_simple_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) -> $retty:ty {
(
$cmd:block,
$mapper:block,
)
}
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd)?;
($mapper)(conn.read_plain_response()?)
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd).await?;
($mapper)(conn.read_plain_response().await?)
},
}
}
)*
}
};
}
/// A macro for implementing action commands that return `OK`.
///
/// Each function should return the command to pass.
macro_rules! implement_action_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) $cmd:block
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<()> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd)?;
conn.read_response()?.expect_ok()?;
Ok(())
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<()> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd).await?;
conn.read_response().await?.expect_ok()?;
Ok(())
},
}
}
)*
}
};
}
implement_list_commands! {
/// Queries a list of UPS devices.
pub fn list_ups() -> Vec<(String, String)> {
(
{ &["UPS"] },
{ |row: Response| row.expect_ups() },
)
}
/// Queries the list of client IP addresses connected to the given device.
pub fn list_clients(ups_name: &str) -> Vec<String> {
(
{ &["CLIENT", ups_name] },
{ |row: Response| row.expect_client() },
)
}
/// Queries the list of variables for a UPS device.
pub fn list_vars(ups_name: &str) -> Vec<Variable> {
(
{ &["VAR", ups_name] },
{ |row: Response| row.expect_var() },
)
}
/// Queries the list of mutable variables for a UPS device.
pub fn list_mutable_vars(ups_name: &str) -> Vec<Variable> {
(
{ &["RW", ups_name] },
{ |row: Response| row.expect_rw() },
)
}
/// Queries the list of commands available for the given device.
pub fn list_commands(ups_name: &str) -> Vec<String> {
(
{ &["CMD", ups_name] },
{ |row: Response| row.expect_cmd() },
)
}
/// Queries the possible ranges of a UPS variable.
pub fn list_var_range(ups_name: &str, variable: &str) -> Vec<VariableRange> {
(
{ &["RANGE", ups_name, variable] },
{ |row: Response| row.expect_range() },
)
}
/// Queries the possible enum values of a UPS variable.
pub fn list_var_enum(ups_name: &str, variable: &str) -> Vec<String> {
(
{ &["ENUM", ups_name, variable] },
{ |row: Response| row.expect_enum() },
)
}
}
implement_get_commands! {
/// Queries one variable for a UPS device.
pub fn get_var(ups_name: &str, variable: &str) -> Variable {
(
{ &["VAR", ups_name, variable] },
{ |row: Response| row.expect_var() },
)
}
/// Queries the description of a UPS variable.
pub fn get_var_description(ups_name: &str, variable: &str) -> String {
(
{ &["DESC", ups_name, variable] },
{ |row: Response| row.expect_desc() },
)
}
/// Queries the type of a UPS variable.
pub fn get_var_type(ups_name: &str, variable: &str) -> VariableDefinition {
(
{ &["TYPE", ups_name, variable] },
{ |row: Response| row.expect_type() },
)
}
/// Queries the description of a UPS command.
pub fn get_command_description(ups_name: &str, variable: &str) -> String {
(
{ &["CMDDESC", ups_name, variable] },
{ |row: Response| row.expect_cmddesc() },
)
}
/// Queries the description of a UPS device.
pub fn get_ups_description(ups_name: &str) -> String {
(
{ &["UPSDESC", ups_name] },
{ |row: Response| row.expect_upsdesc() },
)
}
/// Queries the number of logins to the specified UPS.
pub fn get_num_logins(ups_name: &str) -> i32 {
(
{ &["NUMLOGINS", ups_name] },
{ |row: Response| row.expect_numlogins() },
)
}
}
implement_simple_commands! {
/// Queries the network protocol version.
pub fn get_network_version() -> String {
(
{ Command::NetworkVersion },
{ |row: String| Ok(row) },
)
}
/// Queries the server NUT version.
pub fn get_server_version() -> String {
(
{ Command::Version },
{ |row: String| Ok(row) },
)
}
}
implement_action_commands! {
/// Sends the login username.
pub(crate) fn set_username(username: &str) {
Command::SetUsername(username)
}
/// Sends the login password.
pub(crate) fn set_password(password: &str) {
Command::SetPassword(password)
}
/// Gracefully shuts down the connection.
pub(crate) fn logout() {
Command::Logout
}
}

196
rups/src/config.rs Normal file
View file

@ -0,0 +1,196 @@
use core::fmt;
use std::convert::{TryFrom, TryInto};
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use crate::ClientError;
/// A host specification.
#[derive(Clone, Debug)]
pub enum Host {
/// A TCP hostname, and address (IP + port).
Tcp(TcpHost),
// TODO: Support Unix socket streams.
}
impl Host {
/// Returns the hostname as given, if any.
pub fn hostname(&self) -> Option<String> {
match self {
Host::Tcp(host) => Some(host.hostname.to_owned()),
// _ => None,
}
}
}
impl Default for Host {
fn default() -> Self {
(String::from("localhost"), 3493)
.try_into()
.expect("Failed to parse local hostname; this is a bug.")
}
}
impl From<SocketAddr> for Host {
fn from(addr: SocketAddr) -> Self {
let hostname = addr.ip().to_string();
Self::Tcp(TcpHost { hostname, addr })
}
}
/// A TCP address, preserving the original DNS hostname if any.
#[derive(Clone, Debug)]
pub struct TcpHost {
pub(crate) hostname: String,
pub(crate) addr: SocketAddr,
}
impl TryFrom<(String, u16)> for Host {
type Error = ClientError;
fn try_from(hostname_port: (String, u16)) -> Result<Self, Self::Error> {
let (hostname, _) = hostname_port.clone();
let addr = hostname_port
.to_socket_addrs()
.map_err(ClientError::Io)?
.next()
.ok_or_else(|| {
ClientError::Io(std::io::Error::new(
std::io::ErrorKind::AddrNotAvailable,
"no address given",
))
})?;
Ok(Host::Tcp(TcpHost { hostname, addr }))
}
}
/// An authentication mechanism.
#[derive(Clone)]
pub struct Auth {
/// The username of the user to login as.
pub(crate) username: String,
/// Optional password assigned to the remote user.
pub(crate) password: Option<String>,
}
impl Auth {
/// Initializes authentication credentials with a username, and optionally a password.
pub fn new(username: String, password: Option<String>) -> Self {
Auth { username, password }
}
}
impl fmt::Debug for Auth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Auth")
.field("username", &self.username)
.field("password", &self.password.as_ref().map(|_| "(redacted)"))
.finish()
}
}
/// Configuration for connecting to a remote NUT server.
#[derive(Clone, Debug)]
pub struct Config {
pub(crate) host: Host,
pub(crate) auth: Option<Auth>,
pub(crate) timeout: Duration,
pub(crate) ssl: bool,
pub(crate) ssl_insecure: bool,
pub(crate) debug: bool,
}
impl Config {
/// Creates a connection configuration.
pub fn new(
host: Host,
auth: Option<Auth>,
timeout: Duration,
ssl: bool,
ssl_insecure: bool,
debug: bool,
) -> Self {
Config {
host,
auth,
timeout,
ssl,
ssl_insecure,
debug,
}
}
}
/// A builder for [`Config`].
#[derive(Clone, Debug, Default)]
pub struct ConfigBuilder {
host: Option<Host>,
auth: Option<Auth>,
timeout: Option<Duration>,
ssl: Option<bool>,
ssl_insecure: Option<bool>,
debug: Option<bool>,
}
impl ConfigBuilder {
/// Initializes an empty builder for [`Config`].
pub fn new() -> Self {
ConfigBuilder::default()
}
/// Sets the connection host, such as the TCP address and port.
pub fn with_host(mut self, host: Host) -> Self {
self.host = Some(host);
self
}
/// Sets the optional authentication parameters.
pub fn with_auth(mut self, auth: Option<Auth>) -> Self {
self.auth = auth;
self
}
/// Sets the network connection timeout. This may be ignored by non-network
/// connections, such as Unix domain sockets.
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
/// Enables SSL on the connection.
///
/// This will enable strict SSL verification (including hostname),
/// unless `.with_insecure_ssl` is also set to `true`.
#[cfg(feature = "ssl")]
pub fn with_ssl(mut self, ssl: bool) -> Self {
self.ssl = Some(ssl);
self
}
/// Turns off SSL verification.
///
/// Note: you must still use `.with_ssl(true)` to turn on SSL.
#[cfg(feature = "ssl")]
pub fn with_insecure_ssl(mut self, ssl_insecure: bool) -> Self {
self.ssl_insecure = Some(ssl_insecure);
self
}
/// Enables debugging network calls by printing to stderr.
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = Some(debug);
self
}
/// Builds the configuration with this builder.
pub fn build(self) -> Config {
Config::new(
self.host.unwrap_or_default(),
self.auth,
self.timeout.unwrap_or_else(|| Duration::from_secs(5)),
self.ssl.unwrap_or(false),
self.ssl_insecure.unwrap_or(false),
self.debug.unwrap_or(false),
)
}
}

79
rups/src/error.rs Normal file
View file

@ -0,0 +1,79 @@
use core::fmt;
use std::io;
/// A NUT-native error.
#[derive(Debug)]
pub enum NutError {
/// Occurs when the username/password combination is rejected.
AccessDenied,
/// Occurs when the specified UPS device does not exist.
UnknownUps,
/// Occurs when the response type or content wasn't expected at the current stage.
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 trying to initialize a strict SSL connection with an invalid hostname.
SslInvalidHostname,
/// Occurs when the client used a feature that is disabled by the server.
FeatureNotConfigured,
/// Generic (usually internal) client error.
Generic(String),
}
impl fmt::Display for NutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AccessDenied => write!(f, "Authentication failed"),
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::SslInvalidHostname => write!(
f,
"Given hostname cannot be used for a strict SSL connection"
),
Self::FeatureNotConfigured => write!(f, "Feature not configured by server"),
Self::Generic(msg) => write!(f, "Internal client error: {}", msg),
}
}
}
impl std::error::Error for NutError {}
/// Encapsulation for errors emitted by the client library.
#[derive(Debug)]
pub enum ClientError {
/// Encapsulates IO errors.
Io(io::Error),
/// Encapsulates NUT and client-specific errors.
Nut(NutError),
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => err.fmt(f),
Self::Nut(err) => err.fmt(f),
}
}
}
impl std::error::Error for ClientError {}
impl From<io::Error> for ClientError {
fn from(err: io::Error) -> Self {
ClientError::Io(err)
}
}
impl From<NutError> for ClientError {
fn from(err: NutError) -> Self {
ClientError::Nut(err)
}
}
/// Result type for [`ClientError`]
pub type Result<T> = std::result::Result<T, ClientError>;

23
rups/src/lib.rs Normal file
View file

@ -0,0 +1,23 @@
#![deny(missing_docs)]
//! # rups
//!
//! The `rups` crate provides a network client implementation
//! for Network UPS Tools (NUT) servers.
pub use config::*;
pub use error::*;
pub use var::*;
/// Blocking client implementation for NUT.
pub mod blocking;
/// Async client implementation for NUT, using Tokio.
#[cfg(feature = "async")]
pub mod tokio;
mod cmd;
mod config;
mod error;
#[cfg(feature = "ssl")]
mod ssl;
mod var;

30
rups/src/ssl/mod.rs Normal file
View file

@ -0,0 +1,30 @@
use crate::Config;
/// The certificate validation mechanism that allows any certificate.
pub struct InsecureCertificateValidator {
debug: bool,
}
impl InsecureCertificateValidator {
/// Initialize a new instance.
pub fn new(config: &Config) -> Self {
InsecureCertificateValidator {
debug: config.debug,
}
}
}
impl rustls::ServerCertVerifier for InsecureCertificateValidator {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
if self.debug {
eprintln!("DEBUG <- (!) Certificate received, but not verified");
}
Ok(rustls::ServerCertVerified::assertion())
}
}

189
rups/src/tokio/mod.rs Normal file
View file

@ -0,0 +1,189 @@
use std::net::SocketAddr;
use crate::cmd::{Command, Response};
use crate::tokio::stream::ConnectionStream;
use crate::{Config, Host, NutError};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
mod stream;
/// An async NUT client connection.
pub enum Connection {
/// A TCP connection.
Tcp(TcpConnection),
}
impl Connection {
/// Initializes a connection to a NUT server (upsd).
pub async fn new(config: &Config) -> crate::Result<Self> {
let mut conn = match &config.host {
Host::Tcp(host) => Self::Tcp(TcpConnection::new(config.clone(), &host.addr).await?),
};
conn.get_network_version().await?;
conn.login(config).await?;
Ok(conn)
}
/// Gracefully closes the connection.
pub async fn close(mut self) -> crate::Result<()> {
self.logout().await?;
Ok(())
}
/// Sends username and password, as applicable.
async fn login(&mut self, config: &Config) -> crate::Result<()> {
if let Some(auth) = config.auth.clone() {
// Pass username and check for 'OK'
self.set_username(&auth.username).await?;
// Pass password and check for 'OK'
if let Some(password) = &auth.password {
self.set_password(password).await?;
}
}
Ok(())
}
}
/// A blocking TCP NUT client connection.
pub struct TcpConnection {
config: Config,
stream: ConnectionStream,
}
impl TcpConnection {
async fn new(config: Config, socket_addr: &SocketAddr) -> crate::Result<Self> {
// Create the TCP connection
let tcp_stream = TcpStream::connect(socket_addr).await?;
let mut connection = Self {
config,
stream: ConnectionStream::Plain(tcp_stream),
};
connection = connection.enable_ssl().await?;
Ok(connection)
}
#[cfg(feature = "async-ssl")]
async fn enable_ssl(mut self) -> crate::Result<Self> {
if self.config.ssl {
// Send TLS request and check for 'OK'
self.write_cmd(Command::StartTLS).await?;
self.read_response()
.await
.map_err(|e| {
if let crate::ClientError::Nut(NutError::FeatureNotConfigured) = e {
crate::ClientError::Nut(NutError::SslNotSupported)
} else {
e
}
})?
.expect_ok()?;
let mut ssl_config = rustls::ClientConfig::new();
let dns_name: webpki::DNSName;
if self.config.ssl_insecure {
ssl_config
.dangerous()
.set_certificate_verifier(std::sync::Arc::new(
crate::ssl::InsecureCertificateValidator::new(&self.config),
));
dns_name = webpki::DNSNameRef::try_from_ascii_str("www.google.com")
.unwrap()
.to_owned();
} else {
// Try to get hostname as given (e.g. localhost can be used for strict SSL, but not 127.0.0.1)
let hostname = self
.config
.host
.hostname()
.ok_or(crate::ClientError::Nut(NutError::SslInvalidHostname))?;
dns_name = webpki::DNSNameRef::try_from_ascii_str(&hostname)
.map_err(|_| crate::ClientError::Nut(NutError::SslInvalidHostname))?
.to_owned();
ssl_config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
};
let config = tokio_rustls::TlsConnector::from(std::sync::Arc::new(ssl_config));
// Wrap and override the TCP stream
self.stream = self.stream.upgrade_ssl(config, dns_name.as_ref()).await?;
}
Ok(self)
}
#[cfg(not(feature = "async-ssl"))]
async fn enable_ssl(self) -> crate::Result<Self> {
Ok(self)
}
pub(crate) async fn write_cmd(&mut self, line: Command<'_>) -> crate::Result<()> {
let line = format!("{}\n", line);
if self.config.debug {
eprint!("DEBUG -> {}", line);
}
self.stream.write_all(line.as_bytes()).await?;
self.stream.flush().await?;
Ok(())
}
async fn parse_line(
reader: &mut BufReader<&mut ConnectionStream>,
debug: bool,
) -> crate::Result<Vec<String>> {
let mut raw = String::new();
reader.read_line(&mut raw).await?;
if debug {
eprint!("DEBUG <- {}", raw);
}
raw = raw[..raw.len() - 1].to_string(); // Strip off \n
// Parse args by splitting whitespace, minding quotes for args with multiple words
let args = shell_words::split(&raw)
.map_err(|e| NutError::Generic(format!("Parsing server response failed: {}", e)))?;
Ok(args)
}
pub(crate) async fn read_response(&mut self) -> crate::Result<Response> {
let mut reader = BufReader::new(&mut self.stream);
let args = Self::parse_line(&mut reader, self.config.debug).await?;
Response::from_args(args)
}
pub(crate) async fn read_plain_response(&mut self) -> crate::Result<String> {
let mut reader = BufReader::new(&mut self.stream);
let args = Self::parse_line(&mut reader, self.config.debug).await?;
Ok(args.join(" "))
}
pub(crate) async fn read_list(&mut self, query: &[&str]) -> crate::Result<Vec<Response>> {
let mut reader = BufReader::new(&mut self.stream);
let args = Self::parse_line(&mut reader, self.config.debug).await?;
Response::from_args(args)?.expect_begin_list(query)?;
let mut lines: Vec<Response> = Vec::new();
loop {
let args = Self::parse_line(&mut reader, self.config.debug).await?;
let resp = Response::from_args(args)?;
match resp {
Response::EndList(_) => {
break;
}
_ => lines.push(resp),
}
}
Ok(lines)
}
}

100
rups/src/tokio/stream.rs Normal file
View file

@ -0,0 +1,100 @@
use std::io::Error;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
/// A wrapper for various Tokio stream types.
pub enum ConnectionStream {
/// A plain TCP stream.
Plain(TcpStream),
/// A stream wrapped with SSL using `rustls`.
#[cfg(feature = "async-ssl")]
Ssl(Box<tokio_rustls::client::TlsStream<ConnectionStream>>),
}
impl ConnectionStream {
/// Wraps the current stream with SSL using `rustls`.
#[cfg(feature = "async-ssl")]
pub async fn upgrade_ssl(
self,
config: tokio_rustls::TlsConnector,
dns_name: webpki::DNSNameRef<'_>,
) -> crate::Result<ConnectionStream> {
Ok(ConnectionStream::Ssl(Box::new(
config
.connect(dns_name, self)
.await
.map_err(crate::ClientError::Io)?,
)))
}
}
impl AsyncRead for ConnectionStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
match self.get_mut() {
Self::Plain(stream) => {
let pinned = Pin::new(stream);
pinned.poll_read(cx, buf)
}
#[cfg(feature = "async-ssl")]
Self::Ssl(stream) => {
let pinned = Pin::new(stream);
pinned.poll_read(cx, buf)
}
}
}
}
impl AsyncWrite for ConnectionStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
match self.get_mut() {
Self::Plain(stream) => {
let pinned = Pin::new(stream);
pinned.poll_write(cx, buf)
}
#[cfg(feature = "async-ssl")]
Self::Ssl(stream) => {
let pinned = Pin::new(stream);
pinned.poll_write(cx, buf)
}
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match self.get_mut() {
Self::Plain(stream) => {
let pinned = Pin::new(stream);
pinned.poll_flush(cx)
}
#[cfg(feature = "async-ssl")]
Self::Ssl(stream) => {
let pinned = Pin::new(stream);
pinned.poll_flush(cx)
}
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match self.get_mut() {
Self::Plain(stream) => {
let pinned = Pin::new(stream);
pinned.poll_shutdown(cx)
}
#[cfg(feature = "async-ssl")]
Self::Ssl(stream) => {
let pinned = Pin::new(stream);
pinned.poll_shutdown(cx)
}
}
}
}

345
rups/src/var.rs Normal file
View file

@ -0,0 +1,345 @@
use core::fmt;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::time::Duration;
/// Well-known variable keys for NUT UPS devices.
///
/// List retrieved from: https://networkupstools.org/docs/user-manual.chunked/apcs01.html
pub mod key {
/// Device model.
pub const DEVICE_MODEL: &str = "device.model";
/// Device manufacturer.
pub const DEVICE_MANUFACTURER: &str = "device.mfr";
/// Device serial number.
pub const DEVICE_SERIAL: &str = "device.serial";
/// Device type.
pub const DEVICE_TYPE: &str = "device.type";
/// Device description.
pub const DEVICE_DESCRIPTION: &str = "device.description";
/// Device administrator name.
pub const DEVICE_CONTACT: &str = "device.contact";
/// Device physical location.
pub const DEVICE_LOCATION: &str = "device.location";
/// Device part number.
pub const DEVICE_PART: &str = "device.part";
/// Device MAC address.
pub const DEVICE_MAC_ADDRESS: &str = "device.macaddr";
/// Device uptime.
pub const DEVICE_UPTIME: &str = "device.uptime";
}
/// Well-known variables for NUT UPS devices.
///
/// List retrieved from: https://networkupstools.org/docs/user-manual.chunked/apcs01.html
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Variable {
/// Device model.
DeviceModel(String),
/// Device manufacturer.
DeviceManufacturer(String),
/// Device serial number.
DeviceSerial(String),
/// Device type.
DeviceType(DeviceType),
/// Device description.
DeviceDescription(String),
/// Device administrator name.
DeviceContact(String),
/// Device physical location.
DeviceLocation(String),
/// Device part number.
DevicePart(String),
/// Device MAC address.
DeviceMacAddress(String),
/// Device uptime.
DeviceUptime(Duration),
/// Any other variable. Value is a tuple of (key, value).
Other((String, String)),
}
impl Variable {
/// Parses a variable from its key and value.
pub fn parse(name: &str, value: String) -> Variable {
use self::key::*;
match name {
DEVICE_MODEL => Self::DeviceModel(value),
DEVICE_MANUFACTURER => Self::DeviceManufacturer(value),
DEVICE_SERIAL => Self::DeviceSerial(value),
DEVICE_TYPE => Self::DeviceType(DeviceType::from(value)),
DEVICE_DESCRIPTION => Self::DeviceDescription(value),
DEVICE_CONTACT => Self::DeviceContact(value),
DEVICE_LOCATION => Self::DeviceLocation(value),
DEVICE_PART => Self::DevicePart(value),
DEVICE_MAC_ADDRESS => Self::DeviceMacAddress(value),
DEVICE_UPTIME => Self::DeviceUptime(Duration::from_secs(
value.parse().expect("invalid uptime value"),
)),
_ => Self::Other((name.into(), value)),
}
}
/// Returns the NUT name of the variable.
pub fn name(&self) -> &str {
use self::key::*;
match self {
Self::DeviceModel(_) => DEVICE_MODEL,
Self::DeviceManufacturer(_) => DEVICE_MANUFACTURER,
Self::DeviceSerial(_) => DEVICE_SERIAL,
Self::DeviceType(_) => DEVICE_TYPE,
Self::DeviceDescription(_) => DEVICE_DESCRIPTION,
Self::DeviceContact(_) => DEVICE_CONTACT,
Self::DeviceLocation(_) => DEVICE_LOCATION,
Self::DevicePart(_) => DEVICE_PART,
Self::DeviceMacAddress(_) => DEVICE_MAC_ADDRESS,
Self::DeviceUptime(_) => DEVICE_UPTIME,
Self::Other((name, _)) => name.as_str(),
}
}
/// Returns the value of the NUT variable.
pub fn value(&self) -> String {
match self {
Self::DeviceModel(value) => value.clone(),
Self::DeviceManufacturer(value) => value.clone(),
Self::DeviceSerial(value) => value.clone(),
Self::DeviceType(value) => value.to_string(),
Self::DeviceDescription(value) => value.clone(),
Self::DeviceContact(value) => value.clone(),
Self::DeviceLocation(value) => value.clone(),
Self::DevicePart(value) => value.clone(),
Self::DeviceMacAddress(value) => value.clone(),
Self::DeviceUptime(value) => value.as_secs().to_string(),
Self::Other((_, value)) => value.clone(),
}
}
}
impl fmt::Display for Variable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.name(), self.value())
}
}
/// NUT device type.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum DeviceType {
/// UPS (Uninterruptible Power Supply)
Ups,
/// PDU (Power Distribution Unit)
Pdu,
/// SCD (Solar Controller Device)
Scd,
/// PSU (Power Supply Unit)
Psu,
/// ATS (Automatic Transfer Switch)
Ats,
/// Other device type.
Other(String),
}
impl DeviceType {
/// Convert from string.
pub fn from(v: String) -> DeviceType {
match v.as_str() {
"ups" => Self::Ups,
"pdu" => Self::Pdu,
"scd" => Self::Scd,
"psu" => Self::Psu,
"ats" => Self::Ats,
_ => Self::Other(v),
}
}
}
impl fmt::Display for DeviceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ups => write!(f, "ups"),
Self::Pdu => write!(f, "pdu"),
Self::Scd => write!(f, "scd"),
Self::Psu => write!(f, "psu"),
Self::Ats => write!(f, "ats"),
Self::Other(val) => write!(f, "other({})", val),
}
}
}
/// NUT Variable type
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[allow(dead_code)]
pub(crate) enum VariableType {
/// A mutable variable (`RW`).
Rw,
/// An enumerated type, which supports a few specific values (`ENUM`).
Enum,
/// A string with a maximum size (`STRING:n`).
String(usize),
/// A numeric type, either integer or float, comprised in the range defined by `LIST RANGE`.
Range,
/// A simple numeric value, either integer or float.
Number,
}
impl TryFrom<&str> for VariableType {
type Error = crate::ClientError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"RW" => Ok(Self::Rw),
"ENUM" => Ok(Self::Enum),
"RANGE" => Ok(Self::Range),
"NUMBER" => Ok(Self::Number),
other => {
if other.starts_with("STRING:") {
let size = other
.splitn(2, ':')
.nth(1)
.map(|s| s.parse().ok())
.flatten()
.ok_or_else(|| {
crate::ClientError::Nut(crate::NutError::Generic(
"Invalid STRING definition".into(),
))
})?;
Ok(Self::String(size))
} else {
Err(crate::ClientError::Nut(crate::NutError::Generic(format!(
"Unrecognized variable type: {}",
value
))))
}
}
}
}
}
/// NUT Variable definition.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct VariableDefinition(String, HashSet<VariableType>);
impl VariableDefinition {
/// The name of this variable.
pub fn name(&self) -> &str {
self.0.as_str()
}
/// Whether this variable is mutable.
pub fn is_mutable(&self) -> bool {
self.1.contains(&VariableType::Rw)
}
/// Whether this variable is an enumerated type.
pub fn is_enum(&self) -> bool {
self.1.contains(&VariableType::Enum)
}
/// Whether this variable is a String type
pub fn is_string(&self) -> bool {
self.1.iter().any(|t| matches!(t, VariableType::String(_)))
}
/// Whether this variable is a numeric type,
/// either integer or float, comprised in a range
pub fn is_range(&self) -> bool {
self.1.contains(&VariableType::Range)
}
/// Whether this variable is a numeric type, either integer or float.
pub fn is_number(&self) -> bool {
self.1.contains(&VariableType::Number)
}
/// Returns the max string length, if applicable.
pub fn get_string_length(&self) -> Option<usize> {
self.1.iter().find_map(|t| match t {
VariableType::String(n) => Some(*n),
_ => None,
})
}
}
impl<A: ToString> TryFrom<(A, Vec<&str>)> for VariableDefinition {
type Error = crate::ClientError;
fn try_from(value: (A, Vec<&str>)) -> Result<Self, Self::Error> {
Ok(VariableDefinition(
value.0.to_string(),
value
.1
.iter()
.map(|s| VariableType::try_from(*s))
.collect::<crate::Result<HashSet<VariableType>>>()?,
))
}
}
/// A range of values for a variable.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct VariableRange(pub String, pub String);
#[cfg(test)]
mod tests {
use std::iter::FromIterator;
use super::*;
#[test]
fn test_parse_variable_definition() {
assert_eq!(
VariableDefinition::try_from(("var0", vec![])).unwrap(),
VariableDefinition("var0".into(), HashSet::new())
);
assert_eq!(
VariableDefinition::try_from(("var1", vec!["RW"])).unwrap(),
VariableDefinition(
"var1".into(),
HashSet::from_iter(vec![VariableType::Rw].into_iter())
)
);
assert_eq!(
VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"])).unwrap(),
VariableDefinition(
"var1".into(),
HashSet::from_iter(vec![VariableType::Rw, VariableType::String(123)].into_iter())
)
);
assert!(
VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"]))
.unwrap()
.is_mutable()
);
assert!(
VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"]))
.unwrap()
.is_string()
);
assert!(
!VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"]))
.unwrap()
.is_enum()
);
assert!(
!VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"]))
.unwrap()
.is_number()
);
assert!(
!VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"]))
.unwrap()
.is_range()
);
assert_eq!(
VariableDefinition::try_from(("var1", vec!["RW", "STRING:123"]))
.unwrap()
.get_string_length(),
Some(123)
);
}
}