1use std::io::Read;
13use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
14use std::sync::Arc;
15use std::thread::JoinHandle;
16
17use tiny_http::{Header, Response as HttpResponse, Server};
18
19use crate::error::{Error, Result};
20
21use super::deadline::Watch;
22use super::Router;
23
24pub const MAX_BODY_BYTES: usize = 1024 * 1024;
26
27pub const WORKERS: usize = 4;
31
32pub const MAX_HANDLERS: usize = 64;
38
39static HANDLERS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
41
42static LAST_REQUEST_MS: AtomicU64 = AtomicU64::new(0);
46
47static OPEN_CHANNELS: AtomicUsize = AtomicUsize::new(0);
50
51fn epoch() -> std::time::Instant {
52 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
53 *START.get_or_init(std::time::Instant::now)
54}
55
56fn note_request() {
57 let ms = epoch().elapsed().as_millis() as u64;
58 LAST_REQUEST_MS.store(ms.max(1), Ordering::Relaxed);
59}
60
61pub fn used_within(window: std::time::Duration) -> bool {
71 if OPEN_CHANNELS.load(Ordering::Relaxed) > 0 {
72 return true;
73 }
74 let last = LAST_REQUEST_MS.load(Ordering::Relaxed);
75 last != 0 && epoch().elapsed().as_millis() as u64 <= last + window.as_millis() as u64
76}
77
78pub struct Bound {
80 server: Arc<Server>,
81 address: String,
82}
83
84pub fn bind(host: &str, port: u16) -> Result<Bound> {
86 let server = Arc::new(Server::http((host, port)).map_err(|e| Error::Http {
87 reason: format!("cannot bind {host}:{port}: {e}"),
88 })?);
89 let address = server
90 .server_addr()
91 .to_ip()
92 .map(|a| a.to_string())
93 .unwrap_or_else(|| format!("{host}:{port}"));
94 if address
95 .parse::<std::net::SocketAddr>()
96 .is_ok_and(|a| !a.ip().is_loopback())
97 {
98 tracing::warn!(
99 address = %address,
100 "bound to {address}, which is not a loopback address: every host that can reach this interface can read this repository's AI layer, its diagnostics and its peers, unencrypted; it can change nothing but through a signed mesh message (ADR 0126); bind 127.0.0.1 unless that is intended"
101 );
102 }
103 Ok(Bound { server, address })
104}
105
106pub fn bind_declared(host: &str, port: u16, source: &str) -> Result<Bound> {
112 let server = Arc::new(Server::http((host, port)).map_err(|e| Error::Http {
113 reason: format!("cannot bind {host}:{port}: {e}"),
114 })?);
115 let address = server
116 .server_addr()
117 .to_ip()
118 .map(|a| a.to_string())
119 .unwrap_or_else(|| format!("{host}:{port}"));
120 tracing::info!(
121 address = %address,
122 source = %source,
123 "bound {address}, the address {source} declares; this repository's AI layer is readable by every host that can reach this interface, which is what a deployment means"
124 );
125 Ok(Bound { server, address })
126}
127
128pub fn bind_or_fallback(host: &str, port: u16) -> Result<Bound> {
131 match bind(host, port) {
132 Ok(b) => Ok(b),
133 Err(e) if port != 0 => {
134 tracing::warn!("{e}; taking a free port instead");
135 bind(host, 0)
136 }
137 Err(e) => Err(e),
138 }
139}
140
141impl Bound {
142 pub fn address(&self) -> &str {
144 &self.address
145 }
146
147 pub fn url(&self) -> String {
149 format!("http://{}", self.address)
150 }
151
152 pub fn start(self, router: Router) -> Running {
155 let stopping = Arc::new(AtomicBool::new(false));
156 let local = self
157 .address
158 .parse()
159 .unwrap_or(std::net::SocketAddr::from(([0, 0, 0, 0], 0)));
160 let watch = Arc::new(Watch::new(local, crate::lease::timings().read_deadline));
161 let mut threads: Vec<JoinHandle<()>> = (0..WORKERS)
162 .map(|n| {
163 let server = Arc::clone(&self.server);
164 let router = router.clone();
165 let stopping = Arc::clone(&stopping);
166 let watch = Arc::clone(&watch);
167 std::thread::Builder::new()
168 .name(format!("http-{n}"))
169 .spawn(move || worker(&server, &router, &watch, &stopping))
170 .expect("spawn an http worker")
171 })
172 .collect();
173 let sweeper = {
174 let stopping = Arc::clone(&stopping);
175 std::thread::Builder::new()
176 .name("http-deadline".into())
177 .spawn(move || watch.run(&stopping))
178 .expect("spawn the http deadline sweeper")
179 };
180 threads.push(sweeper);
181 Running {
182 server: self.server,
183 address: self.address,
184 stopping,
185 threads,
186 }
187 }
188}
189
190pub struct Running {
192 server: Arc<Server>,
193 address: String,
194 stopping: Arc<AtomicBool>,
195 threads: Vec<JoinHandle<()>>,
196}
197
198impl Running {
199 pub fn address(&self) -> &str {
201 &self.address
202 }
203
204 pub fn url(&self) -> String {
206 format!("http://{}", self.address)
207 }
208
209 pub fn stop(self) {
213 self.stopping.store(true, Ordering::SeqCst);
214 for _ in &self.threads {
216 self.server.unblock();
217 }
218 for t in self.threads {
219 let _ = t.join();
220 }
221 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
224 while HANDLERS.load(Ordering::SeqCst) > 0 && std::time::Instant::now() < deadline {
225 std::thread::sleep(std::time::Duration::from_millis(20));
226 }
227 drop(self.server);
228 closed(&self.address);
229 tracing::info!(address = %self.address, "http stopped");
230 }
231}
232
233const CLOSE_GRACE: std::time::Duration = std::time::Duration::from_secs(2);
235
236fn closed(address: &str) {
245 let Ok(address) = address.parse::<std::net::SocketAddr>() else {
246 return;
247 };
248 let deadline = std::time::Instant::now() + CLOSE_GRACE;
249 let attempt = std::time::Duration::from_millis(100);
250 while std::time::Instant::now() < deadline {
251 if std::net::TcpStream::connect_timeout(&address, attempt).is_err() {
252 return;
253 }
254 std::thread::sleep(std::time::Duration::from_millis(5));
255 }
256 tracing::warn!(%address, "the listening socket still accepts after the server stopped");
257}
258
259fn worker(server: &Server, router: &Router, watch: &Arc<Watch>, stopping: &AtomicBool) {
260 loop {
261 match server.recv() {
262 Ok(request) => hand_off(router, watch, request),
263 Err(_) if stopping.load(Ordering::SeqCst) => break,
264 Err(e) => tracing::warn!("accepting a connection failed: {e}"),
265 }
266 }
267}
268
269fn hand_off(router: &Router, watch: &Arc<Watch>, request: tiny_http::Request) {
273 if HANDLERS.fetch_add(1, Ordering::SeqCst) >= MAX_HANDLERS {
274 HANDLERS.fetch_sub(1, Ordering::SeqCst);
275 let probe = matches!(
276 request.method(),
277 tiny_http::Method::Get | tiny_http::Method::Head
278 ) && request.url() == "/";
279 if probe {
280 answer(router, watch, request);
281 } else {
282 super::deadline::refused();
283 watch.stop_reading(request.remote_addr());
284 let busy = super::router::Response::error(
285 503,
286 "busy",
287 &format!("{MAX_HANDLERS} requests are being answered; ask again"),
288 )
289 .with_header("Retry-After", "1");
290 respond(request, busy, false);
291 }
292 return;
293 }
294 let router = router.clone();
295 let watch = Arc::clone(watch);
296 let spawned = std::thread::Builder::new()
297 .name("http-request".into())
298 .spawn(move || {
299 answer(&router, &watch, request);
300 HANDLERS.fetch_sub(1, Ordering::SeqCst);
301 });
302 released_unless_spawned(spawned, &HANDLERS);
303}
304
305fn released_unless_spawned(spawned: std::io::Result<JoinHandle<()>>, handlers: &AtomicUsize) {
308 if let Err(e) = spawned {
309 handlers.fetch_sub(1, Ordering::SeqCst);
310 tracing::warn!("a request could not be given a thread: {e}");
311 }
312}
313
314fn answer(router: &Router, watch: &Watch, request: tiny_http::Request) {
315 note_request();
316 let peer = request.remote_addr().copied();
317 watch.reading(peer.as_ref());
318 if let Some(request) = upgrade(router, watch, request) {
319 answer_http(router, watch, request);
320 }
321 watch.answered(peer.as_ref());
322}
323
324fn upgrade(
331 router: &Router,
332 watch: &Watch,
333 request: tiny_http::Request,
334) -> Option<tiny_http::Request> {
335 let headers: Vec<(String, String)> = request
336 .headers()
337 .iter()
338 .map(|h| (h.field.as_str().to_string(), h.value.as_str().to_string()))
339 .collect();
340 let probe =
341 super::Request::parse_target(&request.method().to_string(), request.url(), Vec::new())
342 .with_headers(headers)
343 .with_remote(request.remote_addr().map(|a| a.ip()));
344 let accepted = match router.websocket(&probe) {
345 None => return Some(request),
346 Some(Ok(accepted)) => accepted,
347 Some(Err(response)) => {
348 watch.busy(request.remote_addr());
349 respond(request, response, false);
350 return None;
351 }
352 };
353 watch.live(request.remote_addr());
355 let hung_up = watch.hung_up(request.remote_addr());
358 let key = accepted.accept.clone();
359 let response = HttpResponse::empty(101).with_header(
360 Header::from_bytes("Sec-WebSocket-Accept", key.as_bytes())
361 .expect("the accept value is header-safe"),
362 );
363 let socket = request.upgrade("websocket", response);
364 let spawned = std::thread::Builder::new()
365 .name("events".into())
366 .spawn(move || {
367 OPEN_CHANNELS.fetch_add(1, Ordering::Relaxed);
368 accepted.serve_until(Box::new(socket), hung_up);
369 OPEN_CHANNELS.fetch_sub(1, Ordering::Relaxed);
370 note_request();
371 });
372 if let Err(e) = spawned {
373 tracing::warn!("a live channel could not be started: {e}");
374 }
375 None
376}
377
378fn answer_http(router: &Router, watch: &Watch, mut request: tiny_http::Request) {
379 let method = request.method().to_string();
380 let head = method == "HEAD";
381 let method = if head { "GET".to_string() } else { method };
382 let target = request.url().to_string();
383 let remote = request.remote_addr().map(|a| a.ip());
384 let headers: Vec<(String, String)> = request
385 .headers()
386 .iter()
387 .map(|h| (h.field.as_str().to_string(), h.value.as_str().to_string()))
388 .collect();
389 let mut body = Vec::new();
390 if let Err(e) = request
391 .as_reader()
392 .take(MAX_BODY_BYTES as u64 + 1)
393 .read_to_end(&mut body)
394 {
395 tracing::warn!("cannot read a request body: {e}");
396 return;
397 }
398 if !watch.busy(request.remote_addr()) {
399 return;
401 }
402 let response = if body.len() > MAX_BODY_BYTES {
403 watch.stop_reading(request.remote_addr());
405 super::router::Response::error(
406 413,
407 "too_large",
408 &format!("the body is over {MAX_BODY_BYTES} bytes"),
409 )
410 } else {
411 router.handle(
412 &super::Request::parse_target(&method, &target, body)
413 .with_headers(headers)
414 .with_remote(remote),
415 )
416 };
417 tracing::debug!(
419 method = %method,
420 target = %crate::redaction::redact_secrets(&target).text,
421 status = response.status,
422 "response"
423 );
424 respond(request, response, head);
425}
426
427fn respond(request: tiny_http::Request, response: super::router::Response, head: bool) {
429 let (body, length) = wire_body(&response, head);
432 let mut out = HttpResponse::new(
433 response.status.into(),
434 Vec::new(),
435 std::io::Cursor::new(body),
436 Some(length),
437 None,
438 )
439 .with_chunked_threshold(usize::MAX)
440 .with_status_code(response.status)
441 .with_header(Header::from_bytes("Content-Type", response.content_type).expect("static header"));
442 if !response
446 .headers
447 .iter()
448 .any(|(name, _)| name.eq_ignore_ascii_case("cache-control"))
449 {
450 out.add_header(Header::from_bytes("Cache-Control", "no-store").expect("static header"));
451 }
452 for (name, value) in &response.headers {
453 if name.eq_ignore_ascii_case("content-length") {
455 continue;
456 }
457 if let Ok(h) = Header::from_bytes(name.as_bytes(), value.as_bytes()) {
458 out.add_header(h);
459 }
460 }
461 if let Err(e) = request.respond(out) {
462 tracing::debug!("client went away before the response was written: {e}");
463 }
464}
465
466fn wire_body(response: &super::router::Response, head: bool) -> (Vec<u8>, usize) {
471 if response.status == 304 {
472 let named = response
473 .headers
474 .iter()
475 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
476 .and_then(|(_, value)| value.trim().parse().ok());
477 return (Vec::new(), named.unwrap_or(0));
478 }
479 let body: Vec<u8> = if head {
480 Vec::new()
481 } else {
482 response.body.as_bytes().to_vec()
483 };
484 let length = body.len();
485 (body, length)
486}
487
488#[cfg(unix)]
494pub fn stdin_is_a_pipe() -> bool {
495 use std::os::fd::FromRawFd;
496 use std::os::unix::fs::FileTypeExt;
497 let stdin = std::mem::ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(0) });
500 stdin
501 .metadata()
502 .map(|m| m.file_type().is_fifo() || m.file_type().is_socket())
503 .unwrap_or(false)
504}
505
506#[cfg(not(unix))]
508pub fn stdin_is_a_pipe() -> bool {
509 false
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn a_304_writes_no_body_and_announces_the_length_it_names() {
518 use super::super::router::Response;
519 let not_modified = Response::new(304, "text/css", "").with_header("Content-Length", "1234");
520 assert_eq!(wire_body(¬_modified, false), (Vec::new(), 1234));
521 let unnamed = Response::new(304, "text/css", "ignored");
522 assert_eq!(wire_body(&unnamed, false), (Vec::new(), 0));
523 let ok = Response::new(200, "text/css", "body{}");
524 assert_eq!(wire_body(&ok, false), (b"body{}".to_vec(), 6));
525 assert_eq!(
526 wire_body(&ok, true),
527 (Vec::new(), 0),
528 "a HEAD writes nothing"
529 );
530 }
531
532 #[test]
533 fn a_request_that_could_not_be_given_a_thread_gives_back_its_slot() {
534 let handlers = AtomicUsize::new(3);
535 released_unless_spawned(Err(std::io::Error::other("no threads left")), &handlers);
536 assert_eq!(handlers.load(Ordering::SeqCst), 2);
537 let spawned = std::thread::Builder::new().spawn(|| {});
538 released_unless_spawned(spawned, &handlers);
539 assert_eq!(
540 handlers.load(Ordering::SeqCst),
541 2,
542 "a request on its thread keeps its slot until it is answered"
543 );
544 }
545}