1use std::{
7 net::{IpAddr, SocketAddr},
8 time::Duration,
9};
10
11use tokio::{
12 io::{AsyncReadExt, AsyncWriteExt},
13 net::{TcpStream, UdpSocket},
14 time::timeout,
15};
16
17use hickory_proto::{
18 op::{Message, Query},
19 rr::{Name, RData, RecordType},
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Target {
25 pub host: String,
26 pub port: u16,
27 pub priority: u16,
28 pub weight: u16,
29}
30
31impl Target {
32 pub fn session_url(&self) -> String {
34 let host = if self.host.contains(':') { format!("[{}]", self.host) } else { self.host.clone() };
35 if self.port == 443 {
36 format!("https://{host}/.well-known/jmap")
37 } else {
38 format!("https://{host}:{}/.well-known/jmap", self.port)
39 }
40 }
41}
42
43pub fn name_servers(resolv_conf: &str) -> Vec<IpAddr> {
45 resolv_conf
46 .lines()
47 .filter_map(|line| line.trim().strip_prefix("nameserver"))
48 .filter_map(|rest| rest.split_whitespace().next())
49 .filter_map(|address| address.split('%').next()?.parse().ok())
50 .collect()
51}
52
53pub fn query(domain: &str, id: u16) -> Result<Vec<u8>, String> {
55 let name = Name::from_ascii(format!("_jmap._tcp.{}.", domain.trim_end_matches('.'))).map_err(|e| e.to_string())?;
56 let mut message = Message::query();
57 message.metadata.id = id;
58 message.metadata.recursion_desired = true;
59 message.add_query(Query::query(name, RecordType::SRV));
60 message.to_vec().map_err(|e| e.to_string())
61}
62
63pub fn targets(answer: &[u8], id: u16) -> Result<Option<Vec<Target>>, String> {
66 let message = Message::from_vec(answer).map_err(|e| e.to_string())?;
67 if message.metadata.id != id {
68 return Err("an answer to another question".into());
69 }
70 if message.metadata.truncation {
71 return Ok(None);
72 }
73 let mut out: Vec<Target> = message
74 .answers
75 .iter()
76 .filter_map(|record| match &record.data {
77 RData::SRV(srv) => Some(Target {
78 host: srv.target.to_utf8().trim_end_matches('.').to_owned(),
79 port: srv.port,
80 priority: srv.priority,
81 weight: srv.weight,
82 }),
83 _ => None,
84 })
85 .filter(|target| !target.host.is_empty())
86 .collect();
87 out.sort_by_key(|target| (target.priority, std::cmp::Reverse(target.weight)));
88 Ok(Some(out))
89}
90
91async fn within<T>(step: impl Future<Output = std::io::Result<T>>) -> Result<T, String> {
93 timeout(Duration::from_secs(3), step).await.map_err(|_| "no answer".to_owned())?.map_err(|e| e.to_string())
94}
95
96async fn ask(server: IpAddr, question: &[u8], id: u16) -> Result<Vec<Target>, String> {
97 let address = SocketAddr::new(server, 53);
98 let bind: SocketAddr = if server.is_ipv4() { ([0, 0, 0, 0], 0).into() } else { ([0u16; 8], 0).into() };
99 let socket = UdpSocket::bind(bind).await.map_err(|e| e.to_string())?;
100 socket.send_to(question, address).await.map_err(|e| e.to_string())?;
101 let mut buffer = vec![0; 4096];
102 let (read, from) = within(socket.recv_from(&mut buffer)).await?;
103 if from != address {
104 return Err("an answer from elsewhere".into());
105 }
106 if let Some(targets) = targets(&buffer[..read], id)? {
107 return Ok(targets);
108 }
109 let mut stream = within(TcpStream::connect(address)).await?;
111 let length = u16::try_from(question.len()).map_err(|e| e.to_string())?;
112 let mut message = length.to_be_bytes().to_vec();
113 message.extend_from_slice(question);
114 within(stream.write_all(&message)).await?;
115 let mut length = [0; 2];
116 within(stream.read_exact(&mut length)).await?;
117 let mut answer = vec![0; u16::from_be_bytes(length) as usize];
118 within(stream.read_exact(&mut answer)).await?;
119 targets(&answer, id)?.ok_or_else(|| "the answer was cut over TCP too".into())
120}
121
122pub async fn lookup(domain: &str) -> Result<Vec<Target>, String> {
124 let resolv_conf = tokio::fs::read_to_string("/etc/resolv.conf").await.unwrap_or_default();
125 let servers = name_servers(&resolv_conf);
126 if servers.is_empty() {
127 return Err("no name server in /etc/resolv.conf".into());
128 }
129 let id = u16::try_from(std::process::id() % 65_536).unwrap_or(1) ^ 0x5a5a;
130 let question = query(domain, id)?;
131 let mut last = String::new();
132 for server in servers {
133 match ask(server, &question, id).await {
134 Ok(targets) => return Ok(targets),
135 Err(error) => last = error,
136 }
137 }
138 Err(last)
139}
140
141#[cfg(test)]
142mod tests {
143 use hickory_proto::rr::{Record, rdata::SRV};
144
145 use super::*;
146
147 #[test]
148 fn resolv_conf() {
149 let text = "# x\nnameserver 192.0.2.1\nnameserver fe80::1%en0\nsearch example\nnameserver nonsense\n";
150 let servers = name_servers(text);
151 assert_eq!(servers, ["192.0.2.1".parse::<IpAddr>().unwrap(), "fe80::1".parse().unwrap()]);
152 }
153
154 #[test]
155 fn questions_and_answers() {
156 let question = query("firma.example", 7).unwrap();
157 let mut answer = Message::from_vec(&question).unwrap();
158 assert_eq!(answer.queries[0].name().to_utf8(), "_jmap._tcp.firma.example.");
159 answer.metadata.message_type = hickory_proto::op::MessageType::Response;
160 let name = Name::from_ascii("_jmap._tcp.firma.example.").unwrap();
161 for (priority, weight, host) in
162 [(20, 0, "b.firma.example."), (10, 1, "a.firma.example."), (10, 5, "c.firma.example.")]
163 {
164 let srv = SRV::new(priority, weight, 8443, Name::from_ascii(host).unwrap());
165 answer.answers.push(Record::from_rdata(name.clone(), 300, RData::SRV(srv)));
166 }
167 let found = targets(&answer.to_vec().unwrap(), 7).unwrap().unwrap();
168 let hosts: Vec<_> = found.iter().map(|t| t.host.as_str()).collect();
169 assert_eq!(hosts, ["c.firma.example", "a.firma.example", "b.firma.example"]);
170 assert_eq!(found[0].session_url(), "https://c.firma.example:8443/.well-known/jmap");
171 assert!(targets(&answer.to_vec().unwrap(), 8).is_err());
172 answer.metadata.truncation = true;
173 assert_eq!(targets(&answer.to_vec().unwrap(), 7).unwrap(), None);
174 }
175
176 #[tokio::test]
178 #[ignore = "needs the network"]
179 async fn live() {
180 let found = lookup("fastmail.com").await.unwrap();
181 assert!(!found.is_empty(), "fastmail.com publishes _jmap._tcp");
182 println!("{found:?}");
183 }
184}