Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rewrite function #46

Merged
merged 2 commits into from
Nov 18, 2023
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ url = "2.1.0"

[dev-dependencies]
hyper = { version = "1.0.0", features = ["http1", "server"] }
hyper-util = { git = "https://github.com/hyperium/hyper-util.git" }
hyper-util = { version = "0.1.1", features = ["tokio"] }
http-body-util = "0.1.0"
tempfile = "3"
tokio = { version = "1.0.0", features = ["macros", "rt-multi-thread", "net", "io-util"] }
Expand Down
85 changes: 74 additions & 11 deletions src/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::{
io::{Error as IoError, ErrorKind as IoErrorKind},
future::Future,
io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult},
ops::BitAnd,
path::PathBuf,
sync::Arc,
time::SystemTime,
};

use futures_util::future::BoxFuture;
use http::{header, HeaderValue, Method, Request};
use mime_guess::MimeGuess;
use tokio::fs::File;
Expand Down Expand Up @@ -70,6 +72,32 @@ pub struct Resolver<O = TokioFileOpener> {
///
/// Typically initialized with `AcceptEncoding::all()` or `AcceptEncoding::none()`.
pub allowed_encodings: AcceptEncoding,

/// Optional function that can rewrite requests.
///
/// This function is called after parsing the request and before querying the filesystem.
///
/// See `set_rewrite` for a convenience setter that simplifies these types.
pub rewrite: Option<Arc<dyn (Fn(ResolveParams) -> BoxRewriteFuture) + Send + Sync>>,
}

/// Future returned by a rewrite function. See `Resolver::set_rewrite`.
pub type BoxRewriteFuture = BoxFuture<'static, IoResult<ResolveParams>>;

/// All of the parsed request parameters used in resolving a file.
///
/// This struct is primarily used for `Resolver::rewrite` / `Resolver::set_rewrite`.
#[derive(Debug, Clone)]
pub struct ResolveParams {
/// Sanitized path of the request.
pub path: PathBuf,
/// Whether a directory was requested. (The request path ended with a slash.)
pub is_dir_request: bool,
/// Intersection of the request `Accept-Encoding` header and `allowed_encodings`.
///
/// Only modify this field to disable encodings. Enabling additional encodings here may cause
/// a client to receive encodings it does not understand.
pub accept_encoding: AcceptEncoding,
}

/// The result of `Resolver` methods.
Expand All @@ -93,7 +121,7 @@ pub enum ResolveResult<F = File> {
}

/// Some IO errors are expected when serving files, and mapped to a regular result here.
fn map_open_err<F>(err: IoError) -> Result<ResolveResult<F>, IoError> {
fn map_open_err<F>(err: IoError) -> IoResult<ResolveResult<F>> {
match err.kind() {
IoErrorKind::NotFound => Ok(ResolveResult::NotFound),
IoErrorKind::PermissionDenied => Ok(ResolveResult::PermissionDenied),
Expand All @@ -114,18 +142,38 @@ impl<O: FileOpener> Resolver<O> {
Self {
opener: Arc::new(opener),
allowed_encodings: AcceptEncoding::none(),
rewrite: None,
}
}

/// Configure a function that can rewrite requests.
///
/// This function is called after parsing the request and before querying the filesystem.
///
/// ```rust
/// let mut resolver = hyper_staticfile::Resolver::new("/");
/// resolver.set_rewrite(|mut params| async move {
/// if params.path.extension() == Some("htm".as_ref()) {
/// params.path.set_extension("html");
/// }
/// Ok(params)
/// });
/// ```
pub fn set_rewrite<R, F>(&mut self, rewrite: F) -> &mut Self
where
R: Future<Output = IoResult<ResolveParams>> + Send + 'static,
F: (Fn(ResolveParams) -> R) + Send + Sync + 'static,
{
self.rewrite = Some(Arc::new(move |params| Box::pin(rewrite(params))));
self
}

/// Resolve the request by trying to find the file in the root.
///
/// The returned future may error for unexpected IO errors, passing on the `std::io::Error`.
/// Certain expected IO errors are handled, though, and simply reflected in the result. These are
/// `NotFound` and `PermissionDenied`.
pub async fn resolve_request<B>(
&self,
req: &Request<B>,
) -> Result<ResolveResult<O::File>, IoError> {
pub async fn resolve_request<B>(&self, req: &Request<B>) -> IoResult<ResolveResult<O::File>> {
// Handle only `GET`/`HEAD` and absolute paths.
match *req.method() {
Method::HEAD | Method::GET => {}
Expand Down Expand Up @@ -157,12 +205,26 @@ impl<O: FileOpener> Resolver<O> {
&self,
request_path: &str,
accept_encoding: AcceptEncoding,
) -> Result<ResolveResult<O::File>, IoError> {
) -> IoResult<ResolveResult<O::File>> {
// Sanitize input path.
let RequestedPath {
sanitized: mut path,
let requested_path = RequestedPath::resolve(request_path);

// Apply optional rewrite.
let ResolveParams {
mut path,
is_dir_request,
} = RequestedPath::resolve(request_path);
accept_encoding,
} = {
let mut params = ResolveParams {
path: requested_path.sanitized,
is_dir_request: requested_path.is_dir_request,
accept_encoding,
};
if let Some(ref rewrite) = self.rewrite {
params = rewrite(params).await?;
}
params
};

// Try to open the file.
let file = match self.opener.open(&path).await {
Expand Down Expand Up @@ -219,7 +281,7 @@ impl<O: FileOpener> Resolver<O> {
file: FileWithMetadata<O::File>,
path: PathBuf,
accept_encoding: AcceptEncoding,
) -> Result<ResolveResult<O::File>, IoError> {
) -> IoResult<ResolveResult<O::File>> {
// Determine MIME-type. This needs to happen before we resolve a pre-encoded file.
let mime = MimeGuess::from_path(&path)
.first()
Expand Down Expand Up @@ -263,6 +325,7 @@ impl<O> Clone for Resolver<O> {
Self {
opener: self.opener.clone(),
allowed_encodings: self.allowed_encodings,
rewrite: self.rewrite.clone(),
}
}
}
Expand Down
Loading