use anyhow::{anyhow, Context, Error, Result};
use fehler::{throw, throws};
use std::fs::File;
use std::io::prelude::*;
use url::Url;
#[derive(Debug, Copy, Clone)]
pub enum ResourceAccess {
LocalOnly,
RemoteAllowed,
}
impl ResourceAccess {
pub fn permits(self, url: &Url) -> bool {
match self {
ResourceAccess::LocalOnly if is_local(url) => true,
ResourceAccess::RemoteAllowed => true,
_ => false,
}
}
}
fn is_local(url: &Url) -> bool {
url.scheme() == "file" && url.to_file_path().is_ok()
}
#[cfg(feature = "reqwest")]
#[throws]
fn fetch_http(url: &Url) -> Vec<u8> {
let mut response =
reqwest::blocking::get(url.clone()).with_context(|| format!("Failed to GET {}", url))?;
if response.status().is_success() {
let mut buffer = Vec::new();
response
.read_to_end(&mut buffer)
.with_context(|| format!("Failed to read from URL {}", url))?;
buffer
} else {
throw!(anyhow!(
"HTTP error status {} by GET {}",
response.status(),
url
))
}
}
#[cfg(not(feature = "reqwest"))]
#[throws]
fn fetch_http(url: &Url) -> Vec<u8> {
let output = std::process::Command::new("curl")
.arg("-fsSL")
.arg(url.to_string())
.output()
.with_context(|| format!("curl -fsSL {} failed to spawn", url))?;
if output.status.success() {
output.stdout
} else {
throw!(anyhow!(
"curl -fsSL {} failed: {}",
url,
String::from_utf8_lossy(&output.stderr)
))
}
}
pub fn read_url(url: &Url, access: ResourceAccess) -> Result<Vec<u8>> {
if !access.permits(url) {
throw!(anyhow!(
"Access denied to URL {} by policy {:?}",
url,
access
))
}
match url.scheme() {
"file" => match url.to_file_path() {
Ok(path) => {
let mut buffer = Vec::new();
File::open(path)
.with_context(|| format!("Failed to open file at {}", url))?
.read_to_end(&mut buffer)
.with_context(|| format!("Failed to read from file at {}", url))?;
Ok(buffer)
}
Err(_) => Err(anyhow!("Cannot convert URL {} to file path", url)),
},
"http" | "https" => fetch_http(url),
_ => Err(anyhow!("Cannot read from URL {}, protocol not supported")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
#[cfg(unix)]
fn resource_access_permits_local_resource() {
let resource = Url::parse("file:///foo/bar").unwrap();
assert!(ResourceAccess::LocalOnly.permits(&resource));
assert!(ResourceAccess::RemoteAllowed.permits(&resource));
}
#[test]
#[cfg(unix)]
fn resource_access_permits_remote_file_url() {
let resource = Url::parse("file://example.com/foo/bar").unwrap();
assert!(!ResourceAccess::LocalOnly.permits(&resource));
assert!(ResourceAccess::RemoteAllowed.permits(&resource));
}
#[test]
fn resource_access_permits_https_url() {
let resource = Url::parse("https:///foo/bar").unwrap();
assert!(!ResourceAccess::LocalOnly.permits(&resource));
assert!(ResourceAccess::RemoteAllowed.permits(&resource));
}
#[test]
fn read_url_with_http_url_fails_if_local_only_access() {
let url = "https://eu.httpbin.org/status/404"
.parse::<url::Url>()
.unwrap();
let error = read_url(&url, ResourceAccess::LocalOnly)
.unwrap_err()
.to_string();
assert_eq!(
error,
"Access denied to URL https://eu.httpbin.org/status/404 by policy LocalOnly"
);
}
#[test]
fn read_url_with_http_url_fails_when_status_404() {
let url = "https://eu.httpbin.org/status/404"
.parse::<url::Url>()
.unwrap();
let result = read_url(&url, ResourceAccess::RemoteAllowed);
assert!(result.is_err(), "Unexpected success: {:?}", result);
let error = result.unwrap_err().to_string();
if cfg!(feature = "reqwest") {
assert_eq!(
error,
"HTTP error status 404 Not Found by GET https://eu.httpbin.org/status/404"
)
} else {
assert!(
error.contains("curl -fsSL https://eu.httpbin.org/status/404 failed:"),
"Error did not contain expected string: {}",
error
);
assert!(
error.contains("URL returned error: 404"),
"Error did not contain expected string: {}",
error
);
}
}
#[test]
fn read_url_with_http_url_returns_content_when_status_200() {
let url = "https://eu.httpbin.org/bytes/100"
.parse::<url::Url>()
.unwrap();
let result = read_url(&url, ResourceAccess::RemoteAllowed);
assert!(result.is_ok(), "Unexpected error: {:?}", result);
assert_eq!(result.unwrap().len(), 100);
}
}