1use async_trait::async_trait;
2use std::default::Default;
3
4use ssi_dids::did_resolve::{
5 DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID,
6 TYPE_DID_LD_JSON,
7};
8use ssi_dids::{DIDMethod, Document};
9pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
10
11const TOR_SOCKS_PORT: usize = 9050;
12
13#[non_exhaustive]
17#[derive(Clone)]
18pub struct DIDOnion {
19 pub proxy_url: String,
20}
21
22impl DIDOnion {
23 fn with_port(port: usize) -> Self {
24 Self {
25 proxy_url: format!("socks5h://127.0.0.1:{}", port),
26 }
27 }
28}
29
30impl Default for DIDOnion {
31 fn default() -> Self {
32 Self::with_port(TOR_SOCKS_PORT)
33 }
34}
35
36fn did_onion_url(did: &str) -> Result<String, ResolutionMetadata> {
37 let mut parts = did.split(':').peekable();
38 let onion_address = match (parts.next(), parts.next(), parts.next()) {
39 (Some("did"), Some("onion"), Some(domain_name)) => domain_name,
40 _ => {
41 return Err(ResolutionMetadata::from_error(ERROR_INVALID_DID));
42 }
43 };
44 for c in onion_address.chars() {
45 if c == '.' || c == ':' {
47 return Err(ResolutionMetadata::from_error(ERROR_INVALID_DID));
48 }
49 }
50 let path = match parts.peek() {
51 Some(_) => parts.collect::<Vec<&str>>().join("/"),
52 None => ".well-known".to_string(),
53 };
54 let url = format!("http://{}.onion/{}/did.json", onion_address, path);
55 Ok(url)
56}
57
58#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
59#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
60impl DIDResolver for DIDOnion {
61 async fn resolve(
62 &self,
63 did: &str,
64 input_metadata: &ResolutionInputMetadata,
65 ) -> (
66 ResolutionMetadata,
67 Option<Document>,
68 Option<DocumentMetadata>,
69 ) {
70 let (mut res_meta, doc_data, doc_meta_opt) =
71 self.resolve_representation(did, input_metadata).await;
72 let doc_opt = if doc_data.is_empty() {
73 None
74 } else {
75 match serde_json::from_slice(&doc_data) {
76 Ok(doc) => doc,
77 Err(err) => {
78 return (
79 ResolutionMetadata::from_error(
80 &("JSON Error: ".to_string() + &err.to_string()),
81 ),
82 None,
83 None,
84 )
85 }
86 }
87 };
88 res_meta.content_type = None;
91 (res_meta, doc_opt, doc_meta_opt)
92 }
93
94 async fn resolve_representation(
95 &self,
96 did: &str,
97 input_metadata: &ResolutionInputMetadata,
98 ) -> (ResolutionMetadata, Vec<u8>, Option<DocumentMetadata>) {
99 let url = match did_onion_url(did) {
100 Err(meta) => return (meta, Vec::new(), None),
101 Ok(url) => url,
102 };
103
104 let mut headers = reqwest::header::HeaderMap::new();
105 headers.insert(
106 reqwest::header::USER_AGENT,
107 reqwest::header::HeaderValue::from_static(USER_AGENT),
108 );
109
110 let mut client_builder = reqwest::Client::builder().default_headers(headers);
111 #[cfg(not(target_arch = "wasm32"))]
112 match reqwest::Proxy::all(&self.proxy_url) {
113 Ok(proxy) => {
114 client_builder = client_builder.proxy(proxy);
115 }
116 Err(err) => {
117 return (
118 ResolutionMetadata::from_error(&format!("Error constructing proxy: {}", err)),
119 Vec::new(),
120 None,
121 )
122 }
123 };
124 let client = match client_builder.build() {
125 Ok(c) => c,
126 Err(err) => {
127 return (
128 ResolutionMetadata::from_error(&format!("Error building HTTP client: {}", err)),
129 Vec::new(),
130 None,
131 )
132 }
133 };
134 let accept = input_metadata
135 .accept
136 .clone()
137 .unwrap_or_else(|| "application/json".to_string());
138 let resp = match client.get(&url).header("Accept", accept).send().await {
139 Ok(req) => req,
140 Err(err) => {
141 return (
142 ResolutionMetadata::from_error(&format!(
143 "Error sending HTTP request : {}",
144 err
145 )),
146 Vec::new(),
147 None,
148 )
149 }
150 };
151 match resp.error_for_status_ref() {
152 Ok(_) => (),
153 Err(err) => {
154 return (
155 ResolutionMetadata::from_error(&err.to_string()),
156 Vec::new(),
157 Some(DocumentMetadata::default()),
158 )
159 }
160 };
161 let doc_representation = match resp.bytes().await {
162 Ok(bytes) => bytes.to_vec(),
163 Err(err) => {
164 return (
165 ResolutionMetadata::from_error(
166 &("Error reading HTTP response: ".to_string() + &err.to_string()),
167 ),
168 Vec::new(),
169 None,
170 )
171 }
172 };
173 (
175 ResolutionMetadata {
176 error: None,
177 content_type: Some(TYPE_DID_LD_JSON.to_string()),
178 property_set: None,
179 },
180 doc_representation,
181 Some(DocumentMetadata::default()),
182 )
183 }
184}
185
186impl DIDMethod for DIDOnion {
187 fn name(&self) -> &'static str {
188 "onion"
189 }
190
191 fn to_resolver(&self) -> &dyn DIDResolver {
192 self
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[async_std::test]
201 async fn parse_did_onion() {
202 assert_eq!(
203 did_onion_url("did:onion:fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid").unwrap(),
204 "http://fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid.onion/.well-known/did.json"
205 );
206 assert_eq!(
207 did_onion_url("did:onion:fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid:user:alice").unwrap(),
208 "http://fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid.onion/user/alice/did.json"
209 );
210 assert_eq!(
211 did_onion_url(
212 "did:onion:fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid:u:bob"
213 )
214 .unwrap(),
215 "http://fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid.onion/u/bob/did.json"
216 );
217 }
218
219 const TORGAP_DEMO_DID: &str =
220 "did:onion:fscst5exmlmr262byztwz4kzhggjlzumvc2ndvgytzoucr2tkgxf7mid";
221
222 #[tokio::test]
223 #[ignore]
224 async fn did_onion_resolve_live() {
225 let (res_meta, doc_opt, _doc_meta) = DIDOnion::default()
226 .resolve(TORGAP_DEMO_DID, &ResolutionInputMetadata::default())
227 .await;
228 assert_eq!(res_meta.error, None);
229 assert!(doc_opt.is_some());
230 }
231
232 }