Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add password_command for sasl and nick_password #583

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

# 2024.12 (2024-09-17)

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 @@ -159,9 +159,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 @@ -294,8 +297,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 @@ -305,15 +308,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 {
Copy link
Contributor Author

@4e554c4c 4e554c4c Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to refactor this bit out (the custom From impl) for this PR, but it turns out that every single Error in Halloy has to be Clone

im not a huge fan but also fixing that is out of scope for this PR

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 @@ -151,31 +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(),
),
_ => {
// 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
Loading