Skip to content

Commit

Permalink
Merge pull request #583 from 4e554c4c/password_command_sasl
Browse files Browse the repository at this point in the history
Add password_command for sasl and `nick_password`
  • Loading branch information
casperstorm committed Sep 27, 2024
2 parents 638c412 + 2e8a375 commit 1b334aa
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 62 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Unreleased
- New configuration options
- Ability to define a shell command for loading a NICKSERV password. See [configuration](https://halloy.squidowl.org/configuration/servers/index.html#nick_password_command)
- Ability to define a shell command for loading a SASL password. See [configuration](https://halloy.squidowl.org/configuration/servers/sasl/plain.html)

Fixed:

- Errors from password commands are now caught and displayed to the user.

Added:

Expand Down
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
27 changes: 21 additions & 6 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 @@ -168,9 +168,12 @@ impl Config {
}

let path = Self::path();
if !path.try_exists()? {
return Err(Error::ConfigMissing { has_yaml_config: has_yaml_config()? });
}
let content = fs::read_to_string(path)
.await
.map_err(|e| Error::Read(e.to_string()))?;
.map_err(|e| Error::LoadConfigFile(e.to_string()))?;

let Configuration {
theme,
Expand Down Expand Up @@ -315,8 +318,8 @@ pub fn random_nickname_with_seed<R: Rng>(rng: &mut R) -> String {
}

/// Has YAML configuration file.
pub fn has_yaml_config() -> bool {
config_dir().join("config.yaml").exists()
fn has_yaml_config() -> Result<bool, Error> {
Ok(config_dir().join("config.yaml").try_exists()?)
}

fn default_tooltip() -> bool {
Expand All @@ -326,15 +329,27 @@ fn default_tooltip() -> bool {
#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("config could not be read: {0}")]
Read(String),
LoadConfigFile(String),
#[error("command could not be run: {0}")]
ExecutePasswordCommand(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),
#[error("Only one of password, password_file and password_command can be set.")]
DuplicatePassword,
#[error("Only one of nick_password, nick_password_file and nick_password_command can be set.")]
DuplicateNickPassword,
#[error("Exactly one of sasl.plain.password, sasl.plain.password_file or sasl.plain.password_command must be set.")]
DuplicateSaslPassword,
#[error("Config does not exist")]
ConfigMissing { has_yaml_config: bool },
}

impl From<std::io::Error> for Error {
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
85 changes: 54 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 Down Expand Up @@ -52,6 +52,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::ExecutePasswordCommand(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 +100,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::DuplicatePassword);
}
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::DuplicatePassword);
}
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::DuplicateNickPassword);
}
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::DuplicateNickPassword);
}
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::DuplicateSaslPassword);
}
Sasl::External { .. } => {
// no passwords to read
}
}
}
}
Expand Down
47 changes: 22 additions & 25 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,31 +154,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(),
),
_ => {
// 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(),
)
}
}
},
// 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.
Err(config::Error::ConfigMissing {
has_yaml_config: true,
}) => (
Screen::Migration(screen::Migration::new()),
Config::default(),
Task::none(),
),
// Show regular welcome screen for new users.
Err(config::Error::ConfigMissing {
has_yaml_config: false,
}) => (
Screen::Welcome(screen::Welcome::new()),
Config::default(),
Task::none(),
),
Err(error) => (
Screen::Help(screen::Help::new(error)),
Config::default(),
Task::none(),
),
};

(
Expand Down

0 comments on commit 1b334aa

Please sign in to comment.