Commit 21cf63b48e3c7f02ebf1d146de15aabd704929cd
Init
Charles E. Lehner committed on 2/8/2021, 3:39:19 AMFiles changed
.gitignore | added |
Cargo.toml | added |
LICENSE | added |
Makefile | added |
README.md | added |
src/dpip.rs | added |
src/main.rs | added |
Cargo.toml | ||
---|---|---|
@@ -1,0 +1,27 @@ | ||
1 … | +[package] | |
2 … | +name = "dillo-did" | |
3 … | +version = "0.1.0" | |
4 … | +authors = ["Charles E. Lehner <cel@celehner.com>"] | |
5 … | +license = "Apache-2.0" | |
6 … | +edition = "2018" | |
7 … | + | |
8 … | +[dependencies] | |
9 … | +tokio = { version = "1.0", features = ["rt-multi-thread"] } | |
10 … | +# didkit = { git = "https://github.com/spruceid/didkit/", rev = "c79e92f32ca1f07f2fadec0bb0860089e5aa9f7d" } | |
11 … | +ssi = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e", features = ["http-did"] } | |
12 … | +did-key = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e" } | |
13 … | +did-web = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e" } | |
14 … | +did-tezos = { git = "https://github.com/spruceid/ssi/", rev = "69255be2836475549bd366fd1ef7e8168a3ac52e" } | |
15 … | +thiserror = "1.0" | |
16 … | +async-std = "1.9" | |
17 … | +serde_json = "1.0" | |
18 … | + | |
19 … | +[[bin]] | |
20 … | +name = "did-dpi" | |
21 … | +path = "src/main.rs" | |
22 … | + | |
23 … | +# Optimize release for small binary size | |
24 … | +[profile.release] | |
25 … | +opt-level = 'z' | |
26 … | +lto = true | |
27 … | +codegen-units = 1 |
LICENSE | ||
---|---|---|
@@ -1,0 +1,201 @@ | ||
1 … | + Apache License | |
2 … | + Version 2.0, January 2004 | |
3 … | + http://www.apache.org/licenses/ | |
4 … | + | |
5 … | + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
6 … | + | |
7 … | + 1. Definitions. | |
8 … | + | |
9 … | + "License" shall mean the terms and conditions for use, reproduction, | |
10 … | + and distribution as defined by Sections 1 through 9 of this document. | |
11 … | + | |
12 … | + "Licensor" shall mean the copyright owner or entity authorized by | |
13 … | + the copyright owner that is granting the License. | |
14 … | + | |
15 … | + "Legal Entity" shall mean the union of the acting entity and all | |
16 … | + other entities that control, are controlled by, or are under common | |
17 … | + control with that entity. For the purposes of this definition, | |
18 … | + "control" means (i) the power, direct or indirect, to cause the | |
19 … | + direction or management of such entity, whether by contract or | |
20 … | + otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
21 … | + outstanding shares, or (iii) beneficial ownership of such entity. | |
22 … | + | |
23 … | + "You" (or "Your") shall mean an individual or Legal Entity | |
24 … | + exercising permissions granted by this License. | |
25 … | + | |
26 … | + "Source" form shall mean the preferred form for making modifications, | |
27 … | + including but not limited to software source code, documentation | |
28 … | + source, and configuration files. | |
29 … | + | |
30 … | + "Object" form shall mean any form resulting from mechanical | |
31 … | + transformation or translation of a Source form, including but | |
32 … | + not limited to compiled object code, generated documentation, | |
33 … | + and conversions to other media types. | |
34 … | + | |
35 … | + "Work" shall mean the work of authorship, whether in Source or | |
36 … | + Object form, made available under the License, as indicated by a | |
37 … | + copyright notice that is included in or attached to the work | |
38 … | + (an example is provided in the Appendix below). | |
39 … | + | |
40 … | + "Derivative Works" shall mean any work, whether in Source or Object | |
41 … | + form, that is based on (or derived from) the Work and for which the | |
42 … | + editorial revisions, annotations, elaborations, or other modifications | |
43 … | + represent, as a whole, an original work of authorship. For the purposes | |
44 … | + of this License, Derivative Works shall not include works that remain | |
45 … | + separable from, or merely link (or bind by name) to the interfaces of, | |
46 … | + the Work and Derivative Works thereof. | |
47 … | + | |
48 … | + "Contribution" shall mean any work of authorship, including | |
49 … | + the original version of the Work and any modifications or additions | |
50 … | + to that Work or Derivative Works thereof, that is intentionally | |
51 … | + submitted to Licensor for inclusion in the Work by the copyright owner | |
52 … | + or by an individual or Legal Entity authorized to submit on behalf of | |
53 … | + the copyright owner. For the purposes of this definition, "submitted" | |
54 … | + means any form of electronic, verbal, or written communication sent | |
55 … | + to the Licensor or its representatives, including but not limited to | |
56 … | + communication on electronic mailing lists, source code control systems, | |
57 … | + and issue tracking systems that are managed by, or on behalf of, the | |
58 … | + Licensor for the purpose of discussing and improving the Work, but | |
59 … | + excluding communication that is conspicuously marked or otherwise | |
60 … | + designated in writing by the copyright owner as "Not a Contribution." | |
61 … | + | |
62 … | + "Contributor" shall mean Licensor and any individual or Legal Entity | |
63 … | + on behalf of whom a Contribution has been received by Licensor and | |
64 … | + subsequently incorporated within the Work. | |
65 … | + | |
66 … | + 2. Grant of Copyright License. Subject to the terms and conditions of | |
67 … | + this License, each Contributor hereby grants to You a perpetual, | |
68 … | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
69 … | + copyright license to reproduce, prepare Derivative Works of, | |
70 … | + publicly display, publicly perform, sublicense, and distribute the | |
71 … | + Work and such Derivative Works in Source or Object form. | |
72 … | + | |
73 … | + 3. Grant of Patent License. Subject to the terms and conditions of | |
74 … | + this License, each Contributor hereby grants to You a perpetual, | |
75 … | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
76 … | + (except as stated in this section) patent license to make, have made, | |
77 … | + use, offer to sell, sell, import, and otherwise transfer the Work, | |
78 … | + where such license applies only to those patent claims licensable | |
79 … | + by such Contributor that are necessarily infringed by their | |
80 … | + Contribution(s) alone or by combination of their Contribution(s) | |
81 … | + with the Work to which such Contribution(s) was submitted. If You | |
82 … | + institute patent litigation against any entity (including a | |
83 … | + cross-claim or counterclaim in a lawsuit) alleging that the Work | |
84 … | + or a Contribution incorporated within the Work constitutes direct | |
85 … | + or contributory patent infringement, then any patent licenses | |
86 … | + granted to You under this License for that Work shall terminate | |
87 … | + as of the date such litigation is filed. | |
88 … | + | |
89 … | + 4. Redistribution. You may reproduce and distribute copies of the | |
90 … | + Work or Derivative Works thereof in any medium, with or without | |
91 … | + modifications, and in Source or Object form, provided that You | |
92 … | + meet the following conditions: | |
93 … | + | |
94 … | + (a) You must give any other recipients of the Work or | |
95 … | + Derivative Works a copy of this License; and | |
96 … | + | |
97 … | + (b) You must cause any modified files to carry prominent notices | |
98 … | + stating that You changed the files; and | |
99 … | + | |
100 … | + (c) You must retain, in the Source form of any Derivative Works | |
101 … | + that You distribute, all copyright, patent, trademark, and | |
102 … | + attribution notices from the Source form of the Work, | |
103 … | + excluding those notices that do not pertain to any part of | |
104 … | + the Derivative Works; and | |
105 … | + | |
106 … | + (d) If the Work includes a "NOTICE" text file as part of its | |
107 … | + distribution, then any Derivative Works that You distribute must | |
108 … | + include a readable copy of the attribution notices contained | |
109 … | + within such NOTICE file, excluding those notices that do not | |
110 … | + pertain to any part of the Derivative Works, in at least one | |
111 … | + of the following places: within a NOTICE text file distributed | |
112 … | + as part of the Derivative Works; within the Source form or | |
113 … | + documentation, if provided along with the Derivative Works; or, | |
114 … | + within a display generated by the Derivative Works, if and | |
115 … | + wherever such third-party notices normally appear. The contents | |
116 … | + of the NOTICE file are for informational purposes only and | |
117 … | + do not modify the License. You may add Your own attribution | |
118 … | + notices within Derivative Works that You distribute, alongside | |
119 … | + or as an addendum to the NOTICE text from the Work, provided | |
120 … | + that such additional attribution notices cannot be construed | |
121 … | + as modifying the License. | |
122 … | + | |
123 … | + You may add Your own copyright statement to Your modifications and | |
124 … | + may provide additional or different license terms and conditions | |
125 … | + for use, reproduction, or distribution of Your modifications, or | |
126 … | + for any such Derivative Works as a whole, provided Your use, | |
127 … | + reproduction, and distribution of the Work otherwise complies with | |
128 … | + the conditions stated in this License. | |
129 … | + | |
130 … | + 5. Submission of Contributions. Unless You explicitly state otherwise, | |
131 … | + any Contribution intentionally submitted for inclusion in the Work | |
132 … | + by You to the Licensor shall be under the terms and conditions of | |
133 … | + this License, without any additional terms or conditions. | |
134 … | + Notwithstanding the above, nothing herein shall supersede or modify | |
135 … | + the terms of any separate license agreement you may have executed | |
136 … | + with Licensor regarding such Contributions. | |
137 … | + | |
138 … | + 6. Trademarks. This License does not grant permission to use the trade | |
139 … | + names, trademarks, service marks, or product names of the Licensor, | |
140 … | + except as required for reasonable and customary use in describing the | |
141 … | + origin of the Work and reproducing the content of the NOTICE file. | |
142 … | + | |
143 … | + 7. Disclaimer of Warranty. Unless required by applicable law or | |
144 … | + agreed to in writing, Licensor provides the Work (and each | |
145 … | + Contributor provides its Contributions) on an "AS IS" BASIS, | |
146 … | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
147 … | + implied, including, without limitation, any warranties or conditions | |
148 … | + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
149 … | + PARTICULAR PURPOSE. You are solely responsible for determining the | |
150 … | + appropriateness of using or redistributing the Work and assume any | |
151 … | + risks associated with Your exercise of permissions under this License. | |
152 … | + | |
153 … | + 8. Limitation of Liability. In no event and under no legal theory, | |
154 … | + whether in tort (including negligence), contract, or otherwise, | |
155 … | + unless required by applicable law (such as deliberate and grossly | |
156 … | + negligent acts) or agreed to in writing, shall any Contributor be | |
157 … | + liable to You for damages, including any direct, indirect, special, | |
158 … | + incidental, or consequential damages of any character arising as a | |
159 … | + result of this License or out of the use or inability to use the | |
160 … | + Work (including but not limited to damages for loss of goodwill, | |
161 … | + work stoppage, computer failure or malfunction, or any and all | |
162 … | + other commercial damages or losses), even if such Contributor | |
163 … | + has been advised of the possibility of such damages. | |
164 … | + | |
165 … | + 9. Accepting Warranty or Additional Liability. While redistributing | |
166 … | + the Work or Derivative Works thereof, You may choose to offer, | |
167 … | + and charge a fee for, acceptance of support, warranty, indemnity, | |
168 … | + or other liability obligations and/or rights consistent with this | |
169 … | + License. However, in accepting such obligations, You may act only | |
170 … | + on Your own behalf and on Your sole responsibility, not on behalf | |
171 … | + of any other Contributor, and only if You agree to indemnify, | |
172 … | + defend, and hold each Contributor harmless for any liability | |
173 … | + incurred by, or claims asserted against, such Contributor by reason | |
174 … | + of your accepting any such warranty or additional liability. | |
175 … | + | |
176 … | + END OF TERMS AND CONDITIONS | |
177 … | + | |
178 … | + APPENDIX: How to apply the Apache License to your work. | |
179 … | + | |
180 … | + To apply the Apache License to your work, attach the following | |
181 … | + boilerplate notice, with the fields enclosed by brackets "[]" | |
182 … | + replaced with your own identifying information. (Don't include | |
183 … | + the brackets!) The text should be enclosed in the appropriate | |
184 … | + comment syntax for the file format. We also recommend that a | |
185 … | + file or class name and description of purpose be included on the | |
186 … | + same "printed page" as the copyright notice for easier | |
187 … | + identification within third-party archives. | |
188 … | + | |
189 … | + Copyright [yyyy] [name of copyright owner] | |
190 … | + | |
191 … | + Licensed under the Apache License, Version 2.0 (the "License"); | |
192 … | + you may not use this file except in compliance with the License. | |
193 … | + You may obtain a copy of the License at | |
194 … | + | |
195 … | + http://www.apache.org/licenses/LICENSE-2.0 | |
196 … | + | |
197 … | + Unless required by applicable law or agreed to in writing, software | |
198 … | + distributed under the License is distributed on an "AS IS" BASIS, | |
199 … | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
200 … | + See the License for the specific language governing permissions and | |
201 … | + limitations under the License. |
Makefile | ||
---|---|---|
@@ -1,0 +1,40 @@ | ||
1 … | +.POSIX: | |
2 … | +DILLO_DIR=~/.dillo | |
3 … | +DPI_DIR=$(DILLO_DIR)/dpi | |
4 … | +DPIDRC=$(DILLO_DIR)/dpidrc | |
5 … | +PROTO=did | |
6 … | +NAME=did | |
7 … | +BIN_NAME=did.dpi | |
8 … | +BIN=target/debug/did-dpi | |
9 … | +SRC = src/main.rs | |
10 … | + | |
11 … | +all: $(BIN) | |
12 … | + | |
13 … | +test: | |
14 … | + @#cargo test | |
15 … | + cargo build | |
16 … | + -dpidc stop | |
17 … | + @#timeout 1 dillo did:asdf | |
18 … | + | |
19 … | +$(BIN): $(SRC) | |
20 … | + cargo build | |
21 … | + | |
22 … | +install: $(BIN) install-proto | |
23 … | + mkdir -p $(DPI_DIR)/$(NAME) | |
24 … | + cp -f $(BIN) $(DPI_DIR)/$(NAME)/$(BIN_NAME) | |
25 … | + | |
26 … | +link: $(BIN) install-proto | |
27 … | + mkdir -p $(DPI_DIR)/$(NAME) | |
28 … | + ln -frs $(BIN) $(DPI_DIR)/$(NAME)/$(BIN_NAME) | |
29 … | + | |
30 … | +install-proto: | |
31 … | + test -e $(DPIDRC) || cp /etc/dillo/dpidrc $(DPIDRC) | |
32 … | + grep -qF 'proto.$(PROTO)=$(NAME)/$(BIN_NAME)' $(DPIDRC) ||\ | |
33 … | + echo 'proto.$(PROTO)=$(NAME)/$(BIN_NAME)' >> $(DPIDRC) | |
34 … | + | |
35 … | +clean: | |
36 … | + cargo clean | |
37 … | + | |
38 … | +uninstall: | |
39 … | + rm -f $(DPI_DIR)/$(NAME)/$(BIN_NAME) | |
40 … | + test -s $(DPIDRC) && sed -i~ '/proto\.$(PROTO)=$(NAME)\/$(BIN_NAME)/d' $(DPIDRC) |
README.md | ||
---|---|---|
@@ -1,0 +1,50 @@ | ||
1 … | +# dillo-did | |
2 … | + | |
3 … | +[Dillo][] plugin for [did][] ([Decentralized Identifier][did-core]) URIs. Written in [Rust][]. Requires [Rust nightly][]. Build using [Cargo][]. Uses [`ssi`][]/[DIDKit][]. | |
4 … | + | |
5 … | +## Screenshot | |
6 … | + | |
7 … | +![dillo-did-key.png](&WqNWKdBcMwCAy4SatQOZ5XoaGu1B4IpUQcZrfFbjJTk=.sha256) | |
8 … | + | |
9 … | +## Install from [git-ssb][] | |
10 … | +``` | |
11 … | +git clone ssb://%T8QyhgamH7fmOiiYV/iWiSRmTDdxXje77teBB2yijGU=.sha256 dillo-did | |
12 … | +cd dillo-did | |
13 … | +make install | |
14 … | +dpidc stop | |
15 … | +``` | |
16 … | +You can also use `make link` instead of `make install`, to install via a symlink. | |
17 … | + | |
18 … | +## Built-in supported [DID methods][] | |
19 … | + | |
20 … | +- [did:key][] | |
21 … | +- [did:web][] | |
22 … | +- `did:tz` | |
23 … | + | |
24 … | +## Fallback resolver for additional DID methods | |
25 … | + | |
26 … | +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][]. | |
27 … | + | |
28 … | +## TODO | |
29 … | + | |
30 … | +- Make external resolver URL(s) configurable via a config file | |
31 … | +- Render DID documents more nicely | |
32 … | + | |
33 … | +## License | |
34 … | + | |
35 … | +Apache License, Version 2.0 | |
36 … | + | |
37 … | +[Dillo]: https://www.dillo.org/ | |
38 … | +[Rust]: https://www.rust-lang.org/ | |
39 … | +[Rust nightly]: https://doc.rust-lang.org/stable/book/appendix-07-nightly-rust.html | |
40 … | +[Cargo]: https://github.com/rust-lang/cargo | |
41 … | +[did]: https://www.iana.org/assignments/uri-schemes/prov/did | |
42 … | +[did-core]: https://www.w3.org/TR/did-core/ | |
43 … | +[git-ssb]: %n92DiQh7ietE+R+X/I403LQoyf2DtR3WQfCkDKlheQU=.sha256 | |
44 … | +[DIDKit]: https://github.com/spruceid/didkit | |
45 … | +[`ssi`]: https://github.com/spruceid/ssi | |
46 … | +[did:key]: https://w3c-ccg.github.io/did-method-key/ | |
47 … | +[did:web]: https://w3c-ccg.github.io/did-method-web/ | |
48 … | +[did-http]: https://w3c-ccg.github.io/did-resolution/#bindings-https | |
49 … | +[DID methods]: https://w3c.github.io/did-core/#methods | |
50 … | +[Universal Resolver]: https://github.com/decentralized-identity/universal-registrar/ |
src/dpip.rs | |||
---|---|---|---|
@@ -1,0 +1,228 @@ | |||
1 … | +use async_std::io::Bytes; | ||
2 … | +use async_std::io::Cursor; | ||
3 … | +use async_std::io::Read; | ||
4 … | +use async_std::io::ReadExt; | ||
5 … | +use async_std::stream::StreamExt; | ||
6 … | +use async_std::task::block_on; | ||
7 … | +use core::fmt::Display; | ||
8 … | +use core::fmt::Formatter; | ||
9 … | +use core::str::FromStr; | ||
10 … | +use std::collections::BTreeMap; | ||
11 … | +use std::result::Result; | ||
12 … | +use thiserror::Error; | ||
13 … | + | ||
14 … | + | ||
15 … | +pub struct Dpip { | ||
16 … | + pub name: Option<String>, | ||
17 … | + pub properties: BTreeMap<String, String>, | ||
18 … | +} | ||
19 … | + | ||
20 … | + | ||
21 … | +pub enum DpipParseError { | ||
22 … | + | ||
23 … | + InvalidStart(char), | ||
24 … | + | ||
25 … | + InvalidEnd(char), | ||
26 … | + | ||
27 … | + InvalidValueStart(char), | ||
28 … | + | ||
29 … | + InvalidValueEnd(char), | ||
30 … | + | ||
31 … | + InvalidKeySpace, | ||
32 … | + | ||
33 … | + InvalidKeyQuote, | ||
34 … | + | ||
35 … | + EOF, | ||
36 … | + | ||
37 … | + IO( | async_std::io::Error),||
38 … | + | ||
39 … | + Utf8( | std::string::FromUtf8Error),||
40 … | +} | ||
41 … | + | ||
42 … | +async fn read_value<T>(bytes: &mut Bytes<T>) -> Result<String, DpipParseError> | ||
43 … | +where | ||
44 … | + T: Read + Unpin, | ||
45 … | +{ | ||
46 … | + let mut value_bytes = Vec::new(); | ||
47 … | + match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
48 … | + b'\'' => {} | ||
49 … | + byte => return Err(DpipParseError::InvalidValueStart(byte.into())), | ||
50 … | + } | ||
51 … | + while let Some(b) = match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
52 … | + b'\'' => match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
53 … | + b'\'' => Some(b'\''), | ||
54 … | + b' ' => None, | ||
55 … | + b => return Err(DpipParseError::InvalidValueEnd(b.into())), | ||
56 … | + }, | ||
57 … | + b => Some(b), | ||
58 … | + } { | ||
59 … | + value_bytes.push(b); | ||
60 … | + } | ||
61 … | + let name = String::from_utf8(value_bytes)?; | ||
62 … | + Ok(name) | ||
63 … | +} | ||
64 … | + | ||
65 … | +async fn read_initial_key_value<T>( | ||
66 … | + bytes: &mut Bytes<T>, | ||
67 … | +) -> Result<(String, Option<String>), DpipParseError> | ||
68 … | +where | ||
69 … | + T: Read + Unpin, | ||
70 … | +{ | ||
71 … | + let mut name_bytes = Vec::new(); | ||
72 … | + while let Some(b) = match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
73 … | + b' ' => { | ||
74 … | + let name = String::from_utf8(name_bytes)?; | ||
75 … | + return Ok((name, None)); | ||
76 … | + } | ||
77 … | + b'=' => { | ||
78 … | + let name = String::from_utf8(name_bytes)?; | ||
79 … | + let value = read_value(bytes).await?; | ||
80 … | + return Ok((name, Some(value))); | ||
81 … | + } | ||
82 … | + b => Some(b), | ||
83 … | + } { | ||
84 … | + name_bytes.push(b); | ||
85 … | + } | ||
86 … | + Err(DpipParseError::EOF) | ||
87 … | +} | ||
88 … | + | ||
89 … | +async fn read_key_value<T>(bytes: &mut Bytes<T>) -> Result<Option<(String, String)>, DpipParseError> | ||
90 … | +where | ||
91 … | + T: Read + Unpin, | ||
92 … | +{ | ||
93 … | + let mut key_bytes = Vec::new(); | ||
94 … | + while let Some(b) = match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
95 … | + b'\'' => { | ||
96 … | + if key_bytes.is_empty() { | ||
97 … | + return Ok(None); | ||
98 … | + } else { | ||
99 … | + return Err(DpipParseError::InvalidKeyQuote); | ||
100 … | + } | ||
101 … | + } | ||
102 … | + b' ' => { | ||
103 … | + return Err(DpipParseError::InvalidKeySpace); | ||
104 … | + } | ||
105 … | + b'=' => None, | ||
106 … | + b => Some(b), | ||
107 … | + } { | ||
108 … | + key_bytes.push(b); | ||
109 … | + } | ||
110 … | + let key = String::from_utf8(key_bytes)?; | ||
111 … | + let value = read_value(bytes).await?; | ||
112 … | + Ok(Some((key, value))) | ||
113 … | +} | ||
114 … | + | ||
115 … | +impl Dpip { | ||
116 … | + /// Create a dpip command with the given command name | ||
117 … | + pub fn cmd(cmd: &str) -> Self { | ||
118 … | + let mut dpip = Self::default(); | ||
119 … | + dpip.properties.insert("cmd".to_string(), cmd.to_string()); | ||
120 … | + dpip | ||
121 … | + } | ||
122 … | + | ||
123 … | + /// Create a dpip command for serving a page | ||
124 … | + pub fn start_send_page(url: &str) -> Self { | ||
125 … | + let mut tag = Dpip::cmd("start_send_page"); | ||
126 … | + tag.properties.insert("url".to_string(), url.to_string()); | ||
127 … | + tag | ||
128 … | + } | ||
129 … | + | ||
130 … | + /// Create a dpip command for sending a status message | ||
131 … | + pub fn send_status_message(msg: &str) -> Self { | ||
132 … | + let mut tag = Dpip::cmd("send_status_message"); | ||
133 … | + tag.properties.insert("msg".to_string(), msg.to_string()); | ||
134 … | + tag | ||
135 … | + } | ||
136 … | + | ||
137 … | + /// Read a dpip tag. | ||
138 … | + /// | ||
139 … | + /// Format (from dillo/dpip/dpip.c): | ||
140 … | + /// ``` | ||
141 … | + /// "<"[*alpha] *(<name>"="Quote<escaped_value>Quote) " "Quote">" | ||
142 … | + /// ``` | ||
143 … | + pub async fn read<T>(reader: &mut T) -> Result<Dpip, DpipParseError> | ||
144 … | + where | ||
145 … | + T: Read + Unpin, | ||
146 … | + { | ||
147 … | + let mut map = BTreeMap::new(); | ||
148 … | + let mut bytes = reader.bytes(); | ||
149 … | + match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
150 … | + b'<' => {} | ||
151 … | + byte => return Err(DpipParseError::InvalidStart(byte.into())), | ||
152 … | + } | ||
153 … | + let tag_name = match read_initial_key_value(&mut bytes).await? { | ||
154 … | + (key, Some(value)) => { | ||
155 … | + map.insert(key, value); | ||
156 … | + None | ||
157 … | + } | ||
158 … | + (key, None) => Some(key), | ||
159 … | + }; | ||
160 … | + while let Some((key, value)) = read_key_value(&mut bytes).await? { | ||
161 … | + map.insert(key, value); | ||
162 … | + } | ||
163 … | + match bytes.next().await.ok_or(DpipParseError::EOF)?? { | ||
164 … | + b'>' => {} | ||
165 … | + byte => return Err(DpipParseError::InvalidEnd(byte.into())), | ||
166 … | + } | ||
167 … | + | ||
168 … | + Ok(Dpip { | ||
169 … | + name: tag_name, | ||
170 … | + properties: map, | ||
171 … | + }) | ||
172 … | + } | ||
173 … | +} | ||
174 … | + | ||
175 … | +impl FromStr for Dpip { | ||
176 … | + type Err = DpipParseError; | ||
177 … | + fn from_str(tag: &str) -> std::result::Result<Self, Self::Err> { | ||
178 … | + let mut reader = Cursor::new(tag.as_bytes().to_vec()); | ||
179 … | + block_on(Dpip::read(&mut reader)) | ||
180 … | + } | ||
181 … | +} | ||
182 … | + | ||
183 … | +impl Display for Dpip { | ||
184 … | + fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { | ||
185 … | + write!(f, "<")?; | ||
186 … | + if let Some(name) = &self.name { | ||
187 … | + write!(f, "{} ", name)?; | ||
188 … | + } | ||
189 … | + for (key, value) in &self.properties { | ||
190 … | + write!(f, "{}='{}' ", key, value.replace("'", "''"))?; | ||
191 … | + } | ||
192 … | + write!(f, "'>") | ||
193 … | + } | ||
194 … | +} | ||
195 … | + | ||
196 … | + | ||
197 … | +mod tests { | ||
198 … | + use super::*; | ||
199 … | + | ||
200 … | + | ||
201 … | + fn dpip() { | ||
202 … | + block_on(dpip_async()); | ||
203 … | + } | ||
204 … | + async fn dpip_async() { | ||
205 … | + let tags = b"<a='b' '><c='d' e='f g' '><h='>' '><i='j''k' '>"; | ||
206 … | + let mut reader = Cursor::new(tags.to_vec()); | ||
207 … | + let tag = Dpip::read(&mut reader).await.unwrap(); | ||
208 … | + assert_eq!(tag.to_string(), "<a='b' '>"); | ||
209 … | + let tag = Dpip::read(&mut reader).await.unwrap(); | ||
210 … | + assert_eq!(tag.to_string(), "<c='d' e='f g' '>"); | ||
211 … | + let tag = Dpip::read(&mut reader).await.unwrap(); | ||
212 … | + assert_eq!(tag.to_string(), "<h='>' '>"); | ||
213 … | + let tag = Dpip::read(&mut reader).await.unwrap(); | ||
214 … | + assert_eq!(tag.to_string(), "<i='j''k' '>"); | ||
215 … | + | ||
216 … | + let tag: Dpip = "<key='wouldn''t it be nice' '>".parse().unwrap(); | ||
217 … | + assert_eq!(tag.properties["key"], "wouldn't it be nice"); | ||
218 … | + | ||
219 … | + let tags = b"<a='isn''t that=''cool'' yes' '>"; | ||
220 … | + let mut reader = Cursor::new(tags.to_vec()); | ||
221 … | + let tag = Dpip::read(&mut reader).await.unwrap(); | ||
222 … | + assert_eq!(tag.properties["a"], "isn't that='cool' yes"); | ||
223 … | + | ||
224 … | + let tags = b"<bad = '>' '>"; | ||
225 … | + let mut reader = Cursor::new(tags.to_vec()); | ||
226 … | + Dpip::read(&mut reader).await.unwrap_err(); | ||
227 … | + } | ||
228 … | +} |
src/main.rs | ||
---|---|---|
@@ -1,0 +1,309 @@ | ||
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 … | +} |
Built with git-ssb-web