add /ssh-unlock-disk command

This commit is contained in:
2026-04-16 10:54:19 +03:00
parent 486a596154
commit 27f2c8db49
6 changed files with 1464 additions and 222 deletions

View File

@@ -1,4 +1,11 @@
use std::{collections::HashSet, sync::Arc};
use std::{
collections::HashSet,
fs::File,
io::Read,
path::PathBuf,
sync::Arc,
time::Duration,
};
use anyhow::{Context as _, Result as AnyhowResult};
use deltachat::{
@@ -106,16 +113,14 @@ pub async fn wol_command(
}
let mac = match args.first() {
None => {
match ctx_lock.config.machines.values().find(|m| m.is_default) {
Some(machine) => machine.mac,
None => {
let usage = wol_usage(&ctx_lock.config);
chat::send_text_msg(&dchat_ctx_lock, chat_id, usage).await?;
return Ok(());
}
None => match ctx_lock.config.machines.values().find(|m| m.is_default) {
Some(machine) => machine.mac,
None => {
let usage = wol_usage(&ctx_lock.config);
chat::send_text_msg(&dchat_ctx_lock, chat_id, usage).await?;
return Ok(());
}
}
},
Some(arg) => {
if let Some(machine) = ctx_lock.config.machines.get(*arg) {
machine.mac
@@ -186,6 +191,24 @@ async fn send_magic_packet(mac: MacAddress) -> AnyhowResult<()> {
Ok(())
}
fn ssh_unlock_disk_usage(config: &BotConfig) -> String {
if !config.machines.is_empty() {
let list = config
.machines
.keys()
.cloned()
.collect::<Vec<_>>()
.join("|");
if config.default_machine().is_some() {
format!("Usage: /ssh-unlock-disk [{list}] <password>")
} else {
format!("Usage: /ssh-unlock-disk {list} <password>")
}
} else {
"Command /ssh-unlock-disk unavailable: no machines configured".to_string()
}
}
pub async fn ssh_unlock_disk_command(
dchat_ctx: Arc<Mutex<Context>>,
ctx: Arc<Mutex<BotContext>>,
@@ -202,7 +225,139 @@ pub async fn ssh_unlock_disk_command(
return Ok(());
}
chat::send_text_msg(&dchat_ctx_lock, chat_id, format!("*ssh unlock disk* {:?}", args)).await?;
if args.is_empty() || args.len() > 2 {
log::warn!("wrong number of args");
chat::send_text_msg(
&dchat_ctx_lock,
chat_id,
ssh_unlock_disk_usage(&ctx_lock.config),
)
.await?;
return Ok(());
}
let Some((machine_name, machine)) = ({
if args.len() > 1 {
ctx_lock.config.machines.get(args[0]).map(|m| (args[0], m))
} else if let Some((name, machine)) = ctx_lock.config.default_machine() {
log::info!("default machine: {:?} {:?}", name, machine);
Some((name, machine))
} else {
log::warn!("no machine found :/");
None
}
}) else {
chat::send_text_msg(
&dchat_ctx_lock,
chat_id,
ssh_unlock_disk_usage(&ctx_lock.config),
)
.await?;
return Ok(());
};
let Some(remote_unlock_cfg) = machine.remote_unlock.as_ref() else {
chat::send_text_msg(
&dchat_ctx_lock,
chat_id,
format!("Remote unlock is not configured for machine '{machine_name}"),
)
.await?;
return Ok(());
};
let ssh_key_relative_path: PathBuf = remote_unlock_cfg.ssh_key_file.clone().into();
let ssh_key_path = data_path().join(ssh_key_relative_path);
log::debug!("Reading SSH key from {:?}", &ssh_key_path);
let mut ssh_key_file = File::open(ssh_key_path)?;
let mut ssh_key = String::new();
ssh_key_file.read_to_string(&mut ssh_key)?;
drop(ssh_key_file);
let password = args[args.len() - 1].to_owned();
let host = machine.static_ip.to_string();
let ssh_key =
russh::keys::PrivateKey::from_openssh(ssh_key).context("Invalid remote unlock SSH key")?;
drop(ctx_lock);
drop(dchat_ctx_lock);
tokio::spawn(async move {
let ssh_config = Arc::new(russh::client::Config {
inactivity_timeout: None,
..Default::default()
});
// TODO implement real host key verification (needs config file changes)
const SSH_TIMEOUT: Duration = Duration::from_secs(15);
log::info!("Connecting to SSH {}:22", &host);
let mut connection = tokio::time::timeout(
SSH_TIMEOUT,
russh::client::connect(ssh_config, (host, 22), crate::ssh::ClientHandler {}),
)
.await
.context("Connection to host timed out")?
.context("Cannot connect to host")?;
log::info!("Trying to authenticate as root");
let auth_result = tokio::time::timeout(
SSH_TIMEOUT,
connection.authenticate_publickey(
"root",
PrivateKeyWithHashAlg::new(
Arc::new(ssh_key),
connection.best_supported_rsa_hash().await?.flatten(),
),
),
)
.await
.context("SSH authentication timed out")?
.context("SSH auth against remote unlock target failed")?;
// ignoring both fields because we only have the key
if let AuthResult::Failure {
remaining_methods: _,
partial_success: _,
} = auth_result
{
anyhow::bail!("SSH authentication failed");
}
log::info!("Opening session");
let mut channel = tokio::time::timeout(SSH_TIMEOUT, connection.channel_open_session())
.await
.context("Timed out")?
.context("Cannot open SSH session")?;
log::info!("Requesting shell");
tokio::time::timeout(SSH_TIMEOUT, channel.request_shell(true))
.await
.context("Timed out")?
.context("Cannot request disk password prompt")?;
log::info!("Sending password");
let input = password + "\n";
tokio::time::timeout(SSH_TIMEOUT, channel.data(input.as_bytes()))
.await
.context("Timed out")?
.context("Cannot send password via SSH")?;
loop {
let Some(msg) = tokio::time::timeout(SSH_TIMEOUT, channel.wait())
.await
.context("Timed out")?
else {
break;
};
log::info!("Received message from SSH server: {:?}", msg);
}
Ok(())
})
.await?
.context("SSH unlock error")?;
let dchat_ctx_lock = dchat_ctx.lock().await;
chat::send_text_msg(&dchat_ctx_lock, chat_id, "Unlock password sent".to_owned()).await?;
Ok(())
}