From 21cf63b48e3c7f02ebf1d146de15aabd704929cd Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Sun, 7 Feb 2021 22:38:24 -0500 Subject: [PATCH] Init --- .gitignore | 1 + Cargo.toml | 27 +++++ LICENSE | 201 ++++++++++++++++++++++++++++++++++ Makefile | 40 +++++++ README.md | 50 +++++++++ src/dpip.rs | 228 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 309 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 856 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/dpip.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ccdf833 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "dillo-did" +version = "0.1.0" +authors = ["Charles E. Lehner "] +license = "Apache-2.0" +edition = "2018" + +[dependencies] +tokio = { version = "1.0", features = ["rt-multi-thread"] } +# didkit = { git = "https://github.com/spruceid/didkit/", rev = "c79e92f32ca1f07f2fadec0bb0860089e5aa9f7d" } +ssi = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e", features = ["http-did"] } +did-key = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e" } +did-web = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e" } +did-tezos = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e" } +thiserror = "1.0" +async-std = "1.9" +serde_json = "1.0" + +[[bin]] +name = "did-dpi" +path = "src/main.rs" + +# Optimize release for small binary size +[profile.release] +opt-level = 'z' +lto = true +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..626f39b --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.POSIX: +DILLO_DIR=~/.dillo +DPI_DIR=$(DILLO_DIR)/dpi +DPIDRC=$(DILLO_DIR)/dpidrc +PROTO=did +NAME=did +BIN_NAME=did.dpi +BIN=target/debug/did-dpi +SRC = src/main.rs + +all: $(BIN) + +test: + @#cargo test + cargo build + -dpidc stop + @#timeout 1 dillo did:asdf + +$(BIN): $(SRC) + cargo build + +install: $(BIN) install-proto + mkdir -p $(DPI_DIR)/$(NAME) + cp -f $(BIN) $(DPI_DIR)/$(NAME)/$(BIN_NAME) + +link: $(BIN) install-proto + mkdir -p $(DPI_DIR)/$(NAME) + ln -frs $(BIN) $(DPI_DIR)/$(NAME)/$(BIN_NAME) + +install-proto: + test -e $(DPIDRC) || cp /etc/dillo/dpidrc $(DPIDRC) + grep -qF 'proto.$(PROTO)=$(NAME)/$(BIN_NAME)' $(DPIDRC) ||\ + echo 'proto.$(PROTO)=$(NAME)/$(BIN_NAME)' >> $(DPIDRC) + +clean: + cargo clean + +uninstall: + rm -f $(DPI_DIR)/$(NAME)/$(BIN_NAME) + test -s $(DPIDRC) && sed -i~ '/proto\.$(PROTO)=$(NAME)\/$(BIN_NAME)/d' $(DPIDRC) diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b2758e --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# dillo-did + +[Dillo][] plugin for [did][] ([Decentralized Identifier][did-core]) URIs. Written in [Rust][]. Requires [Rust nightly][]. Build using [Cargo][]. Uses [`ssi`][]/[DIDKit][]. + +## Screenshot + +![dillo-did-key.png](&WqNWKdBcMwCAy4SatQOZ5XoaGu1B4IpUQcZrfFbjJTk=.sha256) + +## Install from [git-ssb][] +``` +git clone ssb://%T8QyhgamH7fmOiiYV/iWiSRmTDdxXje77teBB2yijGU=.sha256 dillo-did +cd dillo-did +make install +dpidc stop +``` +You can also use `make link` instead of `make install`, to install via a symlink. + +## Built-in supported [DID methods][] + +- [did:key][] +- [did:web][] +- `did:tz` + +## Fallback resolver for additional DID methods + +Set environmental variable `DID_RESOLVER` to a URL for a [DID Resolver HTTP(S) endpoint][did-http] - e.g. to an instance of [Universal Resolver][]. + +## TODO + +- Make external resolver URL(s) configurable via a config file +- Render DID documents more nicely + +## License + +Apache License, Version 2.0 + +[Dillo]: https://www.dillo.org/ +[Rust]: https://www.rust-lang.org/ +[Rust nightly]: https://doc.rust-lang.org/stable/book/appendix-07-nightly-rust.html +[Cargo]: https://github.com/rust-lang/cargo +[did]: https://www.iana.org/assignments/uri-schemes/prov/did +[did-core]: https://www.w3.org/TR/did-core/ +[git-ssb]: %n92DiQh7ietE+R+X/I403LQoyf2DtR3WQfCkDKlheQU=.sha256 +[DIDKit]: https://github.com/spruceid/didkit +[`ssi`]: https://github.com/spruceid/ssi +[did:key]: https://w3c-ccg.github.io/did-method-key/ +[did:web]: https://w3c-ccg.github.io/did-method-web/ +[did-http]: https://w3c-ccg.github.io/did-resolution/#bindings-https +[DID methods]: https://w3c.github.io/did-core/#methods +[Universal Resolver]: https://github.com/decentralized-identity/universal-registrar/ diff --git a/src/dpip.rs b/src/dpip.rs new file mode 100644 index 0000000..8f53d3e --- /dev/null +++ b/src/dpip.rs @@ -0,0 +1,228 @@ +use async_std::io::Bytes; +use async_std::io::Cursor; +use async_std::io::Read; +use async_std::io::ReadExt; +use async_std::stream::StreamExt; +use async_std::task::block_on; +use core::fmt::Display; +use core::fmt::Formatter; +use core::str::FromStr; +use std::collections::BTreeMap; +use std::result::Result; +use thiserror::Error; + +#[derive(Debug, Clone, Default)] +pub struct Dpip { + pub name: Option, + pub properties: BTreeMap, +} + +#[derive(Error, Debug)] +pub enum DpipParseError { + #[error("Invalid tag start character '{0}'")] + InvalidStart(char), + #[error("Invalid tag end character '{0}'")] + InvalidEnd(char), + #[error("Invalid value start character '{0}'")] + InvalidValueStart(char), + #[error("Tag value expected escaped quote or end but found '{0}'")] + InvalidValueEnd(char), + #[error("Unexpected space in key")] + InvalidKeySpace, + #[error("Unexpected quote in key")] + InvalidKeyQuote, + #[error("Stream ended before reading tag")] + EOF, + #[error(transparent)] + IO(#[from] async_std::io::Error), + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), +} + +async fn read_value(bytes: &mut Bytes) -> Result +where + T: Read + Unpin, +{ + let mut value_bytes = Vec::new(); + match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b'\'' => {} + byte => return Err(DpipParseError::InvalidValueStart(byte.into())), + } + while let Some(b) = match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b'\'' => match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b'\'' => Some(b'\''), + b' ' => None, + b => return Err(DpipParseError::InvalidValueEnd(b.into())), + }, + b => Some(b), + } { + value_bytes.push(b); + } + let name = String::from_utf8(value_bytes)?; + Ok(name) +} + +async fn read_initial_key_value( + bytes: &mut Bytes, +) -> Result<(String, Option), DpipParseError> +where + T: Read + Unpin, +{ + let mut name_bytes = Vec::new(); + while let Some(b) = match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b' ' => { + let name = String::from_utf8(name_bytes)?; + return Ok((name, None)); + } + b'=' => { + let name = String::from_utf8(name_bytes)?; + let value = read_value(bytes).await?; + return Ok((name, Some(value))); + } + b => Some(b), + } { + name_bytes.push(b); + } + Err(DpipParseError::EOF) +} + +async fn read_key_value(bytes: &mut Bytes) -> Result, DpipParseError> +where + T: Read + Unpin, +{ + let mut key_bytes = Vec::new(); + while let Some(b) = match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b'\'' => { + if key_bytes.is_empty() { + return Ok(None); + } else { + return Err(DpipParseError::InvalidKeyQuote); + } + } + b' ' => { + return Err(DpipParseError::InvalidKeySpace); + } + b'=' => None, + b => Some(b), + } { + key_bytes.push(b); + } + let key = String::from_utf8(key_bytes)?; + let value = read_value(bytes).await?; + Ok(Some((key, value))) +} + +impl Dpip { + /// Create a dpip command with the given command name + pub fn cmd(cmd: &str) -> Self { + let mut dpip = Self::default(); + dpip.properties.insert("cmd".to_string(), cmd.to_string()); + dpip + } + + /// Create a dpip command for serving a page + pub fn start_send_page(url: &str) -> Self { + let mut tag = Dpip::cmd("start_send_page"); + tag.properties.insert("url".to_string(), url.to_string()); + tag + } + + /// Create a dpip command for sending a status message + pub fn send_status_message(msg: &str) -> Self { + let mut tag = Dpip::cmd("send_status_message"); + tag.properties.insert("msg".to_string(), msg.to_string()); + tag + } + + /// Read a dpip tag. + /// + /// Format (from dillo/dpip/dpip.c): + /// ``` + /// "<"[*alpha] *("="QuoteQuote) " "Quote">" + /// ``` + pub async fn read(reader: &mut T) -> Result + where + T: Read + Unpin, + { + let mut map = BTreeMap::new(); + let mut bytes = reader.bytes(); + match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b'<' => {} + byte => return Err(DpipParseError::InvalidStart(byte.into())), + } + let tag_name = match read_initial_key_value(&mut bytes).await? { + (key, Some(value)) => { + map.insert(key, value); + None + } + (key, None) => Some(key), + }; + while let Some((key, value)) = read_key_value(&mut bytes).await? { + map.insert(key, value); + } + match bytes.next().await.ok_or(DpipParseError::EOF)?? { + b'>' => {} + byte => return Err(DpipParseError::InvalidEnd(byte.into())), + } + + Ok(Dpip { + name: tag_name, + properties: map, + }) + } +} + +impl FromStr for Dpip { + type Err = DpipParseError; + fn from_str(tag: &str) -> std::result::Result { + let mut reader = Cursor::new(tag.as_bytes().to_vec()); + block_on(Dpip::read(&mut reader)) + } +} + +impl Display for Dpip { + fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "<")?; + if let Some(name) = &self.name { + write!(f, "{} ", name)?; + } + for (key, value) in &self.properties { + write!(f, "{}='{}' ", key, value.replace("'", "''"))?; + } + write!(f, "'>") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dpip() { + block_on(dpip_async()); + } + async fn dpip_async() { + let tags = b"' '>"; + let mut reader = Cursor::new(tags.to_vec()); + let tag = Dpip::read(&mut reader).await.unwrap(); + assert_eq!(tag.to_string(), ""); + let tag = Dpip::read(&mut reader).await.unwrap(); + assert_eq!(tag.to_string(), ""); + let tag = Dpip::read(&mut reader).await.unwrap(); + assert_eq!(tag.to_string(), "' '>"); + let tag = Dpip::read(&mut reader).await.unwrap(); + assert_eq!(tag.to_string(), ""); + + let tag: Dpip = "".parse().unwrap(); + assert_eq!(tag.properties["key"], "wouldn't it be nice"); + + let tags = b""; + let mut reader = Cursor::new(tags.to_vec()); + let tag = Dpip::read(&mut reader).await.unwrap(); + assert_eq!(tag.properties["a"], "isn't that='cool' yes"); + + let tags = b"' '>"; + let mut reader = Cursor::new(tags.to_vec()); + Dpip::read(&mut reader).await.unwrap_err(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4ec4693 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,309 @@ +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; +}