Compare commits

..
Author SHA1 Message Date
William Casarin c12da6a229 Command result notices
This introduces a new notice-like event with more structure so that clients
can know if an event is sucessfully written to the database. Clients
can't really do much with the current NOTICE messages, but with these
structured result messages, it can know if an event was sucessfully
saved.

Whenever there is an error and we have an id available (event id, sub
id, etc) we return these structured OK events instead.

Example:

When saving the following event:
    ["EVENT",{"id": "event_id" }]

The server will now return events in the following format:
    ["OK", event_id, "true|false", message]

For example, on a successful save:
    ["OK", "event_id", "true"]

If we already have the event:
    ["OK", "event_id", "true", "duplicate"]

If the event is rejected:
    ["OK", "event_id", "false", "you are blocked"]

If a subscription fails:
    ["OK", "sub_id", "false", "Too many subscriptions"]

NIP coming soon!
2022-11-09 07:44:39 -06:00
William Casarin 7adc5c9af7 perf: dont create intermediate vecs when matching subs
Avoid creating intermediate vectors when matching subscriptions. We can
just iterate over the hashmap directly.
2022-11-09 07:30:43 -06:00
Greg Heartsfield 9dd4571bee refactor: reduce level of some common DB logs 2022-11-06 13:49:32 -06:00
Greg Heartsfield 9db5a26b9c refactor: more consistent logging messages 2022-11-05 16:11:20 -05:00
Greg Heartsfield ac345b5744 refactor: do not quote server-generated client id in logs 2022-11-05 15:59:39 -05:00
Greg Heartsfield 675662c7fb improvement: upgrade docker builder and base images 2022-11-05 13:24:17 -05:00
Greg Heartsfield 505b0cb71f improvement: upgrade multiple dependencies
Updating anyhow v1.0.65 -> v1.0.66
Updating async-trait v0.1.57 -> v0.1.58
Updating axum v0.5.16 -> v0.5.17
Updating axum-core v0.2.8 -> v0.2.9
Updating base64 v0.13.0 -> v0.13.1
Updating bumpalo v3.11.0 -> v3.11.1
Updating cc v1.0.73 -> v1.0.74
Updating cxx v1.0.79 -> v1.0.80
Updating cxx-build v1.0.79 -> v1.0.80
Updating cxxbridge-flags v1.0.79 -> v1.0.80
Updating cxxbridge-macro v1.0.79 -> v1.0.80
Updating futures v0.3.24 -> v0.3.25
Updating futures-channel v0.3.24 -> v0.3.25
Updating futures-core v0.3.24 -> v0.3.25
Updating futures-executor v0.3.24 -> v0.3.25
Updating futures-io v0.3.24 -> v0.3.25
Updating futures-macro v0.3.24 -> v0.3.25
Updating futures-sink v0.3.24 -> v0.3.25
Updating futures-task v0.3.24 -> v0.3.25
Updating futures-util v0.3.24 -> v0.3.25
Updating getrandom v0.2.7 -> v0.2.8
Updating h2 v0.3.14 -> v0.3.15
Updating hyper v0.14.20 -> v0.14.22
Updating iana-time-zone v0.1.51 -> v0.1.53
Updating libc v0.2.135 -> v0.2.137
Updating mio v0.8.4 -> v0.8.5
Updating native-tls v0.2.10 -> v0.2.11
Updating num_cpus v1.13.1 -> v1.14.0
Updating once_cell v1.15.0 -> v1.16.0
Updating openssl-sys v0.9.76 -> v0.9.77
Updating parking_lot_core v0.9.3 -> v0.9.4
Updating pest v2.4.0 -> v2.4.1
Updating pest_derive v2.4.0 -> v2.4.1
Updating pest_generator v2.4.0 -> v2.4.1
Updating pest_meta v2.4.0 -> v2.4.1
Updating pkg-config v0.3.25 -> v0.3.26
Updating ppv-lite86 v0.2.16 -> v0.2.17
Updating prost v0.11.0 -> v0.11.2
Updating prost-derive v0.11.0 -> v0.11.2
Updating prost-types v0.11.1 -> v0.11.2
Updating serde v1.0.145 -> v1.0.147
Updating serde_derive v1.0.145 -> v1.0.147
Updating serde_json v1.0.86 -> v1.0.87
Updating syn v1.0.102 -> v1.0.103
  Adding windows-sys v0.42.0
  Adding windows_aarch64_gnullvm v0.42.0
  Adding windows_aarch64_msvc v0.42.0
  Adding windows_i686_gnu v0.42.0
  Adding windows_i686_msvc v0.42.0
  Adding windows_x86_64_gnu v0.42.0
  Adding windows_x86_64_gnullvm v0.42.0
  Adding windows_x86_64_msvc v0.42.0
2022-11-05 10:59:03 -05:00
Greg Heartsfield e8aa450802 build: bump version to 0.7.1 2022-11-05 10:35:38 -05:00
Greg Heartsfield 5a8860bb09 feat: log user-agent if present 2022-11-05 10:29:25 -05:00
Greg Heartsfield 11e43eccf9 refactor: add unit to ping_interval config 2022-11-05 07:42:08 -05:00
William Casarin 50577b2dfa feat: add network.ping_interval setting
Add a ping interval setting that allows you to customize the websocket
ping interval. The default of 5 minutes may be too high for some proxy
servers that disconnect connections that are held open for too long.
2022-11-05 07:40:28 -05:00
William Casarin a6cb6f8486 refactor: rename get_header_remote_ip -> get_header_string
This function has nothing to do with remote ips!
2022-11-05 07:37:18 -05:00
Greg Heartsfield ae5bf98d87 feat: retrieve client IP from header in config.toml
If the config.toml has defined a HTTP header to look for a remote IP,
that will be logged.  Otherwise, the socket address IP will be used.

closes: https://todo.sr.ht/~gheartsfield/nostr-rs-relay/47
2022-11-04 18:05:01 -05:00
William Casarin 1cf9d719f0 feat: look for proxied ip headers
This enables support for using the proxied IP from cloudflare. The damus
relay is behind cloudflare, so to get accurate remote ip logging we need
to look at the headers instead of the socket address.

Signed-off-by: William Casarin <jb55@jb55.com>
2022-11-04 17:09:28 -05:00
William Casarin 311f4b5283 refactor: switch new connections to debug log
These are pretty spammy on busy relays. I've been using the info log to
monitor spam attacks, and these are the least useful info log.

Leave the "stopping connection" log because it at least provides useful
sent/received information.

Signed-off-by: William Casarin <jb55@jb55.com>
2022-11-04 07:59:53 -05:00
Greg Heartsfield 14b5a51e3a fix: log ephemeral events after send 2022-11-04 07:55:38 -05:00
Greg Heartsfield 8ecce3f566 feat: show client IP in logs 2022-11-02 18:33:44 -05:00
Greg Heartsfield caffbbbede build: bump version to 0.7.0 2022-10-16 15:42:11 -05:00
Greg Heartsfield 81045ad3d0 improvement: upgrade multiple dependencies
Updating anyhow v1.0.64 -> v1.0.65
  Adding codespan-reporting v0.11.1
