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;
51const MAX_RECEIVED_BYTES: usize = 64 * 1024 * 1024;
55
56pub(crate) use crate::c2pa_validate::{NetworkNeed, OcspPurpose};
57
58#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
66#[serde(default)]
67pub struct NetworkReport {
68 pub enabled: bool,
70 pub needed: Vec<Value>,
72 pub requests: Vec<NetworkRequest>,
74}
75
76impl NetworkReport {
77 pub(crate) fn mark_unusable(&mut self, purpose: &str, detail: String) {
81 if let Some(request) = self
82 .requests
83 .iter_mut()
84 .find(|request| request.purpose == purpose && request.outcome == "fetched")
85 {
86 request.outcome = "failed".to_string();
87 request.detail = detail;
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94pub struct NetworkRequest {
95 pub purpose: String,
98 pub url: String,
100 pub outcome: String,
106 pub detail: String,
108}
109
110#[derive(Debug, Clone, Default, PartialEq, Eq)]
113pub(crate) struct OnlineEvidence {
114 pub(crate) manifest_store: Option<Vec<u8>>,
116 pub(crate) ocsp_responses: HashMap<String, String>,
120 pub(crate) ocsp_unreachable: Vec<String>,
123 pub(crate) external_data: HashMap<String, String>,
125 pub(crate) external_data_media_types: HashMap<String, String>,
127 pub(crate) external_data_unreachable: Vec<String>,
129 pub(crate) did_documents: HashMap<String, Value>,
131}
132
133impl OnlineEvidence {
134 pub(crate) fn is_empty(&self) -> bool {
135 self.manifest_store.is_none()
136 && self.ocsp_responses.is_empty()
137 && self.ocsp_unreachable.is_empty()
138 && self.external_data.is_empty()
139 && self.external_data_media_types.is_empty()
140 && self.external_data_unreachable.is_empty()
141 && self.did_documents.is_empty()
142 }
143}
144
145impl NetworkNeed {
146 pub(crate) fn purpose_token(&self) -> &'static str {
148 match self {
149 Self::RemoteManifest { .. } => "remote_manifest",
150 Self::Ocsp {
151 purpose: OcspPurpose::ClaimSigner,
152 ..
153 } => "ocsp.claim_signer",
154 Self::Ocsp { .. } => "ocsp.cawg_identity",
155 Self::DidDocument { .. } => "did_document",
156 Self::ExternalData { .. } => "external_data",
157 }
158 }
159
160 pub(crate) fn url(&self) -> &str {
162 match self {
163 Self::RemoteManifest { uri } => uri,
164 Self::Ocsp { responder_url, .. } => responder_url,
165 Self::DidDocument { url, .. } => url,
166 Self::ExternalData { uri, .. } => uri,
167 }
168 }
169
170 fn consent_line(&self) -> String {
172 let what = match self {
173 Self::RemoteManifest { .. } => "the manifest store this file points to".to_string(),
174 Self::Ocsp {
175 purpose: OcspPurpose::ClaimSigner,
176 ..
177 } => "whether the signing certificate has been revoked".to_string(),
178 Self::Ocsp {
179 purpose: OcspPurpose::CawgIdentity { assertion_label },
180 ..
181 } => format!("whether the identity certificate in {assertion_label} has been revoked"),
182 Self::DidDocument { did, .. } => format!("the issuer document for {did}"),
183 Self::ExternalData {
184 assertion_label, ..
185 } => format!("content stored outside the file, for {assertion_label}"),
186 };
187 format!("{what}, from {}", host_of(self.url()))
188 }
189}
190
191pub(crate) fn network_needs(output: &crate::c2pa_validate::VerifyOutput) -> Vec<NetworkNeed> {
193 output.network_needs.clone()
194}
195
196pub(crate) fn gather(
204 options: &VerifyOptions,
205 needs: &[NetworkNeed],
206) -> (NetworkReport, OnlineEvidence) {
207 let needed: Vec<Value> = needs.iter().map(NetworkNeed::to_json).collect();
208 let enabled = resolve_online(options, needs);
209 if !enabled || needs.is_empty() {
210 return (
211 NetworkReport {
212 enabled,
213 needed,
214 requests: Vec::new(),
215 },
216 OnlineEvidence::default(),
217 );
218 }
219 let fetcher = new_fetcher(options.online_allow_private_networks);
220 let (requests, evidence) = fetch_all(fetcher.as_ref(), needs);
221 (
222 NetworkReport {
223 enabled: true,
224 needed,
225 requests,
226 },
227 evidence,
228 )
229}
230
231pub(crate) fn online_enabled(options: &VerifyOptions) -> bool {
237 resolve_online(options, &[])
238}
239
240pub(crate) fn resolve_online(options: &VerifyOptions, needs: &[NetworkNeed]) -> bool {
241 if let Some(explicit) = options.online {
242 return explicit;
243 }
244 match environment_online() {
245 Ok(Some(enabled)) => return enabled,
246 Ok(None) => {}
247 Err(_) => return false,
250 }
251 if !interactive_online_consent_allowed() || needs.is_empty() {
252 return false;
253 }
254 match saved_online_preference() {
255 Ok(Some(OnlinePreference::On)) => true,
256 Ok(Some(OnlinePreference::Off)) => false,
257 Ok(Some(OnlinePreference::Ask)) | Ok(None) => {
258 let lines: Vec<String> = needs.iter().map(NetworkNeed::consent_line).collect();
259 prompt_for_online_consent(&lines)
260 .ok()
261 .flatten()
262 .unwrap_or(false)
263 }
264 Err(_) => false,
265 }
266}
267
268pub(crate) fn apply_evidence(options: &VerifyOptions, evidence: &OnlineEvidence) -> VerifyOptions {
270 let mut next = options.clone();
271 next.online = Some(false);
275 if !evidence.did_documents.is_empty() {
276 let mut documents = next.cawg_did_documents.unwrap_or_default();
277 documents.extend(
278 evidence
279 .did_documents
280 .iter()
281 .map(|(did, document)| (did.clone(), document.clone())),
282 );
283 next.cawg_did_documents = Some(documents);
284 }
285 if !evidence.ocsp_responses.is_empty() {
286 let mut responses = next.ocsp_responses.unwrap_or_default();
287 responses.extend(evidence.ocsp_responses.clone());
288 next.ocsp_responses = Some(responses);
289 }
290 if !evidence.ocsp_unreachable.is_empty() {
291 let mut unreachable = next.ocsp_unreachable.unwrap_or_default();
292 unreachable.extend(evidence.ocsp_unreachable.iter().cloned());
293 next.ocsp_unreachable = Some(unreachable);
294 }
295 if !evidence.external_data.is_empty() {
296 let mut data = next.external_data.unwrap_or_default();
297 data.extend(evidence.external_data.clone());
298 next.external_data = Some(data);
299 }
300 if !evidence.external_data_media_types.is_empty() {
301 let mut media_types = next.external_data_media_types.unwrap_or_default();
302 media_types.extend(evidence.external_data_media_types.clone());
303 next.external_data_media_types = Some(media_types);
304 }
305 if !evidence.external_data_unreachable.is_empty() {
306 let mut unreachable = next.external_data_unreachable.unwrap_or_default();
307 unreachable.extend(evidence.external_data_unreachable.iter().cloned());
308 next.external_data_unreachable = Some(unreachable);
309 }
310 next
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub(crate) enum FetchMethod {
319 Get,
320 Post,
321}
322
323#[cfg_attr(
325 not(feature = "online"),
326 allow(dead_code, reason = "only the native fetcher reads a request")
327)]
328pub(crate) struct FetchRequest<'a> {
329 pub(crate) url: &'a str,
330 pub(crate) method: FetchMethod,
331 pub(crate) body: Option<&'a [u8]>,
332 pub(crate) content_type: Option<&'a str>,
333 pub(crate) accept: Option<&'a str>,
334 pub(crate) limit: usize,
336 pub(crate) allow_http: bool,
338 pub(crate) remaining_requests: usize,
342}
343
344#[derive(Debug)]
345pub(crate) struct FetchOk {
346 pub(crate) bytes: Vec<u8>,
347 pub(crate) media_type: Option<String>,
348 pub(crate) requests_used: usize,
349}
350
351#[derive(Debug)]
352pub(crate) struct FetchErr {
353 pub(crate) blocked: bool,
355 pub(crate) received_response: bool,
358 pub(crate) detail: String,
359 pub(crate) requests_used: usize,
360 pub(crate) received_bytes: usize,
363}
364
365pub(crate) type FetchResult = Result<FetchOk, FetchErr>;
366
367pub(crate) trait Fetcher {
368 fn fetch(&self, request: &FetchRequest<'_>) -> FetchResult;
369}
370
371fn blocked(detail: impl Into<String>, requests_used: usize) -> FetchErr {
372 FetchErr {
373 blocked: true,
374 received_response: false,
375 detail: detail.into(),
376 requests_used,
377 received_bytes: 0,
378 }
379}
380
381#[derive(Debug)]
382struct FetchBudget {
383 requests: usize,
384 received_bytes: usize,
385}
386
387impl Default for FetchBudget {
388 fn default() -> Self {
389 Self {
390 requests: MAX_REQUESTS,
391 received_bytes: MAX_RECEIVED_BYTES,
392 }
393 }
394}
395
396pub(crate) struct FetchSession {
403 fetcher: Option<Box<dyn Fetcher>>,
406 allow_private_networks: bool,
407 budget: FetchBudget,
408}
409
410impl FetchSession {
411 pub(crate) fn new(allow_private_networks: bool) -> Self {
412 Self {
413 fetcher: None,
414 allow_private_networks,
415 budget: FetchBudget::default(),
416 }
417 }
418
419 #[cfg(test)]
420 pub(crate) fn with_fetcher(fetcher: Box<dyn Fetcher>) -> Self {
421 Self {
422 fetcher: Some(fetcher),
423 allow_private_networks: false,
424 budget: FetchBudget::default(),
425 }
426 }
427
428 pub(crate) fn fetch(&mut self, need: &NetworkNeed) -> (NetworkRequest, FetchResult) {
429 if let Some(exhausted) = budget_exhaustion(&self.budget, need) {
430 return exhausted;
431 }
432 let allow_private_networks = self.allow_private_networks;
433 let fetcher = self
434 .fetcher
435 .get_or_insert_with(|| new_fetcher(allow_private_networks));
436 fetch_one(fetcher.as_ref(), &mut self.budget, need)
437 }
438}
439
440fn request_for_need<'a>(
441 need: &'a NetworkNeed,
442 remaining_requests: usize,
443 remaining_bytes: usize,
444) -> FetchRequest<'a> {
445 match need {
446 NetworkNeed::RemoteManifest { uri } => FetchRequest {
447 url: uri,
448 method: FetchMethod::Get,
449 body: None,
450 content_type: None,
451 accept: Some("application/c2pa"),
452 limit: MAX_MANIFEST_BYTES.min(remaining_bytes),
453 allow_http: false,
454 remaining_requests,
455 },
456 NetworkNeed::Ocsp {
457 responder_url,
458 request_der,
459 ..
460 } => FetchRequest {
461 url: responder_url,
462 method: FetchMethod::Post,
463 body: Some(request_der),
464 content_type: Some("application/ocsp-request"),
465 accept: Some("application/ocsp-response"),
466 limit: MAX_OCSP_BYTES.min(remaining_bytes),
467 allow_http: true,
470 remaining_requests,
471 },
472 NetworkNeed::DidDocument { url, .. } => FetchRequest {
473 url,
474 method: FetchMethod::Get,
475 body: None,
476 content_type: None,
477 accept: Some("application/did+json, application/json"),
478 limit: MAX_DID_DOCUMENT_BYTES.min(remaining_bytes),
479 allow_http: false,
480 remaining_requests,
481 },
482 NetworkNeed::ExternalData { uri, .. } => FetchRequest {
483 url: uri,
484 method: FetchMethod::Get,
485 body: None,
486 content_type: None,
487 accept: None,
488 limit: MAX_EXTERNAL_DATA_BYTES.min(remaining_bytes),
489 allow_http: false,
490 remaining_requests,
491 },
492 }
493}
494
495fn budget_exhaustion(
496 budget: &FetchBudget,
497 need: &NetworkNeed,
498) -> Option<(NetworkRequest, FetchResult)> {
499 let detail = if budget.requests == 0 {
500 format!("the {MAX_REQUESTS} request budget for one verification is spent")
501 } else if budget.received_bytes == 0 {
502 format!("the {MAX_RECEIVED_BYTES} byte response budget for one verification is spent")
503 } else {
504 return None;
505 };
506 Some((
507 NetworkRequest {
508 purpose: need.purpose_token().to_string(),
509 url: need.url().to_string(),
510 outcome: "skipped".to_string(),
511 detail: detail.clone(),
512 },
513 Err(blocked(detail, 0)),
514 ))
515}
516
517fn fetch_one(
518 fetcher: &dyn Fetcher,
519 budget: &mut FetchBudget,
520 need: &NetworkNeed,
521) -> (NetworkRequest, FetchResult) {
522 if let Some(exhausted) = budget_exhaustion(budget, need) {
523 return exhausted;
524 }
525 let purpose = need.purpose_token().to_string();
526 let url = need.url().to_string();
527
528 let request = request_for_need(need, budget.requests, budget.received_bytes);
529 let result = fetcher.fetch(&request);
530 let (requests_used, received_bytes, outcome, detail) = match &result {
531 Ok(response) => (
532 response.requests_used,
533 response.bytes.len(),
534 "fetched",
535 format!("{} bytes", response.bytes.len()),
536 ),
537 Err(error) => (
538 error.requests_used,
539 error.received_bytes,
540 if error.blocked { "blocked" } else { "failed" },
541 error.detail.clone(),
542 ),
543 };
544 budget.requests = budget.requests.saturating_sub(requests_used);
545 budget.received_bytes = budget.received_bytes.saturating_sub(received_bytes);
546
547 (
548 NetworkRequest {
549 purpose,
550 url,
551 outcome: outcome.to_string(),
552 detail,
553 },
554 result,
555 )
556}
557
558pub(crate) fn fetch_all(
560 fetcher: &dyn Fetcher,
561 needs: &[NetworkNeed],
562) -> (Vec<NetworkRequest>, OnlineEvidence) {
563 let mut requests = Vec::with_capacity(needs.len());
564 let mut evidence = OnlineEvidence::default();
565 let mut budget = MAX_REQUESTS;
566
567 for need in needs {
568 let purpose = need.purpose_token().to_string();
569 let url = need.url().to_string();
570 if budget == 0 {
571 requests.push(NetworkRequest {
572 purpose,
573 url,
574 outcome: "skipped".to_string(),
575 detail: format!("the {MAX_REQUESTS} request budget for one verification is spent"),
576 });
577 continue;
578 }
579
580 let request = match need {
581 NetworkNeed::RemoteManifest { uri } => FetchRequest {
582 url: uri,
583 method: FetchMethod::Get,
584 body: None,
585 content_type: None,
586 accept: Some("application/c2pa"),
587 limit: MAX_MANIFEST_BYTES,
588 allow_http: false,
589 remaining_requests: usize::MAX,
590 },
591 NetworkNeed::Ocsp {
592 responder_url,
593 request_der,
594 ..
595 } => FetchRequest {
596 url: responder_url,
597 method: FetchMethod::Post,
598 body: Some(request_der),
599 content_type: Some("application/ocsp-request"),
600 accept: Some("application/ocsp-response"),
601 limit: MAX_OCSP_BYTES,
602 allow_http: true,
606 remaining_requests: usize::MAX,
607 },
608 NetworkNeed::DidDocument { url, .. } => FetchRequest {
609 url,
610 method: FetchMethod::Get,
611 body: None,
612 content_type: None,
613 accept: Some("application/did+json, application/json"),
614 limit: MAX_DID_DOCUMENT_BYTES,
615 allow_http: false,
616 remaining_requests: usize::MAX,
617 },
618 NetworkNeed::ExternalData { uri, .. } => FetchRequest {
619 url: uri,
620 method: FetchMethod::Get,
621 body: None,
622 content_type: None,
623 accept: None,
624 limit: MAX_EXTERNAL_DATA_BYTES,
625 allow_http: false,
626 remaining_requests: usize::MAX,
627 },
628 };
629
630 let (outcome, detail) = match fetcher.fetch(&request) {
631 Ok(FetchOk {
632 bytes,
633 media_type,
634 requests_used,
635 }) => {
636 budget = budget.saturating_sub(requests_used.max(1));
637 accept_evidence(&mut evidence, need, bytes, media_type)
638 }
639 Err(FetchErr {
640 blocked,
641 received_response,
642 detail,
643 requests_used,
644 ..
645 }) => {
646 budget = budget.saturating_sub(requests_used.max(1));
647 if let NetworkNeed::Ocsp {
648 certificate_sha256_hex,
649 ..
650 } = need
651 {
652 if received_response {
653 evidence
654 .ocsp_responses
655 .insert(certificate_sha256_hex.clone(), String::new());
656 } else if !blocked {
657 push_unreachable(&mut evidence, certificate_sha256_hex);
658 }
659 }
660 if let NetworkNeed::ExternalData { uri, .. } = need {
661 if !evidence
662 .external_data_unreachable
663 .iter()
664 .any(|existing| existing == uri)
665 {
666 evidence.external_data_unreachable.push(uri.clone());
667 }
668 }
669 (
670 if blocked { "blocked" } else { "failed" }.to_string(),
671 detail,
672 )
673 }
674 };
675 requests.push(NetworkRequest {
676 purpose,
677 url,
678 outcome,
679 detail,
680 });
681 }
682
683 (requests, evidence)
684}
685
686fn push_unreachable(evidence: &mut OnlineEvidence, certificate_sha256_hex: &str) {
687 if !evidence
688 .ocsp_unreachable
689 .iter()
690 .any(|existing| existing == certificate_sha256_hex)
691 {
692 evidence
693 .ocsp_unreachable
694 .push(certificate_sha256_hex.to_string());
695 }
696}
697
698fn accept_evidence(
700 evidence: &mut OnlineEvidence,
701 need: &NetworkNeed,
702 bytes: Vec<u8>,
703 media_type: Option<String>,
704) -> (String, String) {
705 let length = bytes.len();
706 match need {
707 NetworkNeed::RemoteManifest { .. } => {
708 evidence.manifest_store = Some(bytes);
709 ("fetched".to_string(), format!("{length} bytes"))
710 }
711 NetworkNeed::Ocsp {
712 certificate_sha256_hex,
713 ..
714 } => {
715 evidence
716 .ocsp_responses
717 .insert(certificate_sha256_hex.clone(), base64_encode(&bytes));
718 ("fetched".to_string(), format!("{length} bytes"))
719 }
720 NetworkNeed::DidDocument { did, .. } => match serde_json::from_slice::<Value>(&bytes) {
721 Ok(document) => {
722 evidence.did_documents.insert(did.clone(), document);
723 ("fetched".to_string(), format!("{length} bytes"))
724 }
725 Err(error) => (
726 "failed".to_string(),
727 format!("the DID document is not JSON: {error}"),
728 ),
729 },
730 NetworkNeed::ExternalData { uri, .. } => {
731 evidence
732 .external_data
733 .insert(uri.clone(), base64_encode(&bytes));
734 if let Some(media_type) = media_type {
735 evidence
736 .external_data_media_types
737 .insert(uri.clone(), media_type);
738 }
739 ("fetched".to_string(), format!("{length} bytes"))
740 }
741 }
742}
743
744fn split_url(url: &str) -> Option<(&str, &str, &str)> {
751 let (scheme, remainder) = url.split_once("://")?;
752 if scheme.is_empty() || remainder.is_empty() {
753 return None;
754 }
755 let end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len());
756 let (authority, rest) = remainder.split_at(end);
757 if authority.is_empty() {
758 return None;
759 }
760 Some((scheme, authority, rest))
761}
762
763fn host_of(url: &str) -> String {
765 let Some((_, authority, _)) = split_url(url) else {
766 return url.to_string();
767 };
768 let authority = authority
769 .rsplit_once('@')
770 .map_or(authority, |(_, host)| host);
771 if let Some(end) = authority.find(']') {
772 return authority[..=end].to_string();
773 }
774 authority
775 .split_once(':')
776 .map_or(authority, |(host, _)| host)
777 .to_string()
778}
779
780fn base64_encode(input: &[u8]) -> String {
787 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
788 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
789 for chunk in input.chunks(3) {
790 let b0 = u32::from(chunk[0]);
791 let b1 = chunk.get(1).copied().map_or(0, u32::from);
792 let b2 = chunk.get(2).copied().map_or(0, u32::from);
793 let triple = (b0 << 16) | (b1 << 8) | b2;
794 out.push(ALPHABET[(triple >> 18) as usize & 0x3f] as char);
795 out.push(ALPHABET[(triple >> 12) as usize & 0x3f] as char);
796 out.push(if chunk.len() > 1 {
797 ALPHABET[(triple >> 6) as usize & 0x3f] as char
798 } else {
799 '='
800 });
801 out.push(if chunk.len() > 2 {
802 ALPHABET[triple as usize & 0x3f] as char
803 } else {
804 '='
805 });
806 }
807 out
808}
809
810#[cfg(feature = "online")]
815pub(crate) fn new_fetcher(allow_private_networks: bool) -> Box<dyn Fetcher> {
816 Box::new(native::NativeFetcher::new(allow_private_networks))
817}
818
819#[cfg(not(feature = "online"))]
820pub(crate) fn new_fetcher(_allow_private_networks: bool) -> Box<dyn Fetcher> {
821 Box::new(DisabledFetcher)
822}
823
824#[cfg(not(feature = "online"))]
827struct DisabledFetcher;
828
829#[cfg(not(feature = "online"))]
830impl Fetcher for DisabledFetcher {
831 fn fetch(&self, _request: &FetchRequest<'_>) -> FetchResult {
832 Err(blocked(
833 "this build was compiled without the online feature",
834 0,
835 ))
836 }
837}
838
839#[cfg(feature = "online")]
840pub(crate) mod native {
841 use std::io::Read;
842 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
843 use std::time::Duration;
844 use std::{io, net};
845
846 use super::{
847 blocked, split_url, FetchErr, FetchMethod, FetchOk, FetchRequest, FetchResult, Fetcher,
848 };
849
850 const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
851 const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
852 const USER_AGENT: &str = concat!("encypher-c2pa/", env!("CARGO_PKG_VERSION"));
853 const BLOCKED_ADDRESS_MARKER: &str = "address is not permitted";
856
857 fn failed(detail: impl Into<String>, requests_used: usize, received_bytes: usize) -> FetchErr {
858 FetchErr {
859 blocked: false,
860 received_response: false,
863 detail: detail.into(),
864 requests_used,
865 received_bytes,
866 }
867 }
868
869 fn oversized(
870 detail: impl Into<String>,
871 requests_used: usize,
872 received_bytes: usize,
873 ) -> FetchErr {
874 FetchErr {
875 blocked: true,
876 received_response: true,
877 detail: detail.into(),
878 requests_used,
879 received_bytes,
880 }
881 }
882
883 pub(crate) fn blocked_reason(address: IpAddr) -> Option<&'static str> {
888 match address {
889 IpAddr::V4(address) => blocked_reason_v4(address),
890 IpAddr::V6(address) => blocked_reason_v6(address),
891 }
892 }
893
894 fn blocked_reason_v4(address: Ipv4Addr) -> Option<&'static str> {
895 let [a, b, c, _] = address.octets();
896 Some(match (a, b, c) {
897 (0, _, _) => "unspecified network",
898 (10, _, _) => "private network",
899 (100, 64..=127, _) => "carrier-grade NAT",
900 (127, _, _) => "loopback",
901 (169, 254, _) => "link-local, including the cloud metadata service",
902 (172, 16..=31, _) => "private network",
903 (192, 0, 0) => "IETF protocol assignment",
904 (192, 0, 2) => "documentation",
905 (192, 168, _) => "private network",
906 (198, 18..=19, _) => "benchmarking",
907 (198, 51, 100) => "documentation",
908 (203, 0, 113) => "documentation",
909 (224..=239, _, _) => "multicast",
910 (240..=255, _, _) => "reserved or broadcast",
911 _ => return None,
912 })
913 }
914
915 fn blocked_reason_v6(address: Ipv6Addr) -> Option<&'static str> {
916 if let Some(embedded) = embedded_ipv4(address) {
919 if let Some(reason) = blocked_reason_v4(embedded) {
920 return Some(reason);
921 }
922 }
923 let segments = address.segments();
924 if address.is_unspecified() {
925 return Some("unspecified address");
926 }
927 if address.is_loopback() {
928 return Some("loopback");
929 }
930 if address.is_multicast() {
931 return Some("multicast");
932 }
933 Some(match segments[0] {
934 first if first & 0xfe00 == 0xfc00 => "unique local",
935 first if first & 0xffc0 == 0xfe80 => "link-local",
936 0x2001 if segments[1] == 0x0db8 => "documentation",
937 0x0100 if segments[1] == 0 && segments[2] == 0 && segments[3] == 0 => "discard-only",
938 _ => return None,
939 })
940 }
941
942 fn embedded_ipv4(address: Ipv6Addr) -> Option<Ipv4Addr> {
943 let segments = address.segments();
944 let octets = address.octets();
945 let tail = Ipv4Addr::new(octets[12], octets[13], octets[14], octets[15]);
946 if segments[0..5] == [0, 0, 0, 0, 0] && (segments[5] == 0xffff || segments[5] == 0) {
948 return Some(tail);
949 }
950 if segments[0] == 0x2002 {
952 return Some(Ipv4Addr::new(octets[2], octets[3], octets[4], octets[5]));
953 }
954 if segments[0] == 0x0064 && segments[1] == 0xff9b {
956 return Some(tail);
957 }
958 None
959 }
960
961 pub(crate) struct FilteringResolver {
968 pub(crate) allow_private_networks: bool,
969 }
970
971 impl ureq::Resolver for FilteringResolver {
972 fn resolve(&self, netloc: &str) -> io::Result<Vec<SocketAddr>> {
973 let resolved: Vec<SocketAddr> = netloc.to_socket_addrs()?.collect();
974 if self.allow_private_networks {
975 return Ok(resolved);
976 }
977 let mut refused: Option<(net::IpAddr, &'static str)> = None;
978 let permitted: Vec<SocketAddr> = resolved
979 .into_iter()
980 .filter(|address| match blocked_reason(address.ip()) {
981 Some(reason) => {
982 refused.get_or_insert((address.ip(), reason));
983 false
984 }
985 None => true,
986 })
987 .collect();
988 if permitted.is_empty() {
989 let detail = match refused {
990 Some((address, reason)) => format!(
991 "{netloc} resolves to {address}, a {reason} {BLOCKED_ADDRESS_MARKER}"
992 ),
993 None => format!("{netloc} resolves to no address"),
994 };
995 return Err(io::Error::new(io::ErrorKind::PermissionDenied, detail));
996 }
997 Ok(permitted)
998 }
999 }
1000
1001 pub(crate) struct NativeFetcher {
1002 agent: ureq::Agent,
1003 intranet: bool,
1008 }
1009
1010 impl NativeFetcher {
1011 pub(crate) fn new(allow_private_networks: bool) -> Self {
1012 Self {
1013 agent: ureq::AgentBuilder::new()
1014 .timeout_connect(CONNECT_TIMEOUT)
1015 .timeout(REQUEST_TIMEOUT)
1016 .redirects(0)
1020 .user_agent(USER_AGENT)
1021 .resolver(FilteringResolver {
1022 allow_private_networks,
1023 })
1024 .build(),
1025 intranet: allow_private_networks,
1026 }
1027 }
1028 }
1029
1030 impl Fetcher for NativeFetcher {
1031 fn fetch(&self, request: &FetchRequest<'_>) -> FetchResult {
1032 if request.remaining_requests == 0 {
1033 return Err(blocked(
1034 "the request budget is spent before this request",
1035 0,
1036 ));
1037 }
1038 let url = request.url;
1039 match split_url(url) {
1040 None => return Err(blocked(format!("{url} is not an absolute URL"), 0)),
1041 Some((scheme, _, _)) if scheme.eq_ignore_ascii_case("https") => {}
1042 Some((scheme, _, _))
1043 if (request.allow_http || self.intranet)
1044 && scheme.eq_ignore_ascii_case("http") => {}
1045 Some((scheme, _, _)) => {
1046 return Err(blocked(
1047 format!("{scheme} is not allowed for this request; https is required"),
1048 0,
1049 ))
1050 }
1051 }
1052
1053 let used = 1;
1054 let mut call = match request.method {
1055 FetchMethod::Get => self.agent.get(url),
1056 FetchMethod::Post => self.agent.post(url),
1057 };
1058 if let Some(content_type) = request.content_type {
1059 call = call.set("content-type", content_type);
1060 }
1061 if let Some(accept) = request.accept {
1062 call = call.set("accept", accept);
1063 }
1064 let response = match request.body {
1065 Some(body) => call.send_bytes(body),
1066 None => call.call(),
1067 };
1068 let response = match response {
1069 Ok(response) => response,
1070 Err(ureq::Error::Status(status, response)) if (300..400).contains(&status) => {
1071 let location = response.header("location").unwrap_or("<missing>");
1072 let detail = if request.remaining_requests <= used {
1073 format!("redirect to {location} refused: the request budget is spent")
1074 } else {
1075 format!("redirect to {location} refused")
1076 };
1077 return Err(blocked(detail, used));
1078 }
1079 Err(ureq::Error::Status(status, _)) => {
1080 return Err(failed(
1081 format!("the server answered HTTP {status}"),
1082 used,
1083 0,
1084 ))
1085 }
1086 Err(ureq::Error::Transport(transport)) => {
1087 let detail = transport.to_string();
1088 return Err(if detail.contains(BLOCKED_ADDRESS_MARKER) {
1089 blocked(detail, used)
1090 } else {
1091 failed(detail, used, 0)
1092 });
1093 }
1094 };
1095 if (300..400).contains(&response.status()) {
1096 let location = response.header("location").unwrap_or("<missing>");
1097 let detail = if request.remaining_requests <= used {
1098 format!("redirect to {location} refused: the request budget is spent")
1099 } else {
1100 format!("redirect to {location} refused")
1101 };
1102 return Err(blocked(detail, used));
1103 }
1104 read_body(response, request.limit, used)
1105 }
1106 }
1107
1108 fn read_body(response: ureq::Response, limit: usize, used: usize) -> FetchResult {
1109 let media_type = response
1110 .header("content-type")
1111 .and_then(|value| value.split(';').next())
1112 .map(str::trim)
1113 .filter(|value| !value.is_empty())
1114 .map(str::to_ascii_lowercase);
1115 let mut bytes = Vec::new();
1116 let capped = u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1);
1117 if let Err(error) = response.into_reader().take(capped).read_to_end(&mut bytes) {
1118 let received_bytes = bytes.len();
1119 return Err(failed(error.to_string(), used, received_bytes));
1120 }
1121 if bytes.len() > limit {
1122 let received_bytes = bytes.len();
1123 return Err(oversized(
1124 format!("the response is larger than the {limit} byte limit"),
1125 used,
1126 received_bytes,
1127 ));
1128 }
1129 Ok(FetchOk {
1130 bytes,
1131 media_type,
1132 requests_used: used,
1133 })
1134 }
1135}
1136
1137mod tests;