use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::online_consent::{
environment_online, interactive_online_consent_allowed, prompt_for_online_consent,
saved_online_preference, OnlinePreference,
};
use crate::VerifyOptions;
const MAX_MANIFEST_BYTES: usize = 64 * 1024 * 1024;
const MAX_OCSP_BYTES: usize = 64 * 1024;
const MAX_DID_DOCUMENT_BYTES: usize = 256 * 1024;
const MAX_EXTERNAL_DATA_BYTES: usize = 64 * 1024 * 1024;
const MAX_REQUESTS: usize = 16;
const MAX_RECEIVED_BYTES: usize = 64 * 1024 * 1024;
pub(crate) use crate::c2pa_validate::{NetworkNeed, OcspPurpose};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct NetworkReport {
pub enabled: bool,
pub needed: Vec<Value>,
pub requests: Vec<NetworkRequest>,
}
impl NetworkReport {
pub(crate) fn mark_unusable(&mut self, purpose: &str, detail: String) {
if let Some(request) = self
.requests
.iter_mut()
.find(|request| request.purpose == purpose && request.outcome == "fetched")
{
request.outcome = "failed".to_string();
request.detail = detail;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkRequest {
pub purpose: String,
pub url: String,
pub outcome: String,
pub detail: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct OnlineEvidence {
pub(crate) manifest_store: Option<Vec<u8>>,
pub(crate) ocsp_responses: HashMap<String, String>,
pub(crate) ocsp_unreachable: Vec<String>,
pub(crate) external_data: HashMap<String, String>,
pub(crate) external_data_media_types: HashMap<String, String>,
pub(crate) external_data_unreachable: Vec<String>,
pub(crate) did_documents: HashMap<String, Value>,
}
impl OnlineEvidence {
pub(crate) fn is_empty(&self) -> bool {
self.manifest_store.is_none()
&& self.ocsp_responses.is_empty()
&& self.ocsp_unreachable.is_empty()
&& self.external_data.is_empty()
&& self.external_data_media_types.is_empty()
&& self.external_data_unreachable.is_empty()
&& self.did_documents.is_empty()
}
}
impl NetworkNeed {
pub(crate) fn purpose_token(&self) -> &'static str {
match self {
Self::RemoteManifest { .. } => "remote_manifest",
Self::Ocsp {
purpose: OcspPurpose::ClaimSigner,
..
} => "ocsp.claim_signer",
Self::Ocsp { .. } => "ocsp.cawg_identity",
Self::DidDocument { .. } => "did_document",
Self::ExternalData { .. } => "external_data",
}
}
pub(crate) fn url(&self) -> &str {
match self {
Self::RemoteManifest { uri } => uri,
Self::Ocsp { responder_url, .. } => responder_url,
Self::DidDocument { url, .. } => url,
Self::ExternalData { uri, .. } => uri,
}
}
fn consent_line(&self) -> String {
let what = match self {
Self::RemoteManifest { .. } => "the manifest store this file points to".to_string(),
Self::Ocsp {
purpose: OcspPurpose::ClaimSigner,
..
} => "whether the signing certificate has been revoked".to_string(),
Self::Ocsp {
purpose: OcspPurpose::CawgIdentity { assertion_label },
..
} => format!("whether the identity certificate in {assertion_label} has been revoked"),
Self::DidDocument { did, .. } => format!("the issuer document for {did}"),
Self::ExternalData {
assertion_label, ..
} => format!("content stored outside the file, for {assertion_label}"),
};
format!("{what}, from {}", host_of(self.url()))
}
}
pub(crate) fn network_needs(output: &crate::c2pa_validate::VerifyOutput) -> Vec<NetworkNeed> {
output.network_needs.clone()
}
pub(crate) fn gather(
options: &VerifyOptions,
needs: &[NetworkNeed],
) -> (NetworkReport, OnlineEvidence) {
let needed: Vec<Value> = needs.iter().map(NetworkNeed::to_json).collect();
let enabled = resolve_online(options, needs);
if !enabled || needs.is_empty() {
return (
NetworkReport {
enabled,
needed,
requests: Vec::new(),
},
OnlineEvidence::default(),
);
}
let fetcher = new_fetcher(options.online_allow_private_networks);
let (requests, evidence) = fetch_all(fetcher.as_ref(), needs);
(
NetworkReport {
enabled: true,
needed,
requests,
},
evidence,
)
}
pub(crate) fn online_enabled(options: &VerifyOptions) -> bool {
resolve_online(options, &[])
}
pub(crate) fn resolve_online(options: &VerifyOptions, needs: &[NetworkNeed]) -> bool {
if let Some(explicit) = options.online {
return explicit;
}
match environment_online() {
Ok(Some(enabled)) => return enabled,
Ok(None) => {}
Err(_) => return false,
}
if !interactive_online_consent_allowed() || needs.is_empty() {
return false;
}
match saved_online_preference() {
Ok(Some(OnlinePreference::On)) => true,
Ok(Some(OnlinePreference::Off)) => false,
Ok(Some(OnlinePreference::Ask)) | Ok(None) => {
let lines: Vec<String> = needs.iter().map(NetworkNeed::consent_line).collect();
prompt_for_online_consent(&lines)
.ok()
.flatten()
.unwrap_or(false)
}
Err(_) => false,
}
}
pub(crate) fn apply_evidence(options: &VerifyOptions, evidence: &OnlineEvidence) -> VerifyOptions {
let mut next = options.clone();
next.online = Some(false);
if !evidence.did_documents.is_empty() {
let mut documents = next.cawg_did_documents.unwrap_or_default();
documents.extend(
evidence
.did_documents
.iter()
.map(|(did, document)| (did.clone(), document.clone())),
);
next.cawg_did_documents = Some(documents);
}
if !evidence.ocsp_responses.is_empty() {
let mut responses = next.ocsp_responses.unwrap_or_default();
responses.extend(evidence.ocsp_responses.clone());
next.ocsp_responses = Some(responses);
}
if !evidence.ocsp_unreachable.is_empty() {
let mut unreachable = next.ocsp_unreachable.unwrap_or_default();
unreachable.extend(evidence.ocsp_unreachable.iter().cloned());
next.ocsp_unreachable = Some(unreachable);
}
if !evidence.external_data.is_empty() {
let mut data = next.external_data.unwrap_or_default();
data.extend(evidence.external_data.clone());
next.external_data = Some(data);
}
if !evidence.external_data_media_types.is_empty() {
let mut media_types = next.external_data_media_types.unwrap_or_default();
media_types.extend(evidence.external_data_media_types.clone());
next.external_data_media_types = Some(media_types);
}
if !evidence.external_data_unreachable.is_empty() {
let mut unreachable = next.external_data_unreachable.unwrap_or_default();
unreachable.extend(evidence.external_data_unreachable.iter().cloned());
next.external_data_unreachable = Some(unreachable);
}
next
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FetchMethod {
Get,
Post,
}
#[cfg_attr(
not(feature = "online"),
allow(dead_code, reason = "only the native fetcher reads a request")
)]
pub(crate) struct FetchRequest<'a> {
pub(crate) url: &'a str,
pub(crate) method: FetchMethod,
pub(crate) body: Option<&'a [u8]>,
pub(crate) content_type: Option<&'a str>,
pub(crate) accept: Option<&'a str>,
pub(crate) limit: usize,
pub(crate) allow_http: bool,
pub(crate) remaining_requests: usize,
}
#[derive(Debug)]
pub(crate) struct FetchOk {
pub(crate) bytes: Vec<u8>,
pub(crate) media_type: Option<String>,
pub(crate) requests_used: usize,
}
#[derive(Debug)]
pub(crate) struct FetchErr {
pub(crate) blocked: bool,
pub(crate) received_response: bool,
pub(crate) detail: String,
pub(crate) requests_used: usize,
pub(crate) received_bytes: usize,
}
pub(crate) type FetchResult = Result<FetchOk, FetchErr>;
pub(crate) trait Fetcher {
fn fetch(&self, request: &FetchRequest<'_>) -> FetchResult;
}
fn blocked(detail: impl Into<String>, requests_used: usize) -> FetchErr {
FetchErr {
blocked: true,
received_response: false,
detail: detail.into(),
requests_used,
received_bytes: 0,
}
}
#[derive(Debug)]
struct FetchBudget {
requests: usize,
received_bytes: usize,
}
impl Default for FetchBudget {
fn default() -> Self {
Self {
requests: MAX_REQUESTS,
received_bytes: MAX_RECEIVED_BYTES,
}
}
}
pub(crate) struct FetchSession {
fetcher: Option<Box<dyn Fetcher>>,
allow_private_networks: bool,
budget: FetchBudget,
}
impl FetchSession {
pub(crate) fn new(allow_private_networks: bool) -> Self {
Self {
fetcher: None,
allow_private_networks,
budget: FetchBudget::default(),
}
}
#[cfg(test)]
pub(crate) fn with_fetcher(fetcher: Box<dyn Fetcher>) -> Self {
Self {
fetcher: Some(fetcher),
allow_private_networks: false,
budget: FetchBudget::default(),
}
}
pub(crate) fn fetch(&mut self, need: &NetworkNeed) -> (NetworkRequest, FetchResult) {
if let Some(exhausted) = budget_exhaustion(&self.budget, need) {
return exhausted;
}
let allow_private_networks = self.allow_private_networks;
let fetcher = self
.fetcher
.get_or_insert_with(|| new_fetcher(allow_private_networks));
fetch_one(fetcher.as_ref(), &mut self.budget, need)
}
}
fn request_for_need<'a>(
need: &'a NetworkNeed,
remaining_requests: usize,
remaining_bytes: usize,
) -> FetchRequest<'a> {
match need {
NetworkNeed::RemoteManifest { uri } => FetchRequest {
url: uri,
method: FetchMethod::Get,
body: None,
content_type: None,
accept: Some("application/c2pa"),
limit: MAX_MANIFEST_BYTES.min(remaining_bytes),
allow_http: false,
remaining_requests,
},
NetworkNeed::Ocsp {
responder_url,
request_der,
..
} => FetchRequest {
url: responder_url,
method: FetchMethod::Post,
body: Some(request_der),
content_type: Some("application/ocsp-request"),
accept: Some("application/ocsp-response"),
limit: MAX_OCSP_BYTES.min(remaining_bytes),
allow_http: true,
remaining_requests,
},
NetworkNeed::DidDocument { url, .. } => FetchRequest {
url,
method: FetchMethod::Get,
body: None,
content_type: None,
accept: Some("application/did+json, application/json"),
limit: MAX_DID_DOCUMENT_BYTES.min(remaining_bytes),
allow_http: false,
remaining_requests,
},
NetworkNeed::ExternalData { uri, .. } => FetchRequest {
url: uri,
method: FetchMethod::Get,
body: None,
content_type: None,
accept: None,
limit: MAX_EXTERNAL_DATA_BYTES.min(remaining_bytes),
allow_http: false,
remaining_requests,
},
}
}
fn budget_exhaustion(
budget: &FetchBudget,
need: &NetworkNeed,
) -> Option<(NetworkRequest, FetchResult)> {
let detail = if budget.requests == 0 {
format!("the {MAX_REQUESTS} request budget for one verification is spent")
} else if budget.received_bytes == 0 {
format!("the {MAX_RECEIVED_BYTES} byte response budget for one verification is spent")
} else {
return None;
};
Some((
NetworkRequest {
purpose: need.purpose_token().to_string(),
url: need.url().to_string(),
outcome: "skipped".to_string(),
detail: detail.clone(),
},
Err(blocked(detail, 0)),
))
}
fn fetch_one(
fetcher: &dyn Fetcher,
budget: &mut FetchBudget,
need: &NetworkNeed,
) -> (NetworkRequest, FetchResult) {
if let Some(exhausted) = budget_exhaustion(budget, need) {
return exhausted;
}
let purpose = need.purpose_token().to_string();
let url = need.url().to_string();
let request = request_for_need(need, budget.requests, budget.received_bytes);
let result = fetcher.fetch(&request);
let (requests_used, received_bytes, outcome, detail) = match &result {
Ok(response) => (
response.requests_used,
response.bytes.len(),
"fetched",
format!("{} bytes", response.bytes.len()),
),
Err(error) => (
error.requests_used,
error.received_bytes,
if error.blocked { "blocked" } else { "failed" },
error.detail.clone(),
),
};
budget.requests = budget.requests.saturating_sub(requests_used);
budget.received_bytes = budget.received_bytes.saturating_sub(received_bytes);
(
NetworkRequest {
purpose,
url,
outcome: outcome.to_string(),
detail,
},
result,
)
}
pub(crate) fn fetch_all(
fetcher: &dyn Fetcher,
needs: &[NetworkNeed],
) -> (Vec<NetworkRequest>, OnlineEvidence) {
let mut requests = Vec::with_capacity(needs.len());
let mut evidence = OnlineEvidence::default();
let mut budget = MAX_REQUESTS;
for need in needs {
let purpose = need.purpose_token().to_string();
let url = need.url().to_string();
if budget == 0 {
requests.push(NetworkRequest {
purpose,
url,
outcome: "skipped".to_string(),
detail: format!("the {MAX_REQUESTS} request budget for one verification is spent"),
});
continue;
}
let request = match need {
NetworkNeed::RemoteManifest { uri } => FetchRequest {
url: uri,
method: FetchMethod::Get,
body: None,
content_type: None,
accept: Some("application/c2pa"),
limit: MAX_MANIFEST_BYTES,
allow_http: false,
remaining_requests: usize::MAX,
},
NetworkNeed::Ocsp {
responder_url,
request_der,
..
} => FetchRequest {
url: responder_url,
method: FetchMethod::Post,
body: Some(request_der),
content_type: Some("application/ocsp-request"),
accept: Some("application/ocsp-response"),
limit: MAX_OCSP_BYTES,
allow_http: true,
remaining_requests: usize::MAX,
},
NetworkNeed::DidDocument { url, .. } => FetchRequest {
url,
method: FetchMethod::Get,
body: None,
content_type: None,
accept: Some("application/did+json, application/json"),
limit: MAX_DID_DOCUMENT_BYTES,
allow_http: false,
remaining_requests: usize::MAX,
},
NetworkNeed::ExternalData { uri, .. } => FetchRequest {
url: uri,
method: FetchMethod::Get,
body: None,
content_type: None,
accept: None,
limit: MAX_EXTERNAL_DATA_BYTES,
allow_http: false,
remaining_requests: usize::MAX,
},
};
let (outcome, detail) = match fetcher.fetch(&request) {
Ok(FetchOk {
bytes,
media_type,
requests_used,
}) => {
budget = budget.saturating_sub(requests_used.max(1));
accept_evidence(&mut evidence, need, bytes, media_type)
}
Err(FetchErr {
blocked,
received_response,
detail,
requests_used,
..
}) => {
budget = budget.saturating_sub(requests_used.max(1));
if let NetworkNeed::Ocsp {
certificate_sha256_hex,
..
} = need
{
if received_response {
evidence
.ocsp_responses
.insert(certificate_sha256_hex.clone(), String::new());
} else if !blocked {
push_unreachable(&mut evidence, certificate_sha256_hex);
}
}
if let NetworkNeed::ExternalData { uri, .. } = need {
if !evidence
.external_data_unreachable
.iter()
.any(|existing| existing == uri)
{
evidence.external_data_unreachable.push(uri.clone());
}
}
(
if blocked { "blocked" } else { "failed" }.to_string(),
detail,
)
}
};
requests.push(NetworkRequest {
purpose,
url,
outcome,
detail,
});
}
(requests, evidence)
}
fn push_unreachable(evidence: &mut OnlineEvidence, certificate_sha256_hex: &str) {
if !evidence
.ocsp_unreachable
.iter()
.any(|existing| existing == certificate_sha256_hex)
{
evidence
.ocsp_unreachable
.push(certificate_sha256_hex.to_string());
}
}
fn accept_evidence(
evidence: &mut OnlineEvidence,
need: &NetworkNeed,
bytes: Vec<u8>,
media_type: Option<String>,
) -> (String, String) {
let length = bytes.len();
match need {
NetworkNeed::RemoteManifest { .. } => {
evidence.manifest_store = Some(bytes);
("fetched".to_string(), format!("{length} bytes"))
}
NetworkNeed::Ocsp {
certificate_sha256_hex,
..
} => {
evidence
.ocsp_responses
.insert(certificate_sha256_hex.clone(), base64_encode(&bytes));
("fetched".to_string(), format!("{length} bytes"))
}
NetworkNeed::DidDocument { did, .. } => match serde_json::from_slice::<Value>(&bytes) {
Ok(document) => {
evidence.did_documents.insert(did.clone(), document);
("fetched".to_string(), format!("{length} bytes"))
}
Err(error) => (
"failed".to_string(),
format!("the DID document is not JSON: {error}"),
),
},
NetworkNeed::ExternalData { uri, .. } => {
evidence
.external_data
.insert(uri.clone(), base64_encode(&bytes));
if let Some(media_type) = media_type {
evidence
.external_data_media_types
.insert(uri.clone(), media_type);
}
("fetched".to_string(), format!("{length} bytes"))
}
}
}
fn split_url(url: &str) -> Option<(&str, &str, &str)> {
let (scheme, remainder) = url.split_once("://")?;
if scheme.is_empty() || remainder.is_empty() {
return None;
}
let end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len());
let (authority, rest) = remainder.split_at(end);
if authority.is_empty() {
return None;
}
Some((scheme, authority, rest))
}
fn host_of(url: &str) -> String {
let Some((_, authority, _)) = split_url(url) else {
return url.to_string();
};
let authority = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host);
if let Some(end) = authority.find(']') {
return authority[..=end].to_string();
}
authority
.split_once(':')
.map_or(authority, |(host, _)| host)
.to_string()
}
fn base64_encode(input: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = u32::from(chunk[0]);
let b1 = chunk.get(1).copied().map_or(0, u32::from);
let b2 = chunk.get(2).copied().map_or(0, u32::from);
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(triple >> 18) as usize & 0x3f] as char);
out.push(ALPHABET[(triple >> 12) as usize & 0x3f] as char);
out.push(if chunk.len() > 1 {
ALPHABET[(triple >> 6) as usize & 0x3f] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[triple as usize & 0x3f] as char
} else {
'='
});
}
out
}
#[cfg(feature = "online")]
pub(crate) fn new_fetcher(allow_private_networks: bool) -> Box<dyn Fetcher> {
Box::new(native::NativeFetcher::new(allow_private_networks))
}
#[cfg(not(feature = "online"))]
pub(crate) fn new_fetcher(_allow_private_networks: bool) -> Box<dyn Fetcher> {
Box::new(DisabledFetcher)
}
#[cfg(not(feature = "online"))]
struct DisabledFetcher;
#[cfg(not(feature = "online"))]
impl Fetcher for DisabledFetcher {
fn fetch(&self, _request: &FetchRequest<'_>) -> FetchResult {
Err(blocked(
"this build was compiled without the online feature",
0,
))
}
}
#[cfg(feature = "online")]
pub(crate) mod native {
use std::io::Read;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
use std::time::Duration;
use std::{io, net};
use super::{
blocked, split_url, FetchErr, FetchMethod, FetchOk, FetchRequest, FetchResult, Fetcher,
};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
const USER_AGENT: &str = concat!("encypher-c2pa/", env!("CARGO_PKG_VERSION"));
const BLOCKED_ADDRESS_MARKER: &str = "address is not permitted";
fn failed(detail: impl Into<String>, requests_used: usize, received_bytes: usize) -> FetchErr {
FetchErr {
blocked: false,
received_response: false,
detail: detail.into(),
requests_used,
received_bytes,
}
}
fn oversized(
detail: impl Into<String>,
requests_used: usize,
received_bytes: usize,
) -> FetchErr {
FetchErr {
blocked: true,
received_response: true,
detail: detail.into(),
requests_used,
received_bytes,
}
}
pub(crate) fn blocked_reason(address: IpAddr) -> Option<&'static str> {
match address {
IpAddr::V4(address) => blocked_reason_v4(address),
IpAddr::V6(address) => blocked_reason_v6(address),
}
}
fn blocked_reason_v4(address: Ipv4Addr) -> Option<&'static str> {
let [a, b, c, _] = address.octets();
Some(match (a, b, c) {
(0, _, _) => "unspecified network",
(10, _, _) => "private network",
(100, 64..=127, _) => "carrier-grade NAT",
(127, _, _) => "loopback",
(169, 254, _) => "link-local, including the cloud metadata service",
(172, 16..=31, _) => "private network",
(192, 0, 0) => "IETF protocol assignment",
(192, 0, 2) => "documentation",
(192, 168, _) => "private network",
(198, 18..=19, _) => "benchmarking",
(198, 51, 100) => "documentation",
(203, 0, 113) => "documentation",
(224..=239, _, _) => "multicast",
(240..=255, _, _) => "reserved or broadcast",
_ => return None,
})
}
fn blocked_reason_v6(address: Ipv6Addr) -> Option<&'static str> {
if let Some(embedded) = embedded_ipv4(address) {
if let Some(reason) = blocked_reason_v4(embedded) {
return Some(reason);
}
}
let segments = address.segments();
if address.is_unspecified() {
return Some("unspecified address");
}
if address.is_loopback() {
return Some("loopback");
}
if address.is_multicast() {
return Some("multicast");
}
Some(match segments[0] {
first if first & 0xfe00 == 0xfc00 => "unique local",
first if first & 0xffc0 == 0xfe80 => "link-local",
0x2001 if segments[1] == 0x0db8 => "documentation",
0x0100 if segments[1] == 0 && segments[2] == 0 && segments[3] == 0 => "discard-only",
_ => return None,
})
}
fn embedded_ipv4(address: Ipv6Addr) -> Option<Ipv4Addr> {
let segments = address.segments();
let octets = address.octets();
let tail = Ipv4Addr::new(octets[12], octets[13], octets[14], octets[15]);
if segments[0..5] == [0, 0, 0, 0, 0] && (segments[5] == 0xffff || segments[5] == 0) {
return Some(tail);
}
if segments[0] == 0x2002 {
return Some(Ipv4Addr::new(octets[2], octets[3], octets[4], octets[5]));
}
if segments[0] == 0x0064 && segments[1] == 0xff9b {
return Some(tail);
}
None
}
pub(crate) struct FilteringResolver {
pub(crate) allow_private_networks: bool,
}
impl ureq::Resolver for FilteringResolver {
fn resolve(&self, netloc: &str) -> io::Result<Vec<SocketAddr>> {
let resolved: Vec<SocketAddr> = netloc.to_socket_addrs()?.collect();
if self.allow_private_networks {
return Ok(resolved);
}
let mut refused: Option<(net::IpAddr, &'static str)> = None;
let permitted: Vec<SocketAddr> = resolved
.into_iter()
.filter(|address| match blocked_reason(address.ip()) {
Some(reason) => {
refused.get_or_insert((address.ip(), reason));
false
}
None => true,
})
.collect();
if permitted.is_empty() {
let detail = match refused {
Some((address, reason)) => format!(
"{netloc} resolves to {address}, a {reason} {BLOCKED_ADDRESS_MARKER}"
),
None => format!("{netloc} resolves to no address"),
};
return Err(io::Error::new(io::ErrorKind::PermissionDenied, detail));
}
Ok(permitted)
}
}
pub(crate) struct NativeFetcher {
agent: ureq::Agent,
intranet: bool,
}
impl NativeFetcher {
pub(crate) fn new(allow_private_networks: bool) -> Self {
Self {
agent: ureq::AgentBuilder::new()
.timeout_connect(CONNECT_TIMEOUT)
.timeout(REQUEST_TIMEOUT)
.redirects(0)
.user_agent(USER_AGENT)
.resolver(FilteringResolver {
allow_private_networks,
})
.build(),
intranet: allow_private_networks,
}
}
}
impl Fetcher for NativeFetcher {
fn fetch(&self, request: &FetchRequest<'_>) -> FetchResult {
if request.remaining_requests == 0 {
return Err(blocked(
"the request budget is spent before this request",
0,
));
}
let url = request.url;
match split_url(url) {
None => return Err(blocked(format!("{url} is not an absolute URL"), 0)),
Some((scheme, _, _)) if scheme.eq_ignore_ascii_case("https") => {}
Some((scheme, _, _))
if (request.allow_http || self.intranet)
&& scheme.eq_ignore_ascii_case("http") => {}
Some((scheme, _, _)) => {
return Err(blocked(
format!("{scheme} is not allowed for this request; https is required"),
0,
))
}
}
let used = 1;
let mut call = match request.method {
FetchMethod::Get => self.agent.get(url),
FetchMethod::Post => self.agent.post(url),
};
if let Some(content_type) = request.content_type {
call = call.set("content-type", content_type);
}
if let Some(accept) = request.accept {
call = call.set("accept", accept);
}
let response = match request.body {
Some(body) => call.send_bytes(body),
None => call.call(),
};
let response = match response {
Ok(response) => response,
Err(ureq::Error::Status(status, response)) if (300..400).contains(&status) => {
let location = response.header("location").unwrap_or("<missing>");
let detail = if request.remaining_requests <= used {
format!("redirect to {location} refused: the request budget is spent")
} else {
format!("redirect to {location} refused")
};
return Err(blocked(detail, used));
}
Err(ureq::Error::Status(status, _)) => {
return Err(failed(
format!("the server answered HTTP {status}"),
used,
0,
))
}
Err(ureq::Error::Transport(transport)) => {
let detail = transport.to_string();
return Err(if detail.contains(BLOCKED_ADDRESS_MARKER) {
blocked(detail, used)
} else {
failed(detail, used, 0)
});
}
};
if (300..400).contains(&response.status()) {
let location = response.header("location").unwrap_or("<missing>");
let detail = if request.remaining_requests <= used {
format!("redirect to {location} refused: the request budget is spent")
} else {
format!("redirect to {location} refused")
};
return Err(blocked(detail, used));
}
read_body(response, request.limit, used)
}
}
fn read_body(response: ureq::Response, limit: usize, used: usize) -> FetchResult {
let media_type = response
.header("content-type")
.and_then(|value| value.split(';').next())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_ascii_lowercase);
let mut bytes = Vec::new();
let capped = u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1);
if let Err(error) = response.into_reader().take(capped).read_to_end(&mut bytes) {
let received_bytes = bytes.len();
return Err(failed(error.to_string(), used, received_bytes));
}
if bytes.len() > limit {
let received_bytes = bytes.len();
return Err(oversized(
format!("the response is larger than the {limit} byte limit"),
used,
received_bytes,
));
}
Ok(FetchOk {
bytes,
media_type,
requests_used: used,
})
}
}
mod tests;