mirror of
https://github.com/scsibug/nostr-rs-relay.git
synced 2025-09-01 03:40:46 -04:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9710ea27aa | ||
|
783a6e1042 | ||
|
4171a8870e | ||
|
8f3891c781 | ||
|
415d32299b | ||
|
5a19a8876f |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -573,7 +573,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "nostr-rs-relay"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"config",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nostr-rs-relay"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@@ -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}
|
||||
|
36
README.md
36
README.md
@@ -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.
|
||||
|
23
config.toml
23
config.toml
@@ -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
|
||||
|
@@ -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(),
|
||||
|
19
src/db.rs
19
src/db.rs
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
@@ -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 {
|
||||
|
27
src/main.rs
27
src/main.rs
@@ -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);
|
||||
},
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user