Files: 0a2159981e51aa2801cd12c60af8d5dac9a253ee / src / main.rs
10155 bytesRaw
1 | use async_std::io::BufReader; |
2 | // use async_std::io::BufWriter; |
3 | use async_std::net::TcpListener; |
4 | use async_std::net::TcpStream; |
5 | use async_std::path::Path; |
6 | use async_std::prelude::*; |
7 | use async_std::stream::StreamExt; |
8 | use did_key::DIDKey; |
9 | use did_tezos::DIDTz; |
10 | use did_web::DIDWeb; |
11 | use dpip::Dpip; |
12 | use serde_json::Value; |
13 | use ssi::did::{DIDMethods, Document, Resource}; |
14 | use ssi::did_resolve::{ |
15 | dereference as dereference_did_url, Content, DereferencingInputMetadata, HTTPDIDResolver, |
16 | SeriesResolver, |
17 | }; |
18 | use ssi::jsonld::{canonicalize_json_string, is_iri}; |
19 | use std::env::{var, VarError}; |
20 | use thiserror::Error; |
21 | use tokio::task; |
22 | |
23 | mod dpip; |
24 | |
25 | |
26 | pub enum DpiError { |
27 | |
28 | Dpip( | dpip::DpipParseError),
29 | |
30 | MissingCmd, |
31 | |
32 | ExpectedAuth(String), |
33 | |
34 | InvalidAuth(String, String), |
35 | |
36 | MissingAuthMsg, |
37 | |
38 | MissingURL, |
39 | |
40 | IO( | async_std::io::Error),
41 | |
42 | JSON( | serde_json::Error),
43 | |
44 | Env( | VarError),
45 | |
46 | UnknownCmd(String), |
47 | } |
48 | |
49 | fn escape_html(s: &str) -> String { |
50 | s.replace("&", "&") |
51 | .replace("<", "<") |
52 | .replace(">", ">") |
53 | .replace("\"", """) |
54 | } |
55 | |
56 | // Pretty-print JSON Value as HTML, with IRIs hyperlinked. |
57 | fn linkify_value(value: &Value, indent: usize) -> String { |
58 | let nl = "\n".to_string() + &" ".repeat(indent); |
59 | let nl1 = "\n".to_string() + &" ".repeat(indent + 1); |
60 | match value { |
61 | Value::Object(object) => { |
62 | let mut string = "{".to_string() + &nl1; |
63 | let mut first = true; |
64 | for (key, value) in object { |
65 | if first { |
66 | first = false; |
67 | } else { |
68 | string.push(','); |
69 | string.push_str(&nl1); |
70 | } |
71 | string += &linkify_value(&Value::String(key.to_string()), indent + 1); |
72 | string += ": "; |
73 | string += &linkify_value(value, indent + 1); |
74 | } |
75 | string + &nl + "}" |
76 | } |
77 | Value::String(string) => { |
78 | if is_iri(&string) { |
79 | format!( |
80 | "\"<a href=\"{}\">{}</a>\"", |
81 | escape_html(string), |
82 | escape_html(canonicalize_json_string(string).trim_matches('"')) |
83 | ) |
84 | } else { |
85 | canonicalize_json_string(string) |
86 | } |
87 | } |
88 | Value::Bool(true) => "true".to_string(), |
89 | Value::Bool(false) => "false".to_string(), |
90 | Value::Number(num) => num.to_string(), |
91 | Value::Array(vec) => { |
92 | let mut string = "[".to_string() + &nl1; |
93 | let mut first = true; |
94 | for value in vec { |
95 | if first { |
96 | first = false; |
97 | } else { |
98 | string.push(','); |
99 | string.push_str(&nl1); |
100 | } |
101 | string += &linkify_value(value, indent + 1); |
102 | } |
103 | string + &nl + "]" |
104 | } |
105 | Value::Null => "null".to_string(), |
106 | } |
107 | } |
108 | |
109 | async fn serve_linkified_object( |
110 | stream: &mut TcpStream, |
111 | url: &str, |
112 | value: &Value, |
113 | ) -> Result<(), DpiError> { |
114 | stream |
115 | .write_all(&Dpip::start_send_page(url).to_string().as_bytes()) |
116 | .await?; |
117 | let html = linkify_value(value, 0); |
118 | stream |
119 | .write_all(format!("Content-Type: text/html\n\n<!doctype html><html><head><title>{}</title><meta charset=\"utf-8\"></head><body><pre>{}</pre></body></html>", escape_html(url), html).as_bytes()) |
120 | .await?; |
121 | Ok(()) |
122 | } |
123 | |
124 | async fn serve_did_document( |
125 | stream: &mut TcpStream, |
126 | url: &str, |
127 | doc: &Document, |
128 | ) -> Result<(), DpiError> { |
129 | let value = serde_json::to_value(doc)?; |
130 | return serve_linkified_object(stream, url, &value).await; |
131 | } |
132 | |
133 | async fn serve_object( |
134 | stream: &mut TcpStream, |
135 | url: &str, |
136 | object: &Resource, |
137 | ) -> Result<(), DpiError> { |
138 | let value = serde_json::to_value(object)?; |
139 | return serve_linkified_object(stream, url, &value).await; |
140 | } |
141 | |
142 | async fn serve_plain(stream: &mut TcpStream, url: &str, body: &str) -> Result<(), DpiError> { |
143 | stream |
144 | .write_all(&Dpip::start_send_page(url).to_string().as_bytes()) |
145 | .await?; |
146 | stream |
147 | .write_all(format!("Content-Type: text/plain\n\n{}", body).as_bytes()) |
148 | .await?; |
149 | Ok(()) |
150 | } |
151 | |
152 | async fn handle_request(stream: &mut TcpStream, url: &str) -> Result<(), DpiError> { |
153 | if !url.starts_with("did:") { |
154 | let tag = Dpip::start_send_page(url); |
155 | stream.write_all(&tag.to_string().as_bytes()).await?; |
156 | stream |
157 | .write_all(b"Content-Type: text/plain\n\nNot Found\n") |
158 | .await?; |
159 | return Ok(()); |
160 | } |
161 | let url = match url.split("#").next() { |
162 | Some(url) => url, |
163 | None => url, |
164 | }; |
165 | |
166 | // Set up the DID resolver |
167 | let mut resolvers = Vec::new(); |
168 | // Built-in resolvable DID methods |
169 | let mut methods = DIDMethods::default(); |
170 | methods.insert(&DIDKey); |
171 | methods.insert(&DIDTz); |
172 | methods.insert(&DIDWeb); |
173 | resolvers.push(methods.to_resolver()); |
174 | // Fallback to resolve over HTTP(S) |
175 | let resolver_url_opt = match var("DID_RESOLVER") { |
176 | Ok(url) => Ok(Some(url)), |
177 | Err(VarError::NotPresent) => Ok(None), |
178 | Err(err) => Err(err), |
179 | }?; |
180 | let fallback_resolver_opt = match resolver_url_opt { |
181 | Some(url) => Some(HTTPDIDResolver::new(&url)), |
182 | None => None, |
183 | }; |
184 | if let Some(fallback_resolver) = &fallback_resolver_opt { |
185 | resolvers.push(fallback_resolver); |
186 | } |
187 | let resolver = SeriesResolver { resolvers }; |
188 | |
189 | let deref_input_meta = DereferencingInputMetadata::default(); |
190 | stream |
191 | .write_all( |
192 | &Dpip::send_status_message("Dereferencing DID URL...") |
193 | .to_string() |
194 | .as_bytes(), |
195 | ) |
196 | .await?; |
197 | let (deref_meta, content, _content_meta) = |
198 | dereference_did_url(&resolver, url, &deref_input_meta).await; |
199 | if let Some(error) = deref_meta.error { |
200 | return serve_plain(stream, url, &error).await; |
201 | } |
202 | let content_type = deref_meta.content_type.unwrap_or_default(); |
203 | match content { |
204 | Content::DIDDocument(did_doc) => { |
205 | return serve_did_document(stream, url, &did_doc).await; |
206 | } |
207 | Content::Object(object) => { |
208 | return serve_object(stream, url, &object).await; |
209 | } |
210 | _ => {} |
211 | } |
212 | // Only send content-types supported by Dillo |
213 | let content_type = match &content_type[..] { |
214 | "text/html" | "image/gif" | "image/png" | "image/jpeg" => content_type, |
215 | _ => "text/plain".to_string(), |
216 | }; |
217 | let content_vec = content.into_vec().unwrap(); |
218 | stream |
219 | .write_all(&Dpip::start_send_page(url).to_string().as_bytes()) |
220 | .await?; |
221 | stream |
222 | .write_all(format!("Content-Type: {}\n\n", content_type).as_bytes()) |
223 | .await?; |
224 | stream.write_all(&content_vec).await?; |
225 | Ok(()) |
226 | } |
227 | |
228 | async fn serve_error(mut stream: TcpStream, url: &str, err: DpiError) -> Result<(), DpiError> { |
229 | let tag = Dpip::start_send_page(url); |
230 | stream.write_all(&tag.to_string().as_bytes()).await?; |
231 | stream |
232 | .write_all(format!("Content-Type: text/plain\n\n{}\n\n{:?}\n", err, err).as_bytes()) |
233 | .await?; |
234 | Ok(()) |
235 | } |
236 | |
237 | async fn handle_client(mut stream: TcpStream, auth: &str) -> Result<(), DpiError> { |
238 | let mut reader = BufReader::new(&stream); |
239 | // Read and validate auth command |
240 | let tag = Dpip::read(&mut reader).await?; |
241 | let cmd = tag.properties.get("cmd").ok_or(DpiError::MissingCmd)?; |
242 | if cmd != "auth" { |
243 | return Err(DpiError::ExpectedAuth(cmd.to_string())); |
244 | } |
245 | let msg = tag.properties.get("msg").ok_or(DpiError::MissingAuthMsg)?; |
246 | if msg != auth { |
247 | return Err(DpiError::InvalidAuth(auth.to_string(), msg.to_string())); |
248 | } |
249 | // Read next command |
250 | let tag = Dpip::read(&mut reader).await?; |
251 | let cmd = tag.properties.get("cmd").ok_or(DpiError::MissingCmd)?; |
252 | match &cmd[..] { |
253 | "DpiBye" => { |
254 | eprintln!("[dillo-did]: Got DpiBye."); |
255 | std::process::exit(0) |
256 | } |
257 | "open_url" => { |
258 | let url = tag.properties.get("url").ok_or(DpiError::MissingURL)?; |
259 | match handle_request(&mut stream, url).await { |
260 | Ok(ok) => Ok(ok), |
261 | Err(err) => serve_error(stream, url, err).await, |
262 | } |
263 | } |
264 | _ => { |
265 | eprintln!("tag: {:?}", tag); |
266 | Err(DpiError::UnknownCmd(cmd.to_string())) |
267 | } |
268 | } |
269 | } |
270 | |
271 | async fn accept_loop(listener: &TcpListener, auth: &str) { |
272 | let mut incoming = listener.incoming(); |
273 | while let Some(stream) = incoming.next().await { |
274 | let stream = stream.unwrap(); |
275 | let auth = auth.to_string(); |
276 | task::spawn(async move { |
277 | match handle_client(stream, &auth).await { |
278 | Ok(ok) => ok, |
279 | Err(err) => { |
280 | eprintln!("[dillo-did]: error: {}", err); |
281 | } |
282 | } |
283 | }); |
284 | } |
285 | } |
286 | |
287 | fn main() { |
288 | let rt = tokio::runtime::Runtime::new().unwrap(); |
289 | rt.block_on(main_async()) |
290 | } |
291 | |
292 | async fn main_async() { |
293 | println!("[dillo-did]: starting"); |
294 | let homedir = var("HOME").unwrap(); |
295 | let keys_path = Path::new(&homedir).join(".dillo/dpid_comm_keys"); |
296 | let keys = async_std::fs::read_to_string(keys_path).await.unwrap(); |
297 | let (_port, auth) = match keys.split(" ").collect::<Vec<&str>>().as_slice() { |
298 | [port_str, auth_str] => ( |
299 | u16::from_str_radix(port_str, 10).unwrap(), |
300 | auth_str.trim().to_string(), |
301 | ), |
302 | _ => panic!("Unable to parse dpid comm keys file"), |
303 | }; |
304 | use std::os::unix::io::AsRawFd; |
305 | use std::os::unix::io::FromRawFd; |
306 | let stdin_fd = std::io::stdin().as_raw_fd(); |
307 | let listener = unsafe { TcpListener::from_raw_fd(stdin_fd) }; |
308 | accept_loop(&listener, &auth).await; |
309 | } |
310 |
Built with git-ssb-web