Compare commits

...

8 Commits
0.2.0 ... 0.2.2

Author SHA1 Message Date
Greg Heartsfield
14e59ed278 build: bump version to 0.2.2 2021-12-31 16:34:52 -06:00
Greg Heartsfield
5ad383f257 fix: incorrect logic on empty filters for hidden events 2021-12-31 16:34:10 -06:00
Greg Heartsfield
9710ea27aa build: bump version to 0.2.1 2021-12-31 15:38:58 -06:00
Greg Heartsfield
783a6e1042 docs: fix docker examples 2021-12-31 15:28:26 -06:00
Greg Heartsfield
4171a8870e 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
2021-12-31 15:19:35 -06:00
Greg Heartsfield
8f3891c781 docs: docker and config updates 2021-12-31 14:08:04 -06:00
Greg Heartsfield
415d32299b fix: docker run references the correct database file 2021-12-31 14:05:11 -06:00
Greg Heartsfield
5a19a8876f feat: allow database directory configuration
Adds configuration options for database directory, either on command
line through (--db dir-name) or the config.toml file.

Fixes: https://todo.sr.ht/~gheartsfield/nostr-rs-relay/13
2021-12-31 11:51:57 -06:00
10 changed files with 120 additions and 32 deletions

2
Cargo.lock generated
View File

@@ -573,7 +573,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]]
name = "nostr-rs-relay"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"bitcoin_hashes",
"config",

View File

@@ -1,6 +1,6 @@
[package]
name = "nostr-rs-relay"
version = "0.2.0"
version = "0.2.2"
edition = "2021"
[dependencies]

View File

