use core::fmt;
use std::time::Duration;
use super::{FetchError, Target, get_inner, parse_url, url_for_message};
#[derive(Debug, Clone, Copy)]
pub struct RedirectPolicy {
from_host: &'static str,
to_hosts: &'static [&'static str],
https_only: bool,
}
impl RedirectPolicy {
pub const GITHUB_RELEASES: Self = Self {
from_host: "github.com",
to_hosts: &[
"release-assets.githubusercontent.com",
"objects.githubusercontent.com",
"github-releases.githubusercontent.com",
],
https_only: true,
};
#[cfg(test)]
pub(crate) const fn loopback(
from_host: &'static str,
to_hosts: &'static [&'static str],
) -> Self {
Self {
from_host,
to_hosts,
https_only: false,
}
}
fn admit_first(&self, url: &str) -> Result<Target, AssetError> {
let target = parse_url(url)?;
if self.https_only && !target.https {
return Err(AssetError::NotHttps {
url: url_for_message(url).to_owned(),
});
}
Ok(target)
}
pub(crate) fn admit_redirect(
&self,
from: &Target,
location: &str,
) -> Result<Target, AssetError> {
let next = parse_url(location)?;
if self.https_only && !next.https {
return Err(AssetError::NotHttps {
url: location.to_owned(),
});
}
let allowed = from.host.eq_ignore_ascii_case(self.from_host)
&& self
.to_hosts
.iter()
.any(|host| next.host.eq_ignore_ascii_case(host));
if !allowed {
return Err(AssetError::OffHost {
location: location.to_owned(),
});
}
Ok(next)
}
}
#[derive(Debug)]
pub enum AssetError {
NotHttps {
url: String,
},
OffHost {
location: String,
},
SecondRedirect {
location: String,
},
Fetch(FetchError),
}
impl fmt::Display for AssetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotHttps { url } => write!(
f,
"{url} is not https; shep downloads a binary only over https"
),
Self::OffHost { location } => write!(
f,
"the download was redirected to {location}, which is not GitHub's release asset \
host; shep follows one redirect, from github.com to that host"
),
Self::SecondRedirect { location } => write!(
f,
"the download was redirected a second time, to {location}; shep follows one"
),
Self::Fetch(source) => source.fmt(f),
}
}
}
impl core::error::Error for AssetError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Fetch(source) => Some(source),
Self::NotHttps { .. } | Self::OffHost { .. } | Self::SecondRedirect { .. } => None,
}
}
}
impl From<FetchError> for AssetError {
fn from(source: FetchError) -> Self {
Self::Fetch(source)
}
}
pub async fn get_asset(
url: &str,
policy: &RedirectPolicy,
limit: usize,
timeout: Duration,
) -> Result<Vec<u8>, AssetError> {
match tokio::time::timeout(timeout, follow(url, policy, limit)).await {
Ok(result) => result,
Err(_elapsed) => Err(AssetError::Fetch(FetchError::Timeout)),
}
}
async fn follow(url: &str, policy: &RedirectPolicy, limit: usize) -> Result<Vec<u8>, AssetError> {
let first = policy.admit_first(url)?;
let location = match get_inner(&first, limit).await {
Err(FetchError::Redirect { location }) => location,
other => return Ok(other?),
};
let second = policy.admit_redirect(&first, &location)?;
match get_inner(&second, limit).await {
Err(FetchError::Redirect { location }) => Err(AssetError::SecondRedirect { location }),
other => Ok(other?),
}
}