Skip to content

Commit

Permalink
Add password_command for sasl and nick_password`
Browse files Browse the repository at this point in the history
This commit builds off of squidowl#552 and allows the user to supply a command
instead of a password file anywhere that a password file is used. If the
password command ends unsuccessfully, then an error is displayed.

Trailing whitespace is now trimmed from password commands, as it is
commonplace for commands to leave a trailing newline after the password
they output.

Resolves squidowl#580
  • Loading branch information
4e554c4c committed Sep 20, 2024
1 parent 34f067c commit 89beac9
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 54 deletions.
8 changes: 8 additions & 0 deletions book/src/configuration/servers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ Read nick_password from the file at the given path.[^1]
- **values**: any string
- **default**: not set

## `nick_password_command`

Executes the command with `sh` (or equivalent) and reads `nickname_password` as the output.

- **type**: string
- **values**: any string
- **default**: not set

## `nick_identify_syntax`

The server's NICKSERV IDENTIFY syntax.
Expand Down
18 changes: 16 additions & 2 deletions data/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::path::PathBuf;
use std::string;
use std::{string, str};

use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt;
Expand Down Expand Up @@ -306,16 +306,30 @@ fn default_tooltip() -> bool {
pub enum Error {
#[error("config could not be read: {0}")]
Read(String),
#[error("command could not be run: {0}")]
Execute(String),
#[error("{0}")]
Io(String),
#[error("{0}")]
Parse(String),
#[error("UTF8 parsing error: {0}")]
UI(#[from] string::FromUtf8Error),
StrUtf8Error(#[from] str::Utf8Error),
#[error("UTF8 parsing error: {0}")]
StringUtf8Error(#[from] string::FromUtf8Error),
#[error(transparent)]
LoadSounds(#[from] audio::LoadError),
}

impl Error {
pub fn is_expected(&self) -> bool {
match self {
// If a user doesn't have a config when we start up, then we end up with a read error
Error::Read(_) => true,
_ => false,
}
}
}

impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error.to_string())
Expand Down
5 changes: 5 additions & 0 deletions data/src/config/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct Server {
pub nick_password: Option<String>,
/// The client's NICKSERV password file.
pub nick_password_file: Option<String>,
/// The client's NICKSERV password command.
pub nick_password_command: Option<String>,
/// The server's NICKSERV IDENTIFY syntax.
pub nick_identify_syntax: Option<IdentifySyntax>,
/// Alternative nicknames for the client, if the default is taken.
Expand Down Expand Up @@ -143,6 +145,7 @@ impl Default for Server {
nickname: Default::default(),
nick_password: Default::default(),
nick_password_file: Default::default(),
nick_password_command: Default::default(),
nick_identify_syntax: Default::default(),
alt_nicks: Default::default(),
username: Default::default(),
Expand Down Expand Up @@ -189,6 +192,8 @@ pub enum Sasl {
password: Option<String>,
/// Account password file
password_file: Option<String>,
/// Account password command
password_command: Option<String>,
},
External {
/// The path to PEM encoded X509 user certificate for external auth
Expand Down
88 changes: 57 additions & 31 deletions data/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::BTreeMap;
use std::fmt;
use std::{fmt, str};
use tokio::fs;
use tokio::process::Command;

Expand All @@ -12,6 +12,9 @@ use crate::config::server::Sasl;
use crate::config::Error;

pub type Handle = Sender<proto::Message>;
const DUP_PASS_MSG: &str = "Only one of password, password_file and password_command can be set.";
const DUP_NICK_PASS_MSG: &str = "Only one of nick_password, nick_password_file and nick_password_command can be set.";
const DUP_SASL_PASS_MSG: &str = "Exactly one of sasl.plain.password, sasl.plain.password_file or sasl.plain.password_command must be set.";

#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Server(String);
Expand Down Expand Up @@ -52,6 +55,29 @@ impl<'a> From<(&'a Server, &'a config::Server)> for Entry {
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Map(BTreeMap<Server, config::Server>);

async fn read_from_command(pass_command: &str) -> Result<String, Error> {
let output = if cfg!(target_os = "windows") {
Command::new("cmd")
.arg("/C")
.arg(pass_command)
.output()
.await?
} else {
Command::new("sh")
.arg("-c")
.arg(pass_command)
.output()
.await?
};
if output.status.success() {
// we remove trailing whitespace, which might be present from unix pipelines with a
// trailing newline
Ok(str::from_utf8(&output.stdout)?.trim_end().to_string())
} else {
Err(Error::Execute(String::from_utf8(output.stderr)?))
}
}

impl Map {
pub fn insert(&mut self, name: Server, server: config::Server) {
self.0.insert(name, server);
Expand All @@ -77,62 +103,62 @@ impl Map {
for (_, config) in self.0.iter_mut() {
if let Some(pass_file) = &config.password_file {
if config.password.is_some() || config.password_command.is_some() {
return Err(Error::Parse(
"Only one of password, password_file and password_command can be set."
.to_string(),
));
return Err(Error::Parse(DUP_PASS_MSG.to_string()));
}
let pass = fs::read_to_string(pass_file).await?;
config.password = Some(pass);
}
if let Some(pass_command) = &config.password_command {
if config.password.is_some() {
return Err(Error::Parse(
"Only one of password, password_file and password_command can be set."
.to_string(),
));
return Err(Error::Parse(DUP_PASS_MSG.to_string()));
}
let output = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(["/C", pass_command])
.output()
.await?
} else {
Command::new("sh")
.arg("-c")
.arg(pass_command)
.output()
.await?
};
config.password = Some(String::from_utf8(output.stdout)?);
config.password = Some(read_from_command(pass_command).await?);
}
if let Some(nick_pass_file) = &config.nick_password_file {
if config.nick_password.is_some() {
return Err(Error::Parse(
"Only one of nick_password and nick_password_file can be set.".to_string(),
));
if config.nick_password.is_some() || config.nick_password_command.is_some() {
return Err(Error::Parse(DUP_NICK_PASS_MSG.to_string()));
}
let nick_pass = fs::read_to_string(nick_pass_file).await?;
config.nick_password = Some(nick_pass);
}
if let Some(nick_pass_command) = &config.nick_password_command {
if config.password.is_some() {
return Err(Error::Parse(DUP_NICK_PASS_MSG.to_string()));
}
config.password = Some(read_from_command(nick_pass_command).await?);
}
if let Some(sasl) = &mut config.sasl {
match sasl {
Sasl::Plain {
password: Some(_),
password_file: Some(_),
password_file: None,
password_command: None,
..
} => {
return Err(Error::Parse("Exactly one of sasl.plain.password or sasl.plain.password_file must be set.".to_string()));
}
} => {},
Sasl::Plain {
password: password @ None,
password_file: Some(pass_file),
password_command: None,
..
} => {
let pass = fs::read_to_string(pass_file).await?;
*password = Some(pass);
}
_ => {}
Sasl::Plain {
password: password @ None,
password_file: None,
password_command: Some(pass_command),
..
} => {
let pass = read_from_command(pass_command).await?;
*password = Some(pass);
}
Sasl::Plain { .. } => {
return Err(Error::Parse(DUP_SASL_PASS_MSG.to_string()));
}
Sasl::External { .. } => {
// no passwords to read
}
}
}
}
Expand Down
41 changes: 20 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,29 +151,28 @@ impl Halloy {
command.map(Message::Dashboard),
)
}
Err(error) => match &error {
config::Error::Parse(_) | config::Error::LoadSounds(_) => (
Screen::Help(screen::Help::new(error)),
Config::default(),
Task::none(),
),
_ => {
Err(error) => {
if config::has_yaml_config() {
// If we have a YAML file, but end up in this arm
// it means the user tried to load Halloy with a YAML configuration, but it expected TOML.
if config::has_yaml_config() {
(
Screen::Migration(screen::Migration::new()),
Config::default(),
Task::none(),
)
} else {
// Otherwise, show regular welcome screen for new users.
(
Screen::Welcome(screen::Welcome::new()),
Config::default(),
Task::none(),
)
}
(
Screen::Migration(screen::Migration::new()),
Config::default(),
Task::none(),
)
} else if error.is_expected() {
// Show regular welcome screen for new users.
(
Screen::Welcome(screen::Welcome::new()),
Config::default(),
Task::none(),
)
} else {
(
Screen::Help(screen::Help::new(error)),
Config::default(),
Task::none(),
)
}
},
};
Expand Down

0 comments on commit 89beac9

Please sign in to comment.