Skip to main content

ssi_dids_core/resolution/
http.rs

1use iref::{Uri, UriBuf};
2use reqwest::{header, StatusCode};
3
4use crate::{
5    document::{self, representation::MediaType},
6    DIDResolver, DID,
7};
8
9use super::{Error, Metadata, Output};
10
11pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
12
13/// A DID Resolver implementing a client for the [DID Resolution HTTP(S)
14/// Binding](https://w3c-ccg.github.io/did-resolution/#bindings-https).
15#[derive(Debug, Clone)]
16pub struct HTTPDIDResolver {
17    /// HTTP(S) URL for DID resolver HTTP(S) endpoint.
18    endpoint: UriBuf,
19}
20
21impl HTTPDIDResolver {
22    /// Construct a new HTTP DID Resolver with a given [endpoint][Self::endpoint] URL.
23    pub fn new(url: &Uri) -> Self {
24        Self {
25            endpoint: url.to_owned(),
26        }
27    }
28
29    pub fn endpoint(&self) -> &Uri {
30        &self.endpoint
31    }
32}
33
34#[derive(Debug, thiserror::Error)]
35pub enum InternalError {
36    #[error("unable to initialize HTTP client")]
37    Initialization,
38
39    #[error("HTTP error: {0}")]
40    Reqwest(reqwest::Error),
41
42    #[error("HTTP server returned error code {0}")]
43    Error(StatusCode),
44
45    #[error("missing content-type header")]
46    MissingContentType,
47
48    #[error("content type mismatch")]
49    ContentTypeMismatch,
50
51    #[error("invalid content type")]
52    InvalidContentType,
53}
54
55impl DIDResolver for HTTPDIDResolver {
56    /// Resolve a DID over HTTP(S), using the [DID Resolution HTTP(S) Binding](https://w3c-ccg.github.io/did-resolution/#bindings-https).
57    async fn resolve_representation<'a>(
58        &'a self,
59        did: &'a DID,
60        options: super::Options,
61    ) -> Result<Output<Vec<u8>>, Error> {
62        let query = serde_urlencoded::to_string(&options.parameters).unwrap();
63
64        let did_urlencoded =
65            percent_encoding::utf8_percent_encode(did, percent_encoding::CONTROLS).to_string();
66
67        let mut url = self.endpoint.to_string() + &did_urlencoded;
68        if !query.is_empty() {
69            url.push('?');
70            url.push_str(&query)
71        }
72
73        let url: reqwest::Url = url.parse().unwrap();
74
75        let client = reqwest::Client::builder()
76            .build()
77            .map_err(|_| Error::internal(InternalError::Initialization))?;
78
79        let mut request = client.get(url);
80        if let Some(accept) = options.accept {
81            request = request.header("Accept", accept.to_string());
82        }
83
84        let response = request
85            .header("User-Agent", USER_AGENT)
86            .send()
87            .await
88            .map_err(|e| Error::internal(InternalError::Reqwest(e)))?;
89
90        match response.status() {
91            StatusCode::OK => {
92                let content_type: Option<String> =
93                    match (response.headers().get(header::CONTENT_TYPE), options.accept) {
94                        (Some(content_type), Some(accept)) => {
95                            if content_type == accept.name() {
96                                Some(accept.name().to_string())
97                            } else {
98                                return Err(Error::internal(InternalError::ContentTypeMismatch));
99                            }
100                        }
101                        (Some(content_type), None) => Some(
102                            content_type
103                                .to_str()
104                                .map_err(|_| Error::internal(InternalError::InvalidContentType))?
105                                .to_string(),
106                        ),
107                        (None, Some(_)) => {
108                            return Err(Error::internal(InternalError::MissingContentType))
109                        }
110                        (None, None) => None,
111                    };
112
113                Ok(Output::new(
114                    response
115                        .bytes()
116                        .await
117                        .map_err(|e| Error::internal(InternalError::Reqwest(e)))?
118                        .to_vec(),
119                    document::Metadata::default(),
120                    Metadata::from_content_type(content_type),
121                ))
122            }
123            StatusCode::NOT_FOUND => Err(Error::NotFound),
124            StatusCode::NOT_IMPLEMENTED => {
125                Err(Error::MethodNotSupported(did.method_name().to_string()))
126            }
127            StatusCode::VARIANT_ALSO_NEGOTIATES => Err(Error::RepresentationNotSupported(
128                options
129                    .accept
130                    .map(MediaType::into_name)
131                    .unwrap_or_default()
132                    .to_string(),
133            )),
134            error_code => Err(Error::internal(InternalError::Error(error_code))),
135        }
136    }
137}