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