Skip to content

Commit

Permalink
Merge pull request #27 from ranfdev/nullables
Browse files Browse the repository at this point in the history
Rewrite of ntfy-daemon. Adds basic tests with Nullables and removes any trace of capnp
  • Loading branch information
ranfdev authored Jan 21, 2025
2 parents 3716b22 + 33a4708 commit 3a4acc2
Show file tree
Hide file tree
Showing 29 changed files with 3,058 additions and 1,991 deletions.
1,800 changes: 1,060 additions & 740 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ tracing-subscriber = "0.3"
adw = { version = "0.7", package = "libadwaita", features = ["v1_6"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
capnp = "0.18.0"
capnp-rpc = "0.18.0"
anyhow = "1.0.71"
chrono = "0.4.26"
rand = "0.8.5"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ https://ntfy.sh client application to receive everyday's notifications.
## Architecture

The code is split between the GUI and the underlying ntfy-daemon.
![](./architecture.svg)

## How to run
Use gnome-builder to clone and run the project. Note: after clicking the "run"
Expand Down
12 changes: 12 additions & 0 deletions architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 2 additions & 12 deletions build-aux/com.ranfdev.Notify.Devel.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,6 @@
]
},
"modules": [
{
"name": "capnp",
"buildsystem": "cmake",
"sources": [
{
"type": "archive",
"url": "https://capnproto.org/capnproto-c++-0.10.4.tar.gz",
"sha256": "981e7ef6dbe3ac745907e55a78870fbb491c5d23abd4ebc04e20ec235af4458c"
}
]
},
{
"name": "blueprint-compiler",
"buildsystem": "meson",
Expand All @@ -56,7 +45,8 @@
{
"type": "git",
"url": "https://gitlab.gnome.org/jwestman/blueprint-compiler",
"tag": "v0.14.0"
"tag": "v0.14.0",
"commit": "8e10fcf8692108b9d4ab78f41086c5d7773ef864"
}
]
},
Expand Down
16 changes: 6 additions & 10 deletions ntfy-daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,23 @@ edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html


[build-dependencies]
capnpc = "0.17.2"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
capnp = "0.18.0"
capnp-rpc = "0.18.0"
futures = "0.3.0"
tokio = { version = "1.0.0", features = ["net", "rt", "macros", "parking_lot"]}
tokio-util = { version = "0.7.4", features = ["compat", "io"] }
clap = { version = "4.3.11", features = ["derive"] }
anyhow = "1.0.71"
tokio-stream = { version = "0.1.14", features = ["io-util", "time"] }
tokio-stream = { version = "0.1.14", features = ["io-util", "time", "sync"] }
rusqlite = "0.29.0"
rand = "0.8.5"
reqwest = { version = "0.11.18", features = ["stream", "rustls-tls-native-roots"]}
url = "2.4.0"
generational-arena = "0.2.9"
reqwest = { version = "0.12.9", features = ["stream", "rustls-tls-native-roots"]}
url = { version = "2.4.0", features = ["serde"] }
tracing = "0.1.37"
thiserror = "1.0.49"
regex = "1.9.6"
oo7 = "0.2.1"
async-trait = "0.1.83"
http = "1.1.0"
async-channel = "2.3.1"
5 changes: 0 additions & 5 deletions ntfy-daemon/README.md

This file was deleted.

6 changes: 0 additions & 6 deletions ntfy-daemon/build.rs

This file was deleted.

14 changes: 14 additions & 0 deletions ntfy-daemon/src/actor_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
macro_rules! send_command {
($self:expr, $command:expr) => {{
let (resp_tx, resp_rx) = oneshot::channel();
use anyhow::Context;
$self
.command_tx
.send($command(resp_tx))
.await
.context("Actor mailbox error")?;
resp_rx.await.context("Actor response error")?
}};
}

pub(crate) use send_command;
189 changes: 159 additions & 30 deletions ntfy-daemon/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,196 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, RwLock};

use async_trait::async_trait;

#[derive(Clone)]
pub struct KeyringItem {
attributes: HashMap<String, String>,
// we could zero-out this region of memory
secret: Vec<u8>,
}

impl KeyringItem {
async fn attributes(&self) -> HashMap<String, String> {
self.attributes.clone()
}
async fn secret(&self) -> &[u8] {
&self.secret[..]
}
}

#[async_trait]
trait LightKeyring {
async fn search_items(
&self,
attributes: HashMap<&str, &str>,
) -> anyhow::Result<Vec<KeyringItem>>;
async fn create_item(
&self,
label: &str,
attributes: HashMap<&str, &str>,
secret: &str,
replace: bool,
) -> anyhow::Result<()>;
async fn delete(&self, attributes: HashMap<&str, &str>) -> anyhow::Result<()>;
}

struct RealKeyring {
keyring: oo7::Keyring,
}

#[async_trait]
impl LightKeyring for RealKeyring {
async fn search_items(
&self,
attributes: HashMap<&str, &str>,
) -> anyhow::Result<Vec<KeyringItem>> {
let items = self.keyring.search_items(attributes).await?;

let mut out_items = vec![];
for item in items {
out_items.push(KeyringItem {
attributes: item.attributes().await?,
secret: item.secret().await?.to_vec(),
});
}
Ok(out_items)
}

async fn create_item(
&self,
label: &str,
attributes: HashMap<&str, &str>,
secret: &str,
replace: bool,
) -> anyhow::Result<()> {
self.keyring
.create_item(label, attributes, secret, replace)
.await?;
Ok(())
}

async fn delete(&self, attributes: HashMap<&str, &str>) -> anyhow::Result<()> {
self.keyring.delete(attributes).await?;
Ok(())
}
}

