use std::process::Stdio;
use tokio::process::Command;
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{Result, QsshError, QsshConfig};
use crate::client::QsshClient;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ProxyConfig {
pub proxy_type: ProxyType,
pub target: String,
pub options: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum ProxyType {
Jump,
Command,
None,
}
pub struct ProxyConnection {
config: ProxyConfig,
}
impl ProxyConnection {
pub fn new(config: ProxyConfig) -> Self {
Self { config }
}
pub fn parse_jump_hosts(jump_spec: &str) -> Result<Vec<JumpHost>> {
let mut hosts = Vec::new();
for host_spec in jump_spec.split(',') {
let host = JumpHost::parse(host_spec)?;
hosts.push(host);
}
Ok(hosts)
}
pub async fn connect(&self, final_host: &str, final_port: u16) -> Result<TcpStream> {
match &self.config.proxy_type {
ProxyType::Jump => self.connect_jump(final_host, final_port).await,
ProxyType::Command => self.connect_command(final_host, final_port).await,
ProxyType::None => {
TcpStream::connect(format!("{}:{}", final_host, final_port))
.await
.map_err(|e| QsshError::Connection(format!("Direct connection failed: {}", e)))
}
}
}
async fn connect_jump(&self, final_host: &str, final_port: u16) -> Result<TcpStream> {
let jump_hosts = Self::parse_jump_hosts(&self.config.target)?;
if jump_hosts.is_empty() {
return Err(QsshError::Config("No jump hosts specified".into()));
}
let first_host = &jump_hosts[0];
let mut client = QsshClient::new(QsshConfig {
server: format!("{}:{}", first_host.hostname, first_host.port),
username: first_host.username.clone(),
password: None,
port_forwards: Vec::new(),
use_qkd: false,
qkd_endpoint: None,
qkd_cert_path: None,
qkd_key_path: None,
qkd_ca_path: None,
pq_algorithm: crate::PqAlgorithm::Falcon512,
key_rotation_interval: 3600,
});
client.connect().await?;
for i in 1..jump_hosts.len() {
let next_host = &jump_hosts[i];
client = self.forward_through_host(client, next_host).await?;
}
self.forward_to_final(client, final_host, final_port).await
}
async fn forward_through_host(&self, mut client: QsshClient, host: &JumpHost) -> Result<QsshClient> {
let forward_port = self.allocate_forward_port();
client.request_port_forward(
forward_port,
&host.hostname,
host.port,
).await?;
let new_client = QsshClient::new(QsshConfig {
server: format!("localhost:{}", forward_port),
username: host.username.clone(),
password: None,
port_forwards: Vec::new(),
use_qkd: false,
qkd_endpoint: None,
qkd_cert_path: None,
qkd_key_path: None,
qkd_ca_path: None,
pq_algorithm: crate::PqAlgorithm::Falcon512,
key_rotation_interval: 3600,
});
Ok(new_client)
}
async fn forward_to_final(&self, mut client: QsshClient, host: &str, port: u16) -> Result<TcpStream> {
let channel = client.open_direct_tcpip(host, port).await?;
Ok(channel.into_stream())
}
async fn connect_command(&self, final_host: &str, final_port: u16) -> Result<TcpStream> {
let command = self.config.target.clone()
.replace("%h", final_host)
.replace("%p", &final_port.to_string());
let mut child = Command::new("sh")
.arg("-c")
.arg(&command)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| QsshError::Connection(format!("Failed to spawn proxy command: {}", e)))?;
let stdin = child.stdin.take()
.ok_or_else(|| QsshError::Connection("Failed to get stdin".into()))?;
let stdout = child.stdout.take()
.ok_or_else(|| QsshError::Connection("Failed to get stdout".into()))?;
Ok(ProcessStream::new(stdin, stdout, child).into_tcp_stream().await?)
}
fn allocate_forward_port(&self) -> u16 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(49152..65535)
}
}
#[derive(Debug, Clone)]
pub struct JumpHost {
pub username: String,
pub hostname: String,
pub port: u16,
pub identity_file: Option<String>,
}
impl JumpHost {
pub fn parse(spec: &str) -> Result<Self> {
let mut username = String::from(whoami::username());
let mut hostname = String::new();
let mut port = 22;
let parts: Vec<&str> = spec.split('@').collect();
let host_part = if parts.len() == 2 {
username = parts[0].to_string();
parts[1]
} else {
parts[0]
};
let host_parts: Vec<&str> = host_part.split(':').collect();
hostname = host_parts[0].to_string();
if host_parts.len() == 2 {
port = host_parts[1].parse()
.map_err(|_| QsshError::Config(format!("Invalid port: {}", host_parts[1])))?;
}
Ok(Self {
username,
hostname,
port,
identity_file: None, })
}
}
struct ProcessStream {
stdin: tokio::process::ChildStdin,
stdout: tokio::process::ChildStdout,
_child: tokio::process::Child,
}
impl ProcessStream {
fn new(
stdin: tokio::process::ChildStdin,
stdout: tokio::process::ChildStdout,
child: tokio::process::Child,
) -> Self {
Self {
stdin,
stdout,
_child: child,
}
}
async fn into_tcp_stream(self) -> Result<TcpStream> {
Err(QsshError::Protocol("ProcessStream to TcpStream conversion not fully implemented".into()))
}
}
impl QsshClient {
pub async fn request_port_forward(&mut self, local_port: u16, remote_host: &str, remote_port: u16) -> Result<()> {
log::info!("Setting up port forward: {}:{}:{}", local_port, remote_host, remote_port);
Ok(())
}
pub async fn open_direct_tcpip(&mut self, host: &str, port: u16) -> Result<DirectTcpipChannel> {
log::debug!("Opening direct-tcpip to {}:{}", host, port);
Ok(DirectTcpipChannel::new())
}
}
pub struct DirectTcpipChannel {
}
impl DirectTcpipChannel {
fn new() -> Self {
Self {}
}
pub fn into_stream(self) -> TcpStream {
panic!("DirectTcpipChannel::into_stream not fully implemented")
}
}
pub fn parse_proxy_jump(value: &str) -> Result<ProxyConfig> {
Ok(ProxyConfig {
proxy_type: ProxyType::Jump,
target: value.to_string(),
options: Vec::new(),
})
}
pub fn parse_proxy_command(value: &str) -> Result<ProxyConfig> {
Ok(ProxyConfig {
proxy_type: ProxyType::Command,
target: value.to_string(),
options: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_jump_host_simple() {
let host = JumpHost::parse("jumphost").unwrap();
assert_eq!(host.hostname, "jumphost");
assert_eq!(host.port, 22);
assert_eq!(host.username, whoami::username());
}
#[test]
fn test_parse_jump_host_with_user() {
let host = JumpHost::parse("alice@jumphost").unwrap();
assert_eq!(host.username, "alice");
assert_eq!(host.hostname, "jumphost");
assert_eq!(host.port, 22);
}
#[test]
fn test_parse_jump_host_with_port() {
let host = JumpHost::parse("jumphost:2222").unwrap();
assert_eq!(host.hostname, "jumphost");
assert_eq!(host.port, 2222);
}
#[test]
fn test_parse_jump_host_full() {
let host = JumpHost::parse("bob@jumphost:8022").unwrap();
assert_eq!(host.username, "bob");
assert_eq!(host.hostname, "jumphost");
assert_eq!(host.port, 8022);
}
#[test]
fn test_parse_multiple_jump_hosts() {
let hosts = ProxyConnection::parse_jump_hosts("user1@host1,host2:222,user3@host3:333").unwrap();
assert_eq!(hosts.len(), 3);
assert_eq!(hosts[0].username, "user1");
assert_eq!(hosts[0].hostname, "host1");
assert_eq!(hosts[0].port, 22);
assert_eq!(hosts[1].hostname, "host2");
assert_eq!(hosts[1].port, 222);
assert_eq!(hosts[2].username, "user3");
assert_eq!(hosts[2].hostname, "host3");
assert_eq!(hosts[2].port, 333);
}
#[test]
fn test_proxy_command_substitution() {
let config = ProxyConfig {
proxy_type: ProxyType::Command,
target: "nc -X connect -x proxy:8080 %h %p".to_string(),
options: Vec::new(),
};
let conn = ProxyConnection::new(config);
assert!(conn.config.target.contains("%h"));
assert!(conn.config.target.contains("%p"));
}
}