use crate::{Error, Result};
use ssh2::Session;
use std::io::Read;
use std::net::TcpStream;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct RemoteMachine {
pub host: String,
pub user: Option<String>,
pub port: Option<u16>,
pub identity_file: Option<PathBuf>,
pub freenet_binary: Option<PathBuf>,
pub work_dir: Option<PathBuf>,
}
impl RemoteMachine {
pub fn new(host: impl Into<String>) -> Self {
Self {
host: host.into(),
user: None,
port: None,
identity_file: None,
freenet_binary: None,
work_dir: None,
}
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn identity_file(mut self, path: impl Into<PathBuf>) -> Self {
self.identity_file = Some(path.into());
self
}
pub fn freenet_binary(mut self, path: impl Into<PathBuf>) -> Self {
self.freenet_binary = Some(path.into());
self
}
pub fn work_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.work_dir = Some(path.into());
self
}
pub fn ssh_port(&self) -> u16 {
self.port.unwrap_or(22)
}
pub fn ssh_user(&self) -> String {
self.user
.clone()
.unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "root".to_string()))
}
pub fn remote_work_dir(&self) -> PathBuf {
self.work_dir
.clone()
.unwrap_or_else(|| PathBuf::from("/tmp/freenet-test-network"))
}
pub fn connect(&self) -> Result<Session> {
let addr = format!("{}:{}", self.host, self.ssh_port());
let tcp = TcpStream::connect(&addr).map_err(|e| {
Error::PeerStartupFailed(format!("Failed to connect to {}: {}", addr, e))
})?;
let mut session = Session::new().map_err(|e| {
Error::PeerStartupFailed(format!("Failed to create SSH session: {}", e))
})?;
session.set_tcp_stream(tcp);
session
.handshake()
.map_err(|e| Error::PeerStartupFailed(format!("SSH handshake failed: {}", e)))?;
let username = self.ssh_user();
if let Some(identity) = &self.identity_file {
session
.userauth_pubkey_file(&username, None, identity, None)
.map_err(|e| {
Error::PeerStartupFailed(format!("SSH key authentication failed: {}", e))
})?;
} else {
session.userauth_agent(&username).map_err(|e| {
Error::PeerStartupFailed(format!("SSH agent authentication failed: {}", e))
})?;
}
if !session.authenticated() {
return Err(Error::PeerStartupFailed(
"SSH authentication failed".to_string(),
));
}
Ok(session)
}
pub fn exec(&self, command: &str) -> Result<String> {
let session = self.connect()?;
let mut channel = session
.channel_session()
.map_err(|e| Error::PeerStartupFailed(format!("Failed to open SSH channel: {}", e)))?;
channel
.exec(command)
.map_err(|e| Error::PeerStartupFailed(format!("Failed to execute command: {}", e)))?;
let mut output = String::new();
channel.read_to_string(&mut output).map_err(|e| {
Error::PeerStartupFailed(format!("Failed to read command output: {}", e))
})?;
channel.wait_close().ok();
let exit_status = channel
.exit_status()
.map_err(|e| Error::PeerStartupFailed(format!("Failed to get exit status: {}", e)))?;
if exit_status != 0 {
return Err(Error::PeerStartupFailed(format!(
"Command failed with exit code {}: {}",
exit_status, output
)));
}
Ok(output.trim().to_string())
}
pub fn scp_upload(&self, local_path: &std::path::Path, remote_path: &str) -> Result<()> {
let session = self.connect()?;
let local_file = std::fs::File::open(local_path)
.map_err(|e| Error::PeerStartupFailed(format!("Failed to open local file: {}", e)))?;
let metadata = local_file
.metadata()
.map_err(|e| Error::PeerStartupFailed(format!("Failed to get file metadata: {}", e)))?;
let mut remote_file = session
.scp_send(
std::path::Path::new(remote_path),
0o755, metadata.len(),
None,
)
.map_err(|e| {
Error::PeerStartupFailed(format!("Failed to initiate SCP upload: {}", e))
})?;
std::io::copy(
&mut std::fs::File::open(local_path).unwrap(),
&mut remote_file,
)
.map_err(|e| Error::PeerStartupFailed(format!("Failed to upload file: {}", e)))?;
remote_file.send_eof().ok();
remote_file.wait_eof().ok();
remote_file.close().ok();
remote_file.wait_close().ok();
Ok(())
}
pub fn scp_download(&self, remote_path: &str, local_path: &std::path::Path) -> Result<()> {
let session = self.connect()?;
let (mut remote_file, _stat) = session
.scp_recv(std::path::Path::new(remote_path))
.map_err(|e| {
Error::PeerStartupFailed(format!("Failed to initiate SCP download: {}", e))
})?;
let mut local_file = std::fs::File::create(local_path)
.map_err(|e| Error::PeerStartupFailed(format!("Failed to create local file: {}", e)))?;
std::io::copy(&mut remote_file, &mut local_file)
.map_err(|e| Error::PeerStartupFailed(format!("Failed to download file: {}", e)))?;
remote_file.send_eof().ok();
remote_file.wait_eof().ok();
remote_file.close().ok();
remote_file.wait_close().ok();
Ok(())
}
pub fn discover_public_address(&self) -> Result<String> {
if let Ok(addr) = self.exec("ip route get 8.8.8.8 | awk '{print $7; exit}'") {
if !addr.is_empty() && addr != "127.0.0.1" {
return Ok(addr);
}
}
if let Ok(output) = self.exec("hostname -I") {
for addr in output.split_whitespace() {
if addr.starts_with("192.168.")
|| addr.starts_with("10.")
|| addr.starts_with("172.")
{
return Ok(addr.to_string());
}
}
}
Ok(self.host.clone())
}
}
#[derive(Debug, Clone)]
pub enum PeerLocation {
Local,
Remote(RemoteMachine),
}
impl Default for PeerLocation {
fn default() -> Self {
PeerLocation::Local
}
}