mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2025-03-28 21:22:27 -04:00
404 lines
13 KiB
Rust
404 lines
13 KiB
Rust
//! Event parsing and validation
|
|
use crate::error::Error;
|
|
use crate::error::Result;
|
|
use crate::event::Event;
|
|
use bitcoin_hashes::{sha256, Hash};
|
|
use lazy_static::lazy_static;
|
|
use regex::Regex;
|
|
use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::str::FromStr;
|
|
use tracing::{debug, info};
|
|
|
|
// This handles everything related to delegation, in particular the
|
|
// condition/rune parsing and logic.
|
|
|
|
// Conditions are poorly specified, so we will implement the minimum
|
|
// necessary for now.
|
|
|
|
// fields MUST be either "kind" or "created_at".
|
|
// operators supported are ">", "<", "=", "!".
|
|
// no operations on 'content' are supported.
|
|
|
|
// this allows constraints for:
|
|
// valid date ranges (valid from X->Y dates).
|
|
// specific kinds (publish kind=1,5)
|
|
// kind ranges (publish ephemeral events, kind>19999&kind<30001)
|
|
|
|
// for more complex scenarios (allow delegatee to publish ephemeral
|
|
// AND replacement events), it may be necessary to generate and use
|
|
// different condition strings, since we do not support grouping or
|
|
// "OR" logic.
|
|
|
|
lazy_static! {
|
|
/// Secp256k1 verification instance.
|
|
pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
|
pub enum Field {
|
|
Kind,
|
|
CreatedAt,
|
|
}
|
|
|
|
impl FromStr for Field {
|
|
type Err = Error;
|
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
if value == "kind" {
|
|
Ok(Field::Kind)
|
|
} else if value == "created_at" {
|
|
Ok(Field::CreatedAt)
|
|
} else {
|
|
Err(Error::DelegationParseError)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
|
pub enum Operator {
|
|
LessThan,
|
|
GreaterThan,
|
|
Equals,
|
|
NotEquals,
|
|
}
|
|
impl FromStr for Operator {
|
|
type Err = Error;
|
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
if value == "<" {
|
|
Ok(Operator::LessThan)
|
|
} else if value == ">" {
|
|
Ok(Operator::GreaterThan)
|
|
} else if value == "=" {
|
|
Ok(Operator::Equals)
|
|
} else if value == "!" {
|
|
Ok(Operator::NotEquals)
|
|
} else {
|
|
Err(Error::DelegationParseError)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
|
pub struct ConditionQuery {
|
|
pub conditions: Vec<Condition>,
|
|
}
|
|
|
|
impl ConditionQuery {
|
|
pub fn allows_event(&self, event: &Event) -> bool {
|
|
// check each condition, to ensure that the event complies
|
|
// with the restriction.
|
|
for c in &self.conditions {
|
|
if !c.allows_event(event) {
|
|
// any failing conditions invalidates the delegation
|
|
// on this event
|
|
return false;
|
|
}
|
|
}
|
|
// delegation was permitted unconditionally, or all conditions
|
|
// were true
|
|
true
|
|
}
|
|
}
|
|
|
|
// Verify that the delegator approved the delegation; return a ConditionQuery if so.
|
|
pub fn validate_delegation(
|
|
delegator: &str,
|
|
delegatee: &str,
|
|
cond_query: &str,
|
|
sigstr: &str,
|
|
) -> Option<ConditionQuery> {
|
|
// form the token
|
|
let tok = format!("nostr:delegation:{}:{}", delegatee, cond_query);
|
|
// form SHA256 hash
|
|
let digest: sha256::Hash = sha256::Hash::hash(tok.as_bytes());
|
|
let sig = schnorr::Signature::from_str(sigstr).unwrap();
|
|
if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
|
|
if let Ok(pubkey) = XOnlyPublicKey::from_str(delegator) {
|
|
let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
|
|
if verify.is_ok() {
|
|
// return the parsed condition query
|
|
cond_query.parse::<ConditionQuery>().ok()
|
|
} else {
|
|
debug!("client sent an delegation signature that did not validate");
|
|
None
|
|
}
|
|
} else {
|
|
debug!("client sent malformed delegation pubkey");
|
|
None
|
|
}
|
|
} else {
|
|
info!("error converting delegation digest to secp256k1 message");
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Parsed delegation condition
|
|
/// see https://github.com/nostr-protocol/nips/pull/28#pullrequestreview-1084903800
|
|
/// An example complex condition would be: kind=1,2,3&created_at<1665265999
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
|
pub struct Condition {
|
|
pub field: Field,
|
|
pub operator: Operator,
|
|
pub values: Vec<u64>,
|
|
}
|
|
|
|
impl Condition {
|
|
/// Check if this condition allows the given event to be delegated
|
|
pub fn allows_event(&self, event: &Event) -> bool {
|
|
// determine what the right-hand side of the operator is
|
|
let resolved_field = match &self.field {
|
|
Field::Kind => event.kind,
|
|
Field::CreatedAt => event.created_at,
|
|
};
|
|
match &self.operator {
|
|
Operator::LessThan => {
|
|
// the less-than operator is only valid for single values.
|
|
if self.values.len() == 1 {
|
|
if let Some(v) = self.values.first() {
|
|
return resolved_field < *v;
|
|
}
|
|
}
|
|
}
|
|
Operator::GreaterThan => {
|
|
// the greater-than operator is only valid for single values.
|
|
if self.values.len() == 1 {
|
|
if let Some(v) = self.values.first() {
|
|
return resolved_field > *v;
|
|
}
|
|
}
|
|
}
|
|
Operator::Equals => {
|
|
// equals is interpreted as "must be equal to at least one provided value"
|
|
return self.values.iter().any(|&x| resolved_field == x);
|
|
}
|
|
Operator::NotEquals => {
|
|
// not-equals is interpreted as "must not be equal to any provided value"
|
|
// this is the one case where an empty list of values could be allowed; even though it is a pointless restriction.
|
|
return self.values.iter().all(|&x| resolved_field != x);
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
fn str_to_condition(cs: &str) -> Option<Condition> {
|
|
// a condition is a string (alphanum+underscore), an operator (<>=!), and values (num+comma)
|
|
lazy_static! {
|
|
static ref RE: Regex = Regex::new("([[:word:]]+)([<>=!]+)([,[[:digit:]]]*)").unwrap();
|
|
}
|
|
// match against the regex
|
|
let caps = RE.captures(cs)?;
|
|
let field = caps.get(1)?.as_str().parse::<Field>().ok()?;
|
|
let operator = caps.get(2)?.as_str().parse::<Operator>().ok()?;
|
|
// values are just comma separated numbers, but all must be parsed
|
|
let rawvals = caps.get(3)?.as_str();
|
|
let values = rawvals
|
|
.split_terminator(',')
|
|
.map(|n| n.parse::<u64>().ok())
|
|
.collect::<Option<Vec<_>>>()?;
|
|
// convert field string into Field
|
|
Some(Condition {
|
|
field,
|
|
operator,
|
|
values,
|
|
})
|
|
}
|
|
|
|
/// Parse a condition query from a string slice
|
|
impl FromStr for ConditionQuery {
|
|
type Err = Error;
|
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
// split the string with '&'
|
|
let mut conditions = vec![];
|
|
let condstrs = value.split_terminator('&');
|
|
// parse each individual condition
|
|
for c in condstrs {
|
|
conditions.push(str_to_condition(c).ok_or(Error::DelegationParseError)?);
|
|
}
|
|
Ok(ConditionQuery { conditions })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// parse condition strings
|
|
#[test]
|
|
fn parse_empty() -> Result<()> {
|
|
// given an empty condition query, produce an empty vector
|
|
let empty_cq = ConditionQuery { conditions: vec![] };
|
|
let parsed = "".parse::<ConditionQuery>()?;
|
|
assert_eq!(parsed, empty_cq);
|
|
Ok(())
|
|
}
|
|
|
|
// parse field 'kind'
|
|
#[test]
|
|
fn test_kind_field_parse() -> Result<()> {
|
|
let field = "kind".parse::<Field>()?;
|
|
assert_eq!(field, Field::Kind);
|
|
Ok(())
|
|
}
|
|
// parse field 'created_at'
|
|
#[test]
|
|
fn test_created_at_field_parse() -> Result<()> {
|
|
let field = "created_at".parse::<Field>()?;
|
|
assert_eq!(field, Field::CreatedAt);
|
|
Ok(())
|
|
}
|
|
// parse unknown field
|
|
#[test]
|
|
fn unknown_field_parse() {
|
|
let field = "unk".parse::<Field>();
|
|
assert!(field.is_err());
|
|
}
|
|
|
|
// parse a full conditional query with an empty array
|
|
#[test]
|
|
fn parse_kind_equals_empty() -> Result<()> {
|
|
// given an empty condition query, produce an empty vector
|
|
let kind_cq = ConditionQuery {
|
|
conditions: vec![Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::Equals,
|
|
values: vec![],
|
|
}],
|
|
};
|
|
let parsed = "kind=".parse::<ConditionQuery>()?;
|
|
assert_eq!(parsed, kind_cq);
|
|
Ok(())
|
|
}
|
|
// parse a full conditional query with a single value
|
|
#[test]
|
|
fn parse_kind_equals_singleval() -> Result<()> {
|
|
// given an empty condition query, produce an empty vector
|
|
let kind_cq = ConditionQuery {
|
|
conditions: vec![Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::Equals,
|
|
values: vec![1],
|
|
}],
|
|
};
|
|
let parsed = "kind=1".parse::<ConditionQuery>()?;
|
|
assert_eq!(parsed, kind_cq);
|
|
Ok(())
|
|
}
|
|
// parse a full conditional query with multiple values
|
|
#[test]
|
|
fn parse_kind_equals_multival() -> Result<()> {
|
|
// given an empty condition query, produce an empty vector
|
|
let kind_cq = ConditionQuery {
|
|
conditions: vec![Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::Equals,
|
|
values: vec![1, 2, 4],
|
|
}],
|
|
};
|
|
let parsed = "kind=1,2,4".parse::<ConditionQuery>()?;
|
|
assert_eq!(parsed, kind_cq);
|
|
Ok(())
|
|
}
|
|
// parse multiple conditions
|
|
#[test]
|
|
fn parse_multi_conditions() -> Result<()> {
|
|
// given an empty condition query, produce an empty vector
|
|
let cq = ConditionQuery {
|
|
conditions: vec![
|
|
Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::GreaterThan,
|
|
values: vec![10000],
|
|
},
|
|
Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::LessThan,
|
|
values: vec![20000],
|
|
},
|
|
Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::NotEquals,
|
|
values: vec![10001],
|
|
},
|
|
Condition {
|
|
field: Field::CreatedAt,
|
|
operator: Operator::LessThan,
|
|
values: vec![1665867123],
|
|
},
|
|
],
|
|
};
|
|
let parsed =
|
|
"kind>10000&kind<20000&kind!10001&created_at<1665867123".parse::<ConditionQuery>()?;
|
|
assert_eq!(parsed, cq);
|
|
Ok(())
|
|
}
|
|
// Check for condition logic on event w/ empty values
|
|
#[test]
|
|
fn condition_with_empty_values() {
|
|
let mut c = Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::GreaterThan,
|
|
values: vec![],
|
|
};
|
|
let e = Event::simple_event();
|
|
assert!(!c.allows_event(&e));
|
|
c.operator = Operator::LessThan;
|
|
assert!(!c.allows_event(&e));
|
|
c.operator = Operator::Equals;
|
|
assert!(!c.allows_event(&e));
|
|
// Not Equals applied to an empty list *is* allowed
|
|
// (pointless, but logically valid).
|
|
c.operator = Operator::NotEquals;
|
|
assert!(c.allows_event(&e));
|
|
}
|
|
|
|
// Check for condition logic on event w/ single value
|
|
#[test]
|
|
fn condition_kind_gt_event_single() {
|
|
let c = Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::GreaterThan,
|
|
values: vec![10],
|
|
};
|
|
let mut e = Event::simple_event();
|
|
// kind is not greater than 10, not allowed
|
|
e.kind = 1;
|
|
assert!(!c.allows_event(&e));
|
|
// kind is greater than 10, allowed
|
|
e.kind = 100;
|
|
assert!(c.allows_event(&e));
|
|
// kind is 10, not allowed
|
|
e.kind = 10;
|
|
assert!(!c.allows_event(&e));
|
|
}
|
|
// Check for condition logic on event w/ multi values
|
|
#[test]
|
|
fn condition_with_multi_values() {
|
|
let mut c = Condition {
|
|
field: Field::Kind,
|
|
operator: Operator::Equals,
|
|
values: vec![0, 10, 20],
|
|
};
|
|
let mut e = Event::simple_event();
|
|
// Allow if event kind is in list for Equals
|
|
e.kind = 10;
|
|
assert!(c.allows_event(&e));
|
|
// Deny if event kind is not in list for Equals
|
|
e.kind = 11;
|
|
assert!(!c.allows_event(&e));
|
|
// Deny if event kind is in list for NotEquals
|
|
e.kind = 10;
|
|
c.operator = Operator::NotEquals;
|
|
assert!(!c.allows_event(&e));
|
|
// Allow if event kind is not in list for NotEquals
|
|
e.kind = 99;
|
|
c.operator = Operator::NotEquals;
|
|
assert!(c.allows_event(&e));
|
|
// Always deny if GreaterThan/LessThan for a list
|
|
c.operator = Operator::LessThan;
|
|
assert!(!c.allows_event(&e));
|
|
c.operator = Operator::GreaterThan;
|
|
assert!(!c.allows_event(&e));
|
|
}
|
|
}
|