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

This commit is contained in:
Greg Heartsfield 2022-08-07 10:15:36 -05:00
parent f4ecd43708
commit 5058d98ad6
No known key found for this signature in database
GPG Key ID: C7F4AA6B95F11E3A
3 changed files with 91 additions and 23 deletions

View File

@ -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 // (sqli-safe), or a string that is filtered to only contain
// hexadecimal characters. Strings that require escaping (tag // hexadecimal characters. Strings that require escaping (tag
// names/values) use parameters. // 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 = let mut query =
"SELECT DISTINCT(e.content), e.created_at FROM event e LEFT JOIN tag t ON e.id=t.event_id " "SELECT DISTINCT(e.content), e.created_at FROM event e LEFT JOIN tag t ON e.id=t.event_id "
.to_owned(); .to_owned();
// query parameters for SQLite // query parameters for SQLite
let mut params: Vec<Box<dyn ToSql>> = vec![]; let mut params: Vec<Box<dyn ToSql>> = vec![];
// individual filter components (single conditions such as an author or event ID) // individual filter components (single conditions such as an author or event ID)
let mut filter_components: Vec<String> = Vec::new(); let mut filter_components: Vec<String> = Vec::new();
// Query for "authors", allowing prefix matches // 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 blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause); let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause);
// add the tag name as the first parameter // 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 // add all tag values that are plain strings as params
params.append(&mut str_vals); params.append(&mut str_vals);
// add all tag values that are blobs as params // add all tag values that are blobs as params

View File

@ -39,9 +39,9 @@ pub struct Event {
pub(crate) tags: Vec<Vec<String>>, pub(crate) tags: Vec<Vec<String>>,
pub(crate) content: String, pub(crate) content: String,
pub(crate) sig: String, pub(crate) sig: String,
// Optimization for tag search, built on demand // Optimization for tag search, built on demand.
#[serde(skip)] #[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. /// Simple tag type for array of array of strings.
@ -56,6 +56,25 @@ where
Ok(opt.unwrap_or_else(Vec::new)) 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. /// Convert network event to parsed/validated event.
impl From<EventCmd> for Result<Event> { impl From<EventCmd> for Result<Event> {
fn from(ec: EventCmd) -> Result<Event> { fn from(ec: EventCmd) -> Result<Event> {
@ -99,17 +118,22 @@ impl Event {
return; return;
} }
// otherwise, build an index // 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 // iterate over tags that have at least 2 elements
for t in self.tags.iter().filter(|x| x.len() > 1) { for t in self.tags.iter().filter(|x| x.len() > 1) {
let tagname = t.get(0).unwrap(); 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(); let tagval = t.get(1).unwrap();
// ensure a vector exists for this tag // ensure a vector exists for this tag
if !idx.contains_key(tagname) { if !idx.contains_key(&tagnamechar) {
idx.insert(tagname.clone(), HashSet::new()); idx.insert(tagnamechar.clone(), HashSet::new());
} }
// get the tag vec and insert entry // 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()); tidx.insert(tagval.clone());
} }
// save the tag structure // save the tag structure
@ -226,9 +250,10 @@ impl Event {
} }
/// Determine if the given tag and value set intersect with tags in this 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 { match &self.tagidx {
Some(idx) => match idx.get(tagname) { // check if this is indexable tagname
Some(idx) => match idx.get(&tagname) {
Some(valset) => { Some(valset) => {
let common = valset.intersection(check); let common = valset.intersection(check);
common.count() > 0 common.count() > 0

View File

@ -1,6 +1,7 @@
//! Subscription and filter parsing //! Subscription and filter parsing
use crate::error::Result; use crate::error::Result;
use crate::event::Event; use crate::event::Event;
use log::*;
use serde::de::Unexpected; use serde::de::Unexpected;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value; use serde_json::Value;
@ -35,7 +36,9 @@ pub struct ReqFilter {
pub limit: Option<u64>, pub limit: Option<u64>,
/// Set of tags /// Set of tags
#[serde(skip)] #[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 { impl<'de> Deserialize<'de> for ReqFilter {
@ -58,6 +61,7 @@ impl<'de> Deserialize<'de> for ReqFilter {
authors: None, authors: None,
limit: None, limit: None,
tags: None, tags: None,
force_no_match: false,
}; };
let mut ts = None; let mut ts = None;
// iterate through each key, and assign values that exist // iterate through each key, and assign values that exist
@ -76,19 +80,25 @@ impl<'de> Deserialize<'de> for ReqFilter {
} else if key == "authors" { } else if key == "authors" {
rf.authors = Deserialize::deserialize(val).ok(); rf.authors = Deserialize::deserialize(val).ok();
} else if key.starts_with('#') && key.len() > 1 && val.is_array() { } else if key.starts_with('#') && key.len() > 1 && val.is_array() {
// remove the prefix info!("testing tag search char: {}", key);
let tagname = &key[1..]; if let Some(tag_search) = tag_search_char_from_filter(key) {
if ts.is_none() { info!("found a character from the tag search: {}", tag_search);
// Initialize the tag if necessary if ts.is_none() {
ts = Some(HashMap::new()); // 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);
} }
}; 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; 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 { impl<'de> Deserialize<'de> for Subscription {
/// Custom deserializer for subscriptions, which have a more /// Custom deserializer for subscriptions, which have a more
/// complex structure than the other message types. /// complex structure than the other message types.
@ -194,7 +224,7 @@ impl ReqFilter {
// get the hashset from the filter. // get the hashset from the filter.
if let Some(map) = &self.tags { if let Some(map) = &self.tags {
for (key, val) in map.iter() { 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 there is no match for this tag, the match fails.
if !tag_match { if !tag_match {
return false; return false;
@ -223,6 +253,7 @@ impl ReqFilter {
&& self.kind_match(event.kind) && self.kind_match(event.kind)
&& self.authors_match(event) && self.authors_match(event)
&& self.tag_match(event) && self.tag_match(event)
&& !self.force_no_match
} }
} }