use actix_web::{
dev::{
always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceRequest, ServiceResponse,
},
error::Error,
guard::{Guard, GuardContext},
http::{
header::{self, ContentType},
Method, StatusCode,
},
HttpMessage, HttpRequest, HttpResponse, ResponseError,
};
use derive_more::{Deref, Display, Error};
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
use static_files::Resource;
use std::{collections::HashMap, ops::Deref, rc::Rc, sync::Arc};
pub type DefaultResourceFiles = HashMap<&'static str, Resource>;
pub trait ResourceFile {
fn data(&self) -> &'static [u8];
fn modified(&self) -> u64;
fn mime_type(&self) -> &str;
}
pub trait ResourceFilesCollection {
type Resource: ResourceFile;
fn get_resource(&self, path: &str) -> Option<&Self::Resource>;
fn contains_key(&self, path: &str) -> bool;
}
impl<R> ResourceFilesCollection for Rc<R>
where
R: ResourceFilesCollection,
{
type Resource = R::Resource;
fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
let r: &R = self;
r.get_resource(path)
}
fn contains_key(&self, path: &str) -> bool {
let r: &R = self;
r.contains_key(path)
}
}
impl<R> ResourceFilesCollection for Arc<R>
where
R: ResourceFilesCollection,
{
type Resource = R::Resource;
fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
let r: &R = self;
r.get_resource(path)
}
fn contains_key(&self, path: &str) -> bool {
let r: &R = self;
r.contains_key(path)
}
}
mod legacy_static_files {
use super::*;
impl ResourceFile for Resource {
fn data(&self) -> &'static [u8] {
self.data
}
fn modified(&self) -> u64 {
self.modified
}
fn mime_type(&self) -> &str {
self.mime_type
}
}
impl ResourceFilesCollection for DefaultResourceFiles {
type Resource = Resource;
fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
self.get(path)
}
fn contains_key(&self, path: &str) -> bool {
self.contains_key(path)
}
}
}
#[allow(clippy::needless_doctest_main)]
pub struct ResourceFiles<C = DefaultResourceFiles> {
not_resolve_defaults: bool,
use_guard: bool,
not_found_resolves_to: Option<String>,
inner: Rc<ResourceFilesInner<C>>,
}
pub struct ResourceFilesInner<C> {
path: String,
files: C,
}
const INDEX_HTML: &str = "index.html";
impl<F> ResourceFiles<F>
where
F: ResourceFilesCollection + 'static,
{
#[must_use]
pub fn new(path: &str, files: F) -> Self {
let inner = ResourceFilesInner {
path: path.into(),
files,
};
Self {
inner: Rc::new(inner),
not_resolve_defaults: false,
not_found_resolves_to: None,
use_guard: false,
}
}
#[must_use]
pub fn do_not_resolve_defaults(mut self) -> Self {
self.not_resolve_defaults = true;
self
}
#[must_use]
pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
self.not_found_resolves_to = Some(path.to_string());
self
}
#[must_use]
pub fn resolve_not_found_to_root(self) -> Self {
self.resolve_not_found_to(INDEX_HTML)
}
#[must_use]
pub fn skip_handler_when_not_found(mut self) -> Self {
self.use_guard = true;
self
}
fn select_guard(&self) -> Box<dyn Guard> {
if self.not_resolve_defaults {
Box::new(NotResolveDefaultsGuard::from(self))
} else {
Box::new(ResolveDefaultsGuard::from(self))
}
}
}
impl<C> Deref for ResourceFiles<C> {
type Target = ResourceFilesInner<C>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
struct NotResolveDefaultsGuard<C> {
inner: Rc<ResourceFilesInner<C>>,
}
impl<C> Guard for NotResolveDefaultsGuard<C>
where
C: ResourceFilesCollection,
{
fn check(&self, ctx: &GuardContext<'_>) -> bool {
self.inner
.files
.contains_key(ctx.head().uri.path().trim_start_matches('/'))
}
}
impl<C> From<&ResourceFiles<C>> for NotResolveDefaultsGuard<C> {
fn from(files: &ResourceFiles<C>) -> Self {
Self {
inner: files.inner.clone(),
}
}
}
struct ResolveDefaultsGuard<C> {
inner: Rc<ResourceFilesInner<C>>,
}
impl<C> Guard for ResolveDefaultsGuard<C>
where
C: ResourceFilesCollection,
{
fn check(&self, ctx: &GuardContext<'_>) -> bool {
let path = ctx.head().uri.path().trim_start_matches('/');
self.inner.files.contains_key(path)
|| ((path.is_empty() || path.ends_with('/'))
&& self
.inner
.files
.contains_key((path.to_string() + INDEX_HTML).as_str()))
}
}
impl<C> From<&ResourceFiles<C>> for ResolveDefaultsGuard<C> {
fn from(files: &ResourceFiles<C>) -> Self {
Self {
inner: files.inner.clone(),
}
}
}
impl<C> HttpServiceFactory for ResourceFiles<C>
where
C: ResourceFilesCollection + 'static,
{
fn register(self, config: &mut AppService) {
let prefix = self.path.trim_start_matches('/');
let rdef = if config.is_root() {
ResourceDef::root_prefix(prefix)
} else {
ResourceDef::prefix(prefix)
};
let guards = if self.use_guard && self.not_found_resolves_to.is_none() {
Some(vec![self.select_guard()])
} else {
None
};
config.register_service(rdef, guards, self, None);
}
}
impl<C> ServiceFactory<ServiceRequest> for ResourceFiles<C>
where
C: ResourceFilesCollection + 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = ResourceFilesService<C>;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(ResourceFilesService {
resolve_defaults: !self.not_resolve_defaults,
not_found_resolves_to: self.not_found_resolves_to.clone(),
inner: self.inner.clone(),
})
.boxed_local()
}
}
#[derive(Deref)]
pub struct ResourceFilesService<C> {
resolve_defaults: bool,
not_found_resolves_to: Option<String>,
#[deref]
inner: Rc<ResourceFilesInner<C>>,
}
impl<C> Service<ServiceRequest> for ResourceFilesService<C>
where
C: ResourceFilesCollection,
{
type Response = ServiceResponse;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
match *req.method() {
Method::HEAD | Method::GET => (),
_ => {
return ok(ServiceResponse::new(
req.into_parts().0,
HttpResponse::MethodNotAllowed()
.insert_header(ContentType::plaintext())
.insert_header((header::ALLOW, "GET, HEAD"))
.body("This resource only supports GET and HEAD."),
));
}
}
let req_path = req.match_info().unprocessed();
let mut item = self.files.get_resource(req_path);
if item.is_none()
&& self.resolve_defaults
&& (req_path.is_empty() || req_path.ends_with('/'))
{
let index_req_path = req_path.to_string() + INDEX_HTML;
item = self
.files
.get_resource(index_req_path.trim_start_matches('/'));
}
let (req, response) = if item.is_some() {
let (req, _) = req.into_parts();
let response = respond_to(&req, item);
(req, response)
} else {
let real_path = match get_pathbuf(req_path) {
Ok(item) => item,
Err(e) => return ok(req.error_response(e)),
};
let (req, _) = req.into_parts();
let mut item = self.files.get_resource(real_path.as_str());
if item.is_none() && self.not_found_resolves_to.is_some() {
let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
item = self.files.get_resource(not_found_path.as_str());
}
let response = respond_to(&req, item);
(req, response)
};
ok(ServiceResponse::new(req, response))
}
}
fn respond_to<Resource: ResourceFile>(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
if let Some(file) = item {
let etag = Some(header::EntityTag::new_strong(format!(
"{:x}:{:x}",
file.data().len(),
file.modified()
)));
let precondition_failed = !any_match(etag.as_ref(), req);
let not_modified = !none_match(etag.as_ref(), req);
let mut resp = HttpResponse::build(StatusCode::OK);
resp.insert_header((header::CONTENT_TYPE, file.mime_type()));
if let Some(etag) = etag {
resp.insert_header(header::ETag(etag));
}
if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified {
return resp.status(StatusCode::NOT_MODIFIED).finish();
}
resp.body(file.data())
} else {
HttpResponse::NotFound().body("Not found")
}
}
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() {
None | Some(header::IfMatch::Any) => true,
Some(header::IfMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.strong_eq(some_etag) {
return true;
}
}
}
false
}
}
}
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfNoneMatch>() {
Some(header::IfNoneMatch::Any) => false,
Some(header::IfNoneMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.weak_eq(some_etag) {
return false;
}
}
}
true
}
None => true,
}
}
#[derive(Debug, PartialEq, Display, Error)]
pub enum UriSegmentError {
#[display(fmt = "The segment started with the wrapped invalid character")]
BadStart(#[error(not(source))] char),
#[display(fmt = "The segment contained the wrapped invalid character")]
BadChar(#[error(not(source))] char),
#[display(fmt = "The segment ended with the wrapped invalid character")]
BadEnd(#[error(not(source))] char),
}
impl ResponseError for UriSegmentError {
fn error_response(&self) -> HttpResponse {
HttpResponse::new(StatusCode::BAD_REQUEST)
}
}
fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
let mut buf = Vec::new();
for segment in path.split('/') {
if segment == ".." {
buf.pop();
} else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
} else if segment.ends_with(':') {
return Err(UriSegmentError::BadEnd(':'));
} else if segment.ends_with('>') {
return Err(UriSegmentError::BadEnd('>'));
} else if segment.ends_with('<') {
return Err(UriSegmentError::BadEnd('<'));
} else if segment.is_empty() {
continue;
} else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\'));
} else {
buf.push(segment);
}
}
Ok(buf.join("/"))
}
#[cfg(test)]
mod tests_error_impl {
use super::*;
fn assert_send_and_sync<T: Send + Sync + 'static>() {}
#[test]
fn test_error_impl() {
assert_send_and_sync::<UriSegmentError>();
}
}