use std::net::{Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use arti_client::config::CfgPath;
use arti_client::{DataStream, TorClient, TorClientConfig};
use futures::Stream;
use safelog::DisplayRedacted;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tor_hsservice::{HsNickname, OnionServiceConfig, RunningOnionService, StreamRequest};
use tor_rtcompat::PreferredRuntime;
use lettre::transport::smtp::authentication::{Credentials, Mechanism};
use lettre::transport::smtp::client::{AsyncSmtpConnection, AsyncTokioStream, TlsParameters};
use lettre::transport::smtp::extension::ClientId;
use crate::config::{Config, Email};
use crate::error::{EmailError, HomeDirError};
pub type DmsTorClient = TorClient<PreferredRuntime>;
#[derive(thiserror::Error, Debug)]
pub enum TorError {
#[error(transparent)]
Arti(#[from] arti_client::Error),
#[error(transparent)]
Smtp(#[from] lettre::transport::smtp::Error),
#[error(transparent)]
Email(#[from] EmailError),
#[error(transparent)]
HomeDir(#[from] HomeDirError),
#[error("tor configuration error: {0}")]
Config(String),
#[error("failed to accept onion stream: {0}")]
Accept(String),
#[error("onion service is disabled or no Tor client is available")]
ServiceDisabled,
}
pub struct OnionEndpoint {
pub service: Arc<RunningOnionService>,
pub streams: Pin<Box<dyn Stream<Item = StreamRequest> + Send>>,
}
impl OnionEndpoint {
pub fn onion_address(&self) -> Option<String> {
onion_address_of(&self.service)
}
}
pub fn onion_address_of(service: &RunningOnionService) -> Option<String> {
service
.onion_address()
.map(|id| id.display_unredacted().to_string())
}
pub fn default_state_dir() -> Result<PathBuf, HomeDirError> {
crate::app::file_path("tor")
}
fn build_client_config(state_dir: &Path) -> Result<TorClientConfig, TorError> {
let mut builder = TorClientConfig::builder();
builder
.storage()
.state_dir(CfgPath::new_literal(state_dir.to_path_buf()))
.cache_dir(CfgPath::new_literal(state_dir.join("cache")));
builder.build().map_err(|e| TorError::Config(e.to_string()))
}
pub async fn bootstrap_tor_client(
state_dir: Option<PathBuf>,
) -> Result<Arc<DmsTorClient>, TorError> {
let dir = match state_dir {
Some(dir) => dir,
None => default_state_dir()?,
};
let config = build_client_config(&dir)?;
let client = TorClient::create_bootstrapped(config).await?;
Ok(client)
}
pub fn launch_onion_service(
client: &DmsTorClient,
nickname: &str,
) -> Result<OnionEndpoint, TorError> {
let nickname: HsNickname = nickname
.parse()
.map_err(|e| TorError::Config(format!("invalid onion service nickname: {e}")))?;
let svc_config = OnionServiceConfig::builder()
.nickname(nickname)
.build()
.map_err(|e| TorError::Config(e.to_string()))?;
let (service, rend_requests) = client
.launch_onion_service(svc_config)?
.ok_or(TorError::ServiceDisabled)?;
let streams = Box::pin(tor_hsservice::handle_rend_requests(rend_requests));
Ok(OnionEndpoint { service, streams })
}
pub async fn accept_stream(request: StreamRequest) -> Result<DataStream, TorError> {
use tor_cell::relaycell::msg::Connected;
request
.accept(Connected::new_empty())
.await
.map_err(|e| TorError::Accept(e.to_string()))
}
#[derive(Debug)]
pub struct TorSmtpStream(pub DataStream);
impl AsyncRead for TorSmtpStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_read(cx, buf)
}
}
impl AsyncWrite for TorSmtpStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Pin::new(&mut self.0).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_shutdown(cx)
}
}
impl AsyncTokioStream for TorSmtpStream {
fn peer_addr(&self) -> std::io::Result<SocketAddr> {
Ok(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0))
}
}
pub async fn send_email_tor(
config: &Config,
client: &DmsTorClient,
email_type: Email,
) -> Result<(), TorError> {
let message = config.create_email(email_type)?;
let envelope = message.envelope().clone();
let body = message.formatted();
let data_stream = client
.connect((config.smtp_server.as_str(), config.smtp_port))
.await?;
let transport: Box<dyn AsyncTokioStream> = Box::new(TorSmtpStream(data_stream));
let hello = ClientId::default();
let mut conn = AsyncSmtpConnection::connect_with_transport(transport, &hello).await?;
let tls = TlsParameters::new_rustls(config.smtp_server.clone())?;
conn.starttls(tls, &hello).await?;
let creds = Credentials::new(config.username.clone(), config.password.clone());
conn.auth(&[Mechanism::Plain, Mechanism::Login], &creds)
.await?;
conn.send(&envelope, &body).await?;
let _ = conn.quit().await;
Ok(())
}