did_utils/methods/web/
resolver.rs

1use crate::methods::resolution::{DIDResolutionMetadata, DIDResolutionOptions, MediaType, ResolutionOutput};
2use async_trait::async_trait;
3use hyper::{
4    client::{connect::Connect, HttpConnector},
5    http::uri::{self, Scheme},
6    Body, Client, Uri,
7};
8use hyper_tls::HttpsConnector;
9
10use crate::ldmodel::Context;
11use crate::methods::{errors::DidWebError, traits::DIDResolver};
12
13use crate::didcore::Document as DIDDocument;
14
15/// A struct for resolving DID Web documents.
16pub struct DidWeb<C>
17where
18    C: Connect + Send + Sync + Clone + 'static,
19{
20    client: Client<C>,
21}
22
23impl DidWeb<HttpConnector> {
24    // Creates a new `DidWeb` resolver with HTTP scheme, for testing only.
25    #[cfg(test)]
26    pub fn http() -> DidWeb<HttpConnector> {
27        DidWeb {
28            client: Client::builder().build::<_, Body>(HttpConnector::new()),
29        }
30    }
31}
32
33impl Default for DidWeb<HttpsConnector<HttpConnector>> {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl DidWeb<HttpsConnector<HttpConnector>> {
40    /// Creates a new `DidWeb` resolver.
41    pub fn new() -> DidWeb<HttpsConnector<HttpConnector>> {
42        DidWeb {
43            client: Client::builder().build::<_, Body>(HttpsConnector::new()),
44        }
45    }
46}
47
48impl<C> DidWeb<C>
49where
50    C: Connect + Send + Sync + Clone + 'static,
51{
52    /// Fetches a DID document from the given URL.
53    ///
54    /// This method performs an HTTP GET request to the provided URL
55    /// and attempts to returns the response body as a string.
56    ///
57    /// # Arguments
58    ///
59    /// * `url` - The URL to fetch the DID document from.
60    ///
61    /// # Returns
62    ///
63    /// A `Result` containing the DID document as a string or a `DidWebError`.
64    async fn fetch_did_document(&self, url: Uri) -> Result<String, DidWebError> {
65        let res = self.client.get(url).await?;
66
67        if !res.status().is_success() {
68            return Err(DidWebError::NonSuccessResponse(res.status()));
69        }
70
71        let body = hyper::body::to_bytes(res.into_body()).await?;
72
73        String::from_utf8(body.to_vec()).map_err(|err| err.into())
74    }
75
76    /// Fetches and parses a DID document for the given DID.
77    ///
78    /// This method first parses the DID Web URL format from the given DID and then constructs
79    /// an URI based on the scheme, domain name, and path. It then fetches the DID document and
80    /// parses the response body.
81    ///
82    /// # Arguments
83    ///
84    /// * `did` - The DID to resolve.
85    ///
86    /// # Returns
87    ///
88    /// A `Result` containing the resolved `DIDDocument` or a `DidWebError`.
89    async fn resolver_fetcher(&self, did: &str) -> Result<DIDDocument, DidWebError> {
90        let (path, domain_name) = parse_did_web_url(did).map_err(|err| DidWebError::RepresentationNotSupported(err.to_string()))?;
91
92        // Use HTTP for localhost only during testing
93        let scheme = if domain_name.starts_with("localhost") {
94            Scheme::HTTP
95        } else {
96            Scheme::HTTPS
97        };
98
99        let url = uri::Builder::new()
100            .scheme(scheme)
101            .authority(domain_name)
102            .path_and_query(path)
103            .build()
104            .map_err(|err| DidWebError::RepresentationNotSupported(err.to_string()))?;
105
106        let json_string = self.fetch_did_document(url).await?;
107
108        let did_document = serde_json::from_str(&json_string).map_err(|err| DidWebError::RepresentationNotSupported(err.to_string()))?;
109
110        Ok(did_document)
111    }
112}
113
114/// Parses a DID Web URL and returns the path and domain name.
115///
116/// # Arguments
117///
118/// * `did` - The DID to parse.
119///
120/// # Returns
121///
122/// A `Result` containing the path and domain name or a `DidWebError`.
123fn parse_did_web_url(did: &str) -> Result<(String, String), DidWebError> {
124    let mut parts = did.split(':').peekable();
125    let domain_name = match (parts.next(), parts.next(), parts.next()) {
126        (Some("did"), Some("web"), Some(domain_name)) => domain_name.replacen("%3A", ":", 1),
127        _ => {
128            return Err(DidWebError::InvalidDid("Invalid DID".to_string()));
129        }
130    };
131
132    let mut path = match parts.peek() {
133        Some(_) => parts.collect::<Vec<&str>>().join("/"),
134        None => ".well-known".to_string(),
135    };
136
137    path = format!("/{}/did.json", path);
138
139    Ok((path, domain_name))
140}
141
142#[async_trait]
143impl<C> DIDResolver for DidWeb<C>
144where
145    C: Connect + Send + Sync + Clone + 'static,
146{
147    /// Resolves a DID to a DID document.
148    ///
149    /// # Arguments
150    ///
151    /// * `did` - The DID to resolve.
152    /// * `_options` - The options for DID resolution.
153    ///
154    /// # Returns
155    ///
156    /// A `ResolutionOutput` containing the resolved DID document and metadata.
157    async fn resolve(&self, did: &str, _options: &DIDResolutionOptions) -> ResolutionOutput {
158        let context = Context::SingleString(String::from("https://w3id.org/did-resolution/v1"));
159
160        match self.resolver_fetcher(did).await {
161            Ok(diddoc) => ResolutionOutput {
162                context,
163                did_document: Some(diddoc),
164                did_resolution_metadata: Some(DIDResolutionMetadata {
165                    error: None,
166                    content_type: Some(MediaType::DidLdJson.to_string()),
167                    additional_properties: None,
168                }),
169                did_document_metadata: None,
170                additional_properties: None,
171            },
172            Err(_err) => ResolutionOutput {
173                context,
174                did_document: None,
175                did_resolution_metadata: None,
176                did_document_metadata: None,
177                additional_properties: None,
178            },
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    use hyper::{
188        service::{make_service_fn, service_fn},
189        Body, Request, Response, Server,
190    };
191
192    use serde_json::Value;
193    use std::convert::Infallible;
194    use std::net::SocketAddr;
195
196    async fn mock_server_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
197        const DID_JSON: &str = r#"
198            {"@context": "https://www.w3.org/ns/did/v1",
199            "id": "did:web:localhost",
200                  "verificationMethod": [{
201                  "id": "did:web:localhost#key1",
202                  "type": "Ed25519VerificationKey2018",
203                  "controller": "did:web:localhost",
204                  "publicKeyJwk": {
205                      "key_id": "ed25519-2020-10-18",
206                      "kty": "OKP",
207                      "crv": "Ed25519",
208                      "x": "G80iskrv_nE69qbGLSpeOHJgmV4MKIzsy5l5iT6pCww"
209                  }
210                  }],
211                  "assertionMethod": ["did:web:localhost#key1"]
212            }"#;
213
214        let response = match req.uri().path() {
215            "/.well-known/did.json" | "/user/alice/did.json" => Response::new(Body::from(DID_JSON)),
216            _ => Response::builder().status(404).body(Body::from("Not Found")).unwrap(),
217        };
218
219        Ok(response)
220    }
221
222    async fn create_mock_server(port: u16) -> String {
223        let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(mock_server_handler)) });
224
225        let addr = SocketAddr::from(([127, 0, 0, 1], port));
226        let server = Server::bind(&addr).serve(make_svc);
227
228        tokio::spawn(async move {
229            server.await.unwrap();
230        });
231
232        "localhost".to_string()
233    }
234
235    #[tokio::test]
236    async fn resolves_document() {
237        let port = 3000;
238        let host = create_mock_server(port).await;
239
240        let formatted_string = format!("did:web:{}%3A{}", host, port);
241
242        let did: &str = &formatted_string;
243
244        let did_web_resolver = DidWeb::http();
245        let output: ResolutionOutput = did_web_resolver.resolve(did, &DIDResolutionOptions::default()).await;
246
247        let expected: Value = serde_json::from_str(
248            r#"{
249                "@context": "https://w3id.org/did-resolution/v1",
250                "didDocument": {
251                    "@context": "https://www.w3.org/ns/did/v1",
252                    "assertionMethod": ["did:web:localhost#key1"],
253                    "id": "did:web:localhost",
254                    "verificationMethod": [
255                        {
256                            "controller": "did:web:localhost",
257                            "id": "did:web:localhost#key1",
258                            "publicKeyJwk": {
259                            "crv": "Ed25519",
260                            "kty": "OKP",
261                            "x": "G80iskrv_nE69qbGLSpeOHJgmV4MKIzsy5l5iT6pCww"
262                            },
263                            "type": "Ed25519VerificationKey2018"
264                        }
265                    ]
266                },
267                "didDocumentMetadata": null,
268                "didResolutionMetadata": {
269                    "contentType": "application/did+ld+json"
270                }
271            }"#,
272        )
273        .unwrap();
274
275        assert_eq!(json_canon::to_string(&output).unwrap(), json_canon::to_string(&expected).unwrap());
276    }
277
278    use crate::methods::web::resolver;
279
280    #[test]
281    fn test_parse_did_web_url() {
282        let input_1 = "did:web:w3c-ccg.github.io";
283        let result_1 = resolver::parse_did_web_url(input_1);
284        assert!(result_1.is_ok(), "Expected Ok, got {:?}", result_1);
285        let (path_1, domain_name_1) = result_1.unwrap();
286        assert_eq!(domain_name_1, "w3c-ccg.github.io");
287        assert_eq!(path_1, "/.well-known/did.json");
288
289        let input_2 = "did:web:w3c-ccg.github.io:user:alice";
290        let result_2 = resolver::parse_did_web_url(input_2);
291        assert!(result_2.is_ok(), "Expected Ok, got {:?}", result_2);
292        let (path_2, domain_name_2) = result_2.unwrap();
293        assert_eq!(domain_name_2, "w3c-ccg.github.io");
294        assert_eq!(path_2, "/user/alice/did.json");
295
296        let input_3 = "did:web:example.com%3A3000:user:alice";
297        let result_3 = resolver::parse_did_web_url(input_3);
298        assert!(result_3.is_ok(), "Expected Ok, got {:?}", result_3);
299        let (path_3, domain_name_3) = result_3.unwrap();
300        assert_eq!(domain_name_3, "example.com:3000");
301        assert_eq!(path_3, "/user/alice/did.json");
302    }
303}