nostr-rs-relay

My dev fork of nostr-rs-relay
git clone git://jb55.com/nostr-rs-relay
Log | Files | Refs | README | LICENSE

commit 5058d98ad6a91c9b44cdc80a98931969219bc881
parent f4ecd43708d17a8f9cd6fb83c94e4b5f08624971
Author: Greg Heartsfield <scsibug@imap.cc>
Date:   Sun,  7 Aug 2022 10:15:36 -0500

fix(NIP-12): only allow single-char tag filters

Diffstat:
Msrc/db.rs | 14+++++++++++++-
Msrc/event.rs | 41+++++++++++++++++++++++++++++++++--------
Msrc/subscription.rs | 59+++++++++++++++++++++++++++++++++++++++++++++--------------
3 files changed, 91 insertions(+), 23 deletions(-)

diff --git a/src/db.rs b/src/db.rs @@ -384,11 +384,23 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) { // (sqli-safe), or a string that is filtered to only contain // hexadecimal characters. Strings that require escaping (tag // names/values) use parameters. + + // if the filter is malformed, don't return anything. + if f.force_no_match { + let empty_query = + "SELECT DISTINCT(e.content), e.created_at FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE 1=0" + .to_owned(); + // query parameters for SQLite + let empty_params: Vec<Box<dyn ToSql>> = vec![]; + return (empty_query, empty_params); + } + let mut query = "SELECT DISTINCT(e.content), e.created_at FROM event e LEFT JOIN tag t ON e.id=t.event_id " .to_owned(); // query parameters for SQLite let mut params: Vec<Box<dyn ToSql>> = vec![]; + // individual filter components (single conditions such as an author or event ID) let mut filter_components: Vec<String> = Vec::new(); // Query for "authors", allowing prefix matches @@ -471,7 +483,7 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) { let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len())); let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause); // add the tag name as the first parameter - params.push(Box::new(key.to_owned())); + params.push(Box::new(key.to_string())); // add all tag values that are plain strings as params params.append(&mut str_vals); // add all tag values that are blobs as params diff --git a/src/event.rs b/src/event.rs @@ -39,9 +39,9 @@ pub struct Event { pub(crate) tags: Vec<Vec<String>>, pub(crate) content: String, pub(crate) sig: String, - // Optimization for tag search, built on demand + // Optimization for tag search, built on demand. #[serde(skip)] - pub(crate) tagidx: Option<HashMap<String, HashSet<String>>>, + pub(crate) tagidx: Option<HashMap<char, HashSet<String>>>, } /// Simple tag type for array of array of strings. @@ -56,6 +56,25 @@ where Ok(opt.unwrap_or_else(Vec::new)) } +/// Attempt to form a single-char tag name. +fn single_char_tagname(tagname: &str) -> Option<char> { + // We return the tag character if and only if the tagname consists + // of a single char. + let mut tagnamechars = tagname.chars(); + let firstchar = tagnamechars.next(); + return match firstchar { + Some(_) => { + // check second char + if tagnamechars.next().is_none() { + firstchar + } else { + None + } + } + None => None, + }; +} + /// Convert network event to parsed/validated event. impl From<EventCmd> for Result<Event> { fn from(ec: EventCmd) -> Result<Event> { @@ -99,17 +118,22 @@ impl Event { return; } // otherwise, build an index - let mut idx: HashMap<String, HashSet<String>> = HashMap::new(); + let mut idx: HashMap<char, HashSet<String>> = HashMap::new(); // iterate over tags that have at least 2 elements for t in self.tags.iter().filter(|x| x.len() > 1) { let tagname = t.get(0).unwrap(); + let tagnamechar_opt = single_char_tagname(tagname); + if tagnamechar_opt.is_none() { + continue; + } + let tagnamechar = tagnamechar_opt.unwrap(); let tagval = t.get(1).unwrap(); // ensure a vector exists for this tag - if !idx.contains_key(tagname) { - idx.insert(tagname.clone(), HashSet::new()); + if !idx.contains_key(&tagnamechar) { + idx.insert(tagnamechar.clone(), HashSet::new()); } // get the tag vec and insert entry - let tidx = idx.get_mut(tagname).expect("could not get tag vector"); + let tidx = idx.get_mut(&tagnamechar).expect("could not get tag vector"); tidx.insert(tagval.clone()); } // save the tag structure @@ -226,9 +250,10 @@ impl Event { } /// Determine if the given tag and value set intersect with tags in this event. - pub fn generic_tag_val_intersect(&self, tagname: &str, check: &HashSet<String>) -> bool { + pub fn generic_tag_val_intersect(&self, tagname: char, check: &HashSet<String>) -> bool { match &self.tagidx { - Some(idx) => match idx.get(tagname) { + // check if this is indexable tagname + Some(idx) => match idx.get(&tagname) { Some(valset) => { let common = valset.intersection(check); common.count() > 0 diff --git a/src/subscription.rs b/src/subscription.rs @@ -1,6 +1,7 @@ //! Subscription and filter parsing use crate::error::Result; use crate::event::Event; +use log::*; use serde::de::Unexpected; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; @@ -35,7 +36,9 @@ pub struct ReqFilter { pub limit: Option<u64>, /// Set of tags #[serde(skip)] - pub tags: Option<HashMap<String, HashSet<String>>>, + pub tags: Option<HashMap<char, HashSet<String>>>, + /// Force no matches due to malformed data + pub force_no_match: bool, } impl<'de> Deserialize<'de> for ReqFilter { @@ -58,6 +61,7 @@ impl<'de> Deserialize<'de> for ReqFilter { authors: None, limit: None, tags: None, + force_no_match: false, }; let mut ts = None; // iterate through each key, and assign values that exist @@ -76,19 +80,25 @@ impl<'de> Deserialize<'de> for ReqFilter { } else if key == "authors" { rf.authors = Deserialize::deserialize(val).ok(); } else if key.starts_with('#') && key.len() > 1 && val.is_array() { - // remove the prefix - let tagname = &key[1..]; - if ts.is_none() { - // Initialize the tag if necessary - ts = Some(HashMap::new()); - } - if let Some(m) = ts.as_mut() { - let tag_vals: Option<Vec<String>> = Deserialize::deserialize(val).ok(); - if let Some(v) = tag_vals { - let hs = HashSet::from_iter(v.into_iter()); - m.insert(tagname.to_owned(), hs); + info!("testing tag search char: {}", key); + if let Some(tag_search) = tag_search_char_from_filter(key) { + info!("found a character from the tag search: {}", tag_search); + if ts.is_none() { + // Initialize the tag if necessary + ts = Some(HashMap::new()); } - }; + if let Some(m) = ts.as_mut() { + let tag_vals: Option<Vec<String>> = Deserialize::deserialize(val).ok(); + if let Some(v) = tag_vals { + let hs = HashSet::from_iter(v.into_iter()); + m.insert(tag_search.to_owned(), hs); + } + }; + } else { + // tag search that is multi-character, don't add to subscription + rf.force_no_match = true; + continue; + } } } rf.tags = ts; @@ -96,6 +106,26 @@ impl<'de> Deserialize<'de> for ReqFilter { } } +/// Attempt to form a single-char identifier from a tag search filter +fn tag_search_char_from_filter(tagname: &str) -> Option<char> { + let tagname_nohash = &tagname[1..]; + // We return the tag character if and only if the tagname consists + // of a single char. + let mut tagnamechars = tagname_nohash.chars(); + let firstchar = tagnamechars.next(); + return match firstchar { + Some(_) => { + // check second char + if tagnamechars.next().is_none() { + firstchar + } else { + None + } + } + None => None, + }; +} + impl<'de> Deserialize<'de> for Subscription { /// Custom deserializer for subscriptions, which have a more /// complex structure than the other message types. @@ -194,7 +224,7 @@ impl ReqFilter { // get the hashset from the filter. if let Some(map) = &self.tags { for (key, val) in map.iter() { - let tag_match = event.generic_tag_val_intersect(key, val); + let tag_match = event.generic_tag_val_intersect(*key, val); // if there is no match for this tag, the match fails. if !tag_match { return false; @@ -223,6 +253,7 @@ impl ReqFilter { && self.kind_match(event.kind) && self.authors_match(event) && self.tag_match(event) + && !self.force_no_match } }