Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use crate::modules::AppState;
use crate::modules::cache::manager::CacheManager;
use crate::modules::proxy::fallback::FallbackImage;
use crate::modules::proxy::fetchable::Fetchable;
use crate::modules::proxy::retry::RetryFetcher;
use crate::modules::proxy::router::SourceRouter;
use crate::modules::proxy::sources::http::HttpFetcher;
use crate::modules::proxy::sources::{
AliasSource, LocalSource, RetryFetcher, S3Source, SourceRouter,
};
use crate::modules::proxy::sources::{AliasSource, LocalSource, S3Source};
use crate::modules::security::allowlist::Allowlist;
use axum::Router;
use std::sync::Arc;
Expand Down Expand Up @@ -62,7 +62,14 @@ pub async fn router(
)
.with_private_ip_check(check_private),
);
Arc::new(AliasSource::new(aliases.clone(), alias_http))
let alias_s3 = s3.clone().map(|x| x as Arc<dyn Fetchable>);
let alias_local = local.clone().map(|x| x as Arc<dyn Fetchable>);
Arc::new(AliasSource::new(
aliases.clone(),
alias_http,
alias_s3,
alias_local,
))
});

let fetcher: Arc<dyn Fetchable> = Arc::new(RetryFetcher::new(
Expand Down
42 changes: 40 additions & 2 deletions src/common/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,13 @@ fn parse_url_aliases(s: &str) -> Option<HashMap<String, String>> {
tracing::warn!("URL_ALIASES: skipping entry {:?} with empty base URL", name);
return None;
}
if !base.starts_with("http://") && !base.starts_with("https://") {
let valid_scheme = base.starts_with("http://")
|| base.starts_with("https://")
|| base.starts_with("s3:/")
|| base.starts_with("local:/");
if !valid_scheme {
tracing::warn!(
"URL_ALIASES: skipping entry {:?} - base URL must be http:// or https://, got {:?}",
"URL_ALIASES: skipping entry {:?} - base must start with http://, https://, s3:/, or local:/, got {:?}",
name, base
);
return None;
Expand Down Expand Up @@ -701,6 +705,40 @@ mod tests {
assert!(map.contains_key("ok"));
}

#[test]
fn test_url_aliases_accepts_s3_base() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PP_PORT", "8080");
std::env::set_var("PP_APP_ENV", "development");
std::env::set_var("PP_URL_ALIASES", "thumbs=s3:/my-bucket/thumbnails");
}
let cfg = super::Configuration::new();
unsafe { std::env::remove_var("PP_URL_ALIASES") };
let map = cfg.url_aliases.clone().unwrap();
assert_eq!(
map.get("thumbs").map(String::as_str),
Some("s3:/my-bucket/thumbnails")
);
}

#[test]
fn test_url_aliases_accepts_local_base() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PP_PORT", "8080");
std::env::set_var("PP_APP_ENV", "development");
std::env::set_var("PP_URL_ALIASES", "assets=local:/var/www/static");
}
let cfg = super::Configuration::new();
unsafe { std::env::remove_var("PP_URL_ALIASES") };
let map = cfg.url_aliases.clone().unwrap();
assert_eq!(
map.get("assets").map(String::as_str),
Some("local:/var/www/static")
);
}

#[test]
fn test_url_aliases_all_invalid_is_none() {
let _guard = ENV_LOCK.lock().unwrap();
Expand Down
3 changes: 3 additions & 0 deletions src/modules/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ pub mod controller;
pub mod dto;
pub mod fallback;
pub mod fetchable;
pub mod retry;
pub mod router;
pub mod service;
pub mod sources;
pub mod video;
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ mod tests {
);
let mut map = std::collections::HashMap::new();
map.insert("mycdn".to_string(), server_uri.to_string());
Arc::new(AliasSource::new(map, http))
Arc::new(AliasSource::new(map, http, None, None))
}

