mirror of
https://github.com/telemt/telemt.git
synced 2026-10-30 12:30:34 +03:00
Slot Budget + Config Store Atomic Writer fixes + Trusted Command
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||
|
||||
use crate::error::{ProxyError, Result};
|
||||
|
||||
pub(super) fn normalize_config_path(path: &Path) -> PathBuf {
|
||||
@@ -22,6 +26,74 @@ pub(super) fn hash_rendered_snapshot(rendered: &str) -> u64 {
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub(super) fn read_config_source(path: &Path) -> Result<(PathBuf, String)> {
|
||||
let mut options = std::fs::OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
options.custom_flags(libc::O_CLOEXEC);
|
||||
let mut file = options
|
||||
.open(path)
|
||||
.map_err(|error| ProxyError::Config(error.to_string()))?;
|
||||
let opened_metadata = file
|
||||
.metadata()
|
||||
.map_err(|error| ProxyError::Config(error.to_string()))?;
|
||||
if !opened_metadata.is_file() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"config source `{}` must be a regular file",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let normalized = normalize_config_path(path);
|
||||
let current_metadata = std::fs::metadata(&normalized)
|
||||
.map_err(|error| ProxyError::Config(error.to_string()))?;
|
||||
if !same_file_identity(&opened_metadata, ¤t_metadata) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"config source `{}` changed while it was opened",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)
|
||||
.map_err(|error| ProxyError::Config(error.to_string()))?;
|
||||
let completed_metadata = file
|
||||
.metadata()
|
||||
.map_err(|error| ProxyError::Config(error.to_string()))?;
|
||||
if !same_file_version(&opened_metadata, &completed_metadata) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"config source `{}` changed while it was read",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
Ok((normalized, contents))
|
||||
}
|
||||
|
||||
fn same_file_identity(left: &std::fs::Metadata, right: &std::fs::Metadata) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
left.dev() == right.dev() && left.ino() == right.ino()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
left.len() == right.len() && left.modified().ok() == right.modified().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn same_file_version(left: &std::fs::Metadata, right: &std::fs::Metadata) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
same_file_identity(left, right)
|
||||
&& left.len() == right.len()
|
||||
&& left.mtime() == right.mtime()
|
||||
&& left.mtime_nsec() == right.mtime_nsec()
|
||||
&& left.ctime() == right.ctime()
|
||||
&& left.ctime_nsec() == right.ctime_nsec()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
same_file_identity(left, right)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn preprocess_includes(
|
||||
content: &str,
|
||||
base_dir: &Path,
|
||||
@@ -42,14 +114,18 @@ pub(super) fn preprocess_includes(
|
||||
let path_str = rest.trim().trim_matches('"');
|
||||
let resolved = base_dir.join(path_str);
|
||||
let normalized = normalize_config_path(&resolved);
|
||||
let cached = source_contents.get(&normalized).cloned();
|
||||
let (normalized, included) = if let Some(included) =
|
||||
source_overrides.get(&normalized).cloned().or(cached)
|
||||
{
|
||||
(normalized, included)
|
||||
} else {
|
||||
read_config_source(&resolved)?
|
||||
};
|
||||
source_files.insert(normalized.clone());
|
||||
let included = source_overrides
|
||||
.get(&normalized)
|
||||
.cloned()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| std::fs::read_to_string(&resolved))
|
||||
.map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
source_contents.insert(normalized, included.clone());
|
||||
source_contents
|
||||
.entry(normalized)
|
||||
.or_insert_with(|| included.clone());
|
||||
let included_dir = resolved.parent().unwrap_or(base_dir);
|
||||
output.push_str(&preprocess_includes(
|
||||
&included,
|
||||
|
||||
+231
-119
@@ -5,7 +5,18 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::ffi::OsString;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::dir::Dir;
|
||||
#[cfg(unix)]
|
||||
use nix::fcntl::{openat, OFlag};
|
||||
#[cfg(unix)]
|
||||
use nix::sys::stat::Mode;
|
||||
|
||||
use bytes::Bytes;
|
||||
use hmac::{Hmac, Mac};
|
||||
@@ -13,6 +24,10 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
use super::*;
|
||||
|
||||
// Path-based static snapshot fallback for platforms without directory descriptors.
|
||||
#[cfg(not(unix))]
|
||||
mod static_site_fallback;
|
||||
|
||||
const WEB_CAPABILITY_CONTEXT: &[u8] = b"tdesktop-web-proxy-bridge-v1\n";
|
||||
const WEB_DEBUG_FINGERPRINT_CONTEXT: &[u8] = b"telemt-web-debug-key-fingerprint-v1\0";
|
||||
const MAX_WEB_STATIC_DEPTH: usize = 64;
|
||||
@@ -193,34 +208,31 @@ fn load_static_site(
|
||||
total_files: &mut usize,
|
||||
total_bytes: &mut usize,
|
||||
) -> Result<WebStaticSite> {
|
||||
let root_metadata = fs::symlink_metadata(root).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static directory `{}`: {error}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static directory `{}` must be a real directory, not a symlink",
|
||||
root.display()
|
||||
)));
|
||||
}
|
||||
let canonical_root = fs::canonicalize(root).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to canonicalize WEB static directory `{}`: {error}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
let mut assets = BTreeMap::new();
|
||||
load_static_directory(
|
||||
&canonical_root,
|
||||
&canonical_root,
|
||||
&mut assets,
|
||||
total_files,
|
||||
total_bytes,
|
||||
limits,
|
||||
0,
|
||||
)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let directory = open_static_root(root)?;
|
||||
load_static_directory(
|
||||
directory,
|
||||
Path::new(""),
|
||||
root,
|
||||
&mut assets,
|
||||
total_files,
|
||||
total_bytes,
|
||||
limits,
|
||||
0,
|
||||
)?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
static_site_fallback::load_static_site_by_path(
|
||||
root,
|
||||
limits,
|
||||
&mut assets,
|
||||
total_files,
|
||||
total_bytes,
|
||||
)?;
|
||||
}
|
||||
if !assets.contains_key(&format!("/{index}")) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static directory `{}` does not contain index `{index}`",
|
||||
@@ -233,54 +245,94 @@ fn load_static_site(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn open_static_root(root: &Path) -> Result<Dir> {
|
||||
Dir::open(
|
||||
root,
|
||||
OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
|
||||
Mode::empty(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"WEB static directory `{}` must be a real directory, not a symlink: {error}",
|
||||
root.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn load_static_directory(
|
||||
mut directory: Dir,
|
||||
relative: &Path,
|
||||
root: &Path,
|
||||
directory: &Path,
|
||||
assets: &mut BTreeMap<String, WebStaticAsset>,
|
||||
total_files: &mut usize,
|
||||
total_bytes: &mut usize,
|
||||
limits: &WebLimitsConfig,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
let entries = fs::read_dir(directory).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to read WEB static directory `{}`: {error}",
|
||||
directory.display()
|
||||
))
|
||||
})?;
|
||||
for entry in entries {
|
||||
let mut entries = Vec::new();
|
||||
for entry in directory.iter() {
|
||||
let entry = entry.map_err(|error| {
|
||||
ProxyError::Config(format!("failed to read WEB static entry: {error}"))
|
||||
ProxyError::Config(format!(
|
||||
"failed to read WEB static directory `{}`: {error}",
|
||||
root.join(relative).display()
|
||||
))
|
||||
})?;
|
||||
let name = entry.file_name().to_bytes();
|
||||
if name == b"." || name == b".." {
|
||||
continue;
|
||||
}
|
||||
if *total_files >= limits.max_static_files {
|
||||
return Err(ProxyError::Config(
|
||||
"WEB static entries exceed process-wide web.limits.max_static_files".to_string(),
|
||||
));
|
||||
}
|
||||
*total_files += 1;
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type().map_err(|error| {
|
||||
entries.push(OsString::from_vec(name.to_vec()));
|
||||
}
|
||||
entries.sort_unstable();
|
||||
|
||||
for name in entries {
|
||||
let relative_path = relative.join(&name);
|
||||
let display_path = root.join(&relative_path);
|
||||
let descriptor = openat(
|
||||
&directory,
|
||||
name.as_os_str(),
|
||||
OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC,
|
||||
Mode::empty(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static entry `{}`: {error}",
|
||||
path.display()
|
||||
"failed to open WEB static entry `{}` without following symlinks: {error}",
|
||||
display_path.display()
|
||||
))
|
||||
})?;
|
||||
if file_type.is_symlink() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static entry `{}` must not be a symlink",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
if file_type.is_dir() {
|
||||
let file = fs::File::from(descriptor);
|
||||
let metadata = file.metadata().map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static entry `{}`: {error}",
|
||||
display_path.display()
|
||||
))
|
||||
})?;
|
||||
if metadata.is_dir() {
|
||||
if depth >= MAX_WEB_STATIC_DEPTH {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static directory `{}` exceeds the maximum nesting depth",
|
||||
path.display()
|
||||
display_path.display()
|
||||
)));
|
||||
}
|
||||
let descriptor = file.into();
|
||||
let child = Dir::from_fd(descriptor).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to open WEB static directory `{}`: {error}",
|
||||
display_path.display()
|
||||
))
|
||||
})?;
|
||||
load_static_directory(
|
||||
child,
|
||||
&relative_path,
|
||||
root,
|
||||
&path,
|
||||
assets,
|
||||
total_files,
|
||||
total_bytes,
|
||||
@@ -289,83 +341,107 @@ fn load_static_directory(
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
if !file_type.is_file() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static entry `{}` must be a regular file",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut options = fs::OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
options.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
|
||||
let file = options.open(&path).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to open WEB static file `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
let metadata = file.metadata().map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static file `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static entry `{}` changed before it was opened",
|
||||
path.display()
|
||||
"WEB static entry `{}` must be a regular file",
|
||||
display_path.display()
|
||||
)));
|
||||
}
|
||||
let file_len = usize::try_from(metadata.len()).map_err(|_| {
|
||||
ProxyError::Config(format!("WEB static file `{}` is too large", path.display()))
|
||||
})?;
|
||||
if file_len > limits.max_static_file_bytes {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static file `{}` exceeds web.limits.max_static_file_bytes",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
*total_bytes = total_bytes.checked_add(file_len).ok_or_else(|| {
|
||||
ProxyError::Config("WEB static snapshot byte count overflowed usize".to_string())
|
||||
})?;
|
||||
if *total_bytes > limits.max_static_bytes {
|
||||
return Err(ProxyError::Config(
|
||||
"WEB static snapshots exceed process-wide web.limits.max_static_bytes".to_string(),
|
||||
));
|
||||
}
|
||||
let relative = path.strip_prefix(root).map_err(|_| {
|
||||
ProxyError::Config("WEB static path escaped its configured root".to_string())
|
||||
})?;
|
||||
let route = static_route(relative)?;
|
||||
let mut body = Vec::with_capacity(file_len);
|
||||
file.take(limits.max_static_file_bytes as u64 + 1)
|
||||
.read_to_end(&mut body)
|
||||
.map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to read WEB static file `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
if body.len() != file_len {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static file `{}` changed while its snapshot was built",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let etag = format!("\"{}\"", hex::encode(Sha256::digest(&body)));
|
||||
assets.insert(
|
||||
route,
|
||||
WebStaticAsset {
|
||||
body: Bytes::from(body),
|
||||
content_type: static_content_type(&path),
|
||||
etag,
|
||||
},
|
||||
);
|
||||
load_static_file(
|
||||
file,
|
||||
&metadata,
|
||||
&relative_path,
|
||||
&display_path,
|
||||
assets,
|
||||
total_bytes,
|
||||
limits,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_static_file(
|
||||
mut file: fs::File,
|
||||
metadata: &fs::Metadata,
|
||||
relative: &Path,
|
||||
display_path: &Path,
|
||||
assets: &mut BTreeMap<String, WebStaticAsset>,
|
||||
total_bytes: &mut usize,
|
||||
limits: &WebLimitsConfig,
|
||||
) -> Result<()> {
|
||||
let file_len = usize::try_from(metadata.len()).map_err(|_| {
|
||||
ProxyError::Config(format!(
|
||||
"WEB static file `{}` is too large",
|
||||
display_path.display()
|
||||
))
|
||||
})?;
|
||||
if file_len > limits.max_static_file_bytes {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static file `{}` exceeds web.limits.max_static_file_bytes",
|
||||
display_path.display()
|
||||
)));
|
||||
}
|
||||
*total_bytes = total_bytes.checked_add(file_len).ok_or_else(|| {
|
||||
ProxyError::Config("WEB static snapshot byte count overflowed usize".to_string())
|
||||
})?;
|
||||
if *total_bytes > limits.max_static_bytes {
|
||||
return Err(ProxyError::Config(
|
||||
"WEB static snapshots exceed process-wide web.limits.max_static_bytes".to_string(),
|
||||
));
|
||||
}
|
||||
let route = static_route(relative)?;
|
||||
let mut body = Vec::with_capacity(file_len);
|
||||
file.by_ref()
|
||||
.take(limits.max_static_file_bytes as u64 + 1)
|
||||
.read_to_end(&mut body)
|
||||
.map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to read WEB static file `{}`: {error}",
|
||||
display_path.display()
|
||||
))
|
||||
})?;
|
||||
let final_metadata = file.metadata().map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to recheck WEB static file `{}`: {error}",
|
||||
display_path.display()
|
||||
))
|
||||
})?;
|
||||
if body.len() != file_len || !static_file_version_matches(metadata, &final_metadata) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static file `{}` changed while its snapshot was built",
|
||||
display_path.display()
|
||||
)));
|
||||
}
|
||||
let etag = format!("\"{}\"", hex::encode(Sha256::digest(&body)));
|
||||
assets.insert(
|
||||
route,
|
||||
WebStaticAsset {
|
||||
body: Bytes::from(body),
|
||||
content_type: static_content_type(relative),
|
||||
etag,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn static_file_version_matches(before: &fs::Metadata, after: &fs::Metadata) -> bool {
|
||||
before.dev() == after.dev()
|
||||
&& before.ino() == after.ino()
|
||||
&& before.len() == after.len()
|
||||
&& before.mtime() == after.mtime()
|
||||
&& before.mtime_nsec() == after.mtime_nsec()
|
||||
&& before.ctime() == after.ctime()
|
||||
&& before.ctime_nsec() == after.ctime_nsec()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn static_file_version_matches(before: &fs::Metadata, after: &fs::Metadata) -> bool {
|
||||
before.len() == after.len()
|
||||
&& before.modified().ok() == after.modified().ok()
|
||||
&& before.created().ok() == after.created().ok()
|
||||
}
|
||||
|
||||
fn static_route(relative: &Path) -> Result<String> {
|
||||
let mut route = String::new();
|
||||
for component in relative.components() {
|
||||
@@ -425,4 +501,40 @@ mod tests {
|
||||
"IpJrt3e7sKtzPyoXy6w-Zj6GGEvsvclN66JzQEfPYLA"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn static_snapshot_remains_anchored_after_root_path_replacement() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path().join("site");
|
||||
let detached = temp.path().join("detached");
|
||||
let replacement = temp.path().join("replacement");
|
||||
fs::create_dir(&root).unwrap();
|
||||
fs::write(root.join("index.html"), b"original").unwrap();
|
||||
fs::create_dir(&replacement).unwrap();
|
||||
fs::write(replacement.join("index.html"), b"replacement").unwrap();
|
||||
|
||||
let directory = open_static_root(&root).unwrap();
|
||||
fs::rename(&root, &detached).unwrap();
|
||||
symlink(&replacement, &root).unwrap();
|
||||
|
||||
let mut assets = BTreeMap::new();
|
||||
let mut total_files = 0;
|
||||
let mut total_bytes = 0;
|
||||
load_static_directory(
|
||||
directory,
|
||||
Path::new(""),
|
||||
&root,
|
||||
&mut assets,
|
||||
&mut total_files,
|
||||
&mut total_bytes,
|
||||
&WebLimitsConfig::default(),
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(assets["/index.html"].body.as_ref(), b"original");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn load_static_site_by_path(
|
||||
root: &Path,
|
||||
limits: &WebLimitsConfig,
|
||||
assets: &mut BTreeMap<String, WebStaticAsset>,
|
||||
total_files: &mut usize,
|
||||
total_bytes: &mut usize,
|
||||
) -> Result<()> {
|
||||
let root_metadata = fs::symlink_metadata(root).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static directory `{}`: {error}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static directory `{}` must be a real directory, not a symlink",
|
||||
root.display()
|
||||
)));
|
||||
}
|
||||
let canonical_root = fs::canonicalize(root).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to canonicalize WEB static directory `{}`: {error}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
load_static_directory(
|
||||
&canonical_root,
|
||||
&canonical_root,
|
||||
assets,
|
||||
total_files,
|
||||
total_bytes,
|
||||
limits,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn load_static_directory(
|
||||
root: &Path,
|
||||
directory: &Path,
|
||||
assets: &mut BTreeMap<String, WebStaticAsset>,
|
||||
total_files: &mut usize,
|
||||
total_bytes: &mut usize,
|
||||
limits: &WebLimitsConfig,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
let entries = fs::read_dir(directory).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to read WEB static directory `{}`: {error}",
|
||||
directory.display()
|
||||
))
|
||||
})?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|error| {
|
||||
ProxyError::Config(format!("failed to read WEB static entry: {error}"))
|
||||
})?;
|
||||
if *total_files >= limits.max_static_files {
|
||||
return Err(ProxyError::Config(
|
||||
"WEB static entries exceed process-wide web.limits.max_static_files".to_string(),
|
||||
));
|
||||
}
|
||||
*total_files += 1;
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type().map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static entry `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
if file_type.is_symlink() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static entry `{}` must not be a symlink",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
if file_type.is_dir() {
|
||||
if depth >= MAX_WEB_STATIC_DEPTH {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static directory `{}` exceeds the maximum nesting depth",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
load_static_directory(
|
||||
root,
|
||||
&path,
|
||||
assets,
|
||||
total_files,
|
||||
total_bytes,
|
||||
limits,
|
||||
depth + 1,
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
if !file_type.is_file() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static entry `{}` must be a regular file",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let file = fs::File::open(&path).map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to open WEB static file `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
let metadata = file.metadata().map_err(|error| {
|
||||
ProxyError::Config(format!(
|
||||
"failed to inspect WEB static file `{}`: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB static entry `{}` changed before it was opened",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let relative = path.strip_prefix(root).map_err(|_| {
|
||||
ProxyError::Config("WEB static path escaped its configured root".to_string())
|
||||
})?;
|
||||
load_static_file(file, &metadata, relative, &path, assets, total_bytes, limits)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user