initial commit

This commit is contained in:
William Casarin 2024-07-07 22:29:29 -05:00
commit 24a1c0dfc2
15 changed files with 509 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
target/
.direnv/
.buildcmd
.build-result
shell.nix
.envrc
tags

19
COPYING Normal file
View file

@ -0,0 +1,19 @@
Copyright 2024 Damus Nostr, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

143
Cargo.lock generated Normal file
View file

@ -0,0 +1,143 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "noteguard"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"toml",
"tracing",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "noteguard"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"
tracing = "0.1.40"

2
Makefile Normal file
View file

@ -0,0 +1,2 @@
tags:
find src -name '*.rs' | xargs ctags

23
README.md Normal file
View file

@ -0,0 +1,23 @@
# noteguard
A high performance note filter plugin system for [strfry]
## Usage
Filters are registered and loaded from the [noteguard.toml](noteguard.toml) config.
You can add any new filter you want by implementing the `NoteFilter` trait and registering it with noteguard via the `register_filter` method.
The `pipeline` config specifies the order in which filters are run. When the first `reject` or `shadowReject` action is hit, then the pipeline stops and returns the rejection error.
```toml
pipeline = ["ratelimit"]
[filters.ratelimit]
notes_per_second = 1
whitelist = ["127.0.0.1"]
```
[strfry]: https://github.com/hoytech/strfry

6
noteguard.toml Normal file
View file

@ -0,0 +1,6 @@
pipeline = ["ratelimit"]
[filters.ratelimit]
notes_per_second = 1
whitelist = ["127.0.0.1"]

5
src/filters/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod rate_limit;
mod whitelist;
pub use rate_limit::RateLimit;
pub use whitelist::Whitelist;

54
src/filters/rate_limit.rs Normal file
View file

@ -0,0 +1,54 @@
use crate::{Action, InputMessage, NoteFilter, OutputMessage};
use serde::Deserialize;
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub struct RateInfo {
pub last_note: Instant,
}
#[derive(Deserialize, Default)]
pub struct RateLimit {
pub notes_per_second: u64,
pub whitelist: Option<Vec<String>>,
#[serde(skip)]
pub sources: HashMap<String, RateInfo>,
}
impl NoteFilter for RateLimit {
fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage {
if let Some(whitelist) = &self.whitelist {
if whitelist.contains(&msg.source_info) {
return OutputMessage::new(msg.event.id.clone(), Action::Accept, None);
}
}
if self.sources.contains_key(&msg.source_info) {
let now = Instant::now();
let entry = self.sources.get_mut(&msg.source_info).expect("impossiburu");
if now - entry.last_note < Duration::from_secs(self.notes_per_second) {
return OutputMessage::new(
msg.event.id.clone(),
Action::Reject,
Some("rate-limited: you are noting too fast".to_string()),
);
} else {
entry.last_note = Instant::now();
return OutputMessage::new(msg.event.id.clone(), Action::Accept, None);
}
} else {
self.sources.insert(
msg.source_info.to_owned(),
RateInfo {
last_note: Instant::now(),
},
);
return OutputMessage::new(msg.event.id.clone(), Action::Accept, None);
}
}
fn name(&self) -> &'static str {
"ratelimit"
}
}

26
src/filters/whitelist.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::{Action, InputMessage, NoteFilter, OutputMessage};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Whitelist {
pub pubkeys: Vec<String>,
pub ips: Vec<String>,
}
impl NoteFilter for Whitelist {
fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage {
if self.pubkeys.contains(&msg.event.pubkey) || self.ips.contains(&msg.source_info) {
OutputMessage::new(msg.event.id.clone(), Action::Accept, None)
} else {
OutputMessage::new(
msg.event.id.clone(),
Action::Reject,
Some("blocked: pubkey not on the whitelist".to_string()),
)
}
}
fn name(&self) -> &'static str {
"whitelist"
}
}

6
src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod filters;
mod messages;
mod note_filter;
pub use messages::{Action, InputMessage, OutputMessage};
pub use note_filter::{Note, NoteFilter};

143
src/main.rs Normal file
View file

