1use std::collections::HashMap;
2use std::io::BufReader;
3#[cfg(feature = "https")]
4use std::io::{Cursor, Read, Write};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, SystemTime};
7
8use crate::auth::{
9 parse_authenticate_headers, AuthDecision, AuthHandler, AuthTarget, BasicAuthHandler, Challenge,
10 DynAuthHandler,
11};
12use crate::date::parse_http_date;
13use crate::errors::NanoGetError;
14use crate::http::{self, BoxStream};
15#[cfg(feature = "https")]
16use crate::https;
17use crate::request::{should_follow_redirect, Header, Method, RedirectPolicy, Request};
18use crate::response::Response;
19use crate::url::{ToUrl, Url};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ConnectionPolicy {
24 Close,
26 Reuse,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CacheMode {
33 Disabled,
35 Memory,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ParserStrictness {
42 Strict,
44 Lenient,
46}
47
48impl ParserStrictness {
49 const fn is_strict(self) -> bool {
50 matches!(self, Self::Strict)
51 }
52}
53
54#[derive(Debug, Clone)]
58pub struct ProxyConfig {
59 url: Url,
60 headers: Vec<Header>,
61}
62
63impl ProxyConfig {
64 pub fn new<U: ToUrl>(url: U) -> Result<Self, NanoGetError> {
69 let url = url.to_url()?;
70 if !url.is_http() {
71 return Err(NanoGetError::UnsupportedProxyScheme(url.scheme.clone()));
72 }
73
74 Ok(Self {
75 url,
76 headers: Vec::new(),
77 })
78 }
79
80 pub fn url(&self) -> &Url {
82 &self.url
83 }
84
85 pub fn headers(&self) -> &[Header] {
87 &self.headers
88 }
89
90 pub fn add_header(
94 &mut self,
95 name: impl Into<String>,
96 value: impl Into<String>,
97 ) -> Result<&mut Self, NanoGetError> {
98 let name = name.into();
99 validate_proxy_header_name(&name)?;
100 self.headers.push(Header::new(name, value)?);
101 Ok(self)
102 }
103}
104
105#[derive(Clone)]
107pub struct ClientBuilder {
108 config: ClientConfig,
109}
110
111impl ClientBuilder {
112 pub fn new() -> Self {
114 Self {
115 config: ClientConfig::default(),
116 }
117 }
118
119 pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self {
122 self.config.redirect_policy = policy;
123 self
124 }
125
126 pub fn connection_policy(mut self, policy: ConnectionPolicy) -> Self {
129 self.config.connection_policy = policy;
130 self
131 }
132
133 pub fn cache_mode(mut self, mode: CacheMode) -> Self {
135 self.config.cache_mode = mode;
136 self
137 }
138
139 pub fn parser_strictness(mut self, strictness: ParserStrictness) -> Self {
143 self.config.parser_strictness = strictness;
144 self
145 }
146
147 pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
149 self.config.proxy = Some(proxy);
150 self
151 }
152
153 pub fn auth_handler(mut self, handler: Arc<dyn AuthHandler + Send + Sync>) -> Self {
155 self.config.auth_handler = Some(handler);
156 self
157 }
158
159 pub fn proxy_auth_handler(mut self, handler: Arc<dyn AuthHandler + Send + Sync>) -> Self {
161 self.config.proxy_auth_handler = Some(handler);
162 self
163 }
164
165 pub fn basic_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
167 let handler = Arc::new(BasicAuthHandler::new(
168 username.into(),
169 password.into(),
170 AuthTarget::Origin,
171 ));
172 self.config.auth_handler = Some(handler);
173 self
174 }
175
176 pub fn basic_proxy_auth(
178 mut self,
179 username: impl Into<String>,
180 password: impl Into<String>,
181 ) -> Self {
182 let handler = Arc::new(BasicAuthHandler::new(
183 username.into(),
184 password.into(),
185 AuthTarget::Proxy,
186 ));
187 self.config.proxy_auth_handler = Some(handler);
188 self
189 }
190
191 pub fn preemptive_basic_auth(
194 mut self,
195 username: impl Into<String>,
196 password: impl Into<String>,
197 ) -> Self {
198 let handler = BasicAuthHandler::new(username.into(), password.into(), AuthTarget::Origin);
199 self.config.preemptive_authorization = Some(handler.header_value().to_string());
200 self.config.auth_handler = Some(Arc::new(handler));
201 self
202 }
203
204 pub fn preemptive_basic_proxy_auth(
207 mut self,
208 username: impl Into<String>,
209 password: impl Into<String>,
210 ) -> Self {
211 let handler = BasicAuthHandler::new(username.into(), password.into(), AuthTarget::Proxy);
212 self.config.preemptive_proxy_authorization = Some(handler.header_value().to_string());
213 self.config.proxy_auth_handler = Some(Arc::new(handler));
214 self
215 }
216
217 pub fn build(self) -> Client {
219 Client {
220 inner: Arc::new(ClientInner {
221 config: self.config,
222 cache: Arc::new(Mutex::new(MemoryCache::default())),
223 }),
224 }
225 }
226}
227
228impl Default for ClientBuilder {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234#[derive(Clone)]
239pub struct Client {
240 inner: Arc<ClientInner>,
241}
242
243struct ClientInner {
244 config: ClientConfig,
245 cache: Arc<Mutex<MemoryCache>>,
246}
247
248impl Client {
249 pub fn builder() -> ClientBuilder {
251 ClientBuilder::new()
252 }
253
254 pub fn session(&self) -> Session {
256 Session {
257 config: self.inner.config.clone(),
258 cache: Arc::clone(&self.inner.cache),
259 connection: None,
260 }
261 }
262
263 pub fn execute(&self, request: Request) -> Result<Response, NanoGetError> {
265 self.session().execute(request)
266 }
267
268 pub fn execute_ref(&self, request: &Request) -> Result<Response, NanoGetError> {
270 self.execute(request.clone())
271 }
272
273 pub fn get<U: ToUrl>(&self, url: U) -> Result<String, NanoGetError> {
275 let request = Request::get(url)?.with_redirect_policy(self.inner.config.redirect_policy);
276 self.execute(request).and_then(Response::into_body_text)
277 }
278
279 pub fn get_bytes<U: ToUrl>(&self, url: U) -> Result<Vec<u8>, NanoGetError> {
281 let request = Request::get(url)?.with_redirect_policy(self.inner.config.redirect_policy);
282 self.execute(request).map(|response| response.body)
283 }
284
285 pub fn head<U: ToUrl>(&self, url: U) -> Result<Response, NanoGetError> {
287 let request = Request::head(url)?.with_redirect_policy(self.inner.config.redirect_policy);
288 self.execute(request)
289 }
290}
291
292impl Default for Client {
293 fn default() -> Self {
294 ClientBuilder::default().build()
295 }
296}
297
298pub struct Session {
303 config: ClientConfig,
304 cache: Arc<Mutex<MemoryCache>>,
305 connection: Option<LiveConnection>,
306}
307
308impl Session {
309 pub fn execute(&mut self, request: Request) -> Result<Response, NanoGetError> {
312 let redirect_policy = request.effective_redirect_policy(self.config.redirect_policy);
313 let mut current = request;
314 let mut followed = 0usize;
315
316 loop {
317 let response = self.execute_one(current.clone())?;
318
319 match redirect_policy {
320 RedirectPolicy::None => return Ok(response),
321 RedirectPolicy::Follow { max_redirects } => {
322 if !should_follow_redirect(response.status_code) {
323 return Ok(response);
324 }
325
326 let Some(location) = response.header("location") else {
327 return Ok(response);
328 };
329
330 if followed >= max_redirects {
331 return Err(NanoGetError::RedirectLimitExceeded(max_redirects));
332 }
333
334 let next_url = current.url().resolve(location)?;
335 let same_authority = current.url().same_authority(&next_url);
336 current = current.clone_with_url(next_url);
337 if !same_authority {
338 current.remove_headers_named("authorization");
339 current.disable_preemptive_origin_auth();
340 }
341 followed += 1;
342 }
343 }
344 }
345 }
346
347 pub fn execute_ref(&mut self, request: &Request) -> Result<Response, NanoGetError> {
349 self.execute(request.clone())
350 }
351
352 pub fn execute_pipelined(
360 &mut self,
361 requests: &[Request],
362 ) -> Result<Vec<Response>, NanoGetError> {
363 if requests.is_empty() {
364 return Ok(Vec::new());
365 }
366
367 for request in requests {
368 validate_request_conditionals(request)?;
369 }
370
371 if self.config.connection_policy == ConnectionPolicy::Close {
372 return Err(NanoGetError::Pipeline(
373 "pipelining requires ConnectionPolicy::Reuse".to_string(),
374 ));
375 }
376
377 let key = connection_key(&self.config.proxy, requests[0].url());
378 for request in requests {
379 if request.url().scheme == "https" && self.config.proxy.is_none() {
380 if connection_key(&self.config.proxy, request.url()) != key {
381 return Err(NanoGetError::Pipeline(
382 "all pipelined requests must target the same connection".to_string(),
383 ));
384 }
385 } else if connection_key(&self.config.proxy, request.url()) != key {
386 return Err(NanoGetError::Pipeline(
387 "all pipelined requests must share the same connection".to_string(),
388 ));
389 }
390 }
391
392 self.ensure_connection(&requests[0])?;
393 {
394 let connection = self
395 .connection
396 .as_mut()
397 .ok_or_else(|| NanoGetError::Pipeline("missing live connection".to_string()))?;
398
399 for request in requests {
400 let send_target = SendTarget::for_request(&self.config, request);
401 let prepared = prepared_request(request, &self.config, send_target)?;
402 let target = request_target(&prepared, &self.config.proxy);
403 http::write_request(connection.reader.get_mut(), &prepared, &target, false)?;
404 }
405 connection.reader.get_mut().flush()?;
406 }
407
408 let mut responses = Vec::with_capacity(requests.len());
409 for (index, request) in requests.iter().enumerate() {
410 let parsed = {
411 let connection = self
412 .connection
413 .as_mut()
414 .ok_or_else(|| NanoGetError::Pipeline("missing live connection".to_string()))?;
415 crate::response::read_parsed_response(
416 &mut connection.reader,
417 request.method(),
418 self.config.parser_strictness.is_strict(),
419 )
420 };
421 let parsed = match parsed {
422 Ok(parsed) => parsed,
423 Err(error) if pipeline_retryable_parse_error(&error) => {
424 self.connection = None;
425 for remaining in requests.iter().skip(index) {
426 responses.push(self.execute_one(remaining.clone())?);
427 }
428 return Ok(responses);
429 }
430 Err(error) => return Err(error),
431 };
432 responses.push(parsed.response);
433 if parsed.connection_close && index + 1 != requests.len() {
434 self.connection = None;
435 for remaining in requests.iter().skip(index + 1) {
436 responses.push(self.execute_one(remaining.clone())?);
437 }
438 return Ok(responses);
439 }
440 if parsed.connection_close {
441 self.connection = None;
442 }
443 }
444
445 Ok(responses)
446 }
447
448 fn execute_one(&mut self, request: Request) -> Result<Response, NanoGetError> {
449 validate_request_conditionals(&request)?;
450 let auth_context = effective_auth_context(&request, &self.config);
451 let cache_directives = CacheControl::from_headers(request.headers());
452 if self.config.cache_mode != CacheMode::Memory && cache_directives.only_if_cached {
453 return Ok(gateway_timeout_response());
454 }
455
456 let now = SystemTime::now();
457 let mut bypass_standard_cache = false;
458 if self.config.cache_mode == CacheMode::Memory
459 && request.method() == Method::Get
460 && request.has_header("range")
461 && !cache_directives.no_store
462 {
463 let range_lookup = self
464 .cache
465 .lock()
466 .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?
467 .lookup_range(&request, now, &auth_context);
468 match range_lookup {
469 Some(RangeCacheLookup::Hit(response)) => {
470 return Ok(response_for_method(response, request.method()))
471 }
472 Some(RangeCacheLookup::UnsatisfiedOnlyIfCached) => {
473 return Ok(gateway_timeout_response())
474 }
475 Some(RangeCacheLookup::IfRangeMismatch) => {
476 bypass_standard_cache = true;
477 }
478 None => {}
479 }
480 }
481
482 let cache_lookup = if !bypass_standard_cache
483 && self.config.cache_mode == CacheMode::Memory
484 && request.method() == Method::Get
485 && !cache_directives.no_store
486 {
487 self.cache
488 .lock()
489 .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?
490 .lookup(&request, now, &auth_context)
491 } else {
492 None
493 };
494
495 match cache_lookup {
496 Some(CacheLookup::Fresh(response)) => {
497 return Ok(response_for_method(response, request.method()))
498 }
499 Some(CacheLookup::Stale(entry)) => {
500 let revalidation = self.execute_stale(request, *entry)?;
501 return Ok(revalidation);
502 }
503 Some(CacheLookup::UnsatisfiedOnlyIfCached) => return Ok(gateway_timeout_response()),
504 None => {}
505 }
506
507 let mut current = request;
508 let mut seen_origin_challenges = Vec::new();
509 let mut seen_proxy_challenges = Vec::new();
510
511 loop {
512 let timed_response = self.send_request(¤t)?;
513 let response = timed_response.response.clone();
514
515 if response.status_code == 401 {
516 if let Some(next_request) = self.maybe_retry_auth(
517 AuthTarget::Origin,
518 ¤t,
519 &response,
520 &mut seen_origin_challenges,
521 )? {
522 current = next_request;
523 continue;
524 }
525 } else if response.status_code == 407 {
526 if let Some(next_request) = self.maybe_retry_auth(
527 AuthTarget::Proxy,
528 ¤t,
529 &response,
530 &mut seen_proxy_challenges,
531 )? {
532 current = next_request;
533 continue;
534 }
535 }
536
537 let final_auth_context = effective_auth_context(¤t, &self.config);
538 self.store_in_cache(¤t, &timed_response, &final_auth_context)?;
539 return Ok(response);
540 }
541 }
542
543 fn execute_stale(
544 &mut self,
545 request: Request,
546 entry: CacheEntry,
547 ) -> Result<Response, NanoGetError> {
548 let mut conditional_request = request.clone();
549 if !has_user_conditionals(&conditional_request) {
550 if let Some(etag) = &entry.etag {
551 conditional_request.if_none_match(etag.clone())?;
552 } else if let Some(last_modified) = &entry.last_modified {
553 conditional_request.set_header("If-Modified-Since", last_modified.clone())?;
554 }
555 }
556
557 let response = self.send_request(&conditional_request)?.response;
558 if response.status_code == 304 {
559 let merged = {
560 let mut cache = self
561 .cache
562 .lock()
563 .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?;
564 cache.merge_not_modified(&request, &entry, &response, SystemTime::now())?
565 };
566 return Ok(response_for_method(merged, request.method()));
567 }
568
569 let timed_response = TimedResponse::synthetic(response.clone());
570 let auth_context = effective_auth_context(&request, &self.config);
571 self.store_in_cache(&request, &timed_response, &auth_context)?;
572 Ok(response)
573 }
574
575 fn send_request(&mut self, request: &Request) -> Result<TimedResponse, NanoGetError> {
576 let should_reuse = self.config.connection_policy == ConnectionPolicy::Reuse;
577 let mut retried = false;
578
579 loop {
580 let result = if should_reuse {
581 self.ensure_connection(request)?;
582 self.send_on_live_connection(request)
583 } else {
584 self.send_ephemeral(request)
585 };
586
587 match result {
588 Ok(response) => return Ok(response),
589 Err(error) if should_reuse && !retried => {
590 self.connection = None;
591 retried = true;
592 if retryable_reconnect_error(&error) {
593 continue;
594 }
595 return Err(error);
596 }
597 Err(error) => return Err(error),
598 }
599 }
600 }
601
602 fn send_ephemeral(&self, request: &Request) -> Result<TimedResponse, NanoGetError> {
603 let mut connection = open_connection(&self.config, request)?;
604 let prepared = prepared_request(
605 request,
606 &self.config,
607 SendTarget::for_request(&self.config, request),
608 )?;
609 let target = request_target(&prepared, &self.config.proxy);
610
611 let request_time = SystemTime::now();
612 http::write_request(connection.reader.get_mut(), &prepared, &target, true)?;
613 connection.reader.get_mut().flush()?;
614 let parsed = crate::response::read_parsed_response(
615 &mut connection.reader,
616 request.method(),
617 self.config.parser_strictness.is_strict(),
618 )?;
619 let response_time = SystemTime::now();
620 Ok(TimedResponse {
621 response: parsed.response,
622 request_time,
623 response_time,
624 })
625 }
626
627 fn send_on_live_connection(
628 &mut self,
629 request: &Request,
630 ) -> Result<TimedResponse, NanoGetError> {
631 let send_target = SendTarget::for_request(&self.config, request);
632 let prepared = prepared_request(request, &self.config, send_target)?;
633 let target = request_target(&prepared, &self.config.proxy);
634 let connection = self.connection.as_mut().ok_or_else(|| {
635 NanoGetError::Io(std::io::Error::new(
636 std::io::ErrorKind::NotConnected,
637 "missing persistent connection",
638 ))
639 })?;
640
641 let request_time = SystemTime::now();
642 http::write_request(connection.reader.get_mut(), &prepared, &target, false)?;
643 connection.reader.get_mut().flush()?;
644 let parsed = crate::response::read_parsed_response(
645 &mut connection.reader,
646 request.method(),
647 self.config.parser_strictness.is_strict(),
648 )?;
649 let response_time = SystemTime::now();
650 let response = parsed.response;
651 let unexpected_head_body_bytes = request.method() == Method::Head
652 && !parsed.connection_close
653 && !connection.reader.buffer().is_empty();
654 if parsed.connection_close || unexpected_head_body_bytes {
655 self.connection = None;
656 }
657 Ok(TimedResponse {
658 response,
659 request_time,
660 response_time,
661 })
662 }
663
664 fn ensure_connection(&mut self, request: &Request) -> Result<(), NanoGetError> {
665 let desired = connection_key(&self.config.proxy, request.url());
666 let keep_existing = self
667 .connection
668 .as_ref()
669 .map(|connection| connection.key == desired)
670 .unwrap_or(false);
671 if keep_existing {
672 return Ok(());
673 }
674
675 self.connection = Some(open_connection(&self.config, request)?);
676 Ok(())
677 }
678
679 fn store_in_cache(
680 &self,
681 request: &Request,
682 timed_response: &TimedResponse,
683 auth_context: &AuthContext,
684 ) -> Result<(), NanoGetError> {
685 if self.config.cache_mode != CacheMode::Memory
686 || !matches!(request.method(), Method::Get | Method::Head)
687 || CacheControl::from_headers(request.headers()).no_store
688 {
689 return Ok(());
690 }
691
692 let mut cache = self
693 .cache
694 .lock()
695 .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?;
696 cache.store(request, timed_response, auth_context);
697 Ok(())
698 }
699
700 fn maybe_retry_auth(
701 &self,
702 target: AuthTarget,
703 request: &Request,
704 response: &Response,
705 seen_challenges: &mut Vec<Vec<Challenge>>,
706 ) -> Result<Option<Request>, NanoGetError> {
707 let handler = match target {
708 AuthTarget::Origin => self.config.auth_handler.as_ref(),
709 AuthTarget::Proxy => self.config.proxy_auth_handler.as_ref(),
710 };
711 maybe_retry_request_auth(handler, target, request, response, seen_challenges)
712 }
713}
714
715#[derive(Clone)]
716struct ClientConfig {
717 redirect_policy: RedirectPolicy,
718 connection_policy: ConnectionPolicy,
719 cache_mode: CacheMode,
720 parser_strictness: ParserStrictness,
721 proxy: Option<ProxyConfig>,
722 auth_handler: Option<DynAuthHandler>,
723 proxy_auth_handler: Option<DynAuthHandler>,
724 preemptive_authorization: Option<String>,
725 preemptive_proxy_authorization: Option<String>,
726}
727
728impl Default for ClientConfig {
729 fn default() -> Self {
730 Self {
731 redirect_policy: RedirectPolicy::none(),
732 connection_policy: ConnectionPolicy::Close,
733 cache_mode: CacheMode::Disabled,
734 parser_strictness: ParserStrictness::Strict,
735 proxy: None,
736 auth_handler: None,
737 proxy_auth_handler: None,
738 preemptive_authorization: None,
739 preemptive_proxy_authorization: None,
740 }
741 }
742}
743
744struct LiveConnection {
745 key: ConnectionKey,
746 reader: BufReader<BoxStream>,
747}
748
749#[cfg(feature = "https")]
750struct PrefixedStream<S> {
751 prefix: Cursor<Vec<u8>>,
752 stream: S,
753}
754
755#[cfg(feature = "https")]
756impl<S> PrefixedStream<S> {
757 fn new(stream: S, prefix: Vec<u8>) -> Self {
758 Self {
759 prefix: Cursor::new(prefix),
760 stream,
761 }
762 }
763}
764
765#[cfg(feature = "https")]
766impl<S: Read> Read for PrefixedStream<S> {
767 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
768 let mut total = 0usize;
769
770 if (self.prefix.position() as usize) < self.prefix.get_ref().len() {
771 total += self.prefix.read(buf)?;
772 if total == buf.len() {
773 return Ok(total);
774 }
775 }
776
777 let read = self.stream.read(&mut buf[total..])?;
778 Ok(total + read)
779 }
780}
781
782#[cfg(feature = "https")]
783impl<S: Write> Write for PrefixedStream<S> {
784 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
785 self.stream.write(buf)
786 }
787
788 fn flush(&mut self) -> std::io::Result<()> {
789 self.stream.flush()
790 }
791}
792
793#[derive(Debug, Clone)]
794struct TimedResponse {
795 response: Response,
796 request_time: SystemTime,
797 response_time: SystemTime,
798}
799
800impl TimedResponse {
801 fn synthetic(response: Response) -> Self {
802 let now = SystemTime::now();
803 Self {
804 response,
805 request_time: now,
806 response_time: now,
807 }
808 }
809}
810
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
812enum SendTarget {
813 Direct,
814 HttpProxy,
815 Tunnel,
816}
817
818impl SendTarget {
819 fn for_request(config: &ClientConfig, request: &Request) -> Self {
820 match config.proxy {
821 Some(_) if request.url().is_http() => Self::HttpProxy,
822 Some(_) => Self::Tunnel,
823 None => Self::Direct,
824 }
825 }
826
827 fn uses_proxy(self) -> bool {
828 matches!(self, Self::HttpProxy)
829 }
830
831 fn allows_origin_auth(self) -> bool {
832 true
833 }
834
835 fn allows_proxy_auth(self) -> bool {
836 matches!(self, Self::HttpProxy)
837 }
838}
839
840#[derive(Debug, Clone, PartialEq, Eq, Default)]
841struct AuthContext {
842 origin: Option<String>,
843 proxy: Option<String>,
844}
845
846#[derive(Debug, Clone, PartialEq, Eq)]
847enum ConnectionKey {
848 Direct { scheme: String, authority: String },
849 HttpProxy { proxy: String },
850 HttpsTunnel { proxy: String, target: String },
851}
852
853fn connection_key(proxy: &Option<ProxyConfig>, url: &Url) -> ConnectionKey {
854 match proxy {
855 Some(proxy) if url.is_http() => ConnectionKey::HttpProxy {
856 proxy: proxy.url().authority_form(),
857 },
858 Some(proxy) => ConnectionKey::HttpsTunnel {
859 proxy: proxy.url().authority_form(),
860 target: url.authority_form(),
861 },
862 None => ConnectionKey::Direct {
863 scheme: url.scheme.clone(),
864 authority: url.authority_form(),
865 },
866 }
867}
868
869fn prepared_request(
870 request: &Request,
871 config: &ClientConfig,
872 send_target: SendTarget,
873) -> Result<Request, NanoGetError> {
874 let mut prepared = request.clone();
875
876 if !send_target.allows_proxy_auth() {
877 prepared.remove_headers_named("proxy-authorization");
878 }
879
880 if send_target.uses_proxy() {
881 for header in config.proxy.iter().flat_map(|proxy| proxy.headers()) {
882 if prepared.has_header(header.name()) {
883 continue;
884 }
885 prepared.add_header(header.name().to_string(), header.value().to_string())?;
886 }
887 }
888 if send_target.allows_origin_auth()
889 && prepared.preemptive_origin_auth_allowed()
890 && !prepared.has_header("authorization")
891 {
892 if let Some(value) = &config.preemptive_authorization {
893 prepared.authorization(value.clone())?;
894 }
895 }
896
897 if send_target.allows_proxy_auth() && !prepared.has_header("proxy-authorization") {
898 if let Some(value) = &config.preemptive_proxy_authorization {
899 prepared.proxy_authorization(value.clone())?;
900 }
901 }
902
903 Ok(prepared)
904}
905
906fn request_target(request: &Request, proxy: &Option<ProxyConfig>) -> String {
907 if proxy.is_some() && request.url().is_http() {
908 request.url().absolute_form()
909 } else {
910 request.url().origin_form()
911 }
912}
913
914fn open_connection(
915 config: &ClientConfig,
916 request: &Request,
917) -> Result<LiveConnection, NanoGetError> {
918 let key = connection_key(&config.proxy, request.url());
919 let stream = match &config.proxy {
920 Some(proxy) if request.url().is_http() => {
921 let stream = http::connect_tcp(&proxy.url().authority_form())?;
922 Box::new(stream) as BoxStream
923 }
924 Some(proxy) => open_https_tunnel(config, request, proxy)?,
925 None if request.url().is_http() => {
926 let stream = http::connect_tcp(&request.url().authority_form())?;
927 Box::new(stream) as BoxStream
928 }
929 None if request.url().is_https() => {
930 #[cfg(feature = "https")]
931 {
932 https::connect_tls(request.url())?
933 }
934 #[cfg(not(feature = "https"))]
935 {
936 return Err(NanoGetError::HttpsFeatureRequired);
937 }
938 }
939 None => {
940 return Err(NanoGetError::UnsupportedScheme(
941 request.url().scheme.clone(),
942 ))
943 }
944 };
945
946 Ok(LiveConnection {
947 key,
948 reader: BufReader::new(stream),
949 })
950}
951
952fn open_https_tunnel(
953 config: &ClientConfig,
954 request: &Request,
955 proxy: &ProxyConfig,
956) -> Result<BoxStream, NanoGetError> {
957 let mut current = request.clone();
958 let mut seen_proxy_challenges = Vec::new();
959
960 loop {
961 let mut stream = http::connect_tcp(&proxy.url().authority_form())?;
962 let connect_headers = prepared_connect_headers(¤t, config, proxy)?;
963 let authority = request.url().authority_form();
964 http::write_connect_request(&mut stream, &authority, &connect_headers, false)?;
965 std::io::Write::flush(&mut stream)?;
966 let mut reader = BufReader::new(stream);
967 let head = http::read_response_head(&mut reader, config.parser_strictness.is_strict())?;
968 if (200..=299).contains(&head.status_code) {
969 let prefetched = reader.buffer().to_vec();
970 let stream = reader.into_inner();
971 #[cfg(feature = "https")]
972 {
973 if prefetched.is_empty() {
974 return https::connect_tls_over_stream(request.url(), stream);
975 }
976 return https::connect_tls_over_stream(
977 request.url(),
978 PrefixedStream::new(stream, prefetched),
979 );
980 }
981 #[cfg(not(feature = "https"))]
982 {
983 let _ = prefetched;
984 let _ = stream;
985 return Err(NanoGetError::HttpsFeatureRequired);
986 }
987 }
988
989 if head.status_code != 407 {
990 break Err(NanoGetError::ProxyConnectFailed(
991 head.status_code,
992 head.reason_phrase,
993 ));
994 }
995 let response = Response {
996 version: head.version,
997 status_code: head.status_code,
998 reason_phrase: head.reason_phrase.clone(),
999 headers: head.headers,
1000 trailers: Vec::new(),
1001 body: Vec::new(),
1002 };
1003 let retry = maybe_retry_request_auth(
1004 config.proxy_auth_handler.as_ref(),
1005 AuthTarget::Proxy,
1006 ¤t,
1007 &response,
1008 &mut seen_proxy_challenges,
1009 )?;
1010 if let Some(retry) = retry {
1011 current = retry;
1012 continue;
1013 }
1014 break Err(NanoGetError::ProxyConnectFailed(
1015 response.status_code,
1016 response.reason_phrase,
1017 ));
1018 }
1019}
1020
1021#[derive(Default)]
1022struct MemoryCache {
1023 entries: HashMap<String, Vec<CacheEntry>>,
1024 partial_entries: HashMap<String, Vec<PartialCacheEntry>>,
1025}
1026
1027impl MemoryCache {
1028 fn lookup(
1029 &self,
1030 request: &Request,
1031 now: SystemTime,
1032 auth_context: &AuthContext,
1033 ) -> Option<CacheLookup> {
1034 let request_cache_control = CacheControl::from_headers(request.headers());
1035 let Some(entries) = self.entries.get(&request.url().cache_key()) else {
1036 return if request_cache_control.only_if_cached {
1037 Some(CacheLookup::UnsatisfiedOnlyIfCached)
1038 } else {
1039 None
1040 };
1041 };
1042 let Some(entry) = entries
1043 .iter()
1044 .filter(|entry| entry.matches(request, auth_context))
1045 .max_by_key(|entry| entry.response_time)
1046 else {
1047 return if request_cache_control.only_if_cached {
1048 Some(CacheLookup::UnsatisfiedOnlyIfCached)
1049 } else {
1050 None
1051 };
1052 };
1053
1054 if !entry.satisfies_request(&request_cache_control, now) {
1055 return if request_cache_control.only_if_cached {
1056 Some(CacheLookup::UnsatisfiedOnlyIfCached)
1057 } else {
1058 Some(CacheLookup::Stale(Box::new(entry.clone())))
1059 };
1060 }
1061
1062 Some(CacheLookup::Fresh(entry.response_with_age(now)))
1063 }
1064
1065 fn lookup_range(
1066 &self,
1067 request: &Request,
1068 now: SystemTime,
1069 auth_context: &AuthContext,
1070 ) -> Option<RangeCacheLookup> {
1071 let range_header = request.header("range")?;
1072 let range_spec = parse_single_range(range_header)?;
1073 let request_cache_control = CacheControl::from_headers(request.headers());
1074 let if_range = request.header("if-range").map(str::trim);
1075 let key = request.url().cache_key();
1076
1077 if let Some(entry) = self.entries.get(&key).and_then(|entries| {
1078 entries
1079 .iter()
1080 .filter(|entry| entry.matches(request, auth_context))
1081 .max_by_key(|entry| entry.response_time)
1082 }) {
1083 if !if_range_matches_entry(
1084 if_range,
1085 entry.etag.as_deref(),
1086 entry.last_modified.as_deref(),
1087 ) {
1088 return Some(if request_cache_control.only_if_cached {
1089 RangeCacheLookup::UnsatisfiedOnlyIfCached
1090 } else {
1091 RangeCacheLookup::IfRangeMismatch
1092 });
1093 }
1094 if entry.satisfies_request(&request_cache_control, now) {
1095 return Some(RangeCacheLookup::Hit(entry.range_response(range_spec, now)));
1096 }
1097 }
1098
1099 if let Some(entry) = self.partial_entries.get(&key).and_then(|entries| {
1100 entries
1101 .iter()
1102 .filter(|entry| entry.matches(request, auth_context))
1103 .max_by_key(|entry| entry.response_time)
1104 }) {
1105 if !if_range_matches_entry(
1106 if_range,
1107 Some(entry.etag.as_str()),
1108 entry.last_modified.as_deref(),
1109 ) {
1110 return Some(if request_cache_control.only_if_cached {
1111 RangeCacheLookup::UnsatisfiedOnlyIfCached
1112 } else {
1113 RangeCacheLookup::IfRangeMismatch
1114 });
1115 }
1116 let cached_range = entry
1117 .satisfies_request(&request_cache_control, now)
1118 .then(|| entry.range_response(range_spec, now))
1119 .flatten();
1120 if let Some(response) = cached_range {
1121 return Some(RangeCacheLookup::Hit(response));
1122 }
1123 }
1124
1125 if request_cache_control.only_if_cached {
1126 Some(RangeCacheLookup::UnsatisfiedOnlyIfCached)
1127 } else {
1128 None
1129 }
1130 }
1131
1132 fn store(&mut self, request: &Request, response: &TimedResponse, auth_context: &AuthContext) {
1133 if request.method() == Method::Get && response.response.status_code == 206 {
1134 self.store_partial(request, response, auth_context);
1135 return;
1136 }
1137
1138 let Some(entry) = CacheEntry::new(request, response, auth_context.clone()) else {
1139 return;
1140 };
1141
1142 let key = request.url().cache_key();
1143 let variants = self.entries.entry(key).or_default();
1144 if request.method() == Method::Head {
1145 if let Some(existing) = variants
1146 .iter_mut()
1147 .find(|existing| existing.same_variant(&entry))
1148 {
1149 if head_update_is_compatible(existing, &entry) {
1150 let body = existing.response.body.clone();
1151 let mut updated = entry;
1152 updated.response.body = body;
1153 *existing = updated;
1154 } else {
1155 existing.freshness_lifetime = Duration::from_secs(0);
1156 existing.cache_control.no_cache = true;
1157 }
1158 }
1159 return;
1160 }
1161
1162 if let Some(existing) = variants
1163 .iter_mut()
1164 .find(|existing| existing.same_variant(&entry))
1165 {
1166 *existing = entry;
1167 return;
1168 }
1169
1170 variants.push(entry);
1171 }
1172
1173 fn store_partial(
1174 &mut self,
1175 request: &Request,
1176 timed_response: &TimedResponse,
1177 auth_context: &AuthContext,
1178 ) {
1179 let Some(partial) = PartialCacheEntry::new(request, timed_response, auth_context.clone())
1180 else {
1181 return;
1182 };
1183
1184 let key = request.url().cache_key();
1185 let combined_entry = {
1186 let variants = self.partial_entries.entry(key.clone()).or_default();
1187 if let Some(existing) = variants
1188 .iter_mut()
1189 .find(|existing| existing.same_variant(&partial))
1190 {
1191 existing.merge_from(partial);
1192 existing.promote_complete()
1193 } else {
1194 let inserted = partial;
1195 let combined_entry = inserted.promote_complete();
1196 variants.push(inserted);
1197 combined_entry
1198 }
1199 };
1200
1201 if let Some(combined) = combined_entry {
1202 self.upsert_complete_entry(key, combined);
1203 }
1204 }
1205
1206 fn upsert_complete_entry(&mut self, key: String, entry: CacheEntry) {
1207 let variants = self.entries.entry(key).or_default();
1208 if let Some(existing) = variants
1209 .iter_mut()
1210 .find(|candidate| candidate.same_variant(&entry))
1211 {
1212 *existing = entry;
1213 } else {
1214 variants.push(entry);
1215 }
1216 }
1217
1218 fn merge_not_modified(
1219 &mut self,
1220 request: &Request,
1221 stale: &CacheEntry,
1222 not_modified: &Response,
1223 now: SystemTime,
1224 ) -> Result<Response, NanoGetError> {
1225 let variants = self
1226 .entries
1227 .get_mut(&request.url().cache_key())
1228 .ok_or_else(|| NanoGetError::Cache("stale cache entry disappeared".to_string()))?;
1229 let existing = variants
1230 .iter_mut()
1231 .find(|entry| entry.same_variant(stale))
1232 .ok_or_else(|| NanoGetError::Cache("stale cache variant disappeared".to_string()))?;
1233
1234 merge_headers_for_304(&mut existing.response.headers, ¬_modified.headers);
1235 existing.cache_control = CacheControl::from_headers(&existing.response.headers);
1236 existing.request_time = now;
1237 existing.response_time = now;
1238 existing.freshness_lifetime = compute_freshness_lifetime(&existing.response, now);
1239 existing.age_header = parse_age_header(&existing.response.headers);
1240 existing.date_header = existing.response.header("date").and_then(parse_http_date);
1241 existing.etag = header_value(&existing.response.headers, "etag").map(str::to_string);
1242 existing.last_modified =
1243 header_value(&existing.response.headers, "last-modified").map(str::to_string);
1244 Ok(existing.response.clone())
1245 }
1246}
1247
1248enum RangeCacheLookup {
1249 Hit(Response),
1250 UnsatisfiedOnlyIfCached,
1251 IfRangeMismatch,
1252}
1253
1254#[derive(Clone, Copy)]
1255struct ByteRange {
1256 start: Option<usize>,
1257 end: Option<usize>,
1258}
1259
1260impl ByteRange {
1261 fn resolve(self, total_length: usize) -> Option<(usize, usize)> {
1262 if total_length == 0 {
1263 return None;
1264 }
1265
1266 match (self.start, self.end) {
1267 (Some(start), Some(end)) if start <= end && start < total_length => {
1268 Some((start, end.min(total_length - 1)))
1269 }
1270 (Some(start), None) if start < total_length => Some((start, total_length - 1)),
1271 (None, Some(suffix_len)) if suffix_len > 0 => {
1272 let len = suffix_len.min(total_length);
1273 Some((total_length - len, total_length - 1))
1274 }
1275 _ => None,
1276 }
1277 }
1278}
1279
1280#[derive(Clone)]
1281struct CacheEntry {
1282 vary_headers: Vec<VaryHeader>,
1283 response: Response,
1284 request_time: SystemTime,
1285 response_time: SystemTime,
1286 freshness_lifetime: Duration,
1287 cache_control: CacheControl,
1288 etag: Option<String>,
1289 last_modified: Option<String>,
1290 age_header: Option<Duration>,
1291 date_header: Option<SystemTime>,
1292 auth_context: AuthContext,
1293}
1294
1295impl CacheEntry {
1296 fn new(
1297 request: &Request,
1298 timed_response: &TimedResponse,
1299 auth_context: AuthContext,
1300 ) -> Option<Self> {
1301 let response = &timed_response.response;
1302 let cache_control = CacheControl::from_headers(&response.headers);
1303 if cache_control.no_store || !is_cacheable_status(response.status_code) {
1304 return None;
1305 }
1306 if auth_context.proxy.is_some() {
1307 return None;
1308 }
1309 if auth_context.origin.is_some() && !(cache_control.public || cache_control.private) {
1310 return None;
1311 }
1312
1313 let vary_headers = extract_vary_headers(request, response)?;
1314 Some(Self {
1315 vary_headers,
1316 response: response.clone(),
1317 request_time: timed_response.request_time,
1318 response_time: timed_response.response_time,
1319 freshness_lifetime: compute_freshness_lifetime(response, timed_response.response_time),
1320 cache_control,
1321 etag: response.header("etag").map(str::to_string),
1322 last_modified: response.header("last-modified").map(str::to_string),
1323 age_header: parse_age_header(&response.headers),
1324 date_header: response.header("date").and_then(parse_http_date),
1325 auth_context,
1326 })
1327 }
1328
1329 fn matches(&self, request: &Request, auth_context: &AuthContext) -> bool {
1330 self.auth_context == *auth_context
1331 && self.vary_headers.iter().all(|vary| vary.matches(request))
1332 }
1333
1334 fn same_variant(&self, other: &Self) -> bool {
1335 self.vary_headers == other.vary_headers
1336 }
1337
1338 fn is_fresh(&self, now: SystemTime) -> bool {
1339 if self.cache_control.no_cache {
1340 return false;
1341 }
1342
1343 self.current_age(now) <= self.freshness_lifetime
1344 }
1345
1346 fn current_age(&self, now: SystemTime) -> Duration {
1347 let apparent_age = match self.date_header {
1348 Some(date) => self
1349 .response_time
1350 .duration_since(date)
1351 .unwrap_or_else(|_| Duration::from_secs(0)),
1352 None => Duration::from_secs(0),
1353 };
1354 let corrected_received_age = self
1355 .age_header
1356 .map(|age| age.max(apparent_age))
1357 .unwrap_or(apparent_age);
1358 let response_delay = self
1359 .response_time
1360 .duration_since(self.request_time)
1361 .unwrap_or_else(|_| Duration::from_secs(0));
1362 let corrected_initial_age = corrected_received_age + response_delay;
1363 let resident_time = now
1364 .duration_since(self.response_time)
1365 .unwrap_or_else(|_| Duration::from_secs(0));
1366 corrected_initial_age + resident_time
1367 }
1368
1369 fn remaining_freshness(&self, now: SystemTime) -> Duration {
1370 self.freshness_lifetime
1371 .saturating_sub(self.current_age(now))
1372 }
1373
1374 fn staleness(&self, now: SystemTime) -> Duration {
1375 self.current_age(now)
1376 .saturating_sub(self.freshness_lifetime)
1377 }
1378
1379 fn satisfies_request(&self, request_cache_control: &CacheControl, now: SystemTime) -> bool {
1380 if request_cache_control.no_cache || self.cache_control.no_cache {
1381 return false;
1382 }
1383
1384 let age = self.current_age(now);
1385 if request_cache_control
1386 .max_age
1387 .is_some_and(|max_age| age > Duration::from_secs(max_age))
1388 {
1389 return false;
1390 }
1391
1392 if request_cache_control
1393 .min_fresh
1394 .is_some_and(|min_fresh| self.remaining_freshness(now) < Duration::from_secs(min_fresh))
1395 {
1396 return false;
1397 }
1398
1399 if self.is_fresh(now) {
1400 return true;
1401 }
1402
1403 if self.cache_control.must_revalidate || self.cache_control.proxy_revalidate {
1404 return false;
1405 }
1406
1407 match request_cache_control.max_stale {
1408 Some(None) => true,
1409 Some(Some(max_stale)) => self.staleness(now) <= Duration::from_secs(max_stale),
1410 None => false,
1411 }
1412 }
1413
1414 fn response_with_age(&self, now: SystemTime) -> Response {
1415 let mut response = self.response.clone();
1416 set_age_header(&mut response.headers, self.current_age(now));
1417 response
1418 }
1419
1420 fn range_response(&self, range: ByteRange, now: SystemTime) -> Response {
1421 match range.resolve(self.response.body.len()) {
1422 Some((start, end)) => {
1423 let mut response = self.response_with_age(now);
1424 response.status_code = 206;
1425 response.reason_phrase = "Partial Content".to_string();
1426 response.body = self.response.body[start..=end].to_vec();
1427 response
1428 .headers
1429 .retain(|header| !header.matches_name("content-range"));
1430 response
1431 .headers
1432 .retain(|header| !header.matches_name("content-length"));
1433 response.headers.push(Header::unchecked(
1434 "Content-Range",
1435 format!("bytes {start}-{end}/{}", self.response.body.len()),
1436 ));
1437 response.headers.push(Header::unchecked(
1438 "Content-Length",
1439 response.body.len().to_string(),
1440 ));
1441 response
1442 }
1443 None => {
1444 let mut response = self.response_with_age(now);
1445 response.status_code = 416;
1446 response.reason_phrase = "Range Not Satisfiable".to_string();
1447 response.body.clear();
1448 response
1449 .headers
1450 .retain(|header| !header.matches_name("content-range"));
1451 response
1452 .headers
1453 .retain(|header| !header.matches_name("content-length"));
1454 response.headers.push(Header::unchecked(
1455 "Content-Range",
1456 format!("bytes */{}", self.response.body.len()),
1457 ));
1458 response
1459 .headers
1460 .push(Header::unchecked("Content-Length", "0".to_string()));
1461 response
1462 }
1463 }
1464 }
1465}
1466
1467enum CacheLookup {
1468 Fresh(Response),
1469 Stale(Box<CacheEntry>),
1470 UnsatisfiedOnlyIfCached,
1471}
1472
1473#[derive(Clone)]
1474struct PartialCacheEntry {
1475 vary_headers: Vec<VaryHeader>,
1476 response: Response,
1477 request_time: SystemTime,
1478 response_time: SystemTime,
1479 freshness_lifetime: Duration,
1480 cache_control: CacheControl,
1481 age_header: Option<Duration>,
1482 date_header: Option<SystemTime>,
1483 auth_context: AuthContext,
1484 etag: String,
1485 last_modified: Option<String>,
1486 total_length: usize,
1487 segments: Vec<ByteSegment>,
1488}
1489
1490#[derive(Clone)]
1491struct ByteSegment {
1492 start: usize,
1493 end: usize,
1494 bytes: Vec<u8>,
1495}
1496
1497impl PartialCacheEntry {
1498 fn new(
1499 request: &Request,
1500 timed_response: &TimedResponse,
1501 auth_context: AuthContext,
1502 ) -> Option<Self> {
1503 let response = &timed_response.response;
1504 let cache_control = CacheControl::from_headers(&response.headers);
1505 if cache_control.no_store || response.status_code != 206 {
1506 return None;
1507 }
1508 if auth_context.proxy.is_some() {
1509 return None;
1510 }
1511 if auth_context.origin.is_some() && !(cache_control.public || cache_control.private) {
1512 return None;
1513 }
1514 let content_range = response.header("content-range")?;
1515 let (start, end, total_length) = parse_content_range(content_range)?;
1516 if end < start || end.saturating_sub(start) + 1 != response.body.len() {
1517 return None;
1518 }
1519 let etag = response.header("etag")?.trim().to_string();
1520 if !is_strong_etag(&etag) {
1521 return None;
1522 }
1523 let vary_headers = extract_vary_headers(request, response)?;
1524 Some(Self {
1525 vary_headers,
1526 response: response.clone(),
1527 request_time: timed_response.request_time,
1528 response_time: timed_response.response_time,
1529 freshness_lifetime: compute_freshness_lifetime(response, timed_response.response_time),
1530 cache_control,
1531 age_header: parse_age_header(&response.headers),
1532 date_header: response.header("date").and_then(parse_http_date),
1533 auth_context,
1534 etag,
1535 last_modified: response.header("last-modified").map(str::to_string),
1536 total_length,
1537 segments: vec![ByteSegment {
1538 start,
1539 end,
1540 bytes: response.body.clone(),
1541 }],
1542 })
1543 }
1544
1545 fn matches(&self, request: &Request, auth_context: &AuthContext) -> bool {
1546 self.auth_context == *auth_context
1547 && self.vary_headers.iter().all(|vary| vary.matches(request))
1548 }
1549
1550 fn same_variant(&self, other: &Self) -> bool {
1551 self.vary_headers == other.vary_headers
1552 && self.auth_context == other.auth_context
1553 && self.etag == other.etag
1554 && self.total_length == other.total_length
1555 }
1556
1557 fn merge_from(&mut self, mut other: Self) {
1558 self.request_time = other.request_time;
1559 self.response_time = other.response_time;
1560 self.freshness_lifetime = other.freshness_lifetime;
1561 self.cache_control = other.cache_control;
1562 self.age_header = other.age_header;
1563 self.date_header = other.date_header;
1564 self.last_modified = other.last_modified.take();
1565 self.response = other.response;
1566 self.segments.append(&mut other.segments);
1567 normalize_segments(&mut self.segments);
1568 }
1569
1570 fn current_age(&self, now: SystemTime) -> Duration {
1571 let apparent_age = match self.date_header {
1572 Some(date) => self
1573 .response_time
1574 .duration_since(date)
1575 .unwrap_or_else(|_| Duration::from_secs(0)),
1576 None => Duration::from_secs(0),
1577 };
1578 let corrected_received_age = self
1579 .age_header
1580 .map(|age| age.max(apparent_age))
1581 .unwrap_or(apparent_age);
1582 let response_delay = self
1583 .response_time
1584 .duration_since(self.request_time)
1585 .unwrap_or_else(|_| Duration::from_secs(0));
1586 let corrected_initial_age = corrected_received_age + response_delay;
1587 let resident_time = now
1588 .duration_since(self.response_time)
1589 .unwrap_or_else(|_| Duration::from_secs(0));
1590 corrected_initial_age + resident_time
1591 }
1592
1593 fn remaining_freshness(&self, now: SystemTime) -> Duration {
1594 self.freshness_lifetime
1595 .saturating_sub(self.current_age(now))
1596 }
1597
1598 fn staleness(&self, now: SystemTime) -> Duration {
1599 self.current_age(now)
1600 .saturating_sub(self.freshness_lifetime)
1601 }
1602
1603 fn is_fresh(&self, now: SystemTime) -> bool {
1604 if self.cache_control.no_cache {
1605 return false;
1606 }
1607 self.current_age(now) <= self.freshness_lifetime
1608 }
1609
1610 fn satisfies_request(&self, request_cache_control: &CacheControl, now: SystemTime) -> bool {
1611 if request_cache_control.no_cache || self.cache_control.no_cache {
1612 return false;
1613 }
1614
1615 let age = self.current_age(now);
1616 if request_cache_control
1617 .max_age
1618 .is_some_and(|max_age| age > Duration::from_secs(max_age))
1619 {
1620 return false;
1621 }
1622
1623 if request_cache_control
1624 .min_fresh
1625 .is_some_and(|min_fresh| self.remaining_freshness(now) < Duration::from_secs(min_fresh))
1626 {
1627 return false;
1628 }
1629
1630 if self.is_fresh(now) {
1631 return true;
1632 }
1633
1634 if self.cache_control.must_revalidate || self.cache_control.proxy_revalidate {
1635 return false;
1636 }
1637
1638 match request_cache_control.max_stale {
1639 Some(None) => true,
1640 Some(Some(max_stale)) => self.staleness(now) <= Duration::from_secs(max_stale),
1641 None => false,
1642 }
1643 }
1644
1645 fn range_response(&self, range: ByteRange, now: SystemTime) -> Option<Response> {
1646 let (start, end) = range.resolve(self.total_length)?;
1647 let segment = self
1648 .segments
1649 .iter()
1650 .find(|segment| segment.start <= start && segment.end >= end)?;
1651 let offset_start = start - segment.start;
1652 let offset_end = end - segment.start + 1;
1653
1654 let mut response = self.response.clone();
1655 set_age_header(&mut response.headers, self.current_age(now));
1656 response.status_code = 206;
1657 response.reason_phrase = "Partial Content".to_string();
1658 response.body = segment.bytes[offset_start..offset_end].to_vec();
1659 response
1660 .headers
1661 .retain(|header| !header.matches_name("content-range"));
1662 response
1663 .headers
1664 .retain(|header| !header.matches_name("content-length"));
1665 response.headers.push(Header::unchecked(
1666 "Content-Range",
1667 format!("bytes {start}-{end}/{}", self.total_length),
1668 ));
1669 response.headers.push(Header::unchecked(
1670 "Content-Length",
1671 response.body.len().to_string(),
1672 ));
1673 Some(response)
1674 }
1675
1676 fn promote_complete(&self) -> Option<CacheEntry> {
1677 if self.segments.len() != 1 {
1678 return None;
1679 }
1680 let segment = &self.segments[0];
1681 if segment.start != 0 || segment.end + 1 != self.total_length {
1682 return None;
1683 }
1684
1685 let mut headers: Vec<Header> = self
1686 .response
1687 .headers
1688 .iter()
1689 .filter(|header| {
1690 !header.matches_name("content-range") && !header.matches_name("content-length")
1691 })
1692 .cloned()
1693 .collect();
1694 headers.push(Header::unchecked(
1695 "Content-Length",
1696 self.total_length.to_string(),
1697 ));
1698
1699 let response = Response {
1700 version: self.response.version,
1701 status_code: 200,
1702 reason_phrase: "OK".to_string(),
1703 headers,
1704 trailers: Vec::new(),
1705 body: segment.bytes.clone(),
1706 };
1707
1708 Some(CacheEntry {
1709 vary_headers: self.vary_headers.clone(),
1710 response,
1711 request_time: self.request_time,
1712 response_time: self.response_time,
1713 freshness_lifetime: self.freshness_lifetime,
1714 cache_control: self.cache_control,
1715 etag: Some(self.etag.clone()),
1716 last_modified: self.last_modified.clone(),
1717 age_header: self.age_header,
1718 date_header: self.date_header,
1719 auth_context: self.auth_context.clone(),
1720 })
1721 }
1722}
1723
1724#[derive(Debug, Clone, PartialEq, Eq)]
1725struct VaryHeader {
1726 name: String,
1727 values: Vec<String>,
1728}
1729
1730impl VaryHeader {
1731 fn matches(&self, request: &Request) -> bool {
1732 let mut values = request
1733 .headers_named(&self.name)
1734 .map(|header| header.value());
1735 for expected in &self.values {
1736 match values.next() {
1737 Some(value) if value == expected => {}
1738 _ => return false,
1739 }
1740 }
1741 values.next().is_none()
1742 }
1743}
1744
1745#[derive(Debug, Clone, Copy, Default)]
1746struct CacheControl {
1747 no_store: bool,
1748 no_cache: bool,
1749 max_age: Option<u64>,
1750 max_stale: Option<Option<u64>>,
1751 min_fresh: Option<u64>,
1752 only_if_cached: bool,
1753 must_revalidate: bool,
1754 proxy_revalidate: bool,
1755 public: bool,
1756 private: bool,
1757}
1758
1759impl CacheControl {
1760 fn from_headers(headers: &[Header]) -> Self {
1761 let mut directives = CacheControl::default();
1762 for value in headers
1763 .iter()
1764 .filter(|header| header.matches_name("cache-control"))
1765 .map(Header::value)
1766 {
1767 for directive in value
1768 .split(',')
1769 .map(str::trim)
1770 .filter(|value| !value.is_empty())
1771 {
1772 let (name, argument) = match directive.split_once('=') {
1773 Some((name, argument)) => {
1774 (name.trim().to_ascii_lowercase(), Some(argument.trim()))
1775 }
1776 None => (directive.to_ascii_lowercase(), None),
1777 };
1778 match name.as_str() {
1779 "no-store" => directives.no_store = true,
1780 "no-cache" => directives.no_cache = true,
1781 "must-revalidate" => directives.must_revalidate = true,
1782 "proxy-revalidate" => directives.proxy_revalidate = true,
1783 "public" => directives.public = true,
1784 "private" => directives.private = true,
1785 "only-if-cached" => directives.only_if_cached = true,
1786 "max-age" => {
1787 directives.max_age = argument.and_then(parse_directive_u64);
1788 }
1789 "max-stale" if argument.is_none() => {
1790 directives.max_stale = Some(None);
1791 }
1792 "max-stale" => {
1793 directives.max_stale = argument.and_then(parse_directive_u64).map(Some);
1794 }
1795 "min-fresh" => {
1796 directives.min_fresh = argument.and_then(parse_directive_u64);
1797 }
1798 _ => {}
1799 }
1800 }
1801 }
1802
1803 directives
1804 }
1805}
1806
1807fn response_for_method(mut response: Response, method: Method) -> Response {
1808 if method == Method::Head {
1809 response.body.clear();
1810 }
1811
1812 response
1813}
1814
1815fn pipeline_retryable_parse_error(error: &NanoGetError) -> bool {
1816 matches!(
1817 error,
1818 NanoGetError::Io(_) | NanoGetError::IncompleteMessage(_)
1819 )
1820}
1821
1822fn retryable_reconnect_error(error: &NanoGetError) -> bool {
1823 matches!(error, NanoGetError::Connect(_) | NanoGetError::Tls(_))
1824 || pipeline_retryable_parse_error(error)
1825}
1826
1827fn has_user_conditionals(request: &Request) -> bool {
1828 [
1829 "if-none-match",
1830 "if-match",
1831 "if-modified-since",
1832 "if-unmodified-since",
1833 "if-range",
1834 ]
1835 .iter()
1836 .any(|name| request.has_header(name))
1837}
1838
1839fn extract_vary_headers(request: &Request, response: &Response) -> Option<Vec<VaryHeader>> {
1840 let vary_names: Vec<String> = response
1841 .headers_named("vary")
1842 .flat_map(|header| header.value().split(','))
1843 .map(str::trim)
1844 .filter(|value| !value.is_empty())
1845 .map(str::to_ascii_lowercase)
1846 .collect();
1847 if vary_names.is_empty() {
1848 return Some(Vec::new());
1849 }
1850 if vary_names.iter().any(|value| value == "*") {
1851 return None;
1852 }
1853
1854 Some(
1855 vary_names
1856 .iter()
1857 .map(|name| VaryHeader {
1858 name: name.clone(),
1859 values: request
1860 .headers_named(name)
1861 .map(|header| header.value().to_string())
1862 .collect(),
1863 })
1864 .collect(),
1865 )
1866}
1867
1868fn is_cacheable_status(status_code: u16) -> bool {
1869 matches!(
1870 status_code,
1871 200 | 203 | 204 | 300 | 301 | 404 | 405 | 410 | 414 | 501
1872 )
1873}
1874
1875fn compute_freshness_lifetime(response: &Response, now: SystemTime) -> Duration {
1876 let cache_control = CacheControl::from_headers(&response.headers);
1877 if let Some(max_age) = cache_control.max_age {
1878 return Duration::from_secs(max_age);
1879 }
1880
1881 if let Some(expires) = response.header("expires").and_then(parse_http_date) {
1882 if let Some(date) = response.header("date").and_then(parse_http_date) {
1883 return expires
1884 .duration_since(date)
1885 .unwrap_or_else(|_| Duration::from_secs(0));
1886 }
1887 return expires
1888 .duration_since(now)
1889 .unwrap_or_else(|_| Duration::from_secs(0));
1890 }
1891
1892 if let (Some(last_modified), Some(date)) = (
1893 response.header("last-modified").and_then(parse_http_date),
1894 response.header("date").and_then(parse_http_date),
1895 ) {
1896 return date
1897 .duration_since(last_modified)
1898 .map(|age| (age / 10).min(Duration::from_secs(86_400)))
1899 .unwrap_or(Duration::from_secs(0));
1900 }
1901
1902 Duration::from_secs(0)
1903}
1904
1905fn header_value<'a>(headers: &'a [Header], name: &str) -> Option<&'a str> {
1906 headers
1907 .iter()
1908 .find(|header| header.matches_name(name))
1909 .map(Header::value)
1910}
1911
1912fn merge_headers_for_304(stored: &mut Vec<Header>, fresh: &[Header]) {
1913 for header in fresh {
1914 if header.matches_name("content-length")
1915 || header.matches_name("transfer-encoding")
1916 || header.matches_name("content-range")
1917 {
1918 continue;
1919 }
1920 stored.retain(|existing| !existing.matches_name(header.name()));
1921 stored.push(header.clone());
1922 }
1923}
1924
1925fn parse_age_header(headers: &[Header]) -> Option<Duration> {
1926 header_value(headers, "age")
1927 .and_then(|value| value.trim().parse::<u64>().ok())
1928 .map(Duration::from_secs)
1929}
1930
1931fn set_age_header(headers: &mut Vec<Header>, age: Duration) {
1932 let age_seconds = age.as_secs().to_string();
1933 headers.retain(|header| !header.matches_name("age"));
1934 headers.push(Header::unchecked("Age", age_seconds));
1935}
1936
1937fn head_update_is_compatible(existing: &CacheEntry, candidate: &CacheEntry) -> bool {
1938 let current_etag = existing.etag.as_deref();
1939 let candidate_etag = candidate.etag.as_deref();
1940 if let (Some(current), Some(next)) = (current_etag, candidate_etag) {
1941 if current != next {
1942 return false;
1943 }
1944 }
1945
1946 if candidate
1947 .response
1948 .header("content-length")
1949 .and_then(|value| value.parse::<usize>().ok())
1950 .is_some_and(|content_length| content_length != existing.response.body.len())
1951 {
1952 return false;
1953 }
1954
1955 true
1956}
1957
1958fn parse_directive_u64(value: &str) -> Option<u64> {
1959 let value = value.trim().trim_matches('"');
1960 value.parse::<u64>().ok()
1961}
1962
1963fn parse_single_range(value: &str) -> Option<ByteRange> {
1964 let value = value.trim();
1965 let bytes = value.strip_prefix("bytes=")?.trim();
1966 if bytes.contains(',') {
1967 return None;
1968 }
1969 let (start, end) = bytes.split_once('-')?;
1970 if start.is_empty() {
1971 return Some(ByteRange {
1972 start: None,
1973 end: end.parse().ok(),
1974 });
1975 }
1976 let start_value = start.parse().ok()?;
1977 if end.is_empty() {
1978 return Some(ByteRange {
1979 start: Some(start_value),
1980 end: None,
1981 });
1982 }
1983 Some(ByteRange {
1984 start: Some(start_value),
1985 end: end.parse().ok(),
1986 })
1987}
1988
1989fn parse_content_range(value: &str) -> Option<(usize, usize, usize)> {
1990 let value = value.trim();
1991 let bytes = value.strip_prefix("bytes ")?;
1992 let (range, complete_length) = bytes.split_once('/')?;
1993 let complete_length = complete_length.parse().ok()?;
1994 let (start, end) = range.split_once('-')?;
1995 let start = start.parse().ok()?;
1996 let end = end.parse().ok()?;
1997 Some((start, end, complete_length))
1998}
1999
2000fn is_strong_etag(value: &str) -> bool {
2001 let value = value.trim();
2002 value.starts_with('"')
2003 && value.ends_with('"')
2004 && !value.starts_with("W/")
2005 && !value.starts_with("w/")
2006}
2007
2008fn if_range_matches_entry(
2009 if_range: Option<&str>,
2010 etag: Option<&str>,
2011 last_modified: Option<&str>,
2012) -> bool {
2013 let Some(if_range) = if_range else {
2014 return true;
2015 };
2016
2017 if if_range.starts_with('"') || if_range.starts_with("W/") || if_range.starts_with("w/") {
2018 return etag.is_some_and(|cached| cached == if_range);
2019 }
2020
2021 let Some(if_range_date) = parse_http_date(if_range) else {
2022 return false;
2023 };
2024 let Some(last_modified_date) = last_modified.and_then(parse_http_date) else {
2025 return false;
2026 };
2027 last_modified_date <= if_range_date
2028}
2029
2030fn validate_request_conditionals(request: &Request) -> Result<(), NanoGetError> {
2031 let Some(if_range) = request.header("if-range") else {
2032 return Ok(());
2033 };
2034 if !request.has_header("range") {
2035 return Err(NanoGetError::InvalidConditionalRequest(
2036 "If-Range requires Range".to_string(),
2037 ));
2038 }
2039 let if_range = if_range.trim();
2040 if if_range.starts_with("W/") || if_range.starts_with("w/") {
2041 return Err(NanoGetError::InvalidConditionalRequest(
2042 "weak ETags are not valid in If-Range".to_string(),
2043 ));
2044 }
2045 Ok(())
2046}
2047
2048fn normalize_segments(segments: &mut Vec<ByteSegment>) {
2049 if segments.is_empty() {
2050 return;
2051 }
2052 segments.sort_by_key(|segment| segment.start);
2053 let mut merged: Vec<ByteSegment> = Vec::with_capacity(segments.len());
2054
2055 for segment in segments.drain(..) {
2056 if let Some(last) = merged.last_mut() {
2057 if segment.start <= last.end.saturating_add(1) {
2058 let merged_start = last.start;
2059 let merged_end = last.end.max(segment.end);
2060 let mut bytes = vec![0u8; merged_end - merged_start + 1];
2061 let last_offset = last.start - merged_start;
2062 bytes[last_offset..last_offset + last.bytes.len()].copy_from_slice(&last.bytes);
2063 let segment_offset = segment.start - merged_start;
2064 bytes[segment_offset..segment_offset + segment.bytes.len()]
2065 .copy_from_slice(&segment.bytes);
2066 last.start = merged_start;
2067 last.end = merged_end;
2068 last.bytes = bytes;
2069 continue;
2070 }
2071 }
2072 merged.push(segment);
2073 }
2074
2075 *segments = merged;
2076}
2077
2078fn validate_proxy_header_name(name: &str) -> Result<(), NanoGetError> {
2079 match name.to_ascii_lowercase().as_str() {
2080 "host" | "connection" | "content-length" | "transfer-encoding" | "trailer" | "upgrade" => {
2081 Err(NanoGetError::ProtocolManagedHeader(name.to_string()))
2082 }
2083 "keep-alive" | "proxy-connection" | "te" => {
2084 Err(NanoGetError::HopByHopHeader(name.to_string()))
2085 }
2086 _ => Ok(()),
2087 }
2088}
2089
2090fn effective_auth_context(request: &Request, config: &ClientConfig) -> AuthContext {
2091 AuthContext {
2092 origin: request
2093 .header("authorization")
2094 .map(str::to_string)
2095 .or_else(|| config.preemptive_authorization.clone()),
2096 proxy: request
2097 .header("proxy-authorization")
2098 .map(str::to_string)
2099 .or_else(|| config.preemptive_proxy_authorization.clone())
2100 .filter(|_| config.proxy.is_some()),
2101 }
2102}
2103
2104fn prepared_connect_headers(
2105 request: &Request,
2106 config: &ClientConfig,
2107 proxy: &ProxyConfig,
2108) -> Result<Vec<Header>, NanoGetError> {
2109 let mut headers: Vec<Header> = proxy.headers().to_vec();
2110 headers.retain(|header| !header.matches_name("authorization"));
2111
2112 if headers
2113 .iter()
2114 .any(|header| header.matches_name("proxy-authorization"))
2115 {
2116 return Ok(headers);
2117 }
2118
2119 let proxy_authorization = request
2120 .header("proxy-authorization")
2121 .map(str::to_string)
2122 .or_else(|| config.preemptive_proxy_authorization.clone());
2123 if let Some(value) = proxy_authorization {
2124 headers.push(Header::new("Proxy-Authorization", value)?);
2125 }
2126
2127 Ok(headers)
2128}
2129
2130fn maybe_retry_request_auth(
2131 handler: Option<&DynAuthHandler>,
2132 target: AuthTarget,
2133 request: &Request,
2134 response: &Response,
2135 seen_challenges: &mut Vec<Vec<Challenge>>,
2136) -> Result<Option<Request>, NanoGetError> {
2137 let header_name = match target {
2138 AuthTarget::Origin => "www-authenticate",
2139 AuthTarget::Proxy => "proxy-authenticate",
2140 };
2141 if !response
2142 .headers
2143 .iter()
2144 .any(|header| header.matches_name(header_name))
2145 {
2146 return Ok(None);
2147 }
2148
2149 let existing_header = match target {
2150 AuthTarget::Origin => "authorization",
2151 AuthTarget::Proxy => "proxy-authorization",
2152 };
2153 if request.has_header(existing_header) {
2154 return Ok(None);
2155 }
2156
2157 let challenges = parse_authenticate_headers(&response.headers, header_name)?;
2158 if challenges.is_empty() {
2159 return Ok(None);
2160 }
2161 if seen_challenges
2162 .iter()
2163 .any(|previous| previous == &challenges)
2164 {
2165 return Ok(None);
2166 }
2167 seen_challenges.push(challenges.clone());
2168
2169 let Some(handler) = handler else {
2170 return Ok(None);
2171 };
2172
2173 match handler.respond(target, request.url(), &challenges, request, response)? {
2174 AuthDecision::UseHeaders(headers) => {
2175 let mut retry = request.clone();
2176 retry.remove_headers_named(existing_header);
2177 for header in headers {
2178 if header.matches_name(existing_header) {
2179 retry.set_header(header.name().to_string(), header.value().to_string())?;
2180 }
2181 }
2182 Ok(Some(retry))
2183 }
2184 AuthDecision::NoMatch => Ok(None),
2185 AuthDecision::Abort => Err(NanoGetError::AuthenticationRejected(match target {
2186 AuthTarget::Origin => "origin authentication handler aborted".to_string(),
2187 AuthTarget::Proxy => "proxy authentication handler aborted".to_string(),
2188 })),
2189 }
2190}
2191
2192fn gateway_timeout_response() -> Response {
2193 Response {
2194 version: crate::response::HttpVersion::Http11,
2195 status_code: 504,
2196 reason_phrase: "Gateway Timeout".to_string(),
2197 headers: Vec::new(),
2198 trailers: Vec::new(),
2199 body: Vec::new(),
2200 }
2201}
2202
2203#[cfg(test)]
2204mod tests {
2205 use std::io::{Read, Write};
2206 use std::net::TcpListener;
2207 use std::sync::{Arc, Mutex};
2208 use std::thread;
2209 use std::time::SystemTime;
2210
2211 use std::time::{Duration, UNIX_EPOCH};
2212
2213 use super::{
2214 if_range_matches_entry, is_strong_etag, normalize_segments, parse_content_range,
2215 parse_single_range, pipeline_retryable_parse_error, prepared_connect_headers,
2216 retryable_reconnect_error, validate_request_conditionals, AuthContext, ByteRange,
2217 ByteSegment, CacheControl, CacheEntry, CacheMode, Client, ClientBuilder, ClientConfig,
2218 ConnectionPolicy, MemoryCache, ParserStrictness, PartialCacheEntry, ProxyConfig,
2219 TimedResponse,
2220 };
2221 use crate::auth::{AuthDecision, AuthHandler, AuthTarget, Challenge};
2222 use crate::request::{Header, Method, RedirectPolicy, Request};
2223 use crate::response::{HttpVersion, Response};
2224 use crate::url::Url;
2225
2226 struct AbortAuthHandler;
2227
2228 impl AuthHandler for AbortAuthHandler {
2229 fn respond(
2230 &self,
2231 _target: AuthTarget,
2232 _url: &Url,
2233 _challenges: &[Challenge],
2234 _request: &Request,
2235 _response: &Response,
2236 ) -> Result<AuthDecision, crate::NanoGetError> {
2237 Ok(AuthDecision::Abort)
2238 }
2239 }
2240
2241 fn timed_response(response: Response) -> TimedResponse {
2242 let now = SystemTime::now();
2243 TimedResponse {
2244 response,
2245 request_time: now,
2246 response_time: now,
2247 }
2248 }
2249
2250 #[cfg(feature = "https")]
2251 #[test]
2252 fn prefixed_stream_reads_prefix_before_inner_stream() {
2253 let inner = std::io::Cursor::new(b"tail".to_vec());
2254 let mut stream = super::PrefixedStream::new(inner, b"head".to_vec());
2255 let mut body = Vec::new();
2256 stream.read_to_end(&mut body).unwrap();
2257 assert_eq!(body, b"headtail");
2258 }
2259
2260 fn response(status: u16, headers: Vec<crate::Header>, body: &[u8]) -> Response {
2261 Response {
2262 version: HttpVersion::Http11,
2263 status_code: status,
2264 reason_phrase: "OK".to_string(),
2265 headers,
2266 trailers: Vec::new(),
2267 body: body.to_vec(),
2268 }
2269 }
2270
2271 struct NoMatchAuthHandler;
2272
2273 impl AuthHandler for NoMatchAuthHandler {
2274 fn respond(
2275 &self,
2276 _target: AuthTarget,
2277 _url: &Url,
2278 _challenges: &[Challenge],
2279 _request: &Request,
2280 _response: &Response,
2281 ) -> Result<AuthDecision, crate::NanoGetError> {
2282 Ok(AuthDecision::NoMatch)
2283 }
2284 }
2285
2286 struct UseHeadersAuthHandler {
2287 header_name: &'static str,
2288 header_value: &'static str,
2289 }
2290
2291 impl AuthHandler for UseHeadersAuthHandler {
2292 fn respond(
2293 &self,
2294 _target: AuthTarget,
2295 _url: &Url,
2296 _challenges: &[Challenge],
2297 _request: &Request,
2298 _response: &Response,
2299 ) -> Result<AuthDecision, crate::NanoGetError> {
2300 Ok(AuthDecision::UseHeaders(vec![
2301 Header::unchecked("X-Ignored", "1"),
2302 Header::unchecked(self.header_name, self.header_value),
2303 ]))
2304 }
2305 }
2306
2307 fn spawn_recording_server(
2308 responses: Vec<Vec<u8>>,
2309 ) -> (u16, Arc<Mutex<Vec<String>>>, thread::JoinHandle<()>) {
2310 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2311 let port = listener.local_addr().unwrap().port();
2312 let requests = Arc::new(Mutex::new(Vec::<String>::new()));
2313 let requests_for_thread = Arc::clone(&requests);
2314 let handle = thread::spawn(move || {
2315 for response in responses {
2316 let (mut stream, _) = listener.accept().unwrap();
2317 let mut request = Vec::new();
2318 let mut chunk = [0u8; 256];
2319 let mut read = stream.read(&mut chunk).unwrap();
2320 while read > 0 {
2321 request.extend_from_slice(&chunk[..read]);
2322 if request.windows(4).any(|window| window == b"\r\n\r\n") {
2323 break;
2324 }
2325 read = stream.read(&mut chunk).unwrap();
2326 }
2327 requests_for_thread
2328 .lock()
2329 .unwrap()
2330 .push(String::from_utf8_lossy(&request).into_owned());
2331 stream.write_all(&response).unwrap();
2332 }
2333 });
2334 (port, requests, handle)
2335 }
2336
2337 #[test]
2338 fn builder_configures_client() {
2339 let proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
2340 let client = Client::builder()
2341 .redirect_policy(RedirectPolicy::follow(4))
2342 .connection_policy(ConnectionPolicy::Reuse)
2343 .cache_mode(CacheMode::Memory)
2344 .proxy(proxy)
2345 .build();
2346
2347 let session = client.session();
2348 let request = Request::get("http://example.com").unwrap();
2349 assert_eq!(session.config.redirect_policy, RedirectPolicy::follow(4));
2350 assert_eq!(session.config.parser_strictness, ParserStrictness::Strict);
2351 assert_eq!(request.method(), Method::Get);
2352 }
2353
2354 #[test]
2355 fn cache_control_parser_recognizes_directives() {
2356 let mut request = Request::get("http://example.com").unwrap();
2357 request
2358 .add_header(
2359 "Cache-Control",
2360 "no-store, max-age=30, no-cache, max-stale=10, min-fresh=4, only-if-cached",
2361 )
2362 .unwrap();
2363 let cache_control = CacheControl::from_headers(request.headers());
2364 assert!(cache_control.no_store);
2365 assert!(cache_control.no_cache);
2366 assert_eq!(cache_control.max_age, Some(30));
2367 assert_eq!(cache_control.max_stale, Some(Some(10)));
2368 assert_eq!(cache_control.min_fresh, Some(4));
2369 assert!(cache_control.only_if_cached);
2370 }
2371
2372 #[test]
2373 fn default_client_builder_matches_release_defaults() {
2374 let client = ClientBuilder::default().build();
2375 let session = client.session();
2376 assert_eq!(session.config.connection_policy, ConnectionPolicy::Close);
2377 assert_eq!(session.config.cache_mode, CacheMode::Disabled);
2378 assert_eq!(session.config.redirect_policy, RedirectPolicy::none());
2379 }
2380
2381 #[test]
2382 fn request_date_helpers_accept_cacheable_times() {
2383 let mut request = Request::get("http://example.com").unwrap();
2384 request
2385 .if_modified_since(UNIX_EPOCH + Duration::from_secs(784_111_777))
2386 .unwrap();
2387 assert!(request.header("if-modified-since").is_some());
2388 }
2389
2390 #[test]
2391 fn cache_control_parser_recognizes_bare_max_stale() {
2392 let mut request = Request::get("http://example.com").unwrap();
2393 request.add_header("Cache-Control", "max-stale").unwrap();
2394 let cache_control = CacheControl::from_headers(request.headers());
2395 assert_eq!(cache_control.max_stale, Some(None));
2396 }
2397
2398 #[test]
2399 fn cache_control_parser_recognizes_revalidation_and_visibility_directives() {
2400 let headers = vec![crate::request::Header::unchecked(
2401 "Cache-Control",
2402 "must-revalidate, proxy-revalidate, public, private",
2403 )];
2404 let cache_control = CacheControl::from_headers(&headers);
2405 assert!(cache_control.must_revalidate);
2406 assert!(cache_control.proxy_revalidate);
2407 assert!(cache_control.public);
2408 assert!(cache_control.private);
2409 }
2410
2411 #[test]
2412 fn cache_control_parser_is_case_insensitive_and_handles_quoted_values() {
2413 let headers = vec![crate::request::Header::unchecked(
2414 "Cache-Control",
2415 "MAX-AGE=\"60\", MIN-FRESH=5, ONLY-IF-CACHED",
2416 )];
2417 let cache_control = CacheControl::from_headers(&headers);
2418 assert_eq!(cache_control.max_age, Some(60));
2419 assert_eq!(cache_control.min_fresh, Some(5));
2420 assert!(cache_control.only_if_cached);
2421 }
2422
2423 #[test]
2424 fn builder_sets_parser_strictness() {
2425 let client = Client::builder()
2426 .parser_strictness(ParserStrictness::Lenient)
2427 .build();
2428 let session = client.session();
2429 assert_eq!(session.config.parser_strictness, ParserStrictness::Lenient);
2430 }
2431
2432 #[test]
2433 fn proxy_config_validates_scheme_and_headers() {
2434 let error = ProxyConfig::new("https://example.com").unwrap_err();
2435 assert!(matches!(
2436 error,
2437 crate::NanoGetError::UnsupportedProxyScheme(_)
2438 ));
2439
2440 let mut proxy = ProxyConfig::new("http://example.com:8080").unwrap();
2441 proxy.add_header("X-Proxy", "yes").unwrap();
2442 assert_eq!(proxy.url().authority_form(), "example.com:8080");
2443 assert_eq!(proxy.headers().len(), 1);
2444 }
2445
2446 #[test]
2447 fn validate_request_conditionals_checks_if_range_requirements() {
2448 let request = Request::get("http://example.com").unwrap();
2449 assert!(validate_request_conditionals(&request).is_ok());
2450
2451 let mut invalid = Request::get("http://example.com").unwrap();
2452 invalid.if_range("\"v1\"").unwrap();
2453 assert!(matches!(
2454 validate_request_conditionals(&invalid),
2455 Err(crate::NanoGetError::InvalidConditionalRequest(_))
2456 ));
2457
2458 let mut weak = Request::get("http://example.com").unwrap();
2459 weak.range_bytes(Some(0), Some(1)).unwrap();
2460 weak.if_range("W/\"v1\"").unwrap();
2461 assert!(matches!(
2462 validate_request_conditionals(&weak),
2463 Err(crate::NanoGetError::InvalidConditionalRequest(_))
2464 ));
2465 }
2466
2467 #[test]
2468 fn parse_and_range_helpers_cover_edge_cases() {
2469 assert!(parse_single_range("bytes=0-2").is_some());
2470 assert!(parse_single_range("bytes=2-").is_some());
2471 assert!(parse_single_range("bytes=-4").is_some());
2472 assert!(parse_single_range("bytes=0-1,3-4").is_none());
2473 assert!(parse_content_range("bytes 0-1/4").is_some());
2474 assert!(parse_content_range("invalid").is_none());
2475 assert!(is_strong_etag("\"v1\""));
2476 assert!(!is_strong_etag("W/\"v1\""));
2477
2478 let suffix = ByteRange {
2479 start: None,
2480 end: Some(2),
2481 };
2482 assert_eq!(suffix.resolve(5), Some((3, 4)));
2483 let invalid = ByteRange {
2484 start: Some(10),
2485 end: Some(12),
2486 };
2487 assert_eq!(invalid.resolve(5), None);
2488 let open_ended = ByteRange {
2489 start: Some(2),
2490 end: None,
2491 };
2492 assert_eq!(open_ended.resolve(5), Some((2, 4)));
2493 let empty = ByteRange {
2494 start: Some(0),
2495 end: Some(0),
2496 };
2497 assert_eq!(empty.resolve(0), None);
2498 }
2499
2500 #[test]
2501 fn if_range_matching_handles_etags_and_dates() {
2502 assert!(if_range_matches_entry(None, None, None));
2503 assert!(if_range_matches_entry(Some("\"v1\""), Some("\"v1\""), None));
2504 assert!(!if_range_matches_entry(
2505 Some("\"v1\""),
2506 Some("\"v2\""),
2507 None
2508 ));
2509 assert!(if_range_matches_entry(
2510 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
2511 None,
2512 Some("Sun, 06 Nov 1994 08:49:37 GMT")
2513 ));
2514 assert!(!if_range_matches_entry(
2515 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
2516 None,
2517 Some("Sun, 07 Nov 1994 08:49:37 GMT")
2518 ));
2519 }
2520
2521 #[test]
2522 fn normalize_segments_merges_adjacent_and_overlapping_ranges() {
2523 let mut segments = Vec::new();
2524 normalize_segments(&mut segments);
2525 assert!(segments.is_empty());
2526
2527 let mut segments = vec![
2528 ByteSegment {
2529 start: 0,
2530 end: 2,
2531 bytes: b"abc".to_vec(),
2532 },
2533 ByteSegment {
2534 start: 2,
2535 end: 4,
2536 bytes: b"cde".to_vec(),
2537 },
2538 ByteSegment {
2539 start: 8,
2540 end: 9,
2541 bytes: b"xy".to_vec(),
2542 },
2543 ];
2544 normalize_segments(&mut segments);
2545 assert_eq!(segments.len(), 2);
2546 assert_eq!(segments[0].bytes, b"abcde");
2547 }
2548
2549 #[test]
2550 fn cache_entry_range_response_handles_satisfiable_and_unsatisfiable_ranges() {
2551 let request = Request::get("http://example.com").unwrap();
2552 let cached = response(
2553 200,
2554 vec![crate::Header::unchecked("Content-Length", "6")],
2555 b"abcdef",
2556 );
2557 let entry =
2558 CacheEntry::new(&request, &timed_response(cached), AuthContext::default()).unwrap();
2559 let partial = entry.range_response(
2560 ByteRange {
2561 start: Some(1),
2562 end: Some(3),
2563 },
2564 SystemTime::now(),
2565 );
2566 assert_eq!(partial.status_code, 206);
2567 assert_eq!(partial.body, b"bcd");
2568
2569 let unsatisfiable = entry.range_response(
2570 ByteRange {
2571 start: Some(9),
2572 end: Some(12),
2573 },
2574 SystemTime::now(),
2575 );
2576 assert_eq!(unsatisfiable.status_code, 416);
2577 }
2578
2579 #[test]
2580 fn partial_cache_entry_promotes_complete_when_segments_cover_the_representation() {
2581 let request = Request::get("http://example.com").unwrap();
2582 let first = response(
2583 206,
2584 vec![
2585 crate::Header::unchecked("Cache-Control", "max-age=60"),
2586 crate::Header::unchecked("ETag", "\"v1\""),
2587 crate::Header::unchecked("Content-Range", "bytes 0-2/6"),
2588 crate::Header::unchecked("Content-Length", "3"),
2589 ],
2590 b"abc",
2591 );
2592 let second = response(
2593 206,
2594 vec![
2595 crate::Header::unchecked("Cache-Control", "max-age=60"),
2596 crate::Header::unchecked("ETag", "\"v1\""),
2597 crate::Header::unchecked("Content-Range", "bytes 3-5/6"),
2598 crate::Header::unchecked("Content-Length", "3"),
2599 ],
2600 b"def",
2601 );
2602 let mut partial =
2603 PartialCacheEntry::new(&request, &timed_response(first), AuthContext::default())
2604 .unwrap();
2605 partial.merge_from(
2606 PartialCacheEntry::new(&request, &timed_response(second), AuthContext::default())
2607 .unwrap(),
2608 );
2609 let promoted = partial.promote_complete().unwrap();
2610 assert_eq!(promoted.response.status_code, 200);
2611 assert_eq!(promoted.response.body, b"abcdef");
2612 }
2613
2614 #[test]
2615 fn memory_cache_stores_206_segments_and_promotes_to_full_entry() {
2616 let mut cache = MemoryCache::default();
2617 let url = "http://example.com/path";
2618
2619 let mut first = Request::get(url).unwrap();
2620 first.range_bytes(Some(0), Some(2)).unwrap();
2621 let response_one = response(
2622 206,
2623 vec![
2624 crate::Header::unchecked("Cache-Control", "max-age=60"),
2625 crate::Header::unchecked("ETag", "\"v1\""),
2626 crate::Header::unchecked("Content-Range", "bytes 0-2/6"),
2627 crate::Header::unchecked("Content-Length", "3"),
2628 ],
2629 b"abc",
2630 );
2631 cache.store(
2632 &first,
2633 &timed_response(response_one),
2634 &AuthContext::default(),
2635 );
2636
2637 let mut second = Request::get(url).unwrap();
2638 second.range_bytes(Some(3), Some(5)).unwrap();
2639 let response_two = response(
2640 206,
2641 vec![
2642 crate::Header::unchecked("Cache-Control", "max-age=60"),
2643 crate::Header::unchecked("ETag", "\"v1\""),
2644 crate::Header::unchecked("Content-Range", "bytes 3-5/6"),
2645 crate::Header::unchecked("Content-Length", "3"),
2646 ],
2647 b"def",
2648 );
2649 cache.store(
2650 &second,
2651 &timed_response(response_two),
2652 &AuthContext::default(),
2653 );
2654
2655 let full = Request::get(url).unwrap();
2656 let lookup = cache.lookup(&full, SystemTime::now(), &AuthContext::default());
2657 assert!(matches!(
2658 lookup,
2659 Some(super::CacheLookup::Fresh(ref response)) if response.body == b"abcdef"
2660 ));
2661 }
2662
2663 #[test]
2664 fn prepared_connect_headers_respects_request_and_preemptive_values() {
2665 let mut proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
2666 proxy.add_header("Authorization", "drop-me").unwrap();
2667 let mut request = Request::get("https://example.com").unwrap();
2668 request.proxy_authorization("Basic cmVxOnByb3h5").unwrap();
2669
2670 let config = ClientConfig {
2671 preemptive_proxy_authorization: Some("Basic cHJlZW1wdGl2ZQ==".to_string()),
2672 ..ClientConfig::default()
2673 };
2674
2675 let headers = prepared_connect_headers(&request, &config, &proxy).unwrap();
2676 assert!(headers
2677 .iter()
2678 .any(|header| header.matches_name("proxy-authorization")));
2679 assert!(!headers
2680 .iter()
2681 .any(|header| header.matches_name("authorization")));
2682
2683 let mut proxy_with_auth = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
2684 proxy_with_auth
2685 .add_header("Proxy-Authorization", "Basic cHJveHk=")
2686 .unwrap();
2687 let headers = prepared_connect_headers(&request, &config, &proxy_with_auth).unwrap();
2688 assert_eq!(
2689 headers
2690 .iter()
2691 .filter(|header| header.matches_name("proxy-authorization"))
2692 .count(),
2693 1
2694 );
2695 assert!(headers
2696 .iter()
2697 .any(|header| header.value() == "Basic cHJveHk="));
2698 }
2699
2700 #[test]
2701 fn maybe_retry_auth_handler_abort_maps_to_authentication_rejected() {
2702 let handler: Arc<dyn AuthHandler + Send + Sync> = Arc::new(AbortAuthHandler);
2703 let request = Request::get("http://example.com").unwrap();
2704 let response = response(
2705 401,
2706 vec![crate::Header::unchecked(
2707 "WWW-Authenticate",
2708 "Basic realm=\"api\"",
2709 )],
2710 b"",
2711 );
2712 let mut seen = Vec::new();
2713 let error = super::maybe_retry_request_auth(
2714 Some(&handler),
2715 AuthTarget::Origin,
2716 &request,
2717 &response,
2718 &mut seen,
2719 )
2720 .unwrap_err();
2721 assert!(matches!(
2722 error,
2723 crate::NanoGetError::AuthenticationRejected(_)
2724 ));
2725 }
2726
2727 #[test]
2728 fn execute_pipelined_validates_requests_before_network_io() {
2729 let mut close_session = Client::builder().build().session();
2730 let empty = close_session.execute_pipelined(&[]).unwrap();
2731 assert!(empty.is_empty());
2732
2733 let request = Request::get("http://example.com").unwrap();
2734 let error = close_session.execute_pipelined(&[request]).unwrap_err();
2735 assert!(matches!(error, crate::NanoGetError::Pipeline(_)));
2736
2737 let mut invalid_conditional = Request::get("http://example.com").unwrap();
2738 invalid_conditional.if_range("\"v1\"").unwrap();
2739 let mut reuse_session = Client::builder()
2740 .connection_policy(ConnectionPolicy::Reuse)
2741 .build()
2742 .session();
2743 let error = reuse_session
2744 .execute_pipelined(&[invalid_conditional])
2745 .unwrap_err();
2746 assert!(matches!(
2747 error,
2748 crate::NanoGetError::InvalidConditionalRequest(_)
2749 ));
2750
2751 let mut reuse_session = Client::builder()
2752 .connection_policy(ConnectionPolicy::Reuse)
2753 .build()
2754 .session();
2755 let error = reuse_session
2756 .execute_pipelined(&[
2757 Request::get("http://example.com/one").unwrap(),
2758 Request::get("http://example.org/two").unwrap(),
2759 ])
2760 .unwrap_err();
2761 assert!(matches!(error, crate::NanoGetError::Pipeline(_)));
2762
2763 let mut reuse_session = Client::builder()
2764 .connection_policy(ConnectionPolicy::Reuse)
2765 .build()
2766 .session();
2767 let error = reuse_session
2768 .execute_pipelined(&[
2769 Request::get("https://example.com/one").unwrap(),
2770 Request::get("https://example.org/two").unwrap(),
2771 ])
2772 .unwrap_err();
2773 assert!(matches!(error, crate::NanoGetError::Pipeline(_)));
2774 }
2775
2776 #[test]
2777 fn execute_follow_redirects_without_location_returns_original_response() {
2778 let (port, _requests, handle) = spawn_recording_server(vec![
2779 b"HTTP/1.1 302 Found\r\nContent-Length: 0\r\n\r\n".to_vec(),
2780 ]);
2781 let mut session = Client::builder().build().session();
2782 let request = Request::get(format!("http://127.0.0.1:{port}/"))
2783 .unwrap()
2784 .with_redirect_policy(RedirectPolicy::follow(3));
2785 let response = session.execute(request).unwrap();
2786 assert_eq!(response.status_code, 302);
2787 handle.join().unwrap();
2788 }
2789
2790 #[test]
2791 fn session_execute_ref_clones_and_executes_requests() {
2792 let (port, _requests, handle) = spawn_recording_server(vec![
2793 b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
2794 ]);
2795 let mut session = Client::builder().build().session();
2796 let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
2797 let response = session.execute_ref(&request).unwrap();
2798 assert_eq!(response.body, b"ok");
2799 handle.join().unwrap();
2800 }
2801
2802 #[test]
2803 fn execute_pipelined_handles_connection_close_on_final_response() {
2804 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2805 let port = listener.local_addr().unwrap().port();
2806 let handle = thread::spawn(move || {
2807 let (mut stream, _) = listener.accept().unwrap();
2808 let mut request = Vec::new();
2809 let mut chunk = [0u8; 256];
2810 loop {
2811 let read = stream.read(&mut chunk).unwrap();
2812 assert!(read > 0, "client closed before sending two requests");
2813 request.extend_from_slice(&chunk[..read]);
2814 if request
2815 .windows(4)
2816 .filter(|window| *window == b"\r\n\r\n")
2817 .count()
2818 >= 2
2819 {
2820 break;
2821 }
2822 }
2823 if let Err(error) = stream.write_all(
2824 b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\naHTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 1\r\n\r\nb",
2825 ) {
2826 if error.kind() != std::io::ErrorKind::BrokenPipe {
2827 panic!("unexpected server write failure: {error}");
2828 }
2829 }
2830 });
2831
2832 let mut session = Client::builder()
2833 .connection_policy(ConnectionPolicy::Reuse)
2834 .build()
2835 .session();
2836 let responses = session
2837 .execute_pipelined(&[
2838 Request::get(format!("http://127.0.0.1:{port}/one")).unwrap(),
2839 Request::get(format!("http://127.0.0.1:{port}/two")).unwrap(),
2840 ])
2841 .unwrap();
2842 assert_eq!(responses.len(), 2);
2843 assert_eq!(responses[0].body, b"a");
2844 assert_eq!(responses[1].body, b"b");
2845 assert!(session.connection.is_none());
2846 handle.join().unwrap();
2847 }
2848
2849 #[test]
2850 fn execute_one_only_if_cached_without_memory_cache_returns_gateway_timeout() {
2851 let mut session = Client::builder().build().session();
2852 let mut request = Request::get("http://example.com").unwrap();
2853 request
2854 .add_header("Cache-Control", "only-if-cached")
2855 .unwrap();
2856 let response = session.execute_one(request).unwrap();
2857 assert_eq!(response.status_code, 504);
2858 }
2859
2860 #[test]
2861 fn execute_one_uses_range_cache_hits_and_if_range_mismatch_fallback() {
2862 let (port, requests, handle) = spawn_recording_server(vec![
2863 b"HTTP/1.1 200 OK\r\nCache-Control: max-age=60\r\nContent-Length: 6\r\n\r\nghijkl"
2864 .to_vec(),
2865 ]);
2866 let url = format!("http://127.0.0.1:{port}/range");
2867 let request = Request::get(&url).unwrap();
2868 let cached = response(
2869 200,
2870 vec![
2871 Header::unchecked("Cache-Control", "max-age=60"),
2872 Header::unchecked("ETag", "\"v1\""),
2873 Header::unchecked("Content-Length", "6"),
2874 ],
2875 b"abcdef",
2876 );
2877 let mut session = Client::builder()
2878 .cache_mode(CacheMode::Memory)
2879 .build()
2880 .session();
2881 session.cache.lock().unwrap().store(
2882 &request,
2883 &timed_response(cached),
2884 &AuthContext::default(),
2885 );
2886
2887 let mut range_hit = Request::get(&url).unwrap();
2888 range_hit.range_bytes(Some(1), Some(3)).unwrap();
2889 let hit = session.execute_one(range_hit).unwrap();
2890 assert_eq!(hit.status_code, 206);
2891 assert_eq!(hit.body, b"bcd");
2892
2893 let mut mismatch = Request::get(&url).unwrap();
2894 mismatch.range_bytes(Some(0), Some(2)).unwrap();
2895 mismatch.if_range("\"different\"").unwrap();
2896 let network = session.execute_one(mismatch).unwrap();
2897 assert_eq!(network.status_code, 200);
2898 assert_eq!(network.body, b"ghijkl");
2899 assert_eq!(requests.lock().unwrap().len(), 1);
2900 handle.join().unwrap();
2901 }
2902
2903 #[test]
2904 fn execute_stale_adds_if_modified_since_when_last_modified_exists() {
2905 let (port, requests, handle) = spawn_recording_server(vec![
2906 b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
2907 ]);
2908 let request = Request::get(format!("http://127.0.0.1:{port}/stale")).unwrap();
2909 let stale_response = response(
2910 200,
2911 vec![
2912 Header::unchecked("Cache-Control", "max-age=0"),
2913 Header::unchecked("Last-Modified", "Sun, 06 Nov 1994 08:49:37 GMT"),
2914 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
2915 Header::unchecked("Content-Length", "4"),
2916 ],
2917 b"body",
2918 );
2919 let stale = CacheEntry::new(
2920 &request,
2921 &timed_response(stale_response),
2922 AuthContext::default(),
2923 )
2924 .unwrap();
2925 let mut session = Client::builder()
2926 .cache_mode(CacheMode::Memory)
2927 .build()
2928 .session();
2929 let response = session.execute_stale(request.clone(), stale).unwrap();
2930 assert_eq!(response.status_code, 200);
2931 let captured = requests.lock().unwrap().join("\n");
2932 assert!(captured.to_ascii_lowercase().contains("if-modified-since:"));
2933 handle.join().unwrap();
2934 }
2935
2936 #[test]
2937 fn execute_stale_keeps_existing_user_conditionals() {
2938 let (port, requests, handle) = spawn_recording_server(vec![
2939 b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
2940 ]);
2941 let mut request = Request::get(format!("http://127.0.0.1:{port}/stale")).unwrap();
2942 request.if_none_match("\"client\"").unwrap();
2943 let stale_response = response(
2944 200,
2945 vec![
2946 Header::unchecked("Cache-Control", "max-age=0"),
2947 Header::unchecked("ETag", "\"server\""),
2948 Header::unchecked("Last-Modified", "Sun, 06 Nov 1994 08:49:37 GMT"),
2949 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
2950 Header::unchecked("Content-Length", "4"),
2951 ],
2952 b"body",
2953 );
2954 let stale = CacheEntry::new(
2955 &request,
2956 &timed_response(stale_response),
2957 AuthContext::default(),
2958 )
2959 .unwrap();
2960 let mut session = Client::builder()
2961 .cache_mode(CacheMode::Memory)
2962 .build()
2963 .session();
2964 let response = session.execute_stale(request, stale).unwrap();
2965 assert_eq!(response.status_code, 200);
2966 let captured = requests.lock().unwrap().join("\n").to_ascii_lowercase();
2967 assert!(captured.contains("if-none-match: \"client\""));
2968 handle.join().unwrap();
2969 }
2970
2971 #[test]
2972 fn send_request_retries_network_errors_but_not_protocol_errors() {
2973 let mut session = Client::builder()
2974 .connection_policy(ConnectionPolicy::Reuse)
2975 .build()
2976 .session();
2977 let request = Request::get("http://127.0.0.1:9").unwrap();
2978 let error = session.send_request(&request).unwrap_err();
2979 assert!(matches!(
2980 error,
2981 crate::NanoGetError::Connect(_) | crate::NanoGetError::Io(_)
2982 ));
2983
2984 let (port, _requests, handle) = spawn_recording_server(vec![b"BROKEN\r\n\r\n".to_vec()]);
2985 let mut session = Client::builder()
2986 .connection_policy(ConnectionPolicy::Reuse)
2987 .build()
2988 .session();
2989 let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
2990 let error = session.send_request(&request).unwrap_err();
2991 assert!(matches!(error, crate::NanoGetError::MalformedStatusLine(_)));
2992 handle.join().unwrap();
2993 }
2994
2995 struct AlwaysIoStream;
2996
2997 impl Read for AlwaysIoStream {
2998 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
2999 Err(std::io::Error::new(
3000 std::io::ErrorKind::Other,
3001 "forced read",
3002 ))
3003 }
3004 }
3005
3006 impl Write for AlwaysIoStream {
3007 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
3008 Err(std::io::Error::new(
3009 std::io::ErrorKind::BrokenPipe,
3010 "forced write",
3011 ))
3012 }
3013
3014 fn flush(&mut self) -> std::io::Result<()> {
3015 Ok(())
3016 }
3017 }
3018
3019 #[test]
3020 fn send_request_retry_branch_runs_for_live_connection_io_errors() {
3021 let mut stream = AlwaysIoStream;
3022 let mut buf = [0u8; 1];
3023 assert!(stream.read(&mut buf).is_err());
3024 assert!(stream.write(&buf).is_err());
3025 stream.flush().unwrap();
3026
3027 let mut session = Client::builder()
3028 .connection_policy(ConnectionPolicy::Reuse)
3029 .build()
3030 .session();
3031 let request = Request::get("http://127.0.0.1:9").unwrap();
3032 session.connection = Some(super::LiveConnection {
3033 key: super::connection_key(&session.config.proxy, request.url()),
3034 reader: std::io::BufReader::new(Box::new(AlwaysIoStream)),
3035 });
3036 let error = session.send_request(&request).unwrap_err();
3037 assert!(matches!(
3038 error,
3039 crate::NanoGetError::Connect(_) | crate::NanoGetError::Io(_)
3040 ));
3041 }
3042
3043 #[test]
3044 fn execute_pipelined_propagates_response_parse_errors() {
3045 let (port, _requests, handle) = spawn_recording_server(vec![b"NOT-HTTP\r\n\r\n".to_vec()]);
3046 let mut session = Client::builder()
3047 .connection_policy(ConnectionPolicy::Reuse)
3048 .build()
3049 .session();
3050 let request = Request::get(format!("http://127.0.0.1:{port}/bad")).unwrap();
3051 let error = session.execute_pipelined(&[request]).unwrap_err();
3052 assert!(matches!(error, crate::NanoGetError::MalformedStatusLine(_)));
3053 handle.join().unwrap();
3054 }
3055
3056 #[test]
3057 fn pipeline_retry_classifier_only_retries_close_shaped_errors() {
3058 assert!(pipeline_retryable_parse_error(&crate::NanoGetError::Io(
3059 std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset"),
3060 )));
3061 assert!(pipeline_retryable_parse_error(
3062 &crate::NanoGetError::IncompleteMessage("unexpected EOF".to_string())
3063 ));
3064 assert!(!pipeline_retryable_parse_error(
3065 &crate::NanoGetError::MalformedStatusLine(
3066 "failed to read status line: unexpected EOF".to_string(),
3067 )
3068 ));
3069 assert!(!pipeline_retryable_parse_error(
3070 &crate::NanoGetError::MalformedHeader(
3071 "failed to read header line: unexpected EOF".to_string(),
3072 )
3073 ));
3074 assert!(!pipeline_retryable_parse_error(
3075 &crate::NanoGetError::MalformedStatusLine("HTTP/BROKEN".to_string())
3076 ));
3077 assert!(!pipeline_retryable_parse_error(
3078 &crate::NanoGetError::InvalidChunk("zz".to_string())
3079 ));
3080 }
3081
3082 #[test]
3083 fn reconnect_retry_classifier_includes_connect_and_tls_errors() {
3084 assert!(retryable_reconnect_error(&crate::NanoGetError::Connect(
3085 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"),
3086 )));
3087 assert!(retryable_reconnect_error(&crate::NanoGetError::Tls(
3088 "handshake failed".to_string(),
3089 )));
3090 assert!(!retryable_reconnect_error(
3091 &crate::NanoGetError::MalformedStatusLine(
3092 "failed to read status line: unexpected EOF".to_string(),
3093 )
3094 ));
3095 assert!(!retryable_reconnect_error(
3096 &crate::NanoGetError::MalformedStatusLine("BROKEN".to_string())
3097 ));
3098 }
3099
3100 #[test]
3101 fn send_ephemeral_propagates_prepared_request_failures() {
3102 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3103 let port = listener.local_addr().unwrap().port();
3104 let handle = thread::spawn(move || {
3105 let _ = listener.accept().unwrap();
3106 });
3107
3108 let proxy = super::ProxyConfig {
3109 url: Url::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(),
3110 headers: vec![Header::unchecked("X-Bad", "line\nbreak")],
3111 };
3112 let session = Client::builder().proxy(proxy).build().session();
3113 let request = Request::get("http://example.com").unwrap();
3114 let error = session.send_ephemeral(&request).unwrap_err();
3115 assert!(matches!(error, crate::NanoGetError::InvalidHeaderValue(_)));
3116 handle.join().unwrap();
3117 }
3118
3119 #[test]
3120 fn send_helpers_cover_ephemeral_and_live_connection_paths() {
3121 let (port, _requests, handle) = spawn_recording_server(vec![
3122 b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
3123 ]);
3124 let session = Client::builder().build().session();
3125 let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
3126 let timed = session.send_ephemeral(&request).unwrap();
3127 assert_eq!(timed.response.body, b"ok");
3128 handle.join().unwrap();
3129
3130 let mut session = Client::builder()
3131 .connection_policy(ConnectionPolicy::Reuse)
3132 .build()
3133 .session();
3134 let error = session.send_on_live_connection(&request).unwrap_err();
3135 assert!(matches!(error, crate::NanoGetError::Io(_)));
3136
3137 let (port, _requests, handle) = spawn_recording_server(vec![
3138 b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nok".to_vec(),
3139 ]);
3140 let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
3141 session.ensure_connection(&request).unwrap();
3142 let timed = session.send_on_live_connection(&request).unwrap();
3143 assert_eq!(timed.response.status_code, 200);
3144 assert!(session.connection.is_none());
3145 handle.join().unwrap();
3146 }
3147
3148 #[test]
3149 fn range_lookup_covers_stale_and_partial_only_if_cached_paths() {
3150 let mut cache = MemoryCache::default();
3151 let request = Request::get("http://example.com/stale-range").unwrap();
3152 let stale_response = response(
3153 200,
3154 vec![
3155 Header::unchecked("Cache-Control", "max-age=0"),
3156 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3157 Header::unchecked("ETag", "\"stale\""),
3158 Header::unchecked("Content-Length", "6"),
3159 ],
3160 b"abcdef",
3161 );
3162 cache.store(
3163 &request,
3164 &timed_response(stale_response),
3165 &AuthContext::default(),
3166 );
3167 let mut stale_range = Request::get("http://example.com/stale-range").unwrap();
3168 stale_range.range_bytes(Some(0), Some(2)).unwrap();
3169 assert!(cache
3170 .lookup_range(&stale_range, SystemTime::now(), &AuthContext::default())
3171 .is_none());
3172
3173 let mut partial_request = Request::get("http://example.com/partial-only").unwrap();
3174 partial_request.range_bytes(Some(0), Some(2)).unwrap();
3175 let partial_response = response(
3176 206,
3177 vec![
3178 Header::unchecked("Cache-Control", "max-age=60"),
3179 Header::unchecked("ETag", "\"p2\""),
3180 Header::unchecked("Content-Range", "bytes 0-2/6"),
3181 Header::unchecked("Content-Length", "3"),
3182 ],
3183 b"abc",
3184 );
3185 cache.store(
3186 &partial_request,
3187 &timed_response(partial_response),
3188 &AuthContext::default(),
3189 );
3190
3191 let mut mismatch = Request::get("http://example.com/partial-only").unwrap();
3192 mismatch.range_bytes(Some(0), Some(1)).unwrap();
3193 mismatch.if_range("\"different\"").unwrap();
3194 mismatch
3195 .add_header("Cache-Control", "only-if-cached")
3196 .unwrap();
3197 assert!(matches!(
3198 cache.lookup_range(&mismatch, SystemTime::now(), &AuthContext::default()),
3199 Some(super::RangeCacheLookup::UnsatisfiedOnlyIfCached)
3200 ));
3201
3202 let mut no_segment = Request::get("http://example.com/partial-only").unwrap();
3203 no_segment.range_bytes(Some(4), Some(5)).unwrap();
3204 assert!(cache
3205 .lookup_range(&no_segment, SystemTime::now(), &AuthContext::default())
3206 .is_none());
3207 }
3208
3209 #[test]
3210 fn prepared_request_applies_proxy_headers_for_http_proxy_targets() {
3211 let mut proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
3212 proxy.add_header("X-Proxy", "yes").unwrap();
3213 let config = ClientConfig {
3214 proxy: Some(proxy),
3215 ..ClientConfig::default()
3216 };
3217 let request = Request::get("http://example.com").unwrap();
3218 let prepared =
3219 super::prepared_request(&request, &config, super::SendTarget::HttpProxy).unwrap();
3220 assert_eq!(prepared.header("x-proxy"), Some("yes"));
3221
3222 let https_request = Request::get("https://example.com").unwrap();
3223 let tunnel =
3224 super::prepared_request(&https_request, &config, super::SendTarget::Tunnel).unwrap();
3225 assert_eq!(tunnel.header("x-proxy"), None);
3226 }
3227
3228 #[test]
3229 fn open_connection_rejects_unknown_url_schemes() {
3230 let mut url = Url::parse("http://example.com").unwrap();
3231 url.scheme = "ws".to_string();
3232 let request = Request::new(Method::Get, url).unwrap();
3233 let error = super::open_connection(&ClientConfig::default(), &request)
3234 .err()
3235 .expect("unexpected successful connection");
3236 assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
3237 }
3238
3239 #[test]
3240 fn proxy_send_target_and_connection_keys_cover_tunnel_variants() {
3241 let proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
3242 let config = ClientConfig {
3243 proxy: Some(proxy),
3244 ..ClientConfig::default()
3245 };
3246
3247 let http_request = Request::get("http://example.com").unwrap();
3248 assert!(matches!(
3249 super::SendTarget::for_request(&config, &http_request),
3250 super::SendTarget::HttpProxy
3251 ));
3252
3253 let https_request = Request::get("https://example.com").unwrap();
3254 assert!(matches!(
3255 super::SendTarget::for_request(&config, &https_request),
3256 super::SendTarget::Tunnel
3257 ));
3258
3259 let key = super::connection_key(&config.proxy, https_request.url());
3260 assert!(matches!(key, super::ConnectionKey::HttpsTunnel { .. }));
3261 }
3262
3263 #[test]
3264 fn open_connection_with_proxy_https_path_reaches_tunnel_branch() {
3265 let proxy = ProxyConfig::new("http://127.0.0.1:9").unwrap();
3266 let config = ClientConfig {
3267 proxy: Some(proxy),
3268 ..ClientConfig::default()
3269 };
3270 let request = Request::get("https://example.com").unwrap();
3271 let error = super::open_connection(&config, &request)
3272 .err()
3273 .expect("unexpected successful connection");
3274 assert!(matches!(
3275 error,
3276 crate::NanoGetError::Connect(_)
3277 | crate::NanoGetError::Io(_)
3278 | crate::NanoGetError::HttpsFeatureRequired
3279 | crate::NanoGetError::Tls(_)
3280 | crate::NanoGetError::ProxyConnectFailed(_, _)
3281 ));
3282 }
3283
3284 #[test]
3285 fn memory_cache_lookup_and_range_paths_cover_only_if_cached_and_if_range_logic() {
3286 let mut cache = MemoryCache::default();
3287 let mut only_if_cached = Request::get("http://example.com").unwrap();
3288 only_if_cached
3289 .add_header("Cache-Control", "only-if-cached")
3290 .unwrap();
3291 assert!(matches!(
3292 cache.lookup(&only_if_cached, SystemTime::now(), &AuthContext::default()),
3293 Some(super::CacheLookup::UnsatisfiedOnlyIfCached)
3294 ));
3295
3296 let mut seeded_request = Request::get("http://example.com").unwrap();
3297 seeded_request.add_header("Accept", "text/plain").unwrap();
3298 let seeded_response = response(
3299 200,
3300 vec![
3301 Header::unchecked("Cache-Control", "max-age=60"),
3302 Header::unchecked("Vary", "Accept"),
3303 Header::unchecked("ETag", "\"v1\""),
3304 ],
3305 b"abcdef",
3306 );
3307 cache.store(
3308 &seeded_request,
3309 &timed_response(seeded_response),
3310 &AuthContext::default(),
3311 );
3312
3313 let mut mismatched = Request::get("http://example.com").unwrap();
3314 mismatched.add_header("Accept", "application/json").unwrap();
3315 mismatched
3316 .add_header("Cache-Control", "only-if-cached")
3317 .unwrap();
3318 assert!(matches!(
3319 cache.lookup(&mismatched, SystemTime::now(), &AuthContext::default()),
3320 Some(super::CacheLookup::UnsatisfiedOnlyIfCached)
3321 ));
3322
3323 let mut range_hit = Request::get("http://example.com").unwrap();
3324 range_hit.add_header("Accept", "text/plain").unwrap();
3325 range_hit.range_bytes(Some(1), Some(3)).unwrap();
3326 let lookup = cache.lookup_range(&range_hit, SystemTime::now(), &AuthContext::default());
3327 assert!(matches!(lookup, Some(super::RangeCacheLookup::Hit(_))));
3328
3329 let mut range_mismatch = Request::get("http://example.com").unwrap();
3330 range_mismatch.add_header("Accept", "text/plain").unwrap();
3331 range_mismatch.range_bytes(Some(0), Some(1)).unwrap();
3332 range_mismatch.if_range("\"v2\"").unwrap();
3333 assert!(matches!(
3334 cache.lookup_range(&range_mismatch, SystemTime::now(), &AuthContext::default()),
3335 Some(super::RangeCacheLookup::IfRangeMismatch)
3336 ));
3337
3338 let mut range_mismatch_only_cached = Request::get("http://example.com").unwrap();
3339 range_mismatch_only_cached
3340 .add_header("Accept", "text/plain")
3341 .unwrap();
3342 range_mismatch_only_cached
3343 .range_bytes(Some(0), Some(1))
3344 .unwrap();
3345 range_mismatch_only_cached.if_range("\"v2\"").unwrap();
3346 range_mismatch_only_cached
3347 .add_header("Cache-Control", "only-if-cached")
3348 .unwrap();
3349 assert!(matches!(
3350 cache.lookup_range(
3351 &range_mismatch_only_cached,
3352 SystemTime::now(),
3353 &AuthContext::default()
3354 ),
3355 Some(super::RangeCacheLookup::UnsatisfiedOnlyIfCached)
3356 ));
3357
3358 let mut only_if_cached_range = Request::get("http://example.net").unwrap();
3359 only_if_cached_range.range_bytes(Some(0), Some(1)).unwrap();
3360 only_if_cached_range
3361 .add_header("Cache-Control", "only-if-cached")
3362 .unwrap();
3363 assert!(matches!(
3364 cache.lookup_range(
3365 &only_if_cached_range,
3366 SystemTime::now(),
3367 &AuthContext::default()
3368 ),
3369 Some(super::RangeCacheLookup::UnsatisfiedOnlyIfCached)
3370 ));
3371 }
3372
3373 #[test]
3374 fn partial_cache_lookup_and_promotion_cover_additional_branches() {
3375 let mut cache = MemoryCache::default();
3376 let mut partial_request = Request::get("http://example.com/partial").unwrap();
3377 partial_request.range_bytes(Some(0), Some(2)).unwrap();
3378 let partial_response = response(
3379 206,
3380 vec![
3381 Header::unchecked("Cache-Control", "max-age=60"),
3382 Header::unchecked("ETag", "\"part\""),
3383 Header::unchecked("Content-Range", "bytes 0-2/6"),
3384 Header::unchecked("Content-Length", "3"),
3385 ],
3386 b"abc",
3387 );
3388 cache.store(
3389 &partial_request,
3390 &timed_response(partial_response),
3391 &AuthContext::default(),
3392 );
3393
3394 let mut hit_request = Request::get("http://example.com/partial").unwrap();
3395 hit_request.range_bytes(Some(1), Some(2)).unwrap();
3396 let lookup = cache.lookup_range(&hit_request, SystemTime::now(), &AuthContext::default());
3397 assert!(matches!(lookup, Some(super::RangeCacheLookup::Hit(_))));
3398
3399 let mut miss_request = Request::get("http://example.com/partial").unwrap();
3400 miss_request.range_bytes(Some(4), Some(5)).unwrap();
3401 assert!(cache
3402 .lookup_range(&miss_request, SystemTime::now(), &AuthContext::default())
3403 .is_none());
3404
3405 let mut mismatch = Request::get("http://example.com/partial").unwrap();
3406 mismatch.range_bytes(Some(0), Some(1)).unwrap();
3407 mismatch.if_range("\"different\"").unwrap();
3408 assert!(matches!(
3409 cache.lookup_range(&mismatch, SystemTime::now(), &AuthContext::default()),
3410 Some(super::RangeCacheLookup::IfRangeMismatch)
3411 ));
3412
3413 let invalid_partial = response(
3414 206,
3415 vec![
3416 Header::unchecked("Cache-Control", "max-age=60"),
3417 Header::unchecked("ETag", "\"part\""),
3418 ],
3419 b"abc",
3420 );
3421 cache.store(
3422 &partial_request,
3423 &timed_response(invalid_partial),
3424 &AuthContext::default(),
3425 );
3426 }
3427
3428 #[test]
3429 fn cache_entry_and_partial_entry_helpers_cover_unhit_branches() {
3430 let request = Request::get("http://example.com/cache").unwrap();
3431 let base_response = response(
3432 200,
3433 vec![
3434 Header::unchecked("Cache-Control", "max-age=2"),
3435 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3436 Header::unchecked("ETag", "\"v1\""),
3437 Header::unchecked("Content-Length", "6"),
3438 ],
3439 b"abcdef",
3440 );
3441 let mut entry = CacheEntry::new(
3442 &request,
3443 &timed_response(base_response),
3444 AuthContext::default(),
3445 )
3446 .unwrap();
3447 entry.cache_control.no_cache = true;
3448 assert!(!entry.is_fresh(SystemTime::now()));
3449 entry.cache_control.no_cache = false;
3450 entry.date_header = Some(entry.response_time + Duration::from_secs(1));
3451 let now = entry.response_time + Duration::from_secs(3);
3452 let _ = entry.current_age(now);
3453 let _ = entry.remaining_freshness(now);
3454 let _ = entry.staleness(now);
3455 assert!(!entry.satisfies_request(
3456 &CacheControl {
3457 max_age: Some(1),
3458 ..CacheControl::default()
3459 },
3460 now
3461 ));
3462 assert!(!entry.satisfies_request(
3463 &CacheControl {
3464 min_fresh: Some(99),
3465 ..CacheControl::default()
3466 },
3467 now
3468 ));
3469 entry.freshness_lifetime = Duration::from_secs(0);
3470 assert!(entry.satisfies_request(
3471 &CacheControl {
3472 max_stale: Some(Some(10)),
3473 ..CacheControl::default()
3474 },
3475 now
3476 ));
3477
3478 let partial_response = response(
3479 206,
3480 vec![
3481 Header::unchecked("Cache-Control", "max-age=1"),
3482 Header::unchecked("ETag", "\"p1\""),
3483 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3484 Header::unchecked("Content-Range", "bytes 0-2/3"),
3485 Header::unchecked("Content-Length", "3"),
3486 ],
3487 b"abc",
3488 );
3489 let mut partial = PartialCacheEntry::new(
3490 &request,
3491 &timed_response(partial_response),
3492 AuthContext::default(),
3493 )
3494 .unwrap();
3495 partial.date_header = Some(partial.response_time + Duration::from_secs(1));
3496 let now = partial.response_time + Duration::from_secs(2);
3497 let _ = partial.current_age(now);
3498 let _ = partial.remaining_freshness(now);
3499 let _ = partial.staleness(now);
3500 partial.cache_control.no_cache = true;
3501 assert!(!partial.is_fresh(now));
3502 assert!(!partial.satisfies_request(&CacheControl::default(), now));
3503 partial.cache_control.no_cache = false;
3504 assert!(!partial.satisfies_request(
3505 &CacheControl {
3506 max_age: Some(0),
3507 ..CacheControl::default()
3508 },
3509 now
3510 ));
3511 assert!(!partial.satisfies_request(
3512 &CacheControl {
3513 min_fresh: Some(100),
3514 ..CacheControl::default()
3515 },
3516 now
3517 ));
3518 partial.freshness_lifetime = Duration::from_secs(0);
3519 assert!(partial.satisfies_request(
3520 &CacheControl {
3521 max_stale: Some(Some(10)),
3522 ..CacheControl::default()
3523 },
3524 now
3525 ));
3526 let range = partial
3527 .range_response(
3528 ByteRange {
3529 start: Some(1),
3530 end: Some(2),
3531 },
3532 now,
3533 )
3534 .unwrap();
3535 assert_eq!(range.status_code, 206);
3536 assert_eq!(range.body, b"bc");
3537
3538 partial.segments.push(ByteSegment {
3539 start: 10,
3540 end: 11,
3541 bytes: b"zz".to_vec(),
3542 });
3543 assert!(partial.promote_complete().is_none());
3544 }
3545
3546 #[test]
3547 fn cache_satisfies_request_specific_branches_are_exercised() {
3548 let request = Request::get("http://example.com/age").unwrap();
3549 let base = response(
3550 200,
3551 vec![
3552 Header::unchecked("Cache-Control", "max-age=100"),
3553 Header::unchecked("Content-Length", "1"),
3554 ],
3555 b"x",
3556 );
3557 let mut entry =
3558 CacheEntry::new(&request, &timed_response(base), AuthContext::default()).unwrap();
3559 entry.request_time = UNIX_EPOCH;
3560 entry.response_time = UNIX_EPOCH + Duration::from_secs(10);
3561 entry.date_header = Some(UNIX_EPOCH);
3562 let now = UNIX_EPOCH + Duration::from_secs(20);
3563 assert!(!entry.satisfies_request(
3564 &CacheControl {
3565 max_age: Some(1),
3566 ..CacheControl::default()
3567 },
3568 now
3569 ));
3570 assert!(!entry.satisfies_request(
3571 &CacheControl {
3572 min_fresh: Some(200),
3573 ..CacheControl::default()
3574 },
3575 now
3576 ));
3577
3578 let partial_response = response(
3579 206,
3580 vec![
3581 Header::unchecked("Cache-Control", "max-age=100"),
3582 Header::unchecked("ETag", "\"p-age\""),
3583 Header::unchecked("Content-Range", "bytes 0-0/1"),
3584 Header::unchecked("Content-Length", "1"),
3585 ],
3586 b"x",
3587 );
3588 let mut partial = PartialCacheEntry::new(
3589 &request,
3590 &timed_response(partial_response),
3591 AuthContext::default(),
3592 )
3593 .unwrap();
3594 partial.request_time = UNIX_EPOCH;
3595 partial.response_time = UNIX_EPOCH + Duration::from_secs(10);
3596 partial.date_header = Some(UNIX_EPOCH);
3597 let now = UNIX_EPOCH + Duration::from_secs(20);
3598 assert!(!partial.satisfies_request(
3599 &CacheControl {
3600 max_age: Some(1),
3601 ..CacheControl::default()
3602 },
3603 now
3604 ));
3605 assert!(!partial.satisfies_request(
3606 &CacheControl {
3607 min_fresh: Some(200),
3608 ..CacheControl::default()
3609 },
3610 now
3611 ));
3612 partial.freshness_lifetime = Duration::from_secs(0);
3613 partial.cache_control.must_revalidate = true;
3614 assert!(!partial.satisfies_request(
3615 &CacheControl {
3616 max_stale: Some(None),
3617 ..CacheControl::default()
3618 },
3619 now
3620 ));
3621 partial.cache_control.must_revalidate = false;
3622 assert!(partial.satisfies_request(
3623 &CacheControl {
3624 max_stale: Some(None),
3625 ..CacheControl::default()
3626 },
3627 now
3628 ));
3629 assert!(!partial.satisfies_request(&CacheControl::default(), now));
3630 }
3631
3632 #[test]
3633 fn cache_construction_rejections_and_misc_helpers_cover_branches() {
3634 let request = Request::get("http://example.com/path").unwrap();
3635 let now = SystemTime::now();
3636
3637 let no_store = response(
3638 200,
3639 vec![Header::unchecked("Cache-Control", "no-store")],
3640 b"x",
3641 );
3642 assert!(
3643 CacheEntry::new(&request, &timed_response(no_store), AuthContext::default()).is_none()
3644 );
3645
3646 let cacheable = response(
3647 200,
3648 vec![Header::unchecked("Cache-Control", "max-age=60")],
3649 b"x",
3650 );
3651 assert!(CacheEntry::new(
3652 &request,
3653 &timed_response(cacheable.clone()),
3654 AuthContext {
3655 origin: None,
3656 proxy: Some("p".to_string())
3657 }
3658 )
3659 .is_none());
3660
3661 let no_store_partial = response(
3662 206,
3663 vec![
3664 Header::unchecked("Cache-Control", "no-store"),
3665 Header::unchecked("ETag", "\"v1\""),
3666 Header::unchecked("Content-Range", "bytes 0-0/1"),
3667 Header::unchecked("Content-Length", "1"),
3668 ],
3669 b"x",
3670 );
3671 assert!(PartialCacheEntry::new(
3672 &request,
3673 &timed_response(no_store_partial),
3674 AuthContext::default()
3675 )
3676 .is_none());
3677
3678 let proxy_partial = response(
3679 206,
3680 vec![
3681 Header::unchecked("Cache-Control", "max-age=60"),
3682 Header::unchecked("ETag", "\"v1\""),
3683 Header::unchecked("Content-Range", "bytes 0-0/1"),
3684 Header::unchecked("Content-Length", "1"),
3685 ],
3686 b"x",
3687 );
3688 assert!(PartialCacheEntry::new(
3689 &request,
3690 &timed_response(proxy_partial.clone()),
3691 AuthContext {
3692 origin: None,
3693 proxy: Some("p".to_string())
3694 }
3695 )
3696 .is_none());
3697 assert!(PartialCacheEntry::new(
3698 &request,
3699 &timed_response(proxy_partial.clone()),
3700 AuthContext {
3701 origin: Some("secret".to_string()),
3702 proxy: None
3703 }
3704 )
3705 .is_none());
3706
3707 let invalid_range = response(
3708 206,
3709 vec![
3710 Header::unchecked("Cache-Control", "max-age=60"),
3711 Header::unchecked("ETag", "\"v1\""),
3712 Header::unchecked("Content-Range", "bytes 0-4/10"),
3713 Header::unchecked("Content-Length", "5"),
3714 ],
3715 b"abc",
3716 );
3717 assert!(PartialCacheEntry::new(
3718 &request,
3719 &timed_response(invalid_range),
3720 AuthContext::default()
3721 )
3722 .is_none());
3723
3724 let weak_etag = response(
3725 206,
3726 vec![
3727 Header::unchecked("Cache-Control", "max-age=60"),
3728 Header::unchecked("ETag", "W/\"v1\""),
3729 Header::unchecked("Content-Range", "bytes 0-2/3"),
3730 Header::unchecked("Content-Length", "3"),
3731 ],
3732 b"abc",
3733 );
3734 assert!(PartialCacheEntry::new(
3735 &request,
3736 &timed_response(weak_etag),
3737 AuthContext::default()
3738 )
3739 .is_none());
3740
3741 let mut cache = MemoryCache::default();
3742 let mut get = Request::get("http://example.com/upsert").unwrap();
3743 get.add_header("Accept", "text/plain").unwrap();
3744 let first = response(
3745 200,
3746 vec![
3747 Header::unchecked("Cache-Control", "max-age=60"),
3748 Header::unchecked("Vary", "Accept"),
3749 Header::unchecked("Content-Length", "1"),
3750 ],
3751 b"a",
3752 );
3753 let second = response(
3754 200,
3755 vec![
3756 Header::unchecked("Cache-Control", "max-age=60"),
3757 Header::unchecked("Vary", "Accept"),
3758 Header::unchecked("Content-Length", "1"),
3759 ],
3760 b"b",
3761 );
3762 let first_entry =
3763 CacheEntry::new(&get, &timed_response(first), AuthContext::default()).unwrap();
3764 let second_entry =
3765 CacheEntry::new(&get, &timed_response(second), AuthContext::default()).unwrap();
3766 cache.upsert_complete_entry(get.url().cache_key(), first_entry);
3767 cache.upsert_complete_entry(get.url().cache_key(), second_entry);
3768 assert_eq!(cache.entries[&get.url().cache_key()].len(), 1);
3769
3770 let direct = CacheControl::from_headers(&[Header::unchecked("Cache-Control", "x-test=1")]);
3771 assert!(!direct.no_store);
3772 let head_response = super::response_for_method(
3773 cache.entries[&get.url().cache_key()][0].response.clone(),
3774 Method::Head,
3775 );
3776 assert!(head_response.body.is_empty());
3777
3778 let vary_star_response = response(200, vec![Header::unchecked("Vary", "*")], b"");
3779 assert!(super::extract_vary_headers(&request, &vary_star_response).is_none());
3780
3781 assert!(super::is_cacheable_status(203));
3782 assert!(!super::is_cacheable_status(418));
3783
3784 let expires = response(
3785 200,
3786 vec![
3787 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3788 Header::unchecked("Expires", "Sun, 06 Nov 1994 08:50:37 GMT"),
3789 ],
3790 b"",
3791 );
3792 assert_eq!(
3793 super::compute_freshness_lifetime(&expires, now),
3794 Duration::from_secs(60)
3795 );
3796
3797 let heuristic = response(
3798 200,
3799 vec![
3800 Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3801 Header::unchecked("Last-Modified", "Sun, 06 Nov 1994 08:39:37 GMT"),
3802 ],
3803 b"",
3804 );
3805 assert_eq!(
3806 super::compute_freshness_lifetime(&heuristic, now),
3807 Duration::from_secs(60)
3808 );
3809
3810 let expires_without_date = response(
3811 200,
3812 vec![Header::unchecked(
3813 "Expires",
3814 "Sun, 06 Nov 1994 08:50:37 GMT",
3815 )],
3816 b"",
3817 );
3818 assert_eq!(
3819 super::compute_freshness_lifetime(
3820 &expires_without_date,
3821 UNIX_EPOCH + Duration::from_secs(784_111_700)
3822 ),
3823 Duration::from_secs(137)
3824 );
3825
3826 let existing = CacheEntry::new(
3827 &request,
3828 &timed_response(response(
3829 200,
3830 vec![
3831 Header::unchecked("Cache-Control", "max-age=60"),
3832 Header::unchecked("ETag", "\"v1\""),
3833 Header::unchecked("Content-Length", "3"),
3834 ],
3835 b"abc",
3836 )),
3837 AuthContext::default(),
3838 )
3839 .unwrap();
3840 let different_etag = CacheEntry::new(
3841 &request,
3842 &timed_response(response(
3843 200,
3844 vec![
3845 Header::unchecked("Cache-Control", "max-age=60"),
3846 Header::unchecked("ETag", "\"v2\""),
3847 Header::unchecked("Content-Length", "3"),
3848 ],
3849 b"abc",
3850 )),
3851 AuthContext::default(),
3852 )
3853 .unwrap();
3854 assert!(!super::head_update_is_compatible(
3855 &existing,
3856 &different_etag
3857 ));
3858 let different_length = CacheEntry::new(
3859 &request,
3860 &timed_response(response(
3861 200,
3862 vec![
3863 Header::unchecked("Cache-Control", "max-age=60"),
3864 Header::unchecked("ETag", "\"v1\""),
3865 Header::unchecked("Content-Length", "4"),
3866 ],
3867 b"abcd",
3868 )),
3869 AuthContext::default(),
3870 )
3871 .unwrap();
3872 assert!(!super::head_update_is_compatible(
3873 &existing,
3874 &different_length
3875 ));
3876 let same_length = CacheEntry::new(
3877 &request,
3878 &timed_response(response(
3879 200,
3880 vec![
3881 Header::unchecked("Cache-Control", "max-age=60"),
3882 Header::unchecked("ETag", "\"v1\""),
3883 Header::unchecked("Content-Length", "3"),
3884 ],
3885 b"abc",
3886 )),
3887 AuthContext::default(),
3888 )
3889 .unwrap();
3890 assert!(super::head_update_is_compatible(&existing, &same_length));
3891
3892 assert!(!super::if_range_matches_entry(
3893 Some("invalid-date"),
3894 None,
3895 Some("Sun, 06 Nov 1994 08:49:37 GMT")
3896 ));
3897 assert!(!super::if_range_matches_entry(
3898 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
3899 None,
3900 None
3901 ));
3902 }
3903
3904 #[test]
3905 fn authentication_retry_helper_covers_remaining_branches() {
3906 let request = Request::get("http://example.com").unwrap();
3907 let mut seen = Vec::new();
3908 let empty_challenge_response =
3909 response(401, vec![Header::unchecked("WWW-Authenticate", "")], b"");
3910 assert!(super::maybe_retry_request_auth(
3911 None,
3912 AuthTarget::Origin,
3913 &request,
3914 &empty_challenge_response,
3915 &mut seen
3916 )
3917 .unwrap()
3918 .is_none());
3919
3920 let challenge_response = response(
3921 401,
3922 vec![Header::unchecked("WWW-Authenticate", "Basic realm=\"api\"")],
3923 b"",
3924 );
3925 assert!(super::maybe_retry_request_auth(
3926 None,
3927 AuthTarget::Origin,
3928 &request,
3929 &challenge_response,
3930 &mut seen
3931 )
3932 .unwrap()
3933 .is_none());
3934 assert!(super::maybe_retry_request_auth(
3935 None,
3936 AuthTarget::Origin,
3937 &request,
3938 &challenge_response,
3939 &mut seen
3940 )
3941 .unwrap()
3942 .is_none());
3943
3944 let no_match_handler: Arc<dyn AuthHandler + Send + Sync> = Arc::new(NoMatchAuthHandler);
3945 assert!(super::maybe_retry_request_auth(
3946 Some(&no_match_handler),
3947 AuthTarget::Origin,
3948 &request,
3949 &challenge_response,
3950 &mut Vec::new()
3951 )
3952 .unwrap()
3953 .is_none());
3954
3955 let use_headers_handler: Arc<dyn AuthHandler + Send + Sync> =
3956 Arc::new(UseHeadersAuthHandler {
3957 header_name: "Authorization",
3958 header_value: "Basic dXNlcjpwYXNz",
3959 });
3960 let retried = super::maybe_retry_request_auth(
3961 Some(&use_headers_handler),
3962 AuthTarget::Origin,
3963 &request,
3964 &challenge_response,
3965 &mut Vec::new(),
3966 )
3967 .unwrap()
3968 .unwrap();
3969 assert_eq!(retried.header("authorization"), Some("Basic dXNlcjpwYXNz"));
3970
3971 let abort_handler: Arc<dyn AuthHandler + Send + Sync> = Arc::new(AbortAuthHandler);
3972 let proxy_response = response(
3973 407,
3974 vec![Header::unchecked(
3975 "Proxy-Authenticate",
3976 "Basic realm=\"proxy\"",
3977 )],
3978 b"",
3979 );
3980 let error = super::maybe_retry_request_auth(
3981 Some(&abort_handler),
3982 AuthTarget::Proxy,
3983 &request,
3984 &proxy_response,
3985 &mut Vec::new(),
3986 )
3987 .unwrap_err();
3988 assert!(matches!(
3989 error,
3990 crate::NanoGetError::AuthenticationRejected(message)
3991 if message.contains("proxy authentication handler aborted")
3992 ));
3993 }
3994
3995 #[test]
3996 fn proxy_helper_paths_cover_remaining_validation_and_headers() {
3997 assert!(matches!(
3998 super::validate_proxy_header_name("Host"),
3999 Err(crate::NanoGetError::ProtocolManagedHeader(_))
4000 ));
4001 assert!(matches!(
4002 super::validate_proxy_header_name("TE"),
4003 Err(crate::NanoGetError::HopByHopHeader(_))
4004 ));
4005
4006 let proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
4007 let request = Request::get("https://example.com").unwrap();
4008 let config = ClientConfig {
4009 preemptive_proxy_authorization: Some("Basic cHJveHk6c2VjcmV0".to_string()),
4010 ..ClientConfig::default()
4011 };
4012 let headers = prepared_connect_headers(&request, &config, &proxy).unwrap();
4013 assert!(headers
4014 .iter()
4015 .any(|header| header.matches_name("proxy-authorization")));
4016
4017 let mut request_with_proxy = Request::get("https://example.com").unwrap();
4018 request_with_proxy
4019 .proxy_authorization("Basic explicit")
4020 .unwrap();
4021 let headers = prepared_connect_headers(&request_with_proxy, &config, &proxy).unwrap();
4022 assert!(headers
4023 .iter()
4024 .any(|header| header.value() == "Basic explicit"));
4025
4026 let mut cache = MemoryCache::default();
4027 let get_request = Request::get("http://example.com/head").unwrap();
4028 let get_response = response(
4029 200,
4030 vec![
4031 Header::unchecked("Cache-Control", "max-age=60"),
4032 Header::unchecked("ETag", "\"v1\""),
4033 Header::unchecked("Content-Length", "6"),
4034 ],
4035 b"abcdef",
4036 );
4037 cache.store(
4038 &get_request,
4039 &timed_response(get_response),
4040 &AuthContext::default(),
4041 );
4042 let head_request = Request::head("http://example.com/head").unwrap();
4043 let head_response = response(
4044 200,
4045 vec![
4046 Header::unchecked("Cache-Control", "max-age=60"),
4047 Header::unchecked("ETag", "\"v2\""),
4048 Header::unchecked("Content-Length", "8"),
4049 ],
4050 b"",
4051 );
4052 cache.store(
4053 &head_request,
4054 &timed_response(head_response),
4055 &AuthContext::default(),
4056 );
4057 }
4058
4059 #[cfg(not(feature = "https"))]
4060 #[test]
4061 fn open_https_tunnel_reports_feature_required_after_connect_success() {
4062 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4063 let port = listener.local_addr().unwrap().port();
4064 let handle = thread::spawn(move || {
4065 let (mut stream, _) = listener.accept().unwrap();
4066 let mut request = Vec::new();
4067 let mut chunk = [0u8; 256];
4068 loop {
4069 let read = stream.read(&mut chunk).unwrap();
4070 assert!(read > 0, "client closed before sending CONNECT request");
4071 request.extend_from_slice(&chunk[..read]);
4072 if request.windows(4).any(|window| window == b"\r\n\r\n") {
4073 break;
4074 }
4075 }
4076 stream
4077 .write_all(b"HTTP/1.1 200 Connection Established\r\nContent-Length: 0\r\n\r\n")
4078 .unwrap();
4079 });
4080
4081 let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4082 let config = ClientConfig::default();
4083 let request = Request::get("https://example.com").unwrap();
4084 let error = super::open_https_tunnel(&config, &request, &proxy)
4085 .err()
4086 .expect("expected tunnel failure");
4087 assert!(matches!(error, crate::NanoGetError::HttpsFeatureRequired));
4088 handle.join().unwrap();
4089 }
4090
4091 #[cfg(not(feature = "https"))]
4092 #[test]
4093 fn open_https_tunnel_without_auth_handler_returns_proxy_connect_failed() {
4094 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4095 let port = listener.local_addr().unwrap().port();
4096 let handle = thread::spawn(move || {
4097 let (mut stream, _) = listener.accept().unwrap();
4098 let mut request = Vec::new();
4099 let mut chunk = [0u8; 256];
4100 loop {
4101 let read = stream.read(&mut chunk).unwrap();
4102 assert!(read > 0, "client closed before sending CONNECT request");
4103 request.extend_from_slice(&chunk[..read]);
4104 if request.windows(4).any(|window| window == b"\r\n\r\n") {
4105 break;
4106 }
4107 }
4108 stream
4109 .write_all(
4110 b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
4111 )
4112 .unwrap();
4113 });
4114
4115 let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4116 let request = Request::get("https://example.com").unwrap();
4117 let error = super::open_https_tunnel(&ClientConfig::default(), &request, &proxy)
4118 .err()
4119 .expect("expected proxy failure");
4120 assert!(matches!(
4121 error,
4122 crate::NanoGetError::ProxyConnectFailed(407, _)
4123 ));
4124 handle.join().unwrap();
4125 }
4126
4127 #[cfg(not(feature = "https"))]
4128 #[test]
4129 fn open_https_tunnel_non_auth_proxy_failure_is_reported() {
4130 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4131 let port = listener.local_addr().unwrap().port();
4132 let handle = thread::spawn(move || {
4133 let (mut stream, _) = listener.accept().unwrap();
4134 let mut request = Vec::new();
4135 let mut chunk = [0u8; 256];
4136 loop {
4137 let read = stream.read(&mut chunk).unwrap();
4138 assert!(read > 0, "client closed before sending CONNECT request");
4139 request.extend_from_slice(&chunk[..read]);
4140 if request.windows(4).any(|window| window == b"\r\n\r\n") {
4141 break;
4142 }
4143 }
4144 stream
4145 .write_all(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
4146 .unwrap();
4147 });
4148
4149 let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4150 let request = Request::get("https://example.com").unwrap();
4151 let error = super::open_https_tunnel(&ClientConfig::default(), &request, &proxy)
4152 .err()
4153 .expect("expected proxy failure");
4154 assert!(matches!(
4155 error,
4156 crate::NanoGetError::ProxyConnectFailed(502, _)
4157 ));
4158 handle.join().unwrap();
4159 }
4160
4161 #[cfg(feature = "https")]
4162 #[test]
4163 fn open_https_tunnel_returns_proxy_error_without_handler() {
4164 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4165 let port = listener.local_addr().unwrap().port();
4166 let handle = thread::spawn(move || {
4167 let (mut stream, _) = listener.accept().unwrap();
4168 let mut request = Vec::new();
4169 let mut chunk = [0u8; 256];
4170 loop {
4171 let read = stream.read(&mut chunk).unwrap();
4172 assert!(read > 0, "client closed before sending CONNECT request");
4173 request.extend_from_slice(&chunk[..read]);
4174 if request.windows(4).any(|window| window == b"\r\n\r\n") {
4175 break;
4176 }
4177 }
4178 stream
4179 .write_all(
4180 b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
4181 )
4182 .unwrap();
4183 });
4184
4185 let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4186 let request = Request::get("https://example.com").unwrap();
4187 let error = super::open_https_tunnel(&ClientConfig::default(), &request, &proxy)
4188 .err()
4189 .expect("expected tunnel failure");
4190 assert!(matches!(
4191 error,
4192 crate::NanoGetError::ProxyConnectFailed(407, _)
4193 ));
4194 handle.join().unwrap();
4195 }
4196
4197 #[cfg(not(feature = "https"))]
4198 #[test]
4199 fn open_https_tunnel_retries_proxy_auth_challenges() {
4200 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4201 let port = listener.local_addr().unwrap().port();
4202 let requests = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
4203 let requests_for_thread = Arc::clone(&requests);
4204 let handle = thread::spawn(move || {
4205 for index in 0..2 {
4206 let (mut stream, _) = listener.accept().unwrap();
4207 let mut request = Vec::new();
4208 let mut chunk = [0u8; 256];
4209 let mut read = stream.read(&mut chunk).unwrap();
4210 while read > 0 {
4211 request.extend_from_slice(&chunk[..read]);
4212 if request.windows(4).any(|window| window == b"\r\n\r\n") {
4213 break;
4214 }
4215 read = stream.read(&mut chunk).unwrap();
4216 }
4217 requests_for_thread
4218 .lock()
4219 .unwrap()
4220 .push(String::from_utf8_lossy(&request).into_owned());
4221 if index == 0 {
4222 stream
4223 .write_all(b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n")
4224 .unwrap();
4225 } else {
4226 stream
4227 .write_all(
4228 b"HTTP/1.1 200 Connection Established\r\nContent-Length: 0\r\n\r\n",
4229 )
4230 .unwrap();
4231 }
4232 }
4233 });
4234
4235 let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4236 let config = ClientConfig {
4237 proxy_auth_handler: Some(Arc::new(super::BasicAuthHandler::new(
4238 "proxy",
4239 "secret",
4240 AuthTarget::Proxy,
4241 ))),
4242 ..ClientConfig::default()
4243 };
4244 let request = Request::get("https://example.com").unwrap();
4245 let error = super::open_https_tunnel(&config, &request, &proxy)
4246 .err()
4247 .expect("expected tunnel failure");
4248 assert!(matches!(error, crate::NanoGetError::HttpsFeatureRequired));
4249
4250 let captured = requests.lock().unwrap().clone();
4251 assert_eq!(captured.len(), 2);
4252 assert!(!captured[0].contains("Proxy-Authorization:"));
4253 assert!(captured[1].contains("Proxy-Authorization: Basic"));
4254 handle.join().unwrap();
4255 }
4256
4257 #[cfg(feature = "https")]
4258 #[test]
4259 fn open_https_tunnel_retries_proxy_auth_before_reporting_failure() {
4260 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4261 let port = listener.local_addr().unwrap().port();
4262 let requests = Arc::new(Mutex::new(Vec::<String>::new()));
4263 let requests_for_thread = Arc::clone(&requests);
4264 let handle = thread::spawn(move || {
4265 for index in 0..2 {
4266 let (mut stream, _) = listener.accept().unwrap();
4267 let mut request = Vec::new();
4268 let mut chunk = [0u8; 256];
4269 let mut read = stream.read(&mut chunk).unwrap();
4270 while read > 0 {
4271 request.extend_from_slice(&chunk[..read]);
4272 if request.windows(4).any(|window| window == b"\r\n\r\n") {
4273 break;
4274 }
4275 read = stream.read(&mut chunk).unwrap();
4276 }
4277 requests_for_thread
4278 .lock()
4279 .unwrap()
4280 .push(String::from_utf8_lossy(&request).into_owned());
4281 if index == 0 {
4282 stream
4283 .write_all(
4284 b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
4285 )
4286 .unwrap();
4287 } else {
4288 stream
4289 .write_all(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
4290 .unwrap();
4291 }
4292 }
4293 });
4294
4295 let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4296 let config = ClientConfig {
4297 proxy_auth_handler: Some(Arc::new(super::BasicAuthHandler::new(
4298 "proxy",
4299 "secret",
4300 AuthTarget::Proxy,
4301 ))),
4302 ..ClientConfig::default()
4303 };
4304 let request = Request::get("https://example.com").unwrap();
4305 let error = super::open_https_tunnel(&config, &request, &proxy)
4306 .err()
4307 .expect("expected https feature error");
4308 assert!(matches!(
4309 error,
4310 crate::NanoGetError::ProxyConnectFailed(502, _)
4311 ));
4312
4313 let captured = requests.lock().unwrap().clone();
4314 assert_eq!(captured.len(), 2);
4315 assert!(!captured[0].contains("Proxy-Authorization:"));
4316 assert!(captured[1].contains("Proxy-Authorization: Basic"));
4317 handle.join().unwrap();
4318 }
4319}