use serde::Deserialize;
use serde_with::DeserializeFromStr;
use std::{
fmt::Debug,
net::{self, IpAddr},
path::PathBuf,
str::FromStr,
};
use tor_config_path::{
CfgPath, CfgPathError, CfgPathResolver,
addr::{CfgAddr, CfgAddrError},
};
use tor_general_addr::general::{self, AddrParseError};
#[cfg(feature = "rpc-server")]
use tor_rtcompat::{NetStreamListener, NetStreamProvider};
use crate::HasClientErrorAction;
#[derive(Clone, Debug)]
pub struct ParsedConnectPoint(ConnectPointEnum<Unresolved>);
#[derive(Clone, Debug)]
pub struct ResolvedConnectPoint(pub(crate) ConnectPointEnum<Resolved>);
impl ParsedConnectPoint {
pub fn resolve(
&self,
resolver: &CfgPathResolver,
) -> Result<ResolvedConnectPoint, ResolveError> {
use ConnectPointEnum as CPE;
Ok(ResolvedConnectPoint(match &self.0 {
CPE::Connect(connect) => CPE::Connect(connect.resolve(resolver)?),
CPE::Builtin(builtin) => CPE::Builtin(builtin.clone()),
}))
}
pub fn superuser_permission(&self) -> crate::SuperuserPermission {
self.0.superuser_permission()
}
pub fn is_explicit_abort(&self) -> bool {
self.0.is_explicit_abort()
}
}
impl ResolvedConnectPoint {
pub fn superuser_permission(&self) -> crate::SuperuserPermission {
self.0.superuser_permission()
}
pub fn is_explicit_abort(&self) -> bool {
self.0.is_explicit_abort()
}
}
impl FromStr for ParsedConnectPoint {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let de: ConnectPointDe = toml::from_str(s).map_err(ParseError::InvalidConnectPoint)?;
Ok(ParsedConnectPoint(de.try_into()?))
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ParseError {
#[error("Invalid connect point")]
InvalidConnectPoint(#[source] toml::de::Error),
#[error("Conflicting members in connect point")]
ConflictingMembers,
#[error("Unrecognized format on connect point")]
UnrecognizedFormat,
#[error("inet-auto address was not a loopback address")]
AutoAddressNotLoopback,
}
impl HasClientErrorAction for ParseError {
fn client_action(&self) -> crate::ClientErrorAction {
use crate::ClientErrorAction as A;
match self {
ParseError::InvalidConnectPoint(_) => A::Abort,
ParseError::ConflictingMembers => A::Abort,
ParseError::AutoAddressNotLoopback => A::Decline,
ParseError::UnrecognizedFormat => A::Decline,
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ResolveError {
#[error("Unable to resolve variables in path")]
InvalidPath(#[from] CfgPathError),
#[error("Unable to parse address")]
UnparseableAddr(#[from] AddrParseError),
#[error("Unable to resolve variables in address")]
InvalidAddr(#[from] CfgAddrError),
#[error("Cannot represent expanded path as string")]
PathNotString,
#[error("Tried to bind or connect to a non-loopback TCP address")]
AddressNotLoopback,
#[error("Authorization type not compatible with address family")]
AuthNotCompatible,
#[error("Authorization type not recognized as a supported type")]
AuthNotRecognized,
#[error("Address type not recognized")]
AddressTypeNotRecognized,
#[error("inet-auto without socket_address_file, or vice versa")]
AutoIncompatibleWithSocketFile,
#[error("Path was not absolute")]
PathNotAbsolute,
}
impl HasClientErrorAction for ResolveError {
fn client_action(&self) -> crate::ClientErrorAction {
use crate::ClientErrorAction as A;
match self {
ResolveError::InvalidPath(e) => e.client_action(),
ResolveError::UnparseableAddr(e) => e.client_action(),
ResolveError::InvalidAddr(e) => e.client_action(),
ResolveError::PathNotString => A::Decline,
ResolveError::AddressNotLoopback => A::Decline,
ResolveError::AuthNotCompatible => A::Abort,
ResolveError::AuthNotRecognized => A::Decline,
ResolveError::AddressTypeNotRecognized => A::Decline,
ResolveError::PathNotAbsolute => A::Abort,
ResolveError::AutoIncompatibleWithSocketFile => A::Abort,
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum ConnectPointEnum<R: Addresses> {
Connect(Connect<R>),
Builtin(Builtin),
}
pub(crate) trait Addresses {
type SocketAddr: Clone + std::fmt::Debug;
type Path: Clone + std::fmt::Debug;
}
#[derive(Deserialize, Clone, Debug)]
struct ConnectPointDe {
connect: Option<Connect<Unresolved>>,
builtin: Option<Builtin>,
}
impl TryFrom<ConnectPointDe> for ConnectPointEnum<Unresolved> {
type Error = ParseError;
fn try_from(value: ConnectPointDe) -> Result<Self, Self::Error> {
match value {
ConnectPointDe {
connect: Some(c),
builtin: None,
} => Ok(ConnectPointEnum::Connect(c)),
ConnectPointDe {
connect: None,
builtin: Some(b),
} => Ok(ConnectPointEnum::Builtin(b)),
ConnectPointDe {
connect: Some(_),
builtin: Some(_),
} => Err(ParseError::ConflictingMembers),
_ => Err(ParseError::UnrecognizedFormat),
}
}
}
impl<R: Addresses> ConnectPointEnum<R> {
fn superuser_permission(&self) -> crate::SuperuserPermission {
use crate::SuperuserPermission::*;
match self {
ConnectPointEnum::Connect(connect) => {
if connect.superuser {
Allowed
} else {
NotAllowed
}
}
ConnectPointEnum::Builtin(_) => NotAllowed,
}
}
fn is_explicit_abort(&self) -> bool {
matches!(
self,
ConnectPointEnum::Builtin(Builtin {
builtin: BuiltinVariant::Abort
})
)
}
}
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct Builtin {
pub(crate) builtin: BuiltinVariant,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub(crate) enum BuiltinVariant {
Abort,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(bound = "R::Path : Deserialize<'de>, AddrWithStr<R::SocketAddr> : Deserialize<'de>")]
pub(crate) struct Connect<R: Addresses> {
pub(crate) socket: ConnectAddress<R>,
pub(crate) socket_canonical: Option<AddrWithStr<R::SocketAddr>>,
pub(crate) auth: Auth<R>,
pub(crate) socket_address_file: Option<R::Path>,
#[serde(default)]
pub(crate) superuser: bool,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(bound = "R::Path : Deserialize<'de>, AddrWithStr<R::SocketAddr> : Deserialize<'de>")]
#[serde(untagged, expecting = "a network schema and address")]
pub(crate) enum ConnectAddress<R: Addresses> {
InetAuto(InetAutoAddress),
Socket(AddrWithStr<R::SocketAddr>),
}
#[derive(Clone, Debug, DeserializeFromStr)]
pub(crate) struct InetAutoAddress {
bind: Option<IpAddr>,
}
impl std::fmt::Display for InetAutoAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.bind {
Some(a) => write!(f, "inet-auto:{a}"),
None => write!(f, "inet-auto:auto"),
}
}
}
impl FromStr for InetAutoAddress {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some(addr_part) = s.strip_prefix("inet-auto:") else {
return Err(ParseError::UnrecognizedFormat);
};
if addr_part == "auto" {
return Ok(InetAutoAddress { bind: None });
}
let Ok(addr) = IpAddr::from_str(addr_part) else {
return Err(ParseError::UnrecognizedFormat);
};
if addr.is_loopback() {
Ok(InetAutoAddress { bind: Some(addr) })
} else {
Err(ParseError::AutoAddressNotLoopback)
}
}
}
impl InetAutoAddress {
fn bind_to_addresses(&self) -> Vec<general::SocketAddr> {
match self {
InetAutoAddress { bind: None } => vec![
net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 0).into(),
net::SocketAddr::new(net::Ipv6Addr::LOCALHOST.into(), 0).into(),
],
InetAutoAddress { bind: Some(ip) } => {
vec![net::SocketAddr::new(*ip, 0).into()]
}
}
}
#[cfg(feature = "rpc-client")]
pub(crate) fn validate_parsed_address(
&self,
addr: &general::SocketAddr,
) -> Result<(), crate::ConnectError> {
use general::SocketAddr::Inet;
for sa in self.bind_to_addresses() {
if let (Inet(specified), Inet(got)) = (sa, addr) {
if specified.port() == 0 && specified.ip() == got.ip() {
return Ok(());
}
}
}
Err(crate::ConnectError::SocketAddressFileMismatch)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "rpc-client", derive(Deserialize))]
#[cfg_attr(feature = "rpc-server", derive(serde::Serialize))]
pub(crate) struct AddressFile {
pub(crate) address: String,
}
impl<R: Addresses> ConnectAddress<R> {
fn is_auto(&self) -> bool {
matches!(self, ConnectAddress::InetAuto { .. })
}
}
impl ConnectAddress<Unresolved> {
fn resolve(
&self,
resolver: &CfgPathResolver,
) -> Result<ConnectAddress<Resolved>, ResolveError> {
use ConnectAddress::*;
match self {
InetAuto(a) => Ok(InetAuto(a.clone())),
Socket(s) => Ok(Socket(s.resolve(resolver)?)),
}
}
}
impl ConnectAddress<Resolved> {
fn bind_to_addresses(&self) -> Vec<general::SocketAddr> {
use ConnectAddress::*;
match self {
InetAuto(a) => a.bind_to_addresses(),
Socket(a) => vec![a.as_ref().clone()],
}
}
#[cfg(feature = "rpc-server")]
pub(crate) async fn bind<R>(
&self,
runtime: &R,
) -> Result<(R::Listener, String), crate::ConnectError>
where
R: NetStreamProvider<general::SocketAddr>,
{
use crate::ConnectError;
match self {
ConnectAddress::InetAuto(auto) => {
let bind_one =
async |addr: &general::SocketAddr| -> Result<(R::Listener, String), crate::ConnectError> {
let listener = runtime.listen(addr).await?;
let local_addr = listener.local_addr()?.try_to_string().ok_or_else(|| ConnectError::Internal("Can't represent auto socket as string!".into()))?;
Ok((listener,local_addr))
};
let mut first_error = None;
for addr in auto.bind_to_addresses() {
match bind_one(&addr).await {
Ok(result) => {
return Ok(result);
}
Err(e) => {
if first_error.is_none() {
first_error = Some(e);
}
}
}
}
Err(first_error.unwrap_or_else(|| {
ConnectError::Internal("No auto addresses to bind!?".into())
}))
}
ConnectAddress::Socket(addr) => {
let listener = runtime.listen(addr.as_ref()).await?;
Ok((listener, addr.as_str().to_owned()))
}
}
}
}
impl Connect<Unresolved> {
fn resolve(&self, resolver: &CfgPathResolver) -> Result<Connect<Resolved>, ResolveError> {
let socket = self.socket.resolve(resolver)?;
let socket_canonical = self
.socket_canonical
.as_ref()
.map(|sc| sc.resolve(resolver))
.transpose()?;
let auth = self.auth.resolve(resolver)?;
let socket_address_file = self
.socket_address_file
.as_ref()
.map(|p| p.path(resolver))
.transpose()?;
Connect {
socket,
socket_canonical,
auth,
socket_address_file,
superuser: self.superuser,
}
.validate()
}
}
impl Connect<Resolved> {
fn validate(self) -> Result<Self, ResolveError> {
use general::SocketAddr::{Inet, Unix};
for bind_addr in self.socket.bind_to_addresses() {
match (bind_addr, &self.auth) {
(Inet(addr), _) if !addr.ip().is_loopback() => {
return Err(ResolveError::AddressNotLoopback);
}
(Inet(_), Auth::None) => return Err(ResolveError::AuthNotCompatible),
(_, Auth::Unrecognized(_)) => return Err(ResolveError::AuthNotRecognized),
(Inet(_), Auth::Cookie { .. }) => {}
(Unix(_), _) => {}
(_, _) => return Err(ResolveError::AddressTypeNotRecognized),
};
}
if self.socket.is_auto() != self.socket_address_file.is_some() {
return Err(ResolveError::AutoIncompatibleWithSocketFile);
}
self.check_absolute_paths()?;
Ok(self)
}
fn check_absolute_paths(&self) -> Result<(), ResolveError> {
for bind_addr in self.socket.bind_to_addresses() {
sockaddr_check_absolute(&bind_addr)?;
}
if let Some(sa) = &self.socket_canonical {
sockaddr_check_absolute(sa.as_ref())?;
}
self.auth.check_absolute_paths()?;
if self
.socket_address_file
.as_ref()
.is_some_and(|p| !p.is_absolute())
{
return Err(ResolveError::PathNotAbsolute);
}
Ok(())
}
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Auth<R: Addresses> {
None,
Cookie {
path: R::Path,
},
#[serde(untagged)]
Unrecognized(toml::Value),
}
impl Auth<Unresolved> {
fn resolve(&self, resolver: &CfgPathResolver) -> Result<Auth<Resolved>, ResolveError> {
match self {
Auth::None => Ok(Auth::None),
Auth::Cookie { path } => Ok(Auth::Cookie {
path: path.path(resolver)?,
}),
Auth::Unrecognized(x) => Ok(Auth::Unrecognized(x.clone())),
}
}
}
impl Auth<Resolved> {
fn check_absolute_paths(&self) -> Result<(), ResolveError> {
match self {
Auth::None => Ok(()),
Auth::Cookie { path } => {
if path.is_absolute() {
Ok(())
} else {
Err(ResolveError::PathNotAbsolute)
}
}
Auth::Unrecognized(_) => Ok(()),
}
}
}
#[derive(Clone, Debug)]
struct Unresolved;
impl Addresses for Unresolved {
type SocketAddr = String;
type Path = CfgPath;
}
#[derive(Clone, Debug)]
pub(crate) struct Resolved;
impl Addresses for Resolved {
type SocketAddr = general::SocketAddr;
type Path = PathBuf;
}
#[derive(
Clone, Debug, derive_more::AsRef, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
)]
pub(crate) struct AddrWithStr<A>
where
A: Clone + Debug,
{
string: String,
#[as_ref]
addr: A,
}
impl<A> AddrWithStr<A>
where
A: Clone + Debug,
{
pub(crate) fn as_str(&self) -> &str {
self.string.as_str()
}
pub(crate) fn set_string_from<B: Clone + Debug>(&mut self, other: &AddrWithStr<B>) {
self.string = other.string.clone();
}
}
impl AddrWithStr<String> {
pub(crate) fn resolve(
&self,
resolver: &CfgPathResolver,
) -> Result<AddrWithStr<general::SocketAddr>, ResolveError> {
let AddrWithStr { string, addr } = self;
let addr: CfgAddr = addr.parse()?;
let substituted = addr.substitutions_will_apply();
let addr = addr.address(resolver)?;
let string = if substituted {
addr.try_to_string().ok_or(ResolveError::PathNotString)?
} else {
string.clone()
};
Ok(AddrWithStr { string, addr })
}
}
impl<A> FromStr for AddrWithStr<A>
where
A: Clone + Debug + FromStr,
{
type Err = <A as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr = s.parse()?;
let string = s.to_owned();
Ok(Self { string, addr })
}
}
impl<A> std::fmt::Display for AddrWithStr<A>
where
A: Clone + Debug + std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.string)
}
}
fn sockaddr_check_absolute(s: &general::SocketAddr) -> Result<(), ResolveError> {
match s {
general::SocketAddr::Inet(_) => Ok(()),
general::SocketAddr::Unix(sa) => match sa.as_pathname() {
Some(p) if !p.is_absolute() => Err(ResolveError::PathNotAbsolute),
_ => Ok(()),
},
_ => Err(ResolveError::AddressTypeNotRecognized),
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use assert_matches::assert_matches;
fn parse(s: &str) -> ParsedConnectPoint {
s.parse().unwrap()
}
#[test]
fn examples() {
let _e1 = parse(
r#"
[builtin]
builtin = "abort"
"#,
);
let _e2 = parse(
r#"
[connect]
socket = "unix:/var/run/arti/rpc_socket"
auth = "none"
"#,
);
let _e3 = parse(
r#"
[connect]
socket = "inet:[::1]:9191"
socket_canonical = "inet:[::1]:2020"
auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
"#,
);
let _e4 = parse(
r#"
[connect]
socket = "inet:[::1]:9191"
socket_canonical = "inet:[::1]:2020"
[connect.auth.cookie]
path = "/home/user/.arti_rpc/cookie"
"#,
);
}
#[test]
fn parse_errors() {
let r: Result<ParsedConnectPoint, _> = "not a toml string".parse();
assert_matches!(r, Err(ParseError::InvalidConnectPoint(_)));
let r: Result<ParsedConnectPoint, _> = "[squidcakes]".parse();
assert_matches!(r, Err(ParseError::UnrecognizedFormat));
let r: Result<ParsedConnectPoint, _> = r#"
[builtin]
builtin = "abort"
[connect]
socket = "inet:[::1]:9191"
socket_canonical = "inet:[::1]:2020"
auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
"#
.parse();
assert_matches!(r, Err(ParseError::ConflictingMembers));
}
#[test]
fn resolve_errors() {
let resolver = CfgPathResolver::default();
let r: ParsedConnectPoint = r#"
[connect]
socket = "inet:[::1]:9191"
socket_canonical = "inet:[::1]:2020"
[connect.auth.esp]
telekinetic_handshake = 3
"#
.parse()
.unwrap();
let err = r.resolve(&resolver).err();
assert_matches!(err, Some(ResolveError::AuthNotRecognized));
let r: ParsedConnectPoint = r#"
[connect]
socket = "inet:[::1]:9191"
socket_canonical = "inet:[::1]:2020"
auth = "foo"
"#
.parse()
.unwrap();
let err = r.resolve(&resolver).err();
assert_matches!(err, Some(ResolveError::AuthNotRecognized));
}
}