use std::{borrow::Cow, fmt, num::NonZeroU16};
use derivative::Derivative;
use crate::tor::control_client::{
commands::TorCommand,
error::TorClientError,
parsers,
parsers::ParseError,
response::ResponseLine,
types::{KeyBlob, KeyType, PortMapping, PrivateKey},
};
#[derive(Debug, Copy, Clone)]
pub enum AddOnionFlag {
DiscardPK,
Detach,
BasicAuth,
NonAnonymous,
MaxStreamsCloseCircuit,
}
impl fmt::Display for AddOnionFlag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use AddOnionFlag::{BasicAuth, Detach, DiscardPK, MaxStreamsCloseCircuit, NonAnonymous};
match self {
DiscardPK => write!(f, "DiscardPK"),
Detach => write!(f, "Detach"),
BasicAuth => write!(f, "BasicAuth"),
NonAnonymous => write!(f, "NonAnonymous"),
MaxStreamsCloseCircuit => write!(f, "MaxStreamsCloseCircuit"),
}
}
}
pub struct AddOnion<'a> {
key_type: KeyType,
key_blob: KeyBlob<'a>,
flags: Vec<AddOnionFlag>,
port_mapping: PortMapping,
num_streams: Option<NonZeroU16>,
}
impl<'a> AddOnion<'a> {
pub fn new(
key_type: KeyType,
key_blob: KeyBlob<'a>,
flags: Vec<AddOnionFlag>,
port_mapping: PortMapping,
num_streams: Option<NonZeroU16>,
) -> Self {
Self {
key_type,
key_blob,
flags,
port_mapping,
num_streams,
}
}
}
impl TorCommand for AddOnion<'_> {
type Error = TorClientError;
type Output = AddOnionResponse;
fn to_command_string(&self) -> Result<String, Self::Error> {
let mut s = String::from("ADD_ONION ");
s.push_str(self.key_type.as_tor_repr());
s.push(':');
s.push_str(self.key_blob.as_tor_repr());
if !self.flags.is_empty() {
let flags = self.flags.iter().map(|f| f.to_string()).collect::<Vec<_>>().join(",");
s.push_str(&format!(" Flags={flags}"));
}
if let Some(num_streams) = self.num_streams {
s.push_str(&format!(" NumStreams={num_streams}"));
}
s.push_str(&format!(
" Port={},{}",
self.port_mapping.onion_port(),
self.port_mapping.proxied_address()
));
Ok(s)
}
fn parse_responses(&self, mut responses: Vec<ResponseLine>) -> Result<Self::Output, Self::Error> {
let last_response = responses.pop().ok_or(TorClientError::UnexpectedEof)?;
if let Some(err) = last_response.err() {
if err.contains("Onion address collision") {
return Err(TorClientError::OnionAddressCollision);
}
return Err(TorClientError::TorCommandFailed(err.to_owned()));
}
let mut service_id = None;
let mut private_key = None;
for response in responses {
let (key, values) = parsers::key_value(&response.value)?;
let value = values.into_iter().next().ok_or(TorClientError::KeyValueNoValue)?;
match &*key {
"ServiceID" => {
service_id = Some(value.into_owned());
},
"PrivateKey" => {
let mut split = value.split(':');
let key = split
.next()
.ok_or_else(|| ParseError("PrivateKey field was empty".to_string()))?;
let value = split
.next()
.map(|v| Cow::from(v.to_owned()))
.ok_or_else(|| ParseError("Failed to parse private key".to_string()))?;
private_key = match key {
"ED25519-V3" => Some(PrivateKey::Ed25519V3(value.into_owned())),
"RSA1024" => Some(PrivateKey::Rsa1024(value.into_owned())),
k => {
return Err(
ParseError(format!("Server returned unrecognised private key type '{k}'")).into(),
);
},
};
},
_ => {
},
}
}
let service_id = service_id.ok_or(TorClientError::AddOnionNoServiceId)?;
Ok(AddOnionResponse {
service_id,
private_key,
onion_port: self.port_mapping.onion_port(),
})
}
}
impl fmt::Display for AddOnion<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ADD_ONION (KeyType={} KeyBlob={} Flags={} PortMapping={})",
self.key_type.as_tor_repr(),
self.key_blob,
self.flags.iter().fold(String::new(), |acc, f| format!("{acc}, {f}")),
self.port_mapping
)
}
}
#[derive(Derivative, Clone)]
#[derivative(Debug)]
pub struct AddOnionResponse {
pub(crate) service_id: String,
#[derivative(Debug = "ignore")]
pub(crate) private_key: Option<PrivateKey>,
pub(crate) onion_port: u16,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn to_command_string() {
let key = "this-is-a-key".to_string();
let command = AddOnion::new(
KeyType::New,
KeyBlob::String(&key),
vec![],
PortMapping::from_port(9090),
None,
);
assert_eq!(
command.to_command_string().unwrap(),
format!("ADD_ONION NEW:{key} Port=9090,127.0.0.1:9090")
);
}
}