use rings_core::dht::Did;
use crate::error::Error;
use crate::error::Result;
use crate::onion::OnionExitDescriptor;
use crate::onion::OnionExitService;
use crate::onion::OnionExitTransport;
pub use crate::onion::OnionProxyTarget;
use crate::onion::OnionRoute;
use crate::onion::OnionServiceName;
use crate::online::OnlineNodeType;
#[cfg(rings_native)]
pub mod http;
pub const ONION_PROXY_TCP_SERVICE: &str = "tcp";
pub const ONION_PROXY_HTTPS_SERVICE: &str = "https";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OnionProxyProtocol {
TcpConnect,
HttpsProxy,
}
impl OnionProxyProtocol {
pub const fn exit_service(self) -> &'static str {
match self {
Self::TcpConnect => ONION_PROXY_TCP_SERVICE,
Self::HttpsProxy => ONION_PROXY_HTTPS_SERVICE,
}
}
pub const fn exit_transport(self) -> OnionExitTransport {
match self {
Self::TcpConnect => OnionExitTransport::Tcp,
Self::HttpsProxy => OnionExitTransport::Tcp,
}
}
pub const fn label(self) -> &'static str {
match self {
Self::TcpConnect => "tcp-connect",
Self::HttpsProxy => "https-proxy",
}
}
fn default_exit_service_name(self) -> OnionServiceName {
match self {
Self::TcpConnect => OnionServiceName::tcp(),
Self::HttpsProxy => OnionServiceName::https(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OnionProxyConfig {
pub protocol: OnionProxyProtocol,
service: OnionServiceName,
pub hop_count: usize,
pub allow_short_paths: bool,
}
impl OnionProxyConfig {
pub fn new(protocol: OnionProxyProtocol, hop_count: usize, allow_short_paths: bool) -> Self {
Self {
protocol,
service: protocol.default_exit_service_name(),
hop_count,
allow_short_paths,
}
}
pub fn with_service(
protocol: OnionProxyProtocol,
service: OnionServiceName,
hop_count: usize,
allow_short_paths: bool,
) -> Result<Self> {
validate_proxy_service(protocol, &service)?;
Ok(Self {
protocol,
service,
hop_count,
allow_short_paths,
})
}
pub fn tcp_connect(hop_count: usize, allow_short_paths: bool) -> Self {
Self::new(OnionProxyProtocol::TcpConnect, hop_count, allow_short_paths)
}
pub fn tcp_connect_service(
service: OnionServiceName,
hop_count: usize,
allow_short_paths: bool,
) -> Result<Self> {
Self::with_service(
OnionProxyProtocol::TcpConnect,
service,
hop_count,
allow_short_paths,
)
}
pub fn https_proxy(hop_count: usize, allow_short_paths: bool) -> Self {
Self::new(OnionProxyProtocol::HttpsProxy, hop_count, allow_short_paths)
}
pub fn exit_service(&self) -> &str {
self.service.as_str()
}
pub fn exit_service_name(&self) -> &OnionServiceName {
&self.service
}
pub fn exit_transport(&self) -> OnionExitTransport {
self.protocol.exit_transport()
}
pub(crate) fn accepts_exit_descriptor(&self, descriptor: &OnionExitDescriptor) -> bool {
match self.protocol {
OnionProxyProtocol::TcpConnect => {
matches!(
descriptor.node_type,
OnlineNodeType::Native | OnlineNodeType::Ffi
) && descriptor
.service
.matches(self.service.as_str(), OnionExitTransport::Tcp)
}
OnionProxyProtocol::HttpsProxy => {
self.service == OnionServiceName::https()
&& descriptor
.offers_service_transport(self.service.as_str(), OnionExitTransport::Tcp)
}
}
}
}
fn validate_proxy_service(protocol: OnionProxyProtocol, service: &OnionServiceName) -> Result<()> {
if protocol == OnionProxyProtocol::HttpsProxy && service != &OnionServiceName::https() {
return Err(Error::InvalidConfig(format!(
"onion HTTPS proxy requires service {:?}",
OnionServiceName::https().as_str()
)));
}
if let Some(expected) = OnionExitService::reserved_transport(service.as_str()) {
if expected != protocol.exit_transport() {
return Err(Error::InvalidConfig(format!(
"onion proxy service {:?} requires {:?} transport, got {:?}",
service.as_str(),
expected,
protocol.exit_transport()
)));
}
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OnionProxyRoute {
pub protocol: OnionProxyProtocol,
pub target: OnionProxyTarget,
pub route: OnionRoute,
}
impl OnionProxyRoute {
pub fn exit_did(&self) -> Did {
self.route.exit_did()
}
pub fn exit_service(&self) -> &str {
self.route.service()
}
pub const fn exit_transport(&self) -> OnionExitTransport {
self.protocol.exit_transport()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::error::Result;
#[test]
fn test_proxy_protocol_maps_to_exit_service() {
assert_eq!(OnionProxyProtocol::TcpConnect.exit_service(), "tcp");
assert_eq!(OnionProxyProtocol::HttpsProxy.exit_service(), "https");
assert_eq!(
OnionProxyProtocol::TcpConnect.exit_transport(),
OnionExitTransport::Tcp
);
assert_eq!(
OnionProxyProtocol::HttpsProxy.exit_transport(),
OnionExitTransport::Tcp
);
}
#[test]
fn test_proxy_config_is_target_agnostic() {
let proxy = OnionProxyConfig::https_proxy(3, false);
assert_eq!(proxy.exit_service(), "https");
assert_eq!(proxy.exit_transport(), OnionExitTransport::Tcp);
assert_eq!(proxy.hop_count, 3);
assert!(!proxy.allow_short_paths);
}
#[test]
fn test_tcp_proxy_config_accepts_custom_tcp_service() -> Result<()> {
let service = OnionServiceName::parse("web")?;
let proxy = OnionProxyConfig::tcp_connect_service(service, 2, true)?;
assert_eq!(proxy.exit_service(), "web");
assert_eq!(proxy.exit_transport(), OnionExitTransport::Tcp);
assert_eq!(proxy.hop_count, 2);
assert!(proxy.allow_short_paths);
Ok(())
}
#[test]
fn test_tcp_proxy_config_accepts_https_tcp_service() -> Result<()> {
let proxy = OnionProxyConfig::tcp_connect_service(OnionServiceName::https(), 1, false)?;
assert_eq!(proxy.exit_service(), "https");
assert_eq!(proxy.exit_transport(), OnionExitTransport::Tcp);
Ok(())
}
#[test]
fn test_target_authority_parses_domain_targets() -> Result<()> {
let target = OnionProxyTarget::parse_authority("Example.COM.:443")?;
assert_eq!(target.host(), "example.com");
assert_eq!(target.port(), 443);
assert_eq!(target.authority(), "example.com:443");
Ok(())
}
#[test]
fn test_target_authority_parses_ipv6_targets() -> Result<()> {
let target = OnionProxyTarget::parse_authority("[2001:db8::1]:8443")?;
assert_eq!(target.host(), "2001:db8::1");
assert_eq!(target.port(), 8443);
assert_eq!(target.authority(), "[2001:db8::1]:8443");
Ok(())
}
#[test]
fn test_target_authority_rejects_missing_port() {
assert!(matches!(
OnionProxyTarget::parse_authority("example.com"),
Err(Error::OnionProxyTarget(
crate::onion::OnionProxyTargetError::MissingPort
))
));
}
}