@ -0,0 +1,143 @@
use noteguard::filters::RateLimit;
use noteguard::{Action, InputMessage, NoteFilter, OutputMessage};
use serde::de::DeserializeOwned;
use serde::Deserialize;
use std::collections::HashMap;
use std::io::{self, BufRead, Read, Write};
#[derive(Deserialize)]
struct Config {
pipeline: Vec<String>,
filters: HashMap<String, toml::Value>,
}
type ConstructFilter = Box<fn(toml::Value) -> Result<Box<dyn NoteFilter>, toml::de::Error>>;
#[derive(Default)]
struct Noteguard {
registered_filters: HashMap<String, ConstructFilter>,
loaded_filters: Vec<Box<dyn NoteFilter>>,
}
impl Noteguard {
pub fn new() -> Self {
let mut noteguard = Noteguard::default();
noteguard.register_builtin_filters();
noteguard
}
pub fn register_filter<F: NoteFilter + 'static + Default + DeserializeOwned>(&mut self) {
self.registered_filters.insert(
F::name(&F::default()).to_string(),
Box::new(|filter_config| {
filter_config
.try_into()
.map(|filter: F| Box::new(filter) as Box<dyn NoteFilter>)
}),
);
}
/// All builtin filters are registered here, and are made available with
/// every new instance of [`Noteguard`]
fn register_builtin_filters(&mut self) {
self.register_filter::<RateLimit>();
}
/// Run the loaded filters. You must call `load_config` before calling this, otherwise
/// not filters will be run.
fn run(&mut self, input: InputMessage) -> OutputMessage {
let mut mout: Option<OutputMessage> = None;
let id = input.event.id.clone();
for filter in &mut self.loaded_filters {
let out = filter.filter_note(&input);
match out.action {
Action::Accept => {
mout = Some(out);
continue;
}
Action::Reject => {
return out;
}
Action::ShadowReject => {
return out;
}
}
}
mout.unwrap_or_else(|| OutputMessage::new(id, Action::Accept, None))
}
/// Initializes a noteguard config. If it finds any filter configurations
/// matching the registered filters, it loads those into our filter pipeline.
fn load_config(&mut self, config: &Config) -> Result<(), toml::de::Error> {
self.loaded_filters.clear();
for (name, config_value) in &config.filters {
if let Some(constructor) = self.registered_filters.get(name.as_str()) {
let filter = constructor(config_value.clone())?;
self.loaded_filters.push(filter);
} else {
panic!("Found config settings with no matching filter: {}", name);
}
}
Ok(())
}
}
fn main() {
let config_path = "noteguard.toml";
let mut noteguard = Noteguard::new();
let config: Config = {
let mut file = std::fs::File::open(config_path).expect("Failed to open config file");
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Failed to read config file");
toml::from_str(&contents).expect("Failed to parse config file")
};
noteguard
.load_config(&config)
.expect("Expected filter config to be loaded ok");
let stdin = io::stdin();
let stdout = io::stdout();
let handle = stdout.lock();
let mut writer = io::BufWriter::new(handle);
for line in stdin.lock().lines() {
match line {
Ok(input) => {
let input_message: InputMessage = match serde_json::from_str(&input) {
Ok(msg) => msg,
Err(e) => {
eprintln!("Failed to parse input: {}", e);
continue;
}
};
if input_message.message_type != "new" {
eprintln!("Unexpected request type");
continue;
}
let output_message = noteguard.run(input_message);
match serde_json::to_string(&output_message) {
Ok(json) => {
writeln!(writer, "{}", json).unwrap();
writer.flush().unwrap();
}
Err(e) => {
eprintln!("Failed to serialize output: {}", e);
}
}
}
Err(e) => {
eprintln!("Failed to read line: {}", e);
}
}
}
}

37
src/messages.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::Note;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct InputMessage {
#[serde(rename = "type")]
pub message_type: String,
pub event: Note,
#[serde(rename = "receivedAt")]
pub received_at: u64,
#[serde(rename = "sourceType")]
pub source_type: String,
#[serde(rename = "sourceInfo")]
pub source_info: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Action {
Accept,
Reject,
ShadowReject,
}
#[derive(Serialize)]
pub struct OutputMessage {
pub id: String,
pub action: Action,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg: Option<String>,
}
impl OutputMessage {
pub fn new(id: String, action: Action, msg: Option<String>) -> Self {
OutputMessage { id, action, msg }
}
}

20
src/note_filter.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::{InputMessage, OutputMessage};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Note {
pub id: String,
pub pubkey: String,
pub content: String,
pub created_at: i64,
pub kind: i64,
pub tags: Vec<Vec<String>>,
pub sig: String,
}
pub trait NoteFilter {
fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage;
/// A key corresponding to an entry in the noteguard.toml file.
fn name(&self) -> &'static str;
}

5
test/test-inputs Normal file
View file

@ -0,0 +1,5 @@
{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.1","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}
{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.1","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}
{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.1","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}
{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}
{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}