feat: audit codebase (#72)
Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
@@ -13,7 +13,7 @@ use crate::LOCAL_KEYS;
|
||||
use crate::protocol::{
|
||||
ACTIVITY_KINDS, Activity, GroupAdmins, GroupId, GroupKey, GroupMembers, GroupMetadata,
|
||||
GroupPins, GroupRoles, MEMBERSHIP_KINDS, Membership, PIN_LIST, Pin, RENDER_KINDS, STATE_KINDS,
|
||||
TimelineWindow, tag_value,
|
||||
TimelineWindow, is_newer_event, tag_value,
|
||||
};
|
||||
|
||||
const REPLAY_LIMIT: usize = 100;
|
||||
@@ -91,7 +91,8 @@ pub struct Group {
|
||||
membership: Membership,
|
||||
membership_events: Vec<Event>,
|
||||
elsewhere: Option<GroupKey>,
|
||||
state_ids: HashMap<Kind, EventId>,
|
||||
/// The last accepted (created_at, id) per state kind.
|
||||
state_ids: HashMap<Kind, (Timestamp, EventId)>,
|
||||
rows: Vec<Row>,
|
||||
index: HashSet<EventId>,
|
||||
reactions: HashMap<EventId, Vec<Reaction>>,
|
||||
@@ -694,6 +695,15 @@ impl Group {
|
||||
}
|
||||
|
||||
fn absorb_state(&mut self, event: &Event, me: PublicKey) -> bool {
|
||||
// Relays deliver same-kind state events in any order.
|
||||
if !is_newer_event(
|
||||
self.state_ids.get(&event.kind).copied(),
|
||||
event.created_at,
|
||||
event.id,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match event.kind {
|
||||
Kind::GroupMetadata => match GroupMetadata::parse(event) {
|
||||
Ok(metadata) => self.metadata = Some(metadata),
|
||||
@@ -724,7 +734,10 @@ impl Group {
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
self.state_ids.insert(event.kind, event.id) != Some(event.id)
|
||||
self.state_ids
|
||||
.insert(event.kind, (event.created_at, event.id));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn adopt_roster_membership(&mut self, me: PublicKey) -> bool {
|
||||
|
||||
+43
-17
@@ -76,6 +76,8 @@ pub struct GroupsRegistry {
|
||||
groups: Vec<Entity<Group>>,
|
||||
previews: HashMap<GroupKey, Entity<Group>>,
|
||||
list: GroupList,
|
||||
/// The (created_at, id) of the last accepted group list event.
|
||||
list_stamp: Option<(Timestamp, EventId)>,
|
||||
browse: HashMap<SubscriptionId, RelayUrl>,
|
||||
routes: HashMap<SubscriptionId, GroupKey>,
|
||||
observers: HashMap<GroupKey, Subscription>,
|
||||
@@ -132,6 +134,7 @@ impl GroupsRegistry {
|
||||
groups: Vec::new(),
|
||||
previews: HashMap::new(),
|
||||
list: GroupList::default(),
|
||||
list_stamp: None,
|
||||
browse: HashMap::new(),
|
||||
routes: HashMap::new(),
|
||||
observers: HashMap::new(),
|
||||
@@ -455,6 +458,8 @@ impl GroupsRegistry {
|
||||
self.fork_authors.clear();
|
||||
self.syncing.clear();
|
||||
self.list = GroupList::default();
|
||||
// A new signer's list events must not be judged against the old stamp.
|
||||
self.list_stamp = None;
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
@@ -488,24 +493,26 @@ impl GroupsRegistry {
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
// Show the cached groups before the relays answer.
|
||||
let task: Task<Result<GroupList, Error>> = cx.background_spawn(async move {
|
||||
let me = signer.get_public_key_async().await?;
|
||||
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
|
||||
let task: Task<Result<(GroupList, Option<Event>), Error>> =
|
||||
cx.background_spawn(async move {
|
||||
let me = signer.get_public_key_async().await?;
|
||||
let filter = Filter::new().author(me).kind(Kind::SimpleGroups).limit(1);
|
||||
|
||||
match client.database().query(filter).await?.into_iter().next() {
|
||||
Some(event) => {
|
||||
let list = GroupList::parse(&event)?;
|
||||
Ok(Self::hydrate_list(list, &signer, &me).await)
|
||||
match client.database().query(filter).await?.into_iter().next() {
|
||||
Some(event) => {
|
||||
let list =
|
||||
Self::hydrate_list(GroupList::parse(&event)?, &signer, &me).await;
|
||||
Ok((list, Some(event)))
|
||||
}
|
||||
None => Ok((GroupList::default(), None)),
|
||||
}
|
||||
None => Ok(GroupList::default()),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.tasks
|
||||
.push(cx.spawn(async move |this, cx| match task.await {
|
||||
Ok(list) => {
|
||||
Ok((list, event)) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.merge(list, cx);
|
||||
this.merge(list, event.as_ref(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -538,7 +545,16 @@ impl GroupsRegistry {
|
||||
self.watch_forks(cx);
|
||||
}
|
||||
|
||||
fn merge(&mut self, list: GroupList, cx: &mut Context<Self>) {
|
||||
fn merge(&mut self, list: GroupList, event: Option<&Event>, cx: &mut Context<Self>) {
|
||||
if let Some(event) = event {
|
||||
// Relays replay list events out of order; only strictly newer applies.
|
||||
if !is_newer_event(self.list_stamp, event.created_at, event.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.list_stamp = Some((event.created_at, event.id));
|
||||
}
|
||||
|
||||
// Carry the event's private section so a later publish can preserve it.
|
||||
self.list
|
||||
.set_content(list.content().map(str::to_owned), list.content_readable());
|
||||
@@ -589,20 +605,21 @@ impl GroupsRegistry {
|
||||
};
|
||||
|
||||
if list.content().is_none() {
|
||||
self.merge(list, cx);
|
||||
self.merge(list, Some(&event), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
let Some(me) = nostr.read(cx).current_user() else {
|
||||
self.merge(list, cx);
|
||||
self.merge(list, Some(&event), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let list = Self::hydrate_list(list, &signer, &me).await;
|
||||
this.update(cx, |this, cx| this.merge(list, cx)).ok();
|
||||
this.update(cx, |this, cx| this.merge(list, Some(&event), cx))
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -864,7 +881,7 @@ impl GroupsRegistry {
|
||||
let me = nostr.read(cx).current_user();
|
||||
let list = self.list.clone();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
// Re-encrypt the private section when it carries local entries; a
|
||||
// section this session could not read is carried verbatim.
|
||||
let content = match (list.has_private(), list.content_readable()) {
|
||||
@@ -893,6 +910,15 @@ impl GroupsRegistry {
|
||||
}
|
||||
};
|
||||
|
||||
// Our own publish is the newest list until a newer event arrives.
|
||||
if let Err(error) = this.update(cx, |this, _cx| {
|
||||
if is_newer_event(this.list_stamp, event.created_at, event.id) {
|
||||
this.list_stamp = Some((event.created_at, event.id));
|
||||
}
|
||||
}) {
|
||||
log::warn!("recording the published list failed: {error}");
|
||||
}
|
||||
|
||||
match client.send_event(&event).to_nip65().await {
|
||||
Ok(output) if output.failed.is_empty() => {}
|
||||
Ok(output) => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use common::stable_id_u64;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
pub(crate) const TIMELINE_WINDOW: usize = 50;
|
||||
@@ -50,6 +49,15 @@ pub(crate) fn tag_value<'a>(tags: &'a Tags, name: &str) -> Option<&'a str> {
|
||||
.and_then(|tag| tag.content())
|
||||
}
|
||||
|
||||
/// Whether `(created_at, id)` is strictly newer than the last accepted stamp.
|
||||
pub(crate) fn is_newer_event(
|
||||
accepted: Option<(Timestamp, EventId)>,
|
||||
created_at: Timestamp,
|
||||
id: EventId,
|
||||
) -> bool {
|
||||
accepted.is_none_or(|last| (created_at, id) > last)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GroupId(String);
|
||||
|
||||
@@ -280,9 +288,7 @@ impl GroupKey {
|
||||
}
|
||||
|
||||
pub fn uniq_id(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
stable_id_u64(&[self.relay.as_str(), self.id.as_str()])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1152,4 +1158,19 @@ mod tests {
|
||||
let malformed = Tag::custom("previous", ["nothex"]);
|
||||
assert_eq!(window.references_seen(Some(&malformed)), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_newer_event_accepts_only_strictly_newer_events() {
|
||||
let lower = EventId::from_byte_array([0x00; 32]);
|
||||
let higher = EventId::from_byte_array([0xff; 32]);
|
||||
let earlier = Timestamp::from(100);
|
||||
let later = Timestamp::from(200);
|
||||
|
||||
assert!(is_newer_event(None, earlier, lower));
|
||||
assert!(!is_newer_event(Some((earlier, lower)), earlier, lower));
|
||||
assert!(is_newer_event(Some((earlier, lower)), later, lower));
|
||||
assert!(!is_newer_event(Some((later, lower)), earlier, higher));
|
||||
assert!(is_newer_event(Some((earlier, lower)), earlier, higher));
|
||||
assert!(!is_newer_event(Some((earlier, higher)), earlier, lower));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use common::stable_id_u64;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
use crate::{GroupId, GroupKey};
|
||||
@@ -25,10 +24,10 @@ pub(crate) fn subscription_id(key: &GroupKey) -> SubscriptionId {
|
||||
}
|
||||
|
||||
pub(crate) fn browse_subscription(relay: &RelayUrl) -> SubscriptionId {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
relay.hash(&mut hasher);
|
||||
|
||||
SubscriptionId::new(format!("{BROWSE_PREFIX}{:08x}", hasher.finish() as u32))
|
||||
SubscriptionId::new(format!(
|
||||
"{BROWSE_PREFIX}{:08x}",
|
||||
stable_id_u64(&[relay.as_str()]) as u32
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn page_subscription(key: &GroupKey) -> SubscriptionId {
|
||||
|
||||
Reference in New Issue
Block a user