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 TLS, UDS, or custom addresses.
101    pub async fn listen(self, port: u16) -> Result<()> {
102        self.bind(port).run().await
103    }
104}
105
106impl BoundApp {
107    /// Select HTTP protocol mode.
108    ///
109    /// `Http::all()` enables automatic `Alt-Svc: h3=":<port>"; ma=86400`.
110    pub fn http(mut self, http: Http) -> Self {
111        self.http = http;
112        self
113    }
114
115    /// Enable `SO_REUSEPORT` on TCP bind (requires feature `listen-reuseport`).
116    pub fn reuseport(mut self, enabled: bool) -> Self {
117        self.app.reuseport = enabled;
118        self
119    }
120
121    /// CLI commands then [`Self::serve`]. Same built-in commands as [`App::run`].
122    pub async fn run(self) -> Result<()> {
123        crate::tracing_init::ensure_tracing();
124        let args: Vec<String> = std::env::args().skip(1).collect();
125        if self.app.run_cli_command(&args).await? {
126            return Ok(());
127        }
128        self.serve_inner().await
129    }
130
131    fn apply_http_mode(mut app: App, http: Http, port: Option<u16>) -> App {
132        match http {
133            Http::All => {
134                if let Some(port) = port {
135                    app.alt_svc = Some(format!("h3=\":{port}\"; ma=86400"));
136                }
137            }
138            _ => {
139                app.alt_svc = None;
140            }
141        }
142        app
143    }
144
145    /// Stop when this future completes (in addition to Ctrl-C / SIGTERM).
146    pub fn shutdown<F>(mut self, f: F) -> Self
147    where
148        F: Future<Output = ()> + Send + 'static,
149    {
150        self.shutdown = Some(Box::pin(f));
151        self
152    }
153
154    /// Enable HTTPS (TCP binds only). Requires feature `tls`.
155    #[cfg(feature = "tls")]
156    pub fn tls(mut self, config: crate::Tls) -> Result<Self> {
157        if matches!(self.bind, Bind::Uds(_)) {
158            return Err(Error::Internal(
159                "TLS cannot be combined with Bind::Uds".into(),
160            ));
161        }
162        self.tls = Some(config.into_runtime()?);
163        self.app.hsts = self.tls.as_ref().map(|t| t.hsts).unwrap_or(false);
164        Ok(self)
165    }
166
167    /// Serve HTTP (no CLI). Prefer [`Self::run`] from `main` so `check`/`routes` work.
168    pub async fn serve(self) -> Result<()> {
169        crate::tracing_init::ensure_tracing();
170        self.serve_inner().await
171    }
172
173    #[cfg(feature = "tls")]
174    async fn serve_inner(self) -> Result<()> {
175        let http = self.http;
176        match self.bind {
177            Bind::Port(port) => {
178                let app = Self::apply_http_mode(self.app, http, Some(port));
179                server::listen(app, Some(port), None, self.shutdown, self.tls).await
180            }
181            Bind::Addr(addr) => {
182                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
183                server::listen(app, None, Some(addr), self.shutdown, self.tls).await
184            }
185            Bind::Str(s) => {
186                let addr: SocketAddr = s.parse().map_err(|e| {
187                    Error::Internal(format!("bind str {s:?}: invalid address: {e}"))
188                })?;
189                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
190                server::listen(app, None, Some(addr), self.shutdown, self.tls).await
191            }
192            Bind::Env { default_port } => {
193                let addr = super::addr_from_env(default_port)?;
194                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
195                server::listen(app, None, Some(addr), self.shutdown, self.tls).await
196            }
197            Bind::Listener(listener) => {
198                let port = listener.local_addr().ok().map(|a| a.port());
199                let app = Self::apply_http_mode(self.app, http, port);
200                server::listen_with_listener(app, listener, self.shutdown, self.tls).await
201            }
202            #[cfg(unix)]
203            Bind::Uds(path) => {
204                let app = Self::apply_http_mode(self.app, http, None);
205                server::listen_uds(app, &path, self.shutdown).await
206            }
207        }
208    }
209
210    #[cfg(not(feature = "tls"))]
211    async fn serve_inner(self) -> Result<()> {
212        let http = self.http;
213        match self.bind {
214            Bind::Port(port) => {
215                let app = Self::apply_http_mode(self.app, http, Some(port));
216                server::listen(app, Some(port), None, self.shutdown).await
217            }
218            Bind::Addr(addr) => {
219                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
220                server::listen(app, None, Some(addr), self.shutdown).await
221            }
222            Bind::Str(s) => {
223                let addr: SocketAddr = s.parse().map_err(|e| {
224                    Error::Internal(format!("bind str {s:?}: invalid address: {e}"))
225                })?;
226                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
227                server::listen(app, None, Some(addr), self.shutdown).await
228            }
229            Bind::Env { default_port } => {
230                let addr = super::addr_from_env(default_port)?;
231                let app = Self::apply_http_mode(self.app, http, Some(addr.port()));
232                server::listen(app, None, Some(addr), self.shutdown).await
233            }
234            Bind::Listener(listener) => {
235                let port = listener.local_addr().ok().map(|a| a.port());
236                let app = Self::apply_http_mode(self.app, http, port);
237                server::listen_with_listener(app, listener, self.shutdown).await
238            }
239            #[cfg(unix)]
240            Bind::Uds(path) => {
241                let app = Self::apply_http_mode(self.app, http, None);
242                server::listen_uds(app, &path, self.shutdown).await
243            }
244        }
245    }
246}