modality_utils/
multiaddr_list.rs

1use 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}