Updating const_format v0.2.28 -> v0.2.30
Updating const_format_proc_macros v0.2.22 -> v0.2.29
Updating crossbeam-utils v0.8.11 -> v0.8.12
  Adding cxx v1.0.79
  Adding cxx-build v1.0.79
  Adding cxxbridge-flags v1.0.79
  Adding cxxbridge-macro v1.0.79
Updating digest v0.10.3 -> v0.10.5
Updating hdrhistogram v7.5.1 -> v7.5.2
Updating iana-time-zone v0.1.50 -> v0.1.51
  Adding iana-time-zone-haiku v0.1.1
Updating itertools v0.10.3 -> v0.10.5
Updating itoa v1.0.3 -> v1.0.4
Updating js-sys v0.3.59 -> v0.3.60
Updating libc v0.2.132 -> v0.2.135
  Adding link-cplusplus v1.0.7
Updating lock_api v0.4.8 -> v0.4.9
Updating once_cell v1.14.0 -> v1.15.0
Updating openssl v0.10.41 -> v0.10.42
Updating openssl-sys v0.9.75 -> v0.9.76
Updating pest v2.3.0 -> v2.4.0
Updating pest_derive v2.3.0 -> v2.4.0
Updating pest_generator v2.3.0 -> v2.4.0
Updating pest_meta v2.3.0 -> v2.4.0
Updating proc-macro2 v1.0.43 -> v1.0.47
Updating rand_core v0.6.3 -> v0.6.4
Updating raw-cpuid v10.5.0 -> v10.6.0
  Adding scratch v1.0.2
Updating serde v1.0.144 -> v1.0.145
Updating serde_derive v1.0.144 -> v1.0.145
Updating serde_json v1.0.85 -> v1.0.86
  Adding sha1 v0.10.5
Updating smallvec v1.9.0 -> v1.10.0
Updating syn v1.0.99 -> v1.0.102
  Adding termcolor v1.1.3
Updating thiserror v1.0.34 -> v1.0.37
Updating thiserror-impl v1.0.34 -> v1.0.37
Updating tokio v1.21.0 -> v1.21.2
Updating tokio-stream v0.1.9 -> v0.1.11
Updating tonic v0.8.1 -> v0.8.2
Updating tower-layer v0.3.1 -> v0.3.2
Updating tracing v0.1.36 -> v0.1.37
Updating tracing-attributes v0.1.22 -> v0.1.23
Updating tracing-core v0.1.29 -> v0.1.30
Updating tracing-subscriber v0.3.15 -> v0.3.16
Updating unicode-ident v1.0.3 -> v1.0.5
Updating unicode-normalization v0.1.21 -> v0.1.22
  Adding unicode-width v0.1.10
Updating uuid v1.1.2 -> v1.2.1
Updating wasm-bindgen v0.2.82 -> v0.2.83
Updating wasm-bindgen-backend v0.2.82 -> v0.2.83
Updating wasm-bindgen-macro v0.2.82 -> v0.2.83
Updating wasm-bindgen-macro-support v0.2.82 -> v0.2.83
Updating wasm-bindgen-shared v0.2.82 -> v0.2.83
Updating web-sys v0.3.59 -> v0.3.60
  Adding winapi-util v0.1.5
2022-10-16 15:33:11 -05:00
Greg Heartsfield 72f8a1aa5c feat(NIP-26): allow searches for delegated public keys
Implements core NIP-26 delegated event functionality.  Events can
include a `delegation` tag that provides a signature and restrictions
on which events can be delegated.

Notable points on the implementation so far:

* Schema has been upgraded to include an index and new column.
* Basic rune parsing/evaluation to implement the example event in the
  NIP, but no more.
* No special logic for deletion.
* No migration logic for determining delegated authors for
  already-stored events.
