use std::path::PathBuf;
use std::process::Stdio;
use anyhow::{Context, Error, Result};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use super::{Downloader, DOWNLOAD_DIRECTORY_TOR};
use crate::{DownloadOptions, VersionSelection};
const TOR_BOOTSTRAPED_LOG: &str = "Bootstrapped 100% (done): Done";
pub struct Tor {
pid: Option<u32>,
path: PathBuf,
version: String,
}
impl Tor {
pub async fn setup_with_version(version_selection: VersionSelection) -> Result<Tor> {
let downloader = Downloader::new_with_options(
DownloadOptions::default().with_version_selection(version_selection),
)
.await?;
downloader.download().await?;
Ok(Tor {
pid: None,
path: downloader.download_path().to_owned(),
version: downloader.version().to_owned(),
})
}
pub async fn setup() -> Result<Tor> {
let tor = Self::setup_with_version(VersionSelection::default()).await?;
tor.post_setup()
.with_context(|| Error::msg("Failed to perform Post-Setup Procedure."))?;
Ok(tor)
}
#[inline]
pub fn pid(&self) -> Option<u32> {
self.pid
}
#[inline]
pub fn version(&self) -> &String {
&self.version
}
pub async fn run(&mut self) -> Result<u32> {
let bin_path = self.tor_bin_dir_path();
let tor_bin = bin_path.join("tor");
let mut child = Command::new(tor_bin)
.stdout(Stdio::piped())
.spawn()
.context("Failed to spawn Tor Process")?;
let pid = child.id().ok_or(Error::msg("No Process ID for Tor"))?;
self.pid = Some(pid);
let stdout = child.stdout.take().context("Failed to retrieve Stdout")?;
let mut reader = BufReader::new(stdout).lines();
tokio::spawn(async move {
child.wait().await.expect("Tor Process errored.");
});
while let Some(line) = reader.next_line().await? {
if line.contains(TOR_BOOTSTRAPED_LOG) {
break;
}
}
Ok(pid)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn kill(&self) -> Result<()> {
use nix::sys::signal::{kill, SIGKILL};
use nix::unistd::Pid;
if let Some(pid) = &self.pid {
let pid = Pid::from_raw(*pid as i32);
kill(pid, Some(SIGKILL))?;
return Ok(());
}
anyhow::bail!("No process for Tor avaialable.")
}
#[cfg(target_os = "windows")]
pub fn kill(&self) -> Result<()> {
eprintln!("Not implemented!");
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn post_setup(&self) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
fn post_setup(&self) -> Result<()> {
use std::env::{set_var, var};
const LD_LIBRARY_PATH: &str = "LD_LIBRARY_PATH";
let bin_dir_path = self.tor_bin_dir_path().display().to_string();
let ld_library_path = var(LD_LIBRARY_PATH).unwrap_or_default();
if !ld_library_path.contains(&bin_dir_path) {
set_var(
LD_LIBRARY_PATH,
format!("{}:{}", ld_library_path, self.tor_bin_dir_path().display()),
);
}
Ok(())
}
fn tor_bin_dir_path(&self) -> PathBuf {
let dl_path = self.path.clone();
dl_path.join(DOWNLOAD_DIRECTORY_TOR)
}
}
impl Drop for Tor {
fn drop(&mut self) {
let _ = self.kill();
}
}
#[cfg(test)]
mod tests {
use crate::{Tor, DEFAULT_VERSION};
#[tokio::test]
async fn setup_tor_instance() {
let tor = Tor::setup().await.expect("Failed to setup a Tor instance.");
let version = tor.version();
assert_eq!(version, DEFAULT_VERSION);
}
#[tokio::test]
async fn pid_returned_by_run_matches() {
let mut tor = Tor::setup().await.expect("Failed to setup a Tor instance.");
let tor_pid = tor.run().await.expect("Failed to run Tor Proxy");
let instance_pid = tor.pid().unwrap();
assert_eq!(tor_pid, instance_pid);
}
}