mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-25 16:01:05 +00:00
433 lines
15 KiB
Rust
433 lines
15 KiB
Rust
use std::{
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
|
|
use anyhow::{Context, Result, anyhow};
|
|
use async_trait::async_trait;
|
|
use directories::BaseDirs;
|
|
use russh::{
|
|
ChannelMsg, Disconnect,
|
|
client::{self, Handler},
|
|
keys::{HashAlg, PrivateKey, decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key},
|
|
};
|
|
use tokio::sync::mpsc;
|
|
|
|
use crate::{
|
|
config::{AuthMethod, Session},
|
|
system::{SystemSnapshot, remote_snapshot_from_kv},
|
|
terminal::{BackendCommand, BackendEvent, BackendTx},
|
|
};
|
|
|
|
pub fn spawn_ssh_terminal(
|
|
runtime: &tokio::runtime::Handle,
|
|
tab_id: String,
|
|
session: Session,
|
|
cols: u16,
|
|
rows: u16,
|
|
events: std::sync::mpsc::Sender<BackendEvent>,
|
|
) -> BackendTx {
|
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<BackendCommand>();
|
|
let task_tab = tab_id.clone();
|
|
runtime.spawn(async move {
|
|
if let Err(err) = run_ssh(
|
|
task_tab.clone(),
|
|
session,
|
|
cols,
|
|
rows,
|
|
cmd_rx,
|
|
events.clone(),
|
|
)
|
|
.await
|
|
{
|
|
let _ = events.send(BackendEvent::Closed {
|
|
tab_id: task_tab,
|
|
reason: format!("{err:#}"),
|
|
});
|
|
}
|
|
});
|
|
BackendTx::Ssh(cmd_tx)
|
|
}
|
|
|
|
pub async fn sample_remote_system(session: Session) -> Result<SystemSnapshot> {
|
|
let (events_tx, _events_rx) = std::sync::mpsc::channel();
|
|
let handle = connect_and_authenticate("remote-metrics", &session, &events_tx).await?;
|
|
let mut channel = handle
|
|
.channel_open_session()
|
|
.await
|
|
.context("open metrics session")?;
|
|
channel
|
|
.exec(true, REMOTE_SYSTEM_PROBE)
|
|
.await
|
|
.context("exec remote metrics probe")?;
|
|
|
|
let mut stdout = Vec::new();
|
|
while let Some(msg) = channel.wait().await {
|
|
match msg {
|
|
ChannelMsg::Data { data } | ChannelMsg::ExtendedData { data, ext: _ } => {
|
|
stdout.extend_from_slice(&data);
|
|
}
|
|
ChannelMsg::Close => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let _ = handle
|
|
.disconnect(Disconnect::ByApplication, "metrics done", "")
|
|
.await;
|
|
let output = String::from_utf8_lossy(&stdout);
|
|
remote_snapshot_from_kv(&output)
|
|
}
|
|
|
|
async fn run_ssh(
|
|
tab_id: String,
|
|
session: Session,
|
|
cols: u16,
|
|
rows: u16,
|
|
mut commands: mpsc::UnboundedReceiver<BackendCommand>,
|
|
events: std::sync::mpsc::Sender<BackendEvent>,
|
|
) -> Result<()> {
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.clone(),
|
|
text: format!(
|
|
"connecting {}@{}:{}...",
|
|
session.user, session.host, session.port
|
|
),
|
|
});
|
|
|
|
let handle = connect_and_authenticate(&tab_id, &session, &events).await?;
|
|
|
|
let mut channel = handle
|
|
.channel_open_session()
|
|
.await
|
|
.context("open session")?;
|
|
channel
|
|
.request_pty(true, "xterm-256color", cols.into(), rows.into(), 0, 0, &[])
|
|
.await
|
|
.context("request pty")?;
|
|
channel.request_shell(true).await.context("request shell")?;
|
|
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.clone(),
|
|
text: format!("connected {}@{}", session.user, session.host),
|
|
});
|
|
let _ = events.send(BackendEvent::Connected {
|
|
tab_id: tab_id.clone(),
|
|
});
|
|
|
|
loop {
|
|
tokio::select! {
|
|
command = commands.recv() => {
|
|
match command {
|
|
Some(BackendCommand::Input(bytes)) => {
|
|
if let Err(err) = channel.data(bytes.as_slice()).await {
|
|
let _ = events.send(BackendEvent::Closed {
|
|
tab_id: tab_id.clone(),
|
|
reason: format!("ssh write error: {err}"),
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
Some(BackendCommand::Resize { cols, rows }) => {
|
|
let _ = channel.window_change(cols.into(), rows.into(), 0, 0).await;
|
|
}
|
|
Some(BackendCommand::Close) | None => {
|
|
let _ = channel.eof().await;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
msg = channel.wait() => {
|
|
match msg {
|
|
Some(ChannelMsg::Data { data }) | Some(ChannelMsg::ExtendedData { data, ext: _ }) => {
|
|
let _ = events.send(BackendEvent::Output {
|
|
tab_id: tab_id.clone(),
|
|
bytes: data.to_vec(),
|
|
});
|
|
}
|
|
Some(ChannelMsg::Close) | None => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = handle
|
|
.disconnect(Disconnect::ByApplication, "bye", "")
|
|
.await;
|
|
let _ = events.send(BackendEvent::Closed {
|
|
tab_id,
|
|
reason: "ssh session closed".into(),
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
async fn connect_and_authenticate(
|
|
tab_id: &str,
|
|
session: &Session,
|
|
events: &std::sync::mpsc::Sender<BackendEvent>,
|
|
) -> Result<russh::client::Handle<ClientHandler>> {
|
|
let config = Arc::new(client::Config {
|
|
inactivity_timeout: Some(std::time::Duration::from_secs(600)),
|
|
..Default::default()
|
|
});
|
|
let addr = format!("{}:{}", session.host, session.port);
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.to_string(),
|
|
text: format!("opening tcp connection to {addr}"),
|
|
});
|
|
let mut handle = client::connect(config, addr.as_str(), ClientHandler)
|
|
.await
|
|
.with_context(|| format!("connect {addr} failed"))?;
|
|
|
|
let authed = match session.auth {
|
|
AuthMethod::Password => {
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.to_string(),
|
|
text: format!(
|
|
"connected to {addr}, sending password authentication for {}",
|
|
session.user
|
|
),
|
|
});
|
|
handle
|
|
.authenticate_password(&session.user, &session.password)
|
|
.await
|
|
.context("password authentication failed")?
|
|
}
|
|
AuthMethod::Key => {
|
|
let source = key_source_label(session);
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.to_string(),
|
|
text: format!("connected to {addr}, loading private key from {source}"),
|
|
});
|
|
let keypair = load_session_private_key(session)?;
|
|
let algorithm = format!("{:?}", keypair.algorithm());
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.to_string(),
|
|
text: format!("private key loaded from {source}, algorithm {algorithm}, sending public key authentication for {}", session.user),
|
|
});
|
|
let key = private_key_with_alg(keypair).context("invalid private key")?;
|
|
handle
|
|
.authenticate_publickey(&session.user, key)
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"public key authentication failed for {}@{}:{} using {} ({})",
|
|
session.user, session.host, session.port, source, algorithm
|
|
)
|
|
})?
|
|
}
|
|
};
|
|
|
|
if !authed {
|
|
let _ = handle
|
|
.disconnect(Disconnect::ByApplication, "auth failed", "")
|
|
.await;
|
|
return Err(anyhow!(
|
|
"{}",
|
|
match session.auth {
|
|
AuthMethod::Password => format!(
|
|
"authentication failed: server rejected password authentication for {}@{}:{}",
|
|
session.user, session.host, session.port
|
|
),
|
|
AuthMethod::Key => format!(
|
|
"authentication failed: server rejected public key authentication for {}@{}:{} using {}",
|
|
session.user,
|
|
session.host,
|
|
session.port,
|
|
key_source_label(session)
|
|
),
|
|
}
|
|
));
|
|
}
|
|
|
|
let _ = events.send(BackendEvent::Status {
|
|
tab_id: tab_id.to_string(),
|
|
text: format!(
|
|
"authentication accepted, opening shell for {}@{}",
|
|
session.user, session.host
|
|
),
|
|
});
|
|
|
|
Ok(handle)
|
|
}
|
|
|
|
fn load_session_private_key(session: &Session) -> Result<PrivateKey> {
|
|
let inline_key = normalize_inline_private_key(&session.private_key_inline);
|
|
let key_path = expand_key_path(session.private_key_path.trim());
|
|
let has_inline = !inline_key.is_empty();
|
|
let has_path = key_path.is_some();
|
|
|
|
if !has_inline && !has_path {
|
|
return Err(anyhow!("private key content or path is required"));
|
|
}
|
|
|
|
let mut errors = Vec::new();
|
|
|
|
if has_inline {
|
|
match decode_secret_key(&inline_key, None) {
|
|
Ok(key) => return Ok(key),
|
|
Err(err) => errors.push(format!("decode private key content: {err}")),
|
|
}
|
|
}
|
|
|
|
if let Some(path) = key_path {
|
|
match load_secret_key(path.as_path(), None) {
|
|
Ok(key) => return Ok(key),
|
|
Err(err) => errors.push(format!("load key {}: {err}", path.display())),
|
|
}
|
|
}
|
|
|
|
Err(anyhow!(errors.join("; ")))
|
|
}
|
|
|
|
fn private_key_with_alg(keypair: PrivateKey) -> Result<PrivateKeyWithHashAlg> {
|
|
let hash_alg = if keypair.algorithm().is_rsa() {
|
|
Some(HashAlg::Sha512)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(
|
|
PrivateKeyWithHashAlg::new(Arc::new(keypair.clone()), hash_alg)
|
|
.or_else(|_| PrivateKeyWithHashAlg::new(Arc::new(keypair), Some(HashAlg::Sha256)))?,
|
|
)
|
|
}
|
|
|
|
fn normalize_inline_private_key(value: &str) -> String {
|
|
let mut normalized = value
|
|
.trim()
|
|
.replace("\\r\\n", "\n")
|
|
.replace("\\n", "\n")
|
|
.replace("\r\n", "\n");
|
|
if !normalized.ends_with('\n') {
|
|
normalized.push('\n');
|
|
}
|
|
normalized
|
|
}
|
|
|
|
fn expand_key_path(value: &str) -> Option<PathBuf> {
|
|
if value.is_empty() {
|
|
return None;
|
|
}
|
|
if value == "~" {
|
|
return BaseDirs::new().map(|dirs| dirs.home_dir().to_path_buf());
|
|
}
|
|
if let Some(rest) = value.strip_prefix("~/") {
|
|
return BaseDirs::new().map(|dirs| dirs.home_dir().join(rest));
|
|
}
|
|
Some(Path::new(value).to_path_buf())
|
|
}
|
|
|
|
fn key_source_label(session: &Session) -> String {
|
|
let path = session.private_key_path.trim();
|
|
let has_inline = !session.private_key_inline.trim().is_empty();
|
|
match (!path.is_empty(), has_inline) {
|
|
(true, true) => format!("inline key or {}", path),
|
|
(true, false) => path.to_string(),
|
|
(false, true) => "inline key text".to_string(),
|
|
(false, false) => "unknown key source".to_string(),
|
|
}
|
|
}
|
|
|
|
const REMOTE_SYSTEM_PROBE: &str = r#"sh -lc '
|
|
os=$(uname -s 2>/dev/null || echo unknown)
|
|
|
|
if [ "$os" = "Linux" ] && [ -r /proc/stat ]; then
|
|
cpu_stat() { awk '"'"'/^cpu / { print ($2+$3+$4+$5+$6+$7+$8), $5 }'"'"' /proc/stat 2>/dev/null; }
|
|
net_stat() { awk -F"[: ]+" '"'"'/:/ && $1!="Inter" && $1!="face" { rx += $3; tx += $11 } END { print rx+0, tx+0 }'"'"' /proc/net/dev 2>/dev/null; }
|
|
|
|
read cpu_total_1 cpu_idle_1 <<EOF
|
|
$(cpu_stat)
|
|
EOF
|
|
read net_rx_1 net_tx_1 <<EOF
|
|
$(net_stat)
|
|
EOF
|
|
sleep 1
|
|
read cpu_total_2 cpu_idle_2 <<EOF
|
|
$(cpu_stat)
|
|
EOF
|
|
read net_rx_2 net_tx_2 <<EOF
|
|
$(net_stat)
|
|
EOF
|
|
|
|
cpu_delta=$((cpu_total_2 - cpu_total_1))
|
|
idle_delta=$((cpu_idle_2 - cpu_idle_1))
|
|
cpu_percent=$(awk -v total="$cpu_delta" -v idle="$idle_delta" '"'"'BEGIN { if (total <= 0) print "0.00"; else printf "%.2f", ((total-idle)/total)*100 }'"'"')
|
|
mem_total=$(awk '"'"'/^MemTotal:/ {print $2 * 1024}'"'"' /proc/meminfo 2>/dev/null)
|
|
mem_available=$(awk '"'"'/^MemAvailable:/ {print $2 * 1024}'"'"' /proc/meminfo 2>/dev/null)
|
|
swap_total=$(awk '"'"'/^SwapTotal:/ {print $2 * 1024}'"'"' /proc/meminfo 2>/dev/null)
|
|
swap_free=$(awk '"'"'/^SwapFree:/ {print $2 * 1024}'"'"' /proc/meminfo 2>/dev/null)
|
|
|
|
echo "CPU_PERCENT=${cpu_percent:-0.00}"
|
|
echo "MEM_TOTAL=${mem_total:-0}"
|
|
echo "MEM_USED=$(( ${mem_total:-0} - ${mem_available:-0} ))"
|
|
echo "SWAP_TOTAL=${swap_total:-0}"
|
|
echo "SWAP_USED=$(( ${swap_total:-0} - ${swap_free:-0} ))"
|
|
echo "NET_RX=$(( ${net_rx_2:-0} - ${net_rx_1:-0} ))"
|
|
echo "NET_TX=$(( ${net_tx_2:-0} - ${net_tx_1:-0} ))"
|
|
df -kP 2>/dev/null | awk "NR > 1 && \$1 !~ /^(tmpfs|devtmpfs|ramfs|overlay|aufs)\$/ { printf \"DISK=%s\t%s\t%s\n\", \$6, \$4 * 1024, \$2 * 1024 }" | head -n 6
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$os" = "Darwin" ]; then
|
|
net_stat() { netstat -ibn 2>/dev/null | awk '"'"'NR > 1 && $7 ~ /^[0-9]+$/ && $10 ~ /^[0-9]+$/ { rx += $7; tx += $10 } END { print rx+0, tx+0 }'"'"'; }
|
|
|
|
read net_rx_1 net_tx_1 <<EOF
|
|
$(net_stat)
|
|
EOF
|
|
sleep 1
|
|
read net_rx_2 net_tx_2 <<EOF
|
|
$(net_stat)
|
|
EOF
|
|
|
|
cpu_percent=$(top -l 2 -n 0 -s 1 2>/dev/null | awk -F"[:,% ]+" '"'"'/CPU usage:/ { user=$3; sys=$5 } END { if (user == "" && sys == "") print "0.00"; else printf "%.2f", user + sys }'"'"')
|
|
mem_total=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
|
|
pagesize=$(sysctl -n hw.pagesize 2>/dev/null || echo 4096)
|
|
vm_output=$(vm_stat 2>/dev/null)
|
|
pages_active=$(printf "%s\n" "$vm_output" | awk '"'"'/Pages active/ { gsub("\\.","",$3); print $3+0 }'"'"')
|
|
pages_wired=$(printf "%s\n" "$vm_output" | awk '"'"'/Pages wired down/ { gsub("\\.","",$4); print $4+0 }'"'"')
|
|
pages_compressed=$(printf "%s\n" "$vm_output" | awk '"'"'/Pages occupied by compressor/ { gsub("\\.","",$5); print $5+0 }'"'"')
|
|
pages_speculative=$(printf "%s\n" "$vm_output" | awk '"'"'/Pages speculative/ { gsub("\\.","",$3); print $3+0 }'"'"')
|
|
mem_used=$(( (${pages_active:-0} + ${pages_wired:-0} + ${pages_compressed:-0} + ${pages_speculative:-0}) * ${pagesize:-4096} ))
|
|
swap_line=$(sysctl vm.swapusage 2>/dev/null || true)
|
|
swap_used=$(printf "%s\n" "$swap_line" | awk -F"[= ,]+" '"'"'
|
|
function mult(unit) { return unit=="K"?1024:(unit=="M"?1048576:(unit=="G"?1073741824:(unit=="T"?1099511627776:1))) }
|
|
/used/ { value=$4; unit=substr(value, length(value), 1); sub(/[A-Za-z]+$/, "", value); printf "%.0f", value * mult(unit) }'"'"')
|
|
swap_total=$(printf "%s\n" "$swap_line" | awk -F"[= ,]+" '"'"'
|
|
function mult(unit) { return unit=="K"?1024:(unit=="M"?1048576:(unit=="G"?1073741824:(unit=="T"?1099511627776:1))) }
|
|
/used/ && /free/ { used=$4; free=$8; unit1=substr(used, length(used), 1); unit2=substr(free, length(free), 1); sub(/[A-Za-z]+$/, "", used); sub(/[A-Za-z]+$/, "", free); printf "%.0f", (used * mult(unit1)) + (free * mult(unit2)) }'"'"')
|
|
|
|
echo "CPU_PERCENT=${cpu_percent:-0.00}"
|
|
echo "MEM_TOTAL=${mem_total:-0}"
|
|
echo "MEM_USED=${mem_used:-0}"
|
|
echo "SWAP_TOTAL=${swap_total:-0}"
|
|
echo "SWAP_USED=${swap_used:-0}"
|
|
echo "NET_RX=$(( ${net_rx_2:-0} - ${net_rx_1:-0} ))"
|
|
echo "NET_TX=$(( ${net_tx_2:-0} - ${net_tx_1:-0} ))"
|
|
df -kP 2>/dev/null | awk "NR > 1 && \$1 !~ /^(devfs|tmpfs|devtmpfs|ramfs|overlay|aufs)\$/ { printf \"DISK=%s\t%s\t%s\n\", \$6, \$4 * 1024, \$2 * 1024 }" | head -n 6
|
|
exit 0
|
|
fi
|
|
|
|
echo "CPU_PERCENT=0.00"
|
|
echo "MEM_TOTAL=0"
|
|
echo "MEM_USED=0"
|
|
echo "SWAP_TOTAL=0"
|
|
echo "SWAP_USED=0"
|
|
echo "NET_RX=0"
|
|
echo "NET_TX=0"
|
|
'"#;
|
|
|
|
struct ClientHandler;
|
|
|
|
#[async_trait]
|
|
impl Handler for ClientHandler {
|
|
type Error = anyhow::Error;
|
|
|
|
async fn check_server_key(
|
|
&mut self,
|
|
_server_public_key: &russh::keys::ssh_key::PublicKey,
|
|
) -> Result<bool, Self::Error> {
|
|
Ok(true)
|
|
}
|
|
}
|