Skip to content

Commit

Permalink
tls, tracing and static files improvements (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
RomanEmreis authored Feb 14, 2025
1 parent d529cc8 commit ad74d05
Show file tree
Hide file tree
Showing 11 changed files with 525 additions and 63 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ tracing = { version = "0.1.41", default-features = false, optional = true }
reqwest = { version = "0.12.12", features = ["blocking", "json", "http2", "brotli", "deflate", "gzip", "zstd", "native-tls"] }
serde = { version = "1.0.217", features = ["derive"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
uuid = { version = "1.12.1", features = ["v4"] }
uuid = { version = "1.13.1", features = ["v4"] }
criterion = { version = "0.5.1", features = ["async_tokio"] }

[features]
Expand Down
3 changes: 2 additions & 1 deletion examples/static_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ async fn main() -> std::io::Result<()> {
// - redirects from "/" -> "/index.html" if presents
// - redirects from "/{file_name}" -> "/file-name.ext"
// - redirects to 404.html if unspecified route is requested
app.use_static_files();
app.map_group("/static")
.use_static_files();

app.run().await
}
12 changes: 6 additions & 6 deletions examples/tls.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use std::time::Duration;
use volga::{App, Json, tls::TlsConfig, ok};
use volga::{App, Json, ok};

#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.bind("127.0.0.1:7878")
.with_tls(TlsConfig::from_pem("examples/tls")
.with_https_redirection()
.with_http_port(7879)
.with_hsts_max_age(Duration::from_secs(30))
.with_hsts_exclude_hosts(&["example.com", "example.net"]));
.with_tls_from_pem("examples/tls")
.with_https_redirection()
.with_http_port(7879)
.with_hsts_max_age(Duration::from_secs(30))
.with_hsts_exclude_hosts(&["example.com", "example.net"]);

app.use_hsts();

Expand Down
11 changes: 4 additions & 7 deletions examples/tracing.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use volga::{App, tracing::TracingConfig};
use volga::App;
use tracing::trace;
use tracing_subscriber::prelude::*;

Expand All @@ -9,13 +9,10 @@ async fn main() -> std::io::Result<()> {
.with(tracing_subscriber::fmt::layer())
.init();

// Configure tracing parameters
let tracing = TracingConfig::new()
.with_header()
.with_header_name("x-span-id");

let mut app = App::new()
.with_tracing(tracing);
.with_default_tracing()
.with_span_header()
.with_span_header_name("x-span-id");

// this middleware won't be in the request span scope
// since it's defined above the tracing middleware
Expand Down
4 changes: 2 additions & 2 deletions src/app/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ impl App {

/// Represents a group of routes
pub struct RouteGroup<'a> {
app: &'a mut App,
prefix: &'a str,
pub(crate) app: &'a mut App,
pub(crate) prefix: &'a str,
}

macro_rules! define_route_group_methods({$($method:ident)*} => {
Expand Down
112 changes: 80 additions & 32 deletions src/fs/static_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
HttpResult,
app::HostEnv,
http::StatusCode,
routing::RouteGroup,
html_file,
html,
status
Expand All @@ -28,10 +29,7 @@ mod file_listing;
async fn index(env: HostEnv) -> HttpResult {
if env.show_files_listing() {
let path = env.content_root().to_path_buf();
let metadata = metadata(&path).await?;
let caching = ResponseCaching::try_from(&metadata)?;

respond_with_folder_impl(path, caching, true).await
respond_with_folder_impl(path, true).await
} else {
let index_path = env.index_path().to_path_buf();
let metadata = metadata(&index_path).await?;
Expand Down Expand Up @@ -67,7 +65,6 @@ async fn respond_with_file(
acc.push(v);
acc
});

let path = env.content_root().join(&path);
match respond_with_file_or_dir_impl(path, headers, env.show_files_listing()).await {
Ok(response) => Ok(response),
Expand All @@ -83,39 +80,28 @@ async fn respond_with_file_or_dir_impl(
show_files_listing: bool
) -> HttpResult {
let metadata = metadata(&path).await?;
let caching = ResponseCaching::try_from(&metadata)?;

if validate_etag(&caching.etag, &headers) {
return status!(304, [
(ETAG, caching.etag())
]);
}

if validate_last_modified(caching.last_modified, &headers) {
return status!(304, [
(LAST_MODIFIED, caching.last_modified())
]);
}

match (metadata.is_dir(), show_files_listing) {
(true, false) => status!(403, "Access is denied."),
(true, true) => respond_with_folder_impl(path, caching, false).await,
(false, _) => respond_with_file_impl(path, caching).await,
(true, true) => respond_with_folder_impl(path, false).await,
(false, _) => {
let caching = ResponseCaching::try_from(&metadata)?;
if validate_etag(&caching.etag, &headers) ||
validate_last_modified(caching.last_modified, &headers) {
status!(304, [
(ETAG, caching.etag()),
(LAST_MODIFIED, caching.last_modified())
])
} else {
respond_with_file_impl(path, caching).await
}
},
}
}

#[inline]
async fn respond_with_folder_impl(
path: PathBuf,
caching: ResponseCaching,
is_root: bool
) -> HttpResult {
async fn respond_with_folder_impl(path: PathBuf, is_root: bool) -> HttpResult {
let html = file_listing::generate_html(&path, is_root).await?;
html!(html, [
(ETAG, caching.etag()),
(LAST_MODIFIED, caching.last_modified()),
(CACHE_CONTROL, caching.cache_control()),
])
html!(html)
}

#[inline]
Expand Down Expand Up @@ -149,10 +135,72 @@ fn max_folder_depth<P: AsRef<Path>>(path: P) -> u32 {
helper(path.as_ref(), 1)
}

impl RouteGroup<'_> {
/// Configures a static assets
///
/// All the `GET`/`HEAD` requests to root `/` will be redirected to `/index.html`
/// as well as all the `GET`/`HEAD` requests to `/{file_name}`
/// will respond with the appropriate page
///
/// # Example
/// ```no_run
/// use volga::{App, app::HostEnv};
///
/// # #[tokio::main]
/// # async fn main() -> std::io::Result<()> {
/// let mut app = App::new();
///
/// // Enables static file server
/// app.map_group("/static")
/// .map_static_assets();
/// # app.run().await
/// # }
/// ```
pub fn map_static_assets(&mut self) -> &mut Self {
// Configure routes depending on root folder depth
let folder_depth = max_folder_depth(self.app.host_env.content_root());
let mut segment = String::new();
for i in 0..folder_depth {
segment.push_str(&format!("/{{path_{}}}", i));
self.map_get(&segment, respond_with_file);
}
self.map_get("/", index)
}

/// Configures a static files server
///
/// This method combines logic [`App::map_static_assets`] and [`App::map_fallback_to_file`].
/// The last one is called if the `fallback_path` is explicitly provided in [`HostEnv`].
///
/// # Example
/// ```no_run
/// use volga::{App, app::HostEnv};
///
/// # #[tokio::main]
/// # async fn main() -> std::io::Result<()> {
/// let mut app = App::new();
///
/// // Enables static file server
/// app
/// .map_group("/static")
/// .use_static_files();
/// # app.run().await
/// # }
/// ```
pub fn use_static_files(&mut self) -> &mut Self {
// Enable fallback to file if it's provided
if self.app.host_env.fallback_path().is_some() {
self.app.map_fallback_to_file();
}

self.map_static_assets()
}
}

impl App {
/// Configures a static files server
///
/// This method combines logic [`map_static_assets`] and [`map_fallback_to_file`].
/// This method combines logic [`App::map_static_assets`] and [`App::map_fallback_to_file`].
/// The last one is called if the `fallback_path` is explicitly provided in [`HostEnv`].
///
/// # Example
Expand Down
18 changes: 13 additions & 5 deletions src/headers/cache_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use crate::error::Error;
#[cfg(feature = "static-files")]
use std::fs::Metadata;

#[cfg(feature = "static-files")]
const DEFAULT_MAX_AGE: u32 = 60 * 60 * 24; // 24 hours

/// Represents the HTTP [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
/// header holds directives (instructions) in both requests and responses that control caching
/// in browsers and shared caches (e.g., Proxies, CDNs).
Expand Down Expand Up @@ -107,11 +110,16 @@ impl From<CacheControl> for String {
/// [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) and
/// [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
pub struct ResponseCaching {
/// Represents [`ETag`](https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/ETag)
/// Represents
/// [`ETag`](https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/ETag)
pub(crate) etag: ETag,
/// Represents [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)

/// Represents
/// [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)
pub(crate) last_modified: SystemTime,
/// Represents [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)

/// Represents
/// [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
pub(crate) cache_control: CacheControl
}

Expand All @@ -126,7 +134,7 @@ impl TryFrom<&Metadata> for ResponseCaching {
let cache_control = CacheControl {
public: true,
immutable: true,
max_age: Some(31536000),
max_age: Some(DEFAULT_MAX_AGE),
..Default::default()
};

Expand Down Expand Up @@ -185,7 +193,7 @@ mod tests {
cache_control: Default::default()
};

assert_eq!(caching.etag(), "123");
assert_eq!(caching.etag(), "\"123\"");
}

#[test]
Expand Down
19 changes: 17 additions & 2 deletions src/headers/etag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::fs::Metadata;
use std::time::UNIX_EPOCH;

/// Represents Entity Tag (ETag) value
#[derive(Debug, Clone)]
pub struct ETag {
inner: Cow<'static, str>,
}
Expand Down Expand Up @@ -67,11 +68,25 @@ impl Display for ETag {
impl ETag {
#[inline]
pub fn new(etag: &str) -> Self {
Self::from(etag.to_owned())
Self::from(format!("\"{etag}\""))
}
}

#[cfg(test)]
mod tests {

use crate::headers::ETag;

#[test]
fn it_creates_etag() {
let etag = ETag::new("foo");

assert_eq!(etag.as_ref(), "\"foo\"");
}

#[test]
fn it_creates_etag_from_string() {
let etag = ETag::from(String::from("\"foo\""));

assert_eq!(etag.as_ref(), "\"foo\"");
}
}
Loading

0 comments on commit ad74d05

Please sign in to comment.