@jpgilldev / steid

steid/src/main.rs
4.5 KBRaw
1use steid::{
2 application::{AppConfig, is_claimed},
3 domain::SetupToken,
4 infrastructure::{
5 self,
6 repository::SqliteUserRepo,
7 web::{
8 context::SetupState, rate_limit::RateLimiter, session_cookie::InsecureCookieTokenStore,
9 },
10 },
11};
12use topcoat::{
13 asset::{AssetBundle, RouterBuilderAssetExt},
14 cookie::RouterBuilderCookieExt,
15 router::{Router, RouterBuilderDiscoverExt},
16 session::{RouterBuilderSessionExt, SessionConfig},
17};
18
19#[tokio::main]
20async fn main() -> Result<(), Box<dyn std::error::Error>> {
21 dotenvy::dotenv().ok();
22
23 let mut config = AppConfig::from_env()?;
24 let pool = infrastructure::database::connect(&config.database_url).await?;
25
26 // Taken out of the config before it reaches the app context, so the only copy the
27 // running application holds is the `SetupToken` in `SetupState` — which is
28 // redacted in `Debug` and dropped entirely once the instance is claimed.
29 let configured_token = config.setup_token.take();
30
31 let mut builder = Router::builder()
32 .assets(AssetBundle::load()?)
33 .cookies()
34 .sessions(session_config(config.insecure_cookies))
35 .discover()
36 .app_context(config)
37 .app_context(pool.clone())
38 // One limiter for the process, shared by every request. Constructing it per
39 // request would count each attempt against an empty window and limit nothing.
40 .app_context(RateLimiter::default());
41
42 // The setup token exists only while the instance is unclaimed, so a claimed
43 // installation has no token in context for a claim attempt to match against.
44 if !is_claimed(&SqliteUserRepo::new(pool)).await? {
45 let token = match &configured_token {
46 // Fails startup rather than serving a claim page guarded by a weak
47 // secret. This is the one moment an operator is watching the output.
48 Some(supplied) => {
49 let token = SetupToken::from_operator(supplied.expose()).map_err(|error| {
50 eprintln!();
51 eprintln!(" !! STEID_SETUP_TOKEN is not usable: {error}");
52 eprintln!(" !! Unset it to have one generated, or supply a stronger value.");
53 eprintln!();
54 error
55 })?;
56 announce_configured_setup();
57 token
58 }
59 None => {
60 let token = SetupToken::generate();
61 announce_setup(&token);
62 token
63 }
64 };
65
66 builder = builder.app_context(SetupState(token));
67 }
68
69 // Graceful shutdown needs no wiring: `topcoat::start` serves until Ctrl+C or
70 // SIGTERM, then stops accepting, lets in-flight requests finish within the
71 // service's shutdown timeout, and returns. A `systemctl restart` therefore does
72 // not cut a clone mid-pack.
73 topcoat::start(builder.build()).await?;
74
75 println!("steid: shut down");
76
77 Ok(())
78}
79
80/// Builds the session configuration.
81///
82/// The hardened default requires a trustworthy origin for its `Secure` cookie;
83/// browsers disagree about whether plain-HTTP localhost qualifies, and where it
84/// doesn't the cookie is dropped silently and every page renders signed out.
85fn session_config(insecure_cookies: bool) -> SessionConfig {
86 if insecure_cookies {
87 eprintln!();
88 eprintln!(" !! STEID_INSECURE_COOKIES is on: the session cookie has no Secure");
89 eprintln!(" !! flag and travels unencrypted. Local development only.");
90 eprintln!();
91
92 SessionConfig::builder()
93 .token_store(InsecureCookieTokenStore::new())
94 .build()
95 } else {
96 SessionConfig::default()
97 }
98}
99
100/// Prints the claim instructions when the operator supplied the token themselves.
101///
102/// Says that a token is in force without repeating it: they already have it, and
103/// printing it would copy a secret they chose into the journal.
104fn announce_configured_setup() {
105 println!();
106 println!(" This steid has no owner yet. Claim it at /auth/setup with the token");
107 println!(" from STEID_SETUP_TOKEN.");
108 println!();
109}
110
111/// Prints the claim instructions. The only time the token is ever revealed.
112fn announce_setup(token: &SetupToken) {
113 println!();
114 println!(" This steid has no owner yet. Claim it at /auth/setup with:");
115 println!();
116 println!(" {}", token.reveal());
117 println!();
118 println!(" The token is held in memory only — restarting issues a new one.");
119 println!();
120}