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
74 .as_bytes()
75 .iter()
76 .copied()
77 .all(|c| c.is_ascii_alphabetic() || (b'2'..=b'7').contains(&c))
78 {
79 return Err(OnionAddrParsingError::NonBase32Char);
80 }
81
82 let mut domain: [u8; 56] = addr.as_bytes()[..56]
83 .try_into()
84 .unwrap_or_else(|e| panic!("We just validated address: {addr} : {e}"));
85 domain.make_ascii_lowercase();
86
87 Ok(domain)
88 }
89
90 pub fn addr_string(&self) -> String {
94 let mut domain = str::from_utf8(&self.domain)
95 .expect("Onion addresses are always containing UTF-8 characters.")
96 .to_string();
97
98 domain.push_str(".onion");
99 domain
100 }
101
102 #[inline]
103 pub const fn port(&self) -> u16 {
104 self.port
105 }
106
107 #[inline]
108 pub const fn domain(&self) -> [u8; 56] {
109 self.domain
110 }
111}
112
113impl Display for OnionAddr {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 let domain = str::from_utf8(&self.domain)
117 .expect("Onion addresses are always containing UTF-8 characters.");
118
119 f.write_str(domain)?;
120 f.write_str(".onion:")?;
121 self.port.fmt(f)
122 }
123}
124
125impl FromStr for OnionAddr {
127 type Err = OnionAddrParsingError;
128
129 fn from_str(addr: &str) -> Result<Self, Self::Err> {
130 let (addr, port) = addr
131 .split_at_checked(62)
132 .ok_or(OnionAddrParsingError::InvalidLength(addr.len()))?;
133
134 let port: u16 = port
136 .starts_with(':')
137 .then(|| port[1..].parse().ok())
138 .flatten()
139 .ok_or(OnionAddrParsingError::InvalidPort)?;
140
141 let domain = Self::check_addr(addr)?;
143
144 Ok(Self { domain, port })
145 }
146}
147
148impl TryFrom<NetworkAddress> for OnionAddr {
149 type Error = NetworkAddressIncorrectZone;
150 fn try_from(value: NetworkAddress) -> Result<Self, Self::Error> {
151 match value {
152 NetworkAddress::Tor(addr) => Ok(addr),
153 NetworkAddress::Clear(_) => Err(NetworkAddressIncorrectZone),
154 }
155 }
156}
157
158impl From<OnionAddr> for NetworkAddress {
159 fn from(value: OnionAddr) -> Self {
160 Self::Tor(value)
161 }
162}
163
164#[cfg(feature = "serde")]
165impl serde::Serialize for OnionAddr {
166 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
167 where
168 S: serde::Serializer,
169 {
170 serializer.collect_str(self)
171 }
172}
173
174#[cfg(feature = "serde")]
175impl<'de> serde::Deserialize<'de> for OnionAddr {
176 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177 where
178 D: serde::Deserializer<'de>,
179 {
180 let s = <String as serde::Deserialize>::deserialize(deserializer)?;
181 s.parse().map_err(serde::de::Error::custom)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use proptest::{collection::vec, prelude::*};
188
189 use super::OnionAddr;
190
191 const VALID_ONION_ADDRESSES: &[&str] = &[
192 "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",
206 "qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion",
207 "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion",
208 "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion",
209 "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion",
210 "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion",
211 ];
212
213 #[test]
214 fn valid_onion_address() {
215 for addr in VALID_ONION_ADDRESSES {
216 assert!(
217 OnionAddr::check_addr(addr).is_ok(),
218 "Address {addr} has been reported as invalid."
219 );
220 }
221 }
222
223 proptest! {
224 #[test]
225 fn parse_random_valid_onion_address(addresses in vec("[[a-z][2-7]]{56}\\.onion", 250)) {
226 for addr in addresses {
227 assert!(
228 OnionAddr::check_addr(&addr).is_ok(),
229 "Address {addr} has been reported as invalid."
230 );
231 }
232 }
233
234 #[test]
235 fn parse_random_valid_onion_address_w_port(
236 mut addresses in vec("[[a-z][2-7]]{56}\\.onion", 250),
237 ports in vec(any::<u16>(), 18)
238 ) {
239 for (addr,port) in addresses.iter_mut().zip(ports) {
240 addr.push(':');
241 addr.push_str(&port.to_string());
242
243 assert!(
244 addr.parse::<OnionAddr>().is_ok(),
245 "Address {addr} has been reported as invalid."
246 );
247 }
248 }
249
250 #[test]
251 fn parse_random_invalid_onion_address(addresses in vec(".{0,62}", 250)) {
252 for addr in addresses {
253 assert!(
254 addr.parse::<OnionAddr>().is_err(),
255 "Input {addr} has been parsed correctly."
256 );
257 }
258 }
259
260 #[test]
261 fn parse_random_invalid_onion_address_w_port(addresses in vec(".{0,62}:.{1,5}", 250)) {
262 for addr in addresses {
263 assert!(
264 addr.parse::<OnionAddr>().is_err(),
265 "Input {addr} has been parsed correctly."
266 );
267 }
268 }
269 }
270}