1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use std::borrow::Cow;
5use std::future;
6use std::net::{SocketAddr, ToSocketAddrs};
7use std::str::FromStr;
8use std::sync::Arc;
9
10use anyhow::anyhow;
11use bytes::Bytes;
12use http::Extensions;
13use http::header::{HeaderName, HeaderValue};
14use http_acl::utils::authority::{Authority, Host};
15use reqwest::{
16 Body, Request, Response, ResponseBuilderExt,
17 dns::{Name, Resolve, Resolving},
18 redirect,
19};
20use reqwest_middleware::{Error, Middleware, Next};
21use thiserror::Error;
22
23pub use http_acl::{
24 self, HttpAcl, HttpAclBuilder, HttpAclHooks, ModifyRequestFn, ModifyResponseFn,
25 RequestMutation, ResponseMutation, ValidateFn,
26};
27
28#[derive(Debug, Clone)]
29pub struct HttpAclMiddleware {
54 acl: Arc<HttpAcl>,
55}
56
57impl HttpAclMiddleware {
58 pub fn new(acl: HttpAcl) -> Self {
60 Self { acl: Arc::new(acl) }
61 }
62
63 pub fn acl(&self) -> Arc<HttpAcl> {
65 self.acl.clone()
66 }
67
68 pub fn dns_resolver(&self) -> Arc<HttpAclDnsResolver> {
76 Arc::new(HttpAclDnsResolver::new(self))
77 }
78
79 pub fn with_dns_resolver(&self, dns_resolver: Arc<dyn Resolve>) -> Arc<HttpAclDnsResolver> {
82 Arc::new(HttpAclDnsResolver::with_dns_resolver(self, dns_resolver))
83 }
84
85 pub fn redirect_policy(&self) -> redirect::Policy {
126 self.redirect_policy_with_max(10)
127 }
128
129 pub fn redirect_policy_with_max(&self, max_redirects: usize) -> redirect::Policy {
131 let acl = self.acl.clone();
132 redirect::Policy::custom(move |attempt| {
133 let deny_reason = 'reason: {
137 if attempt.previous().len() > max_redirects {
138 break 'reason Some("too many redirects".to_string());
139 }
140
141 let url = attempt.url();
142
143 let scheme = url.scheme();
144 if acl.is_scheme_allowed(scheme).is_denied() {
145 break 'reason Some(format!("scheme {scheme} is denied"));
146 }
147
148 let Some(host) = url.host() else {
149 break 'reason Some("missing host".to_string());
150 };
151
152 match host {
153 url::Host::Domain(domain) => {
154 if acl.is_host_allowed(domain).is_denied() {
155 break 'reason Some(format!("host {domain} is denied"));
156 }
157 }
158 url::Host::Ipv4(ip) => {
159 let ip = std::net::IpAddr::V4(ip);
160 if acl.is_ip_allowed(&ip).is_denied() {
161 break 'reason Some(format!("ip {ip} is denied"));
162 }
163 }
164 url::Host::Ipv6(ip) => {
165 let ip = std::net::IpAddr::V6(ip);
166 if acl.is_ip_allowed(&ip).is_denied() {
167 break 'reason Some(format!("ip {ip} is denied"));
168 }
169 }
170 }
171
172 if let Some(port) = url.port_or_known_default()
173 && acl.is_port_allowed(port).is_denied()
174 {
175 break 'reason Some(format!("port {port} is denied"));
176 }
177
178 match percent_encoding::percent_decode_str(url.path()).decode_utf8() {
181 Ok(path) => {
182 if acl.is_url_path_allowed(&path).is_denied() {
183 break 'reason Some(format!("path {path} is denied"));
184 }
185 }
186 Err(_) => break 'reason Some("invalid URL path encoding".to_string()),
187 }
188
189 None
190 };
191
192 match deny_reason {
193 Some(reason) => attempt.error(std::io::Error::other(reason)),
194 None => attempt.follow(),
195 }
196 })
197 }
198}
199
200#[async_trait::async_trait]
201impl Middleware for HttpAclMiddleware {
202 async fn handle(
203 &self,
204 mut req: Request,
205 extensions: &mut Extensions,
206 next: Next<'_>,
207 ) -> std::result::Result<Response, Error> {
208 let scheme = req.url().scheme().to_string();
212 let acl_scheme_match = self.acl.is_scheme_allowed(&scheme);
213 if acl_scheme_match.is_denied() {
214 return Err(Error::Middleware(anyhow!(
215 "scheme {} is denied - {}",
216 scheme,
217 acl_scheme_match
218 )));
219 }
220
221 let method = req.method().as_str();
222 let acl_method_match = self.acl.is_method_allowed(method);
223 if acl_method_match.is_denied() {
224 return Err(Error::Middleware(anyhow!(
225 "method {} is denied - {}",
226 method,
227 acl_method_match
228 )));
229 }
230
231 if let Some(host) = req.url().host_str() {
232 let authority = Authority::parse(host)
233 .map_err(|_| Error::Middleware(anyhow!("invalid host: {}", host)))?;
234
235 match &authority.host {
236 Host::Ip(ip) => {
237 let acl_ip_match = self.acl.is_ip_allowed(ip);
238 if acl_ip_match.is_denied() {
239 return Err(Error::Middleware(anyhow!(
240 "ip {} is denied - {}",
241 ip,
242 acl_ip_match
243 )));
244 }
245 }
246 Host::Domain(domain) => {
247 let acl_host_match = self.acl.is_host_allowed(domain);
248 if acl_host_match.is_denied() {
249 return Err(Error::Middleware(anyhow!(
250 "host {} is denied - {}",
251 domain,
252 acl_host_match
253 )));
254 }
255 }
256 }
257
258 if let Some(port) = req.url().port_or_known_default() {
259 let acl_port_match = self.acl.is_port_allowed(port);
260 if acl_port_match.is_denied() {
261 return Err(Error::Middleware(anyhow!(
262 "port {} is denied - {}",
263 port,
264 acl_port_match
265 )));
266 }
267 }
268
269 for (key, value) in req.headers() {
272 let header_name = key.as_str();
273 let acl_header_match = self
274 .acl
275 .is_header_allowed_bytes(header_name, value.as_bytes());
276 if acl_header_match.is_denied() {
277 return Err(Error::Middleware(anyhow!(
278 "header {}: {} is denied - {}",
279 header_name,
280 header_value_lossy(value),
281 acl_header_match
282 )));
283 }
284 }
285
286 let url_path = percent_encoding::percent_decode_str(req.url().path())
289 .decode_utf8()
290 .map_err(|_| Error::Middleware(anyhow!("invalid URL path encoding")))?;
291 let acl_url_path_match = self.acl.is_url_path_allowed(&url_path);
292 if acl_url_path_match.is_denied() {
293 return Err(Error::Middleware(anyhow!(
294 "path {} is denied - {}",
295 url_path,
296 acl_url_path_match
297 )));
298 }
299
300 let lossy_headers: Vec<(&str, Cow<'_, str>)> = req
304 .headers()
305 .iter()
306 .map(|(k, v)| (k.as_str(), header_value_lossy(v)))
307 .collect();
308 let valid_match = self.acl.is_valid(
309 &scheme,
310 &authority,
311 lossy_headers.iter().map(|(k, v)| (*k, v.as_ref())),
312 req.body().and_then(|b| b.as_bytes()),
313 );
314 if valid_match.is_denied() {
315 return Err(Error::Middleware(anyhow!(
316 "request is denied - {}",
317 valid_match
318 )));
319 }
320
321 if self.acl.has_modify_request() {
322 let (headers, mut raw_headers) = mutation_headers(req.headers());
323 let mut mutation = RequestMutation {
324 headers,
325 body: req
326 .body()
327 .and_then(|b| b.as_bytes())
328 .map(Bytes::copy_from_slice),
329 };
330 self.acl.modify_request(&scheme, &authority, &mut mutation);
331
332 req.headers_mut().clear();
333 for (name, value) in &mutation.headers {
334 let header_name = HeaderName::from_str(name).map_err(|e| {
335 Error::Middleware(anyhow!("invalid header name `{name}`: {e}"))
336 })?;
337 let header_value = match take_raw_header(&mut raw_headers, name, value) {
338 Some(raw) => raw,
339 None => HeaderValue::from_str(value).map_err(|e| {
340 Error::Middleware(anyhow!("invalid header value for `{name}`: {e}"))
341 })?,
342 };
343 req.headers_mut().append(header_name, header_value);
344 }
345 if let Some(body) = mutation.body {
346 *req.body_mut() = Some(Body::from(body));
347 }
348 }
349
350 let mut res = next.run(req, extensions).await?;
351
352 if self.acl.has_modify_response() {
353 let status = res.status();
354 let version = res.version();
355 let url = res.url().clone();
356 let extensions_snapshot = res.extensions().clone();
357 let (headers, mut raw_headers) = mutation_headers(res.headers());
358 let body = res.bytes().await?;
361
362 let mut mutation = ResponseMutation {
363 status: status.as_u16(),
364 headers,
365 body,
366 };
367 self.acl.modify_response(&scheme, &authority, &mut mutation);
368
369 let mut builder = http::Response::builder()
370 .status(mutation.status)
371 .version(version);
372 for (name, value) in &mutation.headers {
373 builder = match take_raw_header(&mut raw_headers, name, value) {
374 Some(raw) => builder.header(name.as_str(), raw),
375 None => builder.header(name.as_str(), value.as_str()),
376 };
377 }
378 if let Some(ext) = builder.extensions_mut() {
385 *ext = extensions_snapshot;
386 }
387 let http_response = builder
388 .url(url)
389 .body(mutation.body)
390 .map_err(|e| Error::Middleware(anyhow!("failed to rebuild response: {e}")))?;
391 res = Response::from(http_response);
392 }
393
394 Ok(res)
395 } else {
396 return Err(Error::Middleware(anyhow!("missing host")));
397 }
398 }
399}
400
401fn header_value_lossy(value: &HeaderValue) -> Cow<'_, str> {
404 String::from_utf8_lossy(value.as_bytes())
405}
406
407type RawHeader = (String, String, HeaderValue);
410
411fn mutation_headers(headers: &http::HeaderMap) -> (Vec<(String, String)>, Vec<RawHeader>) {
416 let mut raw = Vec::new();
417 let list = headers
418 .iter()
419 .map(|(k, v)| {
420 let name = k.as_str().to_string();
421 let value = header_value_lossy(v).into_owned();
422 if std::str::from_utf8(v.as_bytes()).is_err() {
423 raw.push((name.clone(), value.clone(), v.clone()));
424 }
425 (name, value)
426 })
427 .collect();
428 (list, raw)
429}
430
431fn take_raw_header(raw: &mut Vec<RawHeader>, name: &str, value: &str) -> Option<HeaderValue> {
434 let index = raw
435 .iter()
436 .position(|(n, v, _)| n.eq_ignore_ascii_case(name) && v == value)?;
437 Some(raw.remove(index).2)
438}
439
440type BoxError = Box<dyn std::error::Error + Send + Sync>;
441
442struct GaiResolver;
443
444impl Resolve for GaiResolver {
445 fn resolve(&self, name: Name) -> Resolving {
446 Box::pin(async move {
447 let addresses = (name.as_str(), 0)
450 .to_socket_addrs()
451 .map_err(|e| Box::new(e) as BoxError)?;
452 Ok(Box::new(addresses.into_iter()) as Box<dyn Iterator<Item = SocketAddr> + Send>)
453 })
454 }
455}
456
457pub struct HttpAclDnsResolver {
471 dns_resolver: Arc<dyn Resolve>,
472 acl: Arc<HttpAcl>,
473}
474
475impl HttpAclDnsResolver {
476 pub fn new(middleware: &HttpAclMiddleware) -> Self {
478 Self {
479 dns_resolver: Arc::new(GaiResolver),
480 acl: middleware.acl(),
481 }
482 }
483
484 pub fn with_dns_resolver(
487 middleware: &HttpAclMiddleware,
488 dns_resolver: Arc<dyn Resolve>,
489 ) -> Self {
490 Self {
491 dns_resolver,
492 acl: middleware.acl(),
493 }
494 }
495}
496
497impl Resolve for HttpAclDnsResolver {
498 fn resolve(&self, name: Name) -> Resolving {
499 if self.acl.is_host_allowed(name.as_str()).is_denied() {
500 let err: BoxError = Box::new(HttpAclError::HostDenied {
501 host: name.as_str().to_string(),
502 });
503 return Box::pin(future::ready(Err(err)));
504 }
505
506 let acl = self.acl.clone();
507 let resolver = self.dns_resolver.clone();
508
509 Box::pin(async move {
510 if let Some(tcp_address) = acl.resolve_trusted_static_dns_mapping(name.as_str()) {
511 Ok(Box::new(std::iter::once(tcp_address))
515 as Box<dyn Iterator<Item = SocketAddr> + Send>)
516 } else if let Some(tcp_address) = acl.resolve_static_dns_mapping(name.as_str()) {
517 if acl.is_ip_allowed(&tcp_address.ip()).is_allowed()
522 && is_resolved_port_allowed(&acl, tcp_address.port())
523 {
524 Ok(Box::new(std::iter::once(tcp_address))
525 as Box<dyn Iterator<Item = SocketAddr> + Send>)
526 } else {
527 let err: BoxError =
528 Box::new(std::io::Error::other("Static DNS mapping denied by ACL"));
529 Err(err)
530 }
531 } else {
532 let resolved = resolver.resolve(name).await;
533 match resolved {
534 Ok(addresses) => {
535 let filtered = addresses
536 .into_iter()
537 .filter(|addr| {
538 acl.is_ip_allowed(&addr.ip()).is_allowed()
539 && is_resolved_port_allowed(&acl, addr.port())
540 })
541 .collect::<Vec<_>>();
542 Ok(Box::new(filtered.into_iter())
543 as Box<dyn Iterator<Item = SocketAddr> + Send>)
544 }
545 Err(e) => Err(e),
546 }
547 }
548 })
549 }
550}
551
552fn is_resolved_port_allowed(acl: &HttpAcl, port: u16) -> bool {
557 port == 0 || acl.is_port_allowed(port).is_allowed()
558}
559
560#[derive(Error, Debug)]
561pub enum HttpAclError {
567 #[error("Host resolution denied by ACL: {host}")]
569 HostDenied {
570 host: String,
572 },
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use http_acl::acl::AclClassification;
579 use reqwest_middleware::ClientWithMiddleware;
580
581 #[tokio::test]
582 async fn test_http_acl_middleware() {
583 let acl = HttpAcl::builder()
584 .add_denied_host("example.com".to_string())
585 .unwrap()
586 .build();
587
588 let middleware = HttpAclMiddleware::new(acl);
589
590 let client = reqwest_middleware::ClientBuilder::new(
591 reqwest::Client::builder()
592 .dns_resolver(middleware.dns_resolver())
593 .build()
594 .unwrap(),
595 )
596 .with(middleware)
597 .build();
598
599 let request = client.get("http://example.com/").send().await;
600
601 assert!(request.is_err());
602 assert_eq!(
603 request.unwrap_err().to_string(),
604 "host example.com is denied - The entity is denied according to the denied ACL."
605 );
606 }
607
608 #[tokio::test]
609 async fn test_middleware_decodes_percent_encoded_path() {
610 let acl = HttpAcl::builder()
611 .add_allowed_host("example.com".to_string())
612 .unwrap()
613 .add_denied_url_path("/secret file".to_string())
614 .unwrap()
615 .build();
616
617 let middleware = HttpAclMiddleware::new(acl);
618
619 let client =
620 reqwest_middleware::ClientBuilder::new(reqwest::Client::builder().build().unwrap())
621 .with(middleware)
622 .build();
623
624 let request = client.get("http://example.com/secret%20file").send().await;
629
630 assert!(request.is_err());
631 assert!(
632 request
633 .unwrap_err()
634 .to_string()
635 .contains("path /secret file is denied")
636 );
637 }
638
639 #[tokio::test]
640 async fn test_dns_resolver_returns_typed_error_for_denied_host() {
641 let acl = HttpAcl::builder()
642 .add_denied_host("denied.example.com".to_string())
643 .unwrap()
644 .build();
645
646 let middleware = HttpAclMiddleware::new(acl);
647 let resolver = middleware.dns_resolver();
648
649 let name: reqwest::dns::Name = "denied.example.com".parse().unwrap();
650 let err = match resolver.resolve(name).await {
651 Ok(_) => panic!("expected resolution to be denied"),
652 Err(e) => e,
653 };
654
655 let acl_err = err
656 .downcast_ref::<HttpAclError>()
657 .expect("expected a HttpAclError");
658 assert!(matches!(
659 acl_err,
660 HttpAclError::HostDenied { host } if host == "denied.example.com"
661 ));
662 }
663
664 struct FixedResolver(Vec<SocketAddr>);
666
667 impl Resolve for FixedResolver {
668 fn resolve(&self, _name: Name) -> Resolving {
669 let addresses = self.0.clone();
670 Box::pin(async move {
671 Ok(Box::new(addresses.into_iter()) as Box<dyn Iterator<Item = SocketAddr> + Send>)
672 })
673 }
674 }
675
676 #[tokio::test]
677 async fn test_dns_resolver_keeps_port_zero_answers() {
678 let acl = HttpAcl::builder()
683 .ip_acl_default(true)
684 .host_acl_default(true)
685 .build();
686 let middleware = HttpAclMiddleware::new(acl);
687 let resolver = middleware.with_dns_resolver(Arc::new(FixedResolver(vec![
688 "1.1.1.1:0".parse().unwrap(),
689 "1.1.1.1:22".parse().unwrap(),
690 "[64:ff9b::7f00:1]:0".parse().unwrap(),
691 ])));
692
693 let name: Name = "jwks.example.com".parse().unwrap();
694 let addresses = match resolver.resolve(name).await {
695 Ok(addresses) => addresses.collect::<Vec<_>>(),
696 Err(e) => panic!("expected resolution to succeed: {e}"),
697 };
698
699 assert_eq!(addresses, vec!["1.1.1.1:0".parse::<SocketAddr>().unwrap()]);
702 }
703
704 #[tokio::test]
705 async fn test_dns_resolver_resolves_hostnames() {
706 use tokio::io::{AsyncReadExt, AsyncWriteExt};
707 use tokio::net::TcpListener;
708
709 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
710 let addr = listener.local_addr().unwrap();
711
712 tokio::spawn(async move {
713 if let Ok((mut socket, _)) = listener.accept().await {
714 let mut buf = [0u8; 1024];
715 let _ = socket.read(&mut buf).await;
716 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
717 let _ = socket.write_all(response.as_bytes()).await;
718 }
719 });
720
721 let acl = HttpAcl::builder()
722 .non_global_ip_ranges(true)
723 .ip_acl_default(true)
724 .port_acl_default(true)
725 .host_acl_default(true)
726 .build();
727
728 let middleware = HttpAclMiddleware::new(acl);
729
730 let client = reqwest_middleware::ClientBuilder::new(
731 reqwest::Client::builder()
732 .dns_resolver(middleware.dns_resolver())
733 .build()
734 .unwrap(),
735 )
736 .with(middleware)
737 .build();
738
739 let request = client
742 .get(format!("http://localhost:{}/", addr.port()))
743 .send()
744 .await;
745
746 assert!(request.is_ok(), "{:?}", request.err());
747 }
748
749 #[tokio::test]
750 async fn test_trusted_static_dns_mapping_bypasses_ip_port_acl() {
751 use tokio::io::{AsyncReadExt, AsyncWriteExt};
752 use tokio::net::TcpListener;
753
754 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
755 let addr = listener.local_addr().unwrap();
756
757 tokio::spawn(async move {
758 if let Ok((mut socket, _)) = listener.accept().await {
759 let mut buf = [0u8; 1024];
760 let _ = socket.read(&mut buf).await;
761 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
762 let _ = socket.write_all(response.as_bytes()).await;
763 }
764 });
765
766 let acl = HttpAcl::builder()
769 .host_acl_default(true)
770 .add_trusted_static_dns_mapping("trusted.internal".to_string(), addr)
771 .unwrap()
772 .build();
773
774 assert!(acl.is_ip_allowed(&addr.ip()).is_denied());
775 assert!(acl.is_port_allowed(addr.port()).is_denied());
776
777 let middleware = HttpAclMiddleware::new(acl);
778
779 let client = reqwest_middleware::ClientBuilder::new(
780 reqwest::Client::builder()
781 .dns_resolver(middleware.dns_resolver())
782 .build()
783 .unwrap(),
784 )
785 .with(middleware)
786 .build();
787
788 let request = client.get("http://trusted.internal/").send().await;
791
792 assert!(request.is_ok(), "{:?}", request.err());
793 }
794
795 #[tokio::test]
796 async fn test_redirect_policy_blocks_disallowed_target() {
797 use tokio::io::{AsyncReadExt, AsyncWriteExt};
798 use tokio::net::TcpListener;
799
800 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
801 let addr = listener.local_addr().unwrap();
802
803 tokio::spawn(async move {
804 if let Ok((mut socket, _)) = listener.accept().await {
805 let mut buf = [0u8; 1024];
806 let _ = socket.read(&mut buf).await;
807 let response = "HTTP/1.1 302 Found\r\nLocation: http://192.168.1.1/\r\nContent-Length: 0\r\n\r\n";
808 let _ = socket.write_all(response.as_bytes()).await;
809 }
810 });
811
812 let acl = HttpAcl::builder()
815 .non_global_ip_ranges(true)
816 .ip_acl_default(true)
817 .port_acl_default(true)
818 .add_denied_ip_range((
819 "192.168.1.1".parse::<std::net::IpAddr>().unwrap(),
820 "192.168.1.1".parse::<std::net::IpAddr>().unwrap(),
821 ))
822 .unwrap()
823 .build();
824
825 let middleware = HttpAclMiddleware::new(acl);
826
827 let client = reqwest_middleware::ClientBuilder::new(
828 reqwest::Client::builder()
829 .dns_resolver(middleware.dns_resolver())
830 .redirect(middleware.redirect_policy())
831 .build()
832 .unwrap(),
833 )
834 .with(middleware)
835 .build();
836
837 let request = client
838 .get(format!("http://127.0.0.1:{}/", addr.port()))
839 .send()
840 .await;
841
842 assert!(request.is_err());
843 }
844
845 #[test]
846 fn test_no_hooks_configured_leaves_acl_unaffected() {
847 let acl = HttpAcl::builder().build();
848
849 assert!(!acl.has_modify_request());
850 assert!(!acl.has_modify_response());
851 }
852
853 #[tokio::test]
854 async fn test_modify_request_injects_header() {
855 use tokio::io::{AsyncReadExt, AsyncWriteExt};
856 use tokio::net::TcpListener;
857
858 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
859 let addr = listener.local_addr().unwrap();
860
861 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
862 tokio::spawn(async move {
863 if let Ok((mut socket, _)) = listener.accept().await {
864 let mut buf = [0u8; 4096];
865 let n = socket.read(&mut buf).await.unwrap_or(0);
866 let _ = captured_tx.send(String::from_utf8_lossy(&buf[..n]).into_owned());
867 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
868 let _ = socket.write_all(response.as_bytes()).await;
869 }
870 });
871
872 let acl = HttpAcl::builder()
873 .non_global_ip_ranges(true)
874 .ip_acl_default(true)
875 .port_acl_default(true)
876 .host_acl_default(true)
877 .build_full(HttpAclHooks {
878 modify_request_fn: Some(Arc::new(|_scheme, _authority, mutation| {
879 mutation
880 .headers
881 .push(("x-injected-secret".to_string(), "sssh".to_string()));
882 })),
883 ..Default::default()
884 });
885
886 let middleware = HttpAclMiddleware::new(acl);
887
888 let client = reqwest_middleware::ClientBuilder::new(
889 reqwest::Client::builder()
890 .dns_resolver(middleware.dns_resolver())
891 .build()
892 .unwrap(),
893 )
894 .with(middleware)
895 .build();
896
897 let request = client
898 .get(format!("http://127.0.0.1:{}/", addr.port()))
899 .send()
900 .await;
901
902 assert!(request.is_ok(), "{:?}", request.err());
903 let captured = captured_rx.await.unwrap();
904 assert!(
905 captured.to_lowercase().contains("x-injected-secret: sssh"),
906 "captured request did not contain the injected header:\n{captured}"
907 );
908 }
909
910 #[tokio::test]
911 async fn test_modify_request_replaces_body_and_content_length() {
912 use tokio::io::{AsyncReadExt, AsyncWriteExt};
913 use tokio::net::TcpListener;
914
915 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
916 let addr = listener.local_addr().unwrap();
917
918 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
919 tokio::spawn(async move {
920 if let Ok((mut socket, _)) = listener.accept().await {
921 let mut buf = [0u8; 4096];
922 let n = socket.read(&mut buf).await.unwrap_or(0);
923 let _ = captured_tx.send(String::from_utf8_lossy(&buf[..n]).into_owned());
924 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
925 let _ = socket.write_all(response.as_bytes()).await;
926 }
927 });
928
929 let new_body = "a much longer replacement body than the original";
930 let acl = HttpAcl::builder()
931 .non_global_ip_ranges(true)
932 .ip_acl_default(true)
933 .port_acl_default(true)
934 .host_acl_default(true)
935 .build_full(HttpAclHooks {
936 modify_request_fn: Some(Arc::new(move |_scheme, _authority, mutation| {
937 mutation.body = Some(Bytes::from(new_body));
938 })),
939 ..Default::default()
940 });
941
942 let middleware = HttpAclMiddleware::new(acl);
943
944 let client = reqwest_middleware::ClientBuilder::new(
945 reqwest::Client::builder()
946 .dns_resolver(middleware.dns_resolver())
947 .build()
948 .unwrap(),
949 )
950 .with(middleware)
951 .build();
952
953 let request = client
954 .post(format!("http://127.0.0.1:{}/", addr.port()))
955 .body("short")
956 .send()
957 .await;
958
959 assert!(request.is_ok(), "{:?}", request.err());
960 let captured = captured_rx.await.unwrap();
961 assert!(
962 captured.contains(&format!("content-length: {}", new_body.len()))
963 || captured.contains(&format!("Content-Length: {}", new_body.len())),
964 "captured request did not have a Content-Length matching the replaced body:\n{captured}"
965 );
966 assert!(
967 captured.ends_with(new_body),
968 "captured request did not end with the replaced body:\n{captured}"
969 );
970 }
971
972 #[tokio::test]
973 async fn test_modify_response_rewrites_status_headers_and_body() {
974 use tokio::io::{AsyncReadExt, AsyncWriteExt};
975 use tokio::net::TcpListener;
976
977 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
978 let addr = listener.local_addr().unwrap();
979
980 tokio::spawn(async move {
981 if let Ok((mut socket, _)) = listener.accept().await {
982 let mut buf = [0u8; 1024];
983 let _ = socket.read(&mut buf).await;
984 let body = "original body";
985 let response = format!(
986 "HTTP/1.1 200 OK\r\nx-remove-me: yes\r\nContent-Length: {}\r\n\r\n{}",
987 body.len(),
988 body
989 );
990 let _ = socket.write_all(response.as_bytes()).await;
991 }
992 });
993
994 let acl = HttpAcl::builder()
995 .non_global_ip_ranges(true)
996 .ip_acl_default(true)
997 .port_acl_default(true)
998 .host_acl_default(true)
999 .build_full(HttpAclHooks {
1000 modify_response_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1001 mutation.status = 201;
1002 mutation.headers.retain(|(k, _)| k != "x-remove-me");
1003 mutation
1004 .headers
1005 .push(("x-added".to_string(), "yes".to_string()));
1006 mutation.body = Bytes::from_static(b"redacted body");
1007 })),
1008 ..Default::default()
1009 });
1010
1011 let middleware = HttpAclMiddleware::new(acl);
1012
1013 let client = reqwest_middleware::ClientBuilder::new(
1014 reqwest::Client::builder()
1015 .dns_resolver(middleware.dns_resolver())
1016 .build()
1017 .unwrap(),
1018 )
1019 .with(middleware)
1020 .build();
1021
1022 let response = client
1023 .get(format!("http://127.0.0.1:{}/", addr.port()))
1024 .send()
1025 .await
1026 .unwrap();
1027
1028 assert_eq!(response.status(), 201);
1029 assert!(!response.headers().contains_key("x-remove-me"));
1030 assert_eq!(response.headers().get("x-added").unwrap(), "yes");
1031 let body = response.text().await.unwrap();
1032 assert_eq!(body, "redacted body");
1033 }
1034
1035 #[tokio::test]
1036 async fn test_modify_response_preserves_url_and_remote_addr() {
1037 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1038 use tokio::net::TcpListener;
1039
1040 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1041 let addr = listener.local_addr().unwrap();
1042
1043 tokio::spawn(async move {
1044 if let Ok((mut socket, _)) = listener.accept().await {
1045 let mut buf = [0u8; 1024];
1046 let _ = socket.read(&mut buf).await;
1047 let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
1048 let _ = socket.write_all(response.as_bytes()).await;
1049 }
1050 });
1051
1052 let acl = HttpAcl::builder()
1053 .non_global_ip_ranges(true)
1054 .ip_acl_default(true)
1055 .port_acl_default(true)
1056 .host_acl_default(true)
1057 .build_full(HttpAclHooks {
1058 modify_response_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1059 mutation.body = Bytes::from_static(b"changed");
1060 })),
1061 ..Default::default()
1062 });
1063
1064 let middleware = HttpAclMiddleware::new(acl);
1065
1066 let client = reqwest_middleware::ClientBuilder::new(
1067 reqwest::Client::builder()
1068 .dns_resolver(middleware.dns_resolver())
1069 .build()
1070 .unwrap(),
1071 )
1072 .with(middleware)
1073 .build();
1074
1075 let url = format!("http://127.0.0.1:{}/", addr.port());
1076 let response = client.get(&url).send().await.unwrap();
1077
1078 assert_eq!(response.url().as_str(), url);
1079 assert!(
1080 response.remote_addr().is_some(),
1081 "remote_addr() was lost when the response was rebuilt"
1082 );
1083 }
1084
1085 #[tokio::test]
1086 async fn test_modify_request_header_carries_through_redirect() {
1087 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1088 use tokio::net::TcpListener;
1089
1090 let second_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1091 let second_addr = second_listener.local_addr().unwrap();
1092
1093 let (second_captured_tx, second_captured_rx) = tokio::sync::oneshot::channel();
1094 tokio::spawn(async move {
1095 if let Ok((mut socket, _)) = second_listener.accept().await {
1096 let mut buf = [0u8; 4096];
1097 let n = socket.read(&mut buf).await.unwrap_or(0);
1098 let _ = second_captured_tx.send(String::from_utf8_lossy(&buf[..n]).into_owned());
1099 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
1100 let _ = socket.write_all(response.as_bytes()).await;
1101 }
1102 });
1103
1104 let first_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1105 let first_addr = first_listener.local_addr().unwrap();
1106 tokio::spawn(async move {
1107 if let Ok((mut socket, _)) = first_listener.accept().await {
1108 let mut buf = [0u8; 1024];
1109 let _ = socket.read(&mut buf).await;
1110 let response = format!(
1111 "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{}/\r\nContent-Length: 0\r\n\r\n",
1112 second_addr.port()
1113 );
1114 let _ = socket.write_all(response.as_bytes()).await;
1115 }
1116 });
1117
1118 let acl = HttpAcl::builder()
1119 .non_global_ip_ranges(true)
1120 .ip_acl_default(true)
1121 .port_acl_default(true)
1122 .host_acl_default(true)
1123 .build_full(HttpAclHooks {
1124 modify_request_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1125 mutation
1126 .headers
1127 .push(("x-marker".to_string(), "hop-one-only".to_string()));
1128 })),
1129 ..Default::default()
1130 });
1131
1132 let middleware = HttpAclMiddleware::new(acl);
1133
1134 let client = reqwest_middleware::ClientBuilder::new(
1135 reqwest::Client::builder()
1136 .dns_resolver(middleware.dns_resolver())
1137 .redirect(middleware.redirect_policy())
1138 .build()
1139 .unwrap(),
1140 )
1141 .with(middleware)
1142 .build();
1143
1144 let request = client
1145 .get(format!("http://127.0.0.1:{}/", first_addr.port()))
1146 .send()
1147 .await;
1148
1149 assert!(request.is_ok(), "{:?}", request.err());
1150 let second_captured = second_captured_rx.await.unwrap();
1151 assert!(
1157 second_captured.to_lowercase().contains("x-marker"),
1158 "expected the header injected for the original request to carry through \
1159 reqwest's redirect handling to the second hop, but it did not:\n{second_captured}"
1160 );
1161 }
1162
1163 async fn capture_server(
1166 response: &'static [u8],
1167 ) -> (SocketAddr, tokio::sync::oneshot::Receiver<Vec<u8>>) {
1168 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1169 use tokio::net::TcpListener;
1170
1171 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1172 let addr = listener.local_addr().unwrap();
1173 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
1174 tokio::spawn(async move {
1175 if let Ok((mut socket, _)) = listener.accept().await {
1176 let mut buf = [0u8; 4096];
1177 let n = socket.read(&mut buf).await.unwrap_or(0);
1178 let _ = captured_tx.send(buf[..n].to_ascii_lowercase());
1179 let _ = socket.write_all(response).await;
1180 }
1181 });
1182 (addr, captured_rx)
1183 }
1184
1185 fn local_client(hooks: HttpAclHooks, builder: HttpAclBuilder) -> ClientWithMiddleware {
1186 let acl = builder
1187 .non_global_ip_ranges(true)
1188 .ip_acl_default(true)
1189 .port_acl_default(true)
1190 .host_acl_default(true)
1191 .build_full(hooks);
1192 let middleware = HttpAclMiddleware::new(acl);
1193 reqwest_middleware::ClientBuilder::new(
1194 reqwest::Client::builder()
1195 .dns_resolver(middleware.dns_resolver())
1196 .build()
1197 .unwrap(),
1198 )
1199 .with(middleware)
1200 .build()
1201 }
1202
1203 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1204 haystack.windows(needle.len()).any(|w| w == needle)
1205 }
1206
1207 const OK_EMPTY: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
1208
1209 #[tokio::test]
1210 async fn test_obs_text_header_values_are_sent_unchanged() {
1211 let (addr, captured_rx) = capture_server(OK_EMPTY).await;
1212 let client = local_client(HttpAclHooks::default(), HttpAcl::builder());
1213
1214 let request = client
1215 .get(format!("http://127.0.0.1:{}/", addr.port()))
1216 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1217 .header("user-agent", HeaderValue::from_bytes(b"caf\xE9").unwrap())
1218 .send()
1219 .await;
1220
1221 assert!(request.is_ok(), "{:?}", request.err());
1222 let captured = captured_rx.await.unwrap();
1223 assert!(
1224 contains(&captured, b"cookie: a=caf\xE9\r\n"),
1225 "{captured:?}"
1226 );
1227 assert!(
1228 contains(&captured, b"user-agent: caf\xE9\r\n"),
1229 "{captured:?}"
1230 );
1231 }
1232
1233 #[tokio::test]
1234 async fn test_denied_header_with_obs_text_value_is_still_denied() {
1235 let client = local_client(
1236 HttpAclHooks::default(),
1237 HttpAcl::builder()
1238 .add_denied_header("cookie".to_string(), None)
1239 .unwrap(),
1240 );
1241
1242 let request = client
1243 .get("http://127.0.0.1:9/")
1244 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1245 .send()
1246 .await;
1247
1248 let error = request.unwrap_err().to_string();
1249 assert!(error.starts_with("header cookie: "), "{error}");
1250 assert!(error.contains("is denied"), "{error}");
1251 }
1252
1253 #[tokio::test]
1254 async fn test_allowed_header_value_is_compared_as_raw_bytes() {
1255 let builder = || {
1258 HttpAcl::builder()
1259 .add_allowed_header("x-token".to_string(), Some("caf\u{FFFD}".to_string()))
1260 .unwrap()
1261 };
1262
1263 let client = local_client(HttpAclHooks::default(), builder());
1264 let request = client
1265 .get("http://127.0.0.1:9/")
1266 .header("x-token", HeaderValue::from_bytes(b"caf\xE9").unwrap())
1267 .send()
1268 .await;
1269 let error = request.unwrap_err().to_string();
1270 assert!(error.starts_with("header x-token: "), "{error}");
1271
1272 let (addr, captured_rx) = capture_server(OK_EMPTY).await;
1273 let client = local_client(HttpAclHooks::default(), builder());
1274 let request = client
1275 .get(format!("http://127.0.0.1:{}/", addr.port()))
1276 .header(
1277 "x-token",
1278 HeaderValue::from_bytes("caf\u{FFFD}".as_bytes()).unwrap(),
1279 )
1280 .send()
1281 .await;
1282 assert!(request.is_ok(), "{:?}", request.err());
1283 let captured = captured_rx.await.unwrap();
1284 assert!(
1285 contains(&captured, "x-token: caf\u{FFFD}\r\n".as_bytes()),
1286 "{captured:?}"
1287 );
1288 }
1289
1290 #[tokio::test]
1291 async fn test_validate_fn_sees_obs_text_headers() {
1292 let client = local_client(
1295 HttpAclHooks {
1296 validate_fn: Some(Arc::new(|_scheme, _authority, mut headers, _body| {
1297 if headers.any(|(k, v)| k == "cookie" && v == "a=caf\u{FFFD}") {
1298 AclClassification::Denied("cookie seen".to_string())
1299 } else {
1300 AclClassification::AllowedDefault
1301 }
1302 })),
1303 ..Default::default()
1304 },
1305 HttpAcl::builder(),
1306 );
1307
1308 let request = client
1309 .get("http://127.0.0.1:9/")
1310 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1311 .send()
1312 .await;
1313
1314 let error = request.unwrap_err().to_string();
1315 assert!(error.contains("cookie seen"), "{error}");
1316 }
1317
1318 #[tokio::test]
1319 async fn test_modify_request_keeps_obs_text_header_bytes() {
1320 let (addr, captured_rx) = capture_server(OK_EMPTY).await;
1321 let client = local_client(
1322 HttpAclHooks {
1323 modify_request_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1324 mutation.headers.retain(|(k, _)| k != "x-drop-me");
1325 mutation
1326 .headers
1327 .push(("x-injected".to_string(), "yes".to_string()));
1328 })),
1329 ..Default::default()
1330 },
1331 HttpAcl::builder(),
1332 );
1333
1334 let request = client
1335 .get(format!("http://127.0.0.1:{}/", addr.port()))
1336 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1337 .header("x-drop-me", HeaderValue::from_bytes(b"caf\xE9").unwrap())
1338 .send()
1339 .await;
1340
1341 assert!(request.is_ok(), "{:?}", request.err());
1342 let captured = captured_rx.await.unwrap();
1343 assert!(
1344 contains(&captured, b"cookie: a=caf\xE9\r\n"),
1345 "{captured:?}"
1346 );
1347 assert!(contains(&captured, b"x-injected: yes\r\n"), "{captured:?}");
1348 assert!(!contains(&captured, b"x-drop-me"), "{captured:?}");
1349 }
1350
1351 #[tokio::test]
1352 async fn test_modify_response_keeps_obs_text_header_bytes() {
1353 let (addr, _captured_rx) =
1354 capture_server(b"HTTP/1.1 200 OK\r\nx-raw: caf\xE9\r\nContent-Length: 2\r\n\r\nok")
1355 .await;
1356 let client = local_client(
1357 HttpAclHooks {
1358 modify_response_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1359 mutation.body = Bytes::from_static(b"changed");
1360 })),
1361 ..Default::default()
1362 },
1363 HttpAcl::builder(),
1364 );
1365
1366 let response = client
1367 .get(format!("http://127.0.0.1:{}/", addr.port()))
1368 .send()
1369 .await
1370 .unwrap();
1371
1372 assert_eq!(
1373 response.headers().get("x-raw").unwrap().as_bytes(),
1374 b"caf\xE9"
1375 );
1376 assert_eq!(response.text().await.unwrap(), "changed");
1377 }
1378}