git ssb

0+

cel-desktop / dillo-did



Tree: 21cf63b48e3c7f02ebf1d146de15aabd704929cd

Files: 21cf63b48e3c7f02ebf1d146de15aabd704929cd / src / main.rs

10155 bytesRaw
1use async_std::io::BufReader;
2// use async_std::io::BufWriter;
3use async_std::net::TcpListener;
4use async_std::net::TcpStream;
5use async_std::path::Path;
6use async_std::prelude::*;
7use async_std::stream::StreamExt;
8use did_key::DIDKey;
9use did_tezos::DIDTz;
10use did_web::DIDWeb;
11use dpip::Dpip;
12use serde_json::Value;
13use ssi::did::{DIDMethods, Document, Resource};
14use ssi::did_resolve::{
15 dereference as dereference_did_url, Content, DereferencingInputMetadata, HTTPDIDResolver,
16 SeriesResolver,
17};
18use ssi::jsonld::{canonicalize_json_string, is_iri};
19use std::env::{var, VarError};
20use thiserror::Error;
21use tokio::task;
22
23mod dpip;
24
25#[derive(Error, Debug)]
26pub enum DpiError {
27 #[error(transparent)]
28 Dpip(#[from] dpip::DpipParseError),
29 #[error("Missing command tag")]
30 MissingCmd,
31 #[error("Expected auth message but found '{0}'")]
32 ExpectedAuth(String),
33 #[error("Expected auth '{0}' but found '{1}'")]
34 InvalidAuth(String, String),
35 #[error("Missing auth message")]
36 MissingAuthMsg,
37 #[error("Missing URL")]
38 MissingURL,
39 #[error(transparent)]
40 IO(#[from] async_std::io::Error),
41 #[error(transparent)]
42 JSON(#[from] serde_json::Error),
43 #[error(transparent)]
44 Env(#[from] VarError),
45 #[error("Unknown command '{0}'")]
46 UnknownCmd(String),
47}
48
49fn escape_html(s: &str) -> String {
50 s.replace("&", "&")
51 .replace("<", "&lt;")
52 .replace(">", "&gt;")
53 .replace("\"", "&quot;")
54}
55
56// Pretty-print JSON Value as HTML, with IRIs hyperlinked.
57fn 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
109async 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
124async 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
133async 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
142async 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
152async 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
228async 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
237async 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
271async 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
287fn main() {
288 let rt = tokio::runtime::Runtime::new().unwrap();
289 rt.block_on(main_async())
290}
291
292async 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