#[doc(hidden)]
pub fn working_directory() -> &'static str {
static DIR: std::sync::OnceLock<String> = std::sync::OnceLock::new();
DIR.get_or_init(|| {
std::env::current_dir()
.map(|d| d.to_string_lossy().into_owned())
.unwrap_or_default()
})
}
#[doc(hidden)]
pub fn static_fs_path(path: &str) -> Result<String, String> {
let path = percent_decode_path(path)?;
check_request_path(&path)?;
let working_directory = working_directory();
let mut fs_path = String::with_capacity(working_directory.len() + path.len());
fs_path.push_str(working_directory);
fs_path.push_str(&path);
Ok(fs_path)
}
#[doc(hidden)]
pub fn os_specific_path(path: &str) -> std::borrow::Cow<'_, str> {
if std::path::MAIN_SEPARATOR == '/' {
std::borrow::Cow::Borrowed(path)
} else {
std::borrow::Cow::Owned(path.replace('/', std::path::MAIN_SEPARATOR_STR))
}
}
#[doc(hidden)]
pub fn percent_decode_path(path: &str) -> Result<std::borrow::Cow<'_, str>, String> {
if !path.contains('%') {
return Ok(std::borrow::Cow::Borrowed(path));
}
let hex = |b: u8| (b as char).to_digit(16).map(|d| d as u8);
let bytes = path.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(high), Some(low)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
decoded.push(high << 4 | low);
i += 3;
continue;
}
}
decoded.push(bytes[i]);
i += 1;
}
String::from_utf8(decoded)
.map(std::borrow::Cow::Owned)
.map_err(|_| "path is not UTF-8 after percent-decoding".to_string())
}
#[doc(hidden)]
pub fn check_request_path(path: &str) -> Result<(), String> {
for segment in path.split(['/', '\\']) {
if segment == ".." {
return Err("path leaves the web root".to_string());
}
if segment.starts_with('.') && segment != "." && segment != ".well-known" && !serve_dotfiles() {
return Err("hidden files are not served".to_string());
}
if segment.contains('\0') {
return Err("invalid path".to_string());
}
}
let name = path.rsplit(['/', '\\']).next().unwrap_or("").to_ascii_lowercase();
if name.ends_with(".pem") || name.ends_with(".key") || name == "rws.config.toml" {
return Err("key and config files are not served".to_string());
}
Ok(())
}
fn serve_dotfiles() -> bool {
static SERVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*SERVE.get_or_init(|| {
std::env::var("RWS_CONFIG_SERVE_DOTFILES").is_ok_and(|v| v.trim().eq_ignore_ascii_case("true"))
})
}