2022-10-16 15:25:06 -05:00
17 changed files with 616 additions and 261 deletions
Generated
+332 -162
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.6.2" version = "0.7.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
+3 -2
View File
@@ -1,4 +1,4 @@
FROM docker.io/library/rust:1.64.0@sha256:5cf09a76cb9baf4990d121221bbad64927cc5690ee54f246487e302ddc2ba300 as builder FROM docker.io/library/rust:1.65.0@sha256:1bca14676a365d0ed37a1e2a1da86c2bcf883fdf6e6886469434763d94d4afd5 as builder
RUN USER=root cargo new --bin nostr-rs-relay RUN USER=root cargo new --bin nostr-rs-relay
WORKDIR ./nostr-rs-relay WORKDIR ./nostr-rs-relay
@@ -12,7 +12,8 @@ COPY ./src ./src
RUN rm ./target/release/deps/nostr*relay* RUN rm ./target/release/deps/nostr*relay*
RUN cargo build --release RUN cargo build --release
FROM docker.io/library/debian:bullseye-20221004-slim@sha256:8b702518a671c926b5ece4efe386a476eb4777646a36d996d4bd50944f2f11a2 FROM docker.io/library/debian:bullseye-20221024-slim@sha256:76cdda8fe5eb597ef5e712e4c9a9f5f1fb119e69f353daaa7bd6d0f6e66e541d
ARG APP=/usr/src/app ARG APP=/usr/src/app
ARG APP_DATA=/usr/src/app/db ARG APP_DATA=/usr/src/app/db
RUN apt-get update \ RUN apt-get update \
+1
View File
@@ -27,6 +27,7 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
- [x] NIP-15: [End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md) - [x] NIP-15: [End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md)
- [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md) - [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md)
- [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_) - [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_)
- [x] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md)
## Quick Start ## Quick Start
+8
View File
@@ -46,6 +46,14 @@ address = "0.0.0.0"
# Listen on this port # Listen on this port
port = 8080 port = 8080
# If present, read this HTTP header for logging client IP addresses.
# Examples for common proxies, cloudflare:
#remote_ip_header = "x-forwarded-for"
#remote_ip_header = "cf-connecting-ip"
# Websocket ping interval in seconds, defaults to 5 minutes
#ping_interval = 300
[options] [options]
# Reject events that have timestamps greater than this many seconds in # Reject events that have timestamps greater than this many seconds in
# the future. Recommended to reject anything greater than 30 minutes # the future. Recommended to reject anything greater than 30 minutes
+4 -1
View File
@@ -28,9 +28,10 @@ pub struct Database {
pub struct Network { pub struct Network {
pub port: u16, pub port: u16,
pub address: String, pub address: String,
pub remote_ip_header: Option<String>, // retrieve client IP from this HTTP header if present
pub ping_interval_seconds: u32,
} }
//
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(unused)] #[allow(unused)]
pub struct Options { pub struct Options {
@@ -207,7 +208,9 @@ impl Default for Settings {
}, },
network: Network { network: Network {
port: 8080, port: 8080,
ping_interval_seconds: 300,
address: "0.0.0.0".to_owned(), address: "0.0.0.0".to_owned(),
remote_ip_header: None,
}, },
limits: Limits { limits: Limits {
messages_per_sec: None, messages_per_sec: None,
+12 -13
View File
@@ -2,7 +2,6 @@
use crate::close::Close; use crate::close::Close;
use crate::error::Error; use crate::error::Error;
use crate::error::Result; use crate::error::Result;
use crate::event::Event;
use crate::subscription::Subscription; use crate::subscription::Subscription;
use std::collections::HashMap; use std::collections::HashMap;
@@ -14,6 +13,8 @@ const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
/// State for a client connection /// State for a client connection
pub struct ClientConn { pub struct ClientConn {
/// Client IP (either from socket, or configured proxy header
client_ip: String,
/// Unique client identifier generated at connection time /// Unique client identifier generated at connection time
client_id: Uuid, client_id: Uuid,
/// The current set of active client subscriptions /// The current set of active client subscriptions
@@ -24,22 +25,27 @@ pub struct ClientConn {
impl Default for ClientConn { impl Default for ClientConn {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new("unknown".to_owned())
} }
} }
impl ClientConn { impl ClientConn {
/// Create a new, empty connection state. /// Create a new, empty connection state.
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new(client_ip: String) -> Self {
let client_id = Uuid::new_v4(); let client_id = Uuid::new_v4();
ClientConn { ClientConn {
client_ip,
client_id, client_id,
subscriptions: HashMap::new(), subscriptions: HashMap::new(),
max_subs: 32, max_subs: 32,
} }
} }
pub fn subscriptions(&self) -> &HashMap<String, Subscription> {
&self.subscriptions
}
/// Get a short prefix of the client's unique identifier, suitable /// Get a short prefix of the client's unique identifier, suitable
/// for logging. /// for logging.
#[must_use] #[must_use]
@@ -47,16 +53,9 @@ impl ClientConn {
self.client_id.to_string().chars().take(8).collect() self.client_id.to_string().chars().take(8).collect()
} }
/// Find all matching subscriptions.
#[must_use] #[must_use]
pub fn get_matching_subscriptions(&self, e: &Event) -> Vec<&str> { pub fn ip(&self) -> &str {
let mut v: Vec<&str> = vec![]; &self.client_ip
for (id, sub) in &self.subscriptions {
if sub.interested_in_event(e) {
v.push(id);
}
}
v
} }
/// Add a new subscription for this connection. /// Add a new subscription for this connection.
@@ -104,7 +103,7 @@ impl ClientConn {
debug!( debug!(
"removed subscription, currently have {} active subs (cid={})", "removed subscription, currently have {} active subs (cid={})",
self.subscriptions.len(), self.subscriptions.len(),
self.client_id self.get_client_prefix(),
); );
} }
} }
+39 -25
View File
@@ -6,6 +6,7 @@ use crate::event::{single_char_tagname, Event};
use crate::hexrange::hex_range; use crate::hexrange::hex_range;
use crate::hexrange::HexSearch; use crate::hexrange::HexSearch;
use crate::nip05; use crate::nip05;
use crate::notice::Notice;
use crate::schema::{upgrade_db, STARTUP_SQL}; use crate::schema::{upgrade_db, STARTUP_SQL};
use crate::subscription::ReqFilter; use crate::subscription::ReqFilter;
use crate::subscription::Subscription; use crate::subscription::Subscription;
@@ -32,7 +33,7 @@ pub type PooledConnection = r2d2::PooledConnection<r2d2_sqlite::SqliteConnection
/// Events submitted from a client, with a return channel for notices /// Events submitted from a client, with a return channel for notices
pub struct SubmittedEvent { pub struct SubmittedEvent {
pub event: Event, pub event: Event,
pub notice_tx: tokio::sync::mpsc::Sender<String>, pub notice_tx: tokio::sync::mpsc::Sender<Notice>,
} }
/// Database file /// Database file
@@ -142,12 +143,15 @@ pub async fn db_writer(
if next_event.is_none() { if next_event.is_none() {
break; break;
} }
// track if an event write occurred; this is used to
// update the rate limiter
let mut event_write = false; let mut event_write = false;
let subm_event = next_event.unwrap(); let subm_event = next_event.unwrap();
let event = subm_event.event; let event = subm_event.event;
let notice_tx = subm_event.notice_tx; let notice_tx = subm_event.notice_tx;
// check if this event is authorized. // check if this event is authorized.
if let Some(allowed_addrs) = whitelist { if let Some(allowed_addrs) = whitelist {
// TODO: incorporate delegated pubkeys
// if the event address is not in allowed_addrs. // if the event address is not in allowed_addrs.
if !allowed_addrs.contains(&event.pubkey) { if !allowed_addrs.contains(&event.pubkey) {
info!( info!(
@@ -155,7 +159,9 @@ pub async fn db_writer(
event.get_event_id_prefix() event.get_event_id_prefix()
); );
notice_tx notice_tx
.try_send("pubkey is not allowed to publish to this relay".to_owned()) .try_send(Notice::message(
"pubkey is not allowed to publish to this relay".to_owned(),
))
.ok(); .ok();
continue; continue;
} }
@@ -186,10 +192,10 @@ pub async fn db_writer(
event.get_author_prefix() event.get_author_prefix()
); );
notice_tx notice_tx
.try_send( .try_send(Notice::message(
"NIP-05 verification is no longer valid (expired/wrong domain)" "NIP-05 verification is no longer valid (expired/wrong domain)"
.to_owned(), .to_owned(),
) ))
.ok(); .ok();
continue; continue;
} }
@@ -200,7 +206,9 @@ pub async fn db_writer(
event.get_author_prefix() event.get_author_prefix()
); );
notice_tx notice_tx
.try_send("NIP-05 verification needed to publish events".to_owned()) .try_send(Notice::message(
"NIP-05 verification needed to publish events".to_owned(),
))
.ok(); .ok();
continue; continue;
} }
@@ -213,22 +221,23 @@ pub async fn db_writer(
// TODO: cache recent list of authors to remove a DB call. // TODO: cache recent list of authors to remove a DB call.
let start = Instant::now(); let start = Instant::now();
if event.kind >= 20000 && event.kind < 30000 { if event.kind >= 20000 && event.kind < 30000 {
bcast_tx.send(event.clone()).ok();
info!( info!(
"published ephemeral event {:?} from {:?} in {:?}", "published ephemeral event: {:?} from: {:?} in: {:?}",
event.get_event_id_prefix(), event.get_event_id_prefix(),
event.get_author_prefix(), event.get_author_prefix(),
start.elapsed() start.elapsed()
); );
bcast_tx.send(event.clone()).ok();
event_write = true event_write = true
} else { } else {
match write_event(&mut pool.get()?, &event) { match write_event(&mut pool.get()?, &event) {
Ok(updated) => { Ok(updated) => {
if updated == 0 { if updated == 0 {
trace!("ignoring duplicate or deleted event"); trace!("ignoring duplicate or deleted event");
notice_tx.try_send(Notice::duplicate(event.id)).ok();
} else { } else {
info!( info!(
"persisted event {:?} from {:?} in {:?}", "persisted event: {:?} from: {:?} in: {:?}",
event.get_event_id_prefix(), event.get_event_id_prefix(),
event.get_author_prefix(), event.get_author_prefix(),
start.elapsed() start.elapsed()
@@ -236,16 +245,14 @@ pub async fn db_writer(
event_write = true; event_write = true;
// send this out to all clients // send this out to all clients
bcast_tx.send(event.clone()).ok(); bcast_tx.send(event.clone()).ok();
notice_tx.try_send(Notice::saved(event.id)).ok();
} }
} }
Err(err) => { Err(err) => {
warn!("event insert failed: {:?}", err); warn!("event insert failed: {:?}", err);
notice_tx let msg =
.try_send( "relay experienced an error trying to publish the latest event".into();
"relay experienced an error trying to publish the latest event" notice_tx.try_send(Notice::err_msg(msg, event.id)).ok();
.to_owned(),
)
.ok();
} }
} }
} }
@@ -284,12 +291,13 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
let tx = conn.transaction()?; let tx = conn.transaction()?;
// get relevant fields from event and convert to blobs. // get relevant fields from event and convert to blobs.
let id_blob = hex::decode(&e.id).ok(); let id_blob = hex::decode(&e.id).ok();
let pubkey_blob = hex::decode(&e.pubkey).ok(); let pubkey_blob: Option<Vec<u8>> = hex::decode(&e.pubkey).ok();
let delegator_blob: Option<Vec<u8>> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok());
let event_str = serde_json::to_string(&e).ok(); let event_str = serde_json::to_string(&e).ok();
// ignore if the event hash is a duplicate. // ignore if the event hash is a duplicate.
let mut ins_count = tx.execute( let mut ins_count = tx.execute(
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'), FALSE);", "INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);",
params![id_blob, e.created_at, e.kind, pubkey_blob, event_str] params![id_blob, e.created_at, e.kind, pubkey_blob, delegator_blob, event_str]
)?; )?;
if ins_count == 0 { if ins_count == 0 {
// if the event was a duplicate, no need to insert event or // if the event was a duplicate, no need to insert event or
@@ -312,7 +320,7 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
if is_lower_hex(tagval) && (tagval.len() % 2 == 0) { if is_lower_hex(tagval) && (tagval.len() % 2 == 0) {
tx.execute( tx.execute(
"INSERT OR IGNORE INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3)", "INSERT OR IGNORE INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3)",
params![ev_id, &tagname, hex::decode(&tagval).ok()], params![ev_id, &tagname, hex::decode(tagval).ok()],
)?; )?;
} else { } else {
tx.execute( tx.execute(
@@ -439,16 +447,22 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
for auth in authvec { for auth in authvec {
match hex_range(auth) { match hex_range(auth) {
Some(HexSearch::Exact(ex)) => { Some(HexSearch::Exact(ex)) => {
auth_searches.push("author=?".to_owned()); auth_searches.push("author=? OR delegated_by=?".to_owned());
params.push(Box::new(ex.clone()));
params.push(Box::new(ex)); params.push(Box::new(ex));
} }
Some(HexSearch::Range(lower, upper)) => { Some(HexSearch::Range(lower, upper)) => {
auth_searches.push("(author>? AND author<?)".to_owned()); auth_searches.push(
"(author>? AND author<?) OR (delegated_by>? AND delegated_by<?)".to_owned(),
);
params.push(Box::new(lower.clone()));
params.push(Box::new(upper.clone()));
params.push(Box::new(lower)); params.push(Box::new(lower));
params.push(Box::new(upper)); params.push(Box::new(upper));
} }
Some(HexSearch::LowerOnly(lower)) => { Some(HexSearch::LowerOnly(lower)) => {
auth_searches.push("author>?".to_owned()); auth_searches.push("author>? OR delegated_by>?".to_owned());
params.push(Box::new(lower.clone()));
params.push(Box::new(lower)); params.push(Box::new(lower));
} }
None => { None => {
@@ -500,7 +514,7 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
let mut blob_vals: Vec<Box<dyn ToSql>> = vec![]; let mut blob_vals: Vec<Box<dyn ToSql>> = vec![];
for v in val { for v in val {
if (v.len() % 2 == 0) && is_lower_hex(v) { if (v.len() % 2 == 0) && is_lower_hex(v) {
if let Ok(h) = hex::decode(&v) { if let Ok(h) = hex::decode(v) {
blob_vals.push(Box::new(h)); blob_vals.push(Box::new(h));
} }
} else { } else {
@@ -567,7 +581,7 @@ fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
.map(|s| format!("SELECT content, created_at FROM ({})", s)) .map(|s| format!("SELECT content, created_at FROM ({})", s))
.collect(); .collect();
let query: String = subqueries_selects.join(" UNION "); let query: String = subqueries_selects.join(" UNION ");
debug!("final query string: {}", query); trace!("final query string: {}", query);
(query, params) (query, params)
} }
@@ -585,12 +599,12 @@ pub async fn db_query(
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>, mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
) { ) {
task::spawn_blocking(move || { task::spawn_blocking(move || {
debug!("going to query for: {:?}", sub); trace!("going to query for: {:?}", sub);
let mut row_count: usize = 0; let mut row_count: usize = 0;
let start = Instant::now(); let start = Instant::now();
// generate SQL query // generate SQL query
let (q, p) = query_from_sub(&sub); let (q, p) = query_from_sub(&sub);
debug!("SQL generated in {:?}", start.elapsed()); trace!("SQL generated in {:?}", start.elapsed());
// show pool stats // show pool stats
debug!("DB pool stats: {:?}", pool.state()); debug!("DB pool stats: {:?}", pool.state());
let start = Instant::now(); let start = Instant::now();
+14 -8
View File
@@ -30,10 +30,6 @@ use tracing::{debug, info};
// different condition strings, since we do not support grouping or // different condition strings, since we do not support grouping or
// "OR" logic. // "OR" logic.
// using serde_urldecode, we can get a serde data format from the
// condition string. We will then map that with a deserializer that
// maps to a ConditionQuery.
lazy_static! { lazy_static! {
/// Secp256k1 verification instance. /// Secp256k1 verification instance.
pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only(); pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
@@ -88,10 +84,19 @@ pub struct ConditionQuery {
} }
impl ConditionQuery { impl ConditionQuery {
pub fn allows_event(&self, _event: &Event) -> bool { pub fn allows_event(&self, event: &Event) -> bool {
// check each condition, to ensure that the event complies with the restriction. // check each condition, to ensure that the event complies
// with the restriction.
false 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
} }
} }
@@ -331,6 +336,7 @@ mod tests {
Event { Event {
id: "0".to_owned(), id: "0".to_owned(),
pubkey: "0".to_owned(), pubkey: "0".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: vec![], tags: vec![],
+33 -7
View File
@@ -32,6 +32,8 @@ pub struct EventCmd {
pub struct Event { pub struct Event {
pub id: String, pub id: String,
pub(crate) pubkey: String, pub(crate) pubkey: String,
#[serde(skip)]
pub(crate) delegated_by: Option<String>,
pub(crate) created_at: u64, pub(crate) created_at: u64,
pub(crate) kind: u64, pub(crate) kind: u64,
#[serde(deserialize_with = "tag_from_string")] #[serde(deserialize_with = "tag_from_string")]
@@ -84,6 +86,7 @@ impl From<EventCmd> for Result<Event> {
} else if ec.event.is_valid() { } else if ec.event.is_valid() {
let mut e = ec.event; let mut e = ec.event;
e.build_index(); e.build_index();
e.update_delegation();
Ok(e) Ok(e)
} else { } else {
Err(EventInvalid) Err(EventInvalid)
@@ -116,27 +119,45 @@ impl Event {
// if so, return an alternate author for the event // if so, return an alternate author for the event
pub fn delegated_author(&self) -> Option<String> { pub fn delegated_author(&self) -> Option<String> {
// is there a delegation tag? // is there a delegation tag?
let delegation_tag = self.tag_values_by_name("delegation"); let delegation_tag: Vec<String> = self
.tags
.iter()
.filter(|x| x.len() == 4)
.filter(|x| x.get(0).unwrap() == "delegation")
.take(1)
.next()?
.to_vec(); // get first tag
//let delegation_tag = self.tag_values_by_name("delegation");
// delegation tags should have exactly 3 elements after the name (pubkey, condition, sig) // delegation tags should have exactly 3 elements after the name (pubkey, condition, sig)
// the event is signed by the delagatee // the event is signed by the delagatee
let delegatee = &self.pubkey; let delegatee = &self.pubkey;
// the delegation tag references the claimed delagator // the delegation tag references the claimed delagator
let delegator = delegation_tag.get(0)?; let delegator: &str = delegation_tag.get(1)?;
let querystr = delegation_tag.get(1)?; let querystr: &str = delegation_tag.get(2)?;
let sig = delegation_tag.get(2)?; let sig: &str = delegation_tag.get(3)?;
// pass into the validate_delegation
// attempt to get a condition query; this requires the delegation to have a valid signature.
if let Some(cond_query) = validate_delegation(delegator, delegatee, querystr, sig) { if let Some(cond_query) = validate_delegation(delegator, delegatee, querystr, sig) {
// check if this condition query would allow this event. // The signature was valid, now we ensure the delegation
// condition is valid for this event:
if cond_query.allows_event(self) { if cond_query.allows_event(self) {
// since this is allowed, we will provide the delegatee
Some(delegator.into()) Some(delegator.into())
} else { } else {
debug!("an event failed to satisfy delegation conditions");
None None
} }
} else { } else {
debug!("event had had invalid delegation signature");
None None
} }
} }
/// Update delegation status
fn update_delegation(&mut self) {
self.delegated_by = self.delegated_author();
}
/// Build an event tag index /// Build an event tag index
fn build_index(&mut self) { fn build_index(&mut self) {
// if there are no tags; just leave the index as None // if there are no tags; just leave the index as None
@@ -172,7 +193,7 @@ impl Event {
self.pubkey.chars().take(8).collect() self.pubkey.chars().take(8).collect()
} }
/// Retrieve tag values /// Retrieve tag initial values across all tags matching the name
pub fn tag_values_by_name(&self, tag_name: &str) -> Vec<String> { pub fn tag_values_by_name(&self, tag_name: &str) -> Vec<String> {
self.tags self.tags
.iter() .iter()
@@ -296,6 +317,7 @@ mod tests {
Event { Event {
id: "0".to_owned(), id: "0".to_owned(),
pubkey: "0".to_owned(), pubkey: "0".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: vec![], tags: vec![],
@@ -377,6 +399,7 @@ mod tests {
let e = Event { let e = Event {
id: "999".to_owned(), id: "999".to_owned(),
pubkey: "012345".to_owned(), pubkey: "012345".to_owned(),
delegated_by: None,
created_at: 501234, created_at: 501234,
kind: 1, kind: 1,
tags: vec![], tags: vec![],
@@ -394,6 +417,7 @@ mod tests {
let e = Event { let e = Event {
id: "999".to_owned(), id: "999".to_owned(),
pubkey: "012345".to_owned(), pubkey: "012345".to_owned(),
delegated_by: None,
created_at: 501234, created_at: 501234,
kind: 1, kind: 1,
tags: vec![ tags: vec![
@@ -420,6 +444,7 @@ mod tests {
let e = Event { let e = Event {
id: "999".to_owned(), id: "999".to_owned(),
pubkey: "012345".to_owned(), pubkey: "012345".to_owned(),
delegated_by: None,
created_at: 501234, created_at: 501234,
kind: 1, kind: 1,
tags: vec![ tags: vec![
@@ -446,6 +471,7 @@ mod tests {
let e = Event { let e = Event {
id: "999".to_owned(), id: "999".to_owned(),
pubkey: "012345".to_owned(), pubkey: "012345".to_owned(),
delegated_by: None,
created_at: 501234, created_at: 501234,
kind: 1, kind: 1,
tags: vec![ tags: vec![
+1 -1
View File
@@ -35,7 +35,7 @@ impl From<config::Info> for RelayInfo {
description: i.description, description: i.description,
pubkey: i.pubkey, pubkey: i.pubkey,
contact: i.contact, contact: i.contact,
supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 22]), supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 22, 26]),
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()), software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
version: CARGO_PKG_VERSION.map(|x| x.to_owned()), version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
} }
+1
View File
@@ -8,6 +8,7 @@ pub mod event;
pub mod hexrange; pub mod hexrange;
pub mod info; pub mod info;
pub mod nip05; pub mod nip05;
pub mod notice;
pub mod schema; pub mod schema;
pub mod subscription; pub mod subscription;
pub mod utils; pub mod utils;
+1 -1
View File
@@ -34,11 +34,11 @@ fn main() {
// enable tracing with tokio-console // enable tracing with tokio-console
ConsoleLayer::builder().with_default_env().init(); ConsoleLayer::builder().with_default_env().init();
} }
// update with database location // update with database location
if let Some(db) = db_dir { if let Some(db) = db_dir {
settings.database.data_directory = db; settings.database.data_directory = db;
} }
let (_, ctrl_rx): (MpscSender<()>, MpscReceiver<()>) = syncmpsc::channel(); let (_, ctrl_rx): (MpscSender<()>, MpscReceiver<()>) = syncmpsc::channel();
// run this in a new thread // run this in a new thread
let handle = thread::spawn(|| { let handle = thread::spawn(|| {
+48
View File
@@ -0,0 +1,48 @@
use crate::error;
pub enum EventResultStatus {
Saved,
Duplicate,
Error(String),
}
pub struct EventResult {
pub id: String,
pub status: EventResultStatus,
}
pub enum Notice {
Message(String),
EventResult(EventResult),
}
impl Notice {
pub fn err(err: error::Error, id: String) -> Notice {
Notice::err_msg(format!("{}", err), id)
}
pub fn message(msg: String) -> Notice {
Notice::Message(msg)
}
pub fn saved(id: String) -> Notice {
Notice::EventResult(EventResult {
id,
status: EventResultStatus::Saved,
})
}
pub fn duplicate(id: String) -> Notice {
Notice::EventResult(EventResult {
id,
status: EventResultStatus::Duplicate,
})
}
pub fn err_msg(msg: String, id: String) -> Notice {
Notice::EventResult(EventResult {
id,
status: EventResultStatus::Error(msg),
})
}
}
+5 -3
View File
@@ -40,7 +40,7 @@ event_hash BLOB NOT NULL, -- 4-byte hash
first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970) first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970)
created_at INTEGER NOT NULL, -- when the event was authored created_at INTEGER NOT NULL, -- when the event was authored
author BLOB NOT NULL, -- author pubkey author BLOB NOT NULL, -- author pubkey
delegator BLOB, -- delegator pubkey (NIP-26) delegated_by BLOB, -- delegator pubkey (NIP-26)
kind INTEGER NOT NULL, -- event kind kind INTEGER NOT NULL, -- event kind
hidden INTEGER, -- relevant for queries hidden INTEGER, -- relevant for queries
content TEXT NOT NULL -- serialized json of event object content TEXT NOT NULL -- serialized json of event object
@@ -50,6 +50,7 @@ content TEXT NOT NULL -- serialized json of event object
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash); CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at); CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
CREATE INDEX IF NOT EXISTS author_index ON event(author); CREATE INDEX IF NOT EXISTS author_index ON event(author);
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
CREATE INDEX IF NOT EXISTS kind_index ON event(kind); CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
-- Tag Table -- Tag Table
@@ -331,7 +332,7 @@ fn mig_5_to_6(conn: &mut PooledConnection) -> Result<usize> {
if (tagval.len() % 2 == 0) && is_lower_hex(tagval) { if (tagval.len() % 2 == 0) && is_lower_hex(tagval) {
tx.execute( tx.execute(
"INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);", "INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);",
params![event_id, tagname, hex::decode(&tagval).ok()], params![event_id, tagname, hex::decode(tagval).ok()],
)?; )?;
} else { } else {
// otherwise, insert as text // otherwise, insert as text
@@ -357,7 +358,8 @@ fn mig_6_to_7(conn: &mut PooledConnection) -> Result<usize> {
info!("database schema needs update from 6->7"); info!("database schema needs update from 6->7");
// only change is adding a hidden column to events. // only change is adding a hidden column to events.
let upgrade_sql = r##" let upgrade_sql = r##"
ALTER TABLE event ADD delegator BLOB; ALTER TABLE event ADD delegated_by BLOB;
CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by);
PRAGMA user_version = 7; PRAGMA user_version = 7;
"##; "##;
match conn.execute_batch(upgrade_sql) { match conn.execute_batch(upgrade_sql) {
+90 -36
View File
@@ -10,9 +10,11 @@ use crate::event::Event;
use crate::event::EventCmd; use crate::event::EventCmd;
use crate::info::RelayInfo; use crate::info::RelayInfo;
use crate::nip05; use crate::nip05;
use crate::notice::{EventResultStatus, Notice};
use crate::subscription::Subscription; use crate::subscription::Subscription;
use futures::SinkExt; use futures::SinkExt;
use futures::StreamExt; use futures::StreamExt;
use http::header::HeaderMap;
use hyper::header::ACCEPT; use hyper::header::ACCEPT;
use hyper::service::{make_service_fn, service_fn}; use hyper::service::{make_service_fn, service_fn};
use hyper::upgrade::Upgraded; use hyper::upgrade::Upgraded;
@@ -84,11 +86,32 @@ async fn handle_web_request(
Some(config), Some(config),
) )
.await; .await;
let user_agent = get_header_string("user-agent", request.headers());
// determine the remote IP from headers if the exist
let header_ip = settings
.network
.remote_ip_header
.as_ref()
.and_then(|x| get_header_string(x, request.headers()));
// use the socket addr as a backup
let remote_ip =
header_ip.unwrap_or_else(|| remote_addr.ip().to_string());
let client_info = ClientInfo {
remote_ip,
user_agent,
};
// spawn a nostr server with our websocket
tokio::spawn(nostr_server( tokio::spawn(nostr_server(
pool, settings, ws_stream, broadcast, event_tx, shutdown, pool,
client_info,
settings,
ws_stream,
broadcast,
event_tx,
shutdown,
)); ));
} }
// todo: trace, don't print...
Err(e) => println!( Err(e) => println!(
"error when trying to upgrade connection \ "error when trying to upgrade connection \
from address {} to websocket connection. \ from address {} to websocket connection. \
@@ -148,6 +171,12 @@ async fn handle_web_request(
} }
} }
fn get_header_string(header: &str, headers: &HeaderMap) -> Option<String> {
headers
.get(header)
.and_then(|x| x.to_str().ok().map(|x| x.to_string()))
}
// return on a control-c or internally requested shutdown signal // return on a control-c or internally requested shutdown signal
async fn ctrl_c_or_signal(mut shutdown_signal: Receiver<()>) { async fn ctrl_c_or_signal(mut shutdown_signal: Receiver<()>) {
let mut term_signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) let mut term_signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
@@ -167,7 +196,6 @@ async fn ctrl_c_or_signal(mut shutdown_signal: Receiver<()>) {
info!("Shutting down webserver due to SIGTERM"); info!("Shutting down webserver due to SIGTERM");
break; break;
}, },
} }
} }
} }
@@ -378,14 +406,29 @@ fn convert_to_msg(msg: String, max_bytes: Option<usize>) -> Result<NostrMessage>
} }
/// Turn a string into a NOTICE message ready to send over a WebSocket /// Turn a string into a NOTICE message ready to send over a WebSocket
fn make_notice_message(msg: &str) -> Message { fn make_notice_message(notice: Notice) -> Message {
Message::text(json!(["NOTICE", msg]).to_string()) let json = match notice {
Notice::Message(ref msg) => json!(["NOTICE", msg]),
Notice::EventResult(ref res) => match &res.status {
EventResultStatus::Saved => json!(["OK", res.id, "true"]),
EventResultStatus::Duplicate => json!(["OK", res.id, "true", "duplicate"]),
EventResultStatus::Error(msg) => json!(["OK", res.id, "false", msg]),
},
};
Message::text(json.to_string())
}
struct ClientInfo {
remote_ip: String,
user_agent: Option<String>,
} }
/// Handle new client connections. This runs through an event loop /// Handle new client connections. This runs through an event loop
/// for all client communication. /// for all client communication.
async fn nostr_server( async fn nostr_server(
pool: db::SqlitePool, pool: db::SqlitePool,
client_info: ClientInfo,
settings: Settings, settings: Settings,
mut ws_stream: WebSocketStream<Upgraded>, mut ws_stream: WebSocketStream<Upgraded>,
broadcast: Sender<Event>, broadcast: Sender<Event>,
@@ -395,19 +438,20 @@ async fn nostr_server(
// get a broadcast channel for clients to communicate on // get a broadcast channel for clients to communicate on
let mut bcast_rx = broadcast.subscribe(); let mut bcast_rx = broadcast.subscribe();
// Track internal client state // Track internal client state
let mut conn = conn::ClientConn::new(); let mut conn = conn::ClientConn::new(client_info.remote_ip);
// Use the remote IP as the client identifier
let cid = conn.get_client_prefix(); let cid = conn.get_client_prefix();
// Create a channel for receiving query results from the database. // Create a channel for receiving query results from the database.
// we will send out the tx handle to any query we generate. // we will send out the tx handle to any query we generate.
let (query_tx, mut query_rx) = mpsc::channel::<db::QueryResult>(256); let (query_tx, mut query_rx) = mpsc::channel::<db::QueryResult>(256);
// Create channel for receiving NOTICEs // Create channel for receiving NOTICEs
let (notice_tx, mut notice_rx) = mpsc::channel::<String>(32); let (notice_tx, mut notice_rx) = mpsc::channel::<Notice>(32);
// last time this client sent data (message, ping, etc.) // last time this client sent data (message, ping, etc.)
let mut last_message_time = Instant::now(); let mut last_message_time = Instant::now();
// ping interval (every 5 minutes) // ping interval (every 5 minutes)
let default_ping_dur = Duration::from_secs(300); let default_ping_dur = Duration::from_secs(settings.network.ping_interval_seconds.into());
// disconnect after 20 minutes without a ping response or event. // disconnect after 20 minutes without a ping response or event.
let max_quiet_time = Duration::from_secs(60 * 20); let max_quiet_time = Duration::from_secs(60 * 20);
@@ -424,11 +468,14 @@ async fn nostr_server(
// and how many it received from queries. // and how many it received from queries.
let mut client_published_event_count: usize = 0; let mut client_published_event_count: usize = 0;
let mut client_received_event_count: usize = 0; let mut client_received_event_count: usize = 0;
info!("new connection for client: {:?}", cid); debug!("new connection for client: {}, ip: {:?}", cid, conn.ip());
if let Some(ua) = client_info.user_agent {
debug!("client: {} has user-agent: {:?}", cid, ua);
}
loop { loop {
tokio::select! { tokio::select! {
_ = shutdown.recv() => { _ = shutdown.recv() => {
info!("Shutting client connection down due to shutdown: {:?}", cid); info!("Close connection down due to shutdown, client: {}, ip: {:?}", cid, conn.ip());
// server shutting down, exit loop // server shutting down, exit loop
break; break;
}, },
@@ -443,7 +490,7 @@ async fn nostr_server(
ws_stream.send(Message::Ping(Vec::new())).await.ok(); ws_stream.send(Message::Ping(Vec::new())).await.ok();
}, },
Some(notice_msg) = notice_rx.recv() => { Some(notice_msg) = notice_rx.recv() => {
ws_stream.send(make_notice_message(&notice_msg)).await.ok(); ws_stream.send(make_notice_message(notice_msg)).await.ok();
}, },
Some(query_result) = query_rx.recv() => { Some(query_result) = query_rx.recv() => {
// database informed us of a query result we asked for // database informed us of a query result we asked for
@@ -462,12 +509,15 @@ async fn nostr_server(
Ok(global_event) = bcast_rx.recv() => { Ok(global_event) = bcast_rx.recv() => {
// an event has been broadcast to all clients // an event has been broadcast to all clients
// first check if there is a subscription for this event. // first check if there is a subscription for this event.
let matching_subs = conn.get_matching_subscriptions(&global_event); for (s, sub) in conn.subscriptions() {
for s in matching_subs { if !sub.interested_in_event(&global_event) {
continue;
}
// TODO: serialize at broadcast time, instead of // TODO: serialize at broadcast time, instead of
// once for each consumer. // once for each consumer.
if let Ok(event_str) = serde_json::to_string(&global_event) { if let Ok(event_str) = serde_json::to_string(&global_event) {
debug!("sub match for client: {:?}, sub: {:?}, event: {:?}", debug!("sub match for client: {}, sub: {:?}, event: {:?}",
cid, s, cid, s,
global_event.get_event_id_prefix()); global_event.get_event_id_prefix());
// create an event response and send it // create an event response and send it
@@ -488,7 +538,7 @@ async fn nostr_server(
}, },
Some(Ok(Message::Binary(_))) => { Some(Ok(Message::Binary(_))) => {
ws_stream.send( ws_stream.send(
make_notice_message("binary messages are not accepted")).await.ok(); make_notice_message(Notice::message("binary messages are not accepted".into()))).await.ok();
continue; continue;
}, },
Some(Ok(Message::Ping(_) | Message::Pong(_))) => { Some(Ok(Message::Ping(_) | Message::Pong(_))) => {
@@ -498,8 +548,7 @@ async fn nostr_server(
}, },
Some(Err(WsError::Capacity(MessageTooLong{size, max_size}))) => { Some(Err(WsError::Capacity(MessageTooLong{size, max_size}))) => {
ws_stream.send( ws_stream.send(
make_notice_message( make_notice_message(Notice::message(format!("message too large ({} > {})",size, max_size)))).await.ok();
&format!("message too large ({} > {})",size, max_size))).await.ok();
continue; continue;
}, },
None | None |
@@ -507,17 +556,17 @@ async fn nostr_server(
Err(WsError::AlreadyClosed | WsError::ConnectionClosed | Err(WsError::AlreadyClosed | WsError::ConnectionClosed |
WsError::Protocol(tungstenite::error::ProtocolError::ResetWithoutClosingHandshake))) WsError::Protocol(tungstenite::error::ProtocolError::ResetWithoutClosingHandshake)))
=> { => {
debug!("websocket close from client: {:?}",cid); debug!("websocket close from client: {}, ip: {:?}",cid, conn.ip());
break; break;
}, },
Some(Err(WsError::Io(e))) => { Some(Err(WsError::Io(e))) => {
// IO errors are considered fatal // IO errors are considered fatal
warn!("IO error (client: {:?}): {:?}", cid, e); warn!("IO error (client: {}, ip: {:?}): {:?}", cid, conn.ip(), e);
break; break;
} }
x => { x => {
// default condition on error is to close the client connection // default condition on error is to close the client connection
info!("unknown error (client: {:?}): {:?} (closing conn)", cid, x); info!("unknown error (client: {}, ip: {:?}): {:?} (closing conn)", cid, conn.ip(), x);
break; break;
} }
}; };
@@ -531,7 +580,7 @@ async fn nostr_server(
match parsed { match parsed {
Ok(e) => { Ok(e) => {
let id_prefix:String = e.id.chars().take(8).collect(); let id_prefix:String = e.id.chars().take(8).collect();
debug!("successfully parsed/validated event: {:?} from client: {:?}", id_prefix, cid); debug!("successfully parsed/validated event: {:?} from client: {}", id_prefix, cid);
// check if the event is too far in the future. // check if the event is too far in the future.
if e.is_valid_timestamp(settings.options.reject_future_seconds) { if e.is_valid_timestamp(settings.options.reject_future_seconds) {
// Write this to the database. // Write this to the database.
@@ -539,20 +588,22 @@ async fn nostr_server(
event_tx.send(submit_event).await.ok(); event_tx.send(submit_event).await.ok();
client_published_event_count += 1; client_published_event_count += 1;
} else { } else {
info!("client {:?} sent a far future-dated event", cid); info!("client: {} sent a far future-dated event", cid);
if let Some(fut_sec) = settings.options.reject_future_seconds { if let Some(fut_sec) = settings.options.reject_future_seconds {
ws_stream.send(make_notice_message(&format!("The event created_at field is out of the acceptable range (+{}sec) for this relay and was not stored.",fut_sec))).await.ok(); let msg = format!("The event created_at field is out of the acceptable range (+{}sec) for this relay and was not stored.",fut_sec);
let notice = Notice::err_msg(msg, e.id);
ws_stream.send(make_notice_message(notice)).await.ok();
} }
} }
}, },
Err(_) => { Err(_) => {
info!("client {:?} sent an invalid event", cid); info!("client: {} sent an invalid event", cid);
ws_stream.send(make_notice_message("event was invalid")).await.ok(); ws_stream.send(make_notice_message(Notice::message("event was invalid".into()))).await.ok();
} }
} }
}, },
Ok(NostrMessage::SubMsg(s)) => { Ok(NostrMessage::SubMsg(s)) => {
debug!("client {} requesting a subscription", cid); debug!("client: {} requesting a subscription", cid);
// subscription handling consists of: // subscription handling consists of:
// * registering the subscription so future events can be matched // * registering the subscription so future events can be matched
// * making a channel to cancel to request later // * making a channel to cancel to request later
@@ -569,7 +620,7 @@ async fn nostr_server(
}, },
Err(e) => { Err(e) => {
info!("Subscription error: {}", e); info!("Subscription error: {}", e);
ws_stream.send(make_notice_message(&e.to_string())).await.ok(); ws_stream.send(make_notice_message(Notice::err(e, s.id))).await.ok();
} }
} }
}, },
@@ -588,23 +639,23 @@ async fn nostr_server(
conn.unsubscribe(&c); conn.unsubscribe(&c);
} else { } else {
info!("invalid command ignored"); info!("invalid command ignored");
ws_stream.send(make_notice_message("could not parse command")).await.ok(); ws_stream.send(make_notice_message(Notice::message("could not parse command".into()))).await.ok();
} }
}, },
Err(Error::ConnError) => { Err(Error::ConnError) => {
debug!("got connection close/error, disconnecting client: {:?}",cid); debug!("got connection close/error, disconnecting client: {}, ip: {:?}",cid, conn.ip());
break; break;
} }
Err(Error::EventMaxLengthError(s)) => { Err(Error::EventMaxLengthError(s)) => {
info!("client {:?} sent event larger ({} bytes) than max size", cid, s); info!("client: {} sent event larger ({} bytes) than max size", cid, s);
ws_stream.send(make_notice_message("event exceeded max size")).await.ok(); ws_stream.send(make_notice_message(Notice::message("event exceeded max size".into()))).await.ok();
}, },
Err(Error::ProtoParseError) => { Err(Error::ProtoParseError) => {
info!("client {:?} sent event that could not be parsed", cid); info!("client {} sent event that could not be parsed", cid);
ws_stream.send(make_notice_message("could not parse command")).await.ok(); ws_stream.send(make_notice_message(Notice::message("could not parse command".into()))).await.ok();
}, },
Err(e) => { Err(e) => {
info!("got non-fatal error from client: {:?}, error: {:?}", cid, e); info!("got non-fatal error from client: {}, error: {:?}", cid, e);
}, },
} }
}, },
@@ -615,7 +666,10 @@ async fn nostr_server(
stop_tx.send(()).ok(); stop_tx.send(()).ok();
} }
info!( info!(
"stopping connection for client: {:?} (client sent {} event(s), received {})", "stopping connection for client: {}, ip: {:?} (client sent {} event(s), received {})",
cid, client_published_event_count, client_received_event_count cid,
conn.ip(),
client_published_event_count,
client_received_event_count
); );
} }
+23 -1
View File
@@ -217,6 +217,17 @@ impl ReqFilter {
.unwrap_or(true) .unwrap_or(true)
} }
fn delegated_authors_match(&self, event: &Event) -> bool {
if let Some(delegated_pubkey) = &event.delegated_by {
self.authors
.as_ref()
.map(|vs| prefix_match(vs, delegated_pubkey))
.unwrap_or(true)
} else {
false
}
}
fn tag_match(&self, event: &Event) -> bool { fn tag_match(&self, event: &Event) -> bool {
// get the hashset from the filter. // get the hashset from the filter.
if let Some(map) = &self.tags { if let Some(map) = &self.tags {
@@ -248,7 +259,7 @@ impl ReqFilter {
&& self.since.map(|t| event.created_at > t).unwrap_or(true) && self.since.map(|t| event.created_at > t).unwrap_or(true)
&& self.until.map(|t| event.created_at < t).unwrap_or(true) && self.until.map(|t| event.created_at < t).unwrap_or(true)
&& self.kind_match(event.kind) && self.kind_match(event.kind)
&& self.authors_match(event) && (self.authors_match(event) || self.delegated_authors_match(event))
&& self.tag_match(event) && self.tag_match(event)
&& !self.force_no_match && !self.force_no_match
} }
@@ -308,6 +319,7 @@ mod tests {
let e = Event { let e = Event {
id: "foo".to_owned(), id: "foo".to_owned(),
pubkey: "abcd".to_owned(), pubkey: "abcd".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -326,6 +338,7 @@ mod tests {
let e = Event { let e = Event {
id: "abcd".to_owned(), id: "abcd".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -344,6 +357,7 @@ mod tests {
let e = Event { let e = Event {
id: "abcde".to_owned(), id: "abcde".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -363,6 +377,7 @@ mod tests {
let e = Event { let e = Event {
id: "abc".to_owned(), id: "abc".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 50, created_at: 50,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -386,6 +401,7 @@ mod tests {
let e = Event { let e = Event {
id: "abc".to_owned(), id: "abc".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 150, created_at: 150,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -407,6 +423,7 @@ mod tests {
let e = Event { let e = Event {
id: "abc".to_owned(), id: "abc".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 50, created_at: 50,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -425,6 +442,7 @@ mod tests {
let e = Event { let e = Event {
id: "abc".to_owned(), id: "abc".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 1001, created_at: 1001,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -443,6 +461,7 @@ mod tests {
let e = Event { let e = Event {
id: "abc".to_owned(), id: "abc".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -461,6 +480,7 @@ mod tests {
let e = Event { let e = Event {
id: "123".to_owned(), id: "123".to_owned(),
pubkey: "abc".to_owned(), pubkey: "abc".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -479,6 +499,7 @@ mod tests {
let e = Event { let e = Event {
id: "123".to_owned(), id: "123".to_owned(),
pubkey: "bcd".to_owned(), pubkey: "bcd".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),
@@ -497,6 +518,7 @@ mod tests {
let e = Event { let e = Event {
id: "123".to_owned(), id: "123".to_owned(),
pubkey: "xyz".to_owned(), pubkey: "xyz".to_owned(),
delegated_by: None,
created_at: 0, created_at: 0,
kind: 0, kind: 0,
tags: Vec::new(), tags: Vec::new(),