webmachine_rust/
lib.rs

1/*!
2# webmachine-rust
3
4Port of Webmachine-Ruby (https://github.com/webmachine/webmachine-ruby) to Rust.
5
6webmachine-rust is a port of the Ruby version of webmachine. It implements a finite state machine for the HTTP protocol
7that provides semantic HTTP handling (based on the [diagram from the webmachine project](https://webmachine.github.io/images/http-headers-status-v3.png)).
8It is basically a HTTP toolkit for building HTTP-friendly applications using the [Hyper](https://crates.io/crates/hyper) rust crate.
9
10Webmachine-rust works with Hyper and sits between the Hyper Handler and your application code. It provides a resource struct
11with callbacks to handle the decisions required as the state machine is executed against the request with the following sequence.
12
13REQUEST -> Hyper Handler -> WebmachineDispatcher -> Resource -> Your application code -> WebmachineResponse -> Hyper -> RESPONSE
14
15## Features
16
17- Handles the hard parts of content negotiation, conditional requests, and response codes for you.
18- Provides a resource trait and struct with points of extension to let you describe what is relevant about your particular resource.
19
20## Missing Features
21
22Currently, the following features from webmachine-ruby have not been implemented:
23
24- Visual debugger
25- Streaming response bodies
26
27## Implementation Deficiencies:
28
29This implementation has the following deficiencies:
30
31- Automatically decoding request bodies and encoding response bodies.
32- No easy mechanism to generate bodies with different content types (e.g. JSON vs. XML).
33- Dynamically determining the methods allowed on the resource.
34
35## Getting started with Hyper
36
37Follow the getting started documentation from the Hyper crate to setup a Hyper service for your server.
38
39There are two ways of using this crate. You can either use the `WebmachineResource` struct and add callbacks
40for the state you need to modify, or you can create your own resource structs and implement the
41`Resource` trait.
42
43You need to define a WebmachineDispatcher that maps resource paths to your webmachine resources
44(WebmachineResource or structs that implement Resource). WebmachineResource defines all the callbacks
45(via Closures) and values required to implement a resource.
46
47Note: This example uses the maplit crate to provide the `btreemap` macro and the log crate for the logging macros.
48
49 ```no_run
50 use std::convert::Infallible;
51 use std::future::ready;
52 use std::io::Read;
53 use std::net::SocketAddr;
54 use std::sync::Arc;
55
56 use webmachine_rust::*;
57 use webmachine_rust::context::*;
58 use webmachine_rust::headers::*;
59
60 use bytes::Bytes;
61 use futures_util::future::FutureExt;
62 use hyper::{body, Request};
63 use hyper::server::conn::http1;
64 use hyper::service::service_fn;
65 use hyper_util::rt::TokioIo;
66 use maplit::btreemap;
67 use serde_json::{Value, json};
68 use tracing::error;
69 use tokio::net::TcpListener;
70
71 # fn main() {}
72
73 async fn start_server() -> anyhow::Result<()> {
74   // setup the dispatcher, which maps paths to resources. We wrap it in an Arc so we can
75   // use it in the loop below.
76   let dispatcher = Arc::new(WebmachineDispatcher {
77       routes: btreemap!{
78          "/myresource" => WebmachineDispatcher::box_resource(WebmachineResource {
79            // Methods allowed on this resource
80            allowed_methods: owned_vec(&["OPTIONS", "GET", "HEAD", "POST"]),
81            // if the resource exists callback
82            resource_exists: callback(|_, _| true),
83            // callback to render the response for the resource, it has to be async
84            render_response: async_callback(|_, _| {
85                let json_response = json!({
86                   "data": [1, 2, 3, 4]
87                });
88                ready(Ok(Some(Bytes::from(json_response.to_string())))).boxed()
89            }),
90            // callback to process the post for the resource
91            process_post: async_callback(|_, _|  /* Handle the post here */ ready(Ok(true)).boxed() ),
92            // default everything else
93            .. WebmachineResource::default()
94          })
95      }
96   });
97
98   // Create a Hyper server that delegates to the dispatcher. See https://hyper.rs/guides/1/server/hello-world/
99   let addr: SocketAddr = "0.0.0.0:8080".parse()?;
100   let listener = TcpListener::bind(addr).await?;
101   loop {
102        let dispatcher = dispatcher.clone();
103        let (stream, _) = listener.accept().await?;
104        let io = TokioIo::new(stream);
105        tokio::task::spawn(async move {
106            if let Err(err) = http1::Builder::new()
107                .serve_connection(io, service_fn(|req: Request<body::Incoming>| dispatcher.dispatch(req)))
108                .await
109            {
110                error!("Error serving connection: {:?}", err);
111            }
112        });
113   }
114   Ok(())
115 }
116 ```
117
118## Example implementations
119
120For an example of a project using this crate, have a look at the [Pact Mock Server](https://github.com/pact-foundation/pact-core-mock-server/tree/main/pact_mock_server_cli) from the Pact reference implementation.
121*/
122
123#![warn(missing_docs)]
124
125use std::collections::{BTreeMap, HashMap};
126use std::fmt::{Debug, Display, Formatter};
127use std::future::ready;
128use std::pin::Pin;
129use std::time::SystemTime;
130use async_trait::async_trait;
131use bytes::Bytes;
132use chrono::{DateTime, FixedOffset, Utc};
133use futures_util::future::FutureExt;
134use http::{HeaderMap, Request, Response};
135use http_body_util::{BodyExt, Full};
136use hyper::body::{Body, Incoming};
137use itertools::Itertools;
138use lazy_static::lazy_static;
139use maplit::hashmap;
140use tracing::{debug, error, trace};
141
142use context::{WebmachineContext, WebmachineRequest, WebmachineResponse};
143use headers::HeaderValue;
144use crate::content_negotiation::acceptable_content_type;
145use crate::paths::map_path;
146
147#[macro_use] pub mod headers;
148pub mod context;
149pub mod content_negotiation;
150pub mod paths;
151
152/// Type of a Webmachine resource callback
153pub type WebmachineCallback<T> = Box<dyn Fn(&mut WebmachineContext, &WebmachineResource) -> T + Send + Sync>;
154
155/// Wrap a callback in a structure that is safe to call between threads
156pub fn callback<T, RT>(cb: T) -> WebmachineCallback<RT>
157  where T: Fn(&mut WebmachineContext, &WebmachineResource) -> RT + Send + Sync + 'static {
158  Box::new(cb)
159}
160
161/// Wrap an async callback in a structure that is safe to call between threads
162pub type AsyncWebmachineCallback<T> = Pin<Box<dyn Fn(&mut WebmachineContext, &WebmachineResource) -> Pin<Box<dyn Future<Output=T> + Send>> + Send + Sync>>;
163
164/// Wrap a callback in a structure that is safe to call between threads
165pub fn async_callback<T, RT>(cb: T) -> Pin<Box<T>>
166  where T: Fn(&mut WebmachineContext, &WebmachineResource) -> Pin<Box<dyn Future<Output=RT> + Send>> {
167  Box::pin(cb)
168}
169
170/// Convenience function to create a vector of string structs from a slice of strings
171pub fn owned_vec(strings: &[&str]) -> Vec<String> {
172  strings.iter().map(|s| s.to_string()).collect()
173}
174
175/// All webmachine resources implement this trait
176#[async_trait]
177pub trait Resource: Debug {
178  /// This is called just before the final response is constructed and sent. It allows the resource
179  /// an opportunity to modify the response after the webmachine has executed.
180  fn finalise_response(&self, _context: &mut WebmachineContext) {}
181
182  /// This is invoked to render the response for the resource
183  async fn render_response(&self, _context: &mut WebmachineContext) -> anyhow::Result<Option<Bytes>> {
184    Ok(None)
185  }
186
187  /// Is the resource available? Returning false will result in a '503 Service Not Available'
188  /// response. Defaults to true. If the resource is only temporarily not available,
189  /// add a 'Retry-After' response header.
190  fn available(&self, _context: &mut WebmachineContext) -> bool {
191    true
192  }
193
194  /// HTTP methods that are known to the resource. Default includes all standard HTTP methods.
195  /// One could override this to allow additional methods
196  fn known_methods(&self) -> Vec<&str> {
197    vec!["OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "CONNECT", "PATCH"]
198  }
199
200  /// If the URI is too long to be processed, this should return true, which will result in a
201  /// '414 Request URI Too Long' response. Defaults to false.
202  fn uri_too_long(&self, _context: &mut WebmachineContext) -> bool {
203    false
204  }
205
206  /// HTTP methods that are allowed on this resource. Defaults to GET','HEAD and 'OPTIONS'.
207  fn allowed_methods(&self) -> Vec<&str> {
208    vec!["OPTIONS", "GET", "HEAD"]
209  }
210
211  /// If the request is malformed, this should return true, which will result in a
212  /// '400 Malformed Request' response. Defaults to false.
213  fn malformed_request(&self, _context: &mut WebmachineContext) -> bool {
214    false
215  }
216
217  /// Is the client or request not authorized? Returning a Some<String>
218  /// will result in a '401 Unauthorized' response.  Defaults to None. If a Some(String) is
219  /// returned, the string will be used as the value in the WWW-Authenticate header.
220  fn not_authorized(&self, _context: &mut WebmachineContext) -> Option<String> {
221    None
222  }
223
224  /// Is the request or client forbidden? Returning true will result in a '403 Forbidden' response.
225  /// Defaults to false.
226  fn forbidden(&self, _context: &mut WebmachineContext) -> bool {
227    false
228  }
229
230  /// If the request includes any invalid Content-* headers, this should return true, which will
231  /// result in a '501 Not Implemented' response. Defaults to false.
232  fn unsupported_content_headers(&self, _context: &mut WebmachineContext) -> bool {
233    false
234  }
235
236  /// The list of acceptable content types. Defaults to 'application/json'. If the content type
237  /// of the request is not in this list, a '415 Unsupported Media Type' response is returned.
238  /// Wild cards can be used, like `*/*`, `type/*` or `*/sub-type`.
239  fn acceptable_content_types(&self, _context: &mut WebmachineContext) -> Vec<&str> {
240    vec!["application/json"]
241  }
242
243  /// If the entity length on PUT or POST is invalid, this should return false, which will result
244  /// in a '413 Request Entity Too Large' response. Defaults to true.
245  fn valid_entity_length(&self, _context: &mut WebmachineContext) -> bool {
246    true
247  }
248
249  /// This is called just after the response body is rendered and before the final response is
250  /// constructed and sent. This allows the response to be modified. The default implementation
251  /// adds CORS headers to the response.
252  fn finish_request(&self, context: &mut WebmachineContext) {
253    context.response.add_cors_headers(self.allowed_methods().as_slice())
254  }
255
256  /// If the OPTIONS method is supported and is used, this returns a HashMap of headers that
257  /// should appear in the response. Defaults to CORS headers.
258  fn options(&self, _context: &mut WebmachineContext) -> Option<HashMap<String, Vec<String>>> {
259    Some(WebmachineResponse::cors_headers(self.allowed_methods().as_slice()))
260  }
261
262  /// The list of content types that this resource produces. Defaults to 'application/json'. If
263  /// more than one is provided, and the client does not supply an Accept header, the first one
264  /// will be selected.
265  fn produces(&self) -> Vec<&str> {
266    vec!["application/json"]
267  }
268
269  /// The list of content languages that this resource provides. Defaults to an empty list,
270  /// which represents all languages. If more than one is provided, and the client does not
271  /// supply an Accept-Language header, the first one will be selected.
272  fn languages_provided(&self) -> Vec<&str> {
273    vec![]
274  }
275
276  /// The list of charsets that this resource provides. Defaults to an empty list,
277  /// which represents all charsets with ISO-8859-1 as the default. If more than one is provided,
278  /// and the client does not supply an Accept-Charset header, the first one will be selected.
279  fn charsets_provided(&self) -> Vec<&str> {
280    vec![]
281  }
282
283  /// The list of encodings your resource wants to provide. The encoding will be applied to the
284  /// response body automatically by Webmachine. Default includes only the 'identity' encoding.
285  fn encodings_provided(&self) -> Vec<&str> {
286    vec!["identity"]
287  }
288
289  /// The list of header names that should be included in the response's Vary header. The standard
290  /// content negotiation headers (Accept, Accept-Encoding, Accept-Charset, Accept-Language) do
291  /// not need to be specified here as Webmachine will add the correct elements of those
292  /// automatically depending on resource behavior. Default is an empty list.
293  fn variances(&self) -> Vec<&str> {
294    vec![]
295  }
296
297  /// Does the resource exist? Returning a false value will result in a '404 Not Found' response
298  /// unless it is a PUT or POST. Defaults to true.
299  async fn resource_exists(&self, _context: &mut WebmachineContext) -> bool {
300    true
301  }
302
303  /// If this resource is known to have existed previously, this should return true. Default is false.
304  fn previously_existed(&self, _context: &mut WebmachineContext) -> bool {
305    false
306  }
307
308  /// If this resource has moved to a new location permanently, this should return the new
309  /// location as a String. Default is to return None
310  fn moved_permanently(&self, _context: &mut WebmachineContext) -> Option<String> {
311    None
312  }
313
314  /// If this resource has moved to a new location temporarily, this should return the new
315  /// location as a String. Default is to return None
316  fn moved_temporarily(&self, _context: &mut WebmachineContext) -> Option<String> {
317    None
318  }
319
320  /// If this returns true, the client will receive a '409 Conflict' response. This is only
321  /// called for PUT requests. Default is false.
322  fn is_conflict(&self, _context: &mut WebmachineContext) -> bool {
323    false
324  }
325
326  /// Return true if the resource accepts POST requests to nonexistent resources. Defaults to false.
327  fn allow_missing_post(&self, _context: &mut WebmachineContext) -> bool {
328    false
329  }
330
331  /// If this returns a value, it will be used as the value of the ETag header and for
332  /// comparison in conditional requests. Default is None.
333  fn generate_etag(&self, _context: &mut WebmachineContext) -> Option<String> {
334    None
335  }
336
337  /// Returns the last modified date and time of the resource which will be added as the
338  /// Last-Modified header in the response and used in negotiating conditional requests.
339  /// Default is None
340  fn last_modified(&self, _context: &mut WebmachineContext) -> Option<DateTime<FixedOffset>> {
341    None
342  }
343
344  /// Called when a DELETE request should be enacted. Return `Ok(true)` if the deletion succeeded,
345  /// and `Ok(false)` if the deletion was accepted but cannot yet be guaranteed to have finished.
346  /// If the delete fails for any reason, return an Err with the status code you wish returned
347  /// (a 500 status makes sense).
348  /// Defaults to `Ok(true)`.
349  async fn delete_resource(&self, _context: &mut WebmachineContext) -> Result<bool, u16> {
350    Ok(true)
351  }
352
353  /// If POST requests should be treated as a request to put content into a (potentially new)
354  /// resource as opposed to a generic submission for processing, then this should return true.
355  /// If it does return true, then `create_path` will be called and the rest of the request will
356  /// be treated much like a PUT to the path returned by that call. Default is false.
357  fn post_is_create(&self, _context: &mut WebmachineContext) -> bool {
358    false
359  }
360
361  /// If `post_is_create` returns false, then this will be called to process any POST request.
362  /// If it succeeds, return `Ok(true)`, `Ok(false)` otherwise. If it fails for any reason,
363  /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
364  /// Default is false. If you want the result of processing the POST to be a redirect, set
365  /// `context.redirect` to true.
366  async fn process_post(&self, _context: &mut WebmachineContext) -> Result<bool, u16> {
367    Ok(false)
368  }
369
370  /// This will be called on a POST request if `post_is_create` returns true. It should create
371  /// the new resource and return the path as a valid URI part following the dispatcher prefix.
372  /// That path will replace the previous one in the return value of `WebmachineRequest.request_path`
373  /// for all subsequent resource function calls in the course of this request and will be set
374  /// as the value of the Location header of the response. If it fails for any reason,
375  /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
376  /// Default will return an `Ok(WebmachineRequest.request_path)`. If you want the result of
377  /// processing the POST to be a redirect, set `context.redirect` to true.
378  async fn create_path(&self, context: &mut WebmachineContext) -> Result<String, u16> {
379    Ok(context.request.request_path.clone())
380  }
381
382  /// This will be called to process any PUT request. If it succeeds, return `Ok(true)`,
383  /// `Ok(false)` otherwise. If it fails for any reason, return an Err with the status code
384  /// you wish returned (e.g., a 500 status makes sense). Default is `Ok(true)`
385  async fn process_put(&self, _context: &mut WebmachineContext) -> Result<bool, u16> {
386    Ok(true)
387  }
388
389  /// If this returns true, then it is assumed that multiple representations of the response are
390  /// possible and a single one cannot be automatically chosen, so a 300 Multiple Choices will
391  /// be sent instead of a 200. Default is false.
392  fn multiple_choices(&self, _context: &mut WebmachineContext) -> bool {
393    false
394  }
395
396  /// If the resource expires, this should return the date/time it expires. Default is None.
397  fn expires(&self, _context: &mut WebmachineContext) -> Option<DateTime<FixedOffset>> {
398    None
399  }
400}
401
402/// Struct to represent a resource in webmachine
403pub struct WebmachineResource {
404  /// This is called just before the final response is constructed and sent. It allows the resource
405  /// an opportunity to modify the response after the webmachine has executed.
406  pub finalise_response: Option<WebmachineCallback<()>>,
407  /// This is invoked to render the response for the resource
408  pub render_response: AsyncWebmachineCallback<anyhow::Result<Option<Bytes>>>,
409  /// Is the resource available? Returning false will result in a '503 Service Not Available'
410  /// response. Defaults to true. If the resource is only temporarily not available,
411  /// add a 'Retry-After' response header.
412  pub available: WebmachineCallback<bool>,
413  /// HTTP methods that are known to the resource. Default includes all standard HTTP methods.
414  /// One could override this to allow additional methods
415  pub known_methods: Vec<String>,
416  /// If the URI is too long to be processed, this should return true, which will result in a
417  /// '414 Request URI Too Long' response. Defaults to false.
418  pub uri_too_long: WebmachineCallback<bool>,
419  /// HTTP methods that are allowed on this resource. Defaults to GET','HEAD and 'OPTIONS'.
420  pub allowed_methods: Vec<String>,
421  /// If the request is malformed, this should return true, which will result in a
422  /// '400 Malformed Request' response. Defaults to false.
423  pub malformed_request: WebmachineCallback<bool>,
424  /// Is the client or request not authorized? Returning a Some<String>
425  /// will result in a '401 Unauthorized' response.  Defaults to None. If a Some(String) is
426  /// returned, the string will be used as the value in the WWW-Authenticate header.
427  pub not_authorized: WebmachineCallback<Option<String>>,
428  /// Is the request or client forbidden? Returning true will result in a '403 Forbidden' response.
429  /// Defaults to false.
430  pub forbidden: WebmachineCallback<bool>,
431  /// If the request includes any invalid Content-* headers, this should return true, which will
432  /// result in a '501 Not Implemented' response. Defaults to false.
433  pub unsupported_content_headers: WebmachineCallback<bool>,
434  /// The list of acceptable content types. Defaults to 'application/json'. If the content type
435  /// of the request is not in this list, a '415 Unsupported Media Type' response is returned.
436  /// Wild cards can be used, like `*/*`, `type/*` or `*/sub-type`.
437  pub acceptable_content_types: Vec<String>,
438  /// If the entity length on PUT or POST is invalid, this should return false, which will result
439  /// in a '413 Request Entity Too Large' response. Defaults to true.
440  pub valid_entity_length: WebmachineCallback<bool>,
441  /// This is called just after the response body is rendered and before the final response is
442  /// constructed and sent. This allows the response to be modified. The default implementation
443  /// adds CORS headers to the response.
444  pub finish_request: WebmachineCallback<()>,
445  /// If the OPTIONS method is supported and is used, this returns a HashMap of headers that
446  /// should appear in the response. Defaults to CORS headers.
447  pub options: WebmachineCallback<Option<HashMap<String, Vec<String>>>>,
448  /// The list of content types that this resource produces. Defaults to 'application/json'. If
449  /// more than one is provided, and the client does not supply an Accept header, the first one
450  /// will be selected.
451  pub produces: Vec<String>,
452  /// The list of content languages that this resource provides. Defaults to an empty list,
453  /// which represents all languages. If more than one is provided, and the client does not
454  /// supply an Accept-Language header, the first one will be selected.
455  pub languages_provided: Vec<String>,
456  /// The list of charsets that this resource provides. Defaults to an empty list,
457  /// which represents all charsets with ISO-8859-1 as the default. If more than one is provided,
458  /// and the client does not supply an Accept-Charset header, the first one will be selected.
459  pub charsets_provided: Vec<String>,
460  /// The list of encodings your resource wants to provide. The encoding will be applied to the
461  /// response body automatically by Webmachine. Default includes only the 'identity' encoding.
462  pub encodings_provided: Vec<String>,
463  /// The list of header names that should be included in the response's Vary header. The standard
464  /// content negotiation headers (Accept, Accept-Encoding, Accept-Charset, Accept-Language) do
465  /// not need to be specified here as Webmachine will add the correct elements of those
466  /// automatically depending on resource behavior. Default is an empty list.
467  pub variances: Vec<String>,
468  /// Does the resource exist? Returning a false value will result in a '404 Not Found' response
469  /// unless it is a PUT or POST. Defaults to true.
470  pub resource_exists: WebmachineCallback<bool>,
471  /// If this resource is known to have existed previously, this should return true. Default is false.
472  pub previously_existed: WebmachineCallback<bool>,
473  /// If this resource has moved to a new location permanently, this should return the new
474  /// location as a String. Default is to return None
475  pub moved_permanently: WebmachineCallback<Option<String>>,
476  /// If this resource has moved to a new location temporarily, this should return the new
477  /// location as a String. Default is to return None
478  pub moved_temporarily: WebmachineCallback<Option<String>>,
479  /// If this returns true, the client will receive a '409 Conflict' response. This is only
480  /// called for PUT requests. Default is false.
481  pub is_conflict: WebmachineCallback<bool>,
482  /// Return true if the resource accepts POST requests to nonexistent resources. Defaults to false.
483  pub allow_missing_post: WebmachineCallback<bool>,
484  /// If this returns a value, it will be used as the value of the ETag header and for
485  /// comparison in conditional requests. Default is None.
486  pub generate_etag: WebmachineCallback<Option<String>>,
487  /// Returns the last modified date and time of the resource which will be added as the
488  /// Last-Modified header in the response and used in negotiating conditional requests.
489  /// Default is None
490  pub last_modified: WebmachineCallback<Option<DateTime<FixedOffset>>>,
491  /// Called when a DELETE request should be enacted. Return `Ok(true)` if the deletion succeeded,
492  /// and `Ok(false)` if the deletion was accepted but cannot yet be guaranteed to have finished.
493  /// If the delete fails for any reason, return an Err with the status code you wish returned
494  /// (a 500 status makes sense).
495  /// Defaults to `Ok(true)`.
496  pub delete_resource: WebmachineCallback<Result<bool, u16>>,
497  /// If POST requests should be treated as a request to put content into a (potentially new)
498  /// resource as opposed to a generic submission for processing, then this should return true.
499  /// If it does return true, then `create_path` will be called and the rest of the request will
500  /// be treated much like a PUT to the path returned by that call. Default is false.
501  pub post_is_create: WebmachineCallback<bool>,
502  /// If `post_is_create` returns false, then this will be called to process any POST request.
503  /// If it succeeds, return `Ok(true)`, `Ok(false)` otherwise. If it fails for any reason,
504  /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
505  /// Default is false. If you want the result of processing the POST to be a redirect, set
506  /// `context.redirect` to true.
507  pub process_post: AsyncWebmachineCallback<Result<bool, u16>>,
508  /// This will be called on a POST request if `post_is_create` returns true. It should create
509  /// the new resource and return the path as a valid URI part following the dispatcher prefix.
510  /// That path will replace the previous one in the return value of `WebmachineRequest.request_path`
511  /// for all subsequent resource function calls in the course of this request and will be set
512  /// as the value of the Location header of the response. If it fails for any reason,
513  /// return an Err with the status code you wish returned (e.g., a 500 status makes sense).
514  /// Default will return an `Ok(WebmachineRequest.request_path)`. If you want the result of
515  /// processing the POST to be a redirect, set `context.redirect` to true.
516  pub create_path: WebmachineCallback<Result<String, u16>>,
517  /// This will be called to process any PUT request. If it succeeds, return `Ok(true)`,
518  /// `Ok(false)` otherwise. If it fails for any reason, return an Err with the status code
519  /// you wish returned (e.g., a 500 status makes sense). Default is `Ok(true)`
520  pub process_put: WebmachineCallback<Result<bool, u16>>,
521  /// If this returns true, then it is assumed that multiple representations of the response are
522  /// possible and a single one cannot be automatically chosen, so a 300 Multiple Choices will
523  /// be sent instead of a 200. Default is false.
524  pub multiple_choices: WebmachineCallback<bool>,
525  /// If the resource expires, this should return the date/time it expires. Default is None.
526  pub expires: WebmachineCallback<Option<DateTime<FixedOffset>>>
527}
528
529fn true_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
530  true
531}
532
533fn false_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
534  false
535}
536
537fn none_fn<T>(_: &mut WebmachineContext, _: &WebmachineResource) -> Option<T> {
538  None
539}
540
541impl Default for WebmachineResource {
542  fn default() -> WebmachineResource {
543    WebmachineResource {
544      finalise_response: None,
545      available: callback(true_fn),
546      known_methods: owned_vec(&["OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "CONNECT", "PATCH"]),
547      uri_too_long: callback(false_fn),
548      allowed_methods: owned_vec(&["OPTIONS", "GET", "HEAD"]),
549      malformed_request: callback(false_fn),
550      not_authorized: callback(none_fn),
551      forbidden: callback(false_fn),
552      unsupported_content_headers: callback(false_fn),
553      acceptable_content_types: owned_vec(&["application/json"]),
554      valid_entity_length: callback(true_fn),
555      finish_request: callback(|context, resource| {
556        let methods = resource.allowed_methods.iter()
557          .map(|m| m.as_str())
558          .collect_vec();
559        context.response.add_cors_headers(methods.as_slice())
560      }),
561      options: callback(|_, resource| {
562        let methods = resource.allowed_methods.iter()
563          .map(|m| m.as_str())
564          .collect_vec();
565        Some(WebmachineResponse::cors_headers(methods.as_slice()))
566      }),
567      produces: vec!["application/json".to_string()],
568      languages_provided: Vec::new(),
569      charsets_provided: Vec::new(),
570      encodings_provided: vec!["identity".to_string()],
571      variances: Vec::new(),
572      resource_exists: callback(true_fn),
573      previously_existed: callback(false_fn),
574      moved_permanently: callback(none_fn),
575      moved_temporarily: callback(none_fn),
576      is_conflict: callback(false_fn),
577      allow_missing_post: callback(false_fn),
578      generate_etag: callback(none_fn),
579      last_modified: callback(none_fn),
580      delete_resource: callback(|_, _| Ok(true)),
581      post_is_create: callback(false_fn),
582      process_post: async_callback(|_, _| ready(Ok(false)).boxed()),
583      process_put: callback(|_, _| Ok(true)),
584      multiple_choices: callback(false_fn),
585      create_path: callback(|context, _| Ok(context.request.request_path.clone())),
586      expires: callback(none_fn),
587      render_response: async_callback(|_, _| ready(Ok(None)).boxed())
588    }
589  }
590}
591
592impl Debug for WebmachineResource {
593  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
594    write!(f, "WebmachineResource{{}}")
595  }
596}
597
598#[async_trait]
599impl Resource for WebmachineResource {
600  fn finalise_response(&self, context: &mut WebmachineContext) {
601    if let Some(callback) = &self.finalise_response {
602      callback(context, self);
603    }
604  }
605
606  async fn render_response(&self, context: &mut WebmachineContext) -> anyhow::Result<Option<Bytes>> {
607    (self.render_response)(context, self).await
608  }
609
610  fn available(&self, context: &mut WebmachineContext) -> bool {
611    (self.available)(context, self)
612  }
613
614  fn known_methods(&self) -> Vec<&str> {
615    self.known_methods
616      .iter()
617      .map(|s| s.as_str())
618      .collect()
619  }
620
621  fn uri_too_long(&self, context: &mut WebmachineContext) -> bool {
622    (self.uri_too_long)(context, self)
623  }
624
625  fn allowed_methods(&self) -> Vec<&str> {
626    self.allowed_methods
627      .iter()
628      .map(|s| s.as_str())
629      .collect()
630  }
631
632  fn malformed_request(&self, context: &mut WebmachineContext) -> bool {
633    (self.malformed_request)(context, self)
634  }
635
636  fn not_authorized(&self, context: &mut WebmachineContext) -> Option<String> {
637    (self.not_authorized)(context, self)
638  }
639
640  fn forbidden(&self, context: &mut WebmachineContext) -> bool {
641    (self.forbidden)(context, self)
642  }
643
644  fn unsupported_content_headers(&self, context: &mut WebmachineContext) -> bool {
645    (self.unsupported_content_headers)(context, self)
646  }
647
648  fn acceptable_content_types(&self, _context: &mut WebmachineContext) -> Vec<&str> {
649    self.acceptable_content_types
650      .iter()
651      .map(|s| s.as_str())
652      .collect_vec()
653  }
654
655  fn valid_entity_length(&self, context: &mut WebmachineContext) -> bool {
656    (self.valid_entity_length)(context, self)
657  }
658
659  fn finish_request(&self, context: &mut WebmachineContext) {
660    (self.finish_request)(context, self)
661  }
662
663  fn options(&self, context: &mut WebmachineContext) -> Option<HashMap<String, Vec<String>>> {
664    (self.options)(context, self)
665  }
666
667  fn produces(&self) -> Vec<&str> {
668    self.produces
669      .iter()
670      .map(|s| s.as_str())
671      .collect_vec()
672  }
673
674  fn languages_provided(&self) -> Vec<&str> {
675    self.languages_provided.iter()
676      .map(|s| s.as_str())
677      .collect_vec()
678  }
679
680  fn charsets_provided(&self) -> Vec<&str> {
681    self.charsets_provided.iter()
682      .map(|s| s.as_str())
683      .collect_vec()
684  }
685
686  fn encodings_provided(&self) -> Vec<&str> {
687    self.encodings_provided.iter()
688      .map(|s| s.as_str())
689      .collect_vec()
690  }
691
692  fn variances(&self) -> Vec<&str> {
693    self.variances.iter()
694      .map(|s| s.as_str())
695      .collect_vec()
696  }
697
698  async fn resource_exists(&self, context: &mut WebmachineContext) -> bool {
699    (self.resource_exists)(context, self)
700  }
701
702  fn previously_existed(&self, context: &mut WebmachineContext) -> bool {
703    (self.previously_existed)(context, self)
704  }
705
706  fn moved_permanently(&self, context: &mut WebmachineContext) -> Option<String> {
707    (self.moved_permanently)(context, self)
708  }
709
710  fn moved_temporarily(&self, context: &mut WebmachineContext) -> Option<String> {
711    (self.moved_temporarily)(context, self)
712  }
713
714  fn is_conflict(&self, context: &mut WebmachineContext) -> bool {
715    (self.is_conflict)(context, self)
716  }
717
718  fn allow_missing_post(&self, context: &mut WebmachineContext) -> bool {
719    (self.allow_missing_post)(context, self)
720  }
721
722  fn generate_etag(&self, context: &mut WebmachineContext) -> Option<String> {
723    (self.generate_etag)(context, self)
724  }
725
726  fn last_modified(&self, context: &mut WebmachineContext) -> Option<DateTime<FixedOffset>> {
727    (self.last_modified)(context, self)
728  }
729
730  async fn delete_resource(&self, context: &mut WebmachineContext) -> Result<bool, u16> {
731    (self.delete_resource)(context, self)
732  }
733
734  fn post_is_create(&self, context: &mut WebmachineContext) -> bool {
735    (self.post_is_create)(context, self)
736  }
737
738  async fn process_post(&self, context: &mut WebmachineContext) -> Result<bool, u16> {
739    (self.process_post)(context, self).await
740  }
741
742  async fn create_path(&self, context: &mut WebmachineContext) -> Result<String, u16> {
743    (self.create_path)(context, self)
744  }
745
746  async fn process_put(&self, context: &mut WebmachineContext) -> Result<bool, u16> {
747    (self.process_put)(context, self)
748  }
749
750  fn multiple_choices(&self, context: &mut WebmachineContext) -> bool {
751    (self.multiple_choices)(context, self)
752  }
753
754  fn expires(&self, context: &mut WebmachineContext) -> Option<DateTime<FixedOffset>> {
755    (self.expires)(context, self)
756  }
757}
758
759fn sanitise_path(path: &str) -> Vec<String> {
760  path.split("/").filter(|p| !p.is_empty()).map(|p| p.to_string()).collect()
761}
762
763fn join_paths(base: &Vec<String>, path: &Vec<String>) -> String {
764  let mut paths = base.clone();
765  paths.extend_from_slice(path);
766  let filtered: Vec<String> = paths.iter().cloned().filter(|p| !p.is_empty()).collect();
767  if filtered.is_empty() {
768    "/".to_string()
769  } else {
770    let new_path = filtered.iter().join("/");
771    if new_path.starts_with("/") {
772      new_path
773    } else {
774      "/".to_owned() + &new_path
775    }
776  }
777}
778
779const MAX_STATE_MACHINE_TRANSITIONS: u8 = 100;
780
781#[derive(Debug, Clone, PartialEq, Eq, Hash)]
782enum Decision {
783    Start,
784    End(u16),
785    A3Options,
786    B3Options,
787    B4RequestEntityTooLarge,
788    B5UnknownContentType,
789    B6UnsupportedContentHeader,
790    B7Forbidden,
791    B8Authorized,
792    B9MalformedRequest,
793    B10MethodAllowed,
794    B11UriTooLong,
795    B12KnownMethod,
796    B13Available,
797    C3AcceptExists,
798    C4AcceptableMediaTypeAvailable,
799    D4AcceptLanguageExists,
800    D5AcceptableLanguageAvailable,
801    E5AcceptCharsetExists,
802    E6AcceptableCharsetAvailable,
803    F6AcceptEncodingExists,
804    F7AcceptableEncodingAvailable,
805    G7ResourceExists,
806    G8IfMatchExists,
807    G9IfMatchStarExists,
808    G11EtagInIfMatch,
809    H7IfMatchStarExists,
810    H10IfUnmodifiedSinceExists,
811    H11IfUnmodifiedSinceValid,
812    H12LastModifiedGreaterThanUMS,
813    I4HasMovedPermanently,
814    I12IfNoneMatchExists,
815    I13IfNoneMatchStarExists,
816    I7Put,
817    J18GetHead,
818    K5HasMovedPermanently,
819    K7ResourcePreviouslyExisted,
820    K13ETagInIfNoneMatch,
821    L5HasMovedTemporarily,
822    L7Post,
823    L13IfModifiedSinceExists,
824    L14IfModifiedSinceValid,
825    L15IfModifiedSinceGreaterThanNow,
826    L17IfLastModifiedGreaterThanMS,
827    M5Post,
828    M7PostToMissingResource,
829    M16Delete,
830    M20DeleteEnacted,
831    N5PostToMissingResource,
832    N11Redirect,
833    N16Post,
834    O14Conflict,
835    O16Put,
836    O18MultipleRepresentations,
837    O20ResponseHasBody,
838    P3Conflict,
839    P11NewResource
840}
841
842impl Decision {
843    fn is_terminal(&self) -> bool {
844        match self {
845            &Decision::End(_) => true,
846            &Decision::A3Options => true,
847            _ => false
848        }
849    }
850}
851
852enum Transition {
853  To(Decision),
854  Branch(Decision, Decision)
855}
856
857#[derive(Debug, Clone, PartialEq, Eq, Hash)]
858enum DecisionResult {
859  True(String),
860  False(String),
861  StatusCode(u16)
862}
863
864impl DecisionResult {
865  fn wrap(result: bool, reason: &str) -> DecisionResult {
866    if result {
867      DecisionResult::True(format!("is: {}", reason))
868    } else {
869      DecisionResult::False(format!("is not: {}", reason))
870    }
871  }
872}
873
874lazy_static! {
875    static ref TRANSITION_MAP: HashMap<Decision, Transition> = hashmap!{
876        Decision::Start => Transition::To(Decision::B13Available),
877        Decision::B3Options => Transition::Branch(Decision::A3Options, Decision::C3AcceptExists),
878        Decision::B4RequestEntityTooLarge => Transition::Branch(Decision::End(413), Decision::B3Options),
879        Decision::B5UnknownContentType => Transition::Branch(Decision::End(415), Decision::B4RequestEntityTooLarge),
880        Decision::B6UnsupportedContentHeader => Transition::Branch(Decision::End(501), Decision::B5UnknownContentType),
881        Decision::B7Forbidden => Transition::Branch(Decision::End(403), Decision::B6UnsupportedContentHeader),
882        Decision::B8Authorized => Transition::Branch(Decision::B7Forbidden, Decision::End(401)),
883        Decision::B9MalformedRequest => Transition::Branch(Decision::End(400), Decision::B8Authorized),
884        Decision::B10MethodAllowed => Transition::Branch(Decision::B9MalformedRequest, Decision::End(405)),
885        Decision::B11UriTooLong => Transition::Branch(Decision::End(414), Decision::B10MethodAllowed),
886        Decision::B12KnownMethod => Transition::Branch(Decision::B11UriTooLong, Decision::End(501)),
887        Decision::B13Available => Transition::Branch(Decision::B12KnownMethod, Decision::End(503)),
888        Decision::C3AcceptExists => Transition::Branch(Decision::C4AcceptableMediaTypeAvailable, Decision::D4AcceptLanguageExists),
889        Decision::C4AcceptableMediaTypeAvailable => Transition::Branch(Decision::D4AcceptLanguageExists, Decision::End(406)),
890        Decision::D4AcceptLanguageExists => Transition::Branch(Decision::D5AcceptableLanguageAvailable, Decision::E5AcceptCharsetExists),
891        Decision::D5AcceptableLanguageAvailable => Transition::Branch(Decision::E5AcceptCharsetExists, Decision::End(406)),
892        Decision::E5AcceptCharsetExists => Transition::Branch(Decision::E6AcceptableCharsetAvailable, Decision::F6AcceptEncodingExists),
893        Decision::E6AcceptableCharsetAvailable => Transition::Branch(Decision::F6AcceptEncodingExists, Decision::End(406)),
894        Decision::F6AcceptEncodingExists => Transition::Branch(Decision::F7AcceptableEncodingAvailable, Decision::G7ResourceExists),
895        Decision::F7AcceptableEncodingAvailable => Transition::Branch(Decision::G7ResourceExists, Decision::End(406)),
896        Decision::G7ResourceExists => Transition::Branch(Decision::G8IfMatchExists, Decision::H7IfMatchStarExists),
897        Decision::G8IfMatchExists => Transition::Branch(Decision::G9IfMatchStarExists, Decision::H10IfUnmodifiedSinceExists),
898        Decision::G9IfMatchStarExists => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::G11EtagInIfMatch),
899        Decision::G11EtagInIfMatch => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::End(412)),
900        Decision::H7IfMatchStarExists => Transition::Branch(Decision::End(412), Decision::I7Put),
901        Decision::H10IfUnmodifiedSinceExists => Transition::Branch(Decision::H11IfUnmodifiedSinceValid, Decision::I12IfNoneMatchExists),
902        Decision::H11IfUnmodifiedSinceValid => Transition::Branch(Decision::H12LastModifiedGreaterThanUMS, Decision::I12IfNoneMatchExists),
903        Decision::H12LastModifiedGreaterThanUMS => Transition::Branch(Decision::End(412), Decision::I12IfNoneMatchExists),
904        Decision::I4HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::P3Conflict),
905        Decision::I7Put => Transition::Branch(Decision::I4HasMovedPermanently, Decision::K7ResourcePreviouslyExisted),
906        Decision::I12IfNoneMatchExists => Transition::Branch(Decision::I13IfNoneMatchStarExists, Decision::L13IfModifiedSinceExists),
907        Decision::I13IfNoneMatchStarExists => Transition::Branch(Decision::J18GetHead, Decision::K13ETagInIfNoneMatch),
908        Decision::J18GetHead => Transition::Branch(Decision::End(304), Decision::End(412)),
909        Decision::K13ETagInIfNoneMatch => Transition::Branch(Decision::J18GetHead, Decision::L13IfModifiedSinceExists),
910        Decision::K5HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::L5HasMovedTemporarily),
911        Decision::K7ResourcePreviouslyExisted => Transition::Branch(Decision::K5HasMovedPermanently, Decision::L7Post),
912        Decision::L5HasMovedTemporarily => Transition::Branch(Decision::End(307), Decision::M5Post),
913        Decision::L7Post => Transition::Branch(Decision::M7PostToMissingResource, Decision::End(404)),
914        Decision::L13IfModifiedSinceExists => Transition::Branch(Decision::L14IfModifiedSinceValid, Decision::M16Delete),
915        Decision::L14IfModifiedSinceValid => Transition::Branch(Decision::L15IfModifiedSinceGreaterThanNow, Decision::M16Delete),
916        Decision::L15IfModifiedSinceGreaterThanNow => Transition::Branch(Decision::M16Delete, Decision::L17IfLastModifiedGreaterThanMS),
917        Decision::L17IfLastModifiedGreaterThanMS => Transition::Branch(Decision::M16Delete, Decision::End(304)),
918        Decision::M5Post => Transition::Branch(Decision::N5PostToMissingResource, Decision::End(410)),
919        Decision::M7PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(404)),
920        Decision::M16Delete => Transition::Branch(Decision::M20DeleteEnacted, Decision::N16Post),
921        Decision::M20DeleteEnacted => Transition::Branch(Decision::O20ResponseHasBody, Decision::End(202)),
922        Decision::N5PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(410)),
923        Decision::N11Redirect => Transition::Branch(Decision::End(303), Decision::P11NewResource),
924        Decision::N16Post => Transition::Branch(Decision::N11Redirect, Decision::O16Put),
925        Decision::O14Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
926        Decision::O16Put => Transition::Branch(Decision::O14Conflict, Decision::O18MultipleRepresentations),
927        Decision::P3Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
928        Decision::P11NewResource => Transition::Branch(Decision::End(201), Decision::O20ResponseHasBody),
929        Decision::O18MultipleRepresentations => Transition::Branch(Decision::End(300), Decision::End(200)),
930        Decision::O20ResponseHasBody => Transition::Branch(Decision::O18MultipleRepresentations, Decision::End(204))
931    };
932}
933
934fn resource_etag_matches_header_values(
935  resource: &(dyn Resource + Send + Sync),
936  context: &mut WebmachineContext,
937  header: &str
938) -> bool {
939  let header_values = context.request.find_header(header);
940  match resource.generate_etag(context) {
941    Some(etag) => {
942      header_values.iter().find(|val| {
943        if let Some(weak_etag) = val.weak_etag() {
944          weak_etag == etag
945        } else {
946          val.value == etag
947        }
948      }).is_some()
949    },
950    None => false
951  }
952}
953
954fn validate_header_date(
955  request: &WebmachineRequest,
956  header: &str,
957  context_meta: &mut Option<DateTime<FixedOffset>>
958) -> bool {
959  let header_values = request.find_header(header);
960  if let Some(date_value) = header_values.first() {
961    match DateTime::parse_from_rfc2822(&date_value.value) {
962      Ok(datetime) => {
963        *context_meta = Some(datetime.clone());
964        true
965      },
966      Err(err) => {
967        debug!("Failed to parse '{}' header value '{:?}' - {}", header, date_value, err);
968        false
969      }
970    }
971  } else {
972    false
973  }
974}
975
976async fn execute_decision(
977  decision: &Decision,
978  context: &mut WebmachineContext,
979  resource: &(dyn Resource + Send + Sync)
980) -> DecisionResult {
981  match decision {
982    Decision::B10MethodAllowed => {
983      match resource.allowed_methods()
984        .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()) {
985        Some(_) => DecisionResult::True("method is in the list of allowed methods".to_string()),
986        None => {
987          context.response.add_header("Allow", resource.allowed_methods()
988            .iter()
989            .map(|s| HeaderValue::basic(*s))
990            .collect());
991          DecisionResult::False("method is not in the list of allowed methods".to_string())
992        }
993      }
994    },
995    Decision::B11UriTooLong => {
996      DecisionResult::wrap(resource.uri_too_long(context), "URI too long")
997    },
998    Decision::B12KnownMethod => DecisionResult::wrap(resource.known_methods()
999      .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()).is_some(),
1000      "known method"),
1001    Decision::B13Available => {
1002      DecisionResult::wrap(resource.available(context), "available")
1003    },
1004    Decision::B9MalformedRequest => {
1005      DecisionResult::wrap(resource.malformed_request(context), "malformed request")
1006    },
1007    Decision::B8Authorized => {
1008      match resource.not_authorized(context) {
1009        Some(realm) => {
1010          context.response.add_header("WWW-Authenticate", vec![HeaderValue::parse_string(realm.as_str())]);
1011          DecisionResult::False("is not authorized".to_string())
1012        },
1013        None => DecisionResult::True("is not authorized".to_string())
1014      }
1015    },
1016    Decision::B7Forbidden => {
1017      DecisionResult::wrap(resource.forbidden(context), "forbidden")
1018    },
1019    Decision::B6UnsupportedContentHeader => {
1020      DecisionResult::wrap(resource.unsupported_content_headers(context), "unsupported content headers")
1021    },
1022    Decision::B5UnknownContentType => {
1023      DecisionResult::wrap(context.request.is_put_or_post() && !acceptable_content_type(resource, context),
1024        "acceptable content types")
1025    },
1026    Decision::B4RequestEntityTooLarge => {
1027      DecisionResult::wrap(context.request.is_put_or_post() && !resource.valid_entity_length(context),
1028        "valid entity length")
1029    },
1030    Decision::B3Options => DecisionResult::wrap(context.request.is_options(), "options"),
1031    Decision::C3AcceptExists => DecisionResult::wrap(context.request.has_accept_header(), "has accept header"),
1032    Decision::C4AcceptableMediaTypeAvailable => match content_negotiation::matching_content_type(resource, &context.request) {
1033      Some(media_type) => {
1034        context.selected_media_type = Some(media_type);
1035        DecisionResult::True("acceptable media type is available".to_string())
1036      },
1037      None => DecisionResult::False("acceptable media type is not available".to_string())
1038    },
1039    Decision::D4AcceptLanguageExists => DecisionResult::wrap(context.request.has_accept_language_header(),
1040                                                             "has accept language header"),
1041    Decision::D5AcceptableLanguageAvailable => match content_negotiation::matching_language(resource, &context.request) {
1042      Some(language) => {
1043        if language != "*" {
1044          context.selected_language = Some(language.clone());
1045          context.response.add_header("Content-Language", vec![HeaderValue::parse_string(&language)]);
1046        }
1047        DecisionResult::True("acceptable language is available".to_string())
1048      },
1049      None => DecisionResult::False("acceptable language is not available".to_string())
1050    },
1051    Decision::E5AcceptCharsetExists => DecisionResult::wrap(context.request.has_accept_charset_header(),
1052                                                            "accept charset exists"),
1053    Decision::E6AcceptableCharsetAvailable => match content_negotiation::matching_charset(resource, &context.request) {
1054      Some(charset) => {
1055        if charset != "*" {
1056            context.selected_charset = Some(charset.clone());
1057        }
1058        DecisionResult::True("acceptable charset is available".to_string())
1059      },
1060      None => DecisionResult::False("acceptable charset is not available".to_string())
1061    },
1062    Decision::F6AcceptEncodingExists => DecisionResult::wrap(context.request.has_accept_encoding_header(),
1063                                                             "accept encoding exists"),
1064    Decision::F7AcceptableEncodingAvailable => match content_negotiation::matching_encoding(resource, &context.request) {
1065      Some(encoding) => {
1066        context.selected_encoding = Some(encoding.clone());
1067        if encoding != "identity" {
1068            context.response.add_header("Content-Encoding", vec![HeaderValue::parse_string(&encoding)]);
1069        }
1070        DecisionResult::True("acceptable encoding is available".to_string())
1071      },
1072      None => DecisionResult::False("acceptable encoding is not available".to_string())
1073    },
1074    Decision::G7ResourceExists => {
1075      DecisionResult::wrap(resource.resource_exists(context).await, "resource exists")
1076    },
1077    Decision::G8IfMatchExists => DecisionResult::wrap(context.request.has_header("If-Match"),
1078                                                      "match exists"),
1079    Decision::G9IfMatchStarExists | &Decision::H7IfMatchStarExists => DecisionResult::wrap(
1080        context.request.has_header_value("If-Match", "*"), "match star exists"),
1081    Decision::G11EtagInIfMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-Match"),
1082                                                       "etag in if match"),
1083    Decision::H10IfUnmodifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Unmodified-Since"),
1084                                                                 "unmodified since exists"),
1085    Decision::H11IfUnmodifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request, "If-Unmodified-Since", &mut context.if_unmodified_since),
1086                                                                "unmodified since valid"),
1087    Decision::H12LastModifiedGreaterThanUMS => {
1088      match context.if_unmodified_since {
1089        Some(unmodified_since) => {
1090          match resource.last_modified(context) {
1091            Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
1092                                                   "resource last modified date is greater than unmodified since"),
1093            None => DecisionResult::False("resource has no last modified date".to_string())
1094          }
1095        },
1096        None => DecisionResult::False("resource does not provide last modified date".to_string())
1097      }
1098    },
1099    Decision::I7Put => if context.request.is_put() {
1100      context.new_resource = true;
1101      DecisionResult::True("is a PUT request".to_string())
1102    } else {
1103      DecisionResult::False("is not a PUT request".to_string())
1104    },
1105    Decision::I12IfNoneMatchExists => DecisionResult::wrap(context.request.has_header("If-None-Match"),
1106                                                           "none match exists"),
1107    Decision::I13IfNoneMatchStarExists => DecisionResult::wrap(context.request.has_header_value("If-None-Match", "*"),
1108                                                               "none match star exists"),
1109    Decision::J18GetHead => DecisionResult::wrap(context.request.is_get_or_head(),
1110                                                 "is GET or HEAD request"),
1111    Decision::K7ResourcePreviouslyExisted => {
1112      DecisionResult::wrap(resource.previously_existed(context), "resource previously existed")
1113    },
1114    Decision::K13ETagInIfNoneMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-None-Match"),
1115                                                           "ETag in if none match"),
1116    Decision::L5HasMovedTemporarily => {
1117      match resource.moved_temporarily(context) {
1118        Some(location) => {
1119          context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
1120          DecisionResult::True("resource has moved temporarily".to_string())
1121        },
1122        None => DecisionResult::False("resource has not moved temporarily".to_string())
1123      }
1124    },
1125    Decision::L7Post | &Decision::M5Post | &Decision::N16Post => DecisionResult::wrap(context.request.is_post(),
1126                                                                                      "a POST request"),
1127    Decision::L13IfModifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Modified-Since"),
1128                                                               "if modified since exists"),
1129    Decision::L14IfModifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request,
1130        "If-Modified-Since", &mut context.if_modified_since), "modified since valid"),
1131    Decision::L15IfModifiedSinceGreaterThanNow => {
1132        let datetime = context.if_modified_since.unwrap();
1133        let timezone = datetime.timezone();
1134        DecisionResult::wrap(datetime > Utc::now().with_timezone(&timezone),
1135                             "modified since greater than now")
1136    },
1137    Decision::L17IfLastModifiedGreaterThanMS => {
1138      match context.if_modified_since {
1139        Some(unmodified_since) => {
1140          match resource.last_modified(context) {
1141            Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
1142                                                   "last modified greater than modified since"),
1143            None => DecisionResult::False("resource has no last modified date".to_string())
1144          }
1145        },
1146        None => DecisionResult::False("resource does not return if_modified_since".to_string())
1147      }
1148    },
1149    Decision::I4HasMovedPermanently | &Decision::K5HasMovedPermanently => {
1150      match resource.moved_permanently(context) {
1151        Some(location) => {
1152          context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
1153          DecisionResult::True("resource has moved permanently".to_string())
1154        },
1155        None => DecisionResult::False("resource has not moved permanently".to_string())
1156      }
1157    },
1158    Decision::M7PostToMissingResource | &Decision::N5PostToMissingResource => {
1159      if resource.allow_missing_post(context) {
1160        context.new_resource = true;
1161        DecisionResult::True("resource allows POST to missing resource".to_string())
1162      } else {
1163        DecisionResult::False("resource does not allow POST to missing resource".to_string())
1164      }
1165    },
1166    Decision::M16Delete => DecisionResult::wrap(context.request.is_delete(),
1167                                                "a DELETE request"),
1168    Decision::M20DeleteEnacted => {
1169      match resource.delete_resource(context).await {
1170        Ok(result) => DecisionResult::wrap(result, "resource DELETE succeeded"),
1171        Err(status) => DecisionResult::StatusCode(status)
1172      }
1173    },
1174    Decision::N11Redirect => {
1175      if resource.post_is_create(context) {
1176        match resource.create_path(context).await {
1177          Ok(path) => {
1178            let base_path = sanitise_path(&context.request.base_path);
1179            let new_path = join_paths(&base_path, &sanitise_path(&path));
1180            context.request.request_path = path.clone();
1181            context.response.add_header("Location", vec![HeaderValue::basic(&new_path)]);
1182            DecisionResult::wrap(context.redirect, "should redirect")
1183          },
1184          Err(status) => DecisionResult::StatusCode(status)
1185        }
1186      } else {
1187        match resource.process_post(context).await {
1188          Ok(_) => DecisionResult::wrap(context.redirect, "processing POST succeeded"),
1189          Err(status) => DecisionResult::StatusCode(status)
1190        }
1191      }
1192    },
1193    Decision::P3Conflict | &Decision::O14Conflict => {
1194      DecisionResult::wrap(resource.is_conflict(context), "resource conflict")
1195    },
1196    Decision::P11NewResource => {
1197      if context.request.is_put() {
1198        match resource.process_put(context).await {
1199          Ok(_) => DecisionResult::wrap(context.new_resource, "process PUT succeeded"),
1200          Err(status) => DecisionResult::StatusCode(status)
1201        }
1202      } else {
1203        DecisionResult::wrap(context.new_resource, "new resource creation succeeded")
1204      }
1205    },
1206    Decision::O16Put => DecisionResult::wrap(context.request.is_put(), "a PUT request"),
1207    Decision::O18MultipleRepresentations => {
1208      DecisionResult::wrap(resource.multiple_choices(context), "multiple choices exist")
1209    },
1210    Decision::O20ResponseHasBody => DecisionResult::wrap(context.response.has_body(), "response has a body"),
1211    _ => DecisionResult::False("default decision is false".to_string())
1212  }
1213}
1214
1215async fn execute_state_machine(
1216  context: &mut WebmachineContext,
1217  resource: &(dyn Resource + Send + Sync)
1218) {
1219  let mut state = Decision::Start;
1220  let mut decisions: Vec<(Decision, bool, Decision)> = Vec::new();
1221  let mut loop_count = 0;
1222  while !state.is_terminal() {
1223    loop_count += 1;
1224    if loop_count >= MAX_STATE_MACHINE_TRANSITIONS {
1225      panic!("State machine has not terminated within {} transitions!", loop_count);
1226    }
1227    trace!("state is {:?}", state);
1228    state = match TRANSITION_MAP.get(&state) {
1229      Some(transition) => match transition {
1230        Transition::To(decision) => {
1231          trace!("Transitioning to {:?}", decision);
1232          decision.clone()
1233        },
1234        Transition::Branch(decision_true, decision_false) => {
1235          match execute_decision(&state, context, resource).await {
1236            DecisionResult::True(reason) => {
1237              trace!("Transitioning from {:?} to {:?} as decision is true -> {}", state, decision_true, reason);
1238              decisions.push((state, true, decision_true.clone()));
1239              decision_true.clone()
1240            },
1241            DecisionResult::False(reason) => {
1242              trace!("Transitioning from {:?} to {:?} as decision is false -> {}", state, decision_false, reason);
1243              decisions.push((state, false, decision_false.clone()));
1244              decision_false.clone()
1245            },
1246            DecisionResult::StatusCode(code) => {
1247              let decision = Decision::End(code);
1248              trace!("Transitioning from {:?} to {:?} as decision is a status code", state, decision);
1249              decisions.push((state, false, decision.clone()));
1250              decision.clone()
1251            }
1252          }
1253        }
1254      },
1255      None => {
1256        error!("Error transitioning from {:?}, the TRANSITION_MAP is mis-configured", state);
1257        decisions.push((state, false, Decision::End(500)));
1258        Decision::End(500)
1259      }
1260    }
1261  }
1262  trace!("Final state is {:?}", state);
1263  match state {
1264    Decision::End(status) => context.response.status = status,
1265    Decision::A3Options => {
1266      context.response.status = 204;
1267      match resource.options(context) {
1268        Some(headers) => context.response.add_headers(headers),
1269        None => ()
1270      }
1271    },
1272    _ => ()
1273  }
1274}
1275
1276fn update_paths_for_resource(
1277  request: &mut WebmachineRequest,
1278  base_path: &str,
1279  mapped_parts: &Vec<(String, Option<String>)>
1280) {
1281  request.base_path = base_path.into();
1282  request.path_vars = mapped_parts.iter()
1283    .filter_map(|(part, id)| id.as_ref().map(|id| (id.clone(), part.clone())))
1284    .collect();
1285  let base_parts = base_path.split('/').count() - 1;
1286  let sub_parts = mapped_parts.iter()
1287    .dropping(base_parts)
1288    .map(|(part, _)| part)
1289    .collect_vec();
1290  request.sub_path = if sub_parts.is_empty() {
1291    None
1292  } else {
1293    Some(sub_parts.iter().join("/"))
1294  };
1295}
1296
1297fn parse_header_values(value: &str) -> Vec<HeaderValue> {
1298  if value.is_empty() {
1299    Vec::new()
1300  } else {
1301    value.split(',').map(|s| HeaderValue::parse_string(s.trim())).collect()
1302  }
1303}
1304
1305fn headers_from_http_request(headers: &HeaderMap<http::HeaderValue>) -> HashMap<String, Vec<HeaderValue>> {
1306  headers.keys()
1307    .map(|key| (key.to_string(), headers.get_all(key)
1308      .iter()
1309      .flat_map(|value| parse_header_values(value.to_str().unwrap_or_default()))
1310      .collect_vec()
1311    ))
1312    .collect()
1313}
1314
1315fn decode_query(query: &str) -> String {
1316  let mut chars = query.chars();
1317  let mut ch = chars.next();
1318  let mut result = String::new();
1319
1320  while ch.is_some() {
1321    let c = ch.unwrap();
1322    if c == '%' {
1323      let c1 = chars.next();
1324      let c2 = chars.next();
1325      match (c1, c2) {
1326        (Some(v1), Some(v2)) => {
1327          let mut s = String::new();
1328          s.push(v1);
1329          s.push(v2);
1330          let decoded: Result<Vec<u8>, _> = hex::decode(s);
1331          match decoded {
1332            Ok(n) => result.push(n[0] as char),
1333            Err(_) => {
1334              result.push('%');
1335              result.push(v1);
1336              result.push(v2);
1337            }
1338          }
1339        },
1340        (Some(v1), None) => {
1341          result.push('%');
1342          result.push(v1);
1343        },
1344        _ => result.push('%')
1345      }
1346    } else if c == '+' {
1347      result.push(' ');
1348    } else {
1349      result.push(c);
1350    }
1351
1352    ch = chars.next();
1353  }
1354
1355  result
1356}
1357
1358fn parse_query(query: &str) -> HashMap<String, Vec<String>> {
1359  if !query.is_empty() {
1360    query.split("&").map(|kv| {
1361      if kv.is_empty() {
1362        vec![]
1363      } else if kv.contains("=") {
1364        kv.splitn(2, "=").collect::<Vec<&str>>()
1365      } else {
1366        vec![kv]
1367      }
1368    }).fold(HashMap::new(), |mut map, name_value| {
1369      if !name_value.is_empty() {
1370        let name = decode_query(name_value[0]);
1371        let value = if name_value.len() > 1 {
1372          decode_query(name_value[1])
1373        } else {
1374          String::new()
1375        };
1376        map.entry(name).or_insert(vec![]).push(value);
1377      }
1378      map
1379    })
1380  } else {
1381    HashMap::new()
1382  }
1383}
1384
1385async fn request_from_http_request<BODY, E>(req: Request<BODY>) -> WebmachineRequest
1386  where BODY: Body<Error = E>,
1387        E: Display
1388{
1389  let request_path = req.uri().path().to_string();
1390  let method = req.method().to_string();
1391  let query = match req.uri().query() {
1392    Some(query) => parse_query(query),
1393    None => HashMap::new()
1394  };
1395  let headers = headers_from_http_request(req.headers());
1396
1397  let body = match req.collect().await {
1398    Ok(body) => {
1399      let body = body.to_bytes();
1400      if body.is_empty() {
1401        None
1402      } else {
1403        Some(body.clone())
1404      }
1405    }
1406    Err(err) => {
1407      error!("Failed to read the request body: {}", err);
1408      None
1409    }
1410  };
1411
1412  WebmachineRequest {
1413    request_path,
1414    base_path: "/".to_string(),
1415    sub_path: None,
1416    path_vars: Default::default(),
1417    method,
1418    headers,
1419    body,
1420    query
1421  }
1422}
1423
1424async fn finalise_response(context: &mut WebmachineContext, resource: &(dyn Resource + Send + Sync)) {
1425  if !context.response.has_header("Content-Type") {
1426    let media_type = match &context.selected_media_type {
1427      &Some(ref media_type) => media_type.clone(),
1428      &None => "application/json".to_string()
1429    };
1430    let charset = match &context.selected_charset {
1431      &Some(ref charset) => charset.clone(),
1432      &None => "ISO-8859-1".to_string()
1433    };
1434    let header = HeaderValue {
1435      value: media_type,
1436      params: hashmap!{ "charset".to_string() => charset },
1437      quote: false
1438    };
1439    context.response.add_header("Content-Type", vec![header]);
1440  }
1441
1442  let mut vary_header = if !context.response.has_header("Vary") {
1443    resource.variances()
1444      .iter()
1445      .map(|h| HeaderValue::parse_string(h))
1446      .collect()
1447  } else {
1448    Vec::new()
1449  };
1450
1451  if resource.languages_provided().len() > 1 {
1452    vary_header.push(h!("Accept-Language"));
1453  }
1454  if resource.charsets_provided().len() > 1 {
1455    vary_header.push(h!("Accept-Charset"));
1456  }
1457  if resource.encodings_provided().len() > 1 {
1458    vary_header.push(h!("Accept-Encoding"));
1459  }
1460  if resource.produces().len() > 1 {
1461    vary_header.push(h!("Accept"));
1462  }
1463
1464  if vary_header.len() > 1 {
1465    context.response.add_header("Vary", vary_header.iter().cloned().unique().collect());
1466  }
1467
1468  if context.request.is_get_or_head() {
1469    if let Some(etag) = resource.generate_etag(context) {
1470      context.response.add_header("ETag", vec![HeaderValue::basic(&etag).quote()]);
1471    }
1472    if let Some(datetime) = resource.expires(context) {
1473      context.response.add_header("Expires", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]);
1474    }
1475    if let Some(datetime) = resource.last_modified(context) {
1476      context.response.add_header("Last-Modified", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]);
1477    }
1478  }
1479
1480  if context.response.body.is_none() && context.response.status == 200 && context.request.is_get() {
1481    match resource.render_response(context).await {
1482      Ok(Some(body)) => context.response.body = Some(body),
1483      Ok(None) => (),
1484      Err(err) => {
1485        error!("render_response failed with an error: {}", err);
1486        context.response.status = 500;
1487        context.response.body = Some(Bytes::from(err.to_string()));
1488      }
1489    }
1490  }
1491
1492  resource.finish_request(context);
1493  resource.finalise_response(context);
1494
1495  if let Ok(duration) = context.start_time.elapsed() {
1496    context.response.add_header("Server-Timing", vec![HeaderValue {
1497      value: "response".to_string(),
1498      params: hashmap!{
1499        "desc".to_string() => "Total Response Time".to_string(),
1500        "dur".to_string() => format!("{:?}", duration)
1501      },
1502      quote: true
1503    }])
1504  }
1505
1506  let body_size = context.response.body.as_ref().map(|bytes| bytes.len()).unwrap_or_default();
1507  debug!(status = context.response.status, headers = ?context.response.headers, body_size, "Final response");
1508}
1509
1510fn generate_http_response(context: &WebmachineContext) -> http::Result<Response<Full<Bytes>>> {
1511  let mut response = Response::builder().status(context.response.status);
1512
1513  for (header, values) in context.response.headers.clone() {
1514    let header_values = values.iter().map(|h| h.to_string()).join(", ");
1515    response = response.header(&header, &header_values);
1516  }
1517  match context.response.body.clone() {
1518    Some(body) => response.body(Full::new(body.into())),
1519    None => response.body(Full::new(Bytes::default()))
1520  }
1521}
1522
1523/// The main hyper dispatcher
1524pub struct WebmachineDispatcher {
1525  /// Map of routes to webmachine resources
1526  pub routes: BTreeMap<&'static str, Box<dyn Resource + Send + Sync>>
1527}
1528
1529impl WebmachineDispatcher {
1530  /// Main dispatch function for the Webmachine. This will look for a matching resource
1531  /// based on the request path. If one is not found, a 404 Not Found response is returned
1532  pub async fn dispatch(&self, req: Request<Incoming>) -> http::Result<Response<Full<Bytes>>> {
1533    let mut context = self.context_from_http_request(req).await;
1534    self.dispatch_to_resource(&mut context).await;
1535    generate_http_response(&context)
1536  }
1537
1538  async fn context_from_http_request(&self, req: Request<Incoming>) -> WebmachineContext {
1539    let now = SystemTime::now();
1540    let request = request_from_http_request(req).await;
1541    WebmachineContext {
1542      request,
1543      response: WebmachineResponse::default(),
1544      start_time: now,
1545      .. WebmachineContext::default()
1546    }
1547  }
1548
1549  fn match_paths(&self, request: &WebmachineRequest) -> Vec<(String, Vec<(String, Option<String>)>)> {
1550    self.routes
1551      .keys()
1552      .filter_map(|k| map_path(request.request_path.as_str(), k)
1553        .map(|result| (k.to_string(), result)))
1554      .collect()
1555  }
1556
1557  fn lookup_resource(&self, path: &str) -> Option<&(dyn Resource + Send + Sync)> {
1558    self.routes.get(path)
1559      .map(|resource| resource.as_ref())
1560  }
1561
1562  /// Dispatches to the matching webmachine resource. If there is no matching resource, returns
1563  /// 404 Not Found response
1564  pub async fn dispatch_to_resource(&self, context: &mut WebmachineContext) {
1565    let body_size = context.request.body.as_ref().map(|bytes| bytes.len()).unwrap_or_default();
1566    debug!(method = context.request.method, request_path = context.request.request_path,
1567      headers = ?context.request.headers, query = ?context.request.query, body_size,
1568      "Incoming request");
1569    let matching_paths = self.match_paths(&context.request);
1570    let ordered_by_length = matching_paths.iter()
1571      .cloned()
1572      .sorted_by(|(a, _), (b, _)| Ord::cmp(&b.len(), &a.len()))
1573      .collect_vec();
1574    match ordered_by_length.first() {
1575      Some((path, parts)) => {
1576        update_paths_for_resource(&mut context.request, path, parts);
1577        if let Some(resource) = self.lookup_resource(path) {
1578          trace!("Dispatching to resource {:?}", resource);
1579          execute_state_machine(context, resource).await;
1580          finalise_response(context, resource).await;
1581        } else {
1582          context.response.status = 404;
1583        }
1584      },
1585      None => context.response.status = 404
1586    };
1587  }
1588
1589  /// Convenience function to box a resource
1590  pub fn box_resource<R: Resource + Send + Sync + 'static>(resource: R) -> Box<dyn Resource + Send + Sync> {
1591    Box::new(resource)
1592  }
1593}
1594
1595#[cfg(test)]
1596mod tests;
1597
1598#[cfg(test)]
1599mod content_negotiation_tests;