1use actix_service::{Service, ServiceFactory};
2use actix_web::{
3 dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
4 error::Error,
5 http::{header, Method, StatusCode},
6 HttpMessage, HttpRequest, HttpResponse, ResponseError,
7};
8use derive_more::{Display, Error};
9use futures::future::{ok, FutureExt, LocalBoxFuture, Ready};
10use std::{
11 collections::HashMap,
12 ops::Deref,
13 rc::Rc,
14 task::{Context, Poll},
15};
16
17pub struct Resource {
19 pub data: &'static [u8],
20 pub etag: String,
21 pub mime_type: String,
22}
23
24pub struct ResourceFiles {
47 not_resolve_defaults: bool,
48 not_found_resolves_to: Option<String>,
49 inner: Rc<ResourceFilesInner>,
50}
51
52pub struct ResourceFilesInner {
53 path: String,
54 files: HashMap<&'static str, Resource>,
55}
56
57const INDEX_HTML: &str = "index.html";
58
59impl ResourceFiles {
60 pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self {
61 let inner = ResourceFilesInner {
62 path: path.into(),
63 files,
64 };
65 Self {
66 inner: Rc::new(inner),
67 not_resolve_defaults: false,
68 not_found_resolves_to: None,
69 }
70 }
71
72 pub fn do_not_resolve_defaults(mut self) -> Self {
75 self.not_resolve_defaults = true;
76 self
77 }
78
79 pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
83 self.not_found_resolves_to = Some(path.to_string());
84 self
85 }
86
87 pub fn resolve_not_found_to_root(self) -> Self {
91 self.resolve_not_found_to(INDEX_HTML)
92 }
93}
94
95impl Deref for ResourceFiles {
96 type Target = ResourceFilesInner;
97
98 fn deref(&self) -> &Self::Target {
99 &self.inner
100 }
101}
102
103impl HttpServiceFactory for ResourceFiles {
104 fn register(self, config: &mut AppService) {
105 let rdef = if config.is_root() {
106 ResourceDef::root_prefix(&self.path)
107 } else {
108 ResourceDef::prefix(&self.path)
109 };
110 config.register_service(rdef, None, self, None)
111 }
112}
113
114impl ServiceFactory for ResourceFiles {
115 type Config = ();
116 type Request = ServiceRequest;
117 type Response = ServiceResponse;
118 type Error = Error;
119 type Service = ResourceFilesService;
120 type InitError = ();
121 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
122
123 fn new_service(&self, _: ()) -> Self::Future {
124 ok(ResourceFilesService {
125 resolve_defaults: !self.not_resolve_defaults,
126 not_found_resolves_to: self.not_found_resolves_to.clone(),
127 inner: self.inner.clone(),
128 })
129 .boxed_local()
130 }
131}
132
133pub struct ResourceFilesService {
134 resolve_defaults: bool,
135 not_found_resolves_to: Option<String>,
136 inner: Rc<ResourceFilesInner>,
137}
138
139impl Deref for ResourceFilesService {
140 type Target = ResourceFilesInner;
141
142 fn deref(&self) -> &Self::Target {
143 &self.inner
144 }
145}
146
147impl<'a> Service for ResourceFilesService {
148 type Request = ServiceRequest;
149 type Response = ServiceResponse;
150 type Error = Error;
151 type Future = Ready<Result<Self::Response, Self::Error>>;
152
153 fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
154 Poll::Ready(Ok(()))
155 }
156
157 fn call(&mut self, req: ServiceRequest) -> Self::Future {
158 match *req.method() {
159 Method::HEAD | Method::GET => (),
160 _ => {
161 return ok(ServiceResponse::new(
162 req.into_parts().0,
163 HttpResponse::MethodNotAllowed()
164 .header(header::CONTENT_TYPE, "text/plain")
165 .header(header::ALLOW, "GET, HEAD")
166 .body("This resource only supports GET and HEAD."),
167 ));
168 }
169 }
170
171 let req_path = req.match_info().path();
172
173 let mut item = self.files.get(req_path);
174
175 if item.is_none()
176 && self.resolve_defaults
177 && (req_path.is_empty() || req_path.ends_with("/"))
178 {
179 let index_req_path = req_path.to_string() + INDEX_HTML;
180 item = self.files.get(index_req_path.as_str());
181 }
182
183 let (req, response) = if item.is_some() {
184 let (req, _) = req.into_parts();
185 let response = respond_to(&req, item);
186 (req, response)
187 } else {
188 let real_path = match get_pathbuf(req_path) {
189 Ok(item) => item,
190 Err(e) => return ok(req.error_response(e)),
191 };
192
193 let (req, _) = req.into_parts();
194
195 let mut item = self.files.get(real_path.as_str());
196
197 if item.is_none() && self.not_found_resolves_to.is_some() {
198 let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
199 item = self.files.get(not_found_path.as_str());
200 }
201
202 let response = respond_to(&req, item);
203 (req, response)
204 };
205
206 ok(ServiceResponse::new(req, response))
207 }
208}
209
210fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
211 if let Some(file) = item {
212 let etag = Some(header::EntityTag::strong(file.etag.clone()));
213
214 let precondition_failed = !any_match(etag.as_ref(), req);
215
216 let not_modified = !none_match(etag.as_ref(), req);
217
218 let mut resp = HttpResponse::build(StatusCode::OK);
219 resp.set_header(header::CONTENT_TYPE, &file.mime_type[0..]);
220
221 if let Some(etag) = etag {
222 resp.set(header::ETag(etag));
223 }
224
225 if precondition_failed {
226 return resp.status(StatusCode::PRECONDITION_FAILED).finish();
227 } else if not_modified {
228 return resp.status(StatusCode::NOT_MODIFIED).finish();
229 }
230
231 resp.body(file.data)
232 } else {
233 HttpResponse::NotFound().body("Not found")
234 }
235}
236
237fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
239 match req.get_header::<header::IfMatch>() {
240 None | Some(header::IfMatch::Any) => true,
241 Some(header::IfMatch::Items(ref items)) => {
242 if let Some(some_etag) = etag {
243 for item in items {
244 if item.strong_eq(some_etag) {
245 return true;
246 }
247 }
248 }
249 false
250 }
251 }
252}
253
254fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
256 match req.get_header::<header::IfNoneMatch>() {
257 Some(header::IfNoneMatch::Any) => false,
258 Some(header::IfNoneMatch::Items(ref items)) => {
259 if let Some(some_etag) = etag {
260 for item in items {
261 if item.weak_eq(some_etag) {
262 return false;
263 }
264 }
265 }
266 true
267 }
268 None => true,
269 }
270}
271
272#[derive(Debug, PartialEq, Display, Error)]
273pub enum UriSegmentError {
274 #[display(fmt = "The segment started with the wrapped invalid character")]
276 BadStart(#[error(not(source))] char),
277
278 #[display(fmt = "The segment contained the wrapped invalid character")]
280 BadChar(#[error(not(source))] char),
281
282 #[display(fmt = "The segment ended with the wrapped invalid character")]
284 BadEnd(#[error(not(source))] char),
285}
286
287#[cfg(test)]
288mod tests_error_impl {
289 use super::*;
290
291 fn assert_send_and_sync<T: Send + Sync + 'static>() {}
292
293 #[test]
294 fn test_error_impl() {
295 assert_send_and_sync::<UriSegmentError>();
297 }
298}
299
300impl ResponseError for UriSegmentError {
302 fn error_response(&self) -> HttpResponse {
303 HttpResponse::new(StatusCode::BAD_REQUEST)
304 }
305}
306
307fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
308 let mut buf = Vec::new();
309 for segment in path.split('/') {
310 if segment == ".." {
311 buf.pop();
312 } else if segment.starts_with('.') {
313 return Err(UriSegmentError::BadStart('.'));
314 } else if segment.starts_with('*') {
315 return Err(UriSegmentError::BadStart('*'));
316 } else if segment.ends_with(':') {
317 return Err(UriSegmentError::BadEnd(':'));
318 } else if segment.ends_with('>') {
319 return Err(UriSegmentError::BadEnd('>'));
320 } else if segment.ends_with('<') {
321 return Err(UriSegmentError::BadEnd('<'));
322 } else if segment.is_empty() {
323 continue;
324 } else if cfg!(windows) && segment.contains('\\') {
325 return Err(UriSegmentError::BadChar('\\'));
326 } else {
327 buf.push(segment)
328 }
329 }
330
331 Ok(buf.join("/"))
332}