use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result, bail};
use bytes::Bytes;
use russh::keys::PrivateKey;
use tokio::sync::mpsc;
use crate::auth::ServerFactory;
use crate::state::{CHANNEL_CAPACITY, DownMsg, SessionQueue, ShutdownSignal, UpMsg};
use russh::server::Server as _;
const INACTIVITY_TIMEOUT: Duration = Duration::from_secs(300);
pub fn serve(
listener: std::net::TcpListener,
key: PrivateKey,
user: String,
password: String,
queue: Arc<SessionQueue>,
shutdown_rx: std::sync::mpsc::Receiver<ShutdownSignal>,
) {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(_) => return,
};
runtime.block_on(async move {
serve_async(listener, key, user, password, queue, shutdown_rx)
.await
.map(|_| ())
.unwrap_or(());
});
}
async fn serve_async(
listener: std::net::TcpListener,
key: PrivateKey,
user: String,
password: String,
queue: Arc<SessionQueue>,
shutdown_rx: std::sync::mpsc::Receiver<ShutdownSignal>,
) -> Result<()> {
let config = Arc::new(russh::server::Config {
inactivity_timeout: Some(INACTIVITY_TIMEOUT),
auth_rejection_time: Duration::from_secs(1),
keys: vec![key],
..Default::default()
});
let mut factory = ServerFactory::new(user, password, queue);
listener
.set_nonblocking(true)
.context("prepare listener for the runtime")?;
let listener =
tokio::net::TcpListener::from_std(listener).context("hand listener to the runtime")?;
let running = factory.run_on_socket(config, &listener);
let handle = running.handle();
let waiter =
tokio::task::spawn_blocking(move || shutdown_rx.recv().map(|_| ()).map_err(|_| ()));
tokio::select! {
result = running => {
result.context("SSH listener failed")?;
}
_ = waiter => {
handle.shutdown("SSH_CLOSE".to_string());
}
}
Ok(())
}
pub(crate) struct AcceptAnyHostKey;
impl russh::client::Handler for AcceptAnyHostKey {
type Error = anyhow::Error;
async fn check_server_key(
&mut self,
_server_public_key: &russh::keys::PublicKeyOrCertificate,
) -> Result<bool> {
Ok(true)
}
}
pub struct OutboundSession {
pub up_rx: mpsc::Receiver<UpMsg>,
pub down_tx: mpsc::Sender<DownMsg>,
pub(crate) handle: russh::client::Handle<AcceptAnyHostKey>,
}
pub fn connect_runtime() -> Result<tokio::runtime::Runtime> {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.context("build SSH client runtime")
}
pub async fn connect_session(
target: &SocketAddr,
username: &str,
password: &str,
) -> Result<OutboundSession> {
let config = Arc::new(russh::client::Config {
inactivity_timeout: Some(INACTIVITY_TIMEOUT),
..Default::default()
});
let mut session = russh::client::connect(config, *target, AcceptAnyHostKey)
.await
.context("SSH_CONNECT dial failed")?;
let authenticated = session
.authenticate_password(username, password)
.await
.context("SSH_CONNECT authentication exchange failed")?;
if !matches!(authenticated, russh::client::AuthResult::Success) {
bail!("SSH_CONNECT authentication rejected");
}
let channel = session
.channel_open_session()
.await
.context("SSH_CONNECT channel open failed")?;
channel
.request_shell(true)
.await
.context("SSH_CONNECT shell request failed")?;
let (mut reader, writer) = channel.split();
let (up_tx, up_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (down_tx, mut down_rx) = mpsc::channel::<DownMsg>(CHANNEL_CAPACITY);
tokio::spawn(async move {
while let Some(message) = reader.wait().await {
match message {
russh::ChannelMsg::Data { data } => {
if up_tx
.send(UpMsg::Data(Bytes::copy_from_slice(&data)))
.await
.is_err()
{
break;
}
}
russh::ChannelMsg::Eof => {
let _ = up_tx.send(UpMsg::Eof).await;
break;
}
russh::ChannelMsg::Close => break,
_ => {}
}
}
});
tokio::spawn(async move {
while let Some(message) = down_rx.recv().await {
let result = match message {
DownMsg::Data(bytes) => writer.data_bytes(bytes).await.map(|_| ()),
DownMsg::Eof => writer.eof().await.map(|_| ()),
};
if result.is_err() {
break;
}
}
let _ = writer.close().await;
});
Ok(OutboundSession {
up_rx,
down_tx,
handle: session,
})
}