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