add /ssh-unlock-disk command
This commit is contained in:
1459
Cargo.lock
generated
1459
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ clap = { version = "4", features = [ "derive" ] }
|
|||||||
deltachat = { path = "./chatmail-core" }
|
deltachat = { path = "./chatmail-core" }
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
eui48 = { version = "1.1.0", features = [ "serde" ] }
|
eui48 = { version = "1.1.0", features = [ "serde" ] }
|
||||||
|
log = { version = "0.4.29" }
|
||||||
|
russh = { version = "0.60.0" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
serde_yaml = { version = "0.9" }
|
serde_yaml = { version = "0.9" }
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
|
|||||||
@@ -79,3 +79,14 @@ pub fn read_config(path: &Path) -> Result<BotConfig, ConfigError> {
|
|||||||
let config = serde_yaml::from_str(&contents)?;
|
let config = serde_yaml::from_str(&contents)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BotConfig {
|
||||||
|
pub fn default_machine(&self) -> Option<(&str, &BotTargetMachineConfig)> {
|
||||||
|
for (name, machine) in self.machines.iter() {
|
||||||
|
if machine.is_default {
|
||||||
|
return Some((name, machine));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
167
src/handler.rs
167
src/handler.rs
@@ -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 anyhow::{Context as _, Result as AnyhowResult};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
@@ -106,16 +113,14 @@ pub async fn wol_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mac = match args.first() {
|
let mac = match args.first() {
|
||||||
None => {
|
None => match ctx_lock.config.machines.values().find(|m| m.is_default) {
|
||||||
match ctx_lock.config.machines.values().find(|m| m.is_default) {
|
|
||||||
Some(machine) => machine.mac,
|
Some(machine) => machine.mac,
|
||||||
None => {
|
None => {
|
||||||
let usage = wol_usage(&ctx_lock.config);
|
let usage = wol_usage(&ctx_lock.config);
|
||||||
chat::send_text_msg(&dchat_ctx_lock, chat_id, usage).await?;
|
chat::send_text_msg(&dchat_ctx_lock, chat_id, usage).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if let Some(machine) = ctx_lock.config.machines.get(*arg) {
|
if let Some(machine) = ctx_lock.config.machines.get(*arg) {
|
||||||
machine.mac
|
machine.mac
|
||||||
@@ -186,6 +191,24 @@ async fn send_magic_packet(mac: MacAddress) -> AnyhowResult<()> {
|
|||||||
Ok(())
|
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(
|
pub async fn ssh_unlock_disk_command(
|
||||||
dchat_ctx: Arc<Mutex<Context>>,
|
dchat_ctx: Arc<Mutex<Context>>,
|
||||||
ctx: Arc<Mutex<BotContext>>,
|
ctx: Arc<Mutex<BotContext>>,
|
||||||
@@ -202,7 +225,139 @@ pub async fn ssh_unlock_disk_command(
|
|||||||
return Ok(());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/main.rs
21
src/main.rs
@@ -1,10 +1,11 @@
|
|||||||
mod config;
|
mod config;
|
||||||
mod handler;
|
mod handler;
|
||||||
|
mod ssh;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result as AnyhowResult};
|
use anyhow::{Context as _, Result as AnyhowResult};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
EventType, Events,
|
EventType, Events, chat,
|
||||||
config::Config,
|
config::Config,
|
||||||
context::Context,
|
context::Context,
|
||||||
message::{Message, MsgId},
|
message::{Message, MsgId},
|
||||||
@@ -39,14 +40,10 @@ fn default_config_paths() -> Vec<PathBuf> {
|
|||||||
let mut paths = vec![PathBuf::from(CONFIG_FILENAME)];
|
let mut paths = vec![PathBuf::from(CONFIG_FILENAME)];
|
||||||
|
|
||||||
if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME")
|
if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME")
|
||||||
.map(|c| PathBuf::from(c))
|
.map(PathBuf::from)
|
||||||
.or(std::env::var("HOME").map(|home| PathBuf::from(home).join(".config")))
|
.or(std::env::var("HOME").map(|home| PathBuf::from(home).join(".config")))
|
||||||
{
|
{
|
||||||
paths.push(
|
paths.push(config_home.join(APP_CONFIG_DIR).join(CONFIG_FILENAME));
|
||||||
PathBuf::from(config_home)
|
|
||||||
.join(APP_CONFIG_DIR)
|
|
||||||
.join(CONFIG_FILENAME),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paths.push(
|
paths.push(
|
||||||
@@ -132,15 +129,13 @@ async fn run_bot(cfg: config::BotConfig) -> AnyhowResult<()> {
|
|||||||
let ev_emitter = dchat_ctx.lock().await.get_event_emitter();
|
let ev_emitter = dchat_ctx.lock().await.get_event_emitter();
|
||||||
|
|
||||||
while let Some(ev) = ev_emitter.recv().await {
|
while let Some(ev) = ev_emitter.recv().await {
|
||||||
match ev.typ {
|
if let EventType::IncomingMsg { chat_id, msg_id } = ev.typ {
|
||||||
EventType::IncomingMsg { chat_id: _, msg_id } => {
|
if let Err(e) = handle_message(dchat_ctx.clone(), bot_context.clone(), msg_id).await {
|
||||||
if let Err(e) = handle_message(dchat_ctx.clone(), bot_context.clone(), msg_id).await
|
|
||||||
{
|
|
||||||
eprintln!("Error in message handler: {e:#}");
|
eprintln!("Error in message handler: {e:#}");
|
||||||
|
let ctx_lock = dchat_ctx.lock().await;
|
||||||
|
chat::send_text_msg(&ctx_lock, chat_id, format!("Error: {e:#}")).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
14
src/ssh.rs
Normal file
14
src/ssh.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use russh::client::Handler;
|
||||||
|
|
||||||
|
pub(crate) struct ClientHandler {}
|
||||||
|
|
||||||
|
impl Handler for ClientHandler {
|
||||||
|
type Error = russh::Error;
|
||||||
|
|
||||||
|
async fn check_server_key(
|
||||||
|
&mut self,
|
||||||
|
server_public_key: &russh::keys::ssh_key::PublicKey,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user