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 qos_admission;
26mod qos_client;
27
28use axum::{
29 Router,
30 extract::Request,
31 http::StatusCode,
32 response::{IntoResponse, Response},
33 serve::{IncomingStream, Listener},
34};
35use serde_json::Value;
36use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
37use tower::Service;
38
39use qos_admission::QosRequestLayer;
40
41use crate::{
42 env::NetworkEnvOverrides,
43 net::qos::{QosPermit, QosPolicy, QosRuntime, RejectReason},
44};
45
46pub use qos_client::{QosHttpClientError, QosHttpResponse, send_request_with_qos};
47
48tokio::task_local! {
49 static QOS_REQUEST_CONTEXT: ();
50}
51
52pub const DEFAULT_HTTP_BIND: &str = "127.0.0.1:8791";
53pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
54pub const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
55pub const DEFAULT_MAX_BODY_BYTES: u64 = 1_048_576;
56pub const DEFAULT_SSL_VERIFY: bool = true;
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct HttpConfig {
61 pub bind_address: HttpBindAddress,
62 pub request_timeout: Duration,
63 pub graceful_shutdown_timeout: Duration,
64 pub max_request_body_bytes: u64,
65 pub proxy: HttpProxyConfig,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct HttpBindAddress {
71 value: String,
72 port: u16,
73}
74
75impl HttpBindAddress {
76 pub fn parse(value: &str) -> Result<Self, HttpConfigError> {
78 let trimmed = value.trim();
79 if trimmed.is_empty() {
80 return Err(HttpConfigError::InvalidBindAddress {
81 value: value.to_owned(),
82 });
83 }
84
85 if let Ok(socket_addr) = trimmed.parse::<std::net::SocketAddr>() {
86 return Self::from_parts(trimmed.to_owned(), socket_addr.port());
87 }
88
89 let Some((host, port)) = trimmed.rsplit_once(':') else {
90 return Err(HttpConfigError::InvalidBindAddress {
91 value: value.to_owned(),
92 });
93 };
94
95 if host.is_empty() || host.contains('/') || host.contains(char::is_whitespace) {
96 return Err(HttpConfigError::InvalidBindAddress {
97 value: value.to_owned(),
98 });
99 }
100
101 let port = port
102 .parse::<u16>()
103 .map_err(|_| HttpConfigError::InvalidBindAddress {
104 value: value.to_owned(),
105 })?;
106
107 Self::from_parts(trimmed.to_owned(), port)
108 }
109
110 pub const fn port(&self) -> u16 {
112 self.port
113 }
114
115 fn from_parts(value: String, port: u16) -> Result<Self, HttpConfigError> {
116 if port == 0 {
117 return Err(HttpConfigError::EphemeralPort);
118 }
119
120 Ok(Self { value, port })
121 }
122}
123
124impl fmt::Display for HttpBindAddress {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 formatter.write_str(&self.value)
127 }
128}
129
130pub fn remote_clients_allowed(config: &HttpConfig, allow_remote_clients: bool) -> bool {
132 allow_remote_clients || is_local_bind(&config.bind_address.to_string())
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct HttpProxyConfig {
138 pub proxy: Option<String>,
139 pub no_proxy_rules: Vec<String>,
140 pub ssl_verify: bool,
141}
142
143pub fn outbound_json_client(config: &HttpConfig) -> Result<reqwest::Client, OutboundClientError> {
145 outbound_json_client_with_policy(config, None, None)
146}
147
148pub fn outbound_json_client_with_policy(
150 config: &HttpConfig,
151 ssl_verify: Option<bool>,
152 connect_timeout: Option<Duration>,
153) -> Result<reqwest::Client, OutboundClientError> {
154 let mut builder = reqwest::Client::builder()
155 .timeout(config.request_timeout)
156 .danger_accept_invalid_certs(!ssl_verify.unwrap_or(config.proxy.ssl_verify));
157 if let Some(timeout) = connect_timeout {
158 builder = builder.connect_timeout(timeout);
159 }
160 if let Some(proxy_url) = &config.proxy.proxy {
161 let no_proxy = reqwest::NoProxy::from_string(&config.proxy.no_proxy_rules.join(","));
162 let proxy = reqwest::Proxy::all(proxy_url)
163 .map_err(|error| OutboundClientError {
164 message: error.to_string(),
165 })?
166 .no_proxy(no_proxy);
167 builder = builder.proxy(proxy);
168 }
169
170 builder.build().map_err(|error| OutboundClientError {
171 message: error.to_string(),
172 })
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct OutboundClientError {
177 pub message: String,
178}
179impl fmt::Display for OutboundClientError {
180 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181 self.message.fmt(formatter)
182 }
183}
184
185impl Error for OutboundClientError {}
186
187impl HttpProxyConfig {
188 pub fn new(
190 proxy: Option<String>,
191 no_proxy_rules: Vec<String>,
192 ssl_verify: bool,
193 ) -> Result<Self, HttpConfigError> {
194 if let Some(proxy_url) = proxy.as_deref() {
195 validate_proxy_url(proxy_url)?;
196 }
197
198 for rule in &no_proxy_rules {
199 if rule.trim().is_empty() {
200 return Err(HttpConfigError::EmptyNoProxyRule);
201 }
202 }
203
204 Ok(Self {
205 proxy,
206 no_proxy_rules,
207 ssl_verify,
208 })
209 }
210
211 pub fn from_overrides(overrides: &NetworkEnvOverrides) -> Result<Self, HttpConfigError> {
213 Self::new(
214 overrides.proxy.clone(),
215 parse_no_proxy_rules(overrides.no_proxy.as_deref())?,
216 overrides.ssl_verify.unwrap_or(DEFAULT_SSL_VERIFY),
217 )
218 }
219
220 pub fn is_proxy_configured(&self) -> bool {
222 self.proxy.is_some()
223 }
224}
225
226impl HttpConfig {
227 pub fn new(
229 bind_address: HttpBindAddress,
230 request_timeout: Duration,
231 graceful_shutdown_timeout: Duration,
232 max_request_body_bytes: u64,
233 proxy: HttpProxyConfig,
234 ) -> Result<Self, HttpConfigError> {
235 if request_timeout.is_zero() {
236 return Err(HttpConfigError::ZeroDuration {
237 field: "request_timeout",
238 });
239 }
240
241 if graceful_shutdown_timeout.is_zero() {
242 return Err(HttpConfigError::ZeroDuration {
243 field: "graceful_shutdown_timeout",
244 });
245 }
246
247 if max_request_body_bytes == 0 {
248 return Err(HttpConfigError::ZeroMaxBodyBytes);
249 }
250
251 Ok(Self {
252 bind_address,
253 request_timeout,
254 graceful_shutdown_timeout,
255 max_request_body_bytes,
256 proxy,
257 })
258 }
259
260 pub fn from_overrides(overrides: &NetworkEnvOverrides) -> Result<Self, HttpConfigError> {
262 let bind_value = overrides.http_bind.as_deref().unwrap_or(DEFAULT_HTTP_BIND);
263 let bind_address = HttpBindAddress::parse(bind_value)?;
264 let request_timeout = overrides
265 .http_request_timeout_ms
266 .map(Duration::from_millis)
267 .unwrap_or(DEFAULT_REQUEST_TIMEOUT);
268 let shutdown_timeout = overrides
269 .http_shutdown_timeout_ms
270 .map(Duration::from_millis)
271 .unwrap_or(DEFAULT_SHUTDOWN_TIMEOUT);
272 let max_body_bytes = overrides
273 .http_max_body_bytes
274 .unwrap_or(DEFAULT_MAX_BODY_BYTES);
275 let proxy = HttpProxyConfig::from_overrides(overrides)?;
276
277 Self::new(
278 bind_address,
279 request_timeout,
280 shutdown_timeout,
281 max_body_bytes,
282 proxy,
283 )
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
289pub enum HttpConfigError {
290 InvalidBindAddress { value: String },
291 EphemeralPort,
292 ZeroDuration { field: &'static str },
293 ZeroMaxBodyBytes,
294 InvalidProxyUrl,
295 EmptyNoProxyRule,
296}
297
298impl fmt::Display for HttpConfigError {
299 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
300 match self {
301 Self::InvalidBindAddress { value } => {
302 write!(formatter, "bind address '{value}' is not host:port")
303 }
304 Self::EphemeralPort => write!(formatter, "bind address must use an explicit port"),
305 Self::ZeroDuration { field } => write!(formatter, "{field} must be greater than zero"),
306 Self::ZeroMaxBodyBytes => write!(
307 formatter,
308 "max request body bytes must be greater than zero"
309 ),
310 Self::InvalidProxyUrl => write!(
311 formatter,
312 "proxy must use http:// or https:// and include a host"
313 ),
314 Self::EmptyNoProxyRule => write!(formatter, "no-proxy entries must not be empty"),
315 }
316 }
317}
318
319impl Error for HttpConfigError {}
320
321#[derive(Debug)]
323pub enum HttpServeError {
324 Bind(std::io::Error),
325 Serve(std::io::Error),
326 ShutdownTimeout,
327}
328
329impl fmt::Display for HttpServeError {
330 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
331 match self {
332 Self::Bind(error) => write!(formatter, "failed to bind HTTP listener: {error}"),
333 Self::Serve(error) => write!(formatter, "HTTP server failed: {error}"),
334 Self::ShutdownTimeout => write!(formatter, "HTTP graceful shutdown timed out"),
335 }
336 }
337}
338
339impl Error for HttpServeError {}
340
341#[derive(Debug)]
343pub enum HttpClientError {
344 InvalidUrl(String),
345 QosRejected(RejectReason),
346 Io(io::Error),
347 Timeout,
348 InvalidResponse,
349 ResponseStatus(u16),
350 ResponseJson(serde_json::Error),
351}
352
353impl fmt::Display for HttpClientError {
354 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
355 match self {
356 Self::InvalidUrl(value) => write!(formatter, "invalid HTTP worker URL: {value}"),
357 Self::QosRejected(reason) => write!(
358 formatter,
359 "HTTP worker request rejected by QoS: {}",
360 reason.as_str()
361 ),
362 Self::Io(error) => write!(formatter, "HTTP worker request failed: {error}"),
363 Self::Timeout => write!(formatter, "HTTP worker request timed out"),
364 Self::InvalidResponse => write!(formatter, "HTTP worker returned invalid response"),
365 Self::ResponseStatus(status) => {
366 write!(formatter, "HTTP worker returned status {status}")
367 }
368 Self::ResponseJson(error) => {
369 write!(formatter, "HTTP worker returned invalid JSON: {error}")
370 }
371 }
372 }
373}
374
375impl Error for HttpClientError {}
376
377pub async fn post_json(
379 config: &HttpConfig,
380 url: &str,
381 payload: &Value,
382) -> Result<Value, HttpClientError> {
383 let request = JsonHttpRequest::parse(url)?;
384 let body = serde_json::to_vec(payload).map_err(HttpClientError::ResponseJson)?;
385 let response = tokio::time::timeout(config.request_timeout, send_json_request(request, body))
386 .await
387 .map_err(|_| HttpClientError::Timeout)??;
388
389 serde_json::from_slice(&response).map_err(HttpClientError::ResponseJson)
390}
391
392pub async fn post_json_with_qos(
394 config: &HttpConfig,
395 qos: &QosRuntime,
396 policy: &QosPolicy,
397 url: &str,
398 payload: &Value,
399) -> Result<Value, HttpClientError> {
400 let permit = if qos_request_context_active() {
401 None
402 } else {
403 Some(
404 qos.admit_request(policy)
405 .map_err(HttpClientError::QosRejected)?,
406 )
407 };
408 let result = post_json(config, url, payload).await;
409 drop(permit);
410 if matches!(result, Err(HttpClientError::Timeout)) {
411 qos.record_timed_out();
412 }
413
414 result
415}
416
417struct JsonHttpRequest {
418 host: String,
419 port: u16,
420 path: String,
421}
422
423impl JsonHttpRequest {
424 fn parse(value: &str) -> Result<Self, HttpClientError> {
425 let remainder = value
426 .strip_prefix("http://")
427 .ok_or_else(|| HttpClientError::InvalidUrl(value.to_owned()))?;
428 let (authority, path) = remainder
429 .split_once('/')
430 .map_or((remainder, "/"), |(authority, path)| {
431 (authority, path.trim_start_matches('/'))
432 });
433 if authority.is_empty() {
434 return Err(HttpClientError::InvalidUrl(value.to_owned()));
435 }
436 let (host, port) = authority
437 .rsplit_once(':')
438 .map(|(host, port)| {
439 let parsed_port = port
440 .parse::<u16>()
441 .map_err(|_| HttpClientError::InvalidUrl(value.to_owned()))?;
442 Ok((host.to_owned(), parsed_port))
443 })
444 .unwrap_or_else(|| Ok((authority.to_owned(), 80)))?;
445 if host.is_empty() || port == 0 {
446 return Err(HttpClientError::InvalidUrl(value.to_owned()));
447 }
448 let path = if path.is_empty() {
449 "/".to_owned()
450 } else {
451 format!("/{path}")
452 };
453
454 Ok(Self { host, port, path })
455 }
456}
457
458async fn send_json_request(
459 request: JsonHttpRequest,
460 body: Vec<u8>,
461) -> Result<Vec<u8>, HttpClientError> {
462 let mut stream = tokio::net::TcpStream::connect((request.host.as_str(), request.port))
463 .await
464 .map_err(HttpClientError::Io)?;
465 let head = format!(
466 "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nAccept: application/json\r\nConnection: close\r\nContent-Length: {}\r\n\r\n",
467 request.path,
468 request.host,
469 body.len()
470 );
471 stream
472 .write_all(head.as_bytes())
473 .await
474 .map_err(HttpClientError::Io)?;
475 stream.write_all(&body).await.map_err(HttpClientError::Io)?;
476 stream.shutdown().await.map_err(HttpClientError::Io)?;
477 let mut response = Vec::new();
478 stream
479 .read_to_end(&mut response)
480 .await
481 .map_err(HttpClientError::Io)?;
482 parse_http_response(response)
483}
484
485fn parse_http_response(response: Vec<u8>) -> Result<Vec<u8>, HttpClientError> {
486 let Some(header_end) = response.windows(4).position(|window| window == b"\r\n\r\n") else {
487 return Err(HttpClientError::InvalidResponse);
488 };
489 let headers = std::str::from_utf8(&response[..header_end])
490 .map_err(|_| HttpClientError::InvalidResponse)?;
491 let status = headers
492 .lines()
493 .next()
494 .and_then(|line| line.split_whitespace().nth(1))
495 .and_then(|value| value.parse::<u16>().ok())
496 .ok_or(HttpClientError::InvalidResponse)?;
497 if !(200..300).contains(&status) {
498 return Err(HttpClientError::ResponseStatus(status));
499 }
500
501 Ok(response[header_end + 4..].to_vec())
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
506pub struct HttpConnectionId(u64);
507
508impl HttpConnectionId {
509 pub const fn get(self) -> u64 {
511 self.0
512 }
513}
514
515pub async fn serve_router(
517 router: Router,
518 config: HttpConfig,
519 shutdown: impl Future<Output = ()> + Send + 'static,
520) -> Result<(), HttpServeError> {
521 let listener = tokio::net::TcpListener::bind(config.bind_address.to_string())
522 .await
523 .map_err(HttpServeError::Bind)?;
524
525 serve_listener(listener, router, config, None, shutdown).await
526}
527
528pub async fn serve_router_with_qos(
530 router: Router,
531 config: HttpConfig,
532 qos: QosRuntime,
533 policy: QosPolicy,
534 shutdown: impl Future<Output = ()> + Send + 'static,
535) -> Result<(), HttpServeError> {
536 let listener = tokio::net::TcpListener::bind(config.bind_address.to_string())
537 .await
538 .map_err(HttpServeError::Bind)?;
539 let listener = QosTcpListener::new(listener, qos.clone(), policy);
540
541 serve_listener(listener, router, config, Some(qos), shutdown).await
542}
543
544pub fn router_with_qos_request_admission(
546 router: Router,
547 qos: QosRuntime,
548 policy: QosPolicy,
549) -> Router {
550 router.layer(QosRequestLayer::new(qos, policy))
551}
552
553async fn serve_listener<L>(
554 listener: L,
555 router: Router,
556 config: HttpConfig,
557 timeout_qos: Option<QosRuntime>,
558 shutdown: impl Future<Output = ()> + Send + 'static,
559) -> Result<(), HttpServeError>
560where
561 L: Listener,
562 L::Addr: fmt::Debug,
563{
564 let (shutdown_started, mut shutdown_observed) = tokio::sync::watch::channel(false);
565 let graceful_shutdown = async move {
566 shutdown.await;
567 let _ = shutdown_started.send(true);
568 };
569 let server = axum::serve(
570 listener,
571 HttpMakeService::new(router, config.request_timeout, timeout_qos),
572 )
573 .with_graceful_shutdown(graceful_shutdown)
574 .into_future();
575
576 tokio::pin!(server);
577 tokio::select! {
578 result = &mut server => result.map_err(HttpServeError::Serve),
579 changed = shutdown_observed.changed() => {
580 let _ = changed;
581 match tokio::time::timeout(config.graceful_shutdown_timeout, &mut server).await {
582 Ok(result) => result.map_err(HttpServeError::Serve),
583 Err(_) => Err(HttpServeError::ShutdownTimeout),
584 }
585 }
586 }
587}
588
589struct HttpMakeService {
590 router: Router,
591 request_timeout: Duration,
592 next_connection_id: Arc<AtomicU64>,
593 timeout_qos: Option<QosRuntime>,
594}
595
596impl HttpMakeService {
597 fn new(router: Router, request_timeout: Duration, timeout_qos: Option<QosRuntime>) -> Self {
598 Self {
599 router,
600 request_timeout,
601 next_connection_id: Arc::new(AtomicU64::new(1)),
602 timeout_qos,
603 }
604 }
605}
606
607impl<'a, L> Service<IncomingStream<'a, L>> for HttpMakeService
608where
609 L: Listener,
610{
611 type Response = HttpConnectionService<Router>;
612 type Error = Infallible;
613 type Future = Ready<Result<Self::Response, Self::Error>>;
614
615 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
616 Poll::Ready(Ok(()))
617 }
618
619 fn call(&mut self, _target: IncomingStream<'a, L>) -> Self::Future {
620 let connection_id =
621 HttpConnectionId(self.next_connection_id.fetch_add(1, Ordering::Relaxed));
622 ready(Ok(HttpConnectionService::new(
623 self.router.clone(),
624 connection_id,
625 self.request_timeout,
626 self.timeout_qos.clone(),
627 )))
628 }
629}
630
631struct HttpConnectionService<S> {
632 inner: S,
633 connection_id: HttpConnectionId,
634 request_timeout: Duration,
635 timeout_qos: Option<QosRuntime>,
636}
637
638impl<S> HttpConnectionService<S> {
639 fn new(
640 inner: S,
641 connection_id: HttpConnectionId,
642 request_timeout: Duration,
643 timeout_qos: Option<QosRuntime>,
644 ) -> Self {
645 Self {
646 inner,
647 connection_id,
648 request_timeout,
649 timeout_qos,
650 }
651 }
652}
653
654impl<S> Clone for HttpConnectionService<S>
655where
656 S: Clone,
657{
658 fn clone(&self) -> Self {
659 Self {
660 inner: self.inner.clone(),
661 connection_id: self.connection_id,
662 request_timeout: self.request_timeout,
663 timeout_qos: self.timeout_qos.clone(),
664 }
665 }
666}
667
668impl<S> Service<Request> for HttpConnectionService<S>
669where
670 S: Service<Request, Response = Response, Error = Infallible> + Send,
671 S::Future: Send + 'static,
672{
673 type Response = Response;
674 type Error = Infallible;
675 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
676
677 fn poll_ready(&mut self, context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
678 self.inner.poll_ready(context)
679 }
680
681 fn call(&mut self, mut request: Request) -> Self::Future {
682 request.extensions_mut().insert(self.connection_id);
683 let future = self.inner.call(request);
684 let request_timeout = self.request_timeout;
685 let timeout_qos = self.timeout_qos.clone();
686 Box::pin(async move {
687 match tokio::time::timeout(request_timeout, future).await {
688 Ok(result) => result,
689 Err(_) => {
690 if let Some(qos) = timeout_qos {
691 qos.record_timed_out();
692 }
693 Ok((StatusCode::REQUEST_TIMEOUT, "request timed out").into_response())
694 }
695 }
696 })
697 }
698}
699
700pub(crate) fn qos_request_context_active() -> bool {
701 QOS_REQUEST_CONTEXT.try_with(|_| ()).is_ok()
702}
703
704struct QosTcpListener {
705 inner: tokio::net::TcpListener,
706 qos: QosRuntime,
707 policy: QosPolicy,
708}
709
710impl QosTcpListener {
711 fn new(inner: tokio::net::TcpListener, qos: QosRuntime, policy: QosPolicy) -> Self {
712 Self { inner, qos, policy }
713 }
714}
715
716impl Listener for QosTcpListener {
717 type Io = QosTcpStream;
718 type Addr = std::net::SocketAddr;
719
720 async fn accept(&mut self) -> (Self::Io, Self::Addr) {
721 loop {
722 match self.inner.accept().await {
723 Ok((stream, address)) => match self.qos.admit_connection(&self.policy) {
724 Ok(permit) => {
725 return (
726 QosTcpStream {
727 inner: stream,
728 _permit: permit,
729 },
730 address,
731 );
732 }
733 Err(_) => {
734 self.qos.record_dropped();
735 drop(stream)
736 }
737 },
738 Err(_) => tokio::time::sleep(Duration::from_secs(1)).await,
739 }
740 }
741 }
742
743 fn local_addr(&self) -> io::Result<Self::Addr> {
744 self.inner.local_addr()
745 }
746}
747
748struct QosTcpStream {
749 inner: tokio::net::TcpStream,
750 _permit: QosPermit,
751}
752
753impl AsyncRead for QosTcpStream {
754 fn poll_read(
755 mut self: Pin<&mut Self>,
756 context: &mut Context<'_>,
757 buffer: &mut ReadBuf<'_>,
758 ) -> Poll<io::Result<()>> {
759 Pin::new(&mut self.inner).poll_read(context, buffer)
760 }
761}
762
763impl AsyncWrite for QosTcpStream {
764 fn poll_write(
765 mut self: Pin<&mut Self>,
766 context: &mut Context<'_>,
767 buffer: &[u8],
768 ) -> Poll<io::Result<usize>> {
769 Pin::new(&mut self.inner).poll_write(context, buffer)
770 }
771
772 fn poll_flush(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
773 Pin::new(&mut self.inner).poll_flush(context)
774 }
775
776 fn poll_shutdown(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
777 Pin::new(&mut self.inner).poll_shutdown(context)
778 }
779}
780
781fn validate_proxy_url(value: &str) -> Result<(), HttpConfigError> {
782 let Some((scheme, remainder)) = value.split_once("://") else {
783 return Err(HttpConfigError::InvalidProxyUrl);
784 };
785
786 if !matches!(scheme, "http" | "https") {
787 return Err(HttpConfigError::InvalidProxyUrl);
788 }
789
790 let authority = remainder.split('/').next().unwrap_or_default();
791 if authority.is_empty() || authority.starts_with('@') {
792 return Err(HttpConfigError::InvalidProxyUrl);
793 }
794 let host_port = authority
795 .rsplit_once('@')
796 .map_or(authority, |(_, host)| host);
797 let host = if let Some(remainder) = host_port.strip_prefix('[') {
798 remainder.split_once(']').map_or("", |(host, _)| host)
799 } else {
800 host_port.split(':').next().unwrap_or_default()
801 };
802 if host.is_empty() {
803 return Err(HttpConfigError::InvalidProxyUrl);
804 }
805
806 Ok(())
807}
808
809fn parse_no_proxy_rules(value: Option<&str>) -> Result<Vec<String>, HttpConfigError> {
810 value
811 .map(|rules| {
812 rules
813 .split(',')
814 .map(str::trim)
815 .map(|rule| {
816 if rule.is_empty() {
817 Err(HttpConfigError::EmptyNoProxyRule)
818 } else {
819 Ok(rule.to_owned())
820 }
821 })
822 .collect()
823 })
824 .unwrap_or_else(|| Ok(Vec::new()))
825}
826
827fn is_local_bind(bind: &str) -> bool {
828 is_loopback_host(authority_host(bind))
829}
830
831fn authority_host(authority: &str) -> &str {
832 if let Some(remainder) = authority.strip_prefix('[') {
833 return remainder
834 .find(']')
835 .map_or(authority, |index| &remainder[..index]);
836 }
837
838 authority
839 .rsplit_once(':')
840 .map_or(authority, |(host, _)| host)
841}
842
843fn is_loopback_host(host: &str) -> bool {
844 host.eq_ignore_ascii_case("localhost")
845 || host
846 .parse::<IpAddr>()
847 .is_ok_and(|address| address.is_loopback())
848}
849
850#[cfg(test)]
851#[path = "http_tests.rs"]
852mod tests;