1use std::time::Duration;
4
5use reqwest::{
6 Client,
7 ClientBuilder,
8 Response,
9 Url,
10};
11use thiserror::Error;
12use xdid_core::{
13 Method,
14 MethodFuture,
15 ResolutionError,
16 did::Did,
17 document::Document,
18};
19
20mod parse;
21mod policy;
22
23const NAME: &str = "web";
24const USER_AGENT: &str = concat!("xdid/", env!("CARGO_PKG_VERSION"));
25
26#[derive(Debug, Clone)]
29pub struct Config {
30 pub max_document_bytes: u64,
33 pub connect_timeout: Duration,
34 pub request_timeout: Duration,
35 pub allow_local: bool,
39}
40
41impl Default for Config {
42 fn default() -> Self {
43 Self {
44 max_document_bytes: 64 * 1024,
45 connect_timeout: Duration::from_secs(5),
46 request_timeout: Duration::from_secs(10),
47 allow_local: false,
48 }
49 }
50}
51
52#[derive(Debug, Error)]
55#[error("failed to build the HTTP client: {0}")]
56pub struct ClientError(String);
57
58pub struct MethodDidWeb {
59 client: Client,
60 config: Config,
61}
62
63impl MethodDidWeb {
64 pub fn new() -> Result<Self, ClientError> {
70 Self::with_config(Config::default())
71 }
72
73 pub fn with_config(config: Config) -> Result<Self, ClientError> {
79 let client = build_client(&config).map_err(|e| ClientError(e.to_string()))?;
80 Ok(Self { client, config })
81 }
82}
83
84#[cfg(not(target_family = "wasm"))]
85fn build_client(config: &Config) -> Result<Client, reqwest::Error> {
86 ClientBuilder::new()
87 .user_agent(USER_AGENT)
88 .redirect(reqwest::redirect::Policy::none())
90 .https_only(!config.allow_local)
91 .connect_timeout(config.connect_timeout)
92 .timeout(config.request_timeout)
93 .build()
94}
95
96#[cfg(target_family = "wasm")]
99fn build_client(_config: &Config) -> Result<Client, reqwest::Error> {
100 ClientBuilder::new().user_agent(USER_AGENT).build()
101}
102
103impl Method for MethodDidWeb {
104 fn method_name(&self) -> &'static str {
105 NAME
106 }
107
108 #[cfg(not(target_family = "wasm"))]
109 fn resolve(&self, did: Did) -> MethodFuture<Result<Document, ResolutionError>> {
110 Box::pin(resolve_inner(self.client.clone(), self.config.clone(), did))
111 }
112
113 #[cfg(target_family = "wasm")]
114 fn resolve(&self, did: Did) -> MethodFuture<Result<Document, ResolutionError>> {
115 Box::pin(send_wrapper::SendWrapper::new(resolve_inner(
118 self.client.clone(),
119 self.config.clone(),
120 did,
121 )))
122 }
123}
124
125async fn resolve_inner(
126 client: Client,
127 config: Config,
128 did: Did,
129) -> Result<Document, ResolutionError> {
130 if did.method_name.as_str() != NAME {
131 return Err(ResolutionError::InvalidDid);
132 }
133
134 let url =
135 parse::parse_url(&did, config.allow_local).map_err(|_| ResolutionError::InvalidDid)?;
136
137 if !config.allow_local {
138 check_target(&url).await?;
139 }
140
141 let res = client
142 .get(url)
143 .header(
144 reqwest::header::ACCEPT,
145 "application/did+json, application/json",
146 )
147 .send()
148 .await
149 .map_err(fetch_failed)?
150 .error_for_status()
151 .map_err(fetch_failed)?;
152
153 let body = read_capped(res, config.max_document_bytes).await?;
154
155 let doc = serde_json::from_slice::<Document>(&body)
156 .map_err(|e| ResolutionError::ResolutionFailed(e.to_string()))?;
157
158 if doc.id != did {
162 return Err(ResolutionError::DocumentMismatch);
163 }
164
165 Ok(doc)
166}
167
168fn fetch_failed(e: reqwest::Error) -> ResolutionError {
171 ResolutionError::ResolutionFailed(e.without_url().to_string())
172}
173
174#[cfg(not(target_family = "wasm"))]
180async fn check_target(url: &Url) -> Result<(), ResolutionError> {
181 use std::net::IpAddr;
182
183 let host = url.host_str().ok_or(ResolutionError::InvalidDid)?;
184 let port = url.port_or_known_default().unwrap_or(443);
185
186 let bare = host.trim_start_matches('[').trim_end_matches(']');
187 let addrs = if let Ok(ip) = bare.parse::<IpAddr>() {
188 vec![ip]
189 } else {
190 tokio::net::lookup_host((host, port))
191 .await
192 .map_err(|e| ResolutionError::ResolutionFailed(e.to_string()))?
193 .map(|addr| addr.ip())
194 .collect()
195 };
196
197 if addrs.is_empty() || addrs.iter().copied().any(policy::is_restricted) {
198 return Err(ResolutionError::TargetNotAllowed);
199 }
200
201 Ok(())
202}
203
204#[cfg(target_family = "wasm")]
205async fn check_target(_url: &Url) -> Result<(), ResolutionError> {
206 Ok(())
207}
208
209#[cfg(not(target_family = "wasm"))]
210async fn read_capped(mut res: Response, max: u64) -> Result<Vec<u8>, ResolutionError> {
211 if res.content_length().is_some_and(|len| len > max) {
212 return Err(ResolutionError::DocumentTooLarge);
213 }
214
215 let cap = res.content_length().unwrap_or(0).min(max);
216 let mut buf = Vec::with_capacity(usize::try_from(cap).unwrap_or(0));
217
218 while let Some(chunk) = res.chunk().await.map_err(fetch_failed)? {
219 if buf.len() as u64 + chunk.len() as u64 > max {
220 return Err(ResolutionError::DocumentTooLarge);
221 }
222 buf.extend_from_slice(&chunk);
223 }
224
225 Ok(buf)
226}
227
228#[cfg(target_family = "wasm")]
231async fn read_capped(res: Response, max: u64) -> Result<Vec<u8>, ResolutionError> {
232 if res.content_length().is_some_and(|len| len > max) {
233 return Err(ResolutionError::DocumentTooLarge);
234 }
235
236 let body = res.bytes().await.map_err(fetch_failed)?;
237 if body.len() as u64 > max {
238 return Err(ResolutionError::DocumentTooLarge);
239 }
240
241 Ok(body.to_vec())
242}