1use actix_web::{
2 dev::{
3 always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
4 ServiceRequest, ServiceResponse,
5 },
6 error::Error,
7 guard::{Guard, GuardContext},
8 http::{
9 header::{self, ContentType},
10 Method, StatusCode,
11 },
12 HttpMessage, HttpRequest, HttpResponse, ResponseError,
13};
14use derive_more::{Deref, Display, Error};
15use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
16use static_files::Resource;
17use std::{collections::HashMap, ops::Deref, rc::Rc};
18
19#[allow(clippy::needless_doctest_main)]
42pub struct ResourceFiles {
43 not_resolve_defaults: bool,
44 use_guard: bool,
45 not_found_resolves_to: Option<String>,
46 inner: Rc<ResourceFilesInner>,
47}
48
49pub struct ResourceFilesInner {
50 path: String,
51 files: HashMap<&'static str, Resource>,
52}
53
54const INDEX_HTML: &str = "index.html";
55
56impl ResourceFiles {
57 pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self {
58 let inner = ResourceFilesInner {
59 path: path.into(),
60 files,
61 };
62 Self {
63 inner: Rc::new(inner),
64 not_resolve_defaults: false,
65 not_found_resolves_to: None,
66 use_guard: false,
67 }
68 }
69
70 pub fn do_not_resolve_defaults(mut self) -> Self {
73 self.not_resolve_defaults = true;
74 self
75 }
76
77 pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
81 self.not_found_resolves_to = Some(path.to_string());
82 self
83 }
84
85 pub fn resolve_not_found_to_root(self) -> Self {
89 self.resolve_not_found_to(INDEX_HTML)
90 }
91
92 pub fn skip_handler_when_not_found(mut self) -> Self {
98 self.use_guard = true;
99 self
100 }
101
102 fn select_guard(&self) -> Box<dyn Guard> {
103 if self.not_resolve_defaults {
104 Box::new(NotResolveDefaultsGuard::from(self))
105 } else {
106 Box::new(ResolveDefaultsGuard::from(self))
107 }
108 }
109}
110
111impl Deref for ResourceFiles {
112 type Target = ResourceFilesInner;
113
114 fn deref(&self) -> &Self::Target {
115 &self.inner
116 }
117}
118
119struct NotResolveDefaultsGuard {
120 inner: Rc<ResourceFilesInner>,
121}
122
123impl Guard for NotResolveDefaultsGuard {
124 fn check(&self, ctx: &GuardContext<'_>) -> bool {
125 self.inner
126 .files
127 .contains_key(ctx.head().uri.path().trim_start_matches('/'))
128 }
129}
130
131impl From<&ResourceFiles> for NotResolveDefaultsGuard {
132 fn from(files: &ResourceFiles) -> Self {
133 Self {
134 inner: files.inner.clone(),
135 }
136 }
137}
138
139struct ResolveDefaultsGuard {
140 inner: Rc<ResourceFilesInner>,
141}
142
143impl Guard for ResolveDefaultsGuard {
144 fn check(&self, ctx: &GuardContext<'_>) -> bool {
145 let path = ctx.head().uri.path().trim_start_matches('/');
146 self.inner.files.contains_key(path)
147 || ((path.is_empty() || path.ends_with('/'))
148 && self
149 .inner
150 .files
151 .contains_key((path.to_string() + INDEX_HTML).as_str()))
152 }
153}
154
155impl From<&ResourceFiles> for ResolveDefaultsGuard {
156 fn from(files: &ResourceFiles) -> Self {
157 Self {
158 inner: files.inner.clone(),
159 }
160 }
161}
162
163impl HttpServiceFactory for ResourceFiles {
164 fn register(self, config: &mut AppService) {
165 let prefix = self.path.trim_start_matches('/');
166 let rdef = if config.is_root() {
167 ResourceDef::root_prefix(prefix)
168 } else {
169 ResourceDef::prefix(prefix)
170 };
171 let guards = if self.use_guard && self.not_found_resolves_to.is_none() {
172 Some(vec![self.select_guard()])
173 } else {
174 None
175 };
176 config.register_service(rdef, guards, self, None);
177 }
178}
179
180impl ServiceFactory<ServiceRequest> for ResourceFiles {
181 type Config = ();
182 type Response = ServiceResponse;
183 type Error = Error;
184 type Service = ResourceFilesService;
185 type InitError = ();
186 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
187
188 fn new_service(&self, _: ()) -> Self::Future {
189 ok(ResourceFilesService {
190 resolve_defaults: !self.not_resolve_defaults,
191 not_found_resolves_to: self.not_found_resolves_to.clone(),
192 inner: self.inner.clone(),
193 })
194 .boxed_local()
195 }
196}
197
198#[derive(Deref)]
199pub struct ResourceFilesService {
200 resolve_defaults: bool,
201 not_found_resolves_to: Option<String>,
202 #[deref]
203 inner: Rc<ResourceFilesInner>,
204}
205
206impl Service<ServiceRequest> for ResourceFilesService {
207 type Response = ServiceResponse;
208 type Error = Error;
209 type Future = Ready<Result<Self::Response, Self::Error>>;
210
211 always_ready!();
212
213 fn call(&self, req: ServiceRequest) -> Self::Future {
214 match *req.method() {
215 Method::HEAD | Method::GET => (),
216 _ => {
217 return ok(ServiceResponse::new(
218 req.into_parts().0,
219 HttpResponse::MethodNotAllowed()
220 .insert_header(ContentType::plaintext())
221 .insert_header((header::ALLOW, "GET, HEAD"))
222 .body("This resource only supports GET and HEAD."),
223 ));
224 }
225 }
226
227 let req_path = req.match_info().unprocessed();
228 let mut item = self.files.get(req_path);
229
230 if item.is_none()
231 && self.resolve_defaults
232 && (req_path.is_empty() || req_path.ends_with('/'))
233 {
234 let index_req_path = req_path.to_string() + INDEX_HTML;
235 item = self.files.get(index_req_path.trim_start_matches('/'));
236 }
237
238 let (req, response) = if item.is_some() {
239 let (req, _) = req.into_parts();
240 let response = respond_to(&req, item);
241 (req, response)
242 } else {
243 let real_path = match get_pathbuf(req_path) {
244 Ok(item) => item,
245 Err(e) => return ok(req.error_response(e)),
246 };
247
248 let (req, _) = req.into_parts();
249
250 let mut item = self.files.get(real_path.as_str());
251
252 if item.is_none() && self.not_found_resolves_to.is_some() {
253 let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
254 item = self.files.get(not_found_path.as_str());
255 }
256
257 let response = respond_to(&req, item);
258 (req, response)
259 };
260
261 ok(ServiceResponse::new(req, response))
262 }
263}
264
265fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
266 if let Some(file) = item {
267 let etag = Some(header::EntityTag::new_strong(format!(
268 "{:x}:{:x}",
269 file.data.len(),
270 file.modified
271 )));
272
273 let precondition_failed = !any_match(etag.as_ref(), req);
274
275 let not_modified = !none_match(etag.as_ref(), req);
276
277 let mut resp = HttpResponse::build(StatusCode::OK);
278 resp.insert_header((header::CONTENT_TYPE, file.mime_type));
279
280 if let Some(etag) = etag {
281 resp.insert_header(header::ETag(etag));
282 }
283
284 if precondition_failed {
285 return resp.status(StatusCode::PRECONDITION_FAILED).finish();
286 } else if not_modified {
287 return resp.status(StatusCode::NOT_MODIFIED).finish();
288 }
289
290 resp.body(file.data)
291 } else {
292 HttpResponse::NotFound().body("Not found")
293 }
294}
295
296fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
298 match req.get_header::<header::IfMatch>() {
299 None | Some(header::IfMatch::Any) => true,
300 Some(header::IfMatch::Items(ref items)) => {
301 if let Some(some_etag) = etag {
302 for item in items {
303 if item.strong_eq(some_etag) {
304 return true;
305 }
306 }
307 }
308 false
309 }
310 }
311}
312
313fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
315 match req.get_header::<header::IfNoneMatch>() {
316 Some(header::IfNoneMatch::Any) => false,
317 Some(header::IfNoneMatch::Items(ref items)) => {
318 if let Some(some_etag) = etag {
319 for item in items {
320 if item.weak_eq(some_etag) {
321 return false;
322 }
323 }
324 }
325 true
326 }
327 None => true,
328 }
329}
330
331#[derive(Debug, PartialEq, Display, Error)]
332pub enum UriSegmentError {
333 #[display(fmt = "The segment started with the wrapped invalid character")]
335 BadStart(#[error(not(source))] char),
336
337 #[display(fmt = "The segment contained the wrapped invalid character")]
339 BadChar(#[error(not(source))] char),
340
341 #[display(fmt = "The segment ended with the wrapped invalid character")]
343 BadEnd(#[error(not(source))] char),
344}
345
346#[cfg(test)]
347mod tests_error_impl {
348 use super::*;
349
350 fn assert_send_and_sync<T: Send + Sync + 'static>() {}
351
352 #[test]
353 fn test_error_impl() {
354 assert_send_and_sync::<UriSegmentError>();
356 }
357}
358
359impl ResponseError for UriSegmentError {
361 fn error_response(&self) -> HttpResponse {
362 HttpResponse::new(StatusCode::BAD_REQUEST)
363 }
364}
365
366fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
367 let mut buf = Vec::new();
368 for segment in path.split('/') {
369 if segment == ".." {
370 buf.pop();
371 } else if segment.starts_with('.') {
372 return Err(UriSegmentError::BadStart('.'));
373 } else if segment.starts_with('*') {
374 return Err(UriSegmentError::BadStart('*'));
375 } else if segment.ends_with(':') {
376 return Err(UriSegmentError::BadEnd(':'));
377 } else if segment.ends_with('>') {
378 return Err(UriSegmentError::BadEnd('>'));
379 } else if segment.ends_with('<') {
380 return Err(UriSegmentError::BadEnd('<'));
381 } else if segment.is_empty() {
382 continue;
383 } else if cfg!(windows) && segment.contains('\\') {
384 return Err(UriSegmentError::BadChar('\\'));
385 } else {
386 buf.push(segment)
387 }
388 }
389
390 Ok(buf.join("/"))
391}