modality_utils/
multiaddr_list.rs1use std::str::FromStr;
2use anyhow::{Result, Error};
3use regex::Regex;
4use hickory_resolver::{
5 config::{ResolverConfig, ResolverOpts},
6 TokioAsyncResolver,
7};
8use reqwest;
9use serde_json::Value;
10use libp2p::multiaddr::Multiaddr;
11
12async fn remove_quotes(s: &str) -> String {
13 let re = Regex::new(r#""(.+)""#).unwrap();
14 if let Some(caps) = re.captures(s) {
15 caps[1].to_string()
16 } else {
17 s.to_string()
18 }
19}
20
21fn matches_peer_id_suffix(s: &str, peer_id: &str) -> bool {
22 let re = Regex::new(&format!(r"/p2p/{}$", peer_id)).unwrap();
23 re.is_match(s)
24}
25
26#[allow(dead_code)]
27async fn resolve_via_cloudflare_dns(name: &str, type_: &str) -> Result<Vec<String>, Error> {
28 let client = reqwest::Client::new();
29 let url = format!(
30 "https://cloudflare-dns.com/dns-query?name={}&type={}",
31 name, type_
32 );
33
34 let response = client
35 .get(&url)
36 .header("accept", "application/dns-json")
37 .send()
38 .await?
39 .json::<Value>()
40 .await?;
41
42 let answers = response["Answer"]
43 .as_array()
44 .map(|arr| {
45 arr.iter()
46 .filter_map(|ans| ans["data"].as_str().map(String::from))
47 .collect()
48 })
49 .unwrap_or_default();
50
51 Ok(answers)
52}
53
54async fn resolve_via_dns(name: &str, type_: &str) -> Result<Vec<String>, Error> {
55 let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
56
57 match type_ {
58 "A" => {
59 let response = resolver.lookup_ip(name).await?;
60 Ok(response
61 .iter()
62 .map(|ip| ip.to_string())
63 .collect())
64 }
65 "TXT" => {
66 let response = resolver.txt_lookup(name).await?;
67 let mut results = Vec::new();
68 for record in response.iter() {
69 let txt = record
70 .txt_data()
71 .iter()
72 .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
73 .collect::<Vec<_>>()
74 .join("");
75 results.push(txt);
76 }
77 Ok(results)
78 }
79 _ => Ok(vec![]),
80 }
81}
82
83pub async fn resolve_dns_entries(entries: Vec<String>) -> Result<Vec<String>, Error> {
84 let mut results = Vec::new();
85
86 for entry in entries {
87 let p2p_re = Regex::new(r"/p2p/(.+)$").unwrap();
88 let p2p_match = p2p_re.captures(&entry);
89
90 if entry.starts_with("/dns/") {
91 let dns_re = Regex::new(r"^/dns/([A-Za-z0-9-.]+)(.*)").unwrap();
92 if let Some(caps) = dns_re.captures(&entry) {
93 let name = &caps[1];
94 let rest = &caps[2];
95 let answers = resolve_via_dns(name, "A").await?;
96 let peer_id = p2p_match.as_ref().map(|m| m[1].to_string());
97
98 for address in answers {
99 let ans = format!("/ip4/{}{}", address, rest);
100 if peer_id.is_none() || matches_peer_id_suffix(&ans, &peer_id.as_ref().unwrap()) {
101 results.push(ans);
102 }
103 }
104 }
105 } else if entry.starts_with("/dnsaddr/") {
106 let dnsaddr_re = Regex::new(r"^/dnsaddr/([A-Za-z0-9-.]+)(.*)").unwrap();
107 if let Some(caps) = dnsaddr_re.captures(&entry) {
108 let name = format!("_dnsaddr.{}", &caps[1]);
109 let mut answers = resolve_via_dns(&name, "TXT").await?;
110 let peer_id = p2p_match.as_ref().map(|m| m[1].to_string());
111
112 for answer in &mut answers {
113 *answer = remove_quotes(answer).await;
114 if let Some(ans_caps) = Regex::new(r"^dnsaddr=(.*)").unwrap().captures(answer) {
115 let ans = &ans_caps[1];
116 if peer_id.is_none() || matches_peer_id_suffix(ans, &peer_id.as_ref().unwrap()) {
117 results.push(ans.to_string());
118 }
119 }
120 }
121 }
122 } else {
123 results.push(entry);
124 }
125 }
126
127 Ok(results)
128}
129
130pub async fn resolve_dns_multiaddrs(multiaddrs: Vec<Multiaddr>) -> Result<Vec<Multiaddr>, Error> {
131 let entries: Vec<String> = multiaddrs.iter().map(|addr| addr.to_string()).collect();
132 let resolved_entries = resolve_dns_entries(entries).await?;
133 let resolved_multiaddrs = resolved_entries.into_iter().filter_map(|entry| Multiaddr::from_str(&entry).ok()).collect();
134 Ok(resolved_multiaddrs)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use tokio;
141
142 #[tokio::test]
143 async fn test_dns_resolution() {
144 let entries = vec![
145 "/dns/example.com/tcp/80/ws/p2p/12D3KooW9pte76rpnggcLYkFaawuTEs5DC5axHkg3cK3cewGxxHd".to_string()
146 ];
147
148 let result = resolve_dns_entries(entries).await.unwrap();
149 assert!(result[0].starts_with("/ip4/"));
150
151 let entries = vec!["/dnsaddr/devnet3.modality.network".to_string()];
152 let result = resolve_dns_entries(entries).await.unwrap();
153 assert_eq!(result.len(), 3);
154 assert!(result[0].starts_with("/ip4/"));
155
156 let entries = vec![
157 "/dnsaddr/devnet3.modality.network/p2p/12D3KooW9pte76rpnggcLYkFaawuTEs5DC5axHkg3cK3cewGxxHd".to_string()
158 ];
159 let result = resolve_dns_entries(entries).await.unwrap();
160 assert_eq!(result.len(), 1);
161 assert!(result[0].ends_with("/p2p/12D3KooW9pte76rpnggcLYkFaawuTEs5DC5axHkg3cK3cewGxxHd"));
162 }
163}