pub mod localhost;
pub mod ssh2;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::error::RegentError;
use crate::hosts::command::CommandResult;
use crate::hosts::handlers::localhost::WhichUser;
use crate::hosts::handlers::ssh2::Ssh2Auth;
use crate::hosts::privilege::Credentials;
use crate::hosts::privilege::Privilege;
use crate::secrets::SecretProvider;
use crate::secrets::SecretReference;
use crate::{LocalHostHandler, Ssh2HostHandler};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TargetUserKind {
CurrentUser,
RawUser(Credentials),
User(SecretReference),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "PascalCase")]
pub struct TargetUser {
pub user_kind: TargetUserKind,
}
impl TargetUser {
pub fn current_user() -> Self {
Self {
user_kind: TargetUserKind::CurrentUser,
}
}
pub fn user(sec_ref: &str, provider: Option<String>) -> Self {
Self {
user_kind: TargetUserKind::User(SecretReference::from(sec_ref, provider)),
}
}
pub fn user_raw(login: &str, password: &str) -> Self {
Self {
user_kind: TargetUserKind::RawUser(Credentials::from(login, password)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ConnectionMethod {
Localhost(TargetUser),
Ssh2(Ssh2Auth),
}
pub trait HostHandler: Sized {
async fn connect(
&mut self,
endpoint: &str,
) -> Result<(), RegentError>;
async fn is_connected(&mut self) -> bool;
async fn disconnect(&mut self) -> Result<(), RegentError>;
async fn is_this_command_available(
&mut self,
command: &str,
privilege: &Privilege,
) -> Result<bool, RegentError>;
async fn run_command(
&mut self,
command: &str,
privilege: &Privilege,
) -> Result<CommandResult, RegentError>;
#[cfg(feature = "windows")]
async fn run_windows_command(&mut self, command: &str) -> Result<CommandResult, RegentError>;
async fn get_file(&mut self, path: PathBuf) -> Result<Vec<u8>, RegentError>;
}
pub enum Handler {
LocalHost(LocalHostHandler),
Ssh2(Ssh2HostHandler),
}
impl Clone for Handler {
fn clone(&self) -> Self {
match self {
Handler::LocalHost(h) => Handler::LocalHost(h.clone()),
Handler::Ssh2(h) => Handler::Ssh2(h.clone()),
}
}
}
impl Handler {
pub fn localhost(localhost_handler: LocalHostHandler) -> Self {
Handler::LocalHost(localhost_handler)
}
pub fn ss2(ss2_handler: Ssh2HostHandler) -> Self {
Handler::Ssh2(ss2_handler)
}
}
impl HostHandler for Handler {
async fn connect(
&mut self,
endpoint: &str,
) -> Result<(), RegentError> {
match self {
Handler::LocalHost(handler) => handler.connect(endpoint).await,
Handler::Ssh2(handler) => handler.connect(endpoint).await,
}
}
async fn is_connected(&mut self) -> bool {
match self {
Handler::LocalHost(handler) => handler.is_connected().await,
Handler::Ssh2(handler) => handler.is_connected().await,
}
}
async fn disconnect(&mut self) -> Result<(), RegentError> {
match self {
Handler::LocalHost(handler) => handler.disconnect().await,
Handler::Ssh2(handler) => handler.disconnect().await,
}
}
async fn is_this_command_available(
&mut self,
command: &str,
privilege: &Privilege,
) -> Result<bool, RegentError> {
match self {
Handler::LocalHost(handler) => {
handler.is_this_command_available(command, privilege).await
}
Handler::Ssh2(handler) => handler.is_this_command_available(command, privilege).await,
}
}
async fn run_command(
&mut self,
command: &str,
privilege: &Privilege,
) -> Result<CommandResult, RegentError> {
match self {
Handler::LocalHost(handler) => handler.run_command(command, privilege).await,
Handler::Ssh2(handler) => handler.run_command(command, privilege).await,
}
}
#[cfg(feature = "windows")]
async fn run_windows_command(&mut self, command: &str) -> Result<CommandResult, RegentError> {
match self {
Handler::LocalHost(handler) => handler.run_windows_command(command).await,
Handler::Ssh2(handler) => handler.run_windows_command(command).await,
}
}
async fn get_file(&mut self, path: PathBuf) -> Result<Vec<u8>, RegentError> {
match self {
Handler::LocalHost(handler) => handler.get_file(path).await,
Handler::Ssh2(handler) => handler.get_file(path).await,
}
}
}
pub fn shell_quote(input: &str) -> String {
let mut quoted = String::with_capacity(input.len() + 2);
quoted.push('\'');
for c in input.chars() {
if c == '\'' {
quoted.push_str("'\\''");
} else {
quoted.push(c);
}
}
quoted.push('\'');
quoted
}
fn sudo_via_askpass(
sudo_binary: &str,
target_user: Option<&str>,
password: &str,
cmd: &str,
) -> String {
let user_flag = match target_user {
Some(username) => format!("-u {} ", shell_quote(username)),
None => String::new(),
};
let inner = format!(
"ASKPASS_HELPER=$(mktemp) && trap 'rm -f \"$ASKPASS_HELPER\"' EXIT && \
printf '#!/bin/sh\\nprintf %%s \"${var}\"\\n' > \"$ASKPASS_HELPER\" && chmod 700 \"$ASKPASS_HELPER\" && \
unset DISPLAY WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR && \
{var}={pw} SUDO_ASKPASS=\"$ASKPASS_HELPER\" SUDO_FORCE_ASKPASS=1 {sudo} -n -A {user_flag}sh -c {cmd}",
var = "REGENT_SUDO_PASSWORD", pw = shell_quote(password),
sudo = sudo_binary,
user_flag = user_flag,
cmd = shell_quote(cmd),
);
format!("sh -c {} 2>&1", shell_quote(&inner))
}
pub fn final_command(cmd: &str, privilege: &Privilege, user: &WhichUser) -> String {
match user {
WhichUser::CurrentUser => match privilege {
Privilege::None => format!("{} 2>&1", cmd),
Privilege::WithSudo => format!("sudo -n sh -c {} 2>&1", shell_quote(cmd)),
Privilege::WithSudoRs => format!("sudo-rs -n sh -c {} 2>&1", shell_quote(cmd)),
},
WhichUser::CurrentUserWithSudoPassword(password) => match privilege {
Privilege::None => format!("{} 2>&1", cmd),
Privilege::WithSudo => sudo_via_askpass("sudo", None, password, cmd),
Privilege::WithSudoRs => sudo_via_askpass("sudo-rs", None, password, cmd),
},
WhichUser::UsernamePassword(credentials) => {
let quoted_username = shell_quote(credentials.username());
let quoted_password = shell_quote(credentials.password());
let quoted_cmd = shell_quote(cmd);
match privilege {
Privilege::None => format!(
"printf '%s\\n' {} | su - {} -c {} 2>&1",
quoted_password, quoted_username, quoted_cmd
),
Privilege::WithSudo => sudo_via_askpass(
"sudo",
Some(credentials.username()),
credentials.password(),
cmd,
),
Privilege::WithSudoRs => sudo_via_askpass(
"sudo-rs",
Some(credentials.username()),
credentials.password(),
cmd,
),
}
}
}
}