This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-server / src / main.rs
31 kB 799 lines
1mod allocator; 2 3#[global_allocator] 4static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 5 6#[allow(non_upper_case_globals)] 7#[unsafe(export_name = "_rjem_malloc_conf")] 8pub static malloc_conf: &[u8] = 9 b"background_thread:true,retain:false,dirty_decay_ms:0,muzzy_decay_ms:0\0"; 10 11use std::collections::BTreeSet; 12use std::num::{NonZeroU32, NonZeroU64}; 13use std::path::{Path, PathBuf}; 14use std::sync::Arc; 15use std::time::Duration; 16 17use tokio_util::sync::CancellationToken; 18 19use anyhow::Context; 20use axum::Json; 21use axum::response::Html; 22use axum::routing::get; 23use base64::Engine; 24use knot_atproto::Atproto; 25use knot_config::HomepageSource; 26use knot_index::Index; 27use knot_runtime::{Clock, HttpTransport, OsEntropy, ReqwestHttp, SystemClock}; 28use knot_secrets::{MasterKey, SealedStore}; 29use knot_types::{ActorId, AuthorName, BranchName, CiLogsAddr, Email, KnotHostname, ObjectCount}; 30use knot_xrpc::XrpcState; 31use tower_http::services::ServeFile; 32 33const MAINTENANCE_SHUTDOWN_DRAIN: Duration = Duration::from_secs(30); 34const EDGE_SHUTDOWN_DRAIN: Duration = Duration::from_secs(40); 35 36const DEFAULT_HOMEPAGE: &str = include_str!("homepage.html"); 37 38struct IndexRepos(Arc<Index>); 39 40impl knot_maintenance::RepoSource for IndexRepos { 41 fn repos(&self) -> Vec<knot_types::RepoDid> { 42 self.0.hosted_repos() 43 } 44 45 fn ready_repos(&self) -> Option<Vec<knot_types::RepoDid>> { 46 match self.0.coverage().registry { 47 knot_index::Coverage::Ready => Some(self.0.hosted_repos()), 48 knot_index::Coverage::Warming => None, 49 } 50 } 51} 52 53struct AtprotoHandleResolver<H, C> { 54 atproto: Arc<Atproto<H, C>>, 55} 56 57impl<H: HttpTransport, C: Clock> knot_pack::HandleResolver for AtprotoHandleResolver<H, C> { 58 fn resolve( 59 &self, 60 handle: knot_types::Handle, 61 ) -> std::pin::Pin< 62 Box<dyn std::future::Future<Output = Option<knot_types::AccountDid>> + Send + '_>, 63 > { 64 Box::pin(async move { self.atproto.resolve_handle_to_did(&handle).await.ok() }) 65 } 66} 67 68fn init_tracing() { 69 let filter = tracing_subscriber::EnvFilter::try_from_default_env() 70 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); 71 tracing_subscriber::fmt() 72 .with_env_filter(filter) 73 .with_writer(std::io::stderr) 74 .init(); 75} 76 77const VALIDATE_CONFIG_ONLY: &str = "--config-only"; 78 79enum ValidateScope { 80 ConfigOnly, 81 Environment, 82} 83 84impl ValidateScope { 85 fn verify(self, config: &knot_config::Validated) -> anyhow::Result<()> { 86 match self { 87 Self::ConfigOnly => Ok(()), 88 Self::Environment => config 89 .verify_environment() 90 .context("verify runtime environment"), 91 } 92 } 93} 94 95fn validate(args: impl Iterator<Item = String>) -> anyhow::Result<()> { 96 let (flags, paths): (Vec<String>, Vec<String>) = args.partition(|arg| arg.starts_with("--")); 97 flags 98 .iter() 99 .find(|flag| flag.as_str() != VALIDATE_CONFIG_ONLY) 100 .map_or(Ok(()), |unknown| { 101 Err(anyhow::anyhow!( 102 "unrecognized flag {unknown}, expected {VALIDATE_CONFIG_ONLY}" 103 )) 104 })?; 105 let scope = match flags.is_empty() { 106 true => ValidateScope::Environment, 107 false => ValidateScope::ConfigOnly, 108 }; 109 let path = match paths.as_slice() { 110 [] => None, 111 [path] => Some(Path::new(path)), 112 extra => anyhow::bail!( 113 "expected at most one configuration path, got {}", 114 extra.len() 115 ), 116 }; 117 let config = knot_config::load(path).context("load configuration")?; 118 scope.verify(&config)?; 119 println!("configuration is valid"); 120 Ok(()) 121} 122 123fn subcommand(name: &str) -> Option<anyhow::Result<()>> { 124 match name { 125 "config-template" => { 126 print!("{}", knot_config::template()); 127 Some(Ok(())) 128 } 129 "validate" => Some(validate(std::env::args().skip(2))), 130 _ => None, 131 } 132} 133 134#[tokio::main] 135async fn main() -> anyhow::Result<()> { 136 #[cfg(target_os = "linux")] 137 rustix::process::set_dumpable_behavior(rustix::process::DumpableBehavior::NotDumpable) 138 .context("disable core dumps and ptrace attachment")?; 139 140 if let Some(result) = std::env::args().nth(1).as_deref().and_then(subcommand) { 141 return result; 142 } 143 144 init_tracing(); 145 146 tracing::info!("!"); 147 tracing::info!("!"); 148 tracing::info!("!"); 149 tracing::info!("> If knot1 was so good then why isn't there a... ( ˶°ㅁ°)"); 150 tracing::info!("..."); 151 tracing::info!("Welcome to knot2!"); 152 tracing::info!("This code was made with love."); 153 tracing::info!("Hachapuri is sho tasty, definitely worth a try. Better than pizza tbh."); 154 tracing::info!("!"); 155 tracing::info!("!"); 156 tracing::info!("!"); 157 158 let config_path = std::env::args().nth(1).map(PathBuf::from); 159 let config = knot_config::load(config_path.as_deref()).context("load configuration")?; 160 config 161 .verify_environment() 162 .context("verify runtime environment")?; 163 164 let resources = knot_resource::init(knot_resource::Ceilings { 165 max_threads: match config.resources.max_threads { 166 0 => None, 167 n => Some(knot_resource::ThreadCount::new(n as usize)), 168 }, 169 max_memory: match config.resources.max_memory_bytes { 170 0 => None, 171 n => Some(knot_resource::MemoryBudget::new(n)), 172 }, 173 }); 174 tracing::info!( 175 threads = resources.threads.get(), 176 memory_bytes = resources.memory.map(knot_resource::MemoryBudget::get), 177 memory_source = ?resources.memory_source, 178 memory_high_bytes = resources 179 .memory_high_bytes 180 .map(knot_resource::MemoryHighBytes::get), 181 "resource governor initialized" 182 ); 183 allocator::install(); 184 185 // I made the hostname stay aa string in the config, 186 // so that confique can layer env-over-file. 187 // Here's where it becomes a real type, 188 // and the DID + the service url stack on this. 189 let hostname = KnotHostname::new(config.server.hostname.clone()) 190 .context("server.hostname isn't a valid knot hostname")?; 191 let knot_did = hostname.knot_did(); 192 let object_format = config.object_format().context("parse git.object_format")?; 193 let default_branch = 194 BranchName::new(config.repo.default_branch.as_str()).context("parse default branch")?; 195 let layout = knot_git::Layout::new(&config.repo.scan_path) 196 .with_default_branch(default_branch) 197 .with_object_format(object_format) 198 .reserving_meta(&knot_did) 199 .context("reserve meta-repo path")?; 200 201 let swept = knot_pack::sweep_incoming(&config.repo.scan_path); 202 if swept > 0 { 203 tracing::info!(swept, "swept abandoned receive staging directories"); 204 } 205 206 layout 207 .bootstrap_meta(&knot_did) 208 .context("bootstrap meta-repo")?; 209 let meta_path = layout 210 .meta_path(&knot_did) 211 .context("resolve meta-repo path")?; 212 213 let index = Arc::new(Index::new(meta_path.clone(), layout.clone())); 214 index.rebuild().context("rebuild index from meta-repo")?; 215 tracing::info!(coverage = ?index.coverage(), "index ready"); 216 217 let warm = Arc::clone(&index); 218 tokio::task::spawn_blocking(move || warm.warm_collaborators()); 219 220 let http = ReqwestHttp::new(config.http_limits()).context("build outbound HTTP client")?; 221 let git_http: Arc<dyn knot_runtime::HttpTransport> = Arc::new( 222 ReqwestHttp::new(config.fork_http_limits()).context("build outbound git fetch client")?, 223 ); 224 let atproto = Arc::new(Atproto::new( 225 http, 226 SystemClock, 227 knot_did.clone(), 228 knot_atproto::PlcDirectory::new(config.atproto.plc_directory.clone()) 229 .context("atproto.plc_directory isn't a valid PLC base URL")?, 230 )); 231 let admins: BTreeSet<_> = config.server.admins.iter().cloned().collect(); 232 let admission = config.acl.admission; 233 let service_owner = config 234 .server 235 .admins 236 .first() 237 .cloned() 238 .context("at least one admin is configured")?; 239 240 let master_key = MasterKey::new( 241 base64::engine::general_purpose::STANDARD 242 .decode( 243 std::env::var(&config.secrets.master_key_env) 244 .context("read master key from environment")? 245 .trim(), 246 ) 247 .context("decode master key as base64")?, 248 ) 249 .context("master key from environment")?; 250 let secrets = Arc::new( 251 SealedStore::open( 252 &config.secrets.sealed_key_file, 253 &master_key, 254 Box::new(OsEntropy), 255 ) 256 .context("open sealed key store")?, 257 ); 258 let knot_signing_key = secrets 259 .ensure(&knot_did) 260 .context("seal knot's own signing key")?; 261 let knot_actor = ActorId::from_secp256k1(knot_signing_key.as_bytes()); 262 let appview_endpoint = config.server.appview_endpoint.clone(); 263 let knot_service_url = knot_types::KnotServiceUrl::new(format!("https://{hostname}")) 264 .context("server.hostname doesn't form a valid knot service URL")?; 265 let did_document = 266 knot_atproto::knot_did_document(&knot_did, &knot_signing_key, &knot_service_url); 267 268 let http_addr = config.server.listen_addr; 269 let listen_limits = knot_edge::ListenLimits::new( 270 knot_edge::HeaderTimeout::from_millis( 271 NonZeroU64::new(config.server.listen_header_timeout_ms) 272 .context("server.listen_header_timeout_ms must be greater than zero")?, 273 ), 274 knot_edge::IdleTimeout::from_millis( 275 NonZeroU64::new(config.server.listen_idle_timeout_ms) 276 .context("server.listen_idle_timeout_ms must be greater than zero")?, 277 ), 278 NonZeroU32::new(config.server.listen_max_connections) 279 .context("server.listen_max_connections must be greater than zero")?, 280 ); 281 // A header name that doesn't parse will never match, 282 // `ProxyTrust::client_peer` falls back to socket, 283 // and every request in the world shares 284 // the proxy's address + its one ratelimit bucket. 285 // So... better to refuse to start. 286 let trusted_proxy_header = config 287 .xrpc 288 .trusted_proxy_header 289 .as_deref() 290 .map(|header| axum::http::HeaderName::from_bytes(header.as_bytes())) 291 .transpose() 292 .context("xrpc.trusted_proxy_header isn't a valid HTTP header name")?; 293 let trusted_proxies = 294 knot_types::TrustedProxies::new(config.xrpc.trusted_proxies.iter().copied()); 295 let proxy_trust = knot_types::ProxyTrust::new(trusted_proxy_header, trusted_proxies); 296 if proxy_trust.trusts_any_peer() && !http_addr.ip().is_loopback() { 297 tracing::warn!( 298 bind = %http_addr, 299 "xrpc.trusted_proxy_header is set without xrpc.trusted_proxies while the HTTP surface takes connections from off-host, so a client that reaches this knot without passing the proxy can forge the header and pick its own rate-limit bucket. List the proxy's address in xrpc.trusted_proxies." 300 ); 301 } 302 let edge_guards = knot_edge::EdgeGuards::new( 303 knot_edge::RequestsPerSecond::new( 304 NonZeroU32::new(config.server.listen_rate_limit_per_second) 305 .context("server.listen_rate_limit_per_second must be greater than zero")?, 306 ), 307 knot_edge::BurstSize::new( 308 NonZeroU32::new(config.server.listen_rate_limit_burst) 309 .context("server.listen_rate_limit_burst must be greater than zero")?, 310 ), 311 knot_edge::MaxInflightRequests::new( 312 NonZeroU32::new(config.server.listen_max_inflight_requests) 313 .context("server.listen_max_inflight_requests must be greater than zero")?, 314 ), 315 knot_edge::RequestTimeout::from_millis( 316 NonZeroU64::new(config.server.listen_request_timeout_ms) 317 .context("server.listen_request_timeout_ms must be greater than zero")?, 318 ), 319 knot_edge::BodyInactivityTimeout::from_millis( 320 NonZeroU64::new(config.server.listen_body_timeout_ms) 321 .context("server.listen_body_timeout_ms must be greater than zero")?, 322 ), 323 knot_edge::WriteRequestTimeout::from_millis( 324 NonZeroU64::new(config.server.listen_write_request_timeout_ms) 325 .context("server.listen_write_request_timeout_ms must be greater than zero")?, 326 ), 327 proxy_trust.clone(), 328 ); 329 let tls_setup = build_tls_setup(&config, &hostname).context("assemble TLS configuration")?; 330 if config.tls.http3 && tls_setup.is_none() { 331 tracing::warn!( 332 "tls.http3 is set without any TLS certificate, so HTTP/3 won't start. Configure a static cert or ACME to serve h3." 333 ); 334 } 335 if tls_setup.is_none() && config.xrpc.trusted_proxy_header.is_none() { 336 tracing::warn!( 337 "running plaintext behind a reverse proxy without xrpc.trusted_proxy_header. Per-IP rate limiting will key on the proxy socket address, throttling all clients as one. Set xrpc.trusted_proxy_header to the header your proxy appends, and xrpc.trusted_proxies to the address it connects from." 338 ); 339 } 340 if config.tls.acme_enabled && http_addr.port() != 443 { 341 tracing::warn!( 342 listen_port = http_addr.port(), 343 "ACME validation over TLS-ALPN-01 needs the certificate authority to reach this host on TCP 443. Map 443 to the listen port if it differs." 344 ); 345 } 346 if config.tls.acme_enabled && config.tls.acme_staging { 347 tracing::warn!( 348 "ACME is using the Let's Encrypt staging directory. Its certificates aren't browser-trusted. Unset tls.acme_staging for real certificates." 349 ); 350 } 351 let ssh_addr = config.server.ssh_listen_addr; 352 let ssh_max_pack_bytes = config.server.ssh_max_pack_bytes as usize; 353 let pack_limits = knot_pack::PackLimits { 354 max_objects: ObjectCount::from(config.pack.max_objects), 355 max_total_bytes: knot_pack::MaxTotalBytes::new(config.pack.max_total_bytes), 356 ..knot_pack::PackLimits::default() 357 }; 358 knot_pack::init_selection_limits(knot_pack::SelectionLimits { 359 max_objects: ObjectCount::from(config.pack.selection_max_objects), 360 time_budget: Duration::from_secs(config.pack.selection_time_budget_secs), 361 }); 362 let host_key = knot_ssh::load_or_create_host_key(&config.server.ssh_host_key_file) 363 .context("load or create SSH host key")?; 364 365 let xrpc_limits = knot_xrpc::LimitConfig { 366 rate: Some(knot_xrpc::RateLimit { 367 burst: knot_xrpc::Burst::new(config.xrpc.preauth_burst), 368 refill: knot_xrpc::RefillMicros::new( 369 config.xrpc.preauth_refill_ms.saturating_mul(1_000), 370 ), 371 }), 372 per_peer_inflight: Some(knot_xrpc::PerPeerInflight::new( 373 config.xrpc.per_peer_inflight as usize, 374 )), 375 global_inflight: Some(knot_xrpc::GlobalInflight::new( 376 config.xrpc.global_inflight as usize, 377 )), 378 }; 379 let byte_limits = knot_xrpc::ByteLimits { 380 body: knot_xrpc::BodyLimit::new(config.xrpc.max_body_bytes as usize), 381 patch: knot_xrpc::PatchLimit::new(config.xrpc.max_patch_bytes as usize), 382 patch_decompressed: knot_xrpc::PatchDecompressedLimit::new( 383 config.xrpc.max_patch_decompressed_bytes, 384 ), 385 response: knot_xrpc::ResponseLimit::new(config.xrpc.max_response_bytes as usize), 386 archive: knot_xrpc::ArchiveLimit::new(config.xrpc.max_archive_bytes), 387 fork_pack: knot_xrpc::ForkPackLimit::new(config.xrpc.fork_max_pack_bytes), 388 pack: knot_xrpc::MaxWireBytes::new(ssh_max_pack_bytes), 389 }; 390 let budgets = knot_xrpc::Budgets { 391 tree_last_commit: knot_xrpc::TreeReadBudget::new(knot_xrpc::ReadBudget::Within( 392 Duration::from_millis(config.xrpc.tree_last_commit_budget_ms), 393 )), 394 blob_last_commit: knot_xrpc::BlobReadBudget::new(knot_xrpc::ReadBudget::Within( 395 Duration::from_millis(config.xrpc.blob_last_commit_budget_ms), 396 )), 397 languages: knot_xrpc::LanguagesReadBudget::new(knot_xrpc::ReadBudget::Within( 398 Duration::from_millis(config.xrpc.languages_budget_ms), 399 )), 400 languages_push: knot_xrpc::LanguagesPushBudget::new(Duration::from_millis( 401 config.xrpc.languages_push_budget_ms, 402 )), 403 }; 404 let committer = knot_xrpc::Committer { 405 name: AuthorName::new(config.git.user_name.clone()), 406 email: Email::new(config.git.user_email.clone()), 407 }; 408 let reservations = Arc::new(knot_xrpc::Reservations::new( 409 knot_xrpc::ReservationTtl::new(config.xrpc.reservation_ttl_secs as i64), 410 knot_xrpc::PerActorQuota::new(config.xrpc.per_actor_reservations as usize), 411 knot_xrpc::GlobalQuota::new(config.xrpc.max_pending_reservations as usize), 412 )); 413 let replay_bounds = knot_events::ReplayBounds::new( 414 knot_events::ReplayEvents::new(config.xrpc.events_replay_buffer as usize) 415 .context("xrpc.events_replay_buffer must be greater than zero")?, 416 knot_events::ReplayBytes::new(config.xrpc.events_replay_bytes as usize) 417 .context("xrpc.events_replay_bytes must be greater than zero")?, 418 ); 419 let events = Arc::new(knot_events::EventLog::new(SystemClock, replay_bounds)); 420 let subscriber_gate = Arc::new(knot_events::SubscriberGate::new( 421 knot_events::GlobalSubscriberLimit::new(config.xrpc.events_max_subscribers as usize), 422 knot_events::PerPeerSubscriberLimit::new(config.xrpc.events_max_per_peer as usize), 423 )); 424 425 let maintenance_enabled = config.maintenance.enabled; 426 let maintenance_options = knot_maintenance::Options::from_config(&config.maintenance); 427 let maintenance_interval = Duration::from_secs(config.maintenance.interval_secs); 428 let maintenance_large_push = 429 knot_maintenance::PushBytes::new(config.maintenance.large_push_bytes); 430 431 let lfs_handle = config 432 .lfs 433 .store_path 434 .as_ref() 435 .map(|path| { 436 knot_lfs::LfsHandle::open( 437 knot_lfs::LfsStorePath::new(path), 438 knot_lfs::LfsSize::new(config.lfs.max_object_bytes), 439 knot_lfs::FreeSpaceFloor::new(config.lfs.free_space_floor_bytes), 440 ) 441 .inspect(|_| { 442 tracing::info!(store = %path.display(), "git-lfs capability enabled"); 443 }) 444 }) 445 .transpose() 446 .context("open LFS object store")?; 447 let lfs_max_ssh_transfers = config.lfs.max_ssh_transfers as usize; 448 let lfs_max_http_downloads = config.lfs.max_http_downloads as usize; 449 let lfs_gc_grace = knot_maintenance::lfs_grace( 450 knot_maintenance::GcGrace::from_secs(config.lfs.gc_grace_secs), 451 knot_maintenance::ReflogRetention::from_secs(config.maintenance.reflog_expire_secs), 452 ); 453 let lfs_gc_interval = 454 knot_maintenance::SweepInterval::new(Duration::from_secs(config.lfs.gc_interval_secs)); 455 if lfs_handle.is_some() && !maintenance_enabled { 456 tracing::warn!( 457 "LFS store configured but maintenance is disabled. Unreferenced LFS objects will accumulate with no garbage collection or orphan sweep." 458 ); 459 } 460 461 let pack_cache_config = knot_pack::CacheConfig { 462 enabled: config.pack_cache.enabled, 463 ttl: Duration::from_secs(config.pack_cache.ttl_secs), 464 max_entry_bytes: knot_pack::MaxEntryBytes::new(config.pack_cache.max_entry_bytes as usize), 465 max_total_bytes: knot_pack::MaxCacheBytes::new(knot_resource::pack_cache_bytes( 466 config.pack_cache.max_total_bytes, 467 ) as usize), 468 }; 469 470 let ci_logs = config 471 .ci 472 .logs_addr 473 .as_deref() 474 .map(CiLogsAddr::new) 475 .transpose() 476 .context("ci.logs_addr must be host:port")?; 477 478 let homepage = config.homepage.source(); 479 let catalog = Arc::new( 480 knot_messages::Catalog::parse(&config.messages).context("parse message templates")?, 481 ); 482 let legacy_admin = legacy_admin_secret(&config); 483 484 knot_config::init(config); 485 486 let (maintenance_handle, maintenance_shutdown, maintenance_task) = if maintenance_enabled { 487 let (scheduler, handle) = knot_maintenance::Scheduler::new( 488 layout.clone(), 489 Arc::new(IndexRepos(Arc::clone(&index))), 490 SystemClock, 491 maintenance_options, 492 maintenance_interval, 493 maintenance_large_push, 494 ); 495 let scheduler = match &lfs_handle { 496 Some(lfs) => { 497 scheduler.with_lfs_gc(Arc::clone(&lfs.store), lfs_gc_grace, lfs_gc_interval) 498 } 499 None => scheduler, 500 }; 501 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); 502 let task = tokio::spawn(scheduler.run(shutdown_rx)); 503 tracing::info!( 504 interval_secs = maintenance_interval.as_secs(), 505 "maintenance scheduler running" 506 ); 507 (handle, Some(shutdown_tx), Some(task)) 508 } else { 509 (knot_maintenance::MaintenanceHandle::disabled(), None, None) 510 }; 511 512 let slots = knot_resource::Slots::for_machine(); 513 514 let ssh_base = knot_ssh::SshState::new(knot_ssh::SshConfig { 515 layout: layout.clone(), 516 index: Arc::clone(&index), 517 atproto: Arc::clone(&atproto), 518 knot_actor, 519 events: Arc::clone(&events), 520 hostname: hostname.clone(), 521 appview: appview_endpoint.clone(), 522 admins: admins.clone(), 523 admission, 524 max_pack_bytes: byte_limits.pack, 525 archive_limit: byte_limits.archive, 526 languages_push_budget: budgets.languages_push, 527 ci_logs: ci_logs.clone(), 528 }) 529 .with_maintenance(maintenance_handle.clone()) 530 .with_limits(pack_limits) 531 .with_slots(slots.clone()) 532 .with_catalog(Arc::clone(&catalog)); 533 let ssh_state = Arc::new(match &lfs_handle { 534 Some(handle) => ssh_base.with_lfs(handle.clone(), lfs_max_ssh_transfers), 535 None => ssh_base, 536 }); 537 538 let xrpc_state = Arc::new(XrpcState { 539 layout: layout.clone(), 540 index: Arc::clone(&index), 541 atproto: Arc::clone(&atproto), 542 secrets, 543 entropy: Arc::new(OsEntropy), 544 ci_logs, 545 admins, 546 admission, 547 knot_did, 548 knot_hostname: hostname, 549 meta_path, 550 knot_service_url, 551 limiter: Arc::new(knot_xrpc::PreAuthLimiter::with_config(xrpc_limits)), 552 cob_locks: Arc::new(knot_xrpc::CobLocks::default()), 553 reservations, 554 proxy_trust, 555 committer, 556 byte_limits, 557 budgets, 558 git_http, 559 pack_limits, 560 service_owner, 561 subscriber_gate, 562 maintenance: maintenance_handle, 563 appview: appview_endpoint, 564 slots: slots.clone(), 565 events, 566 lfs: lfs_handle.map(|handle| knot_xrpc::LfsWeb::new(handle, lfs_max_http_downloads)), 567 catalog: Arc::clone(&catalog), 568 }); 569 570 let resolver: Arc<dyn knot_pack::RepoResolver> = { 571 let index = Arc::clone(&index); 572 Arc::new(move |target: &knot_pack::RepoTarget| match target { 573 knot_pack::RepoTarget::Did(did) => { 574 knot_pack::RepoLookup::from_resolved(index.owner_of(did), |_| did.clone()) 575 } 576 knot_pack::RepoTarget::OwnerPath(owner, path) => knot_pack::RepoLookup::from_resolved( 577 index.resolve_clone_path(owner, path), 578 |found| found, 579 ), 580 }) 581 }; 582 let receive_advertiser = knot_xrpc::receive_advertiser(Arc::clone(&xrpc_state)); 583 let handle_resolver: Arc<dyn knot_pack::HandleResolver> = Arc::new(AtprotoHandleResolver { 584 atproto: Arc::clone(&atproto), 585 }); 586 let (write_routes, early_data_safe) = knot_pack::edge_routes(knot_pack::EdgeConfig { 587 receive: Some(receive_advertiser), 588 handle_resolver: Some(handle_resolver), 589 pack_slots: slots.pack.clone(), 590 cache: pack_cache_config, 591 catalog: Arc::clone(&catalog), 592 hostname: xrpc_state.knot_hostname.clone(), 593 archive_limit: byte_limits.archive, 594 ..knot_pack::EdgeConfig::serving(layout, resolver, Arc::new(SystemClock)) 595 }); 596 let legacy_admin_routes = legacy_admin.map(|secret| { 597 tracing::warn!( 598 route = knot_xrpc::legacy_admin::ADD_MEMBER_ROUTE, 599 "serving the legacy basic-auth admin route" 600 ); 601 knot_xrpc::legacy_admin::router(Arc::clone(&xrpc_state), secret) 602 }); 603 let base_router = write_routes.merge(knot_xrpc::router(xrpc_state)).route( 604 "/.well-known/did.json", 605 get(move || { 606 let document = did_document.clone(); 607 async move { Json(document) } 608 }), 609 ); 610 let base_router = match legacy_admin_routes { 611 Some(routes) => base_router.merge(routes), 612 None => base_router, 613 }; 614 let base_router = match homepage { 615 HomepageSource::Disabled => base_router, 616 HomepageSource::Default => base_router.route("/", get(|| async { Html(DEFAULT_HOMEPAGE) })), 617 HomepageSource::File(path) => base_router.route_service("/", ServeFile::new(path)), 618 }; 619 let app = knot_edge::RequiresFullHandshake::new(base_router); 620 let scheme = if tls_setup.is_some() { "https" } else { "http" }; 621 let edge_config = knot_edge::EdgeConfig { 622 http_addr: knot_edge::PublicBind::new(http_addr), 623 limits: listen_limits, 624 guards: edge_guards, 625 tls: tls_setup, 626 }; 627 tracing::info!("listening on {scheme}://{http_addr} and ssh://{ssh_addr}"); 628 629 let shutdown = CancellationToken::new(); 630 tokio::spawn(allocator::govern_decay(shutdown.clone())); 631 let mut edge_task = tokio::spawn(knot_edge::serve( 632 edge_config, 633 app, 634 early_data_safe, 635 shutdown.clone(), 636 )); 637 let mut ssh_task = { 638 let shutdown = shutdown.clone(); 639 tokio::spawn(async move { knot_ssh::serve(ssh_addr, host_key, ssh_state, shutdown).await }) 640 }; 641 642 let exit = tokio::select! { 643 result = &mut edge_task => FirstExit::Edge(result), 644 result = &mut ssh_task => FirstExit::Ssh(result), 645 () = shutdown_signal() => { 646 tracing::info!("shutdown signal received"); 647 FirstExit::Signal 648 } 649 }; 650 shutdown.cancel(); 651 let drain = async { 652 match &exit { 653 FirstExit::Edge(_) => { 654 let _ = (&mut ssh_task).await; 655 } 656 FirstExit::Ssh(_) => { 657 let _ = (&mut edge_task).await; 658 } 659 FirstExit::Signal => { 660 let _ = (&mut edge_task).await; 661 let _ = (&mut ssh_task).await; 662 } 663 } 664 }; 665 if tokio::time::timeout(EDGE_SHUTDOWN_DRAIN, drain) 666 .await 667 .is_err() 668 { 669 tracing::warn!( 670 timeout_secs = EDGE_SHUTDOWN_DRAIN.as_secs(), 671 "aborting edge drain after timeout" 672 ); 673 } 674 if let Some(shutdown) = maintenance_shutdown { 675 let _ = shutdown.send(true); 676 } 677 if let Some(task) = maintenance_task 678 && tokio::time::timeout(MAINTENANCE_SHUTDOWN_DRAIN, task) 679 .await 680 .is_err() 681 { 682 tracing::warn!( 683 timeout_secs = MAINTENANCE_SHUTDOWN_DRAIN.as_secs(), 684 "aborting maintenance drain after timeout :3" 685 ); 686 } 687 match exit { 688 FirstExit::Edge(result) => result 689 .context("edge server task panicked")? 690 .context("serve edge")?, 691 FirstExit::Ssh(result) => result 692 .context("ssh server task panicked")? 693 .context("serve ssh")?, 694 FirstExit::Signal => {} 695 } 696 Ok(()) 697} 698 699enum FirstExit { 700 Edge(Result<Result<(), knot_edge::EdgeError>, tokio::task::JoinError>), 701 Ssh(Result<Result<(), knot_ssh::SshError>, tokio::task::JoinError>), 702 Signal, 703} 704 705fn legacy_admin_secret( 706 config: &knot_config::Validated, 707) -> Option<knot_xrpc::legacy_admin::LegacyAdminSecret> { 708 let name = config.acl.legacy_admin_secret_env.as_deref()?; 709 let value = zeroize::Zeroizing::new(std::env::var(name).unwrap_or_default()); 710 match knot_xrpc::legacy_admin::LegacyAdminSecret::new(&value) { 711 Ok(secret) => Some(secret), 712 Err(_) => { 713 tracing::warn!( 714 secret_env = name, 715 "the environment variable in acl.legacy_admin_secret_env is unset or empty, so we won't serve the admin route" 716 ); 717 None 718 } 719 } 720} 721 722fn build_tls_setup( 723 config: &knot_config::KnotConfig, 724 hostname: &knot_types::KnotHostname, 725) -> anyhow::Result<Option<knot_edge::TlsSetup>> { 726 let tls = &config.tls; 727 let source = if tls.acme_enabled { 728 knot_edge::CertSource::Acme(knot_edge::AcmeParams { 729 domains: vec![hostname.clone()], 730 contact: knot_edge::AcmeContact::new( 731 tls.acme_contact 732 .clone() 733 .context("tls.acme_contact is required when ACME is enabled")?, 734 )?, 735 cache_dir: knot_edge::AcmeCacheDir::new( 736 tls.acme_cache_dir 737 .clone() 738 .context("tls.acme_cache_dir is required when ACME is enabled")?, 739 ), 740 production: !tls.acme_staging, 741 }) 742 } else { 743 match (&tls.cert_path, &tls.key_path) { 744 (Some(cert_path), Some(key_path)) => { 745 knot_edge::CertSource::Static(knot_edge::StaticCertPaths { 746 cert_path: knot_edge::CertChainPath::new(cert_path.clone()), 747 key_path: knot_edge::PrivateKeyPath::new(key_path.clone()), 748 }) 749 } 750 _ => return Ok(None), 751 } 752 }; 753 754 let internal = match tls.mtls_enabled { 755 true => Some(knot_edge::InternalTls { 756 addr: knot_edge::InternalBind::new(config.server.internal_listen_addr), 757 client_ca_path: knot_edge::ClientCaPath::new( 758 tls.mtls_client_ca_path 759 .clone() 760 .context("tls.mtls_client_ca_path is required when mTLS is enabled")?, 761 ), 762 spki_pin: knot_edge::SpkiPin::from_base64( 763 tls.mtls_admin_spki_pin 764 .as_deref() 765 .context("tls.mtls_admin_spki_pin is required when mTLS is enabled")?, 766 ) 767 .context("parse tls.mtls_admin_spki_pin")?, 768 }), 769 false => None, 770 }; 771 772 Ok(Some(knot_edge::TlsSetup { 773 source, 774 http3: tls.http3, 775 internal, 776 })) 777} 778 779async fn shutdown_signal() { 780 let interrupt = async { 781 let _ = tokio::signal::ctrl_c().await; 782 }; 783 #[cfg(unix)] 784 let terminate = async { 785 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { 786 Ok(mut stream) => { 787 stream.recv().await; 788 } 789 Err(_) => std::future::pending::<()>().await, 790 } 791 }; 792 #[cfg(not(unix))] 793 let terminate = std::future::pending::<()>(); 794 795 tokio::select! { 796 () = interrupt => {} 797 () = terminate => {} 798 } 799}