atagen

malice

public
Star0
Watch0
973f8fbdaf80e2ef6679dd071233fe6b853344b2
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-server/src/main.rsRust159 lines4.9 KB
1
use std::{future::Future, net::SocketAddr, path::PathBuf, time::Duration};
2
3
use axum::{Router, middleware};
4
use listenfd::ListenFd;
5
use malice::{
6
    idle::IdleTracker,
7
    routes::{router, static_router},
8
    state::{AppState, ensure_dirs, load_or_create_master_key},
9
};
10
11
fn env_flag(name: &str, default: bool) -> bool {
12
    match std::env::var(name) {
13
        Ok(value) => {
14
            let value = value.trim().to_ascii_lowercase();
15
            match value.as_str() {
16
                "1" | "true" | "yes" | "on" => true,
17
                "0" | "false" | "no" | "off" => false,
18
                _ => default,
19
            }
20
        }
21
        Err(_) => default,
22
    }
23
}
24
25
fn data_dir() -> PathBuf {
26
    std::env::var("MALICE_DATA_DIR")
27
        .map(PathBuf::from)
28
        .unwrap_or_else(|_| PathBuf::from("local-data"))
29
}
30
31
fn bind_addr() -> SocketAddr {
32
    std::env::var("MALICE_BIND")
33
        .unwrap_or_else(|_| "127.0.0.1:3000".to_owned())
34
        .parse()
35
        .expect("MALICE_BIND must be host:port")
36
}
37
38
fn idle_exit_secs() -> Option<u64> {
39
    std::env::var("MALICE_IDLE_EXIT_SECS")
40
        .ok()
41
        .and_then(|value| value.trim().parse().ok())
42
}
43
44
fn web_dir() -> Option<String> {
45
    if let Ok(dir) = std::env::var("MALICE_WEB_DIR") {
46
        assert!(
47
            std::path::Path::new(&dir).join("index.html").is_file(),
48
            "MALICE_WEB_DIR must point to built frontend assets containing index.html"
49
        );
50
        return Some(dir);
51
    }
52
53
    let local = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("web/dist");
54
    local
55
        .join("index.html")
56
        .is_file()
57
        .then(|| local.to_string_lossy().into_owned())
58
}
59
60
/// Takes over a systemd-activated listener from fd 3 when present, so the
61
/// socket unit can hold the port before the process ever starts. Falls back
62
/// to a plain bind otherwise.
63
async fn bind_listener() -> tokio::net::TcpListener {
64
    let mut listenfd = ListenFd::from_env();
65
    if let Some(listener) = listenfd
66
        .take_tcp_listener(0)
67
        .expect("inspecting inherited sockets")
68
    {
69
        listener
70
            .set_nonblocking(true)
71
            .expect("setting inherited socket nonblocking");
72
        return tokio::net::TcpListener::from_std(listener)
73
            .expect("adopting inherited socket into tokio");
74
    }
75
    let addr = bind_addr();
76
    tracing::info!("listening on {addr}");
77
    tokio::net::TcpListener::bind(addr)
78
        .await
79
        .expect("binding the listener must succeed")
80
}
81
82
/// Serves `app` on `listener`, exiting on ctrl-c or, when
83
/// `MALICE_IDLE_EXIT_SECS` is set, after that long with no request in flight
84
/// or started. `on_shutdown` runs after the exit signal either way.
85
async fn serve(
86
    listener: tokio::net::TcpListener,
87
    app: Router,
88
    on_shutdown: impl Future<Output = ()> + Send + 'static,
89
) {
90
    let idle = idle_exit_secs().map(|secs| (IdleTracker::new(), secs));
91
    let app = match &idle {
92
        Some((tracker, _)) => app.layer(middleware::from_fn_with_state(
93
            tracker.clone(),
94
            malice::idle::track,
95
        )),
96
        None => app,
97
    };
98
    axum::serve(listener, app)
99
        .with_graceful_shutdown(async move {
100
            match idle {
101
                Some((tracker, secs)) => {
102
                    tokio::select! {
103
                        _ = tokio::signal::ctrl_c() => {}
104
                        _ = tracker.wait_idle(Duration::from_secs(secs)) => {
105
                            tracing::info!("idle for {secs}s with no requests, exiting");
106
                        }
107
                    }
108
                }
109
                None => {
110
                    let _ = tokio::signal::ctrl_c().await;
111
                }
112
            }
113
            on_shutdown.await;
114
        })
115
        .await
116
        .expect("serving must succeed");
117
}
118
119
async fn run_hosted() {
120
    let data_dir = data_dir();
121
    ensure_dirs(&data_dir);
122
    let master_key =
123
        load_or_create_master_key(&data_dir).expect("loading the master key must succeed");
124
    let cookie_secure = env_flag("MALICE_SESSION_COOKIE_SECURE", true);
125
    let allow_http_homeserver = env_flag("MALICE_ALLOW_HTTP_HOMESERVER", false);
126
    let state = AppState::new(data_dir, master_key, cookie_secure, allow_http_homeserver);
127
    let app = router(state.clone(), web_dir());
128
    let listener = bind_listener().await;
129
    serve(
130
        listener,
131
        app,
132
        async move { state.shutdown_gracefully().await },
133
    )
134
    .await;
135
}
136
137
async fn run_static_only() {
138
    let app = static_router(web_dir());
139
    let listener = bind_listener().await;
140
    serve(listener, app, async {}).await;
141
}
142
143
#[tokio::main]
144
async fn main() {
145
    tracing_subscriber::fmt()
146
        .with_env_filter(
147
            tracing_subscriber::EnvFilter::try_from_default_env()
148
                .unwrap_or_else(|_| "malice=info,tower_http=info".into()),
149
        )
150
        .with_target(false)
151
        .compact()
152
        .init();
153
154
    if env_flag("MALICE_STATIC_ONLY", false) {
155
        run_static_only().await;
156
    } else {
157
        run_hosted().await;
158
    }
159
}