| 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 | } |