@@ -15,7 +15,6 @@ RUN cargo build --release
FROM debian:buster-slim
ARG APP=/usr/src/app
ARG APP_DATA=/usr/src/app/db
RUN apt-get update \
&& apt-get install -y ca-certificates tzdata sqlite3 \
&& rm -rf /var/lib/apt/lists/*
@@ -35,9 +34,9 @@ COPY --from=builder /nostr-rs-relay/target/release/nostr-rs-relay ${APP}/nostr-r
RUN chown -R $APP_USER:$APP_USER ${APP}
USER $APP_USER
WORKDIR ${APP_DATA}
WORKDIR ${APP}
ENV RUST_LOG=info
ENV APP_DATA=${APP_DATA}
CMD ["../nostr-rs-relay"]
CMD ./nostr-rs-relay --db ${APP_DATA}

View File

@@ -10,17 +10,25 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
## Quick Start
The provided `Dockerfile` will compile and build the server application. Use a bind mount to store the SQLite database outside of the container image, and map the container's 8080 port to a host port (8090 in the example below).
The provided `Dockerfile` will compile and build the server
application. Use a bind mount to store the SQLite database outside of
the container image, and map the container's 8080 port to a host port
(7000 in the example below).
```console
$ docker build -t nostr-rs-relay .
$ docker run -p 8090:8080 --mount src=$(pwd)/nostr_data,target=/usr/src/app/db,type=bind nostr-rs-relay
[2021-12-12T04:20:47Z INFO nostr_rs_relay] Listening on: 0.0.0.0:8080
[2021-12-12T04:20:47Z INFO nostr_rs_relay::db] Opened database for writing
[2021-12-12T04:20:47Z INFO nostr_rs_relay::db] init completed
$ docker run -it -p 7000:8080 \
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind nostr-rs-relay
[2021-12-31T19:58:31Z INFO nostr_rs_relay] listening on: 0.0.0.0:8080
[2021-12-31T19:58:31Z INFO nostr_rs_relay::db] opened database "/usr/src/app/db/nostr.db" for writing
[2021-12-31T19:58:31Z INFO nostr_rs_relay::db] DB version = 2
```
Use a `nostr` client such as [`noscl`](https://github.com/fiatjaf/noscl) to publish and query events.
Use a `nostr` client such as
[`noscl`](https://github.com/fiatjaf/noscl) to publish and query
events.
```console
$ noscl publish "hello world"
@@ -31,6 +39,22 @@ Text Note [81cf...2652] from 296a...9b92 5 seconds ago
hello world
```
## Configuration
The sample `[config.toml](config.toml)` file demonstrates the
configuration available to the relay. This file is optional, but may
be mounted into a docker container like so:
```console
$ docker run -it -p 7000:8080 \
--mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \
nostr-rs-relay
```
Options include rate-limiting, event size limits, and network address
settings.
License
---
This project is MIT licensed.

View File

@@ -1,4 +1,9 @@
# Nostr-rs-relay configuration
[database]
# Directory for SQLite files. Defaults to the current directory. Can
# also be specified (and overriden) with the "--db dirname" command
# line option.
data_directory = "."
[network]
# Bind to this network address
@@ -10,23 +15,27 @@ port = 8080
# Reject events that have timestamps greater than this many seconds in
# the future. Defaults to rejecting anything greater than 30 minutes
# from the current time.
#reject_future_seconds = 1800
reject_future_seconds = 1800
[limits]
# Limit events created per second, averaged over one minute. Must be
# an integer. If not set (or set to 0), defaults to unlimited.
messages_per_sec = 0
# Maximum WebSocket message in bytes. Defaults to 128k.
#max_ws_message_bytes = 131072
# Limit the maximum size of an EVENT message. Defaults to 128 KB.
# Set to 0 for unlimited.
max_event_bytes = 131072
# Maximum WebSocket frame size in bytes. Defaults to 128k.
#max_ws_frame_bytes = 131072
# Maximum WebSocket message in bytes. Defaults to 128 KB.
max_ws_message_bytes = 131072
# Maximum WebSocket frame size in bytes. Defaults to 128 KB.
max_ws_frame_bytes = 131072
# Broadcast buffer size, in number of events. This prevents slow
# readers from consuming memory. Defaults to 4096.
#broadcast_buffer = 4096
broadcast_buffer = 4096
# Event persistence buffer size, in number of events. This provides
# backpressure to senders if writes are slow. Defaults to 16.
#event_persist_buffer = 16
event_persist_buffer = 16

View File

@@ -8,6 +8,12 @@ lazy_static! {
pub static ref SETTINGS: RwLock<Settings> = RwLock::new(Settings::default());
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Database {
pub data_directory: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Network {
@@ -36,7 +42,7 @@ pub struct Retention {
#[allow(unused)]
pub struct Limits {
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_frame_bytes: Option<usize>,
pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory)
@@ -46,6 +52,7 @@ pub struct Limits {
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Settings {
pub database: Database,
pub network: Network,
pub limits: Limits,
pub retention: Retention,
@@ -82,6 +89,9 @@ impl Settings {
impl Default for Settings {
fn default() -> Self {
Settings {
database: Database {
data_directory: ".".to_owned(),
},
network: Network {
port: 8080,
address: "0.0.0.0".to_owned(),

View File

@@ -122,14 +122,18 @@ pub async fn db_writer(
mut shutdown: tokio::sync::broadcast::Receiver<()>,
) -> tokio::task::JoinHandle<Result<()>> {
task::spawn_blocking(move || {
// get database configuration settings
let config = SETTINGS.read().unwrap();
let db_dir = &config.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
// create a connection
let mut conn = Connection::open_with_flags(
Path::new(DB_FILE),
&full_path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
)?;
info!("opened database for writing");
info!("opened database {:?} for writing", full_path);
upgrade_db(&mut conn)?;
// get rate limit settings
let config = SETTINGS.read().unwrap();
let rps_setting = config.limits.messages_per_sec;
let mut lim_opt = None;
let clock = governor::clock::QuantaClock::default();
@@ -141,7 +145,7 @@ pub async fn db_writer(
}
}
loop {
if let Ok(_) = shutdown.try_recv() {
if shutdown.try_recv().is_ok() {
info!("shutting down database writer");
break;
}
@@ -346,7 +350,7 @@ fn query_from_sub(sub: &Subscription) -> String {
filter_clauses.push(fc);
} else {
// never display hidden events
filter_clauses.push("hidden!=FALSE".to_owned());
filter_clauses.push("hidden!=TRUE".to_owned());
}
}
@@ -373,9 +377,12 @@ pub async fn db_query(
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
) {
task::spawn_blocking(move || {
let config = SETTINGS.read().unwrap();
let db_dir = &config.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
let conn =
Connection::open_with_flags(Path::new(DB_FILE), OpenFlags::SQLITE_OPEN_READ_ONLY)
.unwrap();
Connection::open_with_flags(&full_path, OpenFlags::SQLITE_OPEN_READ_ONLY).unwrap();
debug!("opened database for reading");
debug!("going to query for: {:?}", sub);
// generate SQL query

View File

@@ -21,6 +21,8 @@ pub enum Error {
CloseParseFailed,
#[error("Event validation failed")]
EventInvalid,
#[error("Event too large")]
EventMaxLengthError(usize),
#[error("Subscription identifier max length exceeded")]
SubIdMaxLengthError,
#[error("Maximum concurrent subscription count reached")]
@@ -36,6 +38,8 @@ pub enum Error {
SqlError(rusqlite::Error),
#[error("Config error")]
ConfigError(config::ConfigError),
#[error("Data directory does not exist")]
DatabaseDirError,
}
impl From<rusqlite::Error> for Error {

View File

@@ -12,6 +12,8 @@ use nostr_rs_relay::protostream;
use nostr_rs_relay::protostream::NostrMessage::*;
use nostr_rs_relay::protostream::NostrResponse::*;
use std::collections::HashMap;
use std::env;
use std::path::Path;
use tokio::net::{TcpListener, TcpStream};
use tokio::runtime::Builder;
use tokio::sync::broadcast;
@@ -20,17 +22,36 @@ use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tungstenite::protocol::WebSocketConfig;
fn db_from_args(args: Vec<String>) -> Option<String> {
if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
return args.get(2).map(|x| x.to_owned());
}
None
}
/// Start running a Nostr relay server.
fn main() -> Result<(), Error> {
// setup logger
let _ = env_logger::try_init();
// get database directory from args
let args: Vec<String> = env::args().collect();
let db_dir: Option<String> = db_from_args(args);
{
let mut settings = config::SETTINGS.write().unwrap();
// replace default settings with those read from config.toml
let c = config::Settings::new();
let mut c = config::Settings::new();
// update with database location
if let Some(db) = db_dir {
c.database.data_directory = db;
}
*settings = c;
}
let config = config::SETTINGS.read().unwrap();
// do some config validation.
if !Path::new(&config.database.data_directory).is_dir() {
error!("Database directory does not exist");
return Err(Error::DatabaseDirError);
}
debug!("config: {:?}", config);
let addr = format!("{}:{}", config.network.address.trim(), config.network.port);
// configure tokio runtime
@@ -230,6 +251,10 @@ async fn nostr_server(
debug!("got connection close/error, disconnecting client: {}",cid);
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)) => {
info!("got non-fatal error from client: {}, error: {:?}", cid, e);
},

View File

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