1use crate::Error;
2use crate::{duration::NonZeroDuration, hash::NoOpHasherDefault, Result};
3use config::{Config, Environment, File, FileFormat};
4use notify::{event::ModifyKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
5use parking_lot::RwLock;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::{
10 any::{Any, TypeId},
11 collections::HashMap,
12 fs,
13 ops::Deref,
14 path::{Path, PathBuf},
15 sync::Arc,
16 time::Duration,
17};
18use tracing::{error, info};
19
20pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
21
22fn default_version() -> String {
23 CARGO_PKG_VERSION.map(ToOwned::to_owned).unwrap_or_default()
24}
25
26fn default_nips() -> Vec<u32> {
27 vec![
28 0, 1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 25, 26, 28, 33, 40, 70, 30618,
29 30617, 1633, 1632, 1631, 1630, 1621, 1617,
30 ]
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
34#[serde(default)]
35pub struct Information {
36 pub name: String,
37 pub description: String,
38 pub pubkey: Option<String>,
39 pub contact: Option<String>,
40 pub software: String,
41 #[serde(skip_deserializing)]
42 pub version: String,
43 #[serde(skip_deserializing)]
44 pub supported_nips: Vec<u32>,
45}
46
47impl Default for Information {
48 fn default() -> Self {
49 Self {
50 name: Default::default(),
51 description: Default::default(),
52 pubkey: Default::default(),
53 contact: Default::default(),
54 software: Default::default(),
55 version: default_version(),
56 supported_nips: default_nips(),
57 }
58 }
59}
60
61#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
62#[serde(default)]
63pub struct Data {
64 pub path: PathBuf,
65
66 pub db_query_timeout: Option<NonZeroDuration>,
68}
69
70impl Default for Data {
71 fn default() -> Self {
72 Self {
73 path: PathBuf::from("./data"),
74 db_query_timeout: None,
75 }
76 }
77}
78
79#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
81#[serde(default)]
82pub struct Thread {
83 pub http: usize,
85 pub reader: usize,
87}
88
89#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
91#[serde(default)]
92pub struct Network {
93 pub host: String,
95 pub port: u16,
97 pub heartbeat_timeout: NonZeroDuration,
100
101 pub heartbeat_interval: NonZeroDuration,
104
105 pub real_ip_header: Option<String>,
106
107 pub index_redirect_to: Option<String>,
109}
110
111impl Default for Network {
112 fn default() -> Self {
113 Self {
114 host: "127.0.0.1".to_string(),
115 port: 8080,
116 heartbeat_interval: Duration::from_secs(60).try_into().unwrap(),
117 heartbeat_timeout: Duration::from_secs(120).try_into().unwrap(),
118 real_ip_header: None,
119 index_redirect_to: None,
120 }
121 }
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
125#[serde(default)]
126pub struct Limitation {
127 pub max_message_length: usize,
129 pub max_subscriptions: usize,
131 pub max_filters: usize,
133 pub max_limit: u64,
135 pub max_subid_length: usize,
137 pub min_prefix: usize,
139 pub max_event_tags: usize,
141 pub max_event_time_older_than_now: u64,
143 pub max_event_time_newer_than_now: u64,
145}
146
147impl Default for Limitation {
148 fn default() -> Self {
149 Self {
150 max_message_length: 524288,
151 max_subscriptions: 20,
152 max_filters: 10,
153 max_limit: 300,
154 max_subid_length: 100,
155 min_prefix: 10,
156 max_event_tags: 5000,
157 max_event_time_older_than_now: 94608000,
158 max_event_time_newer_than_now: 900,
159 }
160 }
161}
162
163#[derive(Debug, Serialize, Deserialize, Default)]
164#[serde(default)]
165pub struct Setting {
166 pub information: Information,
167 pub data: Data,
168 pub thread: Thread,
169 pub network: Network,
170 pub limitation: Limitation,
171
172 #[serde(flatten)]
174 pub extra: HashMap<String, Value>,
175
176 #[serde(skip)]
178 extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>, NoOpHasherDefault>,
179
180 #[serde(skip)]
182 ext_information: HashMap<String, Value>,
183
184 #[serde(skip)]
186 ext_limitation: HashMap<String, Value>,
187}
188
189impl PartialEq for Setting {
190 fn eq(&self, other: &Self) -> bool {
191 self.information == other.information
192 && self.data == other.data
193 && self.thread == other.thread
194 && self.network == other.network
195 && self.limitation == other.limitation
196 && self.extra == other.extra
197 }
198}
199
200#[derive(Debug, Clone)]
201pub struct SettingWrapper {
202 inner: Arc<RwLock<Setting>>,
203 watcher: Option<Arc<RecommendedWatcher>>,
204}
205
206impl Deref for SettingWrapper {
207 type Target = Arc<RwLock<Setting>>;
208 fn deref(&self) -> &Self::Target {
209 &self.inner
210 }
211}
212
213impl From<Setting> for SettingWrapper {
214 fn from(setting: Setting) -> Self {
215 Self {
216 inner: Arc::new(RwLock::new(setting)),
217 watcher: None,
218 }
219 }
220}
221
222impl SettingWrapper {
223 pub fn reload<P: AsRef<Path>>(&self, file: P, env_prefix: Option<String>) -> Result<()> {
225 let setting = Setting::read(&file, env_prefix)?;
226 {
227 let mut w = self.write();
228 *w = setting;
229 }
230 Ok(())
231 }
232
233 pub fn watch<P: AsRef<Path>, F: Fn(&SettingWrapper) + Send + 'static>(
235 file: P,
236 env_prefix: Option<String>,
237 f: F,
238 ) -> Result<Self> {
239 let mut setting: SettingWrapper = Setting::read(&file, env_prefix.clone())?.into();
240 let c_setting = setting.clone();
241
242 let file = fs::canonicalize(file.as_ref())?;
245 let c_file = file.clone();
246
247 let dir = file
252 .parent()
253 .ok_or_else(|| Error::Message("failed to get config dir".to_owned()))?;
254
255 let mut watcher = RecommendedWatcher::new(
256 move |result: Result<Event, notify::Error>| match result {
257 Ok(event) => {
258 #[cfg(target_os = "windows")]
259 let is_modify = matches!(event.kind, EventKind::Modify(ModifyKind::Any));
261 #[cfg(not(target_os = "windows"))]
262 let is_modify = matches!(event.kind, EventKind::Modify(ModifyKind::Data(_)));
263 if is_modify && event.paths.contains(&c_file) {
264 match c_setting.reload(&c_file, env_prefix.clone()) {
265 Ok(_) => {
266 info!("Reload config success {:?}", c_file);
267 info!("{:?}", c_setting.read());
268 f(&c_setting);
269 }
270 Err(e) => {
271 error!(
272 error = e.to_string(),
273 "failed to reload config {:?}", c_file
274 );
275 }
276 }
277 }
278 }
279 Err(e) => {
280 error!(error = e.to_string(), "failed to watch file {:?}", c_file);
281 }
282 },
283 notify::Config::default(),
284 )?;
285
286 watcher.watch(dir, RecursiveMode::NonRecursive)?;
287 setting.watcher = Some(Arc::new(watcher));
289
290 Ok(setting)
291 }
292}
293
294impl Setting {
295 pub fn add_nip(&mut self, nip: u32) {
297 if !self.information.supported_nips.contains(&nip) {
298 self.information.supported_nips.push(nip);
299 self.information.supported_nips.sort();
300 }
301 }
302
303 pub fn add_information(&mut self, key: String, value: Value) {
305 self.ext_information.insert(key, value);
306 }
307
308 pub fn add_limitation(&mut self, key: String, value: Value) {
310 self.ext_limitation.insert(key, value);
311 }
312
313 pub fn parse_extension<T: DeserializeOwned + Default>(&self, key: &str) -> T {
315 self.extra
316 .get(key)
317 .and_then(|v| {
318 let r = serde_json::from_value::<T>(v.clone());
319 if let Err(err) = &r {
320 error!(error = err.to_string(), "failed to parse {:?} setting", key);
321 }
322 r.ok()
323 })
324 .unwrap_or_default()
325 }
326
327 pub fn set_extension<T: Send + Sync + 'static>(&mut self, val: T) {
329 self.extensions.insert(TypeId::of::<T>(), Box::new(val));
330 }
331
332 pub fn get_extension<T: 'static>(&self) -> Option<&T> {
334 self.extensions
335 .get(&TypeId::of::<T>())
336 .and_then(|boxed| boxed.downcast_ref())
337 }
338
339 pub fn render_information(&self) -> Result<String> {
341 let info = &self.information;
342 let mut val = json!({
343 "name": info.name,
344 "description": info.description,
345 "pubkey": info.pubkey,
346 "contact": info.contact,
347 "software": info.software,
348 "version": info.version,
349 "supported_nips": info.supported_nips,
350 "limitation": &self.limitation,
351 });
352 self.ext_limitation.iter().for_each(|(k, v)| {
353 val["limitation"][k] = v.clone();
354 });
355 self.ext_information.iter().for_each(|(k, v)| {
356 val[k] = v.clone();
357 });
358 Ok(serde_json::to_string_pretty(&val)?)
359 }
360
361 pub fn read<P: AsRef<Path>>(file: P, env_prefix: Option<String>) -> Result<Self> {
363 let builder = Config::builder();
364 let mut config = builder
365 .add_source(File::with_name(file.as_ref().to_str().unwrap()));
370 if let Some(prefix) = env_prefix {
371 config = config.add_source(Self::env_source(&prefix));
372 }
373
374 let config = config.build()?;
375 let mut setting: Setting = config.try_deserialize()?;
376 setting.correct();
377 Ok(setting)
378 }
379
380 fn env_source(prefix: &str) -> Environment {
381 Environment::with_prefix(prefix)
382 .try_parsing(true)
383 .prefix_separator("_")
384 .separator("__")
385 }
388
389 pub fn from_env(env_prefix: String) -> Result<Self> {
391 let mut config = Config::builder();
392 config = config.add_source(Self::env_source(&env_prefix));
393 let config = config.build()?;
394 let mut setting: Setting = config.try_deserialize()?;
395 setting.correct();
396 Ok(setting)
397 }
398
399 pub fn from_str(s: &str, format: FileFormat) -> Result<Self> {
401 let builder = Config::builder();
402 let config = builder.add_source(File::from_str(s, format)).build()?;
403 let mut setting: Setting = config.try_deserialize()?;
404 setting.correct();
405 Ok(setting)
406 }
407
408 fn correct(&mut self) {
409 if self.network.heartbeat_timeout <= self.network.heartbeat_interval {
410 error!("network heartbeat_timeout must bigger than heartbeat_interval, use defaults");
411 self.network.heartbeat_interval = Duration::from_secs(60).try_into().unwrap();
412 self.network.heartbeat_timeout = Duration::from_secs(120).try_into().unwrap();
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use anyhow::Result;
421 use config::FileFormat;
422 use std::{fs, thread::sleep, time::Duration};
423 use tempfile::Builder;
424
425 #[test]
426 fn der() -> Result<()> {
427 let json = r#"{
428 "network": {"port": 1},
429 "information": {"name": "test"},
430 "data": {},
431 "thread": {"http": 1},
432 "limitation": {}
433 }"#;
434
435 let mut def = Setting::default();
436 def.network.port = 1;
437 def.information.name = "test".to_owned();
438 def.thread.http = 1;
439
440 let s2 = serde_json::from_str::<Setting>(json)?;
441 let s1: Setting = Setting::from_str(json, FileFormat::Json)?;
442
443 assert_eq!(def, s1);
444 assert_eq!(def, s2);
445
446 Ok(())
447 }
448
449 #[test]
450 fn render() -> Result<()> {
451 let mut def = Setting::default();
452 def.add_nip(1234567);
453 def.add_limitation("payment_required".to_owned(), json!(true));
454 def.add_information("payments_url".to_owned(), json!("https://payments"));
455 let info = def.render_information()?;
456 let val: Value = serde_json::from_str(&info)?;
457 assert!(val["supported_nips"]
459 .as_array()
460 .unwrap()
461 .contains(&Value::Number(serde_json::Number::from(1234567))));
462 assert_eq!(val["payments_url"], json!("https://payments"));
463 assert_eq!(val["limitation"]["payment_required"], json!(true));
464 Ok(())
465 }
466
467 #[test]
468 fn read() -> Result<()> {
469 let setting = Setting::default();
470 assert_eq!(setting.information.name, "");
471 assert!(setting.information.supported_nips.contains(&1));
472
473 let file = Builder::new()
474 .prefix("nostr-relay-config-test-read")
475 .suffix(".toml")
476 .rand_bytes(0)
477 .tempfile()?;
478
479 let setting = Setting::read(&file, None)?;
480 assert_eq!(setting.information.name, "");
481 assert!(setting.information.supported_nips.contains(&1));
482 fs::write(&file, CONFIG)?;
483
484 temp_env::with_vars(
485 [
486 ("NOSTR_information.description", Some("test")),
487 ("NOSTR_information__contact", Some("test")),
488 ("NOSTR_INFORMATION__PUBKEY", Some("test")),
489 ("NOSTR_NETWORK__PORT", Some("1")),
490 ],
491 || {
492 let setting = Setting::read(&file, Some("NOSTR".to_owned())).unwrap();
493 assert_eq!(setting.information.name, "gnostr-relay".to_string());
494 assert_eq!(setting.information.description, "test".to_string());
495 assert_eq!(setting.information.contact, Some("test".to_string()));
496 assert_eq!(setting.information.pubkey, Some("test".to_string()));
497 assert_eq!(setting.network.port, 1);
498 },
499 );
500 Ok(())
501 }
502
503 #[test]
504 fn from_config_str() -> Result<()> {
505 let setting = Setting::from_str(CONFIG, FileFormat::Toml)?;
506 assert_eq!(setting.information.name, "gnostr-relay".to_string());
507 assert_eq!(setting.network.port, 8080);
508 Ok(())
509 }
510
511 #[test]
512 fn watch() -> Result<()> {
513 let file = Builder::new()
514 .prefix("nostr-relay-config-test-watch")
515 .suffix(".toml")
516 .tempfile()?;
517
518 let setting = SettingWrapper::watch(&file, None, |_s| {})?;
519 {
520 let r = setting.read();
521 assert_eq!(r.information.name, "");
522 assert!(r.information.supported_nips.contains(&1));
523 }
524
525 fs::write(
526 &file,
527 r#"[information]
528 name = "nostr"
529 "#,
530 )?;
531 sleep(Duration::from_secs(1));
532 {
534 let r = setting.read();
535 assert_eq!(r.information.name, "nostr");
536 assert!(r.information.supported_nips.contains(&1));
537 }
538 Ok(())
539 }
540
541 #[test]
542 fn reload() -> Result<()> {
543 let file = Builder::new()
544 .prefix("nostr-relay-config-test-reload")
545 .suffix(".toml")
546 .tempfile()?;
547
548 fs::write(
549 &file,
550 r#"[information]
551 name = "initial"
552 "#,
553 )?;
554
555 let setting_wrapper: SettingWrapper = Setting::read(&file, None)?.into();
556 {
557 let r = setting_wrapper.read();
558 assert_eq!(r.information.name, "initial");
559 }
560
561 fs::write(
562 &file,
563 r#"[information]
564 name = "reloaded"
565 "#,
566 )?;
567
568 setting_wrapper.reload(&file, None)?;
569 {
570 let r = setting_wrapper.read();
571 assert_eq!(r.information.name, "reloaded");
572 }
573 Ok(())
574 }
575
576 #[test]
577 fn correct_heartbeat_duration() -> Result<()> {
578 let json_valid = r#"{
579 "network": {
580 "heartbeat_interval": "60s",
581 "heartbeat_timeout": "120s"
582 }
583 }"#;
584 let setting_valid = Setting::from_str(json_valid, FileFormat::Json)?;
585 assert_eq!(
586 setting_valid.network.heartbeat_interval,
587 Duration::from_secs(60).try_into().unwrap()
588 );
589 assert_eq!(
590 setting_valid.network.heartbeat_timeout,
591 Duration::from_secs(120).try_into().unwrap()
592 );
593
594 let json_invalid = r#"{
595 "network": {
596 "heartbeat_interval": "120s",
597 "heartbeat_timeout": "60s"
598 }
599 }"#;
600 let setting_invalid = Setting::from_str(json_invalid, FileFormat::Json)?;
601 assert_eq!(
603 setting_invalid.network.heartbeat_interval,
604 Duration::from_secs(60).try_into().unwrap()
605 );
606 assert_eq!(
607 setting_invalid.network.heartbeat_timeout,
608 Duration::from_secs(120).try_into().unwrap()
609 );
610 Ok(())
611 }
612
613 #[test]
614 fn from_env() -> Result<()> {
615 temp_env::with_vars(
616 [
617 ("NOSTR_INFORMATION__NAME", Some("env_name")),
618 ("NOSTR_NETWORK__PORT", Some("9000")),
619 ],
620 || {
621 let setting = Setting::from_env("NOSTR".to_owned()).unwrap();
622 assert_eq!(setting.information.name, "env_name".to_string());
623 assert_eq!(setting.network.port, 9000);
624 },
625 );
626 Ok(())
627 }
628
629 const CONFIG: &str = r#"
630# Configuration
631# All duration format reference https://docs.rs/duration-str/latest/duration_str/
632#
633# config relay information
634[information]
635name = "gnostr-relay"
636description = "GnostrApp:a git+nostr workflow utility"
637software = "https://github.com/gnostr-org/gnostr"
638# pubkey = ""
639# contact = ""
640
641# config data path
642[data]
643# the data path (restart required)
644# the events db path is $path/events
645path = "./data"
646
647# Query filter timeout time, default no timeout.
648db_query_timeout = "100ms"
649
650# config network
651[network]
652# Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)
653host = "127.0.0.1"
654# Listen port (restart required)
655port = 8080
656
657# real ip header (default empty)
658# ie: cf-connecting-ip, x-real-ip, x-forwarded-for
659# real_ip_header = "x-forwarded-for"
660
661# redirect to other site when user access the http index page
662index_redirect_to = "https://gnostr.org"
663
664# heartbeat timeout (default 120 seconds, must bigger than heartbeat interval)
665# How long before lack of client response causes a timeout
666# heartbeat_timeout = "2m"
667
668# heartbeat interval (default 60 seconds)
669# How often heartbeat pings are sent
670heartbeat_interval = "1m"
671
672# config thread (restart required)
673[thread]
674# number of http server threads (restart required)
675# default 0 will use the num of cpus
676http = 0
677
678# number of read event threads (restart required)
679# default 0 will use the num of cpus
680reader = 0
681
682[limitation]
683# this is the maximum number of bytes for incoming JSON. default 512K
684max_message_length = 524288
685# total number of subscriptions that may be active on a single websocket connection to this relay. default 20
686max_subscriptions = 20
687# maximum number of filter values in each subscription. default 10
688max_filters = 10
689# the relay server will clamp each filter's limit value to this number. This means the client won't be able to get more than this number of events from a single subscription filter. default 300
690max_limit = 300
691# maximum length of subscription id as a string. default 100
692max_subid_length = 100
693# for authors and ids filters which are to match against a hex prefix, you must provide at least this many hex digits in the prefix. default 10
694min_prefix = 10
695# in any event, this is the maximum number of elements in the tags list. default 5000
696max_event_tags = 5000
697# Events older than this will be rejected. default 3 years
698## max_event_time_older_than_now = 94608000
699# Events newer than this will be rejected. default 15 minutes
700## max_event_time_newer_than_now = 900
701
702# Metrics extension, get the metrics data from https://example.com/metrics?auth=auth_key
703[metrics]
704enabled = true
705# change the auth key
706auth = "auth_key"
707
708# Auth extension
709[auth]
710enabled = false
711
712# # Authenticate the command 'REQ' get event, subscribe filter
713# [auth.req]
714# # only the list IP are allowed to req
715# ip_whitelist = ["127.0.0.1"]
716# # only the list IP are denied to req
717# ip_blacklist = ["127.0.0.1"]
718# # Restrict on nip42 verified pubkey, so client needs to implement nip42 and authenticate success
719# pubkey_whitelist = ["xxxxxx"]
720# pubkey_blacklist = ["xxxx"]
721
722# # Authenticate the command 'EVENT' write event
723# [auth.event]
724# ip_whitelist = ["127.0.0.1"]
725# ip_blacklist = ["127.0.0.1"]
726# # Restrict on nip42 verified pubkey, so client needs to implement nip42 and authenticate success
727# pubkey_whitelist = ["xxxxxx"]
728# pubkey_blacklist = ["xxxx"]
729# # Restrict on event author pubkey, No need nip42 authentication
730# event_pubkey_whitelist = ["xxxxxx"]
731# event_pubkey_blacklist = ["xxxx"]
732# allow_mentioning_whitelisted_pubkeys = true
733
734# IP Rate limiter extension
735[rate_limiter]
736enabled = false
737
738# # interval at second for clearing invalid data to free up memory.
739# # 0 will be converted to default 60 seconds
740# clear_interval = "60s"
741
742# # rate limiter ruler list when write event per user client IP
743# [[rate_limiter.event]]
744# # name of rate limiter, used by metrics
745# name = "all"
746# # description will notice the user when rate limiter exceeded
747# description = "allow only ten events per minute"
748# period = "1m"
749# limit = 10
750
751# # only limit for kinds
752# # support kind list: [1, 2, 3]
753# # kind ranges included(start) to excluded(end): [[0, 10000], [30000, 40000]]
754# # mixed: [1, 2, [30000, 40000]]
755# kinds = [[0, 40000]]
756
757# # skip when ip in whitelist
758ip_whitelist = ["127.0.0.1"]
759
760# [[rate_limiter.event]]
761# name = "kind 10000"
762# description = "allow only five write events per minute when event kind between 0 to 10000"
763# period = "60s"
764# limit = 5
765# kinds = [[0, 10000]]
766
767# NIP-45 Count extension
768# use carefully. see README.md#count
769[count]
770enabled = false
771
772# NIP-50 Search extension
773# use carefully. see README.md#search
774[search]
775enabled = true
776"#;
777}