use lazy_static::lazy_static;
use repl_rs::{Command, Parameter, Result, Value};
use repl_rs::{Convert, Repl};
use std::collections::HashMap;
use std::str::FromStr;
use tokio::runtime::Runtime;
use tor_client_lib::{
auth::TorAuthentication,
control_connection::{OnionServiceMapping, TorControlConnection, TorSocketAddr},
};
lazy_static! {
static ref RUNTIME: Runtime = Runtime::new().unwrap();
}
#[derive(Default)]
struct Context {
connection: Option<TorControlConnection>,
}
fn connect(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let host_port: String = args.get("host_port").unwrap().convert()?;
match RUNTIME.block_on(TorControlConnection::connect(host_port.clone())) {
Ok(connection) => {
context.connection = Some(connection);
Ok(Some(format!("Connected to {}", host_port)))
}
Err(error) => Ok(Some(format!(
"Error connecting to {}: {}",
host_port, error
))),
}
}
fn protocol_info(_args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let connection: &mut TorControlConnection = match &mut context.connection {
Some(connection) => connection,
None => {
return Ok(Some(
"Error: you must connect first with the 'connect' command".to_string(),
))
}
};
match RUNTIME.block_on(connection.get_protocol_info()) {
Ok(protocol_info) => {
let auth_methods = protocol_info.auth_methods.join(", ");
let cookie_file_string = match protocol_info.cookie_file {
Some(cookie_file) => {
format!("Cookie file location: {}\n", cookie_file)
}
None => String::new(),
};
let tor_version = protocol_info.tor_version;
Ok(Some(format!(
"Allowed authentication methods: {}\n{}TOR version: {}",
auth_methods, cookie_file_string, tor_version,
)))
}
Err(error) => Ok(Some(format!("Error getting protocol info: {}", error))),
}
}
fn get_info(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let connection: &mut TorControlConnection = match &mut context.connection {
Some(connection) => connection,
None => {
return Ok(Some(
"Error: you must connect first with the 'connect' command".to_string(),
))
}
};
let info_type: String = args.get("info_type").unwrap().convert()?;
match RUNTIME.block_on(connection.get_info(&info_type)) {
Ok(tor_info) => Ok(Some(format!("{:?}", tor_info))),
Err(error) => Ok(Some(format!("Error getting tor info: {}", error))),
}
}
fn authenticate(args: HashMap<String, Value>, context: &mut Context) -> Result<Option<String>> {
let connection: &mut TorControlConnection = match &mut context.connection {
Some(connection) => connection,
None => {
return Ok(Some(
"Error: you must connect first with the 'connect' command".to_string(),
))
}
};
let auth_type: String = args.get("auth_type").unwrap().convert()?;
match auth_type.as_str() {
"null" => match RUNTIME.block_on(connection.authenticate(&TorAuthentication::Null)) {
Ok(()) => Ok(Some("Authenticated".to_string())),
Err(error) => Ok(Some(format!("Authentication error: {}", error))),
},
"password" => {
let password = rpassword::prompt_password("Tor password: ").unwrap();
match RUNTIME
.block_on(connection.authenticate(&TorAuthentication::HashedPassword(password)))
{
Ok(()) => Ok(Some("Authenticated".to_string())),
Err(error) => Ok(Some(format!("Authentication error: {}", error))),
}
}
"cookie" => {
match RUNTIME.block_on(connection.authenticate(&TorAuthentication::SafeCookie(None))) {
Ok(()) => Ok(Some("Authenticated".to_string())),
Err(error) => Ok(Some(format!("Authentication error: {}", error))),
}
}
_ => Ok(Some(format!("Unknown auth type '{}'", auth_type))),
}
}
fn add_onion_service(
args: HashMap<String, Value>,
context: &mut Context,
) -> Result<Option<String>> {
let connection: &mut TorControlConnection = match &mut context.connection {
Some(connection) => connection,
None => {
return Ok(Some(
"Error: you must connect first with the 'connect' command".to_string(),
))
}
};
let virt_port = args.get("virt_port").unwrap().convert()?;
let listen_address = args.get("listen_address").unwrap().to_string();
let transient = match args.get("transient") {
Some(value) => match value.to_string().parse::<bool>() {
Ok(transient) => transient,
Err(error) => return Ok(Some(format!("Error parsing transient value: {}", error))),
},
None => true,
};
match RUNTIME.block_on(connection.create_onion_service(
&[OnionServiceMapping::new(
virt_port,
Some(TorSocketAddr::from_str(&listen_address).unwrap()),
)],
transient,
None,
)) {
Ok(service) => {
println!(
"public key: {}",
hex::encode(service.signing_key().verifying_key().as_bytes())
);
Ok(Some(format!(
"Onion service with service ID '{}' created",
service.service_id().as_str()
)))
}
Err(error) => Ok(Some(format!("Error creating onion service: {}", error))),
}
}
fn delete_onion_service(
args: HashMap<String, Value>,
context: &mut Context,
) -> Result<Option<String>> {
let connection: &mut TorControlConnection = match &mut context.connection {
Some(connection) => connection,
None => {
return Ok(Some(
"Error: you must connect first with the 'connect' command".to_string(),
))
}
};
let service_id = args.get("service_id").unwrap().to_string();
match RUNTIME.block_on(connection.delete_onion_service(&service_id)) {
Ok(_) => Ok(Some(format!(
"Onion service with service ID '{}' deleted",
service_id
))),
Err(error) => Ok(Some(format!("Error deleting onion service: {}", error))),
}
}
pub fn main() -> Result<()> {
env_logger::init();
let mut repl = Repl::new(Context::default())
.with_name("Tor CLI")
.with_version("v0.1.0")
.with_description("Run commands on a Tor server from the command line")
.add_command(
Command::new("connect", connect)
.with_parameter(Parameter::new("host_port").set_default("localhost:9051")?)?
.with_help("Connect to the Tor server at the given host and port"),
)
.add_command(
Command::new("authenticate", authenticate)
.with_parameter(Parameter::new("auth_type").set_required(true)?)?
.with_help("Authenticate to the Tor server using the specified auth method"),
)
.add_command(Command::new("protocol_info", protocol_info).with_help("Get protocol info"))
.add_command(
Command::new("get_info", get_info)
.with_parameter(Parameter::new("info_type").set_required(true)?)?
.with_help("Get tor info"),
)
.add_command(
Command::new("add_onion_service", add_onion_service)
.with_parameter(Parameter::new("virt_port").set_required(true)?)?
.with_parameter(Parameter::new("listen_address").set_required(true)?)?
.with_parameter(Parameter::new("transient").set_default("true")?)?
.with_help("Create an onion service"),
)
.add_command(
Command::new("delete_onion_service", delete_onion_service)
.with_parameter(Parameter::new("service_id").set_required(true)?)?
.with_help("Delete an onion service"),
);
repl.run()
}