1use std::{
10 convert::Infallible,
11 error::Error,
12 fmt,
13 future::{Future, IntoFuture, Ready, ready},
14 io,
15 net::IpAddr,
16 pin::Pin,
17 sync::{
18 Arc,
19 atomic::{AtomicU64, Ordering},
20 },
21 task::{Context, Poll},
22 time::Duration,
23};
24
25mod outbound;
26mod qos_admission;
27mod qos_client;
28
29use axum::{
30 Router,
31 extract::Request,
32 http::StatusCode,
33 response::{IntoResponse, Response},
34 serve::{IncomingStream, Listener},
35};
36use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
37use tower::Service;
38
39use qos_admission::QosRequestLayer;
40
41use crate::{
42 env::NetworkEnvOverrides,
43 net::qos::{QosPermit, QosPolicy, QosRuntime},
44};
45
46pub use outbound::{
47 HttpClientError, OutboundClientError, outbound_json_client, outbound_json_client_with_policy,
48 post_json, post_json_with_qos,
49};
50pub use qos_client::{QosHttpClientError, QosHttpResponse, send_request_with_qos};
51
52tokio::task_local! {
53 static QOS_REQUEST_CONTEXT: ();
54}
55
56pub const DEFAULT_HTTP_BIND: &str = "127.0.0.1:8791";
57pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
58pub const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
59pub const DEFAULT_MAX_BODY_BYTES: u64 = 1_048_576;
60pub const DEFAULT_SSL_VERIFY: bool = true;
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct HttpConfig {
65 pub bind_address: HttpBindAddress,
66 pub request_timeout: Duration,
67 pub graceful_shutdown_timeout: Duration,
68 pub max_request_body_bytes: u64,
69 pub proxy: HttpProxyConfig,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct HttpBindAddress {
75 value: String,
76 port: u16,
77}
78
79impl HttpBindAddress {
80 pub fn parse(value: &str) -> Result<Self, HttpConfigError> {
82 let trimmed = value.trim();
83 if trimmed.is_empty() {
84 return Err(HttpConfigError::InvalidBindAddress {
85 value: value.to_owned(),
86 });
87 }
88
89 if let Ok(socket_addr) = trimmed.parse::<std::net::SocketAddr>() {
90 return Self::from_parts(trimmed.to_owned(), socket_addr.port());
91 }
92
93 let Some((host, port)) = trimmed.rsplit_once(':') else {
94 return Err(HttpConfigError::InvalidBindAddress {
95 value: value.to_owned(),
96 });
97 };
98
99 if host.is_empty() || host.contains('/') || host.contains(char::is_whitespace) {
100 return Err(HttpConfigError::InvalidBindAddress {
101 value: value.to_owned(),
102 });
103 }
104
105 let port = port
106 .parse::<u16>()
107 .map_err(|_| HttpConfigError::InvalidBindAddress {
108 value: value.to_owned(),
109 })?;
110
111 Self::from_parts(trimmed.to_owned(), port)
112 }
113
114 pub const fn port(&self) -> u16 {
116 self.port
117 }
118
119 fn from_parts(value: String, port: u16) -> Result<Self, HttpConfigError> {
120 if port == 0 {
121 return Err(HttpConfigError::EphemeralPort);
122 }
123
124 Ok(Self { value, port })
125 }
126}
127
128impl fmt::Display for HttpBindAddress {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter.write_str(&self.value)
131 }
132}
133
134pub fn remote_clients_allowed(config: &HttpConfig, allow_remote_clients: bool) -> bool {
136 allow_remote_clients || is_local_bind(&config.bind_address.to_string())
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct HttpProxyConfig {
142 pub proxy: Option<String>,
143 pub no_proxy_rules: Vec<String>,
144 pub ssl_verify: bool,
145}
146
147impl HttpProxyConfig {
148 pub fn new(
150 proxy: Option<String>,
151 no_proxy_rules: Vec<String>,
152 ssl_verify: bool,
153 ) -> Result<Self, HttpConfigError> {
154 if let Some(proxy_url) = proxy.as_deref() {
155 validate_proxy_url(proxy_url)?;
156 }
157
158 for rule in &no_proxy_rules {
159 if rule.trim().is_empty() {
160 return Err(HttpConfigError::EmptyNoProxyRule);
161 }
162 }
163
164 Ok(Self {
165 proxy,
166 no_proxy_rules,
167 ssl_verify,
168 })
169 }
170
171 pub fn from_overrides(overrides: &NetworkEnvOverrides) -> Result<Self, HttpConfigError> {
173 Self::new(
174 overrides.proxy.clone(),
175 parse_no_proxy_rules(overrides.no_proxy.as_deref())?,
176 overrides.ssl_verify.unwrap_or(DEFAULT_SSL_VERIFY),
177 )
178 }
179
180 pub fn is_proxy_configured(&self) -> bool {
182 self.proxy.is_some()
183 }
184}
185
186impl HttpConfig {
187 pub fn new(
189 bind_address: HttpBindAddress,
190 request_timeout: Duration,
191 graceful_shutdown_timeout: Duration,
192 max_request_body_bytes: u64,
193 proxy: HttpProxyConfig,
194 ) -> Result<Self, HttpConfigError> {
195 if request_timeout.is_zero() {
196 return Err(HttpConfigError::ZeroDuration {
197 field: "request_timeout",
198 });
199 }
200
201 if graceful_shutdown_timeout.is_zero() {
202 return Err(HttpConfigError::ZeroDuration {
203 field: "graceful_shutdown_timeout",
204 });
205 }
206
207 if max_request_body_bytes == 0 {
208 return Err(HttpConfigError::ZeroMaxBodyBytes);
209 }
210
211 Ok(Self {
212 bind_address,
213 request_timeout,
214 graceful_shutdown_timeout,
215 max_request_body_bytes,
216 proxy,
217 })
218 }
219
220 pub fn from_overrides(overrides: &NetworkEnvOverrides) -> Result<Self, HttpConfigError> {
222 let bind_value = overrides.http_bind.as_deref().unwrap_or(DEFAULT_HTTP_BIND);
223 let bind_address = HttpBindAddress::parse(bind_value)?;
224 let request_timeout = overrides
225 .http_request_timeout_ms
226 .map(Duration::from_millis)
227 .unwrap_or(DEFAULT_REQUEST_TIMEOUT);
228 let shutdown_timeout = overrides
229 .http_shutdown_timeout_ms
230 .map(Duration::from_millis)
231 .unwrap_or(DEFAULT_SHUTDOWN_TIMEOUT);
232 let max_body_bytes = overrides
233 .http_max_body_bytes
234 .unwrap_or(DEFAULT_MAX_BODY_BYTES);
235 let proxy = HttpProxyConfig::from_overrides(overrides)?;
236
237 Self::new(
238 bind_address,
239 request_timeout,
240 shutdown_timeout,
241 max_body_bytes,
242 proxy,
243 )
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum HttpConfigError {
250 InvalidBindAddress { value: String },
251 EphemeralPort,
252 ZeroDuration { field: &'static str },
253 ZeroMaxBodyBytes,
254 InvalidProxyUrl,
255 EmptyNoProxyRule,
256}
257
258impl fmt::Display for HttpConfigError {
259 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
260 match self {
261 Self::InvalidBindAddress { value } => {
262 write!(formatter, "bind address '{value}' is not host:port")
263 }
264 Self::EphemeralPort => write!(formatter, "bind address must use an explicit port"),
265 Self::ZeroDuration { field } => write!(formatter, "{field} must be greater than zero"),
266 Self::ZeroMaxBodyBytes => write!(
267 formatter,
268 "max request body bytes must be greater than zero"
269 ),
270 Self::InvalidProxyUrl => write!(
271 formatter,
272 "proxy must use http:// or https:// and include a host"
273 ),
274 Self::EmptyNoProxyRule => write!(formatter, "no-proxy entries must not be empty"),
275 }
276 }
277}
278
279impl Error for HttpConfigError {}
280
281#[derive(Debug)]
283pub enum HttpServeError {
284 Bind(std::io::Error),
285 Serve(std::io::Error),
286 ShutdownTimeout,
287}
288
289impl fmt::Display for HttpServeError {
290 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 Self::Bind(error) => write!(formatter, "failed to bind HTTP listener: {error}"),
293 Self::Serve(error) => write!(formatter, "HTTP server failed: {error}"),
294 Self::ShutdownTimeout => write!(formatter, "HTTP graceful shutdown timed out"),
295 }
296 }
297}
298
299impl Error for HttpServeError {}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
303pub struct HttpConnectionId(u64);
304
305impl HttpConnectionId {
306 pub const fn get(self) -> u64 {
308 self.0
309 }
310}
311
312pub async fn serve_router(
314 router: Router,
315 config: HttpConfig,
316 shutdown: impl Future<Output = ()> + Send + 'static,
317) -> Result<(), HttpServeError> {
318 let listener = tokio::net::TcpListener::bind(config.bind_address.to_string())
319 .await
320 .map_err(HttpServeError::Bind)?;
321
322 serve_listener(listener, router, config, None, shutdown).await
323}
324
325pub async fn serve_router_with_qos(
327 router: Router,
328 config: HttpConfig,
329 qos: QosRuntime,
330 policy: QosPolicy,
331 shutdown: impl Future<Output = ()> + Send + 'static,
332) -> Result<(), HttpServeError> {
333 let listener = tokio::net::TcpListener::bind(config.bind_address.to_string())
334 .await
335 .map_err(HttpServeError::Bind)?;
336 let listener = QosTcpListener::new(listener, qos.clone(), policy);
337
338 serve_listener(listener, router, config, Some(qos), shutdown).await
339}
340
341pub fn router_with_qos_request_admission(
343 router: Router,
344 qos: QosRuntime,
345 policy: QosPolicy,
346) -> Router {
347 router.layer(QosRequestLayer::new(qos, policy))
348}
349
350async fn serve_listener<L>(
351 listener: L,
352 router: Router,
353 config: HttpConfig,
354 timeout_qos: Option<QosRuntime>,
355 shutdown: impl Future<Output = ()> + Send + 'static,
356) -> Result<(), HttpServeError>
357where
358 L: Listener,
359 L::Addr: fmt::Debug,
360{
361 let (shutdown_started, mut shutdown_observed) = tokio::sync::watch::channel(false);
362 let graceful_shutdown = async move {
363 shutdown.await;
364 let _ = shutdown_started.send(true);
365 };
366 let server = axum::serve(
367 listener,
368 HttpMakeService::new(router, config.request_timeout, timeout_qos),
369 )
370 .with_graceful_shutdown(graceful_shutdown)
371 .into_future();
372
373 tokio::pin!(server);
374 tokio::select! {
375 result = &mut server => result.map_err(HttpServeError::Serve),
376 changed = shutdown_observed.changed() => {
377 let _ = changed;
378 match tokio::time::timeout(config.graceful_shutdown_timeout, &mut server).await {
379 Ok(result) => result.map_err(HttpServeError::Serve),
380 Err(_) => Err(HttpServeError::ShutdownTimeout),
381 }
382 }
383 }
384}
385
386struct HttpMakeService {
387 router: Router,
388 request_timeout: Duration,
389 next_connection_id: Arc<AtomicU64>,
390 timeout_qos: Option<QosRuntime>,
391}
392
393impl HttpMakeService {
394 fn new(router: Router, request_timeout: Duration, timeout_qos: Option<QosRuntime>) -> Self {
395 Self {
396 router,
397 request_timeout,
398 next_connection_id: Arc::new(AtomicU64::new(1)),
399 timeout_qos,
400 }
401 }
402}
403
404impl<'a, L> Service<IncomingStream<'a, L>> for HttpMakeService
405where
406 L: Listener,
407{
408 type Response = HttpConnectionService<Router>;
409 type Error = Infallible;
410 type Future = Ready<Result<Self::Response, Self::Error>>;
411
412 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
413 Poll::Ready(Ok(()))
414 }
415
416 fn call(&mut self, _target: IncomingStream<'a, L>) -> Self::Future {
417 let connection_id =
418 HttpConnectionId(self.next_connection_id.fetch_add(1, Ordering::Relaxed));
419 ready(Ok(HttpConnectionService::new(
420 self.router.clone(),
421 connection_id,
422 self.request_timeout,
423 self.timeout_qos.clone(),
424 )))
425 }
426}
427
428struct HttpConnectionService<S> {
429 inner: S,
430 connection_id: HttpConnectionId,
431 request_timeout: Duration,
432 timeout_qos: Option<QosRuntime>,
433}
434
435impl<S> HttpConnectionService<S> {
436 fn new(
437 inner: S,
438 connection_id: HttpConnectionId,
439 request_timeout: Duration,
440 timeout_qos: Option<QosRuntime>,
441 ) -> Self {
442 Self {
443 inner,
444 connection_id,
445 request_timeout,
446 timeout_qos,
447 }
448 }
449}
450
451impl<S> Clone for HttpConnectionService<S>
452where
453 S: Clone,
454{
455 fn clone(&self) -> Self {
456 Self {
457 inner: self.inner.clone(),
458 connection_id: self.connection_id,
459 request_timeout: self.request_timeout,
460 timeout_qos: self.timeout_qos.clone(),
461 }
462 }
463}
464
465impl<S> Service<Request> for HttpConnectionService<S>
466where
467 S: Service<Request, Response = Response, Error = Infallible> + Send,
468 S::Future: Send + 'static,
469{
470 type Response = Response;
471 type Error = Infallible;
472 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
473
474 fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
475 self.inner.poll_ready(context)
476 }
477
478 fn call(&mut self, mut request: Request) -> Self::Future {
479 request.extensions_mut().insert(self.connection_id);
480 let future = self.inner.call(request);
481 let request_timeout = self.request_timeout;
482 let timeout_qos = self.timeout_qos.clone();
483 Box::pin(async move {
484 match tokio::time::timeout(request_timeout, future).await {
485 Ok(result) => result,
486 Err(_) => {
487 if let Some(qos) = timeout_qos {
488 qos.record_timed_out();
489 }
490 Ok((StatusCode::REQUEST_TIMEOUT, "request timed out").into_response())
491 }
492 }
493 })
494 }
495}
496
497pub(crate) fn qos_request_context_active() -> bool {
498 QOS_REQUEST_CONTEXT.try_with(|_| ()).is_ok()
499}
500
501struct QosTcpListener {
502 inner: tokio::net::TcpListener,
503 qos: QosRuntime,
504 policy: QosPolicy,
505}
506
507impl QosTcpListener {
508 fn new(inner: tokio::net::TcpListener, qos: QosRuntime, policy: QosPolicy) -> Self {
509 Self { inner, qos, policy }
510 }
511}
512
513impl Listener for QosTcpListener {
514 type Io = QosTcpStream;
515 type Addr = std::net::SocketAddr;
516
517 async fn accept(&mut self) -> (Self::Io, Self::Addr) {
518 loop {
519 match self.inner.accept().await {
520 Ok((stream, address)) => match self.qos.admit_connection(&self.policy) {
521 Ok(permit) => {
522 return (
523 QosTcpStream {
524 inner: stream,
525 _permit: permit,
526 },
527 address,
528 );
529 }
530 Err(_) => {
531 self.qos.record_dropped();
532 drop(stream)
533 }
534 },
535 Err(_) => tokio::time::sleep(Duration::from_secs(1)).await,
536 }
537 }
538 }
539
540 fn local_addr(&self) -> io::Result<Self::Addr> {
541 self.inner.local_addr()
542 }
543}
544
545struct QosTcpStream {
546 inner: tokio::net::TcpStream,
547 _permit: QosPermit,
548}
549
550impl AsyncRead for QosTcpStream {
551 fn poll_read(
552 mut self: Pin<&mut Self>,
553 context: &mut Context<'_>,
554 buffer: &mut ReadBuf<'_>,
555 ) -> Poll<io::Result<()>> {
556 Pin::new(&mut self.inner).poll_read(context, buffer)
557 }
558}
559
560impl AsyncWrite for QosTcpStream {
561 fn poll_write(
562 mut self: Pin<&mut Self>,
563 context: &mut Context<'_>,
564 buffer: &[u8],
565 ) -> Poll<io::Result<usize>> {
566 Pin::new(&mut self.inner).poll_write(context, buffer)
567 }
568
569 fn poll_flush(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
570 Pin::new(&mut self.inner).poll_flush(context)
571 }
572
573 fn poll_shutdown(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
574 Pin::new(&mut self.inner).poll_shutdown(context)
575 }
576}
577
578fn validate_proxy_url(value: &str) -> Result<(), HttpConfigError> {
579 let Some((scheme, remainder)) = value.split_once("://") else {
580 return Err(HttpConfigError::InvalidProxyUrl);
581 };
582
583 if !matches!(scheme, "http" | "https") {
584 return Err(HttpConfigError::InvalidProxyUrl);
585 }
586
587 let authority = remainder.split('/').next().unwrap_or_default();
588 if authority.is_empty() || authority.starts_with('@') {
589 return Err(HttpConfigError::InvalidProxyUrl);
590 }
591 let host_port = authority
592 .rsplit_once('@')
593 .map_or(authority, |(_, host)| host);
594 let host = if let Some(remainder) = host_port.strip_prefix('[') {
595 remainder.split_once(']').map_or("", |(host, _)| host)
596 } else {
597 host_port.split(':').next().unwrap_or_default()
598 };
599 if host.is_empty() {
600 return Err(HttpConfigError::InvalidProxyUrl);
601 }
602
603 Ok(())
604}
605
606fn parse_no_proxy_rules(value: Option<&str>) -> Result<Vec<String>, HttpConfigError> {
607 value
608 .map(|rules| {
609 rules
610 .split(',')
611 .map(str::trim)
612 .map(|rule| {
613 if rule.is_empty() {
614 Err(HttpConfigError::EmptyNoProxyRule)
615 } else {
616 Ok(rule.to_owned())
617 }
618 })
619 .collect()
620 })
621 .unwrap_or_else(|| Ok(Vec::new()))
622}
623
624fn is_local_bind(bind: &str) -> bool {
625 is_loopback_host(authority_host(bind))
626}
627
628fn authority_host(authority: &str) -> &str {
629 if let Some(remainder) = authority.strip_prefix('[') {
630 return remainder
631 .find(']')
632 .map_or(authority, |index| &remainder[..index]);
633 }
634
635 authority
636 .rsplit_once(':')
637 .map_or(authority, |(host, _)| host)
638}
639
640fn is_loopback_host(host: &str) -> bool {
641 host.eq_ignore_ascii_case("localhost")
642 || host
643 .parse::<IpAddr>()
644 .is_ok_and(|address| address.is_loopback())
645}
646
647#[cfg(test)]
648#[path = "mod_tests.rs"]
649mod tests;