use crate::erros::ErroSshCli;
use crate::output;
use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait};
use crate::vps::buscar_por_nome;
use anyhow::Result;
use std::path::PathBuf;
use std::time::Duration;
use tokio::net::TcpListener;
#[allow(clippy::too_many_arguments)]
pub async fn executar_tunnel(
vps_nome: &str,
porta_local: u16,
host_remoto: &str,
porta_remota: u16,
config_override: Option<PathBuf>,
password_override: Option<String>,
key_override: Option<String>,
timeout_ms: u64,
replace_host_key: bool,
) -> Result<()> {
if timeout_ms == 0 {
return Err(ErroSshCli::ArgumentoInvalido(
"tunnel exige --timeout-ms > 0 (one-shot limitado)".to_string(),
)
.into());
}
let mut vps = buscar_por_nome(config_override.clone(), vps_nome)?
.ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.to_string()))?;
if let Some(pwd) = password_override {
vps.senha = secrecy::SecretString::from(pwd);
}
if let Some(k) = key_override {
vps.key_path = Some(k);
}
let caminho = crate::vps::resolver_caminho_config(config_override)?;
let cfg = crate::vps::construir_configuracao(&vps, Some(&caminho), replace_host_key);
tracing::info!(
vps = %vps_nome,
porta_local,
host_remoto,
porta_remota,
timeout_ms,
"iniciando tunnel SSH com deadline"
);
output::escrever_linha(&format!(
"Tunnel SSH: localhost:{} -> {}:{} via {} (timeout {}ms)",
porta_local, host_remoto, porta_remota, vps_nome, timeout_ms
))?;
output::escrever_linha("Pressione Ctrl+C para encerrar antes do deadline.")?;
let cliente: Box<dyn ClienteSshTrait> = <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
let resultado = tokio::time::timeout(
Duration::from_millis(timeout_ms),
executar_tunnel_with_client(vps_nome, porta_local, host_remoto, porta_remota, cliente),
)
.await;
match resultado {
Ok(inner) => inner,
Err(_) => {
tracing::warn!(timeout_ms, "tunnel encerrou por deadline one-shot");
Err(ErroSshCli::TimeoutSsh(timeout_ms).into())
}
}
}
pub async fn executar_tunnel_with_client(
vps_nome: &str,
porta_local: u16,
host_remoto: &str,
porta_remota: u16,
cliente: Box<dyn ClienteSshTrait>,
) -> Result<()> {
let cliente: std::sync::Arc<dyn ClienteSshTrait> = std::sync::Arc::from(cliente);
let listener = TcpListener::bind(format!("127.0.0.1:{porta_local}"))
.await
.map_err(|e| {
ErroSshCli::Generico(format!("falha ao abrir porta local {}: {}", porta_local, e))
})?;
tracing::info!(porta = %porta_local, vps = %vps_nome, "listener TCP local iniciado");
loop {
if crate::signals::cancelado() || crate::signals::terminado() {
tracing::info!("tunnel cancelado por sinal");
break;
}
tokio::select! {
resultado_accept = listener.accept() => {
match resultado_accept {
Ok((soquete, addr)) => {
tracing::debug!(endereco = %addr, "nova conexão local");
let host = host_remoto.to_string();
let cliente_c = cliente.clone();
tokio::spawn(async move {
if let Err(e) = encaminhar(soquete, cliente_c, &host, porta_remota).await {
tracing::warn!(erro = %e, "falha no encaminhamento do tunnel");
}
});
}
Err(e) => {
tracing::error!(erro = %e, "accept falhou");
break;
}
}
}
_ = tokio::time::sleep(Duration::from_millis(200)) => {
}
}
}
let _ = cliente.desconectar().await;
Ok(())
}
async fn encaminhar(
mut local: tokio::net::TcpStream,
cliente: std::sync::Arc<dyn ClienteSshTrait>,
host_remoto: &str,
porta_remota: u16,
) -> Result<()> {
use tokio::io::AsyncWriteExt;
let mut canal = cliente
.abrir_canal_tunel(host_remoto, porta_remota, "127.0.0.1", 0)
.await?;
let (mut lr, mut lw) = local.split();
let (mut cr, mut cw) = tokio::io::split(&mut *canal);
let a = async {
let _ = tokio::io::copy(&mut lr, &mut cw).await;
let _ = cw.shutdown().await;
};
let b = async {
let _ = tokio::io::copy(&mut cr, &mut lw).await;
let _ = lw.shutdown().await;
};
tokio::join!(a, b);
Ok(())
}
#[cfg(test)]
mod testes {
use super::*;
use crate::ssh::cliente::mocks::MockClienteSsh;
use crate::ssh::cliente::{ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado};
use async_trait::async_trait;
use std::path::Path;
use std::sync::Arc;
#[test]
fn timeout_zero_rejeitado_conceitual() {
assert_eq!(0_u64, 0);
}
#[tokio::test]
async fn tunnel_with_client_encerra_com_cancel() {
use crate::ssh::cliente::ClienteSshTrait;
struct Stub;
#[async_trait]
impl ClienteSshTrait for Stub {
async fn conectar(
_cfg: ConfiguracaoConexao,
) -> Result<Box<Self>, crate::erros::ErroSshCli> {
Ok(Box::new(Stub))
}
async fn executar_comando(
&mut self,
_cmd: &str,
_max: usize,
) -> Result<SaidaExecucao, crate::erros::ErroSshCli> {
unreachable!()
}
async fn upload(
&mut self,
_l: &Path,
_r: &Path,
) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
unreachable!()
}
async fn download(
&mut self,
_r: &Path,
_l: &Path,
) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
unreachable!()
}
async fn abrir_canal_tunel(
&self,
_h: &str,
_p: u16,
_o: &str,
_po: u16,
) -> Result<Box<dyn crate::ssh::cliente::CanalTunel>, crate::erros::ErroSshCli>
{
Err(crate::erros::ErroSshCli::CanalFalhou("stub".into()))
}
async fn desconectar(&self) -> Result<(), crate::erros::ErroSshCli> {
Ok(())
}
}
let stub: Box<dyn ClienteSshTrait> = Box::new(Stub);
let _: Arc<dyn ClienteSshTrait> = Arc::from(stub);
let _ = MockClienteSsh::new();
}
}