1use crate::erros::ErroSshCli;
4use crate::output;
5use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait};
6use crate::vps::buscar_por_nome;
7use anyhow::Result;
8use std::path::PathBuf;
9use std::time::Duration;
10use tokio::net::TcpListener;
11
12#[allow(clippy::too_many_arguments)]
14pub async fn executar_tunnel(
15 vps_nome: &str,
16 porta_local: u16,
17 host_remoto: &str,
18 porta_remota: u16,
19 config_override: Option<PathBuf>,
20 password_override: Option<String>,
21 key_override: Option<String>,
22 timeout_ms: u64,
23 replace_host_key: bool,
24) -> Result<()> {
25 if timeout_ms == 0 {
26 return Err(ErroSshCli::ArgumentoInvalido(
27 "tunnel exige --timeout-ms > 0 (one-shot limitado)".to_string(),
28 )
29 .into());
30 }
31
32 let mut vps = buscar_por_nome(config_override.clone(), vps_nome)?
33 .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.to_string()))?;
34
35 if let Some(pwd) = password_override {
36 vps.senha = secrecy::SecretString::from(pwd);
37 }
38 if let Some(k) = key_override {
39 vps.key_path = Some(k);
40 }
41
42 let caminho = crate::vps::resolver_caminho_config(config_override)?;
43 let cfg = crate::vps::construir_configuracao(&vps, Some(&caminho), replace_host_key);
44
45 tracing::info!(
46 vps = %vps_nome,
47 porta_local,
48 host_remoto,
49 porta_remota,
50 timeout_ms,
51 "iniciando tunnel SSH com deadline"
52 );
53
54 let banner = format!(
56 "Tunnel SSH: localhost:{} -> {}:{} via {} (timeout {}ms)",
57 porta_local, host_remoto, porta_remota, vps_nome, timeout_ms
58 );
59 tracing::info!("{banner}");
60 output::imprimir_banner_humano(&banner);
61 output::imprimir_banner_humano("Pressione Ctrl+C para encerrar antes do deadline.");
62
63 let resultado = tokio::time::timeout(Duration::from_millis(timeout_ms), async {
65 let cliente: Box<dyn ClienteSshTrait> =
66 <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
67 executar_tunnel_with_client(vps_nome, porta_local, host_remoto, porta_remota, cliente).await
68 })
69 .await;
70
71 match resultado {
72 Ok(inner) => inner,
73 Err(_) => {
74 tracing::warn!(timeout_ms, "tunnel encerrou por deadline one-shot");
75 Err(ErroSshCli::TimeoutSsh(timeout_ms).into())
76 }
77 }
78}
79
80pub async fn executar_tunnel_with_client(
82 vps_nome: &str,
83 porta_local: u16,
84 host_remoto: &str,
85 porta_remota: u16,
86 cliente: Box<dyn ClienteSshTrait>,
87) -> Result<()> {
88 let cliente: std::sync::Arc<dyn ClienteSshTrait> = std::sync::Arc::from(cliente);
89
90 let listener = TcpListener::bind(format!("127.0.0.1:{porta_local}"))
91 .await
92 .map_err(|e| {
93 ErroSshCli::Generico(format!("falha ao abrir porta local {}: {}", porta_local, e))
94 })?;
95
96 tracing::info!(porta = %porta_local, vps = %vps_nome, "listener TCP local iniciado");
97
98 loop {
99 if crate::signals::cancelado() || crate::signals::terminado() {
100 tracing::info!("tunnel cancelado por sinal");
101 break;
102 }
103
104 tokio::select! {
105 resultado_accept = listener.accept() => {
106 match resultado_accept {
107 Ok((soquete, addr)) => {
108 tracing::debug!(endereco = %addr, "nova conexão local");
109 let host = host_remoto.to_string();
110 let cliente_c = cliente.clone();
111 tokio::spawn(async move {
112 if let Err(e) = encaminhar(soquete, cliente_c, &host, porta_remota).await {
113 tracing::warn!(erro = %e, "falha no encaminhamento do tunnel");
114 }
115 });
116 }
117 Err(e) => {
118 tracing::error!(erro = %e, "accept falhou");
119 break;
120 }
121 }
122 }
123 _ = tokio::time::sleep(Duration::from_millis(200)) => {
124 }
126 }
127 }
128
129 let _ = cliente.desconectar().await;
130 Ok(())
131}
132
133async fn encaminhar(
134 mut local: tokio::net::TcpStream,
135 cliente: std::sync::Arc<dyn ClienteSshTrait>,
136 host_remoto: &str,
137 porta_remota: u16,
138) -> Result<()> {
139 use tokio::io::AsyncWriteExt;
140 let mut canal = cliente
141 .abrir_canal_tunel(host_remoto, porta_remota, "127.0.0.1", 0)
142 .await?;
143 let (mut lr, mut lw) = local.split();
144 let (mut cr, mut cw) = tokio::io::split(&mut *canal);
145 let a = async {
146 let _ = tokio::io::copy(&mut lr, &mut cw).await;
147 let _ = cw.shutdown().await;
148 };
149 let b = async {
150 let _ = tokio::io::copy(&mut cr, &mut lw).await;
151 let _ = lw.shutdown().await;
152 };
153 tokio::join!(a, b);
154 Ok(())
155}
156
157#[cfg(test)]
158mod testes {
159 use super::*;
160 use crate::ssh::cliente::mocks::MockClienteSsh;
161 use crate::ssh::cliente::{ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado};
162 use async_trait::async_trait;
163 use std::path::Path;
164 use std::sync::Arc;
165
166 #[test]
169 fn timeout_zero_rejeitado_conceitual() {
170 assert_eq!(0_u64, 0);
172 }
173
174 #[tokio::test]
175 async fn tunnel_with_client_encerra_com_cancel() {
176 use crate::ssh::cliente::ClienteSshTrait;
177
178 struct Stub;
179 #[async_trait]
180 impl ClienteSshTrait for Stub {
181 async fn conectar(
182 _cfg: ConfiguracaoConexao,
183 ) -> Result<Box<Self>, crate::erros::ErroSshCli> {
184 Ok(Box::new(Stub))
185 }
186 async fn executar_comando(
187 &mut self,
188 _cmd: &str,
189 _max: usize,
190 _stdin: Option<Vec<u8>>,
191 ) -> Result<SaidaExecucao, crate::erros::ErroSshCli> {
192 unreachable!()
193 }
194 async fn upload(
195 &mut self,
196 _l: &Path,
197 _r: &Path,
198 ) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
199 unreachable!()
200 }
201 async fn download(
202 &mut self,
203 _r: &Path,
204 _l: &Path,
205 ) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
206 unreachable!()
207 }
208 async fn abrir_canal_tunel(
209 &self,
210 _h: &str,
211 _p: u16,
212 _o: &str,
213 _po: u16,
214 ) -> Result<Box<dyn crate::ssh::cliente::CanalTunel>, crate::erros::ErroSshCli>
215 {
216 Err(crate::erros::ErroSshCli::CanalFalhou("stub".into()))
217 }
218 async fn desconectar(&self) -> Result<(), crate::erros::ErroSshCli> {
219 Ok(())
220 }
221 }
222
223 let stub: Box<dyn ClienteSshTrait> = Box::new(Stub);
225 let _: Arc<dyn ClienteSshTrait> = Arc::from(stub);
226 let _ = MockClienteSsh::new();
227 }
228}