Add web-based authentication

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez 2023-01-27 09:46:03 +01:00 committed by Alexis Sellier
parent deeb160c2d
commit f07a875ce1
No known key found for this signature in database
14 changed files with 270 additions and 654 deletions

487
Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

@ -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;

View File

@ -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>...]
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<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
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<dyn Signer>, session: &SessionInfo) -> Result<Signature, anyhow::Error> {
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(())
}

View File

@ -278,6 +278,12 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(),
);
}
"web" => term::run_command_args::<rad_web::Options, _>(
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();

View File

@ -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"] }

View File

@ -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]),
)
}

View File

@ -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<Signature, D::Error>
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<String>,
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<DateTime>,
pub resources: Vec<String>,
}
impl TryFrom<siwe::Message> for Session {
type Error = Error;
fn try_from(message: siwe::Message) -> Result<Session, Error> {
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,
}

View File

@ -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()),

View File

@ -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<Body>) -> Response {
Response(
app.clone()
.oneshot(request(path, Method::POST, body))
.await
.unwrap(),
)
}
pub async fn put(app: &Router, path: impl ToString, body: Option<Body>) -> Response {
Response(
app.clone()
.oneshot(request(path, Method::PUT, body))
.await
.unwrap(),
)
}
fn request(path: impl ToString, method: Method, body: Option<Body>) -> Request<Body> {
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 {

View File

@ -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",
)

View File

@ -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!(

View File

@ -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<Context>) -> impl IntoResponse {
let rng = fastrand::Rng::new();
let session_id = repeat_with(|| rng.alphanumeric())
.take(32)
.collect::<String>();
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<Context>,
Path(id): Path<String>,
) -> 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<Context>,
Path(id): Path<String>,
Json(request): Json<AuthRequest>,
Path(session_id): Path<String>,
Json(request): Json<AuthChallenge>,
) -> 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<String, AuthState>,
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::<Vec<u8>>());
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);
}
}

View File

@ -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!(