Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/addr.rs

12.5 KiB, 66 runs

created by r1870400018:553, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use oxedyne_fe2o3_core::prelude::*;
2use oxedyne_fe2o3_stds::regions::Country;
3
4use std::{
5 convert::TryFrom,
6 fmt::{self},
7 net::IpAddr,
8};
9
10pub struct PhoneNumbers;
11
12impl PhoneNumbers {
13
14 pub fn country_to_prefixes(c: &Country) -> Outcome<Vec<u16>> {
15 match c {
16 Country::Australia => Ok(vec![61]),
17 _ => Err(err!(
18 "No prefix defined for country {:?}.", c;
19 Invalid, Input, Missing)),
20 }
21 }
22
23 pub fn prefix_to_country(p: u16) -> Option<Country> {
24 match p {
25 61 => Some(Country::Australia),
26 _ => None,
27 }
28 }
29}
30
31#[derive(Clone, Debug, Default, Eq, PartialEq)]
32pub struct PhoneNumber {
33 pub prefix: u16,
34 pub num: String,
35}
36
37#[derive(Clone, Debug, Default, Eq, PartialEq)]
38pub struct EmailAddress {
39 pub loc: String,
40 pub dom: String,
41}
42
43impl TryFrom<&str> for EmailAddress {
44 type Error = Error<ErrTag>;
45
46 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
47 if s.len() == 0 {
48 return Err(err!(
49 "Trying to interpret an email address from '{}': \
50 length is zero.", s;
51 Decode, String, Invalid, Input));
52 }
53 let mut at = None;
54 for (i, c) in s.chars().enumerate() {
55 match c {
56 '@' => match at {
57 None => at = Some(i),
58 Some(j) => return Err(err!(
59 "Trying to interpret an email address from '{}': '@' \
60 character found at position {} found previously at \
61 position {}.", s, i, j;
62 Decode, String, Invalid, Input)),
63 },
64 ' ' => return Err(err!(
65 "Trying to interpret an email address from '{}': Space \
66 characters are invalid, space found at position {}.", s, i;
67 Decode, String, Invalid, Input)),
68 _ => (),
69 }
70 }
71 match at {
72 None => Err(err!(
73 "Trying to interpret an email address from '{}': '@' \
74 character not found.", s;
75 Decode, String, Invalid, Input)),
76 Some(i) => {
77 let (left, right) = s.split_at(i);
78 if left.len() == 0 {
79 return Err(err!(
80 "Trying to interpret an email address from '{}': \
81 local (left) part of email address has length \
82 zero.", s;
83 Decode, String, Invalid, Input));
84 }
85 if right.len() == 0 {
86 return Err(err!(
87 "Trying to interpret an email address from '{}': \
88 domain (right) part of email address has length \
89 zero.", s;
90 Decode, String, Invalid, Input));
91 }
92 let right = &right[1..];
93 match right.find('@') {
94 Some(j) => return Err(err!(
95 "Trying to interpret an email address from '{}': \
96 invalid '@' character found at position {} in the \
97 domain part '{}'.", s, j, right;
98 Decode, String, Invalid, Input)),
99 None => (),
100 }
101 Ok(EmailAddress {
102 loc: left.to_string(),
103 dom: right.to_string(),
104 })
105 },
106 }
107 }
108}
109
110/// An address on a Hematite-native overlay identity layer.
111///
112/// `real` is the underlying backing identity (whatever the overlay
113/// treats as the host or account behind the address) and `virt` is
114/// the virtual face presented to correspondents. The wire form is
115/// `overlay:<real>//<virt>`. Left as a placeholder type until the
116/// overlay's address model is nailed down by downstream consumers.
117#[derive(Clone, Debug, Default, Eq, PartialEq)]
118pub struct OverlayAddress {
119 real: String,
120 virt: String,
121}
122
123/// One of the several address shapes a contact can be reached at:
124/// phone, email, or a Hematite-native overlay address.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub enum ContactAddress {
127 Phone(PhoneNumber),
128 Email(EmailAddress),
129 Overlay(OverlayAddress),
130}
131
132impl fmt::Display for ContactAddress {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 ContactAddress::Phone(pn) => write!(f, "+{} {}", pn.prefix, pn.num),
136 ContactAddress::Email(email) => write!(f, "{}@{}", email.loc, email.dom),
137 ContactAddress::Overlay(addr) => write!(f, "overlay:{}//{}", addr.real, addr.virt),
138 }
139 }
140}
141
142// FINISHME
143impl TryFrom<&str> for ContactAddress {
144 type Error = Error<ErrTag>;
145
146 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
147 let s = s.trim_start();
148 if s.starts_with("overlay:") {
149 let addr = OverlayAddress::default();
150 Ok(ContactAddress::Overlay(addr))
151 } else if s.contains('@') {
152 let email = res!(EmailAddress::try_from(s));
153 Ok(ContactAddress::Email(email))
154 } else {
155 let pn = PhoneNumber::default();
156 Ok(ContactAddress::Phone(pn))
157 }
158 }
159
160}
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_email_address_decoding_00() -> Outcome<()> {
167 let email = "test@my.domain";
168 let addr = res!(EmailAddress::try_from(email));
169 let expected = EmailAddress { loc: fmt!("test"), dom: fmt!("my.domain") };
170 if addr != expected {
171 return Err(err!(
172 "Decoding of address '{}' should produce '{:?}'.", email, expected;
173 Invalid, Input, Decode, String));
174 }
175 Ok(())
176 }
177
178 #[test]
179 fn test_email_address_decoding_01() -> Outcome<()> {
180 let email = "test@my@domain";
181 let result = EmailAddress::try_from(email);
182 if result.is_ok() {
183 return Err(err!(
184 "Decoding of address '{}' should produce an error.", email;
185 Invalid, Input, Decode, String));
186 }
187 Ok(())
188 }
189
190 #[test]
191 fn test_email_address_decoding_02() -> Outcome<()> {
192 let email = "test @my.domain";
193 let result = EmailAddress::try_from(email);
194 if result.is_ok() {
195 return Err(err!(
196 "Decoding of address '{}' should produce an error.", email;
197 Invalid, Input, Decode, String));
198 }
199 Ok(())
200 }
201}
202
203
204// ┌───────────────────────────────────────────────────────────────────────────┐
205// │ OUTBOUND ADDRESS VETTING │
206// └───────────────────────────────────────────────────────────────────────────┘
207
208/// Whether an address is one a server may connect to on a user's say-so.
209///
210/// A service that opens a connection to a host its user named -- a mail
211/// bridge, a webhook sender, a link previewer -- is a request forgery
212/// waiting to happen: the user names `localhost`, or `169.254.169.254`, or
213/// a private address behind the firewall, and the server dutifully reaches
214/// somewhere the user could never reach themselves. What makes it dangerous
215/// is that the server's *position* is the privilege, not its credentials.
216///
217/// So: loopback, private, link-local, multicast, broadcast, unspecified and
218/// the documentation and benchmark ranges are all refused. What remains is
219/// what the user could have reached from their own machine anyway.
220pub fn is_publicly_routable(ip: &IpAddr) -> bool {
221 match ip {
222 IpAddr::V4(v4) => {
223 if v4.is_loopback() // 127/8
224 || v4.is_private() // 10/8, 172.16/12, 192.168/16
225 || v4.is_link_local() // 169.254/16, and so the cloud metadata address
226 || v4.is_multicast()
227 || v4.is_broadcast()
228 || v4.is_unspecified() // 0.0.0.0
229 || v4.is_documentation() // 192.0.2/24, 198.51.100/24, 203.0.113/24
230 {
231 return false;
232 }
233 let o = v4.octets();
234 // Shared address space (RFC 6598, carrier-grade NAT).
235 if o[0] == 100 && (64..128).contains(&o[1]) { return false; }
236 // Benchmarking (RFC 2544).
237 if o[0] == 198 && (o[1] == 18 || o[1] == 19) { return false; }
238 // Reserved for future use, 240/4 upwards.
239 if o[0] >= 240 { return false; }
240 true
241 }
242 IpAddr::V6(v6) => {
243 if v6.is_loopback()
244 || v6.is_multicast()
245 || v6.is_unspecified()
246 {
247 return false;
248 }
249 let seg = v6.segments();
250 // Unique local, fc00::/7.
251 if (seg[0] & 0xfe00) == 0xfc00 { return false; }
252 // Link-local, fe80::/10.
253 if (seg[0] & 0xffc0) == 0xfe80 { return false; }
254 // Documentation, 2001:db8::/32.
255 if seg[0] == 0x2001 && seg[1] == 0x0db8 { return false; }
256 // An IPv4-mapped address is only as safe as the IPv4 inside it.
257 if let Some(v4) = v6.to_ipv4_mapped() {
258 return is_publicly_routable(&IpAddr::V4(v4));
259 }
260 true
261 }
262 }
263}
264
265/// Resolve a host the user named, and return only the addresses a server
266/// may actually connect to.
267///
268/// Fails, rather than returning an empty list, when the host resolves
269/// entirely into space a server must not reach -- because that is not an
270/// empty result, it is an attempted request forgery, and the caller wants
271/// to say so.
272///
273/// The caller should connect to one of the returned addresses *directly*,
274/// not re-resolve the name: resolving twice invites the answer to change in
275/// between (DNS rebinding), which is the whole trick.
276pub fn resolve_public(host: &str) -> Outcome<Vec<IpAddr>> {
277 // A bare address needs no resolution, and must not get any: a literal
278 // is exactly how the forgery is usually spelled.
279 if let Ok(ip) = host.parse::<IpAddr>() {
280 if !is_publicly_routable(&ip) {
281 return Err(err!(
282 "The address {} is not publicly routable, and this server \
283 will not connect to it on request.", ip;
284 Invalid, Input, Security));
285 }
286 return Ok(vec![ip]);
287 }
288
289 let answers = res!(crate::dns_resolver::lookup_a(host));
290 if answers.is_empty() {
291 return Err(err!(
292 "The host '{}' does not resolve.", host;
293 Invalid, Input, NotFound));
294 }
295 let public: Vec<IpAddr> = answers.into_iter()
296 .map(IpAddr::V4)
297 .filter(is_publicly_routable)
298 .collect();
299 if public.is_empty() {
300 return Err(err!(
301 "The host '{}' resolves only to addresses that are not publicly \
302 routable, and this server will not connect to it on request.", host;
303 Invalid, Input, Security));
304 }
305 Ok(public)
306}
307
308
309#[cfg(test)]
310mod vetting_tests {
311 use super::*;
312
313 fn v4(s: &str) -> IpAddr { s.parse().expect("test address") }
314
315 #[test]
316 fn test_private_space_is_refused() {
317 for s in [
318 "127.0.0.1", // loopback
319 "10.1.2.3", // private
320 "172.16.0.1", // private
321 "192.168.1.1", // private
322 "169.254.169.254", // the cloud metadata service
323 "0.0.0.0", // unspecified
324 "100.64.0.1", // carrier-grade NAT
325 "224.0.0.1", // multicast
326 "255.255.255.255", // broadcast
327 "240.0.0.1", // reserved
328 ] {
329 assert!(!is_publicly_routable(&v4(s)), "{} should be refused", s);
330 }
331 }
332
333 #[test]
334 fn test_public_space_is_allowed() {
335 for s in ["1.1.1.1", "8.8.8.8", "142.250.70.14", "2606:4700::1111"] {
336 assert!(is_publicly_routable(&v4(s)), "{} should be allowed", s);
337 }
338 }
339
340 #[test]
341 fn test_ipv6_private_space_is_refused() {
342 for s in ["::1", "fc00::1", "fe80::1", "2001:db8::1", "::ffff:127.0.0.1"] {
343 assert!(!is_publicly_routable(&v4(s)), "{} should be refused", s);
344 }
345 }
346
347 #[test]
348 fn test_literal_loopback_is_refused_without_dns() {
349 assert!(resolve_public("127.0.0.1").is_err());
350 assert!(resolve_public("::1").is_err());
351 }
352}