struct NullableKeyring {
search_response: Vec<KeyringItem>,
}

impl NullableKeyring {
pub fn new(search_response: Vec<KeyringItem>) -> Self {
Self { search_response }
}
}

#[async_trait]
impl LightKeyring for NullableKeyring {
async fn search_items(
&self,
_attributes: HashMap<&str, &str>,
) -> anyhow::Result<Vec<KeyringItem>> {
Ok(self.search_response.clone())
}

async fn create_item(
&self,
_label: &str,
_attributes: HashMap<&str, &str>,
_secret: &str,
_replace: bool,
) -> anyhow::Result<()> {
Ok(())
}

async fn delete(&self, _attributes: HashMap<&str, &str>) -> anyhow::Result<()> {
Ok(())
}
}
impl NullableKeyring {
pub fn with_credentials(credentials: Vec<Credential>) -> Self {
let mut search_response = vec![];

for cred in credentials {
let attributes = HashMap::from([
("type".to_string(), "password".to_string()),
("username".to_string(), cred.username.clone()),
("server".to_string(), cred.password.clone()),
]);
search_response.push(KeyringItem {
attributes,
secret: cred.password.into_bytes(),
});
}

Self { search_response }
}
}

#[derive(Debug, Clone)]
pub struct Credential {
pub username: String,
pub password: String,
}

#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct Credentials {
keyring: Rc<oo7::Keyring>,
creds: Rc<RefCell<HashMap<String, Credential>>>,
keyring: Arc<dyn LightKeyring + Send + Sync>,
creds: Arc<RwLock<HashMap<String, Credential>>>,
}

impl Credentials {
pub async fn new() -> anyhow::Result<Self> {
let mut this = Self {
keyring: Rc::new(
oo7::Keyring::new()
keyring: Arc::new(RealKeyring {
keyring: oo7::Keyring::new()
.await
.expect("Failed to start Secret Service"),
),
}),
creds: Default::default(),
};
this.load().await?;
Ok(this)
}
pub async fn new_nullable(credentials: Vec<Credential>) -> anyhow::Result<Self> {
let mut this = Self {
keyring: Arc::new(NullableKeyring::with_credentials(credentials)),
creds: Default::default(),
};
this.load().await?;
Ok(this)
}
pub async fn load(&mut self) -> anyhow::Result<()> {
let attrs = HashMap::from([("type", "password")]);
let values = self
.keyring
.search_items(attrs)
.await
.map_err(|e| capnp::Error::failed(e.to_string()))?;
let values = self.keyring.search_items(attrs).await?;

self.creds.borrow_mut().clear();
let mut lock = self.creds.write().unwrap();
lock.clear();
for item in values {
let attrs = item
.attributes()
.await
.map_err(|e| capnp::Error::failed(e.to_string()))?;
self.creds.borrow_mut().insert(
let attrs = item.attributes().await;
lock.insert(
attrs["server"].to_string(),
Credential {
username: attrs["username"].to_string(),
password: std::str::from_utf8(&item.secret().await?)?.to_string(),
password: std::str::from_utf8(&item.secret().await)?.to_string(),
},
);
}
Ok(())
}
pub fn get(&self, server: &str) -> Option<Credential> {
self.creds.borrow().get(server).cloned()
self.creds.read().unwrap().get(server).cloned()
}
pub fn list_all(&self) -> HashMap<String, Credential> {
self.creds.borrow().clone()
self.creds.read().unwrap().clone()
}
pub async fn insert(&self, server: &str, username: &str, password: &str) -> anyhow::Result<()> {
{
if let Some(cred) = self.creds.borrow().get(server) {
if let Some(cred) = self.creds.read().unwrap().get(server) {
if cred.username != username {
anyhow::bail!("You can add only one account per server");
}
Expand All @@ -72,10 +203,9 @@ impl Credentials {
]);
self.keyring
.create_item("Password", attrs, password, true)
.await
.map_err(|e| capnp::Error::failed(e.to_string()))?;
.await?;

self.creds.borrow_mut().insert(
self.creds.write().unwrap().insert(
server.to_string(),
Credential {
username: username.to_string(),
Expand All @@ -87,7 +217,8 @@ impl Credentials {
pub async fn delete(&self, server: &str) -> anyhow::Result<()> {
let creds = {
self.creds
.borrow()
.read()
.unwrap()
.get(server)
.ok_or(anyhow::anyhow!("server creds not found"))?
.clone()
Expand All @@ -97,12 +228,10 @@ impl Credentials {
("username", &creds.username),
("server", server),
]);
self.keyring
.delete(attrs)
.await
.map_err(|e| capnp::Error::failed(e.to_string()))?;
self.keyring.delete(attrs).await?;
self.creds
.borrow_mut()
.write()
.unwrap()
.remove(server)
.ok_or(anyhow::anyhow!("server creds not found"))?;
Ok(())
Expand Down
Loading

0 comments on commit 3a4acc2

Please sign in to comment.