config.rs (70610B)
1 #![forbid(unsafe_code)] 2 3 use crate::{ 4 errors::BaseRelayError, 5 rate_limits::{ 6 TangleAuthRateLimitConfig, TangleEventRateLimitConfig, TangleGroupRateLimitConfig, 7 TangleQueryRateLimitConfig, TangleRateLimitConfig, TangleRateLimitRule, 8 }, 9 relay::{ 10 auth::BaseAuthState, 11 core::{BaseRelay, BaseRelayLimitSettings, BaseRelayLimits}, 12 }, 13 tenant::{CanonicalHost, TenantId, TenantRelayUrl, TenantSchema}, 14 }; 15 use serde::Deserialize; 16 use std::{ 17 collections::BTreeSet, 18 net::SocketAddr, 19 path::{Component, Path, PathBuf}, 20 }; 21 use tangle_crypto::RelaySigner; 22 use tangle_groups::GroupRuntimeConfig; 23 use tangle_protocol::{PublicKeyHex, SubscriptionId}; 24 use tangle_store_pocket::{PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy}; 25 26 const MAX_POCKET_QUERY_SCRAPE_WINDOW_SECONDS: u64 = 86_400; 27 28 #[derive(Debug, Clone, PartialEq, Eq)] 29 pub struct TangleHostRuntimeConfig { 30 listen_addr: SocketAddr, 31 tenant_config_dir: PathBuf, 32 limits: TangleHostLimitsConfig, 33 ops: TangleHostOpsConfig, 34 trusted_proxy: TangleTrustedProxyConfig, 35 tracing: BaseRelayTracingConfig, 36 } 37 38 #[derive(Debug, Clone, PartialEq, Eq)] 39 pub struct TangleHostRuntimeConfigSet { 40 host: TangleHostRuntimeConfig, 41 tenants: Vec<TenantRuntimeConfig>, 42 } 43 44 impl TangleHostRuntimeConfigSet { 45 pub fn new( 46 host: TangleHostRuntimeConfig, 47 tenants: Vec<TenantRuntimeConfig>, 48 ) -> Result<Self, BaseRelayError> { 49 validate_tenant_config_set(&tenants)?; 50 Ok(Self { host, tenants }) 51 } 52 53 pub fn host(&self) -> &TangleHostRuntimeConfig { 54 &self.host 55 } 56 57 pub fn tenants(&self) -> &[TenantRuntimeConfig] { 58 &self.tenants 59 } 60 61 pub fn active_tenants(&self) -> impl Iterator<Item = &TenantRuntimeConfig> { 62 self.tenants.iter().filter(|tenant| !tenant.inactive()) 63 } 64 } 65 66 impl TangleHostRuntimeConfig { 67 pub fn listen_addr(&self) -> SocketAddr { 68 self.listen_addr 69 } 70 71 pub fn tenant_config_dir(&self) -> &std::path::Path { 72 &self.tenant_config_dir 73 } 74 75 pub fn limits(&self) -> TangleHostLimitsConfig { 76 self.limits 77 } 78 79 pub fn ops(&self) -> TangleHostOpsConfig { 80 self.ops 81 } 82 83 pub fn trusted_proxy(&self) -> &TangleTrustedProxyConfig { 84 &self.trusted_proxy 85 } 86 87 pub fn tracing(&self) -> &BaseRelayTracingConfig { 88 &self.tracing 89 } 90 } 91 92 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 93 pub struct TangleHostLimitsConfig { 94 max_total_connections: usize, 95 max_total_subscriptions: usize, 96 tenant_startup_concurrency: usize, 97 } 98 99 impl TangleHostLimitsConfig { 100 pub fn new( 101 max_total_connections: usize, 102 max_total_subscriptions: usize, 103 tenant_startup_concurrency: usize, 104 ) -> Result<Self, BaseRelayError> { 105 require_positive("limits.max_total_connections", max_total_connections)?; 106 require_positive("limits.max_total_subscriptions", max_total_subscriptions)?; 107 require_positive( 108 "limits.tenant_startup_concurrency", 109 tenant_startup_concurrency, 110 )?; 111 Ok(Self { 112 max_total_connections, 113 max_total_subscriptions, 114 tenant_startup_concurrency, 115 }) 116 } 117 118 pub fn max_total_connections(self) -> usize { 119 self.max_total_connections 120 } 121 122 pub fn max_total_subscriptions(self) -> usize { 123 self.max_total_subscriptions 124 } 125 126 pub fn tenant_startup_concurrency(self) -> usize { 127 self.tenant_startup_concurrency 128 } 129 } 130 131 impl Default for TangleHostLimitsConfig { 132 fn default() -> Self { 133 Self::new(10_000, 25_000, 4).expect("default host limits are valid") 134 } 135 } 136 137 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 138 pub struct TangleHostOpsConfig { 139 enabled: bool, 140 expose_tenant_inventory: bool, 141 } 142 143 impl TangleHostOpsConfig { 144 pub fn new(enabled: bool, expose_tenant_inventory: bool) -> Self { 145 Self { 146 enabled, 147 expose_tenant_inventory, 148 } 149 } 150 151 pub fn enabled(self) -> bool { 152 self.enabled 153 } 154 155 pub fn expose_tenant_inventory(self) -> bool { 156 self.expose_tenant_inventory 157 } 158 } 159 160 impl Default for TangleHostOpsConfig { 161 fn default() -> Self { 162 Self::new(true, true) 163 } 164 } 165 166 #[derive(Debug, Clone, PartialEq, Eq)] 167 pub struct TangleTrustedProxyConfig { 168 enabled: bool, 169 trusted_peers: Vec<String>, 170 } 171 172 impl TangleTrustedProxyConfig { 173 pub fn new(enabled: bool, trusted_peers: Vec<String>) -> Result<Self, BaseRelayError> { 174 for peer in &trusted_peers { 175 if peer.trim().is_empty() || peer.trim() != peer { 176 return Err(BaseRelayError::invalid( 177 "trusted_proxy.trusted_peers entries must not be empty or padded", 178 )); 179 } 180 } 181 Ok(Self { 182 enabled, 183 trusted_peers, 184 }) 185 } 186 187 pub fn enabled(&self) -> bool { 188 self.enabled 189 } 190 191 pub fn trusted_peers(&self) -> &[String] { 192 &self.trusted_peers 193 } 194 } 195 196 impl Default for TangleTrustedProxyConfig { 197 fn default() -> Self { 198 Self::new(false, Vec::new()).expect("default trusted proxy config is valid") 199 } 200 } 201 202 #[derive(Debug, Clone, PartialEq, Eq)] 203 pub struct TenantRelayInfoConfig { 204 name: String, 205 description: Option<String>, 206 contact: Option<String>, 207 icon: Option<String>, 208 } 209 210 impl TenantRelayInfoConfig { 211 pub fn new( 212 name: impl Into<String>, 213 description: Option<String>, 214 contact: Option<String>, 215 icon: Option<String>, 216 ) -> Result<Self, BaseRelayError> { 217 let name = name.into(); 218 if name.trim().is_empty() || name.trim() != name { 219 return Err(BaseRelayError::invalid( 220 "info.name must not be empty or padded", 221 )); 222 } 223 Ok(Self { 224 name, 225 description: validate_optional_text("info.description", description)?, 226 contact: validate_optional_text("info.contact", contact)?, 227 icon: validate_optional_text("info.icon", icon)?, 228 }) 229 } 230 231 pub fn name(&self) -> &str { 232 &self.name 233 } 234 235 pub fn description(&self) -> Option<&str> { 236 self.description.as_deref() 237 } 238 239 pub fn contact(&self) -> Option<&str> { 240 self.contact.as_deref() 241 } 242 243 pub fn icon(&self) -> Option<&str> { 244 self.icon.as_deref() 245 } 246 } 247 248 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 249 pub struct TenantBackupExportConfig { 250 backup_enabled: bool, 251 export_enabled: bool, 252 } 253 254 impl TenantBackupExportConfig { 255 pub fn new(backup_enabled: bool, export_enabled: bool) -> Self { 256 Self { 257 backup_enabled, 258 export_enabled, 259 } 260 } 261 262 pub fn backup_enabled(self) -> bool { 263 self.backup_enabled 264 } 265 266 pub fn export_enabled(self) -> bool { 267 self.export_enabled 268 } 269 } 270 271 impl Default for TenantBackupExportConfig { 272 fn default() -> Self { 273 Self::new(true, true) 274 } 275 } 276 277 #[derive(Debug, Clone, PartialEq, Eq)] 278 pub struct TenantRuntimeConfig { 279 tenant_id: TenantId, 280 tenant_schema: TenantSchema, 281 host: CanonicalHost, 282 relay_url: TenantRelayUrl, 283 inactive: bool, 284 info: TenantRelayInfoConfig, 285 pocket: PocketStoreConfig, 286 pocket_query: PocketQueryConfig, 287 groups: GroupRuntimeConfig, 288 auth_ttl_seconds: u64, 289 auth_created_at_skew_seconds: u64, 290 limits: BaseRelayRuntimeLimitsConfig, 291 rate_limits: TangleRateLimitConfig, 292 backup_export: TenantBackupExportConfig, 293 } 294 295 impl TenantRuntimeConfig { 296 pub fn tenant_id(&self) -> &TenantId { 297 &self.tenant_id 298 } 299 300 pub fn tenant_schema(&self) -> &TenantSchema { 301 &self.tenant_schema 302 } 303 304 pub fn host(&self) -> &CanonicalHost { 305 &self.host 306 } 307 308 pub fn relay_url(&self) -> &TenantRelayUrl { 309 &self.relay_url 310 } 311 312 pub fn inactive(&self) -> bool { 313 self.inactive 314 } 315 316 pub fn info(&self) -> &TenantRelayInfoConfig { 317 &self.info 318 } 319 320 pub fn pocket_config(&self) -> &PocketStoreConfig { 321 &self.pocket 322 } 323 324 pub fn pocket_query_config(&self) -> PocketQueryConfig { 325 self.pocket_query 326 } 327 328 pub fn groups(&self) -> &GroupRuntimeConfig { 329 &self.groups 330 } 331 332 pub fn relay_self_pubkey(&self) -> Result<Option<PublicKeyHex>, BaseRelayError> { 333 self.groups 334 .relay_secret() 335 .map(|secret| RelaySigner::from_secret_hex(secret.expose_for_signing())) 336 .transpose() 337 .map(|signer| signer.map(|signer| signer.public_key().clone())) 338 .map_err(BaseRelayError::invalid) 339 } 340 341 pub fn auth_ttl_seconds(&self) -> u64 { 342 self.auth_ttl_seconds 343 } 344 345 pub fn auth_created_at_skew_seconds(&self) -> u64 { 346 self.auth_created_at_skew_seconds 347 } 348 349 pub fn limits(&self) -> BaseRelayRuntimeLimitsConfig { 350 self.limits 351 } 352 353 pub fn rate_limits(&self) -> TangleRateLimitConfig { 354 self.rate_limits 355 } 356 357 pub fn backup_export(&self) -> TenantBackupExportConfig { 358 self.backup_export 359 } 360 361 pub fn to_base_relay_runtime_config( 362 &self, 363 listen_addr: SocketAddr, 364 tracing: BaseRelayTracingConfig, 365 ) -> BaseRelayRuntimeConfig { 366 BaseRelayRuntimeConfig { 367 listen_addr, 368 relay_url: self.relay_url.as_str().to_owned(), 369 pocket: self.pocket.clone(), 370 pocket_query: self.pocket_query, 371 groups: self.groups.clone(), 372 auth_ttl_seconds: self.auth_ttl_seconds, 373 auth_created_at_skew_seconds: self.auth_created_at_skew_seconds, 374 limits: self.limits, 375 rate_limits: self.rate_limits, 376 tracing, 377 } 378 } 379 } 380 381 #[derive(Debug, Clone, PartialEq, Eq)] 382 pub struct BaseRelayRuntimeConfig { 383 listen_addr: SocketAddr, 384 relay_url: String, 385 pocket: PocketStoreConfig, 386 pocket_query: PocketQueryConfig, 387 groups: GroupRuntimeConfig, 388 auth_ttl_seconds: u64, 389 auth_created_at_skew_seconds: u64, 390 limits: BaseRelayRuntimeLimitsConfig, 391 rate_limits: TangleRateLimitConfig, 392 tracing: BaseRelayTracingConfig, 393 } 394 395 impl BaseRelayRuntimeConfig { 396 pub fn listen_addr(&self) -> SocketAddr { 397 self.listen_addr 398 } 399 400 pub fn relay_url(&self) -> &str { 401 &self.relay_url 402 } 403 404 pub fn pocket_config(&self) -> &PocketStoreConfig { 405 &self.pocket 406 } 407 408 pub fn pocket_query_config(&self) -> PocketQueryConfig { 409 self.pocket_query 410 } 411 412 pub fn groups(&self) -> &GroupRuntimeConfig { 413 &self.groups 414 } 415 416 pub fn auth_ttl_seconds(&self) -> u64 { 417 self.auth_ttl_seconds 418 } 419 420 pub fn auth_created_at_skew_seconds(&self) -> u64 { 421 self.auth_created_at_skew_seconds 422 } 423 424 pub fn limits(&self) -> BaseRelayRuntimeLimitsConfig { 425 self.limits 426 } 427 428 pub fn rate_limits(&self) -> TangleRateLimitConfig { 429 self.rate_limits 430 } 431 432 pub fn tracing(&self) -> &BaseRelayTracingConfig { 433 &self.tracing 434 } 435 436 pub fn open_relay(&self) -> Result<BaseRelay, BaseRelayError> { 437 BaseRelay::open_with_groups( 438 &self.pocket, 439 self.limits.base_relay_limits()?, 440 &self.groups, 441 self.pocket_query, 442 ) 443 } 444 445 pub fn auth_state(&self) -> Result<BaseAuthState, BaseRelayError> { 446 BaseAuthState::new( 447 self.relay_url.clone(), 448 self.auth_ttl_seconds, 449 self.auth_created_at_skew_seconds, 450 ) 451 } 452 } 453 454 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 455 pub enum BaseRelayTracingFormat { 456 Compact, 457 Json, 458 } 459 460 impl BaseRelayTracingFormat { 461 pub fn as_str(self) -> &'static str { 462 match self { 463 Self::Compact => "compact", 464 Self::Json => "json", 465 } 466 } 467 } 468 469 #[derive(Debug, Clone, PartialEq, Eq)] 470 pub struct BaseRelayTracingConfig { 471 enabled: bool, 472 filter: String, 473 format: BaseRelayTracingFormat, 474 } 475 476 impl BaseRelayTracingConfig { 477 pub fn new( 478 enabled: bool, 479 filter: impl Into<String>, 480 format: BaseRelayTracingFormat, 481 ) -> Result<Self, BaseRelayError> { 482 let filter = filter.into(); 483 if filter.trim().is_empty() { 484 return Err(BaseRelayError::invalid( 485 "observability.tracing.filter must not be empty", 486 )); 487 } 488 Ok(Self { 489 enabled, 490 filter: filter.trim().to_owned(), 491 format, 492 }) 493 } 494 495 pub fn enabled(&self) -> bool { 496 self.enabled 497 } 498 499 pub fn filter(&self) -> &str { 500 &self.filter 501 } 502 503 pub fn format(&self) -> BaseRelayTracingFormat { 504 self.format 505 } 506 } 507 508 impl Default for BaseRelayTracingConfig { 509 fn default() -> Self { 510 Self::new( 511 true, 512 "info,tangle=info,tangle_runtime=info,tangle_groups=info,tangle_store_pocket=info", 513 BaseRelayTracingFormat::Json, 514 ) 515 .expect("default tracing config is valid") 516 } 517 } 518 519 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 520 pub struct BaseRelayRuntimeLimitsConfig { 521 max_message_length: usize, 522 max_subid_length: usize, 523 max_subscriptions_per_connection: usize, 524 max_filters_per_request: usize, 525 max_tag_values_per_filter: usize, 526 max_query_complexity: usize, 527 max_limit: u64, 528 default_limit: u64, 529 max_event_tags: usize, 530 max_content_length: usize, 531 broadcast_channel_capacity: usize, 532 per_connection_outbound_queue: usize, 533 } 534 535 impl BaseRelayRuntimeLimitsConfig { 536 fn from_document(document: BaseRelayRuntimeLimitsDocument) -> Result<Self, BaseRelayError> { 537 require_positive("limits.max_message_length", document.max_message_length)?; 538 require_positive("limits.max_subid_length", document.max_subid_length)?; 539 require_positive( 540 "limits.max_subscriptions_per_connection", 541 document.max_subscriptions_per_connection, 542 )?; 543 require_positive( 544 "limits.max_filters_per_request", 545 document.max_filters_per_request, 546 )?; 547 require_positive( 548 "limits.max_tag_values_per_filter", 549 document.max_tag_values_per_filter, 550 )?; 551 require_positive("limits.max_query_complexity", document.max_query_complexity)?; 552 require_positive_u64("limits.max_limit", document.max_limit)?; 553 require_positive_u64("limits.default_limit", document.default_limit)?; 554 require_positive("limits.max_event_tags", document.max_event_tags)?; 555 require_positive("limits.max_content_length", document.max_content_length)?; 556 require_positive( 557 "limits.broadcast_channel_capacity", 558 document.broadcast_channel_capacity, 559 )?; 560 require_positive( 561 "limits.per_connection_outbound_queue", 562 document.per_connection_outbound_queue, 563 )?; 564 if document.max_subid_length > SubscriptionId::MAX_LENGTH { 565 return Err(BaseRelayError::invalid(format!( 566 "limits.max_subid_length must be less than or equal to {}", 567 SubscriptionId::MAX_LENGTH 568 ))); 569 } 570 if document.default_limit > document.max_limit { 571 return Err(BaseRelayError::invalid( 572 "limits.default_limit must be less than or equal to limits.max_limit", 573 )); 574 } 575 Ok(Self { 576 max_message_length: document.max_message_length, 577 max_subid_length: document.max_subid_length, 578 max_subscriptions_per_connection: document.max_subscriptions_per_connection, 579 max_filters_per_request: document.max_filters_per_request, 580 max_tag_values_per_filter: document.max_tag_values_per_filter, 581 max_query_complexity: document.max_query_complexity, 582 max_limit: document.max_limit, 583 default_limit: document.default_limit, 584 max_event_tags: document.max_event_tags, 585 max_content_length: document.max_content_length, 586 broadcast_channel_capacity: document.broadcast_channel_capacity, 587 per_connection_outbound_queue: document.per_connection_outbound_queue, 588 }) 589 } 590 591 pub fn max_message_length(self) -> usize { 592 self.max_message_length 593 } 594 595 pub fn max_subid_length(self) -> usize { 596 self.max_subid_length 597 } 598 599 pub fn max_subscriptions_per_connection(self) -> usize { 600 self.max_subscriptions_per_connection 601 } 602 603 pub fn max_filters_per_request(self) -> usize { 604 self.max_filters_per_request 605 } 606 607 pub fn max_tag_values_per_filter(self) -> usize { 608 self.max_tag_values_per_filter 609 } 610 611 pub fn max_query_complexity(self) -> usize { 612 self.max_query_complexity 613 } 614 615 pub fn max_limit(self) -> u64 { 616 self.max_limit 617 } 618 619 pub fn default_limit(self) -> u64 { 620 self.default_limit 621 } 622 623 pub fn max_event_tags(self) -> usize { 624 self.max_event_tags 625 } 626 627 pub fn max_content_length(self) -> usize { 628 self.max_content_length 629 } 630 631 pub fn broadcast_channel_capacity(self) -> usize { 632 self.broadcast_channel_capacity 633 } 634 635 pub fn per_connection_outbound_queue(self) -> usize { 636 self.per_connection_outbound_queue 637 } 638 639 pub fn base_relay_limits(self) -> Result<BaseRelayLimits, BaseRelayError> { 640 BaseRelayLimits::new(BaseRelayLimitSettings { 641 max_pending_events: self.per_connection_outbound_queue, 642 max_subscription_id_length: self.max_subid_length, 643 max_subscriptions: self.max_subscriptions_per_connection, 644 max_filters_per_request: self.max_filters_per_request, 645 max_tag_values_per_filter: self.max_tag_values_per_filter, 646 max_query_complexity: self.max_query_complexity, 647 max_event_tags: self.max_event_tags, 648 max_content_length: self.max_content_length, 649 max_limit: self.max_limit, 650 default_limit: self.default_limit, 651 }) 652 } 653 } 654 655 #[derive(Debug, Deserialize)] 656 #[serde(deny_unknown_fields)] 657 struct TangleHostRuntimeConfigDocument { 658 listen_addr: String, 659 tenant_config_dir: String, 660 #[serde(default)] 661 limits: TangleHostLimitsConfigDocument, 662 #[serde(default)] 663 ops: TangleHostOpsConfigDocument, 664 #[serde(default)] 665 trusted_proxy: TangleTrustedProxyConfigDocument, 666 #[serde(default)] 667 observability: BaseRelayObservabilityConfigDocument, 668 } 669 670 #[derive(Debug, Deserialize)] 671 #[serde(deny_unknown_fields)] 672 struct TangleHostLimitsConfigDocument { 673 max_total_connections: usize, 674 max_total_subscriptions: usize, 675 tenant_startup_concurrency: usize, 676 } 677 678 impl Default for TangleHostLimitsConfigDocument { 679 fn default() -> Self { 680 let defaults = TangleHostLimitsConfig::default(); 681 Self { 682 max_total_connections: defaults.max_total_connections(), 683 max_total_subscriptions: defaults.max_total_subscriptions(), 684 tenant_startup_concurrency: defaults.tenant_startup_concurrency(), 685 } 686 } 687 } 688 689 #[derive(Debug, Deserialize)] 690 #[serde(deny_unknown_fields)] 691 struct TangleHostOpsConfigDocument { 692 enabled: bool, 693 expose_tenant_inventory: bool, 694 } 695 696 impl Default for TangleHostOpsConfigDocument { 697 fn default() -> Self { 698 let defaults = TangleHostOpsConfig::default(); 699 Self { 700 enabled: defaults.enabled(), 701 expose_tenant_inventory: defaults.expose_tenant_inventory(), 702 } 703 } 704 } 705 706 #[derive(Debug, Deserialize)] 707 #[serde(deny_unknown_fields)] 708 struct TangleTrustedProxyConfigDocument { 709 enabled: bool, 710 #[serde(default)] 711 trusted_peers: Vec<String>, 712 } 713 714 impl Default for TangleTrustedProxyConfigDocument { 715 fn default() -> Self { 716 let defaults = TangleTrustedProxyConfig::default(); 717 Self { 718 enabled: defaults.enabled(), 719 trusted_peers: defaults.trusted_peers().to_vec(), 720 } 721 } 722 } 723 724 #[derive(Debug, Deserialize)] 725 #[serde(deny_unknown_fields)] 726 struct TenantRuntimeConfigDocument { 727 tenant_id: String, 728 tenant_schema: String, 729 host: String, 730 relay_url: String, 731 #[serde(default)] 732 inactive: bool, 733 info: TenantRelayInfoConfigDocument, 734 pocket: TenantPocketConfigDocument, 735 pocket_query: BaseRelayPocketQueryConfigDocument, 736 groups: serde_json::Value, 737 auth: BaseRelayAuthConfigDocument, 738 limits: BaseRelayRuntimeLimitsDocument, 739 rate_limits: BaseRelayRateLimitsDocument, 740 #[serde(default)] 741 backup_export: TenantBackupExportConfigDocument, 742 } 743 744 #[derive(Debug, Deserialize)] 745 #[serde(deny_unknown_fields)] 746 struct TenantRelayInfoConfigDocument { 747 name: String, 748 description: Option<String>, 749 contact: Option<String>, 750 icon: Option<String>, 751 } 752 753 #[derive(Debug, Deserialize)] 754 #[serde(deny_unknown_fields)] 755 struct TenantPocketConfigDocument { 756 data_directory: String, 757 sync_policy: BaseRelayPocketSyncPolicyDocument, 758 } 759 760 #[derive(Debug, Deserialize)] 761 #[serde(deny_unknown_fields)] 762 struct TenantBackupExportConfigDocument { 763 backup_enabled: bool, 764 export_enabled: bool, 765 } 766 767 impl Default for TenantBackupExportConfigDocument { 768 fn default() -> Self { 769 let defaults = TenantBackupExportConfig::default(); 770 Self { 771 backup_enabled: defaults.backup_enabled(), 772 export_enabled: defaults.export_enabled(), 773 } 774 } 775 } 776 777 #[derive(Debug, Deserialize)] 778 #[serde(deny_unknown_fields)] 779 struct BaseRelayRuntimeConfigDocument { 780 server: BaseRelayServerConfigDocument, 781 pocket: BaseRelayPocketConfigDocument, 782 groups: serde_json::Value, 783 auth: BaseRelayAuthConfigDocument, 784 limits: BaseRelayRuntimeLimitsDocument, 785 rate_limits: BaseRelayRateLimitsDocument, 786 #[serde(default)] 787 observability: BaseRelayObservabilityConfigDocument, 788 } 789 790 #[derive(Debug, Deserialize)] 791 #[serde(deny_unknown_fields)] 792 struct BaseRelayServerConfigDocument { 793 listen_addr: String, 794 relay_url: String, 795 } 796 797 #[derive(Debug, Deserialize)] 798 #[serde(deny_unknown_fields)] 799 struct BaseRelayPocketConfigDocument { 800 data_directory: String, 801 sync_policy: BaseRelayPocketSyncPolicyDocument, 802 query: BaseRelayPocketQueryConfigDocument, 803 } 804 805 #[derive(Debug, Deserialize)] 806 #[serde(deny_unknown_fields)] 807 struct BaseRelayPocketQueryConfigDocument { 808 allow_scraping: bool, 809 allow_scrape_if_limited_to: u32, 810 allow_scrape_if_max_seconds: u64, 811 } 812 813 #[derive(Debug, Clone, Copy, Deserialize)] 814 #[serde(rename_all = "snake_case")] 815 enum BaseRelayPocketSyncPolicyDocument { 816 FlushOnWrite, 817 FlushOnShutdown, 818 } 819 820 #[derive(Debug, Deserialize)] 821 #[serde(deny_unknown_fields)] 822 struct BaseRelayAuthConfigDocument { 823 challenge_ttl_seconds: u64, 824 created_at_skew_seconds: u64, 825 } 826 827 #[derive(Debug, Deserialize)] 828 #[serde(deny_unknown_fields)] 829 struct BaseRelayRuntimeLimitsDocument { 830 max_message_length: usize, 831 max_subid_length: usize, 832 max_subscriptions_per_connection: usize, 833 max_filters_per_request: usize, 834 max_tag_values_per_filter: usize, 835 max_query_complexity: usize, 836 max_limit: u64, 837 default_limit: u64, 838 max_event_tags: usize, 839 max_content_length: usize, 840 broadcast_channel_capacity: usize, 841 per_connection_outbound_queue: usize, 842 } 843 844 #[derive(Debug, Deserialize)] 845 #[serde(deny_unknown_fields)] 846 struct BaseRelayRateLimitsDocument { 847 auth: BaseRelayAuthRateLimitsDocument, 848 event: BaseRelayEventRateLimitsDocument, 849 group: BaseRelayGroupRateLimitsDocument, 850 req: BaseRelayQueryRateLimitsDocument, 851 count: BaseRelayQueryRateLimitsDocument, 852 } 853 854 #[derive(Debug, Deserialize)] 855 #[serde(deny_unknown_fields)] 856 struct BaseRelayAuthRateLimitsDocument { 857 per_ip: BaseRelayRateLimitRuleDocument, 858 per_pubkey: BaseRelayRateLimitRuleDocument, 859 failures: BaseRelayRateLimitRuleDocument, 860 failures_per_ip: BaseRelayRateLimitRuleDocument, 861 } 862 863 #[derive(Debug, Deserialize)] 864 #[serde(deny_unknown_fields)] 865 struct BaseRelayEventRateLimitsDocument { 866 per_ip: BaseRelayRateLimitRuleDocument, 867 per_pubkey: BaseRelayRateLimitRuleDocument, 868 per_kind: BaseRelayRateLimitRuleDocument, 869 } 870 871 #[derive(Debug, Deserialize)] 872 #[serde(deny_unknown_fields)] 873 struct BaseRelayGroupRateLimitsDocument { 874 write_per_ip: BaseRelayRateLimitRuleDocument, 875 write_per_pubkey: BaseRelayRateLimitRuleDocument, 876 write_per_group: BaseRelayRateLimitRuleDocument, 877 write_per_kind: BaseRelayRateLimitRuleDocument, 878 join_flow: BaseRelayRateLimitRuleDocument, 879 join_flow_per_ip: BaseRelayRateLimitRuleDocument, 880 } 881 882 #[derive(Debug, Deserialize)] 883 #[serde(deny_unknown_fields)] 884 struct BaseRelayQueryRateLimitsDocument { 885 per_ip: BaseRelayRateLimitRuleDocument, 886 per_connection: BaseRelayRateLimitRuleDocument, 887 per_pubkey: BaseRelayRateLimitRuleDocument, 888 per_group: BaseRelayRateLimitRuleDocument, 889 per_kind: BaseRelayRateLimitRuleDocument, 890 broad: BaseRelayRateLimitRuleDocument, 891 } 892 893 #[derive(Debug, Clone, Copy, Deserialize)] 894 #[serde(deny_unknown_fields)] 895 struct BaseRelayRateLimitRuleDocument { 896 window_seconds: u64, 897 max_hits: u64, 898 } 899 900 #[derive(Debug, Default, Deserialize)] 901 #[serde(deny_unknown_fields)] 902 struct BaseRelayObservabilityConfigDocument { 903 #[serde(default)] 904 tracing: BaseRelayTracingConfigDocument, 905 } 906 907 #[derive(Debug, Default, Deserialize)] 908 #[serde(deny_unknown_fields)] 909 struct BaseRelayTracingConfigDocument { 910 enabled: Option<bool>, 911 filter: Option<String>, 912 format: Option<BaseRelayTracingFormatDocument>, 913 } 914 915 #[derive(Debug, Clone, Copy, Deserialize)] 916 #[serde(rename_all = "snake_case")] 917 enum BaseRelayTracingFormatDocument { 918 Compact, 919 Json, 920 } 921 922 pub fn parse_tangle_host_runtime_config_json( 923 raw: &str, 924 ) -> Result<TangleHostRuntimeConfig, BaseRelayError> { 925 reject_legacy_single_relay_config(raw)?; 926 let document = 927 serde_json::from_str::<TangleHostRuntimeConfigDocument>(raw).map_err(|error| { 928 BaseRelayError::invalid(format!( 929 "tangle host runtime config JSON is invalid: {error}" 930 )) 931 })?; 932 let listen_addr = document 933 .listen_addr 934 .parse::<SocketAddr>() 935 .map_err(|error| BaseRelayError::invalid(format!("listen_addr is invalid: {error}")))?; 936 if document.tenant_config_dir.trim().is_empty() 937 || document.tenant_config_dir.trim() != document.tenant_config_dir 938 { 939 return Err(BaseRelayError::invalid( 940 "tenant_config_dir must not be empty or padded", 941 )); 942 } 943 Ok(TangleHostRuntimeConfig { 944 listen_addr, 945 tenant_config_dir: PathBuf::from(document.tenant_config_dir), 946 limits: TangleHostLimitsConfig::new( 947 document.limits.max_total_connections, 948 document.limits.max_total_subscriptions, 949 document.limits.tenant_startup_concurrency, 950 )?, 951 ops: TangleHostOpsConfig::new(document.ops.enabled, document.ops.expose_tenant_inventory), 952 trusted_proxy: TangleTrustedProxyConfig::new( 953 document.trusted_proxy.enabled, 954 document.trusted_proxy.trusted_peers, 955 )?, 956 tracing: base_relay_tracing_config_from_document(document.observability.tracing)?, 957 }) 958 } 959 960 pub fn parse_tenant_runtime_config_json(raw: &str) -> Result<TenantRuntimeConfig, BaseRelayError> { 961 reject_legacy_single_relay_config(raw)?; 962 let document = serde_json::from_str::<TenantRuntimeConfigDocument>(raw).map_err(|error| { 963 BaseRelayError::invalid(format!("tenant runtime config JSON is invalid: {error}")) 964 })?; 965 let tenant_id = TenantId::new(document.tenant_id)?; 966 let tenant_schema = TenantSchema::new(document.tenant_schema)?; 967 let host = CanonicalHost::new(document.host)?; 968 let relay_url = TenantRelayUrl::new(document.relay_url)?; 969 let pocket = PocketStoreConfig::new( 970 PathBuf::from(document.pocket.data_directory), 971 match document.pocket.sync_policy { 972 BaseRelayPocketSyncPolicyDocument::FlushOnWrite => PocketSyncPolicy::FlushOnWrite, 973 BaseRelayPocketSyncPolicyDocument::FlushOnShutdown => PocketSyncPolicy::FlushOnShutdown, 974 }, 975 ) 976 .map_err(|error| BaseRelayError::invalid(error.to_string()))?; 977 let groups_raw = serde_json::to_string(&document.groups).map_err(|error| { 978 BaseRelayError::invalid(format!("groups config JSON is invalid: {error}")) 979 })?; 980 let groups = tangle_groups::parse_group_runtime_config_json(&groups_raw) 981 .map_err(|error| BaseRelayError::invalid(error.to_string()))?; 982 if let Some(group_relay_url) = groups.canonical_relay_url() 983 && group_relay_url.as_str() != relay_url.as_str() 984 { 985 return Err(BaseRelayError::invalid( 986 "groups.canonical_relay_url must match relay_url", 987 )); 988 } 989 let limits = BaseRelayRuntimeLimitsConfig::from_document(document.limits)?; 990 let pocket_query = pocket_query_config_from_document(document.pocket_query, limits)?; 991 if document.auth.created_at_skew_seconds == 0 { 992 return Err(BaseRelayError::invalid( 993 "auth.created_at_skew_seconds must be greater than zero", 994 )); 995 } 996 Ok(TenantRuntimeConfig { 997 tenant_id, 998 tenant_schema, 999 host, 1000 relay_url, 1001 inactive: document.inactive, 1002 info: TenantRelayInfoConfig::new( 1003 document.info.name, 1004 document.info.description, 1005 document.info.contact, 1006 document.info.icon, 1007 )?, 1008 pocket, 1009 pocket_query, 1010 groups, 1011 auth_ttl_seconds: document.auth.challenge_ttl_seconds, 1012 auth_created_at_skew_seconds: document.auth.created_at_skew_seconds, 1013 limits, 1014 rate_limits: base_relay_rate_limits_from_document(document.rate_limits)?, 1015 backup_export: TenantBackupExportConfig::new( 1016 document.backup_export.backup_enabled, 1017 document.backup_export.export_enabled, 1018 ), 1019 }) 1020 } 1021 1022 fn reject_legacy_single_relay_config(raw: &str) -> Result<(), BaseRelayError> { 1023 let value = serde_json::from_str::<serde_json::Value>(raw) 1024 .map_err(|error| BaseRelayError::invalid(format!("config JSON is invalid: {error}")))?; 1025 if value 1026 .as_object() 1027 .is_some_and(|object| object.contains_key("server")) 1028 { 1029 return Err(BaseRelayError::invalid( 1030 "legacy single-relay config is not supported", 1031 )); 1032 } 1033 Ok(()) 1034 } 1035 1036 pub fn parse_base_relay_runtime_config_json( 1037 raw: &str, 1038 ) -> Result<BaseRelayRuntimeConfig, BaseRelayError> { 1039 let document = 1040 serde_json::from_str::<BaseRelayRuntimeConfigDocument>(raw).map_err(|error| { 1041 BaseRelayError::invalid(format!( 1042 "base relay runtime config JSON is invalid: {error}" 1043 )) 1044 })?; 1045 let listen_addr = document 1046 .server 1047 .listen_addr 1048 .parse::<SocketAddr>() 1049 .map_err(|error| { 1050 BaseRelayError::invalid(format!("server.listen_addr is invalid: {error}")) 1051 })?; 1052 let pocket_document = document.pocket; 1053 let pocket = PocketStoreConfig::new( 1054 PathBuf::from(pocket_document.data_directory), 1055 match pocket_document.sync_policy { 1056 BaseRelayPocketSyncPolicyDocument::FlushOnWrite => PocketSyncPolicy::FlushOnWrite, 1057 BaseRelayPocketSyncPolicyDocument::FlushOnShutdown => PocketSyncPolicy::FlushOnShutdown, 1058 }, 1059 ) 1060 .map_err(|error| BaseRelayError::invalid(error.to_string()))?; 1061 let groups_raw = serde_json::to_string(&document.groups).map_err(|error| { 1062 BaseRelayError::invalid(format!("groups config JSON is invalid: {error}")) 1063 })?; 1064 let groups = tangle_groups::parse_group_runtime_config_json(&groups_raw) 1065 .map_err(|error| BaseRelayError::invalid(error.to_string()))?; 1066 let limits = BaseRelayRuntimeLimitsConfig::from_document(document.limits)?; 1067 let pocket_query = pocket_query_config_from_document(pocket_document.query, limits)?; 1068 let rate_limits = base_relay_rate_limits_from_document(document.rate_limits)?; 1069 if document.auth.created_at_skew_seconds == 0 { 1070 return Err(BaseRelayError::invalid( 1071 "auth.created_at_skew_seconds must be greater than zero", 1072 )); 1073 } 1074 let tracing = base_relay_tracing_config_from_document(document.observability.tracing)?; 1075 Ok(BaseRelayRuntimeConfig { 1076 listen_addr, 1077 relay_url: document.server.relay_url, 1078 pocket, 1079 pocket_query, 1080 groups, 1081 auth_ttl_seconds: document.auth.challenge_ttl_seconds, 1082 auth_created_at_skew_seconds: document.auth.created_at_skew_seconds, 1083 limits, 1084 rate_limits, 1085 tracing, 1086 }) 1087 } 1088 1089 fn pocket_query_config_from_document( 1090 document: BaseRelayPocketQueryConfigDocument, 1091 limits: BaseRelayRuntimeLimitsConfig, 1092 ) -> Result<PocketQueryConfig, BaseRelayError> { 1093 if u64::from(document.allow_scrape_if_limited_to) > limits.max_limit() { 1094 return Err(BaseRelayError::invalid( 1095 "pocket.query.allow_scrape_if_limited_to must be less than or equal to limits.max_limit", 1096 )); 1097 } 1098 if document.allow_scrape_if_max_seconds > MAX_POCKET_QUERY_SCRAPE_WINDOW_SECONDS { 1099 return Err(BaseRelayError::invalid(format!( 1100 "pocket.query.allow_scrape_if_max_seconds must be less than or equal to {MAX_POCKET_QUERY_SCRAPE_WINDOW_SECONDS}" 1101 ))); 1102 } 1103 Ok(PocketQueryConfig::new( 1104 document.allow_scraping, 1105 document.allow_scrape_if_limited_to, 1106 document.allow_scrape_if_max_seconds, 1107 )) 1108 } 1109 1110 fn require_positive(field: &str, value: usize) -> Result<(), BaseRelayError> { 1111 if value == 0 { 1112 return Err(BaseRelayError::invalid(format!( 1113 "{field} must be greater than zero" 1114 ))); 1115 } 1116 Ok(()) 1117 } 1118 1119 fn require_positive_u64(field: &str, value: u64) -> Result<(), BaseRelayError> { 1120 if value == 0 { 1121 return Err(BaseRelayError::invalid(format!( 1122 "{field} must be greater than zero" 1123 ))); 1124 } 1125 Ok(()) 1126 } 1127 1128 fn base_relay_rate_limits_from_document( 1129 document: BaseRelayRateLimitsDocument, 1130 ) -> Result<TangleRateLimitConfig, BaseRelayError> { 1131 Ok(TangleRateLimitConfig::new( 1132 TangleAuthRateLimitConfig::new( 1133 base_relay_rate_limit_rule_from_document( 1134 "rate_limits.auth.per_ip", 1135 document.auth.per_ip, 1136 )?, 1137 base_relay_rate_limit_rule_from_document( 1138 "rate_limits.auth.per_pubkey", 1139 document.auth.per_pubkey, 1140 )?, 1141 base_relay_rate_limit_rule_from_document( 1142 "rate_limits.auth.failures", 1143 document.auth.failures, 1144 )?, 1145 base_relay_rate_limit_rule_from_document( 1146 "rate_limits.auth.failures_per_ip", 1147 document.auth.failures_per_ip, 1148 )?, 1149 ), 1150 TangleEventRateLimitConfig::new( 1151 base_relay_rate_limit_rule_from_document( 1152 "rate_limits.event.per_ip", 1153 document.event.per_ip, 1154 )?, 1155 base_relay_rate_limit_rule_from_document( 1156 "rate_limits.event.per_pubkey", 1157 document.event.per_pubkey, 1158 )?, 1159 base_relay_rate_limit_rule_from_document( 1160 "rate_limits.event.per_kind", 1161 document.event.per_kind, 1162 )?, 1163 ), 1164 TangleGroupRateLimitConfig::new( 1165 base_relay_rate_limit_rule_from_document( 1166 "rate_limits.group.write_per_ip", 1167 document.group.write_per_ip, 1168 )?, 1169 base_relay_rate_limit_rule_from_document( 1170 "rate_limits.group.write_per_pubkey", 1171 document.group.write_per_pubkey, 1172 )?, 1173 base_relay_rate_limit_rule_from_document( 1174 "rate_limits.group.write_per_group", 1175 document.group.write_per_group, 1176 )?, 1177 base_relay_rate_limit_rule_from_document( 1178 "rate_limits.group.write_per_kind", 1179 document.group.write_per_kind, 1180 )?, 1181 base_relay_rate_limit_rule_from_document( 1182 "rate_limits.group.join_flow", 1183 document.group.join_flow, 1184 )?, 1185 base_relay_rate_limit_rule_from_document( 1186 "rate_limits.group.join_flow_per_ip", 1187 document.group.join_flow_per_ip, 1188 )?, 1189 ), 1190 base_relay_query_rate_limits_from_document("rate_limits.req", document.req)?, 1191 base_relay_query_rate_limits_from_document("rate_limits.count", document.count)?, 1192 )) 1193 } 1194 1195 fn base_relay_query_rate_limits_from_document( 1196 field: &str, 1197 document: BaseRelayQueryRateLimitsDocument, 1198 ) -> Result<TangleQueryRateLimitConfig, BaseRelayError> { 1199 Ok(TangleQueryRateLimitConfig::new( 1200 base_relay_rate_limit_rule_from_document(&format!("{field}.per_ip"), document.per_ip)?, 1201 base_relay_rate_limit_rule_from_document( 1202 &format!("{field}.per_connection"), 1203 document.per_connection, 1204 )?, 1205 base_relay_rate_limit_rule_from_document( 1206 &format!("{field}.per_pubkey"), 1207 document.per_pubkey, 1208 )?, 1209 base_relay_rate_limit_rule_from_document( 1210 &format!("{field}.per_group"), 1211 document.per_group, 1212 )?, 1213 base_relay_rate_limit_rule_from_document(&format!("{field}.per_kind"), document.per_kind)?, 1214 base_relay_rate_limit_rule_from_document(&format!("{field}.broad"), document.broad)?, 1215 )) 1216 } 1217 1218 fn base_relay_rate_limit_rule_from_document( 1219 field: &str, 1220 document: BaseRelayRateLimitRuleDocument, 1221 ) -> Result<TangleRateLimitRule, BaseRelayError> { 1222 require_positive_u64(&format!("{field}.window_seconds"), document.window_seconds)?; 1223 require_positive_u64(&format!("{field}.max_hits"), document.max_hits)?; 1224 TangleRateLimitRule::new(document.window_seconds, document.max_hits) 1225 } 1226 1227 fn base_relay_tracing_config_from_document( 1228 document: BaseRelayTracingConfigDocument, 1229 ) -> Result<BaseRelayTracingConfig, BaseRelayError> { 1230 BaseRelayTracingConfig::new( 1231 document.enabled.unwrap_or(true), 1232 document.filter.unwrap_or_else(|| { 1233 "info,tangle=info,tangle_runtime=info,tangle_groups=info,tangle_store_pocket=info" 1234 .to_owned() 1235 }), 1236 match document 1237 .format 1238 .unwrap_or(BaseRelayTracingFormatDocument::Json) 1239 { 1240 BaseRelayTracingFormatDocument::Compact => BaseRelayTracingFormat::Compact, 1241 BaseRelayTracingFormatDocument::Json => BaseRelayTracingFormat::Json, 1242 }, 1243 ) 1244 } 1245 1246 fn validate_optional_text( 1247 field: &str, 1248 value: Option<String>, 1249 ) -> Result<Option<String>, BaseRelayError> { 1250 if let Some(value) = value { 1251 if value.trim().is_empty() || value.trim() != value { 1252 return Err(BaseRelayError::invalid(format!( 1253 "{field} must not be empty or padded" 1254 ))); 1255 } 1256 Ok(Some(value)) 1257 } else { 1258 Ok(None) 1259 } 1260 } 1261 1262 fn validate_tenant_config_set(tenants: &[TenantRuntimeConfig]) -> Result<(), BaseRelayError> { 1263 if tenants.iter().all(TenantRuntimeConfig::inactive) { 1264 return Err(BaseRelayError::invalid( 1265 "at least one active tenant is required", 1266 )); 1267 } 1268 let mut tenant_ids = BTreeSet::new(); 1269 let mut tenant_schemas = BTreeSet::new(); 1270 let mut hosts = BTreeSet::new(); 1271 let mut relay_urls = BTreeSet::new(); 1272 let mut relay_self_pubkeys = BTreeSet::new(); 1273 let mut store_paths = BTreeSet::new(); 1274 for tenant in tenants { 1275 insert_unique("tenant_id", tenant.tenant_id().as_str(), &mut tenant_ids)?; 1276 insert_unique( 1277 "tenant_schema", 1278 tenant.tenant_schema().as_str(), 1279 &mut tenant_schemas, 1280 )?; 1281 insert_unique("host", tenant.host().as_str(), &mut hosts)?; 1282 insert_unique("relay_url", tenant.relay_url().as_str(), &mut relay_urls)?; 1283 if let Some(pubkey) = tenant.relay_self_pubkey()? { 1284 insert_unique( 1285 "relay self pubkey", 1286 pubkey.as_str(), 1287 &mut relay_self_pubkeys, 1288 )?; 1289 } 1290 let store_path = canonical_path_key(tenant.pocket_config().data_directory()); 1291 insert_unique("pocket data directory", &store_path, &mut store_paths)?; 1292 } 1293 Ok(()) 1294 } 1295 1296 fn insert_unique( 1297 field: &str, 1298 value: impl Into<String>, 1299 values: &mut BTreeSet<String>, 1300 ) -> Result<(), BaseRelayError> { 1301 let value = value.into(); 1302 if values.insert(value.clone()) { 1303 Ok(()) 1304 } else { 1305 Err(BaseRelayError::invalid(format!( 1306 "duplicate tenant {field}: {value}" 1307 ))) 1308 } 1309 } 1310 1311 fn canonical_path_key(path: &Path) -> String { 1312 let mut normalized = PathBuf::new(); 1313 for component in path.components() { 1314 match component { 1315 Component::CurDir => {} 1316 Component::ParentDir => { 1317 normalized.pop(); 1318 } 1319 Component::Normal(part) => normalized.push(part), 1320 Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()), 1321 } 1322 } 1323 normalized.to_string_lossy().into_owned() 1324 } 1325 1326 #[cfg(test)] 1327 mod tests { 1328 use super::{ 1329 BaseRelayTracingFormat, TangleHostRuntimeConfigSet, TenantRuntimeConfig, 1330 parse_base_relay_runtime_config_json, parse_tangle_host_runtime_config_json, 1331 parse_tenant_runtime_config_json, 1332 }; 1333 use serde_json::{Value, json}; 1334 use std::path::Path; 1335 use tangle_store_pocket::PocketSyncPolicy; 1336 1337 #[test] 1338 fn tangle_host_runtime_config_parses_v1_mvp_example() { 1339 let config = parse_tangle_host_runtime_config_json(include_str!( 1340 "../../../config/tangle.host.example.json" 1341 )) 1342 .expect("host config"); 1343 1344 assert_eq!(config.listen_addr().to_string(), "0.0.0.0:7000"); 1345 assert_eq!(config.tenant_config_dir(), Path::new("tenants")); 1346 assert_eq!(config.limits().max_total_connections(), 10_000); 1347 assert_eq!(config.limits().max_total_subscriptions(), 25_000); 1348 assert_eq!(config.limits().tenant_startup_concurrency(), 4); 1349 assert!(config.ops().enabled()); 1350 assert!(config.ops().expose_tenant_inventory()); 1351 assert!(!config.trusted_proxy().enabled()); 1352 assert!(config.trusted_proxy().trusted_peers().is_empty()); 1353 assert!(config.tracing().enabled()); 1354 assert_eq!(config.tracing().format(), BaseRelayTracingFormat::Json); 1355 } 1356 1357 #[test] 1358 fn tenant_runtime_config_parses_v1_mvp_example() { 1359 let config = parse_tenant_runtime_config_json(include_str!( 1360 "../../../config/tenants/farmers_market.example.json" 1361 )) 1362 .expect("tenant config"); 1363 1364 assert_eq!(config.tenant_id().as_str(), "farmers-market"); 1365 assert_eq!(config.tenant_schema().as_str(), "farmers_market"); 1366 assert_eq!(config.host().as_str(), "relay.radroots.test"); 1367 assert_eq!(config.relay_url().as_str(), "wss://relay.radroots.test"); 1368 assert!(!config.inactive()); 1369 assert_eq!(config.info().name(), "Radroots Farmers Market"); 1370 assert_eq!( 1371 config.info().description(), 1372 Some("Tangle virtual relay tenant for the Radroots farmers market") 1373 ); 1374 assert_eq!( 1375 config.pocket_config().data_directory(), 1376 Path::new("runtime/tenants/farmers_market/pocket") 1377 ); 1378 assert_eq!( 1379 config.pocket_config().sync_policy(), 1380 PocketSyncPolicy::FlushOnShutdown 1381 ); 1382 assert!(config.groups().enabled()); 1383 assert_eq!(config.auth_ttl_seconds(), 300); 1384 assert_eq!(config.auth_created_at_skew_seconds(), 600); 1385 assert_eq!(config.limits().max_subscriptions_per_connection(), 64); 1386 assert_eq!(config.rate_limits().auth().per_ip().max_hits(), 120); 1387 assert!(config.backup_export().backup_enabled()); 1388 assert!(config.backup_export().export_enabled()); 1389 assert!(config.relay_self_pubkey().expect("relay self").is_some()); 1390 } 1391 1392 #[test] 1393 fn tangle_v1_mvp_config_rejects_legacy_single_relay_shape() { 1394 let raw = include_str!("../../../config/tangle.example.json"); 1395 1396 assert_eq!( 1397 parse_tangle_host_runtime_config_json(raw) 1398 .expect_err("legacy host config") 1399 .prefixed_message(), 1400 "invalid: legacy single-relay config is not supported" 1401 ); 1402 assert_eq!( 1403 parse_tenant_runtime_config_json(raw) 1404 .expect_err("legacy tenant config") 1405 .prefixed_message(), 1406 "invalid: legacy single-relay config is not supported" 1407 ); 1408 } 1409 1410 #[test] 1411 fn tangle_host_runtime_config_set_rejects_invalid_tenant_sets() { 1412 let host = parse_tangle_host_runtime_config_json(include_str!( 1413 "../../../config/tangle.host.example.json" 1414 )) 1415 .expect("host config"); 1416 let first = tenant_from_value(first_tenant_value()); 1417 let second = tenant_from_value(second_tenant_value()); 1418 TangleHostRuntimeConfigSet::new(host.clone(), vec![first.clone(), second.clone()]) 1419 .expect("unique tenants"); 1420 1421 assert!( 1422 TangleHostRuntimeConfigSet::new( 1423 host.clone(), 1424 vec![first.clone(), mutate_second("tenant_id", "farmers-market")] 1425 ) 1426 .expect_err("tenant id") 1427 .prefixed_message() 1428 .contains("duplicate tenant tenant_id") 1429 ); 1430 assert!( 1431 TangleHostRuntimeConfigSet::new( 1432 host.clone(), 1433 vec![ 1434 first.clone(), 1435 mutate_second("tenant_schema", "farmers_market") 1436 ] 1437 ) 1438 .expect_err("schema") 1439 .prefixed_message() 1440 .contains("duplicate tenant tenant_schema") 1441 ); 1442 assert!( 1443 TangleHostRuntimeConfigSet::new( 1444 host.clone(), 1445 vec![first.clone(), mutate_second("host", "relay.radroots.test")] 1446 ) 1447 .expect_err("host") 1448 .prefixed_message() 1449 .contains("duplicate tenant host") 1450 ); 1451 assert!( 1452 TangleHostRuntimeConfigSet::new( 1453 host.clone(), 1454 vec![ 1455 first.clone(), 1456 mutate_second("relay_url", "wss://relay.radroots.test") 1457 ] 1458 ) 1459 .expect_err("relay url") 1460 .prefixed_message() 1461 .contains("duplicate tenant relay_url") 1462 ); 1463 assert!( 1464 TangleHostRuntimeConfigSet::new( 1465 host.clone(), 1466 vec![first.clone(), second_with_group_secret("7")] 1467 ) 1468 .expect_err("relay self") 1469 .prefixed_message() 1470 .contains("duplicate tenant relay self pubkey") 1471 ); 1472 assert!( 1473 TangleHostRuntimeConfigSet::new( 1474 host.clone(), 1475 vec![ 1476 first.clone(), 1477 second_with_store_path("./runtime/tenants/farmers_market/pocket") 1478 ] 1479 ) 1480 .expect_err("store") 1481 .prefixed_message() 1482 .contains("duplicate tenant pocket data directory") 1483 ); 1484 assert_eq!( 1485 TangleHostRuntimeConfigSet::new( 1486 host, 1487 vec![inactive_first_tenant(), inactive_second_tenant()] 1488 ) 1489 .expect_err("active tenants") 1490 .prefixed_message(), 1491 "invalid: at least one active tenant is required" 1492 ); 1493 } 1494 1495 fn first_tenant_value() -> Value { 1496 serde_json::from_str(include_str!( 1497 "../../../config/tenants/farmers_market.example.json" 1498 )) 1499 .expect("tenant json") 1500 } 1501 1502 fn second_tenant_value() -> Value { 1503 let mut value = first_tenant_value(); 1504 value["tenant_id"] = json!("seed-coop"); 1505 value["tenant_schema"] = json!("seed_coop"); 1506 value["host"] = json!("seed.relay.radroots.test"); 1507 value["relay_url"] = json!("wss://seed.relay.radroots.test"); 1508 value["pocket"]["data_directory"] = json!("runtime/tenants/seed_coop/pocket"); 1509 value["groups"]["canonical_relay_url"] = json!("wss://seed.relay.radroots.test"); 1510 value["groups"]["relay_secret"] = 1511 json!("8888888888888888888888888888888888888888888888888888888888888888"); 1512 value 1513 } 1514 1515 fn tenant_from_value(value: Value) -> TenantRuntimeConfig { 1516 parse_tenant_runtime_config_json(&value.to_string()).expect("tenant") 1517 } 1518 1519 fn mutate_second(field: &str, field_value: &str) -> TenantRuntimeConfig { 1520 let mut value = second_tenant_value(); 1521 value[field] = json!(field_value); 1522 if field == "relay_url" { 1523 value["groups"]["canonical_relay_url"] = json!(field_value); 1524 } 1525 tenant_from_value(value) 1526 } 1527 1528 fn second_with_group_secret(nibble: &str) -> TenantRuntimeConfig { 1529 let mut value = second_tenant_value(); 1530 value["groups"]["relay_secret"] = json!(nibble.repeat(64)); 1531 tenant_from_value(value) 1532 } 1533 1534 fn second_with_store_path(path: &str) -> TenantRuntimeConfig { 1535 let mut value = second_tenant_value(); 1536 value["pocket"]["data_directory"] = json!(path); 1537 tenant_from_value(value) 1538 } 1539 1540 fn inactive_first_tenant() -> TenantRuntimeConfig { 1541 let mut value = first_tenant_value(); 1542 value["inactive"] = json!(true); 1543 tenant_from_value(value) 1544 } 1545 1546 fn inactive_second_tenant() -> TenantRuntimeConfig { 1547 let mut value = second_tenant_value(); 1548 value["inactive"] = json!(true); 1549 tenant_from_value(value) 1550 } 1551 1552 #[test] 1553 fn base_relay_runtime_config_parses_v2_production_example() { 1554 let config = parse_base_relay_runtime_config_json(include_str!( 1555 "../../../config/tangle.example.json" 1556 )) 1557 .expect("config"); 1558 1559 assert_eq!(config.listen_addr().to_string(), "0.0.0.0:7000"); 1560 assert_eq!(config.relay_url(), "wss://relay.radroots.test"); 1561 assert_eq!( 1562 config.pocket_config().data_directory(), 1563 Path::new("runtime/pocket") 1564 ); 1565 assert_eq!( 1566 config.pocket_config().sync_policy(), 1567 PocketSyncPolicy::FlushOnShutdown 1568 ); 1569 assert!(!config.pocket_query_config().allow_scraping()); 1570 assert_eq!( 1571 config.pocket_query_config().allow_scrape_if_limited_to(), 1572 100 1573 ); 1574 assert_eq!( 1575 config.pocket_query_config().allow_scrape_if_max_seconds(), 1576 3_600 1577 ); 1578 assert!(config.groups().enabled()); 1579 assert!(!config.groups().policy().public_join()); 1580 assert!(!config.groups().policy().invites_enabled()); 1581 assert_eq!(config.auth_ttl_seconds(), 300); 1582 assert_eq!(config.auth_created_at_skew_seconds(), 600); 1583 assert_eq!(config.limits().max_message_length(), 1_048_576); 1584 assert_eq!(config.limits().max_subid_length(), 64); 1585 assert_eq!(config.limits().max_subscriptions_per_connection(), 64); 1586 assert_eq!(config.limits().max_filters_per_request(), 10); 1587 assert_eq!(config.limits().max_tag_values_per_filter(), 100); 1588 assert_eq!(config.limits().max_query_complexity(), 2_048); 1589 assert_eq!(config.limits().max_limit(), 500); 1590 assert_eq!(config.limits().default_limit(), 100); 1591 assert_eq!(config.limits().max_event_tags(), 200); 1592 assert_eq!(config.limits().max_content_length(), 65_536); 1593 assert_eq!(config.limits().broadcast_channel_capacity(), 4_096); 1594 assert_eq!(config.limits().per_connection_outbound_queue(), 256); 1595 assert_eq!(config.rate_limits().auth().per_ip().max_hits(), 120); 1596 assert_eq!(config.rate_limits().auth().per_pubkey().max_hits(), 30); 1597 assert_eq!(config.rate_limits().auth().failures().max_hits(), 5); 1598 assert_eq!(config.rate_limits().auth().failures_per_ip().max_hits(), 20); 1599 assert_eq!(config.rate_limits().event().per_ip().max_hits(), 600); 1600 assert_eq!(config.rate_limits().event().per_pubkey().max_hits(), 120); 1601 assert_eq!(config.rate_limits().event().per_kind().max_hits(), 1_000); 1602 assert_eq!(config.rate_limits().group().write_per_ip().max_hits(), 300); 1603 assert_eq!( 1604 config.rate_limits().group().write_per_pubkey().max_hits(), 1605 60 1606 ); 1607 assert_eq!( 1608 config.rate_limits().group().write_per_group().max_hits(), 1609 90 1610 ); 1611 assert_eq!( 1612 config.rate_limits().group().write_per_kind().max_hits(), 1613 300 1614 ); 1615 assert_eq!(config.rate_limits().group().join_flow().max_hits(), 10); 1616 assert_eq!( 1617 config.rate_limits().group().join_flow_per_ip().max_hits(), 1618 30 1619 ); 1620 assert_eq!(config.rate_limits().req().per_ip().max_hits(), 600); 1621 assert_eq!(config.rate_limits().req().per_connection().max_hits(), 120); 1622 assert_eq!(config.rate_limits().req().per_pubkey().max_hits(), 240); 1623 assert_eq!(config.rate_limits().req().per_group().max_hits(), 240); 1624 assert_eq!(config.rate_limits().req().per_kind().max_hits(), 500); 1625 assert_eq!(config.rate_limits().req().broad().max_hits(), 30); 1626 assert_eq!(config.rate_limits().count().per_ip().max_hits(), 300); 1627 assert_eq!(config.rate_limits().count().per_connection().max_hits(), 60); 1628 assert_eq!(config.rate_limits().count().per_pubkey().max_hits(), 120); 1629 assert_eq!(config.rate_limits().count().per_group().max_hits(), 120); 1630 assert_eq!(config.rate_limits().count().per_kind().max_hits(), 240); 1631 assert_eq!(config.rate_limits().count().broad().max_hits(), 20); 1632 assert!(config.tracing().enabled()); 1633 assert_eq!(config.tracing().format(), BaseRelayTracingFormat::Json); 1634 config.auth_state().expect("auth"); 1635 } 1636 1637 #[test] 1638 fn base_relay_runtime_config_rejects_zero_auth_skew() { 1639 let raw = r#"{ 1640 "server": { 1641 "listen_addr": "127.0.0.1:0", 1642 "relay_url": "wss://relay.radroots.test" 1643 }, 1644 "pocket": { 1645 "data_directory": "runtime/pocket", 1646 "sync_policy": "flush_on_shutdown", 1647 "query": { 1648 "allow_scraping": false, 1649 "allow_scrape_if_limited_to": 100, 1650 "allow_scrape_if_max_seconds": 3600 1651 } 1652 }, 1653 "groups": { 1654 "enabled": false 1655 }, 1656 "auth": { 1657 "challenge_ttl_seconds": 300, 1658 "created_at_skew_seconds": 0 1659 }, 1660 "limits": { 1661 "max_message_length": 1048576, 1662 "max_subid_length": 64, 1663 "max_subscriptions_per_connection": 64, 1664 "max_filters_per_request": 10, 1665 "max_tag_values_per_filter": 100, 1666 "max_query_complexity": 2048, 1667 "max_limit": 500, 1668 "default_limit": 100, 1669 "max_event_tags": 200, 1670 "max_content_length": 65536, 1671 "broadcast_channel_capacity": 4096, 1672 "per_connection_outbound_queue": 256 1673 }, 1674 "rate_limits": { 1675 "auth": { 1676 "per_ip": {"window_seconds": 60, "max_hits": 120}, 1677 "per_pubkey": {"window_seconds": 60, "max_hits": 30}, 1678 "failures": {"window_seconds": 300, "max_hits": 5}, 1679 "failures_per_ip": {"window_seconds": 300, "max_hits": 20} 1680 }, 1681 "event": { 1682 "per_ip": {"window_seconds": 60, "max_hits": 600}, 1683 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 1684 "per_kind": {"window_seconds": 60, "max_hits": 1000} 1685 }, 1686 "group": { 1687 "write_per_ip": {"window_seconds": 60, "max_hits": 300}, 1688 "write_per_pubkey": {"window_seconds": 60, "max_hits": 60}, 1689 "write_per_group": {"window_seconds": 60, "max_hits": 90}, 1690 "write_per_kind": {"window_seconds": 60, "max_hits": 300}, 1691 "join_flow": {"window_seconds": 300, "max_hits": 10}, 1692 "join_flow_per_ip": {"window_seconds": 300, "max_hits": 30} 1693 }, 1694 "req": { 1695 "per_ip": {"window_seconds": 60, "max_hits": 600}, 1696 "per_connection": {"window_seconds": 60, "max_hits": 120}, 1697 "per_pubkey": {"window_seconds": 60, "max_hits": 240}, 1698 "per_group": {"window_seconds": 60, "max_hits": 240}, 1699 "per_kind": {"window_seconds": 60, "max_hits": 500}, 1700 "broad": {"window_seconds": 60, "max_hits": 30} 1701 }, 1702 "count": { 1703 "per_ip": {"window_seconds": 60, "max_hits": 300}, 1704 "per_connection": {"window_seconds": 60, "max_hits": 60}, 1705 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 1706 "per_group": {"window_seconds": 60, "max_hits": 120}, 1707 "per_kind": {"window_seconds": 60, "max_hits": 240}, 1708 "broad": {"window_seconds": 60, "max_hits": 20} 1709 } 1710 } 1711 }"#; 1712 1713 assert_eq!( 1714 parse_base_relay_runtime_config_json(raw) 1715 .expect_err("zero skew") 1716 .prefixed_message(), 1717 "invalid: auth.created_at_skew_seconds must be greater than zero" 1718 ); 1719 } 1720 1721 #[test] 1722 fn base_relay_runtime_config_rejects_unknown_fields() { 1723 let unknown_top_level = r#"{ 1724 "server": { 1725 "listen_addr": "127.0.0.1:0", 1726 "relay_url": "wss://relay.radroots.test" 1727 }, 1728 "pocket": { 1729 "data_directory": "runtime/pocket", 1730 "sync_policy": "flush_on_shutdown", 1731 "query": { 1732 "allow_scraping": false, 1733 "allow_scrape_if_limited_to": 100, 1734 "allow_scrape_if_max_seconds": 3600 1735 } 1736 }, 1737 "groups": { 1738 "enabled": false 1739 }, 1740 "auth": { 1741 "challenge_ttl_seconds": 300, 1742 "created_at_skew_seconds": 600 1743 }, 1744 "limits": { 1745 "max_message_length": 1048576, 1746 "max_subid_length": 64, 1747 "max_subscriptions_per_connection": 64, 1748 "max_filters_per_request": 10, 1749 "max_tag_values_per_filter": 100, 1750 "max_query_complexity": 2048, 1751 "max_limit": 500, 1752 "default_limit": 100, 1753 "max_event_tags": 200, 1754 "max_content_length": 65536, 1755 "broadcast_channel_capacity": 4096, 1756 "per_connection_outbound_queue": 256 1757 }, 1758 "rate_limits": { 1759 "auth": { 1760 "per_ip": {"window_seconds": 60, "max_hits": 120}, 1761 "per_pubkey": {"window_seconds": 60, "max_hits": 30}, 1762 "failures": {"window_seconds": 300, "max_hits": 5}, 1763 "failures_per_ip": {"window_seconds": 300, "max_hits": 20} 1764 }, 1765 "event": { 1766 "per_ip": {"window_seconds": 60, "max_hits": 600}, 1767 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 1768 "per_kind": {"window_seconds": 60, "max_hits": 1000} 1769 }, 1770 "group": { 1771 "write_per_ip": {"window_seconds": 60, "max_hits": 300}, 1772 "write_per_pubkey": {"window_seconds": 60, "max_hits": 60}, 1773 "write_per_group": {"window_seconds": 60, "max_hits": 90}, 1774 "write_per_kind": {"window_seconds": 60, "max_hits": 300}, 1775 "join_flow": {"window_seconds": 300, "max_hits": 10}, 1776 "join_flow_per_ip": {"window_seconds": 300, "max_hits": 30} 1777 }, 1778 "req": { 1779 "per_ip": {"window_seconds": 60, "max_hits": 600}, 1780 "per_connection": {"window_seconds": 60, "max_hits": 120}, 1781 "per_pubkey": {"window_seconds": 60, "max_hits": 240}, 1782 "per_group": {"window_seconds": 60, "max_hits": 240}, 1783 "per_kind": {"window_seconds": 60, "max_hits": 500}, 1784 "broad": {"window_seconds": 60, "max_hits": 30} 1785 }, 1786 "count": { 1787 "per_ip": {"window_seconds": 60, "max_hits": 300}, 1788 "per_connection": {"window_seconds": 60, "max_hits": 60}, 1789 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 1790 "per_group": {"window_seconds": 60, "max_hits": 120}, 1791 "per_kind": {"window_seconds": 60, "max_hits": 240}, 1792 "broad": {"window_seconds": 60, "max_hits": 20} 1793 } 1794 }, 1795 "ignored": true 1796 }"#; 1797 assert!( 1798 parse_base_relay_runtime_config_json(unknown_top_level) 1799 .expect_err("unknown top-level field") 1800 .prefixed_message() 1801 .contains("unknown field `ignored`") 1802 ); 1803 1804 let unknown_nested = r#"{ 1805 "server": { 1806 "listen_addr": "127.0.0.1:0", 1807 "relay_url": "wss://relay.radroots.test" 1808 }, 1809 "pocket": { 1810 "data_directory": "runtime/pocket", 1811 "sync_policy": "flush_on_shutdown", 1812 "query": { 1813 "allow_scraping": false, 1814 "allow_scrape_if_limited_to": 100, 1815 "allow_scrape_if_max_seconds": 3600 1816 } 1817 }, 1818 "groups": { 1819 "enabled": false 1820 }, 1821 "auth": { 1822 "challenge_ttl_seconds": 300, 1823 "created_at_skew_seconds": 600 1824 }, 1825 "limits": { 1826 "max_message_length": 1048576, 1827 "max_subid_length": 64, 1828 "max_subscriptions_per_connection": 64, 1829 "max_filters_per_request": 10, 1830 "max_tag_values_per_filter": 100, 1831 "max_query_complexity": 2048, 1832 "max_limit": 500, 1833 "default_limit": 100, 1834 "max_event_tags": 200, 1835 "max_content_length": 65536, 1836 "broadcast_channel_capacity": 4096, 1837 "per_connection_outbound_queue": 256, 1838 "max_unimplemented_limit": 99 1839 }, 1840 "rate_limits": { 1841 "auth": { 1842 "per_ip": {"window_seconds": 60, "max_hits": 120}, 1843 "per_pubkey": {"window_seconds": 60, "max_hits": 30}, 1844 "failures": {"window_seconds": 300, "max_hits": 5}, 1845 "failures_per_ip": {"window_seconds": 300, "max_hits": 20} 1846 }, 1847 "event": { 1848 "per_ip": {"window_seconds": 60, "max_hits": 600}, 1849 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 1850 "per_kind": {"window_seconds": 60, "max_hits": 1000} 1851 }, 1852 "group": { 1853 "write_per_ip": {"window_seconds": 60, "max_hits": 300}, 1854 "write_per_pubkey": {"window_seconds": 60, "max_hits": 60}, 1855 "write_per_group": {"window_seconds": 60, "max_hits": 90}, 1856 "write_per_kind": {"window_seconds": 60, "max_hits": 300}, 1857 "join_flow": {"window_seconds": 300, "max_hits": 10}, 1858 "join_flow_per_ip": {"window_seconds": 300, "max_hits": 30} 1859 }, 1860 "req": { 1861 "per_ip": {"window_seconds": 60, "max_hits": 600}, 1862 "per_connection": {"window_seconds": 60, "max_hits": 120}, 1863 "per_pubkey": {"window_seconds": 60, "max_hits": 240}, 1864 "per_group": {"window_seconds": 60, "max_hits": 240}, 1865 "per_kind": {"window_seconds": 60, "max_hits": 500}, 1866 "broad": {"window_seconds": 60, "max_hits": 30} 1867 }, 1868 "count": { 1869 "per_ip": {"window_seconds": 60, "max_hits": 300}, 1870 "per_connection": {"window_seconds": 60, "max_hits": 60}, 1871 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 1872 "per_group": {"window_seconds": 60, "max_hits": 120}, 1873 "per_kind": {"window_seconds": 60, "max_hits": 240}, 1874 "broad": {"window_seconds": 60, "max_hits": 20} 1875 } 1876 } 1877 }"#; 1878 assert!( 1879 parse_base_relay_runtime_config_json(unknown_nested) 1880 .expect_err("unknown nested field") 1881 .prefixed_message() 1882 .contains("unknown field `max_unimplemented_limit`") 1883 ); 1884 } 1885 1886 #[test] 1887 fn base_relay_runtime_config_rejects_removed_pocket_options() { 1888 let raw = include_str!("../../../config/tangle.example.json").replace( 1889 " \"data_directory\": \"runtime/pocket\",\n", 1890 " \"data_directory\": \"runtime/pocket\",\n \"map_size_bytes\": 1073741824,\n", 1891 ); 1892 assert!( 1893 parse_base_relay_runtime_config_json(&raw) 1894 .expect_err("removed map size") 1895 .prefixed_message() 1896 .contains("unknown field `map_size_bytes`") 1897 ); 1898 1899 let raw = include_str!("../../../config/tangle.example.json").replace( 1900 " \"data_directory\": \"runtime/pocket\",\n", 1901 " \"data_directory\": \"runtime/pocket\",\n \"reader_slots\": 128,\n", 1902 ); 1903 assert!( 1904 parse_base_relay_runtime_config_json(&raw) 1905 .expect_err("removed readers") 1906 .prefixed_message() 1907 .contains("unknown field `reader_slots`") 1908 ); 1909 } 1910 1911 #[test] 1912 fn base_relay_runtime_config_validates_pocket_query_controls() { 1913 let raw = include_str!("../../../config/tangle.example.json").replace( 1914 " \"allow_scrape_if_limited_to\": 100,\n", 1915 " \"allow_scrape_if_limited_to\": 501,\n", 1916 ); 1917 assert_eq!( 1918 parse_base_relay_runtime_config_json(&raw) 1919 .expect_err("query scrape limit") 1920 .prefixed_message(), 1921 "invalid: pocket.query.allow_scrape_if_limited_to must be less than or equal to limits.max_limit" 1922 ); 1923 1924 let raw = include_str!("../../../config/tangle.example.json").replace( 1925 " \"allow_scrape_if_max_seconds\": 3600\n", 1926 " \"allow_scrape_if_max_seconds\": 86401\n", 1927 ); 1928 assert_eq!( 1929 parse_base_relay_runtime_config_json(&raw) 1930 .expect_err("query scrape window") 1931 .prefixed_message(), 1932 "invalid: pocket.query.allow_scrape_if_max_seconds must be less than or equal to 86400" 1933 ); 1934 } 1935 1936 #[test] 1937 fn base_relay_runtime_config_requires_explicit_query_complexity() { 1938 let raw = include_str!("../../../config/tangle.example.json") 1939 .replace(" \"max_query_complexity\": 2048,\n", ""); 1940 assert!( 1941 parse_base_relay_runtime_config_json(&raw) 1942 .expect_err("missing query complexity") 1943 .prefixed_message() 1944 .contains("missing field `max_query_complexity`") 1945 ); 1946 } 1947 1948 #[test] 1949 fn base_relay_runtime_config_requires_ip_scoped_rate_limits() { 1950 let raw = include_str!("../../../config/tangle.example.json").replace( 1951 " \"per_ip\": {\n \"window_seconds\": 60,\n \"max_hits\": 120\n },\n", 1952 "", 1953 ); 1954 assert!( 1955 parse_base_relay_runtime_config_json(&raw) 1956 .expect_err("missing auth ip") 1957 .prefixed_message() 1958 .contains("missing field `per_ip`") 1959 ); 1960 1961 let raw = include_str!("../../../config/tangle.example.json").replace( 1962 " \"failures\": {\n \"window_seconds\": 300,\n \"max_hits\": 5\n },\n \"failures_per_ip\": {\n \"window_seconds\": 300,\n \"max_hits\": 20\n }\n", 1963 " \"failures\": {\n \"window_seconds\": 300,\n \"max_hits\": 5\n }\n", 1964 ); 1965 assert!( 1966 parse_base_relay_runtime_config_json(&raw) 1967 .expect_err("missing auth failure ip") 1968 .prefixed_message() 1969 .contains("missing field `failures_per_ip`") 1970 ); 1971 1972 let raw = include_str!("../../../config/tangle.example.json").replace( 1973 " \"per_ip\": {\n \"window_seconds\": 60,\n \"max_hits\": 600\n },\n", 1974 "", 1975 ); 1976 assert!( 1977 parse_base_relay_runtime_config_json(&raw) 1978 .expect_err("missing event ip") 1979 .prefixed_message() 1980 .contains("missing field `per_ip`") 1981 ); 1982 1983 let raw = include_str!("../../../config/tangle.example.json").replace( 1984 " \"write_per_ip\": {\n \"window_seconds\": 60,\n \"max_hits\": 300\n },\n", 1985 "", 1986 ); 1987 assert!( 1988 parse_base_relay_runtime_config_json(&raw) 1989 .expect_err("missing group write ip") 1990 .prefixed_message() 1991 .contains("missing field `write_per_ip`") 1992 ); 1993 1994 let raw = include_str!("../../../config/tangle.example.json").replace( 1995 " \"join_flow\": {\n \"window_seconds\": 300,\n \"max_hits\": 10\n },\n \"join_flow_per_ip\": {\n \"window_seconds\": 300,\n \"max_hits\": 30\n }\n", 1996 " \"join_flow\": {\n \"window_seconds\": 300,\n \"max_hits\": 10\n }\n", 1997 ); 1998 assert!( 1999 parse_base_relay_runtime_config_json(&raw) 2000 .expect_err("missing group join ip") 2001 .prefixed_message() 2002 .contains("missing field `join_flow_per_ip`") 2003 ); 2004 } 2005 }