use async_std::io::BufReader; // use async_std::io::BufWriter; use async_std::net::TcpListener; use async_std::net::TcpStream; use async_std::path::Path; use async_std::prelude::*; use async_std::stream::StreamExt; use did_key::DIDKey; use did_tezos::DIDTz; use did_web::DIDWeb; use dpip::Dpip; use serde_json::Value; use ssi::did::{DIDMethods, Document, Resource}; use ssi::did_resolve::{ dereference as dereference_did_url, Content, DereferencingInputMetadata, HTTPDIDResolver, SeriesResolver, }; use ssi::jsonld::{canonicalize_json_string, is_iri}; use std::env::{var, VarError}; use thiserror::Error; use tokio::task; mod dpip; #[derive(Error, Debug)] pub enum DpiError { #[error(transparent)] Dpip(#[from] dpip::DpipParseError), #[error("Missing command tag")] MissingCmd, #[error("Expected auth message but found '{0}'")] ExpectedAuth(String), #[error("Expected auth '{0}' but found '{1}'")] InvalidAuth(String, String), #[error("Missing auth message")] MissingAuthMsg, #[error("Missing URL")] MissingURL, #[error(transparent)] IO(#[from] async_std::io::Error), #[error(transparent)] JSON(#[from] serde_json::Error), #[error(transparent)] Env(#[from] VarError), #[error("Unknown command '{0}'")] UnknownCmd(String), } fn escape_html(s: &str) -> String { s.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) } // Pretty-print JSON Value as HTML, with IRIs hyperlinked. fn linkify_value(value: &Value, indent: usize) -> String { let nl = "\n".to_string() + &" ".repeat(indent); let nl1 = "\n".to_string() + &" ".repeat(indent + 1); match value { Value::Object(object) => { let mut string = "{".to_string() + &nl1; let mut first = true; for (key, value) in object { if first { first = false; } else { string.push(','); string.push_str(&nl1); } string += &linkify_value(&Value::String(key.to_string()), indent + 1); string += ": "; string += &linkify_value(value, indent + 1); } string + &nl + "}" } Value::String(string) => { if is_iri(&string) { format!( "\"{}\"", escape_html(string), escape_html(canonicalize_json_string(string).trim_matches('"')) ) } else { canonicalize_json_string(string) } } Value::Bool(true) => "true".to_string(), Value::Bool(false) => "false".to_string(), Value::Number(num) => num.to_string(), Value::Array(vec) => { let mut string = "[".to_string() + &nl1; let mut first = true; for value in vec { if first { first = false; } else { string.push(','); string.push_str(&nl1); } string += &linkify_value(value, indent + 1); } string + &nl + "]" } Value::Null => "null".to_string(), } } async fn serve_linkified_object( stream: &mut TcpStream, url: &str, value: &Value, ) -> Result<(), DpiError> { stream .write_all(&Dpip::start_send_page(url).to_string().as_bytes()) .await?; let html = linkify_value(value, 0); stream .write_all(format!("Content-Type: text/html\n\n{}
{}
", escape_html(url), html).as_bytes()) .await?; Ok(()) } async fn serve_did_document( stream: &mut TcpStream, url: &str, doc: &Document, ) -> Result<(), DpiError> { let value = serde_json::to_value(doc)?; return serve_linkified_object(stream, url, &value).await; } async fn serve_object( stream: &mut TcpStream, url: &str, object: &Resource, ) -> Result<(), DpiError> { let value = serde_json::to_value(object)?; return serve_linkified_object(stream, url, &value).await; } async fn serve_plain(stream: &mut TcpStream, url: &str, body: &str) -> Result<(), DpiError> { stream .write_all(&Dpip::start_send_page(url).to_string().as_bytes()) .await?; stream .write_all(format!("Content-Type: text/plain\n\n{}", body).as_bytes()) .await?; Ok(()) } async fn handle_request(stream: &mut TcpStream, url: &str) -> Result<(), DpiError> { if !url.starts_with("did:") { let tag = Dpip::start_send_page(url); stream.write_all(&tag.to_string().as_bytes()).await?; stream .write_all(b"Content-Type: text/plain\n\nNot Found\n") .await?; return Ok(()); } let url = match url.split("#").next() { Some(url) => url, None => url, }; // Set up the DID resolver let mut resolvers = Vec::new(); // Built-in resolvable DID methods let mut methods = DIDMethods::default(); methods.insert(&DIDKey); methods.insert(&DIDTz); methods.insert(&DIDWeb); resolvers.push(methods.to_resolver()); // Fallback to resolve over HTTP(S) let resolver_url_opt = match var("DID_RESOLVER") { Ok(url) => Ok(Some(url)), Err(VarError::NotPresent) => Ok(None), Err(err) => Err(err), }?; let fallback_resolver_opt = match resolver_url_opt { Some(url) => Some(HTTPDIDResolver::new(&url)), None => None, }; if let Some(fallback_resolver) = &fallback_resolver_opt { resolvers.push(fallback_resolver); } let resolver = SeriesResolver { resolvers }; let deref_input_meta = DereferencingInputMetadata::default(); stream .write_all( &Dpip::send_status_message("Dereferencing DID URL...") .to_string() .as_bytes(), ) .await?; let (deref_meta, content, _content_meta) = dereference_did_url(&resolver, url, &deref_input_meta).await; if let Some(error) = deref_meta.error { return serve_plain(stream, url, &error).await; } let content_type = deref_meta.content_type.unwrap_or_default(); match content { Content::DIDDocument(did_doc) => { return serve_did_document(stream, url, &did_doc).await; } Content::Object(object) => { return serve_object(stream, url, &object).await; } _ => {} } // Only send content-types supported by Dillo let content_type = match &content_type[..] { "text/html" | "image/gif" | "image/png" | "image/jpeg" => content_type, _ => "text/plain".to_string(), }; let content_vec = content.into_vec().unwrap(); stream .write_all(&Dpip::start_send_page(url).to_string().as_bytes()) .await?; stream .write_all(format!("Content-Type: {}\n\n", content_type).as_bytes()) .await?; stream.write_all(&content_vec).await?; Ok(()) } async fn serve_error(mut stream: TcpStream, url: &str, err: DpiError) -> Result<(), DpiError> { let tag = Dpip::start_send_page(url); stream.write_all(&tag.to_string().as_bytes()).await?; stream .write_all(format!("Content-Type: text/plain\n\n{}\n\n{:?}\n", err, err).as_bytes()) .await?; Ok(()) } async fn handle_client(mut stream: TcpStream, auth: &str) -> Result<(), DpiError> { let mut reader = BufReader::new(&stream); // Read and validate auth command let tag = Dpip::read(&mut reader).await?; let cmd = tag.properties.get("cmd").ok_or(DpiError::MissingCmd)?; if cmd != "auth" { return Err(DpiError::ExpectedAuth(cmd.to_string())); } let msg = tag.properties.get("msg").ok_or(DpiError::MissingAuthMsg)?; if msg != auth { return Err(DpiError::InvalidAuth(auth.to_string(), msg.to_string())); } // Read next command let tag = Dpip::read(&mut reader).await?; let cmd = tag.properties.get("cmd").ok_or(DpiError::MissingCmd)?; match &cmd[..] { "DpiBye" => { eprintln!("[dillo-did]: Got DpiBye."); std::process::exit(0) } "open_url" => { let url = tag.properties.get("url").ok_or(DpiError::MissingURL)?; match handle_request(&mut stream, url).await { Ok(ok) => Ok(ok), Err(err) => serve_error(stream, url, err).await, } } _ => { eprintln!("tag: {:?}", tag); Err(DpiError::UnknownCmd(cmd.to_string())) } } } async fn accept_loop(listener: &TcpListener, auth: &str) { let mut incoming = listener.incoming(); while let Some(stream) = incoming.next().await { let stream = stream.unwrap(); let auth = auth.to_string(); task::spawn(async move { match handle_client(stream, &auth).await { Ok(ok) => ok, Err(err) => { eprintln!("[dillo-did]: error: {}", err); } } }); } } fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(main_async()) } async fn main_async() { println!("[dillo-did]: starting"); let homedir = var("HOME").unwrap(); let keys_path = Path::new(&homedir).join(".dillo/dpid_comm_keys"); let keys = async_std::fs::read_to_string(keys_path).await.unwrap(); let (_port, auth) = match keys.split(" ").collect::>().as_slice() { [port_str, auth_str] => ( u16::from_str_radix(port_str, 10).unwrap(), auth_str.trim().to_string(), ), _ => panic!("Unable to parse dpid comm keys file"), }; use std::os::unix::io::AsRawFd; use std::os::unix::io::FromRawFd; let stdin_fd = std::io::stdin().as_raw_fd(); let listener = unsafe { TcpListener::from_raw_fd(stdin_fd) }; accept_loop(&listener, &auth).await; }