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 output::escrever_linha(&format!(
55 "Tunnel SSH: localhost:{} -> {}:{} via {} (timeout {}ms)",
56 porta_local, host_remoto, porta_remota, vps_nome, timeout_ms
57 ))?;
58 output::escrever_linha("Pressione Ctrl+C para encerrar antes do deadline.")?;
59
60 let cliente: Box<dyn ClienteSshTrait> = <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
61 let resultado = tokio::time::timeout(
62 Duration::from_millis(timeout_ms),
63 executar_tunnel_with_client(vps_nome, porta_local, host_remoto, porta_remota, cliente),
64 )
65 .await;
66
67 match resultado {
68 Ok(inner) => inner,
69 Err(_) => {
70 tracing::warn!(timeout_ms, "tunnel encerrou por deadline one-shot");
71 Err(ErroSshCli::TimeoutSsh(timeout_ms).into())
72 }
73 }
74}
75
76pub async fn executar_tunnel_with_client(
78 vps_nome: &str,
79 porta_local: u16,
80 host_remoto: &str,
81 porta_remota: u16,
82 cliente: Box<dyn ClienteSshTrait>,
83) -> Result<()> {
84 let cliente: std::sync::Arc<dyn ClienteSshTrait> = std::sync::Arc::from(cliente);
85
86 let listener = TcpListener::bind(format!("127.0.0.1:{porta_local}"))
87 .await
88 .map_err(|e| {
89 ErroSshCli::Generico(format!("falha ao abrir porta local {}: {}", porta_local, e))
90 })?;
91
92 tracing::info!(porta = %porta_local, vps = %vps_nome, "listener TCP local iniciado");
93
94 loop {
95 if crate::signals::cancelado() || crate::signals::terminado() {
96 tracing::info!("tunnel cancelado por sinal");
97 break;
98 }
99
100 tokio::select! {
101 resultado_accept = listener.accept() => {
102 match resultado_accept {
103 Ok((soquete, addr)) => {
104 tracing::debug!(endereco = %addr, "nova conexão local");
105 let host = host_remoto.to_string();
106 let cliente_c = cliente.clone();
107 tokio::spawn(async move {
108 if let Err(e) = encaminhar(soquete, cliente_c, &host, porta_remota).await {
109 tracing::warn!(erro = %e, "falha no encaminhamento do tunnel");
110 }
111 });
112 }
113 Err(e) => {
114 tracing::error!(erro = %e, "accept falhou");
115 break;
116 }
117 }
118 }
119 _ = tokio::time::sleep(Duration::from_millis(200)) => {
120 }
122 }
123 }
124
125 let _ = cliente.desconectar().await;
126 Ok(())
127}
128
129async fn encaminhar(
130 mut local: tokio::net::TcpStream,
131 cliente: std::sync::Arc<dyn ClienteSshTrait>,
132 host_remoto: &str,
133 porta_remota: u16,
134) -> Result<()> {
135 use tokio::io::AsyncWriteExt;
136 let mut canal = cliente
137 .abrir_canal_tunel(host_remoto, porta_remota, "127.0.0.1", 0)
138 .await?;
139 let (mut lr, mut lw) = local.split();
140 let (mut cr, mut cw) = tokio::io::split(&mut *canal);
141 let a = async {
142 let _ = tokio::io::copy(&mut lr, &mut cw).await;
143 let _ = cw.shutdown().await;
144 };
145 let b = async {
146 let _ = tokio::io::copy(&mut cr, &mut lw).await;
147 let _ = lw.shutdown().await;
148 };
149 tokio::join!(a, b);
150 Ok(())
151}
152
153#[cfg(test)]
154mod testes {
155 use super::*;
156 use crate::ssh::cliente::mocks::MockClienteSsh;
157 use crate::ssh::cliente::{ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado};
158 use async_trait::async_trait;
159 use std::path::Path;
160 use std::sync::Arc;
161
162 #[test]
165 fn timeout_zero_rejeitado_conceitual() {
166 assert_eq!(0_u64, 0);
168 }
169
170 #[tokio::test]
171 async fn tunnel_with_client_encerra_com_cancel() {
172 use crate::ssh::cliente::ClienteSshTrait;
173
174 struct Stub;
175 #[async_trait]
176 impl ClienteSshTrait for Stub {
177 async fn conectar(
178 _cfg: ConfiguracaoConexao,
179 ) -> Result<Box<Self>, crate::erros::ErroSshCli> {
180 Ok(Box::new(Stub))
181 }
182 async fn executar_comando(
183 &mut self,
184 _cmd: &str,
185 _max: usize,
186 ) -> Result<SaidaExecucao, crate::erros::ErroSshCli> {
187 unreachable!()
188 }
189 async fn upload(
190 &mut self,
191 _l: &Path,
192 _r: &Path,
193 ) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
194 unreachable!()
195 }
196 async fn download(
197 &mut self,
198 _r: &Path,
199 _l: &Path,
200 ) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
201 unreachable!()
202 }
203 async fn abrir_canal_tunel(
204 &self,
205 _h: &str,
206 _p: u16,
207 _o: &str,
208 _po: u16,
209 ) -> Result<Box<dyn crate::ssh::cliente::CanalTunel>, crate::erros::ErroSshCli>
210 {
211 Err(crate::erros::ErroSshCli::CanalFalhou("stub".into()))
212 }
213 async fn desconectar(&self) -> Result<(), crate::erros::ErroSshCli> {
214 Ok(())
215 }
216 }
217
218 let stub: Box<dyn ClienteSshTrait> = Box::new(Stub);
220 let _: Arc<dyn ClienteSshTrait> = Arc::from(stub);
221 let _ = MockClienteSsh::new();
222 }
223}