From 245db1807b190b3674c81952fdd0a7caa90ab7af Mon Sep 17 00:00:00 2001 From: Niko PLP Date: Tue, 11 Jul 2023 22:00:02 +0300 Subject: [PATCH] add_invitation list_invitations in CLI --- Cargo.lock | 76 +++++++++++-- ngcli/Cargo.toml | 3 +- ngcli/src/main.rs | 129 ++++++++++++++++++++- ngd/src/cli.rs | 4 + ngd/src/main.rs | 1 + p2p-broker/src/broker_store/invitation.rs | 88 +++++++++++---- p2p-broker/src/server_ws.rs | 2 + p2p-broker/src/storage.rs | 31 +++++- p2p-broker/src/types.rs | 2 + p2p-net/src/actors/add_invitation.rs | 123 ++++++++++++++++++++ p2p-net/src/actors/list_invitations.rs | 130 ++++++++++++++++++++++ p2p-net/src/actors/mod.rs | 6 + p2p-net/src/broker.rs | 16 +++ p2p-net/src/broker_storage.rs | 12 ++ p2p-net/src/types.rs | 57 ++++++++++ p2p-repo/Cargo.toml | 1 + p2p-repo/src/kcv_store.rs | 1 + p2p-repo/src/types.rs | 8 ++ p2p-repo/src/utils.rs | 23 +++- stores-lmdb/src/kcv_store.rs | 7 +- 20 files changed, 686 insertions(+), 34 deletions(-) create mode 100644 p2p-net/src/actors/add_invitation.rs create mode 100644 p2p-net/src/actors/list_invitations.rs diff --git a/Cargo.lock b/Cargo.lock index d046a71..eff54ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -727,8 +727,11 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "time 0.1.45", + "wasm-bindgen", "winapi", ] @@ -1258,6 +1261,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "duration-str" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f037c488d179e21c87ef5fa9c331e8e62f5dddfa84618b41bb197da03edff1" +dependencies = [ + "chrono", + "nom", + "rust_decimal", + "serde", + "thiserror", + "time 0.3.23", +] + [[package]] name = "ed25519" version = "1.5.3" @@ -2576,6 +2593,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -2823,6 +2846,7 @@ dependencies = [ "base64-url", "blake3", "clap", + "duration-str", "ed25519-dalek", "env_logger", "futures", @@ -2917,6 +2941,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3197,6 +3231,7 @@ dependencies = [ "serde_bare", "serde_bytes", "slice_as_array", + "time 0.3.23", "wasm-bindgen", "web-time", "zeroize", @@ -3466,7 +3501,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time", + "time 0.3.23", ] [[package]] @@ -3918,6 +3953,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust_decimal" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -4176,7 +4221,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time", + "time 0.3.23", ] [[package]] @@ -4655,7 +4700,7 @@ dependencies = [ "sha2 0.10.7", "tauri-utils", "thiserror", - "time", + "time 0.3.23", "url", "uuid 1.3.4", "walkdir", @@ -4856,9 +4901,20 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" dependencies = [ "itoa 1.0.6", "serde", @@ -4874,9 +4930,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" dependencies = [ "time-core", ] @@ -5362,6 +5418,12 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/ngcli/Cargo.toml b/ngcli/Cargo.toml index 87774a4..314242a 100644 --- a/ngcli/Cargo.toml +++ b/ngcli/Cargo.toml @@ -29,4 +29,5 @@ getrandom = "0.2.7" blake3 = "1.3.1" serde = { version = "1.0", features = ["derive"] } serde_bare = "0.5.0" -serde_bytes = "0.11.7" \ No newline at end of file +serde_bytes = "0.11.7" +duration-str = "0.5.1" \ No newline at end of file diff --git a/ngcli/src/main.rs b/ngcli/src/main.rs index 31bbb87..bce112c 100644 --- a/ngcli/src/main.rs +++ b/ngcli/src/main.rs @@ -11,6 +11,7 @@ use ed25519_dalek::*; +use duration_str::parse; use futures::{future, pin_mut, stream, SinkExt, StreamExt}; use p2p_net::actors::*; use p2p_repo::object::Object; @@ -35,7 +36,9 @@ use p2p_net::types::*; use p2p_repo::log::*; use p2p_repo::types::*; -use p2p_repo::utils::{decode_key, generate_keypair, now_timestamp}; +use p2p_repo::utils::{ + decode_key, display_timestamp, generate_keypair, now_timestamp, timestamp_after, +}; use clap::{arg, command, value_parser, ArgAction, Command}; @@ -140,6 +143,22 @@ async fn main() -> Result<(), ProtocolError> { Command::new("list-users") .about("list all users registered in the broker") .arg(arg!(-a --admin "only lists admin users. otherwise, lists only non admin users").required(false))) + .subcommand( + Command::new("add-invitation") + .about("add an invitation to register on the server") + .arg(arg!([EXPIRES] "offset (from now) of time after which the invitation should expire. Format example: 1w 1d 1m. default unit is second").conflicts_with("forever")) + .arg(arg!(-a --admin "user registered with this invitation will have admin permissions").required(false)) + .arg(arg!(-i --multi "many users can use this invitation to register themselves, until the invitation code is deleted by an admin").required(false).conflicts_with("admin").conflicts_with("unique")) + .arg(arg!(-u --unique "this invitation can be used only once. this is the default").required(false).conflicts_with("admin")) + .arg(arg!(-f --forever "this invitation does not expire. it can be used forever (or until deleted by an admin). default if no EXPIRES provided").required(false)) + .arg(arg!(-n --name "optional name of this broker that will be displayed to the user when registering: You have been invited to register an account at [NAME]").required(false)) + .arg(arg!(-m --memo "optional memo about this invitation that will be kept in the server. it will help you to remember who you invited and to manage the invitation").required(false))) + .subcommand( + Command::new("list-invitations") + .about("list all invitations") + .arg(arg!(-a --admin "only lists admin invitations").required(false)) + .arg(arg!(-m --multi "only lists multiple-use invitations").required(false)) + .arg(arg!(-u --unique "only lists unique-use invitations").required(false))) ) .subcommand( Command::new("gen-user") @@ -445,6 +464,114 @@ async fn main() -> Result<(), ProtocolError> { } return res.map(|_| ()); } + Some(("add-invitation", sub2_matches)) => { + log_debug!("add-invitation"); + let expires = sub2_matches.get_one::("EXPIRES"); + let expiry = if expires.is_some() { + let duration = parse(expires.unwrap().as_str()).unwrap(); + timestamp_after(duration) + } else { + 0 + }; + let admin = sub2_matches.get_flag("admin"); + let multi = sub2_matches.get_flag("multi"); + let _unique = sub2_matches.get_flag("unique"); + + let symkey = SymKey::random(); + let invite_code = if admin { + InvitationCode::Admin(symkey.clone()) + } else if multi { + InvitationCode::Multi(symkey.clone()) + } else { + InvitationCode::Unique(symkey.clone()) + }; + + let mut res = do_admin_call( + keys[1], + config_v0, + AddInvitation::V0(AddInvitationV0 { + invite_code, + expiry, + memo: sub2_matches.get_one::("memo").map(|s| s.clone()), + }), + ) + .await; + match res.as_mut() { + Err(e) => log_err!("An error occurred: {e}"), + Ok(AdminResponseContentV0::Invitation(invitation)) => { + invitation + .set_name(sub2_matches.get_one::("name").map(|s| s.clone())); + + log_debug!("{:?}", invitation); + println!("Invitation created successfully. please note carefully the following links. share one of them with the invited user(s)"); + for link in invitation.get_urls() { + println!("The invitation link is: {}", link) + } + } + _ => { + log_err!("Invalid response"); + return Err(ProtocolError::InvalidValue); + } + } + return res.map(|_| ()); + } + Some(("list-invitations", sub2_matches)) => { + log_debug!("invitations"); + let admin = sub2_matches.get_flag("admin"); + let multi = sub2_matches.get_flag("multi"); + let unique = sub2_matches.get_flag("unique"); + let res = do_admin_call( + keys[1], + config_v0, + ListInvitations::V0(ListInvitationsV0 { + admin, + multi, + unique, + }), + ) + .await; + match &res { + Err(e) => log_err!("An error occurred: {e}"), + Ok(AdminResponseContentV0::Invitations(list)) => { + println!( + "Found {} {}invitations", + list.len(), + if admin && multi && unique { + "".to_string() + } else { + let mut name = vec![]; + if admin { + name.push("admin "); + } + if multi { + name.push("multi "); + } + if unique { + name.push("unique "); + } + name.join("or ") + } + ); + for invite in list { + println!( + "{} expires {}. memo={}", + invite.0, + if invite.1 == 0 { + "never".to_string() + } else { + display_timestamp(&invite.1) + }, + invite.2.as_ref().unwrap_or(&"".to_string()) + ); + } + } + _ => { + log_err!("Invalid response"); + return Err(ProtocolError::InvalidValue); + } + } + return res.map(|_| ()); + } _ => panic!("shouldn't happen"), }, _ => println!("Nothing to do."), diff --git a/ngd/src/cli.rs b/ngd/src/cli.rs index 1de91ed..ef2dd01 100644 --- a/ngd/src/cli.rs +++ b/ngd/src/cli.rs @@ -112,6 +112,10 @@ pub(crate) struct Cli { #[arg(long, conflicts_with("registration_off"))] pub registration_open: bool, + /// Registration URL used when creating invitation links, an optional url to redirect the user to, for accepting ToS and making payment, if any. + #[arg(long)] + pub registration_url: Option, + /// Admin userID #[arg(long)] pub admin: Option, diff --git a/ngd/src/main.rs b/ngd/src/main.rs index 2448ed3..7713009 100644 --- a/ngd/src/main.rs +++ b/ngd/src/main.rs @@ -935,6 +935,7 @@ async fn main_inner() -> Result<(), ()> { overlays_configs: vec![overlays_config], registration, admin_user, + registration_url: args.registration_url, })); if args.print_config { diff --git a/p2p-broker/src/broker_store/invitation.rs b/p2p-broker/src/broker_store/invitation.rs index f66b5bf..25f411e 100644 --- a/p2p-broker/src/broker_store/invitation.rs +++ b/p2p-broker/src/broker_store/invitation.rs @@ -17,6 +17,7 @@ use std::time::SystemTime; use p2p_net::types::*; use p2p_repo::kcv_store::KCVStore; use p2p_repo::store::*; +use p2p_repo::types::SymKey; use p2p_repo::types::Timestamp; use p2p_repo::utils::now_timestamp; use serde_bare::from_slice; @@ -33,9 +34,13 @@ impl<'a> Invitation<'a> { // propertie's invitation suffixes const TYPE: u8 = b"t"[0]; - const EXPIRE: u8 = b"e"[0]; + //const EXPIRE: u8 = b"e"[0]; - const ALL_PROPERTIES: [u8; 2] = [Self::TYPE, Self::EXPIRE]; + const PREFIX_EXPIRE: u8 = b"e"[0]; + // propertie's expiry suffixes + const INVITATION: u8 = b"i"[0]; + + const ALL_PROPERTIES: [u8; 1] = [Self::TYPE]; const SUFFIX_FOR_EXIST_CHECK: u8 = Self::TYPE; @@ -52,12 +57,13 @@ impl<'a> Invitation<'a> { pub fn create( id: &InvitationCode, expiry: u32, + memo: &Option, store: &'a dyn KCVStore, ) -> Result, StorageError> { let (code_type, code) = match id { - InvitationCode::Unique(c) => (0, c.slice()), - InvitationCode::Multi(c) => (1, c.slice()), - InvitationCode::Admin(c) => (2, c.slice()), + InvitationCode::Unique(c) => (0u8, c.slice()), + InvitationCode::Multi(c) => (1u8, c.slice()), + InvitationCode::Admin(c) => (2u8, c.slice()), }; let acc = Invitation { id: code.clone(), @@ -66,23 +72,65 @@ impl<'a> Invitation<'a> { if acc.exists() { return Err(StorageError::BackendError); } + let mut value = to_vec(&(code_type, expiry, memo.clone()))?; store.write_transaction(&|tx| { - tx.put( - Self::PREFIX, - &to_vec(code)?, - Some(Self::TYPE), - &to_vec(&code_type)?, - )?; - tx.put( - Self::PREFIX, - &to_vec(code)?, - Some(Self::EXPIRE), - &to_vec(&expiry)?, - )?; + tx.put(Self::PREFIX, &to_vec(code)?, Some(Self::TYPE), &value)?; Ok(()) })?; Ok(acc) } + + pub fn get_all_invitations( + store: &'a dyn KCVStore, + mut admin: bool, + mut unique: bool, + mut multi: bool, + ) -> Result)>, StorageError> { + let size = to_vec(&[0u8; 32])?.len(); + let mut res: Vec<(InvitationCode, u32, Option)> = vec![]; + if !admin && !unique && !multi { + admin = true; + unique = true; + multi = true; + } + for invite in store.get_all_keys_and_values(Self::PREFIX, size, None)? { + if invite.0.len() == size + 2 { + let code: [u8; 32] = from_slice(&invite.0[1..invite.0.len() - 1])?; + if invite.0[size + 1] == Self::TYPE { + let code_type: (u8, u32, Option) = from_slice(&invite.1)?; + let inv_code = match code_type { + (0, ex, memo) => { + if unique { + Some((InvitationCode::Unique(SymKey::ChaCha20Key(code)), ex, memo)) + } else { + None + } + } + (1, ex, memo) => { + if multi { + Some((InvitationCode::Multi(SymKey::ChaCha20Key(code)), ex, memo)) + } else { + None + } + } + (2, ex, memo) => { + if admin { + Some((InvitationCode::Admin(SymKey::ChaCha20Key(code)), ex, memo)) + } else { + None + } + } + _ => panic!("invalid code type value"), + }; + if inv_code.is_some() { + res.push(inv_code.unwrap()); + } + } + } + } + Ok(res) + } + pub fn exists(&self) -> bool { self.store .get( @@ -99,9 +147,9 @@ impl<'a> Invitation<'a> { pub fn is_expired(&self) -> Result { let expire_ser = self .store - .get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::EXPIRE))?; - let expire: u32 = from_slice(&expire_ser)?; - if expire < now_timestamp() { + .get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::TYPE))?; + let expire: (u8, u32, Option) = from_slice(&expire_ser)?; + if expire.1 < now_timestamp() { return Ok(true); } Ok(false) diff --git a/p2p-broker/src/server_ws.rs b/p2p-broker/src/server_ws.rs index ad3423d..5ba69da 100644 --- a/p2p-broker/src/server_ws.rs +++ b/p2p-broker/src/server_ws.rs @@ -787,7 +787,9 @@ pub async fn run_server_v0( overlays_configs: config.overlays_configs, registration: config.registration, admin_user: config.admin_user, + registration_url: config.registration_url, peer_id, + bootstrap, }; broker.set_server_config(server_config); } diff --git a/p2p-broker/src/storage.rs b/p2p-broker/src/storage.rs index e7a3a3d..04daf40 100644 --- a/p2p-broker/src/storage.rs +++ b/p2p-broker/src/storage.rs @@ -59,7 +59,12 @@ impl LmdbBrokerStorage { let accounts_storage = LmdbKCVStore::open(&accounts_path, accounts_key.slice().clone()); let symkey = SymKey::random(); let invite_code = InvitationCode::Admin(symkey.clone()); - let _ = Invitation::create(&invite_code, 0, &accounts_storage)?; + let _ = Invitation::create( + &invite_code, + 0, + &Some("admin user automatically invited at first startup".to_string()), + &accounts_storage, + )?; let invitation = p2p_net::types::Invitation::V0(InvitationV0 { code: Some(symkey), name: Some("your NG Box, as admin".into()), @@ -110,4 +115,28 @@ impl BrokerStorage for LmdbBrokerStorage { log_debug!("list_users that are admin == {admins}"); Ok(Account::get_all_users(admins, &self.accounts_storage)?) } + fn list_invitations( + &self, + admin: bool, + unique: bool, + multi: bool, + ) -> Result)>, ProtocolError> { + log_debug!("list_invitations admin={admin} unique={unique} multi={multi}"); + Ok(Invitation::get_all_invitations( + &self.accounts_storage, + admin, + unique, + multi, + )?) + } + fn add_invitation( + &self, + invite_code: &InvitationCode, + expiry: u32, + memo: &Option, + ) -> Result<(), ProtocolError> { + log_debug!("add_invitation {invite_code} expiry {expiry}"); + Invitation::create(invite_code, expiry, memo, &self.accounts_storage)?; + Ok(()) + } } diff --git a/p2p-broker/src/types.rs b/p2p-broker/src/types.rs index 757df4a..3a0bad5 100644 --- a/p2p-broker/src/types.rs +++ b/p2p-broker/src/types.rs @@ -21,6 +21,8 @@ pub struct DaemonConfigV0 { pub registration: RegistrationConfig, pub admin_user: Option, + + pub registration_url: Option, } /// Daemon config diff --git a/p2p-net/src/actors/add_invitation.rs b/p2p-net/src/actors/add_invitation.rs new file mode 100644 index 0000000..723aa8d --- /dev/null +++ b/p2p-net/src/actors/add_invitation.rs @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2022-2023 Niko Bonnieure, Par le Peuple, NextGraph.org developers + * All rights reserved. + * Licensed under the Apache License, Version 2.0 + * + * or the MIT license , + * at your option. All files in the project carrying such + * notice may not be copied, modified, or distributed except + * according to those terms. +*/ +use crate::broker::{ServerConfig, BROKER}; +use crate::connection::NoiseFSM; +use crate::types::*; +use crate::{actor::*, errors::ProtocolError, types::ProtocolMessage}; + +use async_std::sync::Mutex; +use p2p_repo::log::*; +use p2p_repo::types::PubKey; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use super::StartProtocol; + +/// Add invitation +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AddInvitationV0 { + pub invite_code: InvitationCode, + pub expiry: u32, + pub memo: Option, +} + +/// Add invitation +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum AddInvitation { + V0(AddInvitationV0), +} + +impl AddInvitation { + pub fn code(&self) -> &InvitationCode { + match self { + AddInvitation::V0(o) => &o.invite_code, + } + } + pub fn expiry(&self) -> u32 { + match self { + AddInvitation::V0(o) => o.expiry, + } + } + pub fn memo(&self) -> &Option { + match self { + AddInvitation::V0(o) => &o.memo, + } + } + pub fn get_actor(&self) -> Box { + Actor::::new_responder() + } +} + +impl TryFrom for AddInvitation { + type Error = ProtocolError; + fn try_from(msg: ProtocolMessage) -> Result { + if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 { + content: AdminRequestContentV0::AddInvitation(a), + .. + }))) = msg + { + Ok(a) + } else { + log_debug!("INVALID {:?}", msg); + Err(ProtocolError::InvalidValue) + } + } +} + +impl From for ProtocolMessage { + fn from(msg: AddInvitation) -> ProtocolMessage { + unimplemented!(); + } +} + +impl From for AdminRequestContentV0 { + fn from(msg: AddInvitation) -> AdminRequestContentV0 { + AdminRequestContentV0::AddInvitation(msg) + } +} + +impl Actor<'_, AddInvitation, AdminResponse> {} + +#[async_trait::async_trait] +impl EActor for Actor<'_, AddInvitation, AdminResponse> { + async fn respond( + &mut self, + msg: ProtocolMessage, + fsm: Arc>, + ) -> Result<(), ProtocolError> { + let req = AddInvitation::try_from(msg)?; + let broker = BROKER.read().await; + broker + .get_storage()? + .add_invitation(req.code(), req.expiry(), req.memo())?; + + let invitation = crate::types::Invitation::V0(InvitationV0::new( + broker.get_bootstrap()?.clone(), + Some(req.code().get_symkey()), + None, + broker.get_registration_url().map(|s| s.clone()), + )); + let response: AdminResponseV0 = invitation.into(); + fsm.lock().await.send(response.into()).await?; + Ok(()) + } +} + +impl From for AdminResponseV0 { + fn from(res: Invitation) -> AdminResponseV0 { + AdminResponseV0 { + id: 0, + result: 0, + content: AdminResponseContentV0::Invitation(res), + padding: vec![], + } + } +} diff --git a/p2p-net/src/actors/list_invitations.rs b/p2p-net/src/actors/list_invitations.rs new file mode 100644 index 0000000..7d3a37d --- /dev/null +++ b/p2p-net/src/actors/list_invitations.rs @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022-2023 Niko Bonnieure, Par le Peuple, NextGraph.org developers + * All rights reserved. + * Licensed under the Apache License, Version 2.0 + * + * or the MIT license , + * at your option. All files in the project carrying such + * notice may not be copied, modified, or distributed except + * according to those terms. +*/ +use crate::broker::BROKER; +use crate::connection::NoiseFSM; +use crate::types::*; +use crate::{actor::*, errors::ProtocolError, types::ProtocolMessage}; + +use async_std::sync::Mutex; +use p2p_repo::log::*; +use p2p_repo::types::PubKey; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use super::StartProtocol; + +/// List invitations registered on this broker +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct ListInvitationsV0 { + /// should list only the admin invitations. + pub admin: bool, + /// should list only the unique invitations. + pub unique: bool, + /// should list only the multi invitations. + pub multi: bool, +} + +/// List invitations registered on this broker +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum ListInvitations { + V0(ListInvitationsV0), +} + +impl ListInvitations { + pub fn admin(&self) -> bool { + match self { + Self::V0(o) => o.admin, + } + } + pub fn unique(&self) -> bool { + match self { + Self::V0(o) => o.unique, + } + } + pub fn multi(&self) -> bool { + match self { + Self::V0(o) => o.multi, + } + } + pub fn get_actor(&self) -> Box { + Actor::::new_responder() + } +} + +impl TryFrom for ListInvitations { + type Error = ProtocolError; + fn try_from(msg: ProtocolMessage) -> Result { + if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 { + content: AdminRequestContentV0::ListInvitations(a), + .. + }))) = msg + { + Ok(a) + } else { + //log_debug!("INVALID {:?}", msg); + Err(ProtocolError::InvalidValue) + } + } +} + +impl From for ProtocolMessage { + fn from(msg: ListInvitations) -> ProtocolMessage { + unimplemented!(); + } +} + +impl From for AdminRequestContentV0 { + fn from(msg: ListInvitations) -> AdminRequestContentV0 { + AdminRequestContentV0::ListInvitations(msg) + } +} + +impl Actor<'_, ListInvitations, AdminResponse> {} + +#[async_trait::async_trait] +impl EActor for Actor<'_, ListInvitations, AdminResponse> { + async fn respond( + &mut self, + msg: ProtocolMessage, + fsm: Arc>, + ) -> Result<(), ProtocolError> { + let req = ListInvitations::try_from(msg)?; + let res = BROKER.read().await.get_storage()?.list_invitations( + req.admin(), + req.unique(), + req.multi(), + ); + let response: AdminResponseV0 = res.into(); + fsm.lock().await.send(response.into()).await?; + Ok(()) + } +} + +impl From)>, ProtocolError>> for AdminResponseV0 { + fn from( + res: Result)>, ProtocolError>, + ) -> AdminResponseV0 { + match res { + Err(e) => AdminResponseV0 { + id: 0, + result: e.into(), + content: AdminResponseContentV0::EmptyResponse, + padding: vec![], + }, + Ok(vec) => AdminResponseV0 { + id: 0, + result: 0, + content: AdminResponseContentV0::Invitations(vec), + padding: vec![], + }, + } + } +} diff --git a/p2p-net/src/actors/mod.rs b/p2p-net/src/actors/mod.rs index b77c748..fe3b8af 100644 --- a/p2p-net/src/actors/mod.rs +++ b/p2p-net/src/actors/mod.rs @@ -15,3 +15,9 @@ pub use del_user::*; pub mod list_users; pub use list_users::*; + +pub mod add_invitation; +pub use add_invitation::*; + +pub mod list_invitations; +pub use list_invitations::*; diff --git a/p2p-net/src/broker.rs b/p2p-net/src/broker.rs index 2ba1cab..f8f7120 100644 --- a/p2p-net/src/broker.rs +++ b/p2p-net/src/broker.rs @@ -65,6 +65,9 @@ pub struct ServerConfig { pub registration: RegistrationConfig, pub admin_user: Option, pub peer_id: PubKey, + // when creating invitation links, an optional url to redirect the user to can be used, for accepting ToS and making payment, if any. + pub registration_url: Option, + pub bootstrap: BootstrapContent, } pub static BROKER: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(Broker::new()))); @@ -108,6 +111,19 @@ impl<'a> Broker<'a> { self.config.as_ref() } + pub fn get_registration_url(&self) -> Option<&String> { + self.config + .as_ref() + .and_then(|c| c.registration_url.as_ref()) + } + + pub fn get_bootstrap(&self) -> Result<&BootstrapContent, ProtocolError> { + self.config + .as_ref() + .map(|c| &c.bootstrap) + .ok_or(ProtocolError::BrokerError) + } + pub fn set_storage(&mut self, storage: impl BrokerStorage + 'a) { //log_debug!("set_storage"); self.storage = Some(Box::new(storage)); diff --git a/p2p-net/src/broker_storage.rs b/p2p-net/src/broker_storage.rs index 4cf0bdf..8a0338f 100644 --- a/p2p-net/src/broker_storage.rs +++ b/p2p-net/src/broker_storage.rs @@ -16,4 +16,16 @@ pub trait BrokerStorage: Send + Sync + std::fmt::Debug { fn get_user(&self, user_id: PubKey) -> Result; fn add_user(&self, user_id: PubKey, is_admin: bool) -> Result<(), ProtocolError>; fn list_users(&self, admins: bool) -> Result, ProtocolError>; + fn list_invitations( + &self, + admin: bool, + unique: bool, + multi: bool, + ) -> Result)>, ProtocolError>; + fn add_invitation( + &self, + invite_code: &InvitationCode, + expiry: u32, + memo: &Option, + ) -> Result<(), ProtocolError>; } diff --git a/p2p-net/src/types.rs b/p2p-net/src/types.rs index f72502c..7b68732 100644 --- a/p2p-net/src/types.rs +++ b/p2p-net/src/types.rs @@ -527,6 +527,24 @@ pub enum InvitationCode { Multi(SymKey), } +impl InvitationCode { + pub fn get_symkey(&self) -> SymKey { + match self { + Self::Unique(s) | Self::Admin(s) | Self::Multi(s) => s.clone(), + } + } +} + +impl fmt::Display for InvitationCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Unique(k) => write!(f, "unique {}", k), + Self::Admin(k) => write!(f, "admin {}", k), + Self::Multi(k) => write!(f, "multi {}", k), + } + } +} + /// Invitation to create an account at a broker. Version 0 #[derive(Clone, Debug, Serialize, Deserialize)] pub struct InvitationV0 { @@ -542,6 +560,29 @@ pub struct InvitationV0 { pub url: Option, } +impl InvitationV0 { + pub fn set_bootstrap(&mut self, content: BootstrapContent) { + match content { + BootstrapContent::V0(v0) => self.bootstrap = v0, + } + } + pub fn new( + bootstrap_content: BootstrapContent, + code: Option, + name: Option, + url: Option, + ) -> Self { + match bootstrap_content { + BootstrapContent::V0(v0) => InvitationV0 { + bootstrap: v0, + code, + name, + url, + }, + } + } +} + impl Invitation { pub fn new_v0( bootstrap: BootstrapContentV0, @@ -594,6 +635,14 @@ impl Invitation { } } + pub fn set_name(&mut self, name: Option) { + if name.is_some() { + match self { + Invitation::V0(v0) => v0.name = Some(name.unwrap()), + } + } + } + /// first URL in the list is the ngone one pub fn get_urls(&self) -> Vec { match self { @@ -1753,6 +1802,8 @@ pub enum AdminRequestContentV0 { AddUser(AddUser), DelUser(DelUser), ListUsers(ListUsers), + ListInvitations(ListInvitations), + AddInvitation(AddInvitation), } impl AdminRequestContentV0 { pub fn type_id(&self) -> TypeId { @@ -1760,6 +1811,8 @@ impl AdminRequestContentV0 { Self::AddUser(a) => a.type_id(), Self::DelUser(a) => a.type_id(), Self::ListUsers(a) => a.type_id(), + Self::ListInvitations(a) => a.type_id(), + Self::AddInvitation(a) => a.type_id(), } } pub fn get_actor(&self) -> Box { @@ -1767,6 +1820,8 @@ impl AdminRequestContentV0 { Self::AddUser(a) => a.get_actor(), Self::DelUser(a) => a.get_actor(), Self::ListUsers(a) => a.get_actor(), + Self::ListInvitations(a) => a.get_actor(), + Self::AddInvitation(a) => a.get_actor(), } } } @@ -1849,6 +1904,8 @@ impl From for ProtocolMessage { pub enum AdminResponseContentV0 { EmptyResponse, Users(Vec), + Invitations(Vec<(InvitationCode, u32, Option)>), + Invitation(Invitation), } /// Response to an `AdminRequest` V0 diff --git a/p2p-repo/Cargo.toml b/p2p-repo/Cargo.toml index 9c932fa..d01b36a 100644 --- a/p2p-repo/Cargo.toml +++ b/p2p-repo/Cargo.toml @@ -28,6 +28,7 @@ wasm-bindgen = "0.2" slice_as_array = "1.1.0" curve25519-dalek = "3.2.0" zeroize = { version = "1.6.0", features = ["zeroize_derive"] } +time = { version= "0.3.23", features = ["formatting"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] debug_print = "1.0.0" diff --git a/p2p-repo/src/kcv_store.rs b/p2p-repo/src/kcv_store.rs index 3fc76d5..afa4ee2 100644 --- a/p2p-repo/src/kcv_store.rs +++ b/p2p-repo/src/kcv_store.rs @@ -69,6 +69,7 @@ pub trait ReadTransaction { value: &Vec, ) -> Result<(), StorageError>; + /// retrieves all the keys and values with the given prefix and key_size. if no suffix is specified, then all (including none) the suffices are returned fn get_all_keys_and_values( &self, prefix: u8, diff --git a/p2p-repo/src/types.rs b/p2p-repo/src/types.rs index 6ab13f1..1d41c92 100644 --- a/p2p-repo/src/types.rs +++ b/p2p-repo/src/types.rs @@ -69,6 +69,14 @@ impl SymKey { } } +impl fmt::Display for SymKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ChaCha20Key(k) => write!(f, "{}", base64_url::encode(k)), + } + } +} + impl TryFrom<&[u8]> for SymKey { type Error = NgError; fn try_from(buf: &[u8]) -> Result { diff --git a/p2p-repo/src/utils.rs b/p2p-repo/src/utils.rs index 005b7c0..7cb504d 100644 --- a/p2p-repo/src/utils.rs +++ b/p2p-repo/src/utils.rs @@ -18,7 +18,8 @@ use ed25519_dalek::*; use futures::channel::mpsc; use rand::rngs::OsRng; use rand::RngCore; -use web_time::{SystemTime, UNIX_EPOCH}; +use time::OffsetDateTime; +use web_time::{Duration, SystemTime, UNIX_EPOCH}; use zeroize::Zeroize; pub fn ed_keypair_from_priv_bytes(secret_key: [u8; 32]) -> (PrivKey, PubKey) { @@ -166,4 +167,24 @@ pub fn now_timestamp() -> Timestamp { .unwrap() } +/// returns a new NextGraph Timestamp equivalent to the duration after now. +pub fn timestamp_after(duration: Duration) -> Timestamp { + (((SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + duration).as_secs() + - EPOCH_AS_UNIX_TIMESTAMP) + / 60) + .try_into() + .unwrap() +} + +/// displays the NextGraph Timestamp in UTC. +pub fn display_timestamp(ts: &Timestamp) -> String { + let st = SystemTime::UNIX_EPOCH + + Duration::from_secs(EPOCH_AS_UNIX_TIMESTAMP) + + Duration::from_secs(*ts as u64 * 60u64); + let dt: OffsetDateTime = st.into(); + + dt.format(&time::format_description::parse("[day]/[month]/[year] [hour]:[minute] UTC").unwrap()) + .unwrap() +} + pub type Receiver = mpsc::UnboundedReceiver; diff --git a/stores-lmdb/src/kcv_store.rs b/stores-lmdb/src/kcv_store.rs index b1274c2..e4967de 100644 --- a/stores-lmdb/src/kcv_store.rs +++ b/stores-lmdb/src/kcv_store.rs @@ -251,7 +251,8 @@ impl ReadTransaction for LmdbKCVStore { let vec_key_start = vec![0u8; key_size]; let vec_key_end = vec![255u8; key_size]; let property_start = Self::compute_property(prefix, &vec_key_start, suffix); - let property_end = Self::compute_property(prefix, &vec_key_end, suffix); + let property_end = + Self::compute_property(prefix, &vec_key_end, Some(suffix.unwrap_or(255u8))); let lock = self.environment.read().unwrap(); let reader = lock.read().unwrap(); let mut iter = self @@ -270,8 +271,8 @@ impl ReadTransaction for LmdbKCVStore { { continue; } - } else if val.0.len() > (key_size + 1) { - continue; + // } else if val.0.len() > (key_size + 1) { + // continue; } vector.push((val.0.to_vec(), val.1.to_bytes().unwrap())); }