#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedPath {
pub pod: Option<String>,
pub storage_path: String,
}
pub trait PodResolver: Send + Sync {
fn resolve(&self, host: &str, url_path: &str) -> ResolvedPath;
}
pub struct PathResolver;
impl PodResolver for PathResolver {
fn resolve(&self, _host: &str, url_path: &str) -> ResolvedPath {
ResolvedPath {
pod: None,
storage_path: url_path.to_string(),
}
}
}
pub struct SubdomainResolver {
pub base_domain: String,
}
impl PodResolver for SubdomainResolver {
fn resolve(&self, host: &str, url_path: &str) -> ResolvedPath {
let host_no_port = strip_port(host);
let base = self.base_domain.trim().to_ascii_lowercase();
let host_lc = host_no_port.to_ascii_lowercase();
if host_lc == base {
return ResolvedPath {
pod: None,
storage_path: url_path.to_string(),
};
}
let suffix = format!(".{base}");
if let Some(stripped) = host_lc.strip_suffix(&suffix) {
if is_file_like_label(stripped) {
return ResolvedPath {
pod: None,
storage_path: url_path.to_string(),
};
}
let safe = scrub_dotdot(stripped);
if !safe.is_empty()
&& !safe.contains('.')
&& !safe.contains('/')
&& !safe.contains("..")
{
return ResolvedPath {
pod: Some(safe),
storage_path: url_path.to_string(),
};
}
}
ResolvedPath {
pod: None,
storage_path: url_path.to_string(),
}
}
}
pub fn is_file_like_label(label: &str) -> bool {
let lower = label.to_ascii_lowercase();
if !lower.contains('.') {
return false;
}
const FILE_EXTENSIONS: &[&str] = &[
".ttl", ".html", ".ico", ".svg", ".json", ".jsonld", ".png", ".jpg", ".jpeg", ".gif",
".css", ".js", ".woff", ".woff2", ".txt",
];
FILE_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
}
fn strip_port(host: &str) -> &str {
match host.rfind(':') {
Some(i) => &host[..i],
None => host,
}
}
fn scrub_dotdot(s: &str) -> String {
let mut cur = s.to_string();
loop {
let next = cur.replace("..", "");
if next == cur {
return next;
}
cur = next;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_port_handles_missing_port() {
assert_eq!(strip_port("example.org"), "example.org");
assert_eq!(strip_port("example.org:8080"), "example.org");
}
#[test]
fn scrub_dotdot_is_double_pass() {
assert_eq!(scrub_dotdot("al..ice"), "alice");
assert_eq!(scrub_dotdot("al....ice"), "alice");
assert_eq!(scrub_dotdot("safe"), "safe");
}
#[test]
fn scrub_dotdot_iterative_defeats_bypass() {
let result = scrub_dotdot("....//foo");
assert!(
!result.contains(".."),
"iterative scrub must eliminate all `..`: got {result:?}"
);
assert_eq!(result, "//foo");
}
#[test]
fn subdomain_rejects_dotdot_bypass_as_pod() {
let r = SubdomainResolver {
base_domain: "pods.example.com".into(),
};
let got = r.resolve("....//foo.pods.example.com", "/index.html");
assert_eq!(
got.pod, None,
"bypass attempt must not produce a pod name"
);
}
#[test]
fn path_resolver_ignores_host() {
let r = PathResolver;
let a = r.resolve("anything", "/x");
let b = r.resolve("", "/x");
assert_eq!(a, b);
assert_eq!(a.pod, None);
}
#[test]
fn subdomain_extracts_pod_name() {
let r = SubdomainResolver {
base_domain: "pods.example.com".into(),
};
let got = r.resolve("alice.pods.example.com", "/index.html");
assert_eq!(got.pod.as_deref(), Some("alice"));
assert_eq!(got.storage_path, "/index.html");
}
#[test]
fn subdomain_file_like_label_passes_through() {
let r = SubdomainResolver {
base_domain: "pods.example.com".into(),
};
let got = r.resolve("favicon.ico.pods.example.com", "/");
assert_eq!(got.pod, None, "file-like label must pass through");
assert_eq!(got.storage_path, "/");
}
#[test]
fn subdomain_html_label_passes_through() {
let r = SubdomainResolver {
base_domain: "pods.example.com".into(),
};
let got = r.resolve("index.html.pods.example.com", "/");
assert_eq!(got.pod, None);
}
#[test]
fn subdomain_base_domain_root() {
let r = SubdomainResolver {
base_domain: "pods.example.com".into(),
};
let got = r.resolve("pods.example.com", "/hello");
assert_eq!(got.pod, None);
assert_eq!(got.storage_path, "/hello");
}
#[test]
fn is_file_like_label_matches_known_extensions() {
assert!(is_file_like_label("favicon.ico"));
assert!(is_file_like_label("style.css"));
assert!(is_file_like_label("bundle.js"));
assert!(is_file_like_label("icon.SVG"));
assert!(is_file_like_label("profile.jsonld"));
assert!(!is_file_like_label("hero.webp"), "unknown ext must not match");
assert!(!is_file_like_label("alice"));
assert!(!is_file_like_label("bob-smith"));
assert!(!is_file_like_label("foo.bar"));
}
}