atagen

vineyard

public
Star0
Watch0
ab6cd8e63410ba088ca5b4d4edb18049022ed8eb
1
ssh://brewery@pls.lobotomise.me:33/atagen/vineyard.git
src/config.rsRust315 lines10.1 KB
1
use std::collections::HashSet;
2
use std::fs::read_to_string;
3
use std::net::SocketAddr;
4
use std::path::{Path, PathBuf};
5
6
use nostr::key::PublicKey;
7
use serde::Deserialize;
8
use tokio::sync::Semaphore;
9
10
fn default_listen() -> SocketAddr {
11
    SocketAddr::from(([127, 0, 0, 1], 7777))
12
}
13
14
fn default_database() -> PathBuf {
15
    PathBuf::from("vineyard.sqlite")
16
}
17
18
const fn default_max_connections() -> usize {
19
    32
20
}
21
22
const fn default_max_connections_per_ip() -> usize {
23
    8
24
}
25
26
const fn default_max_subscriptions() -> usize {
27
    16
28
}
29
30
const fn default_max_event_bytes() -> usize {
31
    2_096_896
32
}
33
34
const fn default_max_message_bytes() -> usize {
35
    2_097_152
36
}
37
38
const fn default_max_tags() -> usize {
39
    128
40
}
41
42
const fn default_max_filters() -> usize {
43
    8
44
}
45
46
const fn default_max_filter_bytes() -> usize {
47
    32_768
48
}
49
50
const fn default_max_query_results() -> usize {
51
    500
52
}
53
54
const fn default_max_repositories() -> usize {
55
    4096
56
}
57
58
const fn default_max_discovered_peers() -> usize {
59
    32
60
}
61
62
const fn default_max_query_bytes() -> usize {
63
    16_777_216
64
}
65
66
const fn default_events_per_minute() -> u32 {
67
    120
68
}
69
70
const fn default_queries_per_minute() -> u32 {
71
    120
72
}
73
74
const fn default_max_database_bytes() -> u64 {
75
    1_073_741_824
76
}
77
78
const fn default_min_free_bytes() -> u64 {
79
    67_108_864
80
}
81
82
const fn default_write_timeout_seconds() -> u64 {
83
    10
84
}
85
86
#[derive(Debug, Clone, Deserialize)]
87
#[serde(deny_unknown_fields)]
88
pub struct Config {
89
    #[serde(default = "default_listen")]
90
    pub listen: SocketAddr,
91
    pub public_url: String,
92
    #[serde(default = "default_database")]
93
    pub database: PathBuf,
94
    pub forge_pubkeys: Vec<String>,
95
    #[serde(default)]
96
    pub peers: Vec<Peer>,
97
    #[serde(default)]
98
    pub allow_private_discovery: bool,
99
    #[serde(default)]
100
    pub limits: Limits,
101
}
102
103
#[derive(Debug, Clone, Deserialize)]
104
#[serde(deny_unknown_fields)]
105
pub struct Peer {
106
    pub url: String,
107
    #[serde(default = "default_max_query_results")]
108
    pub query_limit: usize,
109
    pub rescan_from: Option<u64>,
110
}
111
112
#[derive(Debug, Clone, Deserialize)]
113
#[serde(deny_unknown_fields)]
114
pub struct Limits {
115
    #[serde(default = "default_max_repositories")]
116
    pub max_repositories: usize,
117
    #[serde(default = "default_max_discovered_peers")]
118
    pub max_discovered_peers: usize,
119
    #[serde(default = "default_max_connections")]
120
    pub max_connections: usize,
121
    #[serde(default = "default_max_connections_per_ip")]
122
    pub max_connections_per_ip: usize,
123
    #[serde(default = "default_max_subscriptions")]
124
    pub max_subscriptions: usize,
125
    #[serde(default = "default_max_event_bytes")]
126
    pub max_event_bytes: usize,
127
    #[serde(default = "default_max_message_bytes")]
128
    pub max_message_bytes: usize,
129
    #[serde(default = "default_max_tags")]
130
    pub max_tags: usize,
131
    #[serde(default = "default_max_filters")]
132
    pub max_filters: usize,
133
    #[serde(default = "default_max_filter_bytes")]
134
    pub max_filter_bytes: usize,
135
    #[serde(default = "default_max_query_results")]
136
    pub max_query_results: usize,
137
    #[serde(default = "default_max_query_bytes")]
138
    pub max_query_bytes: usize,
139
    #[serde(default = "default_events_per_minute")]
140
    pub events_per_minute: u32,
141
    #[serde(default = "default_queries_per_minute")]
142
    pub queries_per_minute: u32,
143
    #[serde(default = "default_max_database_bytes")]
144
    pub max_database_bytes: u64,
145
    #[serde(default = "default_min_free_bytes")]
146
    pub min_free_bytes: u64,
147
    #[serde(default = "default_write_timeout_seconds")]
148
    pub write_timeout_seconds: u64,
149
}
150
151
impl Default for Limits {
152
    fn default() -> Self {
153
        Self {
154
            max_repositories: default_max_repositories(),
155
            max_discovered_peers: default_max_discovered_peers(),
156
            max_connections: default_max_connections(),
157
            max_connections_per_ip: default_max_connections_per_ip(),
158
            max_subscriptions: default_max_subscriptions(),
159
            max_event_bytes: default_max_event_bytes(),
160
            max_message_bytes: default_max_message_bytes(),
161
            max_tags: default_max_tags(),
162
            max_filters: default_max_filters(),
163
            max_filter_bytes: default_max_filter_bytes(),
164
            max_query_results: default_max_query_results(),
165
            max_query_bytes: default_max_query_bytes(),
166
            events_per_minute: default_events_per_minute(),
167
            queries_per_minute: default_queries_per_minute(),
168
            max_database_bytes: default_max_database_bytes(),
169
            min_free_bytes: default_min_free_bytes(),
170
            write_timeout_seconds: default_write_timeout_seconds(),
171
        }
172
    }
173
}
174
175
impl Config {
176
    pub fn load(path: &Path) -> anyhow::Result<Self> {
177
        let text = read_to_string(path).map_err(|error| {
178
            anyhow::anyhow!("failed to read config {}: {error}", path.display())
179
        })?;
180
        let mut cfg: Self = toml::from_str(&text).map_err(|error| {
181
            anyhow::anyhow!("failed to parse config {}: {error}", path.display())
182
        })?;
183
        if cfg.database.is_relative() {
184
            if let Some(parent) = path.parent() {
185
                if !parent.as_os_str().is_empty() {
186
                    cfg.database = parent.join(&cfg.database);
187
                }
188
            }
189
        }
190
        cfg.validate()?;
191
        Ok(cfg)
192
    }
193
194
    pub fn validate(&self) -> anyhow::Result<()> {
195
        let url = relay_url(&self.public_url)
196
            .ok_or_else(|| anyhow::anyhow!("invalid public_url {}", self.public_url))?;
197
        self.forge_keys()?;
198
        if self.peers.len() > 16 {
199
            anyhow::bail!("at most 16 peer relays are supported");
200
        }
201
        let mut peers = HashSet::new();
202
        for peer in &self.peers {
203
            let peer_url = relay_url(&peer.url).ok_or_else(|| {
204
                anyhow::anyhow!(
205
                    "peer urls must be ws or wss without credentials, queries or fragments"
206
                )
207
            })?;
208
            if peer_url == url || !peers.insert(peer_url) {
209
                anyhow::bail!("peer urls must be unique and distinct from public_url");
210
            }
211
            if !(2..10000).contains(&peer.query_limit) {
212
                anyhow::bail!("peer query_limit must be between 2 and 9999");
213
            }
214
        }
215
        if self.limits.max_connections == 0
216
            || self.limits.max_connections_per_ip == 0
217
            || self.limits.max_subscriptions == 0
218
            || self.limits.max_event_bytes == 0
219
            || self.limits.max_message_bytes == 0
220
            || self.limits.max_tags == 0
221
            || self.limits.max_filters == 0
222
            || self.limits.max_filter_bytes == 0
223
            || self.limits.max_query_results == 0
224
            || self.limits.max_query_bytes == 0
225
            || self.limits.events_per_minute == 0
226
            || self.limits.queries_per_minute == 0
227
            || self.limits.max_database_bytes == 0
228
            || self.limits.min_free_bytes == 0
229
            || self.limits.write_timeout_seconds == 0
230
        {
231
            anyhow::bail!("limits must all be positive");
232
        }
233
        if self.limits.max_connections_per_ip > self.limits.max_connections {
234
            anyhow::bail!("max_connections_per_ip must not exceed max_connections");
235
        }
236
        let min_message = self
237
            .limits
238
            .max_event_bytes
239
            .checked_add(256)
240
            .ok_or_else(|| anyhow::anyhow!("max_event_bytes too large"))?;
241
        if min_message > self.limits.max_message_bytes {
242
            anyhow::bail!("max_message_bytes must fit max_event_bytes plus 256 bytes of overhead");
243
        }
244
        if self.limits.max_event_bytes > self.limits.max_query_bytes {
245
            anyhow::bail!("max_query_bytes must fit one maximum size event");
246
        }
247
        if self.limits.max_repositories == 0 || self.limits.max_repositories >= 10000 {
248
            anyhow::bail!("max_repositories must be between 1 and 9999");
249
        }
250
        if self.limits.max_discovered_peers == 0 || self.limits.max_discovered_peers > 256 {
251
            anyhow::bail!("max_discovered_peers must be between 1 and 256");
252
        }
253
        if self.limits.max_query_results >= 10000 {
254
            anyhow::bail!("max_query_results must be below 10000");
255
        }
256
        if self.limits.max_connections > Semaphore::MAX_PERMITS {
257
            anyhow::bail!("max_connections exceeds the semaphore limit");
258
        }
259
        self.limits
260
            .max_filter_bytes
261
            .checked_mul(self.limits.max_subscriptions)
262
            .ok_or_else(|| anyhow::anyhow!("subscription byte budget is too large"))?;
263
        if self.websocket_path()? == "/healthz" {
264
            anyhow::bail!("public_url must not use the health endpoint");
265
        }
266
        Ok(())
267
    }
268
269
    pub fn forge_keys(&self) -> anyhow::Result<HashSet<PublicKey>> {
270
        if self.forge_pubkeys.is_empty() {
271
            anyhow::bail!("forge_pubkeys must not be empty");
272
        }
273
        let mut set = HashSet::new();
274
        for raw in &self.forge_pubkeys {
275
            let trimmed = raw.trim();
276
            if trimmed.len() != 64 || !trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
277
                anyhow::bail!("invalid forge public key: {raw}");
278
            }
279
            let key: PublicKey = trimmed
280
                .parse()
281
                .map_err(|error| anyhow::anyhow!("invalid forge public key {raw}: {error}"))?;
282
            if !set.insert(key) {
283
                anyhow::bail!("duplicate forge public key: {raw}");
284
            }
285
        }
286
        Ok(set)
287
    }
288
289
    pub fn websocket_path(&self) -> anyhow::Result<String> {
290
        let url = url::Url::parse(&self.public_url)
291
            .map_err(|error| anyhow::anyhow!("invalid public_url {}: {error}", self.public_url))?;
292
        let path = url.path();
293
        if path.is_empty() {
294
            Ok("/".to_owned())
295
        } else if path.starts_with('/') {
296
            Ok(path.to_owned())
297
        } else {
298
            Ok(format!("/{path}"))
299
        }
300
    }
301
}
302
303
pub(crate) fn relay_url(raw: &str) -> Option<url::Url> {
304
    let url = url::Url::parse(raw).ok()?;
305
    if !matches!(url.scheme(), "ws" | "wss")
306
        || url.host_str().is_none()
307
        || !url.username().is_empty()
308
        || url.password().is_some()
309
        || url.query().is_some()
310
        || url.fragment().is_some()
311
    {
312
        return None;
313
    }
314
    Some(url)
315
}