gnostr_relay/
setting.rs

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