cuprate_wire/network_address/
onion_addr.rs
1use std::{
7 fmt::Display,
8 str::{self, FromStr},
9};
10
11use thiserror::Error;
12
13use super::{NetworkAddress, NetworkAddressIncorrectZone};
14
15#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
17pub struct OnionAddr {
18 domain: [u8; 56],
21 pub port: u16,
23}
24
25#[derive(Debug, Error)]
27pub enum OnionAddrParsingError {
28 #[error("Address is either too long or short, length: {0}")]
29 InvalidLength(usize),
30 #[error("Address contain non-utf8 code point at tld byte location: {0:x}")]
31 NonUtf8Char(u8),
32 #[error("This is not an onion address, Tld: {0}")]
33 InvalidTld(String),
34 #[error("Domain contains non base32 characters")]
35 NonBase32Char,
36 #[error("Invalid version. Found: {0}")]
37 InvalidVersion(u8),
38 #[error("The checksum is invalid.")]
39 InvalidChecksum,
40 #[error("Invalid port specified")]
41 InvalidPort,
42}
43
44impl OnionAddr {
45 pub fn new(addr: &str, port: u16) -> Result<Self, OnionAddrParsingError> {
49 Self::check_addr(addr).map(|d| Self { domain: d, port })
50 }
51
52 pub fn check_addr(addr: &str) -> Result<[u8; 56], OnionAddrParsingError> {
56 if addr.len() != 62 {
58 return Err(OnionAddrParsingError::InvalidLength(addr.len()));
59 }
60
61 let Some((domain, tld)) = addr.split_at_checked(56) else {
62 return Err(OnionAddrParsingError::NonUtf8Char(addr.as_bytes()[56]));
63 };
64
65 if tld != ".onion" {
67 return Err(OnionAddrParsingError::InvalidTld(String::from(tld)));
68 }
69
70 if !domain
72 .as_bytes()
73 .iter()
74 .copied()
75 .all(|c| c.is_ascii_lowercase() || (b'2'..=b'7').contains(&c))
76 {
77 return Err(OnionAddrParsingError::NonBase32Char);
78 }
79
80 Ok(addr.as_bytes()[..56]
81 .try_into()
82 .unwrap_or_else(|e| panic!("We just validated address: {addr} : {e}")))
83 }
84
85 pub fn addr_string(&self) -> String {
89 let mut domain = str::from_utf8(&self.domain)
90 .expect("Onion addresses are always containing UTF-8 characters.")
91 .to_string();
92
93 domain.push_str(".onion");
94 domain
95 }
96
97 #[inline]
98 pub const fn port(&self) -> u16 {
99 self.port
100 }
101
102 #[inline]
103 pub const fn domain(&self) -> [u8; 56] {
104 self.domain
105 }
106}
107
108impl Display for OnionAddr {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 let domain = str::from_utf8(&self.domain)
112 .expect("Onion addresses are always containing UTF-8 characters.");
113
114 f.write_str(domain)?;
115 f.write_str(".onion:")?;
116 self.port.fmt(f)
117 }
118}
119
120impl FromStr for OnionAddr {
122 type Err = OnionAddrParsingError;
123
124 fn from_str(addr: &str) -> Result<Self, Self::Err> {
125 let (addr, port) = addr
126 .split_at_checked(62)
127 .ok_or(OnionAddrParsingError::InvalidLength(addr.len()))?;
128
129 let port: u16 = port
131 .starts_with(':')
132 .then(|| port[1..].parse().ok())
133 .flatten()
134 .ok_or(OnionAddrParsingError::InvalidPort)?;
135
136 let domain = Self::check_addr(addr)?;
138
139 Ok(Self { domain, port })
140 }
141}
142
143impl TryFrom<NetworkAddress> for OnionAddr {
144 type Error = NetworkAddressIncorrectZone;
145 fn try_from(value: NetworkAddress) -> Result<Self, Self::Error> {
146 match value {
147 NetworkAddress::Tor(addr) => Ok(addr),
148 NetworkAddress::Clear(_) => Err(NetworkAddressIncorrectZone),
149 }
150 }
151}
152
153impl From<OnionAddr> for NetworkAddress {
154 fn from(value: OnionAddr) -> Self {
155 Self::Tor(value)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use proptest::{collection::vec, prelude::*};
162
163 use super::OnionAddr;
164
165 const VALID_ONION_ADDRESSES: &[&str] = &[
166 "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", "pzhdfe7jraknpj2qgu5cz2u3i4deuyfwmonvzu5i3nyw4t4bmg7o5pad.onion", "monerotoruzizulg5ttgat2emf4d6fbmiea25detrmmy7erypseyteyd.onion", "sfprivg7qec6tdle7u6hdepzjibin6fn3ivm6qlwytr235rh5vc6bfqd.onion", "yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion", "p2pool2giz2r5cpqicajwoazjcxkfujxswtk3jolfk2ubilhrkqam2id.onion", "d6ac5qatnyodxisdehb3i4m7edfvtukxzhhtyadbgaxghcxee2xadpid.onion", "duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion", "featherdvtpi7ckdbkb2yxjfwx3oyvr3xjz3oo4rszylfzjdg6pbm3id.onion", "revuo75joezkbeitqmas4ab6spbrkr4vzbhjmeuv75ovrfqfp47mtjid.onion", "xoe4vn5uwdztif6goazfbmogh6wh5jc4up35bqdflu6bkdc5cas5vjqd.onion", "allyouhavetodecideiswhattodowiththetimethatisgiventoyouu.onion", "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion",
180 "qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion",
181 "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion",
182 "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion",
183 "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion",
184 "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion",
185 ];
186
187 #[test]
188 fn valid_onion_address() {
189 for addr in VALID_ONION_ADDRESSES {
190 assert!(
191 OnionAddr::check_addr(addr).is_ok(),
192 "Address {addr} has been reported as invalid."
193 );
194 }
195 }
196
197 proptest! {
198 #[test]
199 fn parse_valid_onion_address_w_port(ports in vec(any::<u16>(), 18)) {
200 for (addr,port) in VALID_ONION_ADDRESSES.iter().zip(ports) {
201
202 let mut s = (*addr).to_string();
203 s.push(':');
204 s.push_str(&port.to_string());
205
206 assert!(
207 s.parse::<OnionAddr>().is_ok(),
208 "Address {addr} has been reported as invalid."
209 );
210 }
211 }
212
213 #[test]
214 fn invalid_onion_address(addresses in vec("[a-z][2-7]{56}.onion", 250)) {
215 for addr in addresses {
216 assert!(
217 OnionAddr::check_addr(&addr).is_err(),
218 "Address {addr} has been reported as valid."
219 );
220 }
221 }
222
223 #[test]
224 fn parse_invalid_onion_address_w_port(addresses in vec("[a-z][2-7]{56}.onion:[0-9]{1,5}", 250)) {
225 for addr in addresses {
226 assert!(
227 addr.parse::<OnionAddr>().is_err(),
228 "Address {addr} has been reported as valid."
229 );
230 }
231 }
232 }
233}