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
15pub struct DidWeb<C>
17where
18 C: Connect + Send + Sync + Clone + 'static,
19{
20 client: Client<C>,
21}
22
23impl DidWeb<HttpConnector> {
24 #[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 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 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 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 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
114fn 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 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}