From f07a875ce1e672b45f7401a38056b598d17d1935 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Fri, 27 Jan 2023 09:46:03 +0100 Subject: [PATCH] Add web-based authentication Signed-off-by: Sebastian Martinez --- Cargo.lock | 487 +------------------------- radicle-cli/Cargo.toml | 1 + radicle-cli/src/commands.rs | 2 + radicle-cli/src/commands/web.rs | 102 ++++++ radicle-cli/src/main.rs | 6 + radicle-httpd/Cargo.toml | 2 - radicle-httpd/src/api.rs | 2 +- radicle-httpd/src/api/auth.rs | 75 +--- radicle-httpd/src/api/error.rs | 11 +- radicle-httpd/src/api/test.rs | 38 +- radicle-httpd/src/api/v1/delegates.rs | 4 +- radicle-httpd/src/api/v1/projects.rs | 24 +- radicle-httpd/src/api/v1/sessions.rs | 166 ++++----- radicle-httpd/src/api/v1/stats.rs | 4 +- 14 files changed, 270 insertions(+), 654 deletions(-) create mode 100644 radicle-cli/src/commands/web.rs diff --git a/Cargo.lock b/Cargo.lock index 4373a6d8..c0ba7c05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,15 +29,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - [[package]] name = "amplify" version = "4.0.0-beta.7" @@ -94,12 +85,6 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - [[package]] name = "ascii" version = "1.1.0" @@ -128,18 +113,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "auto_impl" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8c1df849285fbacd587de7818cc7d13be6cd2cbcd47a04fb1801b0e2706e33" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -259,18 +232,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -314,12 +275,6 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" -[[package]] -name = "byte-slice-cast" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" - [[package]] name = "byteorder" version = "1.4.3" @@ -331,9 +286,6 @@ name = "bytes" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" -dependencies = [ - "serde", -] [[package]] name = "cc" @@ -500,12 +452,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "crypto-bigint" version = "0.4.9" @@ -685,17 +631,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dialoguer" version = "0.10.3" @@ -837,80 +772,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ethabi" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" -dependencies = [ - "ethereum-types", - "hex", - "once_cell", - "regex", - "serde", - "serde_json", - "sha3", - "thiserror", - "uint", -] - -[[package]] -name = "ethbloom" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" -dependencies = [ - "crunchy", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "scale-info", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" -dependencies = [ - "ethbloom", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "primitive-types", - "scale-info", - "uint", -] - -[[package]] -name = "ethers-core" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade3e9c97727343984e1ceada4fdab11142d2ee3472d2c67027d56b1251d4f15" -dependencies = [ - "arrayvec", - "bytes", - "chrono", - "elliptic-curve", - "ethabi", - "generic-array", - "hex", - "k256", - "open-fastrlp", - "rand 0.8.5", - "rlp", - "rlp-derive", - "serde", - "serde_json", - "strum", - "thiserror", - "tiny-keccak", - "unicode-xid", -] - [[package]] name = "fastrand" version = "1.8.0" @@ -942,18 +803,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] - [[package]] name = "flate2" version = "1.0.25" @@ -979,12 +828,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-channel" version = "0.3.25" @@ -1180,12 +1023,6 @@ dependencies = [ "http", ] -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -1204,12 +1041,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hmac" version = "0.12.1" @@ -1317,44 +1148,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" -dependencies = [ - "rlp", -] - -[[package]] -name = "impl-serde" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" -dependencies = [ - "serde", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "indexmap" version = "1.9.2" @@ -1417,16 +1210,6 @@ dependencies = [ "popol", ] -[[package]] -name = "iri-string" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0586ad318a04c73acdbad33f67969519b5452c80770c4c72059a686da48a7e" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.2" @@ -1474,19 +1257,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "k256" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "sha2 0.10.6", - "sha3", -] - [[package]] name = "keccak" version = "0.1.3" @@ -1684,15 +1454,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "nonempty" version = "0.5.0" @@ -1811,31 +1572,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "open-fastrlp" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", - "ethereum-types", - "open-fastrlp-derive", -] - -[[package]] -name = "open-fastrlp-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" -dependencies = [ - "bytes", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "output_vt100" version = "0.1.3" @@ -1873,32 +1609,6 @@ dependencies = [ "sha2 0.10.6", ] -[[package]] -name = "parity-scale-codec" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ab01d0f889e957861bc65888d5ccbe82c158d0270136ba46820d43837cdf72" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pbkdf2" version = "0.11.0" @@ -2021,30 +1731,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "primitive-types" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" -dependencies = [ - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "scale-info", - "uint", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" -dependencies = [ - "once_cell", - "toml_edit", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2162,6 +1848,7 @@ dependencies = [ "tempfile", "thiserror", "timeago", + "ureq", "zeroize", ] @@ -2252,7 +1939,6 @@ dependencies = [ "axum", "axum-server", "chrono", - "ethers-core", "fastrand", "flate2", "hyper", @@ -2263,7 +1949,6 @@ dependencies = [ "radicle-surf", "serde", "serde_json", - "siwe", "tempfile", "thiserror", "time 0.3.17", @@ -2360,12 +2045,6 @@ dependencies = [ "radicle", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.7.3" @@ -2452,8 +2131,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ - "aho-corasick", - "memchr", "regex-syntax", ] @@ -2492,27 +2169,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - -[[package]] -name = "rlp-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "rsa" version = "0.7.2" @@ -2534,12 +2190,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - [[package]] name = "rustix" version = "0.36.7" @@ -2575,30 +2225,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "scale-info" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001cf62ece89779fd16105b5f515ad0e5cedcd5440d3dd806bb067978e7c3608" -dependencies = [ - "cfg-if", - "derive_more", - "parity-scale-codec", - "scale-info-derive", -] - -[[package]] -name = "scale-info-derive" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303959cf613a6f6efd19ed4b4ad5bf79966a13352716299ad532cfb115f4205c" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "scratch" version = "1.0.3" @@ -2784,22 +2410,6 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" -[[package]] -name = "siwe" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4cc2eafb2354c1aeaeac5f53b7726ca4ea5ad16314a0b5ebacb6676f599c2b" -dependencies = [ - "hex", - "http", - "iri-string", - "k256", - "rand 0.8.5", - "sha3", - "thiserror", - "time 0.3.17", -] - [[package]] name = "slab" version = "0.4.7" @@ -2932,34 +2542,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "subtle" version = "2.4.1" @@ -2995,12 +2577,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tar" version = "0.4.38" @@ -3108,15 +2684,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26" -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -3175,23 +2742,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml_datetime" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" - -[[package]] -name = "toml_edit" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581" -dependencies = [ - "indexmap", - "nom8", - "toml_datetime", -] - [[package]] name = "tower" version = "0.4.13" @@ -3327,18 +2877,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - [[package]] name = "unicode-bidi" version = "0.3.10" @@ -3382,6 +2920,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ureq" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +dependencies = [ + "base64", + "log", + "once_cell", + "serde", + "serde_json", + "url", +] + [[package]] name = "url" version = "2.3.1" @@ -3581,15 +3133,6 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "xattr" version = "0.2.3" diff --git a/radicle-cli/Cargo.toml b/radicle-cli/Cargo.toml index d39dbebb..4b82b281 100644 --- a/radicle-cli/Cargo.toml +++ b/radicle-cli/Cargo.toml @@ -24,6 +24,7 @@ serde_json = { version = "1" } serde_yaml = { version = "0.8" } thiserror = { version = "1" } timeago = { version = "0.3", default-features = false } +ureq = { version = "2.6.1", default-features = false, features = ["json"] } zeroize = { version = "1.1" } [dependencies.radicle] diff --git a/radicle-cli/src/commands.rs b/radicle-cli/src/commands.rs index 70fd80fa..d783ed73 100644 --- a/radicle-cli/src/commands.rs +++ b/radicle-cli/src/commands.rs @@ -42,3 +42,5 @@ pub mod rad_track; pub mod rad_unassign; #[path = "commands/untrack.rs"] pub mod rad_untrack; +#[path = "commands/web.rs"] +pub mod rad_web; diff --git a/radicle-cli/src/commands/web.rs b/radicle-cli/src/commands/web.rs new file mode 100644 index 00000000..5776d62c --- /dev/null +++ b/radicle-cli/src/commands/web.rs @@ -0,0 +1,102 @@ +use std::ffi::OsString; + +use anyhow::anyhow; +use radicle::crypto::{PublicKey, Signature, Signer}; +use serde::{Deserialize, Serialize}; + +use crate::terminal as term; +use crate::terminal::args::{Args, Error, Help}; + +pub const HELP: Help = Help { + name: "web", + description: "Connect web with node", + version: env!("CARGO_PKG_VERSION"), + usage: r#" +Usage + + rad web [...] + +Options + + --host, -h httpd host to bind to + --web, -w interface host to bind to + --verbose, -v Verbose output + --help Print help +"#, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionInfo { + pub session_id: String, + pub public_key: PublicKey, +} + +#[derive(Debug)] +pub struct Options { + pub host: String, + pub web: String, + pub verbose: bool, +} + +impl Args for Options { + fn from_args(args: Vec) -> anyhow::Result<(Self, Vec)> { + use lexopt::prelude::*; + + let mut parser = lexopt::Parser::from_args(args); + let mut host = None; + let mut web = None; + let mut verbose = false; + + while let Some(arg) = parser.next()? { + match arg { + Long("verbose") | Short('v') => verbose = true, + Long("host") | Short('h') => { + host = Some(parser.value()?.to_string_lossy().to_string()) + } + Long("web") | Short('w') => { + web = Some(parser.value()?.to_string_lossy().to_string()) + } + Long("help") => { + return Err(Error::Help.into()); + } + _ => { + return Err(anyhow!(arg.unexpected())); + } + } + } + + Ok(( + Options { + verbose, + host: host.unwrap_or(String::from("0.0.0.0:8080")), + web: web.unwrap_or(String::from("0.0.0.0:3000")), + }, + vec![], + )) + } +} + +pub fn sign(signer: Box, session: &SessionInfo) -> Result { + signer + .try_sign(format!("{}:{}", session.session_id, session.public_key).as_bytes()) + .map_err(anyhow::Error::from) +} + +pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { + let session: SessionInfo = ureq::post(&format!("http://{}/api/v1/sessions", options.host)) + .call()? + .into_json()?; + let profile = ctx.profile()?; + let signer = profile.signer()?; + let signature = sign(signer, &session)?; + term::info!( + "http://{}/session/{}?pk={}&sig={}", + options.web, + session.session_id, + session.public_key, + signature, + ); + + Ok(()) +} diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs index acc4fd91..41aebdfc 100644 --- a/radicle-cli/src/main.rs +++ b/radicle-cli/src/main.rs @@ -278,6 +278,12 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option> args.to_vec(), ); } + "web" => term::run_command_args::( + rad_web::HELP, + "Web", + rad_web::run, + args.to_vec(), + ), _ => { let exe = format!("{}-{}", NAME, exe); let status = process::Command::new(exe.clone()).args(args).status(); diff --git a/radicle-httpd/Cargo.toml b/radicle-httpd/Cargo.toml index 821416dc..1ecedff9 100644 --- a/radicle-httpd/Cargo.toml +++ b/radicle-httpd/Cargo.toml @@ -19,14 +19,12 @@ anyhow = { version = "1" } axum = { version = "0.6.2", default-features = false, features = ["headers", "json", "query", "tokio"] } axum-server = { version = "0.4.4", default-features = false } chrono = { version = "0.4.22" } -ethers-core = { version = "1.0" } fastrand = { version = "1.7.0" } flate2 = { version = "1" } hyper = { version = "0.14.17", default-features = false } lexopt = { version = "0.2.1" } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } -siwe = { version = "0.5" } thiserror = { version = "1" } time = { version = "0.3.17" } tokio = { version = "1.21", default-features = false, features = ["macros", "rt-multi-thread"] } diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index 3bb9a668..a6cf2af5 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -73,7 +73,7 @@ pub fn router(ctx: Context) -> Router { CorsLayer::new() .max_age(Duration::from_secs(86400)) .allow_origin(cors::Any) - .allow_methods([Method::GET, Method::POST, Method::PUT]) + .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::PUT]) .allow_headers([CONTENT_TYPE, AUTHORIZATION]), ) } diff --git a/radicle-httpd/src/api/auth.rs b/radicle-httpd/src/api/auth.rs index 3c29f8b1..74c2666e 100644 --- a/radicle-httpd/src/api/auth.rs +++ b/radicle-httpd/src/api/auth.rs @@ -1,13 +1,8 @@ -use std::convert::TryFrom; -use std::str::FromStr; - -use ethers_core::types::{Signature, H160}; -use serde::{Deserialize, Serialize, Serializer}; +use radicle::crypto::PublicKey; +use serde::{Serialize, Serializer}; use time::OffsetDateTime; -use crate::api::error::Error; - -#[derive(Clone)] +#[derive(Clone, PartialEq, PartialOrd)] pub struct DateTime(pub OffsetDateTime); impl Serialize for DateTime { @@ -16,26 +11,11 @@ impl Serialize for DateTime { } } -#[derive(Deserialize, Serialize)] -pub struct AuthRequest { - pub message: String, - #[serde(deserialize_with = "deserialize_signature")] - pub signature: Signature, -} - -fn deserialize_signature<'de, D>(deserializer: D) -> Result -where - D: serde::de::Deserializer<'de>, -{ - let buf = String::deserialize(deserializer)?; - Signature::from_str(&buf).map_err(serde::de::Error::custom) -} - pub enum AuthState { Authorized(Session), Unauthorized { - nonce: String, - expiration_time: DateTime, + public_key: PublicKey, + expires_at: DateTime, }, } @@ -43,48 +23,7 @@ pub enum AuthState { #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Session { - pub domain: String, - pub address: H160, - pub statement: Option, - pub uri: String, - pub version: u64, - pub chain_id: u64, - pub nonce: String, + pub public_key: PublicKey, pub issued_at: DateTime, - pub expiration_time: Option, - pub resources: Vec, -} - -impl TryFrom for Session { - type Error = Error; - - fn try_from(message: siwe::Message) -> Result { - Ok(Session { - domain: message.domain.host().to_string(), - address: H160(message.address), - statement: None, - uri: message.uri.to_string(), - version: message.version as u64, - chain_id: message.chain_id, - nonce: message.nonce, - issued_at: DateTime(message.issued_at.as_ref().to_owned()), - expiration_time: message - .expiration_time - .map(|x| DateTime(x.as_ref().to_owned())), - resources: message.resources.iter().map(|r| r.to_string()).collect(), - }) - } -} - -#[cfg(test)] -mod test { - #[test] - fn test_auth_request_de() { - let json = serde_json::json!({ - "message": "Hello World!", - "signature": "20096c6ed2bcccb88c9cafbbbbda7a5a3cff6d0ca318c07faa58464083ca40a92f899fbeb26a4c763a7004b13fd0f1ba6c321d4e3a023e30f63c40d4154b99a41c" - }); - - let _req: super::AuthRequest = serde_json::from_value(json).unwrap(); - } + pub expires_at: DateTime, } diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index d2b1dd08..7995f5a6 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -18,13 +18,13 @@ pub enum Error { #[error(transparent)] Env(#[from] std::env::VarError), - /// An error occurred while verifying the siwe message. + /// Profile error. #[error(transparent)] - SiweVerification(#[from] siwe::VerificationError), + Profile(#[from] radicle::profile::Error), - /// An error occurred while parsing the siwe message. + /// Crypto error. #[error(transparent)] - SiweParse(#[from] siwe::ParseError), + Crypto(#[from] radicle::crypto::Error), /// Storage error. #[error(transparent)] @@ -60,8 +60,7 @@ impl IntoResponse for Error { let (status, msg) = match &self { Error::NotFound => (StatusCode::NOT_FOUND, None), Error::Auth(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), - Error::SiweParse(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), - Error::SiweVerification(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), + Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), Error::Git2(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Some(e.message().to_owned()), diff --git a/radicle-httpd/src/api/test.rs b/radicle-httpd/src/api/test.rs index dd43df58..5331c5b7 100644 --- a/radicle-httpd/src/api/test.rs +++ b/radicle-httpd/src/api/test.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::{env, fs}; use axum::body::Body; -use axum::http::Request; +use axum::http::{Method, Request}; use axum::Router; use serde_json::Value; use tower::ServiceExt; @@ -116,20 +116,42 @@ pub fn seed(dir: &Path) -> Context { } } -pub async fn request(app: &Router, path: impl ToString) -> Response { +pub async fn get(app: &Router, path: impl ToString) -> Response { Response( app.clone() - .oneshot( - Request::builder() - .uri(path.to_string()) - .body(Body::empty()) - .unwrap(), - ) + .oneshot(request(path, Method::GET, None)) .await .unwrap(), ) } +pub async fn post(app: &Router, path: impl ToString, body: Option) -> Response { + Response( + app.clone() + .oneshot(request(path, Method::POST, body)) + .await + .unwrap(), + ) +} + +pub async fn put(app: &Router, path: impl ToString, body: Option) -> Response { + Response( + app.clone() + .oneshot(request(path, Method::PUT, body)) + .await + .unwrap(), + ) +} + +fn request(path: impl ToString, method: Method, body: Option) -> Request { + Request::builder() + .method(method) + .uri(path.to_string()) + .header("Content-Type", "application/json") + .body(body.unwrap_or_else(Body::empty)) + .unwrap() +} + pub struct Response(axum::response::Response); impl Response { diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 499ca909..ce25f58b 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -69,13 +69,13 @@ mod routes { use axum::http::StatusCode; use serde_json::json; - use crate::api::test::{self, request, HEAD}; + use crate::api::test::{self, get, HEAD}; #[tokio::test] async fn test_delegates_projects() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request( + let response = get( &app, "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", ) diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 5322fadc..a785ca8d 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -465,13 +465,13 @@ mod routes { use axum::http::StatusCode; use serde_json::json; - use crate::api::test::{self, request, HEAD, HEAD_1}; + use crate::api::test::{self, get, HEAD, HEAD_1}; #[tokio::test] async fn test_projects_root() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/projects").await; + let response = get(&app, "/projects").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -494,7 +494,7 @@ mod routes { async fn test_projects() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp").await; + let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -515,7 +515,7 @@ mod routes { async fn test_projects_commits_root() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits").await; + let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits").await; assert_eq!(response.status(), StatusCode::FOUND); assert_eq!( @@ -637,7 +637,7 @@ mod routes { async fn test_projects_commits() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request( + let response = get( &app, format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/commits/{HEAD}"), ) @@ -703,7 +703,7 @@ mod routes { async fn test_projects_tree() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request( + let response = get( &app, format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/tree/{HEAD}/"), ) @@ -750,7 +750,7 @@ mod routes { ) ); - let response = request( + let response = get( &app, format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/tree/{HEAD}/dir1"), ) @@ -796,7 +796,7 @@ mod routes { async fn test_projects_remotes_root() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes").await; + let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -817,7 +817,7 @@ mod routes { async fn test_projects_remotes() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi").await; + let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/remotes/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -836,7 +836,7 @@ mod routes { async fn test_projects_blob() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request( + let response = get( &app, format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/blob/{HEAD}/README"), ) @@ -872,7 +872,7 @@ mod routes { async fn test_projects_readme() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request( + let response = get( &app, format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/readme/{HEAD}"), ) @@ -908,7 +908,7 @@ mod routes { async fn test_projects_issues_root() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues").await; + let response = get(&app, "/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( diff --git a/radicle-httpd/src/api/v1/sessions.rs b/radicle-httpd/src/api/v1/sessions.rs index 6ce7ad31..0f279845 100644 --- a/radicle-httpd/src/api/v1/sessions.rs +++ b/radicle-httpd/src/api/v1/sessions.rs @@ -1,131 +1,135 @@ -use std::collections::HashMap; -use std::convert::TryInto; -use std::env; use std::iter::repeat_with; -use std::str::FromStr; use axum::extract::State; use axum::response::IntoResponse; -use axum::routing::{get, post}; +use axum::routing::{post, put}; use axum::{Json, Router}; -use ethers_core::utils::hex; -use hyper::http::uri::Authority; +use radicle::crypto::{PublicKey, Signature}; +use serde::{Deserialize, Serialize}; use serde_json::json; -use siwe::Message; use time::{Duration, OffsetDateTime}; -use crate::api::auth::{AuthRequest, AuthState, DateTime, Session}; +use crate::api::auth::{AuthState, DateTime, Session}; use crate::api::axum_extra::Path; use crate::api::error::Error; use crate::api::Context; pub const UNAUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::seconds(60); +pub const AUTHORIZED_SESSIONS_EXPIRATION: Duration = Duration::weeks(1); pub fn router(ctx: Context) -> Router { Router::new() .route("/sessions", post(session_create_handler)) - .route( - "/sessions/:id", - get(session_get_handler).put(session_signin_handler), - ) + .route("/sessions/:id", put(session_signin_handler)) .with_state(ctx) } +#[derive(Debug, Deserialize, Serialize)] +struct AuthChallenge { + sig: Signature, + pk: PublicKey, +} + /// Create session. /// `POST /sessions` async fn session_create_handler(State(ctx): State) -> impl IntoResponse { + let rng = fastrand::Rng::new(); + let session_id = repeat_with(|| rng.alphanumeric()) + .take(32) + .collect::(); + let signer = ctx.profile.signer().map_err(Error::from)?; let expiration_time = OffsetDateTime::now_utc() .checked_add(UNAUTHORIZED_SESSIONS_EXPIRATION) .unwrap(); + let auth_state = AuthState::Unauthorized { + public_key: *signer.public_key(), + expires_at: DateTime(expiration_time), + }; let mut sessions = ctx.sessions.write().await; - let (session_id, nonce) = create_session(&mut sessions, DateTime(expiration_time)); + sessions.insert(session_id.clone(), auth_state); - Json(json!({ "id": session_id, "nonce": nonce })) -} - -/// Get session. -/// `GET /sessions/:id` -async fn session_get_handler( - State(ctx): State, - Path(id): Path, -) -> impl IntoResponse { - let sessions = ctx.sessions.read().await; - let session = sessions.get(&id).ok_or(Error::NotFound)?; - - match session { - AuthState::Authorized(session) => { - Ok::<_, Error>(Json(json!({ "id": id, "session": session }))) - } - AuthState::Unauthorized { - nonce, - expiration_time, - } => Ok::<_, Error>(Json( - json!({ "id": id, "nonce": nonce, "expirationTime": expiration_time }), - )), - } + Ok::<_, Error>(Json( + json!({"sessionId": session_id, "publicKey": signer.public_key()}), + )) } /// Update session. /// `PUT /sessions/:id` async fn session_signin_handler( State(ctx): State, - Path(id): Path, - Json(request): Json, + Path(session_id): Path, + Json(request): Json, ) -> impl IntoResponse { - // Get unauthenticated session data, return early if not found let mut sessions = ctx.sessions.write().await; - let session = sessions.get(&id).ok_or(Error::NotFound)?; - - if let AuthState::Unauthorized { nonce, .. } = session { - let message = Message::from_str(request.message.as_str()).map_err(Error::from)?; - - let host = env::var("RADICLE_DOMAIN").map_err(Error::from)?; - - // Validate nonce - if *nonce != message.nonce { - return Err(Error::Auth("Invalid nonce")); + let session = sessions.get(&session_id).ok_or(Error::NotFound)?; + if let AuthState::Unauthorized { + public_key, + expires_at, + } = session + { + if public_key != &request.pk { + return Err(Error::Auth("Invalid public key")); } - - // Verify that domain is the correct one - let authority = Authority::from_str(&host).map_err(|_| Error::Auth("Invalid host"))?; - if authority != message.domain { - return Err(Error::Auth("Invalid domain")); + if expires_at <= &DateTime(OffsetDateTime::now_utc()) { + return Err(Error::Auth("Session expired")); } - - // Verifies the following: - // - AuthRequest sig matches the address passed in the AuthRequest message. - // - expirationTime is not in the past. - // - notBefore time is in the future. - message - .verify(&request.signature.to_vec(), &Default::default()) - .await + let payload = format!("{}:{}", session_id, request.pk); + request + .pk + .verify(payload.as_bytes(), &request.sig) .map_err(Error::from)?; + let expiration_time = OffsetDateTime::now_utc() + .checked_add(AUTHORIZED_SESSIONS_EXPIRATION) + .unwrap(); + let session = Session { + public_key: request.pk, + issued_at: DateTime(OffsetDateTime::now_utc()), + expires_at: DateTime(expiration_time), + }; + sessions.insert(session_id.clone(), AuthState::Authorized(session)); - let session: Session = message.try_into()?; - sessions.insert(id.clone(), AuthState::Authorized(session.clone())); - - return Ok::<_, Error>(Json(json!({ "id": id, "session": session }))); + return Ok::<_, Error>(()); } Err(Error::Auth("Session already authorized")) } -fn create_session( - map: &mut HashMap, - expiration_time: DateTime, -) -> (String, String) { - let nonce = siwe::generate_nonce(); +#[cfg(test)] +mod routes { + use axum::body::Body; + use axum::http::StatusCode; + use radicle_cli::commands::rad_web::{self, SessionInfo}; - // We generate a value from the RNG for the session id - let rng = fastrand::Rng::new(); - let id = hex::encode(repeat_with(|| rng.u8(..)).take(32).collect::>()); + use crate::api::test::{self, post, put}; - let auth_state = AuthState::Unauthorized { - nonce: nonce.clone(), - expiration_time, - }; + #[tokio::test] + async fn test_session() { + let tmp = tempfile::tempdir().unwrap(); + let ctx = test::seed(tmp.path()); + let app = super::router(ctx.to_owned()); - map.insert(id.clone(), auth_state); + // Create session + let response = post(&app, "/sessions", None).await; + assert_eq!(response.status(), StatusCode::OK); + let json = response.json().await; + let session_info: SessionInfo = serde_json::from_value(json).unwrap(); - (id, nonce) + // Create request body + let signer = ctx.profile.signer().unwrap(); + let signature = rad_web::sign(signer, &session_info).unwrap(); + let body = serde_json::to_vec(&super::AuthChallenge { + sig: signature, + pk: session_info.public_key, + }) + .unwrap(); + + let response = put( + &app, + format!("/sessions/{}", session_info.session_id), + Some(Body::from(body)), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + } } diff --git a/radicle-httpd/src/api/v1/stats.rs b/radicle-httpd/src/api/v1/stats.rs index aadbf8d2..79d10cec 100644 --- a/radicle-httpd/src/api/v1/stats.rs +++ b/radicle-httpd/src/api/v1/stats.rs @@ -29,13 +29,13 @@ mod routes { use axum::http::StatusCode; use serde_json::json; - use crate::api::test::{self, request}; + use crate::api::test::{self, get}; #[tokio::test] async fn test_stats() { let tmp = tempfile::tempdir().unwrap(); let app = super::router(test::seed(tmp.path())); - let response = request(&app, "/stats").await; + let response = get(&app, "/stats").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!(