1mod dispatch;
4mod responses;
5mod transport;
6
7use std::collections::BTreeMap;
8use std::fs;
9use std::fs::File;
10use std::future::Future;
11use std::io::BufReader;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex};
14use std::time::Duration;
15
16use chrono::Utc;
17use futures_util::StreamExt as _;
18use mobius::agent::validate_submission;
19use mobius::middleware::session_files::{PendingSessionFileWrite, SessionFileStore};
20use mobius::protocol::Op;
21use rustls::ServerConfig;
22use rustls::pki_types::{CertificateDer, PrivateKeyDer};
23use tokio::io::{AsyncRead, AsyncWrite};
24use tokio::net::{TcpListener, TcpStream};
25use tokio::sync::broadcast;
26use tokio::task::JoinSet;
27use tokio::time::Instant;
28use tokio_rustls::TlsAcceptor;
29use tokio_tungstenite::accept_hdr_async_with_config;
30use tokio_tungstenite::tungstenite::handshake::server::{
31 Callback, ErrorResponse, Request, Response,
32};
33use tokio_tungstenite::tungstenite::http::StatusCode;
34use tokio_tungstenite::tungstenite::http::header::{HOST, ORIGIN};
35use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
36
37use crate::auth::{AuthStore, ClientIdentity, PairingGrant};
38use crate::bots::BotStore;
39use crate::config::{ConfigStore, CredentialStore, GatewayConfig, TlsConfig};
40use crate::host::{GatewayHost, HostHandle, Rejection};
41use crate::wire::{
42 ClientFrame, ClientKind, ClientMessage, ClientStatus, DirectoryEntry, DirectoryListing,
43 FrameReader, MAX_FRAME_BYTES, ServerFrame, ServerMessage, framed_to_websocket, read_frame,
44 read_frame_with_limit, validate_version, websocket_error, websocket_to_framed, write_frame,
45};
46use crate::{Error, Result};
47
48use self::dispatch::*;
49use self::responses::*;
50use self::transport::*;
51
52const PRE_AUTH_TIMEOUT: Duration = Duration::from_secs(5);
53const MAX_AUTHENTICATED_CONNECTIONS: usize = 32;
54const MAX_PRE_AUTH_CONNECTIONS: usize = 8;
55const MAX_CONNECTIONS: usize = MAX_AUTHENTICATED_CONNECTIONS + MAX_PRE_AUTH_CONNECTIONS;
56const INACTIVITY_TIMEOUT: Duration = Duration::from_secs(72 * 60 * 60);
57const ROUTINE_TICK: Duration = Duration::from_secs(15);
58const MAX_DIRECTORY_ENTRIES: usize = 512;
59const MAX_PENDING_UPLOADS: usize = 8;
60const WEBSOCKET_BRIDGE_BYTES: usize = 16 * 1024;
61
62const _: () = assert!(MAX_FRAME_BYTES <= u32::MAX as usize);
63
64pub struct GatewayServer {
66 config: GatewayConfig,
67 listener: TcpListener,
68 auth: Arc<AuthStore>,
69 host: GatewayHost,
70 bots: Arc<BotStore>,
71}
72
73impl GatewayServer {
74 pub async fn open(state_dir: PathBuf) -> Result<Self> {
76 let (store, config) = ConfigStore::open(state_dir)?;
77 let listener = TcpListener::bind(config.listen).await?;
78 Self::assemble(store, config, listener).await
79 }
80
81 pub async fn bootstrap(
83 state_dir: PathBuf,
84 listen: std::net::SocketAddr,
85 ) -> Result<(Self, PairingGrant)> {
86 let listener = TcpListener::bind(listen).await?;
87 let listen = listener.local_addr()?;
88 let (store, config) = ConfigStore::initialize(state_dir, listen, None)?;
89 let initialized_state = store.state_dir().to_path_buf();
90 let result = match AuthStore::initialize(store.auth_path()) {
91 Ok((_, grant)) => Self::assemble(store, config, listener)
92 .await
93 .map(|server| (server, grant)),
94 Err(error) => Err(error),
95 };
96 match result {
97 Ok(result) => Ok(result),
98 Err(error) => {
99 fs::remove_dir_all(&initialized_state).map_err(|cleanup| {
100 Error::Config(format!(
101 "{error}; failed to remove incomplete gateway state at {}: {cleanup}",
102 initialized_state.display()
103 ))
104 })?;
105 Err(error)
106 }
107 }
108 }
109
110 async fn assemble(
111 store: ConfigStore,
112 config: GatewayConfig,
113 listener: TcpListener,
114 ) -> Result<Self> {
115 let auth = Arc::new(AuthStore::open(store.auth_path())?);
116 let credentials = Arc::new(CredentialStore::open(store.credentials_path())?);
117 let bots = Arc::new(BotStore::open(store.state_dir())?);
118 let host =
119 GatewayHost::start(store, config.clone(), credentials, Arc::clone(&bots)).await?;
120 Ok(Self {
121 config,
122 listener,
123 auth,
124 host,
125 bots,
126 })
127 }
128
129 pub async fn serve(self) -> Result<()> {
131 let websocket_host = self.configured_websocket_host()?;
132 self.serve_with_host(websocket_host).await
133 }
134
135 pub(crate) async fn serve_cloudflare(self, hostname: String) -> Result<()> {
137 let cloudflare = self.config.cloudflare.as_ref().ok_or_else(|| {
138 Error::Config("a Cloudflare hostname requires tunnel configuration".into())
139 })?;
140 if cloudflare
141 .hostname()
142 .is_some_and(|configured| configured != hostname)
143 {
144 return Err(Error::Config(
145 "runtime Cloudflare hostname does not match gateway configuration".into(),
146 ));
147 }
148 self.serve_with_host(Some(hostname)).await
149 }
150
151 async fn serve_with_host(self, websocket_host: Option<String>) -> Result<()> {
152 #[cfg(unix)]
153 {
154 use tokio::signal::unix::{SignalKind, signal};
155
156 let mut interrupts = signal(SignalKind::interrupt())?;
157 let mut terminations = signal(SignalKind::terminate())?;
158 self.serve_until_inactive_with_host(
159 async move {
160 tokio::select! {
161 _ = interrupts.recv() => {}
162 _ = terminations.recv() => {}
163 }
164 },
165 INACTIVITY_TIMEOUT,
166 websocket_host,
167 )
168 .await
169 }
170 #[cfg(not(unix))]
171 self.serve_until_inactive_with_host(
172 async {
173 let _ = tokio::signal::ctrl_c().await;
174 },
175 INACTIVITY_TIMEOUT,
176 websocket_host,
177 )
178 .await
179 }
180
181 pub async fn serve_until(self, shutdown: impl Future<Output = ()>) -> Result<()> {
183 let websocket_host = self.configured_websocket_host()?;
184 self.serve_until_inactive_with_host(shutdown, INACTIVITY_TIMEOUT, websocket_host)
185 .await
186 }
187
188 #[cfg(test)]
189 async fn serve_until_inactive(
190 self,
191 shutdown: impl Future<Output = ()>,
192 inactivity_timeout: Duration,
193 ) -> Result<()> {
194 let websocket_host = self.configured_websocket_host()?;
195 self.serve_until_inactive_with_host(shutdown, inactivity_timeout, websocket_host)
196 .await
197 }
198
199 async fn serve_until_inactive_with_host(
200 self,
201 shutdown: impl Future<Output = ()>,
202 inactivity_timeout: Duration,
203 websocket_host: Option<String>,
204 ) -> Result<()> {
205 self.config.validate()?;
206 let tls = self.config.tls.as_ref().map(tls_acceptor).transpose()?;
207 if tls.is_none() && !self.listener.local_addr()?.ip().is_loopback() {
208 return Err(Error::Config(
209 "plaintext listeners are restricted to loopback".into(),
210 ));
211 }
212 let mut connections = JoinSet::new();
213 let connection_admission =
214 ConnectionAdmission::new(MAX_PRE_AUTH_CONNECTIONS, MAX_AUTHENTICATED_CONNECTIONS);
215 let client_connections = Arc::new(ClientConnections::default());
216 let (client_revocations, _) = broadcast::channel(MAX_CONNECTIONS);
217 let mut has_active_routines = self.bots.has_active_routines(Utc::now().timestamp())?;
218 let inactivity = tokio::time::sleep(inactivity_timeout);
219 tokio::pin!(inactivity);
220 let mut routine_timer = tokio::time::interval(ROUTINE_TICK);
221 routine_timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
222 tokio::pin!(shutdown);
223 loop {
224 tokio::select! {
225 biased;
226 () = &mut shutdown => return Ok(()),
227 _ = routine_timer.tick() => {
228 let now = Utc::now().timestamp();
229 let routines_active = self.bots.has_active_routines(now)?;
230 if has_active_routines && !routines_active && connections.is_empty() {
231 inactivity.as_mut().reset(tokio::time::Instant::now() + inactivity_timeout);
232 }
233 has_active_routines = routines_active;
234 let due = self.bots.take_due(now)?;
235 if !due.is_empty() {
236 let host = self.host.clone();
237 tokio::spawn(async move {
238 for (routine_id, run) in due {
239 if let Err(error) = host.run_due_routine(routine_id.clone(), run).await {
240 eprintln!(
241 "routine run failed: routine_id={routine_id} code={} message={}",
242 error.code, error.message
243 );
244 }
245 }
246 });
247 }
248 }
249 Some(_) = connections.join_next(), if !connections.is_empty() => {
250 if connections.is_empty() {
251 has_active_routines =
252 self.bots.has_active_routines(Utc::now().timestamp())?;
253 if !has_active_routines {
254 inactivity.as_mut().reset(tokio::time::Instant::now() + inactivity_timeout);
255 }
256 }
257 }
258 accepted = async {
259 let admission = connection_admission.admit().await;
260 self.listener.accept().await.map(|accepted| (accepted, admission))
261 }, if connections.len() < MAX_CONNECTIONS => {
262 let ((stream, _), admission) = accepted?;
263 let auth = Arc::clone(&self.auth);
264 let host = self.host.clone();
265 let bots = Arc::clone(&self.bots);
266 let client_connections = Arc::clone(&client_connections);
267 let client_revocations = client_revocations.clone();
268 let tls = tls.clone();
269 let websocket_host = websocket_host.clone();
270 connections.spawn(async move {
271 let auth_deadline = Instant::now() + PRE_AUTH_TIMEOUT;
272 let connection = ConnectionContext {
273 auth,
274 host,
275 bots,
276 client_connections,
277 client_revocations,
278 admission,
279 };
280 if let Some(tls) = tls {
281 if let Ok(Ok(stream)) =
282 tokio::time::timeout_at(auth_deadline, tls.accept(stream)).await
283 {
284 let _ = serve_connection(
285 stream,
286 connection,
287 auth_deadline,
288 None,
289 )
290 .await;
291 }
292 } else {
293 let _ = serve_plaintext_connection(
294 stream,
295 connection,
296 PlaintextHandshake {
297 expected_websocket_host: websocket_host,
298 auth_deadline,
299 },
300 )
301 .await;
302 }
303 });
304 }
305 () = &mut inactivity, if connections.is_empty() && !has_active_routines => {
306 has_active_routines = self.bots.has_active_routines(Utc::now().timestamp())?;
307 if !has_active_routines {
308 return Ok(());
309 }
310 }
311 }
312 }
313 }
314
315 fn configured_websocket_host(&self) -> Result<Option<String>> {
316 self.config
317 .cloudflare
318 .as_ref()
319 .map(|cloudflare| {
320 cloudflare.hostname().map(str::to_owned).ok_or_else(|| {
321 Error::Config(
322 "quick tunnel hostname is unavailable before cloudflared starts".into(),
323 )
324 })
325 })
326 .transpose()
327 }
328
329 #[must_use]
331 pub const fn listen_addr(&self) -> std::net::SocketAddr {
332 self.config.listen
333 }
334}
335
336#[cfg(test)]
337mod tests;