Skip to main content

headless_lms_server/programs/
start_server.rs

1use crate::{
2    config::{self, ServerConfigBuilder, ServerRuntimeConfig, set_server_runtime_config},
3    setup_tracing,
4};
5use actix_session::{
6    SessionMiddleware,
7    config::{CookieContentSecurity, PersistentSession, SessionLifecycle, TtlExtensionPolicy},
8    storage::CookieSessionStore,
9};
10use actix_web::{
11    App, HttpServer,
12    cookie::{Key, SameSite},
13    dev::ServiceRequest,
14    middleware::Logger,
15};
16use dotenvy::dotenv;
17use listenfd::ListenFd;
18use secrecy::ExposeSecret;
19use std::time::Duration;
20
21/// The entrypoint to the server.
22pub async fn main() -> anyhow::Result<()> {
23    dotenv().ok();
24    setup_tracing()?;
25
26    let runtime_config = ServerRuntimeConfig::try_from_env()?;
27    let private_cookie_key = runtime_config.private_cookie_key.clone();
28    let test_mode = runtime_config.test_mode;
29    let allow_no_https_for_development = runtime_config.allow_no_https_for_development;
30    let host = runtime_config.host.clone();
31    let port = runtime_config.port.clone();
32    set_server_runtime_config(runtime_config.clone())?;
33
34    if test_mode {
35        info!("***********************************");
36        info!("*  Starting backend in test mode  *");
37        info!("***********************************");
38    }
39    let server_config = ServerConfigBuilder::from_runtime_config(&runtime_config)
40        .await
41        .expect("Failed to create server config builder from runtime config")
42        .build()
43        .await
44        .expect("Failed to create server config");
45    let mut server = HttpServer::new(move || {
46        let server_config = server_config.clone();
47        App::new()
48            .configure(move |config| config::configure(config, server_config))
49            .wrap(
50                SessionMiddleware::builder(
51                    CookieSessionStore::default(),
52                    Key::from(private_cookie_key.expose_secret().as_bytes()),
53                )
54                .cookie_name("session".to_string())
55                .cookie_secure(!allow_no_https_for_development)
56                .cookie_same_site(SameSite::Strict) // Default api can only be accessed from the main website. Public api will be less strict on this.
57                .cookie_http_only(true) // Cookie is inaccessible from javascript for security
58                .cookie_path("/api".to_string()) // browser won't send the cookie unless this path exists in the request url
59                .cookie_content_security(CookieContentSecurity::Private)
60                .session_lifecycle(SessionLifecycle::PersistentSession(
61                    PersistentSession::default()
62                        .session_ttl(actix_web::cookie::time::Duration::days(100))
63                        .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest),
64                ))
65                .build(),
66            )
67            .wrap(
68                Logger::new(
69                    "Completed %{request_line}xi %s %b bytes - %D ms, request_id=%{request-id}o",
70                )
71                .custom_request_replace("request_line", redacted_request_line),
72            )
73    })
74    // Must outlive ingress-nginx's 60 s upstream keepalive, or nginx reuses a connection we are
75    // closing and answers the (unretried) POST with a 502.
76    .keep_alive(Duration::from_secs(75));
77
78    // this will enable us to keep application running during recompile: systemfd --no-pid -s http::5000 -- cargo watch -x run
79    let mut listenfd = ListenFd::from_env();
80    server = match listenfd.take_tcp_listener(0)? {
81        Some(listener) => server.listen(listener)?,
82        None => {
83            let bind_address = format!("{}:{}", host, port);
84            info!("Binding to address: {}", bind_address);
85            server.bind(bind_address)?
86        }
87    };
88
89    info!("Starting server.");
90    server.run().await?;
91
92    Ok(())
93}
94
95/// The request line for the access log with query values dropped (names kept) and the
96/// student-number linking token masked: student numbers travel in search query parameters and the
97/// token claims one.
98fn redacted_request_line(req: &ServiceRequest) -> String {
99    let mut path = String::with_capacity(req.path().len());
100    let mut is_token_segment = false;
101    for (i, segment) in req.path().split('/').enumerate() {
102        if i > 0 {
103            path.push('/');
104        }
105        path.push_str(if is_token_segment { "{token}" } else { segment });
106        // The route segment the credit registration controller mounts the token routes under.
107        is_token_segment = segment == "student-number-verifications";
108    }
109    let query = req.query_string();
110    if !query.is_empty() {
111        path.push('?');
112        for (i, pair) in query.split('&').enumerate() {
113            if i > 0 {
114                path.push('&');
115            }
116            path.push_str(pair.split('=').next().unwrap_or_default());
117        }
118    }
119    format!("{} {} {:?}", req.method(), path, req.version())
120}