#[tokio::test]
Expand Down
6 changes: 3 additions & 3 deletions src/modules/proxy/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ impl ProxyService {
let is_video = src_ct
.as_deref()
.map(|ct| ct.starts_with("video/"))
.unwrap_or_else(|| crate::modules::proxy::sources::video::is_video_magic(&src_bytes));
.unwrap_or_else(|| crate::modules::proxy::video::is_video_magic(&src_bytes));

if is_video
&& self
Expand All @@ -370,7 +370,7 @@ impl ProxyService {

if is_video {
use crate::modules::proxy::dto::params::SeekMode;
use crate::modules::proxy::sources::video::{extract_frame, probe_duration};
use crate::modules::proxy::video::{extract_frame, probe_duration};

let t_secs = match &params.seek {
None => 0.0,
Expand All @@ -392,7 +392,7 @@ impl ProxyService {
};

match extract_frame(&src_bytes, t_secs, &self.ffmpeg_path).await {
Ok(frame) => match crate::modules::proxy::sources::video::frame_to_png_bytes(frame) {
Ok(frame) => match crate::modules::proxy::video::frame_to_png_bytes(frame) {
Ok(png_bytes) => {
src_bytes = png_bytes;
src_ct = Some("image/png".to_string());
Expand Down
113 changes: 109 additions & 4 deletions src/modules/proxy/sources/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@ use std::sync::Arc;
pub struct AliasSource {
aliases: HashMap<String, String>,
http: Arc<HttpFetcher>,
s3: Option<Arc<dyn Fetchable>>,
local: Option<Arc<dyn Fetchable>>,
}

impl AliasSource {
pub fn new(aliases: HashMap<String, String>, http: Arc<HttpFetcher>) -> Self {
Self { aliases, http }
pub fn new(
aliases: HashMap<String, String>,
http: Arc<HttpFetcher>,
s3: Option<Arc<dyn Fetchable>>,
local: Option<Arc<dyn Fetchable>>,
) -> Self {
Self {
aliases,
http,
s3,
local,
}
}

fn resolve(&self, url: &str) -> Result<String, ProxyError> {
Expand All @@ -36,7 +48,23 @@ impl Fetchable for AliasSource {
resolved_url = resolved.as_str(),
"alias resolved"
);
self.http.fetch(&resolved).await
if resolved.starts_with("s3:/") {
self
.s3
.as_ref()
.ok_or_else(|| ProxyError::InvalidParams("S3 source not configured".to_string()))?
.fetch(&resolved)
.await
} else if resolved.starts_with("local:/") {
self
.local
.as_ref()
.ok_or_else(|| ProxyError::InvalidParams("local source not configured".to_string()))?
.fetch(&resolved)
.await
} else {
self.http.fetch(&resolved).await
}
}
}

Expand All @@ -56,7 +84,35 @@ mod tests {
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
AliasSource::new(map, http)
AliasSource::new(map, http, None, None)
}

struct StubFetcher {
response: Vec<u8>,
content_type: Option<String>,
}

#[async_trait::async_trait]
impl Fetchable for StubFetcher {
async fn fetch(&self, _url: &str) -> Result<(Vec<u8>, Option<String>), ProxyError> {
Ok((self.response.clone(), self.content_type.clone()))
}
}

fn make_alias_source_with_backends(
aliases: Vec<(&str, &str)>,
s3: Option<Arc<dyn Fetchable>>,
local: Option<Arc<dyn Fetchable>>,
) -> AliasSource {
let http = Arc::new(
HttpFetcher::new(10, 1_000_000, Arc::new(Allowlist::new(vec![])))
.with_private_ip_check(false),
);
let map = aliases
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
AliasSource::new(map, http, s3, local)
}

#[test]
Expand Down Expand Up @@ -117,6 +173,55 @@ mod tests {
);
}

#[tokio::test]
async fn test_fetch_dispatches_to_s3_when_resolved_url_starts_with_s3() {
let s3_stub: Arc<dyn Fetchable> = Arc::new(StubFetcher {
response: b"s3data".to_vec(),
content_type: Some("image/png".to_string()),
});
let source =
make_alias_source_with_backends(vec![("mybucket", "s3:/bucket-prefix")], Some(s3_stub), None);
let (bytes, ct) = source.fetch("mybucket:/img.png").await.unwrap();
assert_eq!(bytes, b"s3data");
assert_eq!(ct, Some("image/png".to_string()));
}

#[tokio::test]
async fn test_fetch_dispatches_to_local_when_resolved_url_starts_with_local() {
let local_stub: Arc<dyn Fetchable> = Arc::new(StubFetcher {
response: b"localdata".to_vec(),
content_type: None,
});
let source =
make_alias_source_with_backends(vec![("assets", "local:/uploads")], None, Some(local_stub));
let (bytes, ct) = source.fetch("assets:/photo.jpg").await.unwrap();
assert_eq!(bytes, b"localdata");
assert!(ct.is_none());
}

#[tokio::test]
async fn test_fetch_s3_alias_without_s3_configured_returns_error() {
let source =
make_alias_source_with_backends(vec![("mybucket", "s3:/bucket-prefix")], None, None);
let err = source.fetch("mybucket:/img.png").await.unwrap_err();
assert!(
matches!(&err, ProxyError::InvalidParams(m) if m.contains("S3")),
"unexpected: {:?}",
err
);
}

#[tokio::test]
async fn test_fetch_local_alias_without_local_configured_returns_error() {
let source = make_alias_source_with_backends(vec![("assets", "local:/uploads")], None, None);
let err = source.fetch("assets:/photo.jpg").await.unwrap_err();
assert!(
matches!(&err, ProxyError::InvalidParams(m) if m.contains("local")),
"unexpected: {:?}",
err
);
}

#[tokio::test]
async fn test_fetch_resolves_and_fetches() {
let server = MockServer::start().await;
Expand Down
5 changes: 0 additions & 5 deletions src/modules/proxy/sources/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
pub mod alias;
pub mod http;
pub mod local;
pub mod retry;
pub mod router;
pub mod s3;
pub mod video;

pub use alias::AliasSource;
pub use http::HttpFetcher;
pub use local::LocalSource;
pub use retry::RetryFetcher;
pub use router::SourceRouter;
pub use s3::S3Source;
File renamed without changes.