1use std::collections::HashMap;
2use std::future::Future;
3use std::net::SocketAddr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use anyhow::anyhow;
8use axum::Router;
9use axum::handler::HandlerWithoutStateExt;
10use axum::middleware::{from_fn, from_fn_with_state};
11use tokio::net::TcpListener;
12use tokio::sync::watch;
13use tower_http::services::ServeDir;
14use tower_http::trace::TraceLayer;
15use tracing_subscriber::EnvFilter;
16
17use crate::auth::{Gate, LoginThrottle, User};
18use crate::crypto::parse_key;
19use crate::db::{Db, Migration, MigrationStatus, Migrator};
20use crate::events::Event;
21use crate::mail::Mailer;
22use crate::queue::{Handlers, Job, Queue, Worker};
23use crate::routing::RouteInfo;
24use crate::schedule::Schedule;
25use crate::timezone::Zone;
26use crate::{
27 AppState, Config, Environment, Error, Module, Registry, Result, RouteTable, Views, assets,
28 auth, csrf, session, view,
29};
30
31const SHUTDOWN_GRACE: Duration = Duration::from_secs(30);
33
34const USAGE: &str = "\
35Usage: <app> [command]
36
37Commands:
38 serve Start the web server, queue workers and scheduler (default)
39 migrate Run pending migrations
40 migrate:rollback [--step N]
41 Undo the last N batches of migrations (default 1)
42 migrate:fresh [--seed] Drop all tables, run every migration, optionally seed
43 migrate:status List migrations and whether they have run
44 db:check Compare the registered models with the tables the migrations build
45 db:diff [name] [--yes] Write a migration for what changed in the registered models
46 db:seed Run the seeders
47 queue:work [--queue a,b] [--workers N] [--once]
48 Run queued jobs (until stopped, or --once for what's there)
49 queue:failed List failed jobs
50 queue:retry <id|all> Put failed jobs back on the queue
51 queue:flush Delete failed jobs
52 queue:forget <id> Delete one failed job
53 queue:prune-failed [--hours N]
54 Delete failed jobs older than N hours (default 168)
55 queue:prune-batches [--hours N]
56 Delete batches finished more than N hours ago (default 24)
57 webhook:failed List webhook calls whose processing failed
58 webhook:retry <id> Process a stored webhook call again
59 ui:publish [--force] Copy the UI kit (renox/ui.html and its CSS) into the app to change it
60 cache:prune Delete expired rows of the database cache store
61 session:prune Delete expired sessions (SESSION_DRIVER=database)
62 schedule:list List scheduled tasks and when they run next
63 schedule:run <task> Run one scheduled task now
64 schedule:work Run scheduled tasks (when SCHEDULER=false for serve)
65 route:list List every route with its name, module and guards
66 view:check Compile every template; check route names and typed views
67 view:data [--out P] Write .vscode/renox-components.json for editor autocomplete
68 db:shell Run SQL against the database (`.tables`, `.quit`)
69 down [--secret S] [--retry N]
70 Maintenance mode: answer 503 (visit /S to bypass it)
71 up Leave maintenance mode
72 help Show this message";
73
74pub struct App {
99 config: Option<Config>,
100 modules: Vec<Box<dyn Module>>,
101 migrations: Vec<Migration>,
102 gates: HashMap<String, Gate>,
103 async_gates: HashMap<String, crate::auth::AsyncGate>,
104 gate_before: Option<crate::auth::GateBefore>,
105 registry: Registry,
106 embedded: Option<crate::Embedded>,
107 csp: crate::security::Csp,
108 permissions_policy: crate::security::PermissionsPolicy,
109 provided: HashMap<std::any::TypeId, Arc<dyn std::any::Any + Send + Sync>>,
110 layers: Vec<AppLayer>,
111 limiters: HashMap<String, crate::rate_limit::LimitRule>,
112 detect_locale: bool,
113 disks: Vec<(String, DiskSettings)>,
114 mailers: Vec<(String, MailerSettings)>,
115 xsrf_cookie: bool,
116}
117
118type MailerSettings = Box<dyn Fn(&Config) -> Result<crate::mail::MailConfig> + Send + Sync>;
120
121type DiskSettings = Box<dyn Fn(&Config) -> Result<crate::storage::StorageConfig> + Send + Sync>;
123
124type AppLayer = Box<dyn Fn(Router<AppState>) -> Router<AppState> + Send + Sync>;
127
128impl App {
129 pub fn new() -> Self {
131 Self {
132 config: None,
133 modules: Vec::new(),
134 migrations: Vec::new(),
135 gates: HashMap::new(),
136 async_gates: HashMap::new(),
137 gate_before: None,
138 registry: Registry::default(),
139 embedded: None,
140 csp: crate::security::Csp::default(),
141 permissions_policy: crate::security::PermissionsPolicy::default(),
142 provided: HashMap::new(),
143 layers: Vec::new(),
144 limiters: HashMap::new(),
145 detect_locale: false,
146 disks: Vec::new(),
147 mailers: Vec::new(),
148 xsrf_cookie: false,
149 }
150 }
151
152 pub fn detect_locale(mut self) -> Self {
166 self.detect_locale = true;
167 self
168 }
169
170 pub fn layer<L>(mut self, layer: L) -> Self
194 where
195 L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
196 L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
197 <L::Service as tower::Service<axum::extract::Request>>::Response:
198 axum::response::IntoResponse + 'static,
199 <L::Service as tower::Service<axum::extract::Request>>::Error:
200 Into<std::convert::Infallible> + 'static,
201 <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
202 {
203 self.layers
204 .push(Box::new(move |router| router.layer(layer.clone())));
205 self
206 }
207
208 pub fn csp(mut self, allow: impl FnOnce(&mut crate::security::Csp)) -> Self {
211 allow(&mut self.csp);
212 self
213 }
214
215 pub fn permissions_policy(
231 mut self,
232 allow: impl FnOnce(&mut crate::security::PermissionsPolicy),
233 ) -> Self {
234 allow(&mut self.permissions_policy);
235 self
236 }
237
238 pub fn embed(mut self, embedded: crate::Embedded) -> Self {
242 self.embedded = Some(embedded);
243 self
244 }
245
246 pub fn with_config(config: Config) -> Self {
248 Self {
249 config: Some(config),
250 ..Self::new()
251 }
252 }
253
254 pub fn config(mut self, config: Config) -> Self {
256 self.config = Some(config);
257 self
258 }
259
260 pub fn module(mut self, module: impl Module) -> Self {
262 self.modules.push(Box::new(module));
263 self
264 }
265
266 pub fn migrations(mut self, migrations: &[Migration]) -> Self {
268 self.migrations.extend_from_slice(migrations);
269 self
270 }
271
272 pub fn seeder<F, Fut>(mut self, seeder: F) -> Self
289 where
290 F: Fn(AppState) -> Fut + Send + Sync + 'static,
291 Fut: Future<Output = Result> + Send + 'static,
292 {
293 self.registry.seeder(seeder);
294 self
295 }
296
297 pub fn gate(
307 mut self,
308 name: &str,
309 check: impl Fn(&User) -> bool + Send + Sync + 'static,
310 ) -> Self {
311 self.gates.insert(name.to_owned(), Arc::new(check));
312 self
313 }
314
315 pub fn gate_before(
346 mut self,
347 check: impl Fn(&User, &str) -> Option<bool> + Send + Sync + 'static,
348 ) -> Self {
349 self.gate_before = Some(Arc::new(check));
350 self
351 }
352
353 pub fn gate_async<F, Fut>(mut self, name: &str, check: F) -> Self
356 where
357 F: Fn(User, AppState) -> Fut + Send + Sync + 'static,
358 Fut: Future<Output = Result<bool>> + Send + 'static,
359 {
360 self.async_gates.insert(
361 name.to_owned(),
362 Arc::new(move |user, state| Box::pin(check(user, state))),
363 );
364 self
365 }
366
367 pub fn webhook<W: crate::webhook::Webhook>(mut self) -> Self {
370 self.registry.webhook::<W>();
371 self
372 }
373
374 pub fn model<M: crate::db::Model>(mut self) -> Self {
383 self.registry.model::<M>();
384 self
385 }
386
387 pub fn view_data<T: crate::ViewData>(mut self) -> Self {
400 self.registry.view_data::<T>();
401 self
402 }
403
404 pub fn live_component<C: crate::live_component::LiveComponent>(mut self) -> Self {
406 self.registry.live_component::<C>();
407 self
408 }
409
410 pub fn job<J: Job>(mut self) -> Self {
412 self.registry.job::<J>();
413 self
414 }
415
416 pub fn listen<E, F, Fut>(mut self, listener: F) -> Self
418 where
419 E: Event,
420 F: Fn(E, AppState) -> Fut + Send + Sync + 'static,
421 Fut: Future<Output = Result> + Send + 'static,
422 {
423 self.registry.listen(listener);
424 self
425 }
426
427 pub fn command<F, Fut>(mut self, name: &str, about: &str, run: F) -> Self
430 where
431 F: Fn(crate::command::Args, AppState) -> Fut + Send + Sync + 'static,
432 Fut: Future<Output = Result> + Send + 'static,
433 {
434 self.registry.command(name, about, run);
435 self
436 }
437
438 pub fn typed_command<T: crate::command::AppCommand>(mut self) -> Self {
441 self.registry.typed_command::<T>();
442 self
443 }
444
445 pub fn rate_limiter(
449 mut self,
450 name: &str,
451 rule: impl Fn(&crate::rate_limit::LimitRequest) -> crate::rate_limit::Limit
452 + Send
453 + Sync
454 + 'static,
455 ) -> Self {
456 self.limiters
457 .insert(name.to_owned(), std::sync::Arc::new(rule));
458 self
459 }
460
461 pub fn report<F, Fut>(mut self, reporter: F) -> Self
465 where
466 F: Fn(crate::report::ErrorReport, AppState) -> Fut + Send + Sync + 'static,
467 Fut: Future<Output = ()> + Send + 'static,
468 {
469 self.registry.report(reporter);
470 self
471 }
472
473 pub fn channel<F, Fut>(mut self, name: &str, send: F) -> Self
476 where
477 F: Fn(crate::auth::Recipient, serde_json::Value, AppState) -> Fut + Send + Sync + 'static,
478 Fut: Future<Output = Result> + Send + 'static,
479 {
480 self.registry.channel(name, send);
481 self
482 }
483
484 pub fn xsrf_cookie(mut self) -> Self {
490 self.xsrf_cookie = true;
491 self
492 }
493
494 pub fn mailer(
512 mut self,
513 name: &str,
514 settings: impl Fn(&Config) -> Result<crate::mail::MailConfig> + Send + Sync + 'static,
515 ) -> Self {
516 self.mailers.push((name.to_owned(), Box::new(settings)));
517 self
518 }
519
520 pub fn disk(
539 mut self,
540 name: &str,
541 settings: impl Fn(&Config) -> Result<crate::storage::StorageConfig> + Send + Sync + 'static,
542 ) -> Self {
543 self.disks.push((name.to_owned(), Box::new(settings)));
544 self
545 }
546
547 pub fn provide<T: Send + Sync + 'static>(mut self, value: T) -> Self {
553 self.provided
554 .insert(std::any::TypeId::of::<T>(), Arc::new(value));
555 self
556 }
557
558 pub fn share<F, Fut, T>(mut self, key: &str, compute: F) -> Self
560 where
561 F: Fn(crate::view::ViewContext) -> Fut + Send + Sync + 'static,
562 Fut: Future<Output = Result<T>> + Send + 'static,
563 T: serde::Serialize,
564 {
565 self.registry.share(key, compute);
566 self
567 }
568
569 pub fn templates(
571 mut self,
572 hook: impl Fn(&mut minijinja::Environment<'static>) + Send + Sync + 'static,
573 ) -> Self {
574 self.registry.templates(hook);
575 self
576 }
577
578 pub fn schedule(mut self, define: impl FnOnce(&mut Schedule)) -> Self {
580 define(self.registry.schedule());
581 self
582 }
583
584 pub async fn boot(mut self) -> Result<Kernel> {
586 let config = match self.config {
587 Some(config) => config,
588 None => Config::load()?,
589 };
590
591 self.registry.job::<crate::mail::SendMail>();
592 self.registry.job::<crate::mail::SendMailVia>();
593 self.registry
594 .job::<crate::auth::notifications::SendToChannel>();
595 self.registry.job::<crate::webhook::ProcessWebhook>();
596 #[cfg(feature = "server-events")]
597 self.registry.job::<crate::analytics::ServerEvent>();
598 let mut migrations = crate::queue::MIGRATIONS.to_vec();
599 migrations.push(crate::cache::MIGRATION);
600 migrations.push(crate::session::MIGRATION);
601 migrations.push(crate::grid::MIGRATION);
602 migrations.extend(crate::webhook::MIGRATIONS);
603 migrations.extend(self.migrations);
604 for module in &self.modules {
605 migrations.extend_from_slice(module.migrations());
606 module.register(&mut self.registry);
607 }
608 let Registry {
609 jobs,
610 listeners,
611 schedule,
612 duplicate_job,
613 webhooks,
614 live_components,
615 duplicate_live,
616 commands,
617 templates,
618 components,
619 shares,
620 channels,
621 reporters,
622 permissions,
623 second_factor,
624 duplicate_second_factor,
625 mut account_sections,
626 auth,
627 assets: static_assets,
628 provided: mut module_provided,
629 seeders,
630 models,
631 view_data,
632 } = self.registry;
633 module_provided.extend(self.provided);
635 check_assets(&static_assets)?;
636 let static_assets: Arc<[crate::registry::StaticAsset]> = static_assets.into();
637 if let Some(name) = duplicate_job {
638 return Err(anyhow!("job `{name}` is registered twice").into());
639 }
640 if let Some(name) = duplicate_live {
641 return Err(anyhow!("live component `{name}` is registered twice").into());
642 }
643 if duplicate_second_factor {
644 return Err(
645 anyhow!("two modules set a second login step (`Registry::second_factor`)").into(),
646 );
647 }
648 check_commands(&commands)?;
649 schedule.check()?;
650 let zone: Zone = config.timezone;
651
652 let migrator = Migrator::new(migrations)?;
653 let key = match &config.key {
654 Some(key) => parse_key(key)?,
655 None => parse_key(&crate::generate_key())?,
656 };
657 let db = crate::db::connect(&config).await?.with_key(key.clone());
659
660 let mut router = Router::new();
661 let mut fallback: Option<axum::routing::MethodRouter<AppState>> = None;
662 let mut domains: Vec<(
664 crate::domain::DomainPattern,
665 Router<AppState>,
666 Option<axum::routing::MethodRouter<AppState>>,
667 )> = Vec::new();
668 let mut routes = RouteTable::default();
669 let mut listing: Vec<RouteInfo> = Vec::new();
670 for module in &self.modules {
671 tracing::debug!(module = module.name(), "registering module");
672 let parts = module.routes().into_parts();
673 check_clashes(&listing, &parts.listing, None, module.name())?;
674 router = merge_routes(router, parts.router, module.name())?;
675 for (name, path) in parts.names {
676 routes.insert(name, path)?;
677 }
678 listing.extend(parts.listing.into_iter().map(|info| RouteInfo {
679 module: module.name().to_owned(),
680 ..info
681 }));
682 if let Some(handler) = parts.fallback {
683 if fallback.is_some() {
684 return Err(anyhow!(
685 "two modules set a fallback route (`{}` is the second)",
686 module.name()
687 )
688 .into());
689 }
690 fallback = Some(handler);
691 }
692 for (text, domain_routes) in parts.domains {
693 let pattern = crate::domain::DomainPattern::parse(&text)?;
694 let inner = domain_routes.into_parts();
695 if !inner.domains.is_empty() {
696 return Err(anyhow!(
697 "Routes::domain(\"{text}\", …) inside another domain (module `{}`)",
698 module.name()
699 )
700 .into());
701 }
702 check_clashes(
703 &listing,
704 &inner.listing,
705 Some(pattern.as_str()),
706 module.name(),
707 )?;
708 let at = match domains.iter().position(|(p, _, _)| *p == pattern) {
709 Some(at) => at,
710 None => {
711 domains.push((pattern.clone(), Router::new(), None));
712 domains.len() - 1
713 }
714 };
715 let (_, domain_router, domain_fallback) = &mut domains[at];
716 *domain_router =
717 merge_routes(std::mem::take(domain_router), inner.router, module.name())?;
718 if let Some(handler) = inner.fallback {
719 if domain_fallback.is_some() {
720 return Err(anyhow!(
721 "two fallbacks for the domain `{text}` (module `{}`)",
722 module.name()
723 )
724 .into());
725 }
726 *domain_fallback = Some(handler);
727 }
728 for (name, path) in inner.names {
729 routes.set_domain(&name, pattern.as_str());
730 routes.insert(name, path)?;
731 }
732 listing.extend(inner.listing.into_iter().map(|info| RouteInfo {
733 module: module.name().to_owned(),
734 domain: Some(pattern.as_str().to_owned()),
735 ..info
736 }));
737 }
738 }
739 for layer in self.layers.iter().rev() {
741 router = layer(router);
742 for (_, domain_router, _) in &mut domains {
743 *domain_router = layer(std::mem::take(domain_router));
744 }
745 }
746 listing.extend(framework_routes(&config));
747 listing.sort_by(|a, b| (&a.path, &a.method).cmp(&(&b.path, &b.method)));
748 for info in &listing {
750 if let Some(name) = &info.name {
751 routes.set_method(name, &info.method);
752 }
753 }
754 for route in &listing {
756 for mark in &route.middleware {
757 if let Some(name) = mark.strip_prefix("throttle:")
758 && !name.contains('/')
759 && !self.limiters.contains_key(name)
760 {
761 return Err(anyhow!(
762 "{} {} uses throttle_by(\"{name}\"), but there's no App::rate_limiter(\"{name}\", …)",
763 route.method,
764 route.path
765 )
766 .into());
767 }
768 }
769 }
770 for route in &listing {
771 for provider in route
772 .middleware
773 .iter()
774 .filter_map(|m| m.strip_prefix("webhook:"))
775 {
776 if !webhooks.contains_key(provider) {
777 return Err(anyhow!(
778 "the route {} receives `{provider}` webhooks, but they're not registered: \
779 add `app.webhook::<…>()` in the module's `register`",
780 route.path
781 )
782 .into());
783 }
784 }
785 }
786 let routes = Arc::new(routes);
787
788 let storage = crate::storage::Storage::from_config(&config)?;
789 let mut disks = HashMap::new();
790 for (name, settings) in &self.disks {
791 let valid = !name.is_empty()
792 && name
793 .chars()
794 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
795 if !valid {
796 return Err(anyhow!(
797 "disk name `{name}`: use letters, digits, `-` and `_` (App::disk)"
798 )
799 .into());
800 }
801 let disk = crate::storage::Storage::named(&config, name, &settings(&config)?)?;
802 if disks.insert(name.clone(), disk).is_some() {
803 return Err(anyhow!("two disks are named `{name}` (App::disk)").into());
804 }
805 }
806 let embedded = self.embedded.filter(|_| !config.debug);
808 let shared_counters =
810 (config.cache_store == crate::CacheStore::Database).then(|| db.clone());
811 let versions = Arc::new(crate::embedded::AssetVersions::new(
812 &config.public_path,
813 embedded.map(|e| e.public),
814 ));
815 let views = Views::new(
816 &config,
817 routes.clone(),
818 storage.clone(),
819 embedded.map(|e| e.views),
820 Arc::new(templates),
821 Arc::new(components.iter().map(|c| c.contract()).collect()),
822 zone,
823 versions,
824 )
825 .with_typed(
826 view_data,
827 shares.iter().map(|(key, _)| key.clone()).collect(),
828 );
829 let mut security =
830 crate::security::Security::new(&config, &self.csp, &self.permissions_policy, &listing)?;
831 security.xsrf_cookie = self.xsrf_cookie;
832 let security = Arc::new(security);
833 let mut mailers = HashMap::new();
834 for (name, settings) in &self.mailers {
835 let mailer = Mailer::open(&settings(&config)?, &config)?;
836 if mailers.insert(name.clone(), mailer).is_some() {
837 return Err(anyhow!("two mailers are named `{name}` (App::mailer)").into());
838 }
839 }
840 let mut failover = Vec::new();
841 for name in &config.mail.failover {
842 let mailer = mailers.get(name).ok_or_else(|| {
843 anyhow!("MAIL_FAILOVER names `{name}`, but no mailer has that name: add it with App::mailer")
844 })?;
845 failover.push((name.clone(), mailer.clone()));
846 }
847 let mailer = Mailer::from_config(&config)?.with_failover(failover);
848 let state = AppState::new(crate::state::AppStateInner {
849 security,
850 webhooks: Arc::new(webhooks),
851 live_components: Arc::new(live_components),
852 mailer,
853 mailers: Arc::new(mailers),
854 queue: Queue::new(db.clone(), key.clone()),
855 cache: crate::cache::Cache::new(config.cache_store, db.clone())?,
856 storage,
857 disks: Arc::new(disks),
858 http: crate::http::Http::default(),
859 fakes: Arc::default(),
860 translator: Arc::new(match embedded {
861 Some(files) => crate::i18n::Translator::embedded(files.lang)?,
862 None => crate::i18n::Translator::load(&config.lang_path, config.debug)?,
863 }),
864 session_mirror: (config.session_driver == crate::SessionDriver::Database
866 && config.env == Environment::Testing)
867 .then(Default::default),
868 live: (config.debug && config.env == Environment::Local).then(|| {
869 crate::live::Live::start(vec![
870 config.views_path.clone(),
871 config.public_path.clone(),
872 config.lang_path.clone(),
873 ])
874 }),
875 notification_hub: Arc::new(crate::auth::notifications::Hub::new()),
876 listeners: Arc::new(listeners),
877 inspector: (config.debug && config.env == Environment::Local)
878 .then(|| Arc::new(crate::inspector::Inspector::default())),
879 config: Arc::new(config),
880 routes,
881 views,
882 db,
883 key,
884 gates: Arc::new(crate::auth::Access {
885 gates: self.gates,
886 before: self.gate_before,
887 permissions,
888 }),
889 async_gates: Arc::new(self.async_gates),
890 shares: Arc::new(shares),
891 channels: Arc::new(channels),
892 reporters: Arc::new(reporters),
893 second_factor: second_factor.map(Arc::new),
894 account_sections: {
895 account_sections.sort_by_key(|section| section.order);
896 Arc::new(account_sections)
897 },
898 auth,
899 limiters: Arc::new(
900 self.limiters
901 .into_iter()
902 .map(|(name, rule)| (name, crate::rate_limit::NamedLimiter::new(rule)))
903 .collect(),
904 ),
905 provided: Arc::new(module_provided),
906 throttle: Arc::new(LoginThrottle::new(shared_counters.clone())),
907 detect_locale: self.detect_locale,
908 });
909 if let Some(second) = &state.second_factor
910 && state.url(&second.challenge, &[]).is_err()
911 {
912 return Err(anyhow!(
913 "the second login step's challenge route `{}` doesn't exist",
914 second.challenge
915 )
916 .into());
917 }
918
919 let public = embedded.map(|e| e.public);
920 let default = build_router(router, state.clone(), public, fallback, &static_assets);
921 let router = if domains.is_empty() {
922 default
923 } else {
924 let hosts = domains
925 .into_iter()
926 .map(|(pattern, router, fallback)| {
927 (
928 pattern,
929 build_router(router, state.clone(), public, fallback, &static_assets),
930 )
931 })
932 .collect();
933 crate::domain::dispatch(hosts, default)
934 };
935 Ok(Kernel {
936 listing,
937 router,
938 state,
939 migrator,
940 seeders,
941 models,
942 handlers: Arc::new(jobs),
943 schedule,
944 zone,
945 commands,
946 })
947 }
948
949 pub async fn into_router(self) -> Result<Router> {
951 Ok(self.boot().await?.router)
952 }
953
954 pub fn run(self) -> Result {
957 let args: Vec<String> = std::env::args().skip(1).collect();
958 tokio::runtime::Builder::new_multi_thread()
959 .enable_all()
960 .build()?
961 .block_on(self.run_command(&args))
962 }
963
964 pub async fn run_args(self, args: impl IntoIterator<Item = impl Into<String>>) -> Result {
975 let args: Vec<String> = args.into_iter().map(Into::into).collect();
976 self.run_command(&args).await
977 }
978
979 pub async fn serve(self) -> Result {
981 self.run_command(&[]).await
982 }
983
984 async fn run_command(self, args: &[String]) -> Result {
985 let command = args.first().map(String::as_str).unwrap_or("serve");
986 if matches!(command, "help" | "--help" | "-h") {
987 let mut modules = Registry::default();
989 for module in &self.modules {
990 module.register(&mut modules);
991 }
992 let mut commands = self.registry.commands.clone();
993 commands.extend(modules.commands);
994 println!("{USAGE}{}", app_commands_help(&commands));
995 return Ok(());
996 }
997
998 let config = match self.config.clone() {
999 Some(config) => config,
1000 None => Config::load()?,
1001 };
1002 let long_running = matches!(command, "serve" | "queue:work" | "schedule:work");
1003 init_tracing(&config, long_running);
1004 if command == "serve" && config.key.is_none() && config.env != Environment::Testing {
1005 tracing::warn!(
1006 "APP_KEY is not set; using a temporary key, so sessions end on restart. \
1007 Run `rnx key:generate`."
1008 );
1009 }
1010 let kernel = App {
1011 config: Some(config),
1012 ..self
1013 }
1014 .boot()
1015 .await?;
1016
1017 match command {
1018 "serve" => kernel.serve().await?,
1019 "migrate" => print_done("Migrated", &kernel.migrate().await?),
1020 "migrate:rollback" => {
1021 let steps = flag_value(args, "--step")?.unwrap_or(1);
1022 print_done("Rolled back", &kernel.rollback(steps).await?);
1023 }
1024 "migrate:fresh" => {
1025 println!("Dropped all tables.");
1026 print_done("Migrated", &kernel.fresh().await?);
1027 if args.iter().any(|a| a == "--seed") {
1028 kernel.seed().await?;
1029 println!("Seeded.");
1030 }
1031 }
1032 "db:diff" if !kernel.commands.iter().any(|c| c.name == "db:diff") => {
1033 crate::db::auto_migration::run(&kernel, &args[1..]).await?
1034 }
1035 "db:check" if !kernel.commands.iter().any(|c| c.name == "db:check") => {
1036 kernel.db_check().await?
1037 }
1038 "view:check" if !kernel.commands.iter().any(|c| c.name == "view:check") => {
1039 let problems = kernel.state.views.check(&kernel.state.routes);
1040 for problem in &problems {
1041 println!("{problem}");
1042 }
1043 let checked = kernel.state.views.names().len();
1044 println!("Checked {checked} templates: {} problems.", problems.len());
1045 if !problems.is_empty() {
1046 return Err(anyhow!("{} template problem(s)", problems.len()).into());
1047 }
1048 }
1049 "view:data" if !kernel.commands.iter().any(|c| c.name == "view:data") => {
1050 let path = std::path::Path::new(
1051 flag_text(args, "--out").unwrap_or(".vscode/renox-components.json"),
1052 );
1053 if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
1054 std::fs::create_dir_all(dir)?;
1055 }
1056 let mut text = serde_json::to_string_pretty(&kernel.state.views.custom_data())
1057 .map_err(|e| anyhow!(e))?;
1058 text.push('\n');
1059 std::fs::write(path, text)?;
1060 println!("Wrote {}.", path.display());
1061 }
1062 "migrate:status" => {
1063 for m in kernel.migration_status().await? {
1064 let note = if m.missing {
1065 " (applied, but its file is gone)"
1066 } else if m.changed {
1067 " (edited after it ran; the edit won't run)"
1068 } else {
1069 ""
1070 };
1071 match m.batch {
1072 Some(batch) => println!(" ran (batch {batch}) {}{note}", m.name),
1073 None => println!(" pending {}", m.name),
1074 }
1075 }
1076 }
1077 "db:seed" => {
1078 kernel.seed().await?;
1079 println!("Seeded.");
1080 }
1081 "queue:work" => {
1082 let queues: Vec<String> = flag_text(args, "--queue")
1083 .map(|q| q.split(',').map(|s| s.trim().to_owned()).collect())
1084 .unwrap_or_default();
1085 let worker = kernel.worker(queues);
1086 if args.iter().any(|a| a == "--once") {
1087 println!("Ran {} job(s).", worker.drain().await?);
1088 } else {
1089 let workers = flag_value(args, "--workers")?.unwrap_or(1) as usize;
1090 let (stop, stopped) = watch::channel(false);
1091 let running = tokio::spawn(worker.run(workers, stopped));
1092 shutdown_signal().await;
1093 let _ = stop.send(true);
1094 let _ = running.await;
1095 }
1096 }
1097 "webhook:failed" => {
1098 let failed = crate::webhook::WebhookCall::failed(&kernel.state.db).await?;
1099 if failed.is_empty() {
1100 println!("No failed webhook calls.");
1101 }
1102 for call in failed {
1103 println!(
1104 " #{} {} {}: {}",
1105 call.id,
1106 call.provider,
1107 call.event_id,
1108 call.error
1109 .unwrap_or_default()
1110 .lines()
1111 .next()
1112 .unwrap_or_default()
1113 );
1114 }
1115 }
1116 "webhook:retry" => {
1117 let id: i64 = args
1118 .get(1)
1119 .and_then(|id| id.parse().ok())
1120 .ok_or_else(|| anyhow!("usage: webhook:retry <id>"))?;
1121 if crate::webhook::retry(&kernel.state, id).await? {
1122 println!("Webhook call #{id} queued again.");
1123 } else {
1124 return Err(anyhow!("there is no webhook call #{id}").into());
1125 }
1126 }
1127 "queue:failed" => {
1128 let failed = kernel.state.queue.failed().await?;
1129 if failed.is_empty() {
1130 println!("No failed jobs.");
1131 }
1132 for job in failed {
1133 println!(" #{} {} ({}): {}", job.id, job.job, job.queue, job.error);
1134 }
1135 }
1136 "queue:retry" => {
1137 let id = match args.get(1).map(String::as_str) {
1138 Some("all") => None,
1139 Some(id) => Some(
1140 id.parse()
1141 .map_err(|_| anyhow!("expected a job id or `all`"))?,
1142 ),
1143 None => return Err(anyhow!("usage: queue:retry <id|all>").into()),
1144 };
1145 let queued = match id {
1146 Some(id) => u64::from(kernel.state.queue.retry(id).await?),
1147 None => kernel.state.queue.retry_all().await?,
1148 };
1149 println!("Queued {queued} job(s) again.");
1150 }
1151 "queue:forget" => {
1152 let id: i64 = args
1153 .get(1)
1154 .and_then(|id| id.parse().ok())
1155 .ok_or_else(|| anyhow!("usage: queue:forget <id>"))?;
1156 if kernel.state.queue.forget_failed(id).await? {
1157 println!("Deleted failed job {id}.");
1158 } else {
1159 return Err(anyhow!("no failed job {id}").into());
1160 }
1161 }
1162 "queue:prune-failed" => {
1163 let hours = flag_value(args, "--hours")?.unwrap_or(168);
1164 let age = Duration::from_secs(u64::from(hours) * 3600);
1165 println!(
1166 "Deleted {} failed job(s) older than {hours} hour(s).",
1167 kernel.state.queue.prune_failed(age).await?
1168 );
1169 }
1170 "queue:prune-batches" => {
1171 let hours = flag_value(args, "--hours")?.unwrap_or(24);
1172 let age = Duration::from_secs(u64::from(hours) * 3600);
1173 println!(
1174 "Deleted {} batch(es) finished more than {hours} hour(s) ago.",
1175 kernel.state.queue.prune_batches(age).await?
1176 );
1177 }
1178 "queue:flush" => println!(
1179 "Deleted {} failed job(s).",
1180 kernel.state.queue.flush_failed().await?
1181 ),
1182 "ui:publish" => {
1183 let force = args.iter().any(|a| a == "--force");
1184 crate::assets::publish_ui(&kernel.state.config, force)?;
1185 }
1186 "session:prune" => println!(
1187 "Deleted {} expired session(s).",
1188 crate::Session::prune_expired(&kernel.state.db).await?
1189 ),
1190 "cache:prune" => println!(
1191 "Deleted {} expired cache row(s).",
1192 kernel.state.cache.prune().await?
1193 ),
1194 "schedule:list" => {
1195 if kernel.schedule.is_empty() {
1196 println!("No scheduled tasks.");
1197 }
1198 for run in kernel.schedule.upcoming(kernel.zone) {
1199 let when = match run.at {
1200 Some(at) => run
1201 .zone
1202 .local(at.timestamp())
1203 .format("%Y-%m-%d %H:%M")
1204 .to_string(),
1205 None => "never".to_owned(),
1206 };
1207 println!(" {when:<16} {:<18} {}", run.zone, run.name);
1208 }
1209 }
1210 "schedule:run" => {
1211 let Some(name) = args.get(1).filter(|a| !a.starts_with('-')) else {
1212 return Err(anyhow!("usage: schedule:run <task>").into());
1213 };
1214 kernel.run_scheduled(name).await?;
1215 println!("Ran `{name}`.");
1216 }
1217 "schedule:work" => {
1218 let (stop, stopped) = watch::channel(false);
1219 let running = tokio::spawn(kernel.schedule.clone().run(
1220 kernel.state.clone(),
1221 kernel.zone,
1222 stopped,
1223 ));
1224 shutdown_signal().await;
1225 let _ = stop.send(true);
1226 let _ = running.await;
1227 }
1228 "route:list" => print_routes(kernel.routes()),
1229 "db:shell" => crate::shell::run(kernel.db()).await?,
1230 "down" => {
1231 let secret = flag_text(args, "--secret").map(str::to_owned);
1232 let retry = flag_value(args, "--retry")?.map(u64::from);
1233 let mut options = crate::maintenance::DownOptions::new();
1234 if let Some(secret) = &secret {
1235 options = options.secret(secret.clone());
1236 }
1237 if let Some(retry) = retry {
1238 options = options.retry(retry);
1239 }
1240 crate::maintenance::down(&kernel.state.config.storage_path, options)?;
1241 match secret {
1242 Some(secret) => println!("The app is down. Visit /{secret} to bypass it."),
1243 None => println!("The app is down."),
1244 }
1245 }
1246 "up" => match crate::maintenance::up(&kernel.state.config.storage_path)? {
1247 true => println!("The app is up."),
1248 false => println!("The app was not down."),
1249 },
1250 other if kernel.commands.iter().any(|c| c.name == other) => {
1251 kernel.call(other, args[1..].iter().cloned()).await?;
1252 }
1253 other => {
1254 return Err(anyhow!(
1255 "unknown command `{other}`\n\n{USAGE}{}",
1256 app_commands_help(&kernel.commands)
1257 )
1258 .into());
1259 }
1260 }
1261 Ok(())
1262 }
1263}
1264
1265fn inherited_listener() -> Result<Option<TcpListener>> {
1269 let mut fds = listenfd::ListenFd::from_env();
1270 let Some(listener) = fds
1271 .take_tcp_listener(0)
1272 .map_err(|err| anyhow!("the socket systemd passed isn't a TCP listener: {err}"))?
1273 else {
1274 return Ok(None);
1275 };
1276 listener
1277 .set_nonblocking(true)
1278 .map_err(anyhow::Error::from)?;
1279 Ok(Some(
1280 TcpListener::from_std(listener).map_err(anyhow::Error::from)?,
1281 ))
1282}
1283
1284const BUILT_IN_COMMANDS: &[&str] = &[
1291 "serve",
1292 "migrate",
1293 "migrate:rollback",
1294 "migrate:fresh",
1295 "migrate:status",
1296 "db:seed",
1297 "queue:work",
1298 "queue:failed",
1299 "queue:retry",
1300 "queue:flush",
1301 "queue:forget",
1302 "queue:prune-failed",
1303 "queue:prune-batches",
1304 "webhook:failed",
1305 "webhook:retry",
1306 "cache:prune",
1307 "session:prune",
1308 "ui:publish",
1309 "schedule:list",
1310 "schedule:run",
1311 "schedule:work",
1312 "route:list",
1313 "db:shell",
1314 "down",
1315 "up",
1316 "help",
1317];
1318
1319fn check_commands(commands: &[crate::command::Command]) -> Result {
1320 let mut seen = std::collections::HashSet::new();
1321 for command in commands {
1322 let name = command.name.as_str();
1323 if name.is_empty() || name.starts_with('-') || name.contains(char::is_whitespace) {
1324 return Err(anyhow!("`{name}` is not a valid command name").into());
1325 }
1326 if BUILT_IN_COMMANDS.contains(&name) {
1327 return Err(anyhow!("the command `{name}` is built in; choose another name").into());
1328 }
1329 if !seen.insert(name) {
1330 return Err(anyhow!("the command `{name}` is registered twice").into());
1331 }
1332 }
1333 Ok(())
1334}
1335
1336fn app_commands_help(commands: &[crate::command::Command]) -> String {
1337 if commands.is_empty() {
1338 return String::new();
1339 }
1340 let mut out = String::from("\n\nApp commands:\n");
1341 for command in commands {
1342 out.push_str(&format!(" {:<26}{}\n", command.name, command.about));
1343 }
1344 out.trim_end().to_owned()
1345}
1346
1347impl Default for App {
1348 fn default() -> Self {
1349 Self::new()
1350 }
1351}
1352
1353pub struct Kernel {
1355 router: Router,
1356 listing: Vec<RouteInfo>,
1357 state: AppState,
1358 migrator: Migrator,
1359 seeders: Vec<crate::registry::Seeder>,
1360 models: Vec<crate::db::schema::ModelInfo>,
1361 handlers: Handlers,
1362 schedule: Schedule,
1363 zone: Zone,
1364 commands: Vec<crate::command::Command>,
1365}
1366
1367impl Kernel {
1368 pub(crate) async fn scratch_db(&self) -> Result<Db> {
1370 let db = crate::db::connect_scratch(&self.state.config).await?;
1371 self.migrator.run(&db).await?;
1372 Ok(db)
1373 }
1374
1375 async fn db_check(&self) -> Result {
1377 let db = self.scratch_db().await?;
1378 let reports = crate::db::schema_check::check(&db, &self.models).await;
1379 crate::db::drop_scratch(&db).await;
1380 let (text, problems) = crate::db::schema_check::render(&reports?);
1381 print!("{text}");
1382 if problems > 0 {
1383 return Err(anyhow::anyhow!("{problems} problem(s) in the models").into());
1384 }
1385 Ok(())
1386 }
1387
1388 pub(crate) fn models(&self) -> &[crate::db::schema::ModelInfo] {
1390 &self.models
1391 }
1392
1393 pub async fn call(
1395 &self,
1396 name: &str,
1397 args: impl IntoIterator<Item = impl Into<String>>,
1398 ) -> Result {
1399 let command = self
1400 .commands
1401 .iter()
1402 .find(|c| c.name == name)
1403 .ok_or_else(|| anyhow!("unknown command `{name}`"))?;
1404 let run = (command.run)(self.state.clone(), crate::command::Args::new(args));
1405 crate::context::scope_app(self.state.clone(), run).await
1406 }
1407
1408 pub async fn run_scheduled(&self, name: &str) -> Result {
1412 self.schedule.run_now(self.state.clone(), name).await
1413 }
1414
1415 pub fn router(&self) -> Router {
1417 self.router.clone()
1418 }
1419
1420 pub fn routes(&self) -> &[RouteInfo] {
1422 &self.listing
1423 }
1424
1425 pub fn state(&self) -> &AppState {
1427 &self.state
1428 }
1429
1430 pub fn db(&self) -> &Db {
1432 &self.state.db
1433 }
1434
1435 pub fn mailer(&self) -> &Mailer {
1437 &self.state.mailer
1438 }
1439
1440 pub fn worker(&self, queues: Vec<String>) -> Worker {
1442 Worker::new(self.state.clone(), self.handlers.clone(), queues)
1443 }
1444
1445 pub async fn run_jobs(&self) -> Result<usize> {
1447 self.worker(Vec::new()).drain().await
1448 }
1449
1450 pub async fn serve(self) -> Result {
1453 let config = self.state.config.clone();
1454 let listener = match inherited_listener()? {
1455 Some(listener) => {
1456 tracing::info!("using the socket systemd passed (socket activation)");
1457 listener
1458 }
1459 None => TcpListener::bind(config.addr()).await?,
1460 };
1461 tracing::info!(
1462 "{} listening on http://{}",
1463 config.name,
1464 listener.local_addr()?
1465 );
1466
1467 let (stop, stopped) = watch::channel(false);
1468 let mut background = Vec::new();
1469 if config.queue_workers > 0 {
1470 tracing::info!(workers = config.queue_workers, "queue workers started");
1471 let worker = self.worker(Vec::new());
1472 background.push(tokio::spawn(
1473 worker.run(config.queue_workers, stopped.clone()),
1474 ));
1475 }
1476 if config.scheduler && !self.schedule.is_empty() {
1477 tracing::info!("scheduler started");
1478 background.push(tokio::spawn(self.schedule.clone().run(
1479 self.state.clone(),
1480 self.zone,
1481 stopped,
1482 )));
1483 }
1484
1485 let service = self
1486 .router
1487 .into_make_service_with_connect_info::<SocketAddr>();
1488 let live = self.state.live.clone();
1489 let notification_hub = self.state.notification_hub.clone();
1490 axum::serve(listener, service)
1491 .with_graceful_shutdown(async move {
1492 shutdown_signal().await;
1493 if let Some(live) = live {
1496 live.stop();
1497 }
1498 notification_hub.stop();
1499 })
1500 .await?;
1501
1502 let _ = stop.send(true);
1503 let finished = async {
1504 for task in background {
1505 let _ = task.await;
1506 }
1507 };
1508 if tokio::time::timeout(SHUTDOWN_GRACE, finished)
1509 .await
1510 .is_err()
1511 {
1512 tracing::warn!("background work was still running after {SHUTDOWN_GRACE:?}");
1513 }
1514 tracing::info!("{} stopped", config.name);
1515 Ok(())
1516 }
1517
1518 pub async fn migrate(&self) -> Result<Vec<String>> {
1520 let done = self.migrator.run(self.db()).await?;
1521 Ok(done.into_iter().map(str::to_owned).collect())
1522 }
1523
1524 pub async fn rollback(&self, batches: u32) -> Result<Vec<String>> {
1526 Ok(self.migrator.rollback(self.db(), batches).await?)
1527 }
1528
1529 pub async fn fresh(&self) -> Result<Vec<String>> {
1531 let done = self.migrator.fresh(self.db()).await?;
1532 Ok(done.into_iter().map(str::to_owned).collect())
1533 }
1534
1535 pub async fn migration_status(&self) -> Result<Vec<MigrationStatus>> {
1537 Ok(self.migrator.status(self.db()).await?)
1538 }
1539
1540 pub async fn seed(&self) -> Result {
1542 for seeder in &self.seeders {
1543 crate::context::scope_app(self.state.clone(), seeder(self.state.clone())).await?;
1546 }
1547 Ok(())
1548 }
1549}
1550
1551fn framework_routes(config: &Config) -> Vec<RouteInfo> {
1553 let route = |method: &str, path: &str| RouteInfo {
1554 method: method.to_owned(),
1555 path: path.to_owned(),
1556 name: None,
1557 module: "renox".to_owned(),
1558 middleware: Vec::new(),
1559 domain: None,
1560 };
1561 let mut routes = vec![
1562 route("GET", "/health"),
1563 route("GET", "/robots.txt"),
1564 route("GET", "/favicon.ico"),
1565 route("GET", "/_renox/{asset}"),
1566 route("GET", "/_renox/files/{*key}"),
1567 route("POST", "/_renox/grid/{grid}/prefs"),
1568 route("DELETE", "/_renox/grid/{grid}/prefs"),
1569 route("POST", "/_renox/live/{component}/{action}"),
1570 route("GET", "/storage/{*path}"),
1571 ];
1572 if config.debug {
1573 routes.push(route("GET", "/_renox/mail"));
1574 routes.push(route("GET", "/_renox/mail/{id}"));
1575 }
1576 if config.debug && config.env == Environment::Local {
1577 routes.push(route("GET", "/_renox/live"));
1578 }
1579 routes
1580}
1581
1582fn print_routes(routes: &[RouteInfo]) {
1583 print!("{}", route_table(routes));
1584}
1585
1586fn route_table(routes: &[RouteInfo]) -> String {
1588 use std::fmt::Write as _;
1589 let mut out = String::new();
1590 let with_domains = routes.iter().any(|r| r.domain.is_some());
1591 let rows: Vec<Vec<String>> = routes
1592 .iter()
1593 .map(|r| {
1594 let mut row = vec![
1595 r.method.clone(),
1596 r.path.clone(),
1597 r.name.clone().unwrap_or_default(),
1598 r.module.clone(),
1599 r.middleware
1600 .iter()
1601 .map(|guard| {
1602 if is_role_guard(guard) {
1603 format!("{guard}*")
1604 } else {
1605 guard.clone()
1606 }
1607 })
1608 .collect::<Vec<_>>()
1609 .join(", "),
1610 ];
1611 if with_domains {
1612 row.insert(0, r.domain.clone().unwrap_or_default());
1613 }
1614 row
1615 })
1616 .collect();
1617 let mut header: Vec<String> = ["METHOD", "PATH", "NAME", "MODULE", "MIDDLEWARE"]
1618 .map(str::to_owned)
1619 .to_vec();
1620 if with_domains {
1621 header.insert(0, "DOMAIN".into());
1622 }
1623 let mut widths: Vec<usize> = header.iter().map(String::len).collect();
1624 for row in &rows {
1625 for (width, cell) in widths.iter_mut().zip(row) {
1626 *width = (*width).max(cell.chars().count());
1627 }
1628 }
1629 for row in std::iter::once(&header).chain(&rows) {
1630 let line: Vec<String> = row
1631 .iter()
1632 .zip(&widths)
1633 .map(|(cell, width)| format!("{cell:<width$}", width = *width))
1634 .collect();
1635 let _ = writeln!(out, "{}", line.join(" ").trim_end());
1636 }
1637 if routes
1638 .iter()
1639 .flat_map(|r| &r.middleware)
1640 .any(|g| is_role_guard(g))
1641 {
1642 out.push_str(
1643 "\n* role and permission guards count the user's global roles plus the roles \
1644 given in the request's active scope (permissions::set_scope).\n",
1645 );
1646 }
1647 out
1648}
1649
1650fn is_role_guard(guard: &str) -> bool {
1652 guard.starts_with("role:") || guard.starts_with("permission:")
1653}
1654
1655fn print_done(verb: &str, names: &[String]) {
1656 if names.is_empty() {
1657 println!("Nothing to do.");
1658 }
1659 for name in names {
1660 println!("{verb}: {name}");
1661 }
1662}
1663
1664fn flag_text<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
1665 let i = args.iter().position(|a| a == flag)?;
1666 args.get(i + 1).map(String::as_str)
1667}
1668
1669fn flag_value(args: &[String], flag: &str) -> Result<Option<u32>> {
1670 if !args.iter().any(|a| a == flag) {
1671 return Ok(None);
1672 }
1673 match flag_text(args, flag).and_then(|v| v.parse().ok()) {
1674 Some(value) => Ok(Some(value)),
1675 None => Err(anyhow!("{flag} needs a number").into()),
1676 }
1677}
1678
1679async fn guard(
1682 limit: Option<Duration>,
1683 req: axum::extract::Request,
1684 next: axum::middleware::Next,
1685) -> axum::response::Response {
1686 use axum::response::IntoResponse;
1687 let run = futures_util::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(next.run(req)));
1688 let outcome = match limit {
1689 Some(limit) => match tokio::time::timeout(limit, run).await {
1690 Ok(outcome) => outcome,
1691 Err(_) => {
1692 return Error::Internal(anyhow!(
1693 "the request took longer than REQUEST_TIMEOUT ({}s)",
1694 limit.as_secs()
1695 ))
1696 .into_response();
1697 }
1698 },
1699 None => run.await,
1700 };
1701 outcome.unwrap_or_else(|panic| {
1702 let message = crate::error::panic_message(&*panic);
1703 Error::Internal(anyhow!("the handler panicked: {message}")).into_response()
1704 })
1705}
1706
1707const BACKSTOP_GRACE: Duration = Duration::from_secs(1);
1710
1711async fn backstop(
1716 limit: Option<Duration>,
1717 req: axum::extract::Request,
1718 next: axum::middleware::Next,
1719) -> axum::response::Response {
1720 use axum::response::IntoResponse;
1721 let Some(limit) = limit else {
1722 return next.run(req).await;
1723 };
1724 match tokio::time::timeout(limit + BACKSTOP_GRACE, next.run(req)).await {
1725 Ok(res) => res,
1726 Err(_) => Error::Internal(anyhow!(
1727 "the request took longer than REQUEST_TIMEOUT ({}s) before reaching its handler",
1728 limit.as_secs()
1729 ))
1730 .into_response(),
1731 }
1732}
1733
1734fn merge_routes(
1736 router: Router<AppState>,
1737 other: Router<AppState>,
1738 module: &str,
1739) -> Result<Router<AppState>> {
1740 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| router.merge(other))).map_err(
1741 |panic| {
1742 let message = crate::error::panic_message(&*panic);
1743 anyhow!("the routes of the `{module}` module clash: {message}").into()
1744 },
1745 )
1746}
1747
1748fn check_clashes(
1751 listing: &[RouteInfo],
1752 new: &[RouteInfo],
1753 domain: Option<&str>,
1754 module: &str,
1755) -> Result {
1756 for info in new {
1757 let clash = listing.iter().find(|other| {
1758 other.domain.as_deref() == domain
1759 && other.path == info.path
1760 && (other.method == info.method || other.method == "*" || info.method == "*")
1761 });
1762 if let Some(other) = clash {
1763 let on = domain.map(|d| format!(" on {d}")).unwrap_or_default();
1764 return Err(anyhow!(
1765 "{} {}{on} is defined by both the `{}` and the `{module}` module",
1766 info.method,
1767 info.path,
1768 other.module,
1769 )
1770 .into());
1771 }
1772 }
1773 Ok(())
1774}
1775
1776fn check_assets(assets: &[crate::registry::StaticAsset]) -> Result<()> {
1778 let mut seen = std::collections::HashSet::new();
1779 for asset in assets {
1780 if !asset.path.starts_with('/') || asset.path.contains(['{', '}']) {
1781 return Err(anyhow!(
1782 "Registry::asset(\"{}\", …): the path must start with `/` and have no `{{…}}` parameters",
1783 asset.path
1784 )
1785 .into());
1786 }
1787 if !seen.insert(asset.path) {
1788 return Err(anyhow!(
1789 "two modules serve a file at `{}` (Registry::asset)",
1790 asset.path
1791 )
1792 .into());
1793 }
1794 }
1795 Ok(())
1796}
1797
1798fn build_router(
1799 router: Router<AppState>,
1800 state: AppState,
1801 embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1802 fallback: Option<axum::routing::MethodRouter<AppState>>,
1803 static_assets: &[crate::registry::StaticAsset],
1804) -> Router {
1805 let fallback = fallback.map(|handler| handler.with_state(state.clone()));
1807 let not_found = move |req: axum::extract::Request| {
1808 let fallback = fallback.clone();
1809 async move {
1810 match fallback {
1811 Some(handler) => match tower::ServiceExt::oneshot(handler, req).await {
1812 Ok(res) => res,
1813 Err(never) => match never {},
1814 },
1815 None => axum::response::IntoResponse::into_response(Error::NotFound),
1816 }
1817 }
1818 };
1819 let router = router
1820 .merge(crate::storage::router())
1821 .merge(crate::grid::router())
1822 .merge(crate::live_component::route::router());
1823 let router = if state.config.debug {
1824 router
1825 .merge(crate::mail::preview_router())
1826 .merge(crate::inspector::router())
1827 } else {
1828 router
1829 };
1830 let public = state.config.public_path.clone();
1831 let router = if let Some(files) = embedded_public {
1832 let files = crate::embedded::public_map(files);
1833 let not_found = not_found.clone();
1834 router.fallback(move |req: axum::extract::Request| {
1835 let (files, not_found) = (files.clone(), not_found.clone());
1836 async move {
1837 let res = crate::embedded::serve(&files, req.uri());
1838 if res.status() == axum::http::StatusCode::NOT_FOUND {
1839 not_found(req).await
1840 } else {
1841 res
1842 }
1843 }
1844 })
1845 } else if public.is_dir() {
1846 let files = ServeDir::new(public).fallback(not_found.clone().into_service());
1849 router.fallback(move |req: axum::extract::Request| {
1850 let files = files.clone();
1851 async move {
1852 if req
1855 .uri()
1856 .path()
1857 .split('/')
1858 .any(|segment| segment.len() > 255)
1859 {
1860 return axum::response::IntoResponse::into_response(Error::NotFound);
1861 }
1862 let versioned = crate::embedded::is_versioned(req.uri());
1863 match tower::ServiceExt::oneshot(files, req).await {
1864 Ok(res) => {
1865 let mut res = axum::response::IntoResponse::into_response(res);
1866 if versioned && res.status().is_success() {
1867 res.headers_mut().insert(
1868 axum::http::header::CACHE_CONTROL,
1869 axum::http::HeaderValue::from_static(
1870 "public, max-age=31536000, immutable",
1871 ),
1872 );
1873 }
1874 res
1875 }
1876 Err(never) => match never {},
1877 }
1878 }
1879 })
1880 } else {
1881 router.fallback(not_found)
1882 };
1883
1884 let request_timeout = state.config.request_timeout;
1885 let router: Router = router
1886 .layer(from_fn(
1887 move |req: axum::extract::Request, next: axum::middleware::Next| {
1888 guard(request_timeout, req, next)
1889 },
1890 ))
1891 .layer(from_fn_with_state(
1892 state.clone(),
1893 crate::maintenance::middleware,
1894 ))
1895 .layer(from_fn_with_state(state.clone(), view::middleware))
1896 .layer(from_fn(csrf::middleware))
1897 .layer(from_fn_with_state(state.clone(), auth::middleware))
1898 .layer(from_fn_with_state(state.clone(), crate::i18n::middleware))
1899 .layer(from_fn_with_state(state.clone(), session::middleware))
1900 .layer(from_fn_with_state(
1902 state.clone(),
1903 crate::context::middleware,
1904 ))
1905 .layer(from_fn(
1906 move |req: axum::extract::Request, next: axum::middleware::Next| {
1907 backstop(request_timeout, req, next)
1908 },
1909 ))
1910 .layer(from_fn_with_state(
1912 state.clone(),
1913 crate::inspector::middleware,
1914 ))
1915 .merge(assets::router())
1916 .merge(assets::module_router(static_assets))
1917 .merge(crate::health::router())
1918 .merge(robots(&state, embedded_public))
1919 .merge(favicon(&state, embedded_public))
1920 .merge(crate::live::router())
1921 .merge(public_files(&state))
1922 .layer(axum::extract::DefaultBodyLimit::max(
1923 state.config.upload_max_size,
1924 ))
1925 .layer(
1926 TraceLayer::new_for_http().make_span_with(|req: &axum::extract::Request| {
1927 let ip = crate::ClientIp::of(req).map(|ip| ip.to_string());
1928 let id = req
1929 .headers()
1930 .get(crate::request_id::HEADER)
1931 .and_then(|v| v.to_str().ok())
1932 .unwrap_or_default();
1933 tracing::info_span!(
1934 "request",
1935 id = %id,
1936 method = %req.method(),
1937 uri = %req.uri(),
1938 ip = ip.as_deref().unwrap_or("unknown"),
1939 )
1940 }),
1941 )
1942 .layer(from_fn(crate::request_id::middleware))
1943 .with_state(state.clone());
1944 let limit = state.config.upload_max_size;
1946 let spoofing = from_fn(
1947 move |req: axum::extract::Request, next: axum::middleware::Next| {
1948 crate::method::middleware(req, next, limit)
1949 },
1950 );
1951 Router::new()
1954 .fallback_service(tower::Layer::layer(&spoofing, router))
1955 .layer(from_fn_with_state(state, crate::security::middleware))
1956}
1957
1958fn has_public_file(
1960 state: &AppState,
1961 embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1962 name: &str,
1963) -> bool {
1964 match embedded_public {
1965 Some(files) => files.iter().any(|(path, _)| *path == name),
1966 None => state.config.public_path.join(name).is_file(),
1967 }
1968}
1969
1970fn robots(
1972 state: &AppState,
1973 embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1974) -> Router<AppState> {
1975 if has_public_file(state, embedded_public, "robots.txt") {
1976 Router::new()
1977 } else {
1978 crate::seo::robots_router(state)
1979 }
1980}
1981
1982fn favicon(
1986 state: &AppState,
1987 embedded_public: Option<&'static [(&'static str, &'static [u8])]>,
1988) -> Router<AppState> {
1989 if has_public_file(state, embedded_public, "favicon.ico") {
1990 return Router::new();
1991 }
1992 Router::new().route(
1993 "/favicon.ico",
1994 axum::routing::get(|| async {
1995 (
1996 axum::http::StatusCode::NO_CONTENT,
1997 [(axum::http::header::CACHE_CONTROL, "public, max-age=86400")],
1998 )
1999 }),
2000 )
2001}
2002
2003fn public_files(state: &AppState) -> Router<AppState> {
2005 match state.storage.public_root() {
2006 Some(root) => Router::new()
2007 .nest_service("/storage", ServeDir::new(root))
2008 .layer(axum::middleware::map_response(user_file_headers)),
2009 None => Router::new(),
2010 }
2011}
2012
2013pub(crate) async fn user_file_headers(
2018 mut res: axum::response::Response,
2019) -> axum::response::Response {
2020 use axum::http::HeaderValue;
2021 use axum::http::header::{
2022 CONTENT_DISPOSITION, CONTENT_SECURITY_POLICY, CONTENT_TYPE, X_CONTENT_TYPE_OPTIONS,
2023 };
2024
2025 let content_type = res
2026 .headers()
2027 .get(CONTENT_TYPE)
2028 .and_then(|v| v.to_str().ok())
2029 .unwrap_or_default()
2030 .to_ascii_lowercase();
2031 let headers = res.headers_mut();
2032 headers.insert(
2033 CONTENT_SECURITY_POLICY,
2034 HeaderValue::from_static(
2035 "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox",
2036 ),
2037 );
2038 headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
2039 let document = ["html", "xml", "javascript", "ecmascript"]
2040 .iter()
2041 .any(|kind| content_type.contains(kind))
2042 && !content_type.contains("svg");
2043 if document {
2044 headers.insert(CONTENT_DISPOSITION, HeaderValue::from_static("attachment"));
2045 }
2046 res
2047}
2048
2049fn init_tracing(config: &Config, long_running: bool) {
2051 let default = match (long_running, config.debug) {
2052 (true, true) => "info,renox=debug",
2053 (true, false) => "info",
2054 (false, _) => "warn",
2055 };
2056 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default));
2057 let file = config.log_file.as_ref().and_then(|path| {
2061 match std::fs::OpenOptions::new()
2062 .create(true)
2063 .append(true)
2064 .open(path)
2065 {
2066 Ok(file) => Some(std::sync::Mutex::new(file)),
2067 Err(err) => {
2068 eprintln!("LOG_FILE {}: {err}; logging to stdout", path.display());
2069 None
2070 }
2071 }
2072 });
2073 let json = config.log_format == crate::LogFormat::Json;
2074 let builder = tracing_subscriber::fmt().with_env_filter(filter);
2075 let _ = match (json, file) {
2076 (true, Some(file)) => builder
2077 .json()
2078 .with_current_span(true)
2079 .with_span_list(false)
2080 .with_writer(file)
2081 .try_init(),
2082 (true, None) => builder
2083 .json()
2084 .with_current_span(true)
2085 .with_span_list(false)
2086 .try_init(),
2087 (false, Some(file)) => builder.with_ansi(false).with_writer(file).try_init(),
2088 (false, None) => builder.try_init(),
2089 };
2090}
2091
2092async fn shutdown_signal() {
2093 let ctrl_c = async {
2094 let _ = tokio::signal::ctrl_c().await;
2095 };
2096
2097 #[cfg(unix)]
2098 let terminate = async {
2099 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
2100 Ok(mut signal) => {
2101 signal.recv().await;
2102 }
2103 Err(_) => std::future::pending().await,
2104 }
2105 };
2106 #[cfg(not(unix))]
2107 let terminate = std::future::pending::<()>();
2108
2109 tokio::select! {
2110 _ = ctrl_c => {},
2111 _ = terminate => {},
2112 }
2113 tracing::info!("shutdown signal received");
2114}
2115
2116#[cfg(test)]
2117mod tests {
2118 use super::*;
2119
2120 fn route(path: &str, middleware: &[&str]) -> RouteInfo {
2121 RouteInfo {
2122 method: "GET".into(),
2123 path: path.into(),
2124 name: None,
2125 module: "app".into(),
2126 middleware: middleware.iter().map(|m| (*m).to_owned()).collect(),
2127 domain: None,
2128 }
2129 }
2130
2131 #[test]
2132 fn route_list_says_role_guards_count_the_active_scope() {
2133 let plain = route_table(&[route("/", &["auth"])]);
2134 assert!(!plain.contains('*'), "{plain}");
2135 let table = route_table(&[
2136 route("/orders", &["auth", "permission:orders.view"]),
2137 route("/drafts", &["role:editor"]),
2138 route("/admin", &["gate:admin"]),
2139 ]);
2140 assert!(table.contains("auth, permission:orders.view*"), "{table}");
2141 assert!(table.contains("role:editor*"), "{table}");
2142 assert!(table.contains("gate:admin\n"), "{table}");
2143 assert!(
2144 table.contains("active scope (permissions::set_scope)"),
2145 "{table}"
2146 );
2147 }
2148
2149 struct Note;
2150 impl crate::db::FromRow for Note {
2151 fn from_row(_: &crate::db::Row) -> Result<Self, crate::db::DbError> {
2152 Ok(Note)
2153 }
2154 }
2155 impl crate::db::Model for Note {
2156 const TABLE: &'static str = "notes";
2157 const COLUMNS: &'static [&'static str] = &["id"];
2158 type Key = i64;
2159 fn id(&self) -> i64 {
2160 0
2161 }
2162 fn set_id(&mut self, _: i64) {}
2163 fn values(&self) -> Vec<crate::db::DbValue> {
2164 Vec::new()
2165 }
2166 }
2167
2168 struct Registers;
2169 impl Module for Registers {
2170 fn name(&self) -> &'static str {
2171 "registers"
2172 }
2173 fn register(&self, app: &mut Registry) {
2174 app.model::<crate::auth::User>();
2175 }
2176 }
2177
2178 #[tokio::test]
2179 async fn models_are_kept_once_from_the_app_and_modules() {
2180 let boot = |app: App| async move { app.config(Config::default()).boot().await.unwrap() };
2181 let one = boot(
2182 App::new()
2183 .model::<crate::auth::User>()
2184 .model::<crate::auth::User>()
2185 .module(Registers),
2186 )
2187 .await;
2188 assert_eq!(one.models().len(), 1);
2189 assert_eq!(one.models()[0].table, "users");
2190 let two = boot(
2191 App::new()
2192 .model::<crate::auth::User>()
2193 .model::<Note>()
2194 .module(Registers),
2195 )
2196 .await;
2197 assert_eq!(two.models().len(), 2);
2198 }
2199}