minecraft_java_rs_core/net/
client.rs1use std::net::{IpAddr, SocketAddr};
4use std::sync::Arc;
5use std::time::Duration;
6
7use reqwest::dns::{Addrs, Name, Resolve, Resolving};
8use serde::Deserialize;
9
10#[derive(Debug, Default)]
19struct Ipv4OnlyResolver;
20
21impl Resolve for Ipv4OnlyResolver {
22 fn resolve(&self, name: Name) -> Resolving {
23 let host = name.as_str().to_owned();
24 Box::pin(async move {
25 let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?;
27 let v4: Vec<SocketAddr> = addrs.filter(SocketAddr::is_ipv4).collect();
28 Ok(Box::new(v4.into_iter()) as Addrs)
29 })
30 }
31}
32
33#[derive(Debug)]
44struct DohResolver {
45 client: reqwest::Client,
49 endpoint: String,
51 resolver: IpAddr,
53 ipv4_only: bool,
54}
55
56#[derive(Deserialize)]
58struct DohResponse {
59 #[serde(rename = "Answer", default)]
60 answer: Vec<DohAnswer>,
61}
62
63#[derive(Deserialize)]
64struct DohAnswer {
65 data: String,
68}
69
70impl DohResolver {
71 async fn query(
72 client: reqwest::Client,
73 endpoint: String,
74 host: String,
75 rtype: &'static str,
76 ) -> Result<Vec<IpAddr>, Box<dyn std::error::Error + Send + Sync>> {
77 let resp = client
78 .get(&endpoint)
79 .query(&[("name", host.as_str()), ("type", rtype)])
80 .header("accept", "application/dns-json")
81 .send()
82 .await?
83 .error_for_status()?
84 .json::<DohResponse>()
85 .await?;
86 Ok(resp
87 .answer
88 .into_iter()
89 .filter_map(|a| a.data.parse::<IpAddr>().ok())
90 .collect())
91 }
92}
93
94impl Resolve for DohResolver {
95 fn resolve(&self, name: Name) -> Resolving {
96 let client = self.client.clone();
97 let endpoint = self.endpoint.clone();
98 let resolver = self.resolver;
99 let ipv4_only = self.ipv4_only;
100 let host = name.as_str().to_owned();
101 Box::pin(async move {
102 let debug = std::env::var_os("MJRS_DNS_DEBUG").is_some();
106
107 let mut ips =
108 match DohResolver::query(client.clone(), endpoint.clone(), host.clone(), "A").await
109 {
110 Ok(v) => v,
111 Err(e) => {
112 if debug {
113 eprintln!("[dns] DoH via {resolver} → {host} = ERROR ({e})");
114 }
115 return Err(e);
116 }
117 };
118 if !ipv4_only {
119 if let Ok(v6) = DohResolver::query(client, endpoint, host.clone(), "AAAA").await {
122 ips.extend(v6);
123 }
124 }
125 if debug {
126 eprintln!("[dns] DoH via {resolver} → {host} = {ips:?}");
127 }
128 let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip| SocketAddr::new(ip, 0)).collect();
129 Ok(Box::new(addrs.into_iter()) as Addrs)
130 })
131 }
132}
133
134pub fn build_client(
144 timeout_secs: u64,
145 force_ipv4: bool,
146 dns: Option<IpAddr>,
147) -> reqwest::Result<reqwest::Client> {
148 let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(timeout_secs));
149 match dns {
150 Some(ip) => {
151 let boot = reqwest::Client::builder()
152 .timeout(Duration::from_secs(timeout_secs))
153 .build()?;
154 let endpoint = match ip {
155 IpAddr::V4(v4) => format!("https://{v4}/dns-query"),
156 IpAddr::V6(v6) => format!("https://[{v6}]/dns-query"),
157 };
158 builder = builder.dns_resolver(Arc::new(DohResolver {
159 client: boot,
160 endpoint,
161 resolver: ip,
162 ipv4_only: force_ipv4,
163 }));
164 }
165 None if force_ipv4 => {
166 builder = builder.dns_resolver(Arc::new(Ipv4OnlyResolver));
167 }
168 None => {}
169 }
170 builder.build()
171}
172
173pub fn describe_reqwest_error(err: &reqwest::Error) -> String {
182 if let Some(status) = err.status() {
183 let reason = status.canonical_reason().unwrap_or("unknown");
184 return format!("HTTP {status} {reason}");
185 }
186
187 let kind = if err.is_timeout() {
188 "connection timed out"
189 } else if err.is_connect() {
190 "could not establish connection"
191 } else if err.is_request() {
192 "request could not be sent"
193 } else {
194 "network error"
195 };
196
197 let mut root: Option<String> = None;
200 let mut src = std::error::Error::source(err);
201 while let Some(e) = src {
202 root = Some(e.to_string());
203 src = e.source();
204 }
205
206 match root {
207 Some(cause) => format!("{kind} ({cause})"),
208 None => kind.to_string(),
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn build_client_succeeds_in_all_modes() {
218 assert!(build_client(10, false, None).is_ok());
219 assert!(build_client(10, true, None).is_ok());
220 assert!(build_client(10, false, Some("1.1.1.1".parse().unwrap())).is_ok());
221 assert!(build_client(10, true, Some("1.1.1.1".parse().unwrap())).is_ok());
222 }
223
224 #[tokio::test]
228 #[ignore = "requires internet: queries Cloudflare DoH at 1.1.1.1"]
229 async fn doh_resolver_resolves_real_host() {
230 std::env::set_var("MJRS_DNS_DEBUG", "1");
233
234 let resolver = DohResolver {
235 client: reqwest::Client::builder().build().unwrap(),
236 endpoint: "https://1.1.1.1/dns-query".to_owned(),
237 resolver: "1.1.1.1".parse().unwrap(),
238 ipv4_only: true,
239 };
240 let name: Name = "resources.download.minecraft.net".parse().unwrap();
241 let addrs: Vec<SocketAddr> = resolver.resolve(name).await.unwrap().collect();
242 assert!(!addrs.is_empty(), "DoH returned no addresses");
243 assert!(addrs.iter().all(SocketAddr::is_ipv4), "ipv4_only honoured");
244
245 std::env::remove_var("MJRS_DNS_DEBUG");
246 }
247}