Skip to main content

sova_core/app/
bind.rs

1//! Unified bind target for [`super::BoundApp::serve`].
2
3use crate::error::{Error, Result};
4use crate::server;
5use crate::App;
6use std::future::Future;
7use std::net::SocketAddr;
8use std::path::PathBuf;
9
10/// Where to accept connections.
11pub enum Bind {
12    Port(u16),
13    Addr(SocketAddr),
14    Str(String),
15    Env {
16        default_port: u16,
17    },
18    Listener(std::net::TcpListener),
19    #[cfg(unix)]
20    Uds(PathBuf),
21}
22
23impl From<u16> for Bind {
24    fn from(port: u16) -> Self {
25        Self::Port(port)
26    }
27}
28
29impl From<SocketAddr> for Bind {
30    fn from(addr: SocketAddr) -> Self {
31        Self::Addr(addr)
32    }
33}
34
35impl From<&str> for Bind {
36    fn from(s: &str) -> Self {
37        Self::Str(s.to_string())
38    }
39}
40
41impl From<String> for Bind {
42    fn from(s: String) -> Self {
43        Self::Str(s)
44    }
45}
46
47/// HTTP protocol mode for a bound app.
48///
49/// `all` is preparation for HTTP/3 discovery: TCP responses advertise `Alt-Svc`.
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum Http {
52    H1,
53    H1H2,
54    All,
55}
56
57impl Http {
58    pub const fn h1() -> Self {
59        Self::H1
60    }
61    pub const fn h1_h2() -> Self {
62        Self::H1H2
63    }
64    pub const fn all() -> Self {
65        Self::All
66    }
67}
68
69impl Bind {
70    pub fn str(s: impl Into<String>) -> Self {
71        Self::Str(s.into())
72    }
73}
74
75/// App + bind target + optional programmatic shutdown.
76pub struct BoundApp {
77    app: App,
78    bind: Bind,
79    http: Http,
80    shutdown: Option<server::ExternalShutdown>,
81    #[cfg(feature = "tls")]
82    tls: Option<crate::tls::TlsRuntime>,
83}
84
85impl App {
86    /// Choose a bind target; call [`.serve()`](BoundApp::serve) (optionally after [`.shutdown(...)`](BoundApp::shutdown)).
87    pub fn bind(self, target: impl Into<Bind>) -> BoundApp {
88        BoundApp {
89            app: self,
90            bind: target.into(),
91            http: Http::h1_h2(),
92            shutdown: None,
93            #[cfg(feature = "tls")]
94            tls: None,
95        }
96    }
97
98    /// Bind `0.0.0.0:port`, run CLI if present, otherwise serve.
99    ///
100    /// Prefer this for the common case; use [`Self::bind`] for custom addresses.
101    /// When a plugin attached TLS via [`Self::use_tls`] (e.g. Acme), this serves HTTPS.
102    pub async fn listen(self, port: u16) -> Result<()> {
103        self.bind(port).run().await
104    }
105
106    /// Attach TLS for the next [`Self::listen`] / [`BoundApp::run`].
107    ///
108    /// Plugins like Acme call this during `install` so apps do not need a separate
109    /// `.tls(...)` on [`BoundApp`]. An explicit [`BoundApp::tls`] still wins.
110    #[cfg(feature = "tls")]
111    pub fn use_tls(&mut self, tls: crate::Tls) -> &mut Self {
112        self.tls = Some(tls);
113        self
114    }
115}
116
117impl BoundApp {
118    /// Select HTTP protocol mode.
119    ///
120    /// `Http::all()` enables automatic `Alt-Svc: h3=":<port>"; ma=86400`.
121    pub fn http(mut self, http: Http) -> Self {
122        self.http = http;
123        self
124    }
125
126    /// Enable `SO_REUSEPORT` on TCP bind (requires feature `listen-reuseport`).
127    pub fn reuseport(mut self, enabled: bool) -> Self {
128        self.app.reuseport = enabled;
129        self
130    }
131
132    /// CLI commands then [`Self::serve`]. Same built-in commands as [`App::run`].
133    pub async fn run(self) -> Result<()> {
134        crate::tracing_init::ensure_tracing();
135        let args: Vec<String> = std::env::args().skip(1).collect();
136        if self.app.run_cli_command(&args).await? {
137            return Ok(());
138        }
139        self.serve_inner().await
140    }
141
142    fn apply_http_mode(mut app: App, http: Http, port: Option<u16>) -> App {
143        match http {
144            Http::All => {
145                if let Some(port) = port {
146                    app.alt_svc = Some(format!("h3=\":{port}\"; ma=86400"));
147                }
148            }
149            _ => {
150                app.alt_svc = None;
151            }
152        }
153        app
154    }
155
156    /// Stop when this future completes (in addition to Ctrl-C / SIGTERM).
157    pub fn shutdown<F>(mut self, f: F) -> Self
158    where
159        F: Future<Output = ()> + Send + 'static,
160    {
161        self.shutdown = Some(Box::pin(f));
162        self
163    }
164
165    /// Enable HTTPS (TCP binds only). Requires feature `tls`.
166    ///
167    /// Overrides TLS previously attached with [`App::use_tls`].
168    #[cfg(feature = "tls")]
169    pub fn tls(mut self, config: crate::Tls) -> Result<Self> {
170        if matches!(self.bind, Bind::Uds(_)) {
171            return Err(Error::Internal(
172                "TLS cannot be combined with Bind::Uds".into(),
173            ));
174        }
175        self.app.tls = None;
176        self.tls = Some(config.into_runtime()?);
177        self.app.hsts = self.tls.as_ref().map(|t| t.hsts).unwrap_or(false);
178        Ok(self)
179    }
180
181    /// Serve HTTP (no CLI). Prefer [`Self::run`] from `main` so `check`/`routes` work.
182    pub async fn serve(self) -> Result<()> {
183        crate::tracing_init::ensure_tracing();
184        self.serve_inner().await
185    }
186
187    #[cfg(feature = "tls")]
188    fn take_tls_runtime(&mut self) -> Result<Option<crate::tls::TlsRuntime>> {
189        if self.tls.is_some() {
190            return Ok(self.tls.take());
191        }
192        let Some(config) = self.app.tls.take() else {
193            return Ok(None);
194        };
195        if matches!(self.bind, Bind::Uds(_)) {
196            return Err(Error::Internal(
197                "TLS cannot be combined with Bind::Uds".into(),
198            ));
199        }
200        let runtime = config.into_runtime()?;
201        self.app.hsts = runtime.hsts;
202        Ok(Some(runtime))
203    }
204
205    #[cfg(feature = "tls")]
206    async fn serve_inner(mut self) -> Result<()> {
207        let tls = self.take_tls_runtime()?;
208        let http = self.http;
209        match self.bind {
210            Bind::Port(port) => {
211                let app = Self::apply_http_mode(self.app, http, Some(port));
212                server::listen(app, Some(port), None, self.shutdown, tls).await
213            }
214            Bind::Addr(addr) => {
215                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
216                server::listen(app, None, Some(addr), self.shutdown, tls).await
217            }
218            Bind::Str(s) => {
219                let addr: SocketAddr = s.parse().map_err(|e| {
220                    Error::Internal(format!("bind str {s:?}: invalid address: {e}"))
221                })?;
222                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
223                server::listen(app, None, Some(addr), self.shutdown, tls).await
224            }
225            Bind::Env { default_port } => {
226                let addr = super::addr_from_env(default_port)?;
227                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
228                server::listen(app, None, Some(addr), self.shutdown, tls).await
229            }
230            Bind::Listener(listener) => {
231                let port = listener.local_addr().ok().map(|a| a.port());
232                let app = Self::apply_http_mode(self.app, http, port);
233                server::listen_with_listener(app, listener, self.shutdown, tls).await
234            }
235            #[cfg(unix)]
236            Bind::Uds(path) => {
237                let app = Self::apply_http_mode(self.app, http, None);
238                server::listen_uds(app, &path, self.shutdown).await
239            }
240        }
241    }
242
243    #[cfg(not(feature = "tls"))]
244    async fn serve_inner(self) -> Result<()> {
245        let http = self.http;
246        match self.bind {
247            Bind::Port(port) => {
248                let app = Self::apply_http_mode(self.app, http, Some(port));
249                server::listen(app, Some(port), None, self.shutdown).await
250            }
251            Bind::Addr(addr) => {
252                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
253                server::listen(app, None, Some(addr), self.shutdown).await
254            }
255            Bind::Str(s) => {
256                let addr: SocketAddr = s.parse().map_err(|e| {
257                    Error::Internal(format!("bind str {s:?}: invalid address: {e}"))
258                })?;
259                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
260                server::listen(app, None, Some(addr), self.shutdown).await
261            }
262            Bind::Env { default_port } => {
263                let addr = super::addr_from_env(default_port)?;
264                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
265                server::listen(app, None, Some(addr), self.shutdown).await
266            }
267            Bind::Listener(listener) => {
268                let port = listener.local_addr().ok().map(|a| a.port());
269                let app = Self::apply_http_mode(self.app, http, port);
270                server::listen_with_listener(app, listener, self.shutdown).await
271            }
272            #[cfg(unix)]
273            Bind::Uds(path) => {
274                let app = Self::apply_http_mode(self.app, http, None);
275                server::listen_uds(app, &path, self.shutdown).await
276            }
277        }
278    }
279}