1use std::collections::HashMap;
29
30use serde::{Deserialize, Serialize};
31use serde_json::Value;
32
33use crate::online_consent::{
34 environment_online, interactive_online_consent_allowed, prompt_for_online_consent,
35 saved_online_preference, OnlinePreference,
36};
37use crate::VerifyOptions;
38
39const MAX_MANIFEST_BYTES: usize = 64 * 1024 * 1024;
42const MAX_OCSP_BYTES: usize = 64 * 1024;
45const MAX_DID_DOCUMENT_BYTES: usize = 256 * 1024;
47const MAX_EXTERNAL_DATA_BYTES: usize = 64 * 1024 * 1024;
49const MAX_REQUESTS: usize = 16;
51
52pub(crate) use crate::c2pa_validate::{NetworkNeed, OcspPurpose};
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
62#[serde(default)]
63pub struct NetworkReport {
64 pub enabled: bool,
66 pub needed: Vec<Value>,
68 pub requests: Vec<NetworkRequest>,
70}
71
72impl NetworkReport {
73 pub(crate) fn mark_unusable(&mut self, purpose: &str, detail: String) {
77 if let Some(request) = self
78 .requests
79 .iter_mut()
80 .find(|request| request.purpose == purpose && request.outcome == "fetched")
81 {
82 request.outcome = "failed".to_string();
83 request.detail = detail;
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90pub struct NetworkRequest {
91 pub purpose: String,
94 pub url: String,
96 pub outcome: String,
102 pub detail: String,
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
109pub(crate) struct OnlineEvidence {
110 pub(crate) manifest_store: Option<Vec<u8>>,
112 pub(crate) ocsp_responses: HashMap<String, String>,
114 pub(crate) ocsp_unreachable: Vec<String>,
116 pub(crate) external_data: HashMap<String, String>,
118 pub(crate) did_documents: HashMap<String, Value>,
120}
121
122impl OnlineEvidence {
123 pub(crate) fn is_empty(&self) -> bool {
124 self.manifest_store.is_none()
125 && self.ocsp_responses.is_empty()
126 && self.ocsp_unreachable.is_empty()
127 && self.external_data.is_empty()
128 && self.did_documents.is_empty()
129 }
130}
131
132impl NetworkNeed {
133 pub(crate) fn purpose_token(&self) -> &'static str {
135 match self {
136 Self::RemoteManifest { .. } => "remote_manifest",
137 Self::Ocsp {
138 purpose: OcspPurpose::ClaimSigner,
139 ..
140 } => "ocsp.claim_signer",
141 Self::Ocsp { .. } => "ocsp.cawg_identity",
142 Self::DidDocument { .. } => "did_document",
143 Self::ExternalData { .. } => "external_data",
144 }
145 }
146
147 pub(crate) fn url(&self) -> &str {
149 match self {
150 Self::RemoteManifest { uri } => uri,
151 Self::Ocsp { responder_url, .. } => responder_url,
152 Self::DidDocument { url, .. } => url,
153 Self::ExternalData { uri, .. } => uri,
154 }
155 }
156
157 fn consent_line(&self) -> String {
159 let what = match self {
160 Self::RemoteManifest { .. } => "the manifest store this file points to".to_string(),
161 Self::Ocsp {
162 purpose: OcspPurpose::ClaimSigner,
163 ..
164 } => "whether the signing certificate has been revoked".to_string(),
165 Self::Ocsp {
166 purpose: OcspPurpose::CawgIdentity { assertion_label },
167 ..
168 } => format!("whether the identity certificate in {assertion_label} has been revoked"),
169 Self::DidDocument { did, .. } => format!("the issuer document for {did}"),
170 Self::ExternalData {
171 assertion_label, ..
172 } => format!("content stored outside the file, for {assertion_label}"),
173 };
174 format!("{what}, from {}", host_of(self.url()))
175 }
176}
177
178pub(crate) fn network_needs(output: &crate::c2pa_validate::VerifyOutput) -> Vec<NetworkNeed> {
180 output.network_needs.clone()
181}
182
183pub(crate) fn gather(
191 options: &VerifyOptions,
192 needs: &[NetworkNeed],
193) -> (NetworkReport, OnlineEvidence) {
194 let needed: Vec<Value> = needs.iter().map(NetworkNeed::to_json).collect();
195 let enabled = resolve_online(options, needs);
196 if !enabled || needs.is_empty() {
197 return (
198 NetworkReport {
199 enabled,
200 needed,
201 requests: Vec::new(),
202 },
203 OnlineEvidence::default(),
204 );
205 }
206 let fetcher = new_fetcher(options.online_allow_private_networks);
207 let (requests, evidence) = fetch_all(fetcher.as_ref(), needs);
208 (
209 NetworkReport {
210 enabled: true,
211 needed,
212 requests,
213 },
214 evidence,
215 )
216}
217
218fn resolve_online(options: &VerifyOptions, needs: &[NetworkNeed]) -> bool {
219 if let Some(explicit) = options.online {
220 return explicit;
221 }
222 match environment_online() {
223 Ok(Some(enabled)) => return enabled,
224 Ok(None) => {}
225 Err(_) => return false,
228 }
229 if !interactive_online_consent_allowed() || needs.is_empty() {
230 return false;
231 }
232 match saved_online_preference() {
233 Ok(Some(OnlinePreference::On)) => true,
234 Ok(Some(OnlinePreference::Off)) => false,
235 Ok(Some(OnlinePreference::Ask)) | Ok(None) => {
236 let lines: Vec<String> = needs.iter().map(NetworkNeed::consent_line).collect();
237 prompt_for_online_consent(&lines)
238 .ok()
239 .flatten()
240 .unwrap_or(false)
241 }
242 Err(_) => false,
243 }
244}
245
246pub(crate) fn apply_evidence(options: &VerifyOptions, evidence: &OnlineEvidence) -> VerifyOptions {
248 let mut next = options.clone();
249 next.online = Some(false);
253 if !evidence.did_documents.is_empty() {
254 let mut documents = next.cawg_did_documents.unwrap_or_default();
255 documents.extend(
256 evidence
257 .did_documents
258 .iter()
259 .map(|(did, document)| (did.clone(), document.clone())),
260 );
261 next.cawg_did_documents = Some(documents);
262 }
263 if !evidence.ocsp_responses.is_empty() {
264 let mut responses = next.ocsp_responses.unwrap_or_default();
265 responses.extend(evidence.ocsp_responses.clone());
266 next.ocsp_responses = Some(responses);
267 }
268 if !evidence.ocsp_unreachable.is_empty() {
269 let mut unreachable = next.ocsp_unreachable.unwrap_or_default();
270 unreachable.extend(evidence.ocsp_unreachable.iter().cloned());
271 next.ocsp_unreachable = Some(unreachable);
272 }
273 if !evidence.external_data.is_empty() {
274 let mut data = next.external_data.unwrap_or_default();
275 data.extend(evidence.external_data.clone());
276 next.external_data = Some(data);
277 }
278 next
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub(crate) enum FetchMethod {
287 Get,
288 Post,
289}
290
291#[cfg_attr(
293 not(feature = "online"),
294 allow(dead_code, reason = "only the native fetcher reads a request")
295)]
296pub(crate) struct FetchRequest<'a> {
297 pub(crate) url: &'a str,
298 pub(crate) method: FetchMethod,
299 pub(crate) body: Option<&'a [u8]>,
300 pub(crate) content_type: Option<&'a str>,
301 pub(crate) accept: Option<&'a str>,
302 pub(crate) limit: usize,
304 pub(crate) allow_http: bool,
306}
307
308#[derive(Debug)]
309pub(crate) struct FetchOk {
310 pub(crate) bytes: Vec<u8>,
311 pub(crate) requests_used: usize,
312}
313
314#[derive(Debug)]
315pub(crate) struct FetchErr {
316 pub(crate) blocked: bool,
318 pub(crate) detail: String,
319 pub(crate) requests_used: usize,
320}
321
322pub(crate) type FetchResult = Result<FetchOk, FetchErr>;
323
324pub(crate) trait Fetcher {
325 fn fetch(&self, request: &FetchRequest<'_>) -> FetchResult;
326}
327
328fn blocked(detail: impl Into<String>, requests_used: usize) -> FetchErr {
329 FetchErr {
330 blocked: true,
331 detail: detail.into(),
332 requests_used,
333 }
334}
335
336pub(crate) fn fetch_all(
338 fetcher: &dyn Fetcher,
339 needs: &[NetworkNeed],
340) -> (Vec<NetworkRequest>, OnlineEvidence) {
341 let mut requests = Vec::with_capacity(needs.len());
342 let mut evidence = OnlineEvidence::default();
343 let mut budget = MAX_REQUESTS;
344
345 for need in needs {
346 let purpose = need.purpose_token().to_string();
347 let url = need.url().to_string();
348 if budget == 0 {
349 requests.push(NetworkRequest {
350 purpose,
351 url,
352 outcome: "skipped".to_string(),
353 detail: format!("the {MAX_REQUESTS} request budget for one verification is spent"),
354 });
355 if let NetworkNeed::Ocsp {
356 certificate_sha256_hex,
357 ..
358 } = need
359 {
360 push_unreachable(&mut evidence, certificate_sha256_hex);
361 }
362 continue;
363 }
364
365 let request = match need {
366 NetworkNeed::RemoteManifest { uri } => FetchRequest {
367 url: uri,
368 method: FetchMethod::Get,
369 body: None,
370 content_type: None,
371 accept: Some("application/c2pa"),
372 limit: MAX_MANIFEST_BYTES,
373 allow_http: false,
374 },
375 NetworkNeed::Ocsp {
376 responder_url,
377 request_der,
378 ..
379 } => FetchRequest {
380 url: responder_url,
381 method: FetchMethod::Post,
382 body: Some(request_der),
383 content_type: Some("application/ocsp-request"),
384 accept: Some("application/ocsp-response"),
385 limit: MAX_OCSP_BYTES,
386 allow_http: true,
390 },
391 NetworkNeed::DidDocument { url, .. } => FetchRequest {
392 url,
393 method: FetchMethod::Get,
394 body: None,
395 content_type: None,
396 accept: Some("application/did+json, application/json"),
397 limit: MAX_DID_DOCUMENT_BYTES,
398 allow_http: false,
399 },
400 NetworkNeed::ExternalData { uri, .. } => FetchRequest {
401 url: uri,
402 method: FetchMethod::Get,
403 body: None,
404 content_type: None,
405 accept: None,
406 limit: MAX_EXTERNAL_DATA_BYTES,
407 allow_http: false,
408 },
409 };
410
411 let (outcome, detail) = match fetcher.fetch(&request) {
412 Ok(FetchOk {
413 bytes,
414 requests_used,
415 }) => {
416 budget = budget.saturating_sub(requests_used.max(1));
417 accept_evidence(&mut evidence, need, bytes)
418 }
419 Err(FetchErr {
420 blocked,
421 detail,
422 requests_used,
423 }) => {
424 budget = budget.saturating_sub(requests_used.max(1));
425 if let NetworkNeed::Ocsp {
426 certificate_sha256_hex,
427 ..
428 } = need
429 {
430 push_unreachable(&mut evidence, certificate_sha256_hex);
431 }
432 (
433 if blocked { "blocked" } else { "failed" }.to_string(),
434 detail,
435 )
436 }
437 };
438 requests.push(NetworkRequest {
439 purpose,
440 url,
441 outcome,
442 detail,
443 });
444 }
445
446 (requests, evidence)
447}
448
449fn push_unreachable(evidence: &mut OnlineEvidence, certificate_sha256_hex: &str) {
450 if !evidence
451 .ocsp_unreachable
452 .iter()
453 .any(|existing| existing == certificate_sha256_hex)
454 {
455 evidence
456 .ocsp_unreachable
457 .push(certificate_sha256_hex.to_string());
458 }
459}
460
461fn accept_evidence(
463 evidence: &mut OnlineEvidence,
464 need: &NetworkNeed,
465 bytes: Vec<u8>,
466) -> (String, String) {
467 let length = bytes.len();
468 match need {
469 NetworkNeed::RemoteManifest { .. } => {
470 evidence.manifest_store = Some(bytes);
471 ("fetched".to_string(), format!("{length} bytes"))
472 }
473 NetworkNeed::Ocsp {
474 certificate_sha256_hex,
475 ..
476 } => {
477 evidence
478 .ocsp_responses
479 .insert(certificate_sha256_hex.clone(), base64_encode(&bytes));
480 ("fetched".to_string(), format!("{length} bytes"))
481 }
482 NetworkNeed::DidDocument { did, .. } => match serde_json::from_slice::<Value>(&bytes) {
483 Ok(document) => {
484 evidence.did_documents.insert(did.clone(), document);
485 ("fetched".to_string(), format!("{length} bytes"))
486 }
487 Err(error) => (
488 "failed".to_string(),
489 format!("the DID document is not JSON: {error}"),
490 ),
491 },
492 NetworkNeed::ExternalData { uri, .. } => {
493 evidence
494 .external_data
495 .insert(uri.clone(), base64_encode(&bytes));
496 ("fetched".to_string(), format!("{length} bytes"))
497 }
498 }
499}
500
501fn split_url(url: &str) -> Option<(&str, &str, &str)> {
508 let (scheme, remainder) = url.split_once("://")?;
509 if scheme.is_empty() || remainder.is_empty() {
510 return None;
511 }
512 let end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len());
513 let (authority, rest) = remainder.split_at(end);
514 if authority.is_empty() {
515 return None;
516 }
517 Some((scheme, authority, rest))
518}
519
520fn host_of(url: &str) -> String {
522 let Some((_, authority, _)) = split_url(url) else {
523 return url.to_string();
524 };
525 let authority = authority
526 .rsplit_once('@')
527 .map_or(authority, |(_, host)| host);
528 if let Some(end) = authority.find(']') {
529 return authority[..=end].to_string();
530 }
531 authority
532 .split_once(':')
533 .map_or(authority, |(host, _)| host)
534 .to_string()
535}
536
537fn base64_encode(input: &[u8]) -> String {
544 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
545 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
546 for chunk in input.chunks(3) {
547 let b0 = u32::from(chunk[0]);
548 let b1 = chunk.get(1).copied().map_or(0, u32::from);
549 let b2 = chunk.get(2).copied().map_or(0, u32::from);
550 let triple = (b0 << 16) | (b1 << 8) | b2;
551 out.push(ALPHABET[(triple >> 18) as usize & 0x3f] as char);
552 out.push(ALPHABET[(triple >> 12) as usize & 0x3f] as char);
553 out.push(if chunk.len() > 1 {
554 ALPHABET[(triple >> 6) as usize & 0x3f] as char
555 } else {
556 '='
557 });
558 out.push(if chunk.len() > 2 {
559 ALPHABET[triple as usize & 0x3f] as char
560 } else {
561 '='
562 });
563 }
564 out
565}
566
567#[cfg(feature = "online")]
572fn new_fetcher(allow_private_networks: bool) -> Box<dyn Fetcher> {
573 Box::new(native::NativeFetcher::new(allow_private_networks))
574}
575
576#[cfg(not(feature = "online"))]
577fn new_fetcher(_allow_private_networks: bool) -> Box<dyn Fetcher> {
578 Box::new(DisabledFetcher)
579}
580
581#[cfg(not(feature = "online"))]
584struct DisabledFetcher;
585
586#[cfg(not(feature = "online"))]
587impl Fetcher for DisabledFetcher {
588 fn fetch(&self, _request: &FetchRequest<'_>) -> FetchResult {
589 Err(blocked(
590 "this build was compiled without the online feature",
591 0,
592 ))
593 }
594}
595
596#[cfg(feature = "online")]
597pub(crate) mod native {
598 use std::io::Read;
599 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
600 use std::time::Duration;
601 use std::{io, net};
602
603 use super::{
604 blocked, split_url, FetchErr, FetchMethod, FetchOk, FetchRequest, FetchResult, Fetcher,
605 };
606
607 const MAX_REDIRECTS: usize = 3;
609 const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
610 const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
611 const USER_AGENT: &str = concat!("encypher-c2pa/", env!("CARGO_PKG_VERSION"));
612 const BLOCKED_ADDRESS_MARKER: &str = "address is not permitted";
615
616 fn failed(detail: impl Into<String>, requests_used: usize) -> FetchErr {
617 FetchErr {
618 blocked: false,
619 detail: detail.into(),
620 requests_used,
621 }
622 }
623
624 pub(crate) fn blocked_reason(address: IpAddr) -> Option<&'static str> {
629 match address {
630 IpAddr::V4(address) => blocked_reason_v4(address),
631 IpAddr::V6(address) => blocked_reason_v6(address),
632 }
633 }
634
635 fn blocked_reason_v4(address: Ipv4Addr) -> Option<&'static str> {
636 let [a, b, c, _] = address.octets();
637 Some(match (a, b, c) {
638 (0, _, _) => "unspecified network",
639 (10, _, _) => "private network",
640 (100, 64..=127, _) => "carrier-grade NAT",
641 (127, _, _) => "loopback",
642 (169, 254, _) => "link-local, including the cloud metadata service",
643 (172, 16..=31, _) => "private network",
644 (192, 0, 0) => "IETF protocol assignment",
645 (192, 0, 2) => "documentation",
646 (192, 168, _) => "private network",
647 (198, 18..=19, _) => "benchmarking",
648 (198, 51, 100) => "documentation",
649 (203, 0, 113) => "documentation",
650 (224..=239, _, _) => "multicast",
651 (240..=255, _, _) => "reserved or broadcast",
652 _ => return None,
653 })
654 }
655
656 fn blocked_reason_v6(address: Ipv6Addr) -> Option<&'static str> {
657 if let Some(embedded) = embedded_ipv4(address) {
660 if let Some(reason) = blocked_reason_v4(embedded) {
661 return Some(reason);
662 }
663 }
664 let segments = address.segments();
665 if address.is_unspecified() {
666 return Some("unspecified address");
667 }
668 if address.is_loopback() {
669 return Some("loopback");
670 }
671 if address.is_multicast() {
672 return Some("multicast");
673 }
674 Some(match segments[0] {
675 first if first & 0xfe00 == 0xfc00 => "unique local",
676 first if first & 0xffc0 == 0xfe80 => "link-local",
677 0x2001 if segments[1] == 0x0db8 => "documentation",
678 0x0100 if segments[1] == 0 && segments[2] == 0 && segments[3] == 0 => "discard-only",
679 _ => return None,
680 })
681 }
682
683 fn embedded_ipv4(address: Ipv6Addr) -> Option<Ipv4Addr> {
684 let segments = address.segments();
685 let octets = address.octets();
686 let tail = Ipv4Addr::new(octets[12], octets[13], octets[14], octets[15]);
687 if segments[0..5] == [0, 0, 0, 0, 0] && (segments[5] == 0xffff || segments[5] == 0) {
689 return Some(tail);
690 }
691 if segments[0] == 0x2002 {
693 return Some(Ipv4Addr::new(octets[2], octets[3], octets[4], octets[5]));
694 }
695 if segments[0] == 0x0064 && segments[1] == 0xff9b {
697 return Some(tail);
698 }
699 None
700 }
701
702 pub(crate) fn join_redirect(base: &str, location: &str) -> Option<String> {
704 let location = location.trim();
705 if location.is_empty() {
706 return None;
707 }
708 if location.contains("://") {
709 return Some(location.to_string());
710 }
711 let (scheme, authority, rest) = split_url(base)?;
712 if let Some(network_path) = location.strip_prefix("//") {
713 return Some(format!("{scheme}://{network_path}"));
714 }
715 if location.starts_with('/') {
716 return Some(format!("{scheme}://{authority}{location}"));
717 }
718 let path = rest.split(['?', '#']).next().unwrap_or("");
719 let directory = path.rfind('/').map_or("/", |index| &path[..=index]);
720 Some(format!("{scheme}://{authority}{directory}{location}"))
721 }
722
723 pub(crate) struct FilteringResolver {
730 pub(crate) allow_private_networks: bool,
731 }
732
733 impl ureq::Resolver for FilteringResolver {
734 fn resolve(&self, netloc: &str) -> io::Result<Vec<SocketAddr>> {
735 let resolved: Vec<SocketAddr> = netloc.to_socket_addrs()?.collect();
736 if self.allow_private_networks {
737 return Ok(resolved);
738 }
739 let mut refused: Option<(net::IpAddr, &'static str)> = None;
740 let permitted: Vec<SocketAddr> = resolved
741 .into_iter()
742 .filter(|address| match blocked_reason(address.ip()) {
743 Some(reason) => {
744 refused.get_or_insert((address.ip(), reason));
745 false
746 }
747 None => true,
748 })
749 .collect();
750 if permitted.is_empty() {
751 let detail = match refused {
752 Some((address, reason)) => format!(
753 "{netloc} resolves to {address}, a {reason} {BLOCKED_ADDRESS_MARKER}"
754 ),
755 None => format!("{netloc} resolves to no address"),
756 };
757 return Err(io::Error::new(io::ErrorKind::PermissionDenied, detail));
758 }
759 Ok(permitted)
760 }
761 }
762
763 pub(crate) struct NativeFetcher {
764 agent: ureq::Agent,
765 intranet: bool,
770 }
771
772 impl NativeFetcher {
773 pub(crate) fn new(allow_private_networks: bool) -> Self {
774 Self {
775 agent: ureq::AgentBuilder::new()
776 .timeout_connect(CONNECT_TIMEOUT)
777 .timeout(REQUEST_TIMEOUT)
778 .redirects(0)
781 .user_agent(USER_AGENT)
782 .resolver(FilteringResolver {
783 allow_private_networks,
784 })
785 .build(),
786 intranet: allow_private_networks,
787 }
788 }
789 }
790
791 impl Fetcher for NativeFetcher {
792 fn fetch(&self, request: &FetchRequest<'_>) -> FetchResult {
793 let mut url = request.url.to_string();
794 let mut used = 0;
795
796 for hop in 0..=MAX_REDIRECTS {
797 match split_url(&url) {
798 None => return Err(blocked(format!("{url} is not an absolute URL"), used)),
799 Some((scheme, _, _)) if scheme.eq_ignore_ascii_case("https") => {}
800 Some((scheme, _, _))
801 if (request.allow_http || self.intranet)
802 && scheme.eq_ignore_ascii_case("http") => {}
803 Some((scheme, _, _)) => {
804 return Err(blocked(
805 format!("{scheme} is not allowed for this request; https is required"),
806 used,
807 ))
808 }
809 }
810
811 used += 1;
812 let mut call = match request.method {
813 FetchMethod::Get => self.agent.get(&url),
814 FetchMethod::Post => self.agent.post(&url),
815 };
816 if let Some(content_type) = request.content_type {
817 call = call.set("content-type", content_type);
818 }
819 if let Some(accept) = request.accept {
820 call = call.set("accept", accept);
821 }
822 let response = match request.body {
823 Some(body) => call.send_bytes(body),
824 None => call.call(),
825 };
826 let response = match response {
827 Ok(response) => response,
828 Err(ureq::Error::Status(status, _)) => {
829 return Err(failed(format!("the server answered HTTP {status}"), used))
830 }
831 Err(ureq::Error::Transport(transport)) => {
832 let detail = transport.to_string();
833 return Err(if detail.contains(BLOCKED_ADDRESS_MARKER) {
834 blocked(detail, used)
835 } else {
836 failed(detail, used)
837 });
838 }
839 };
840
841 if (300..400).contains(&response.status()) {
842 if hop == MAX_REDIRECTS {
843 return Err(blocked(
844 format!("more than {MAX_REDIRECTS} redirects"),
845 used,
846 ));
847 }
848 let Some(location) = response.header("location") else {
849 return Err(failed("a redirect carried no location", used));
850 };
851 let Some(next) = join_redirect(&url, location) else {
852 return Err(failed(format!("unusable redirect to {location}"), used));
853 };
854 url = next;
855 continue;
856 }
857
858 return read_body(response, request.limit, used);
859 }
860 Err(blocked(
861 format!("more than {MAX_REDIRECTS} redirects"),
862 used,
863 ))
864 }
865 }
866
867 fn read_body(response: ureq::Response, limit: usize, used: usize) -> FetchResult {
868 let mut bytes = Vec::new();
869 let capped = u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1);
870 if let Err(error) = response.into_reader().take(capped).read_to_end(&mut bytes) {
871 return Err(failed(error.to_string(), used));
872 }
873 if bytes.len() > limit {
874 return Err(blocked(
875 format!("the response is larger than the {limit} byte limit"),
876 used,
877 ));
878 }
879 Ok(FetchOk {
880 bytes,
881 requests_used: used,
882 })
883 }
884}
885
886mod tests;