feat: reject events that are too large

A new configuration setting controls the maximum size of event
messages, and sends a notice to the client if they exceed it.

Fixes https://todo.sr.ht/~gheartsfield/nostr-rs-relay/14
This commit is contained in:
Greg Heartsfield 2021-12-31 15:19:35 -06:00
parent 8f3891c781
commit 4171a8870e
6 changed files with 33 additions and 15 deletions

View File

@ -3,7 +3,7 @@
# Directory for SQLite files. Defaults to the current directory. Can # Directory for SQLite files. Defaults to the current directory. Can
# also be specified (and overriden) with the "--db dirname" command # also be specified (and overriden) with the "--db dirname" command
# line option. # line option.
data_directory = "data" data_directory = "."
[network] [network]
# Bind to this network address # Bind to this network address
@ -15,17 +15,21 @@ port = 8080
# Reject events that have timestamps greater than this many seconds in # Reject events that have timestamps greater than this many seconds in
# the future. Defaults to rejecting anything greater than 30 minutes # the future. Defaults to rejecting anything greater than 30 minutes
# from the current time. # from the current time.
#reject_future_seconds = 1800 reject_future_seconds = 1800
[limits] [limits]
# Limit events created per second, averaged over one minute. Must be # Limit events created per second, averaged over one minute. Must be
# an integer. If not set (or set to 0), defaults to unlimited. # an integer. If not set (or set to 0), defaults to unlimited.
messages_per_sec = 0 messages_per_sec = 0
# Maximum WebSocket message in bytes. Defaults to 128k. # Limit the maximum size of an EVENT message. Defaults to 128 KB.
# Set to 0 for unlimited.
max_event_bytes = 131072
# Maximum WebSocket message in bytes. Defaults to 128 KB.
max_ws_message_bytes = 131072 max_ws_message_bytes = 131072
# Maximum WebSocket frame size in bytes. Defaults to 128k. # Maximum WebSocket frame size in bytes. Defaults to 128 KB.
max_ws_frame_bytes = 131072 max_ws_frame_bytes = 131072
# Broadcast buffer size, in number of events. This prevents slow # Broadcast buffer size, in number of events. This prevents slow

View File

@ -42,7 +42,7 @@ pub struct Retention {
#[allow(unused)] #[allow(unused)]
pub struct Limits { pub struct Limits {
pub messages_per_sec: Option<u32>, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute) pub messages_per_sec: Option<u32>, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute)
pub max_event_bytes: Option<usize>, pub max_event_bytes: Option<usize>, // Maximum size of an EVENT message
pub max_ws_message_bytes: Option<usize>, pub max_ws_message_bytes: Option<usize>,
pub max_ws_frame_bytes: Option<usize>, pub max_ws_frame_bytes: Option<usize>,
pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory) pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory)

View File

@ -145,7 +145,7 @@ pub async fn db_writer(
} }
} }
loop { loop {
if let Ok(_) = shutdown.try_recv() { if shutdown.try_recv().is_ok() {
info!("shutting down database writer"); info!("shutting down database writer");
break; break;
} }

View File

@ -21,6 +21,8 @@ pub enum Error {
CloseParseFailed, CloseParseFailed,
#[error("Event validation failed")] #[error("Event validation failed")]
EventInvalid, EventInvalid,
#[error("Event too large")]
EventMaxLengthError(usize),
#[error("Subscription identifier max length exceeded")] #[error("Subscription identifier max length exceeded")]
SubIdMaxLengthError, SubIdMaxLengthError,
#[error("Maximum concurrent subscription count reached")] #[error("Maximum concurrent subscription count reached")]

View File

@ -23,11 +23,9 @@ use tokio::sync::oneshot;
use tungstenite::protocol::WebSocketConfig; use tungstenite::protocol::WebSocketConfig;
fn db_from_args(args: Vec<String>) -> Option<String> { fn db_from_args(args: Vec<String>) -> Option<String> {
if args.len() == 3 { if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
if args.get(1) == Some(&"--db".to_owned()) {
return args.get(2).map(|x| x.to_owned()); return args.get(2).map(|x| x.to_owned());
} }
}
None None
} }
@ -44,7 +42,7 @@ fn main() -> Result<(), Error> {
let mut c = config::Settings::new(); let mut c = config::Settings::new();
// update with database location // update with database location
if let Some(db) = db_dir { if let Some(db) = db_dir {
c.database.data_directory = db.to_owned(); c.database.data_directory = db;
} }
*settings = c; *settings = c;
} }
@ -253,6 +251,10 @@ async fn nostr_server(
debug!("got connection close/error, disconnecting client: {}",cid); debug!("got connection close/error, disconnecting client: {}",cid);
break; break;
} }
Some(Err(Error::EventMaxLengthError(s))) => {
info!("client {} sent event larger ({} bytes) than max size", cid, s);
nostr_stream.send(NoticeRes("event exceeded max size".to_owned())).await.ok();
},
Some(Err(e)) => { Some(Err(e)) => {
info!("got non-fatal error from client: {}, error: {:?}", cid, e); info!("got non-fatal error from client: {}, error: {:?}", cid, e);
}, },

View File

@ -1,5 +1,6 @@
//! Nostr protocol layered over WebSocket //! Nostr protocol layered over WebSocket
use crate::close::CloseCmd; use crate::close::CloseCmd;
use crate::config;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::event::EventCmd; use crate::event::EventCmd;
use crate::subscription::Subscription; use crate::subscription::Subscription;
@ -51,14 +52,23 @@ pub fn wrap_ws_in_nostr(ws: WebSocketStream<TcpStream>) -> NostrStream {
impl Stream for NostrStream { impl Stream for NostrStream {
type Item = Result<NostrMessage>; type Item = Result<NostrMessage>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// get the configuration
/// Convert Message to NostrMessage /// Convert Message to NostrMessage
fn convert(msg: String) -> Result<NostrMessage> { fn convert(msg: String) -> Result<NostrMessage> {
debug!("raw msg: {}", msg); let config = config::SETTINGS.read().unwrap();
let event_size = msg.len();
debug!("event size is {} bytes", event_size);
let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into()); let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into());
match parsed_res { match parsed_res {
Ok(m) => Ok(m), Ok(m) => {
if let NostrMessage::EventMsg(_) = m {
if let Some(max_size) = config.limits.max_event_bytes {
// check length, ensure that some max size is set.
if msg.len() > max_size && max_size > 0 {
return Err(Error::EventMaxLengthError(msg.len()));
}
}
}
Ok(m)
}
Err(e) => { Err(e) => {
debug!("proto parse error: {:?}", e); debug!("proto parse error: {:?}", e);
Err(Error::ProtoParseError) Err(Error::ProtoParseError)