diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index 700be2f..65f222a 100644 --- a/.github/workflows/lint_and_test.yml +++ b/.github/workflows/lint_and_test.yml @@ -28,4 +28,4 @@ jobs: - name: Run tests env: RUST_MIN_STACK: 8388608 - run: cargo test --verbose + run: cargo test --verbose -- --test-threads=1 --nocapture diff --git a/Cargo.toml b/Cargo.toml index 6aaee9c..7af6a0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,5 @@ anyhow = "1.0.86" tokio = {version ="1.39.3", features=["full"] } async-stream = "0.3.5" futures-core = "0.3.30" +url = "2.5.2" +hex = "0.4.3" diff --git a/src/backend.rs b/src/backend.rs index 76f1865..b50ebaf 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,19 +1,26 @@ use crate::common::{CommonKeypair, DHTEntity}; -use crate::group::Group; +use crate::group::{Group, URL_DHT_KEY, URL_ENCRYPTION_KEY, URL_PUBLIC_KEY, URL_SECRET_KEY}; use crate::repo::Repo; use anyhow::{anyhow, Result}; +use clap::builder::Str; use iroh_blobs::Hash; use std::collections::HashMap; use std::mem; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs; -use tokio::sync::{mpsc::{self, Receiver}, oneshot, broadcast}; +use tokio::sync::{ + broadcast, + mpsc::{self, Receiver}, + oneshot, +}; use tracing::info; +use url::Url; use veilid_core::{ api_startup_config, vld0_generate_keypair, CryptoKey, CryptoSystem, CryptoSystemVLD0, - CryptoTyped, DHTSchema, KeyPair, ProtectedStore, RoutingContext, SharedSecret, UpdateCallback, - VeilidAPI, VeilidConfigInner, VeilidUpdate, CRYPTO_KIND_VLD0, TypedKey, VeilidConfigProtectedStore + CryptoTyped, DHTSchema, KeyPair, ProtectedStore, RoutingContext, SharedSecret, TypedKey, + UpdateCallback, VeilidAPI, VeilidConfigInner, VeilidConfigProtectedStore, VeilidUpdate, + CRYPTO_KEY_LENGTH, CRYPTO_KIND_VLD0, }; use xdg::BaseDirectories; @@ -32,7 +39,7 @@ impl Backend { path: base_path.to_path_buf(), port, veilid_api: None, - update_rx: None, + update_rx: None, groups: HashMap::new(), repos: HashMap::new(), }) @@ -129,7 +136,10 @@ impl Backend { Ok(()) } - async fn wait_for_network(&self, mut update_rx: broadcast::Receiver) -> Result<()> { + async fn wait_for_network( + &self, + mut update_rx: broadcast::Receiver, + ) -> Result<()> { while let Ok(update) = update_rx.recv().await { if let VeilidUpdate::Attachment(attachment_state) = update { if attachment_state.public_internet_ready { @@ -140,7 +150,48 @@ impl Backend { } Ok(()) } - + + pub async fn join_from_url(&mut self, url_string: &str) -> Result> { + let keys = parse_url(url_string)?; + self.join_group(keys).await + } + + pub async fn join_group(&mut self, keys: CommonKeypair) -> Result> { + let routing_context = self.veilid_api.as_ref().unwrap().routing_context()?; + let crypto_system = CryptoSystemVLD0::new(self.veilid_api.as_ref().unwrap().crypto()?); + + let record_key = TypedKey::new(CRYPTO_KIND_VLD0, keys.id); + // First open the DHT record + let dht_record = routing_context + .open_dht_record(record_key.clone(), None) // Don't pass a writer here yet + .await?; + + // Use the owner key from the DHT record as the default writer + let owner_key = dht_record.owner(); // Call the owner() method to get the owner key + + // Reopen the DHT record with the owner key as the writer + let dht_record = routing_context + .open_dht_record( + record_key.clone(), + Some(KeyPair::new( + owner_key.clone(), + keys.secret_key.clone().unwrap(), + )), + ) + .await?; + + let group = Group { + dht_record: dht_record.clone(), + encryption_key: keys.encryption_key.clone(), + routing_context: Arc::new(routing_context), + crypto_system, + repos: Vec::new(), + }; + self.groups.insert(group.id(), Box::new(group.clone())); + + Ok(Box::new(group)) + } + pub async fn create_group(&mut self) -> Result { let veilid = self .veilid_api @@ -149,33 +200,33 @@ impl Backend { let routing_context = veilid.routing_context()?; let schema = DHTSchema::dflt(3)?; let kind = Some(CRYPTO_KIND_VLD0); - + let dht_record = routing_context.create_dht_record(schema, kind).await?; let keypair = vld0_generate_keypair(); let crypto_system = CryptoSystemVLD0::new(veilid.crypto()?); - + let encryption_key = crypto_system.random_shared_secret(); - + let group = Group::new( dht_record.clone(), encryption_key, Arc::new(routing_context), crypto_system, ); - + let protected_store = veilid.protected_store().unwrap(); CommonKeypair { - id: group.id(), - public_key: dht_record.owner().clone(), + id: group.id(), + public_key: dht_record.owner().clone(), secret_key: group.get_secret_key(), encryption_key: group.get_encryption_key(), } .store_keypair(&protected_store) .await .map_err(|e| anyhow!(e))?; - + self.groups.insert(group.id(), Box::new(group.clone())); - + Ok(group) } @@ -183,41 +234,16 @@ impl Backend { if let Some(group) = self.groups.get(&record_key.value) { return Ok(group.clone()); } - + let routing_context = self.veilid_api.as_ref().unwrap().routing_context()?; let protected_store = self.veilid_api.as_ref().unwrap().protected_store().unwrap(); - + // Load the keypair associated with the record_key from the protected store let retrieved_keypair = CommonKeypair::load_keypair(&protected_store, &record_key.value) .await .map_err(|_| anyhow!("Failed to load keypair"))?; - let crypto_system = CryptoSystemVLD0::new(self.veilid_api.as_ref().unwrap().crypto()?); - - // First open the DHT record - let dht_record = routing_context - .open_dht_record(record_key.clone(), None) // Don't pass a writer here yet - .await?; - - // Use the owner key from the DHT record as the default writer - let owner_key = dht_record.owner(); // Call the owner() method to get the owner key - - // Reopen the DHT record with the owner key as the writer - let dht_record = routing_context - .open_dht_record(record_key.clone(), Some(KeyPair::new(owner_key.clone(), retrieved_keypair.secret_key.clone().unwrap()))) - .await?; - - - let group = Group { - dht_record: dht_record.clone(), - encryption_key: retrieved_keypair.encryption_key.clone(), - routing_context: Arc::new(routing_context), - crypto_system, - repos: Vec::new(), - }; - self.groups.insert(group.id(), Box::new(group.clone())); - - Ok(Box::new(group)) + self.join_group(retrieved_keypair).await } pub async fn list_groups(&self) -> Result>> { @@ -320,9 +346,45 @@ impl Backend { pub fn subscribe_updates(&self) -> Option> { self.update_rx.as_ref().map(|rx| rx.resubscribe()) - } + } pub fn get_veilid_api(&self) -> Option<&VeilidAPI> { self.veilid_api.as_ref() } } + +fn find_query(url: &Url, key: &str) -> Result { + for (query_key, value) in url.query_pairs() { + if query_key == key { + return Ok(value.into_owned()); + } + } + + Err(anyhow!("Unable to find parameter {} in URL {:?}", key, url)) +} + +fn crypto_key_from_query(url: &Url, key: &str) -> Result { + let value = find_query(url, key)?; + let bytes = hex::decode(value)?; + let mut key_vec: [u8; CRYPTO_KEY_LENGTH] = [0; CRYPTO_KEY_LENGTH]; + key_vec.copy_from_slice(bytes.as_slice()); + + let key = CryptoKey::from(key_vec); + Ok(key) +} + +pub fn parse_url(url_string: &str) -> Result { + let url = Url::parse(url_string)?; + + let id = crypto_key_from_query(&url, URL_DHT_KEY)?; + let encryption_key = crypto_key_from_query(&url, URL_ENCRYPTION_KEY)?; + let public_key = crypto_key_from_query(&url, URL_PUBLIC_KEY)?; + let secret_key = Some(crypto_key_from_query(&url, URL_SECRET_KEY)?); + + Ok(CommonKeypair { + id, + public_key, + secret_key, + encryption_key, + }) +} diff --git a/src/group.rs b/src/group.rs index 2b3e989..3eab971 100644 --- a/src/group.rs +++ b/src/group.rs @@ -1,13 +1,22 @@ -use serde::{Serialize, Deserialize}; -use eyre::{Result, Error, anyhow}; +use eyre::{anyhow, Error, Result}; +use hex::ToHex; +use serde::{Deserialize, Serialize}; use std::sync::Arc; +use url::Url; use veilid_core::{ - CryptoKey, DHTRecordDescriptor, CryptoTyped, CryptoSystemVLD0, RoutingContext, SharedSecret, TypedKey + CryptoKey, CryptoSystemVLD0, CryptoTyped, DHTRecordDescriptor, RoutingContext, SharedSecret, + TypedKey, }; use crate::common::DHTEntity; use crate::repo::Repo; +pub const PROTOCOL_SCHEME: &str = "save+dweb:"; +pub const URL_DHT_KEY: &str = "dht"; +pub const URL_ENCRYPTION_KEY: &str = "enc"; +pub const URL_PUBLIC_KEY: &str = "pk"; +pub const URL_SECRET_KEY: &str = "sk"; + #[derive(Clone)] pub struct Group { pub dht_record: DHTRecordDescriptor, @@ -29,7 +38,7 @@ impl Group { encryption_key, routing_context, crypto_system, - repos: Vec::new(), + repos: Vec::new(), } } @@ -44,7 +53,7 @@ impl Group { pub fn owner_secret(&self) -> Option { self.dht_record.owner_secret().cloned() } - + pub async fn add_repo(&mut self, repo: Repo) -> Result<()> { self.repos.push(repo); Ok(()) @@ -62,6 +71,26 @@ impl Group { } } + pub fn get_url(&self) -> String { + let mut url = Url::parse(format!("{0}:?", PROTOCOL_SCHEME).as_str()).unwrap(); + + url.query_pairs_mut() + .append_pair(URL_DHT_KEY, self.id().encode_hex::().as_str()) + .append_pair( + URL_ENCRYPTION_KEY, + self.get_encryption_key().encode_hex::().as_str(), + ) + .append_pair( + URL_PUBLIC_KEY, + self.owner_key().encode_hex::().as_str(), + ) + .append_pair( + URL_SECRET_KEY, + self.owner_secret().unwrap().encode_hex::().as_str(), + ); + + url.to_string() + } } impl DHTEntity for Group { diff --git a/src/lib.rs b/src/lib.rs index 4aa509b..7954fc3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,64 +1,87 @@ -pub mod group; -pub mod repo; pub mod backend; pub mod common; pub mod constants; +pub mod group; +pub mod repo; -use crate::constants::{GROUP_NOT_FOUND, UNABLE_TO_SET_GROUP_NAME, UNABLE_TO_GET_GROUP_NAME, TEST_GROUP_NAME, UNABLE_TO_STORE_KEYPAIR, FAILED_TO_LOAD_KEYPAIR, KEYPAIR_NOT_FOUND, FAILED_TO_DESERIALIZE_KEYPAIR, ROUTE_ID_DHT_KEY}; +use crate::constants::{ + FAILED_TO_DESERIALIZE_KEYPAIR, FAILED_TO_LOAD_KEYPAIR, GROUP_NOT_FOUND, KEYPAIR_NOT_FOUND, + ROUTE_ID_DHT_KEY, TEST_GROUP_NAME, UNABLE_TO_GET_GROUP_NAME, UNABLE_TO_SET_GROUP_NAME, + UNABLE_TO_STORE_KEYPAIR, +}; use crate::backend::Backend; use crate::common::{CommonKeypair, DHTEntity}; use veilid_core::{ - vld0_generate_keypair, TypedKey, CRYPTO_KIND_VLD0, VeilidUpdate, VALID_CRYPTO_KINDS + vld0_generate_keypair, TypedKey, VeilidUpdate, CRYPTO_KIND_VLD0, VALID_CRYPTO_KINDS, }; #[cfg(test)] mod tests { use super::*; - use tokio::fs; - use tokio::sync::mpsc; use tmpdir::TmpDir; + use tokio::fs; + use tokio::sync::mpsc; #[tokio::test] async fn basic_test() { let path = TmpDir::new("test_dweb_backend").await.unwrap(); let port = 8080; - fs::create_dir_all(path.as_ref()).await.expect("Failed to create base directory"); + fs::create_dir_all(path.as_ref()) + .await + .expect("Failed to create base directory"); let mut backend = Backend::new(path.as_ref(), port).expect("Unable to create Backend"); backend.start().await.expect("Unable to start"); - let group = backend.create_group().await.expect("Unable to create group"); + let group = backend + .create_group() + .await + .expect("Unable to create group"); - group.set_name(TEST_GROUP_NAME).await.expect(UNABLE_TO_SET_GROUP_NAME); + group + .set_name(TEST_GROUP_NAME) + .await + .expect(UNABLE_TO_SET_GROUP_NAME); let name = group.get_name().await.expect(UNABLE_TO_GET_GROUP_NAME); assert_eq!(name, TEST_GROUP_NAME); backend.stop().await.expect("Unable to stop"); backend.start().await.expect("Unable to restart"); - let mut loaded_group = backend.get_group(TypedKey::new(CRYPTO_KIND_VLD0, group.id())).await.expect(GROUP_NOT_FOUND); + let mut loaded_group = backend + .get_group(TypedKey::new(CRYPTO_KIND_VLD0, group.id())) + .await + .expect(GROUP_NOT_FOUND); let protected_store = backend.get_protected_store().unwrap(); - let keypair_data = protected_store.load_user_secret(group.id().to_string()) + let keypair_data = protected_store + .load_user_secret(group.id().to_string()) .await .expect(FAILED_TO_LOAD_KEYPAIR) .expect(KEYPAIR_NOT_FOUND); - let retrieved_keypair: CommonKeypair = serde_cbor::from_slice(&keypair_data).expect(FAILED_TO_DESERIALIZE_KEYPAIR); + let retrieved_keypair: CommonKeypair = + serde_cbor::from_slice(&keypair_data).expect(FAILED_TO_DESERIALIZE_KEYPAIR); // Check that the id matches group.id() assert_eq!(retrieved_keypair.id, group.id()); // Check that the public_key matches the owner public key from the DHT record - assert_eq!(retrieved_keypair.public_key, loaded_group.get_dht_record().owner().clone()); + assert_eq!( + retrieved_keypair.public_key, + loaded_group.get_dht_record().owner().clone() + ); // Check that the secret and encryption keys match assert_eq!(retrieved_keypair.secret_key, group.get_secret_key()); assert_eq!(retrieved_keypair.encryption_key, group.get_encryption_key()); // Check if we can get group name - let group_name = loaded_group.get_name().await.expect(UNABLE_TO_GET_GROUP_NAME); + let group_name = loaded_group + .get_name() + .await + .expect(UNABLE_TO_GET_GROUP_NAME); assert_eq!(group_name, TEST_GROUP_NAME); // Compare the loaded group's id with the retrieved id @@ -70,33 +93,46 @@ mod tests { let repo_name = "Test Repo"; // Set and get repo name - repo.set_name(repo_name).await.expect("Unable to set repo name"); + repo.set_name(repo_name) + .await + .expect("Unable to set repo name"); let name = repo.get_name().await.expect("Unable to get repo name"); assert_eq!(name, repo_name); // Add repo to group - loaded_group.add_repo(repo.clone()).await.expect("Unable to add repo to group"); + loaded_group + .add_repo(repo.clone()) + .await + .expect("Unable to add repo to group"); // List known repos let repos = loaded_group.list_repos().await; assert!(repos.contains(&repo_key)); // Retrieve repo by key - let loaded_repo = backend.get_repo(repo_key.clone()).await.expect("Repo not found"); + let loaded_repo = backend + .get_repo(repo_key.clone()) + .await + .expect("Repo not found"); // Check if repo name is correctly retrieved - let retrieved_name = loaded_repo.get_name().await.expect("Unable to get repo name after restart"); + let retrieved_name = loaded_repo + .get_name() + .await + .expect("Unable to get repo name after restart"); assert_eq!(retrieved_name, repo_name); // Get the update receiver from the backend - let update_rx = backend.subscribe_updates().expect("Failed to subscribe to updates"); + let update_rx = backend + .subscribe_updates() + .expect("Failed to subscribe to updates"); // Set up a channel to receive AppMessage updates let (message_tx, mut message_rx) = mpsc::channel(1); // Spawn a task to listen for updates tokio::spawn(async move { - let mut rx = update_rx.resubscribe(); + let mut rx = update_rx.resubscribe(); while let Ok(update) = rx.recv().await { if let VeilidUpdate::AppMessage(app_message) = update { // Optionally, filter by route_id or other criteria @@ -106,7 +142,9 @@ mod tests { }); // Get VeilidAPI instance from backend - let veilid_api = backend.get_veilid_api().expect("Failed to get VeilidAPI instance"); + let veilid_api = backend + .get_veilid_api() + .expect("Failed to get VeilidAPI instance"); // Create a new private route let (route_id, route_id_blob) = veilid_api @@ -142,4 +180,33 @@ mod tests { backend.stop().await.expect("Unable to stop"); } + #[tokio::test] + async fn test_join() { + let path = TmpDir::new("test_dweb_backend").await.unwrap(); + let port = 8080; + + fs::create_dir_all(path.as_ref()) + .await + .expect("Failed to create base directory"); + + let mut backend = Backend::new(path.as_ref(), port).expect("Unable to create Backend"); + + backend.start().await.expect("Unable to start"); + let group = backend + .create_group() + .await + .expect("Unable to create group"); + + group + .set_name(TEST_GROUP_NAME) + .await + .expect(UNABLE_TO_SET_GROUP_NAME); + + let url = group.get_url(); + + let keys = backend::parse_url(url.as_str()).expect("URL was parsed back out"); + + assert_eq!(keys.id, group.id()); + backend.stop().await.expect("Unable to stop"); + } }