mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2024-11-22 09:09:07 -05:00
fix(NIP-12): only allow single-char tag filters
This commit is contained in:
parent
f4ecd43708
commit
5058d98ad6
14
src/db.rs
14
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
|
// (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
|
||||||
|
|
41
src/event.rs
41
src/event.rs
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user