1#![warn(missing_docs)]
115
116use std::collections::{BTreeMap, HashMap};
117
118use bytes::Bytes;
119use chrono::{DateTime, FixedOffset, Utc};
120use http::{HeaderMap, Request, Response};
121use http_body_util::{BodyExt, Full};
122use hyper::body::Incoming;
123use itertools::Itertools;
124use lazy_static::lazy_static;
125use maplit::hashmap;
126use tracing::{debug, error, trace};
127
128use context::{WebmachineContext, WebmachineRequest, WebmachineResponse};
129use headers::HeaderValue;
130
131#[macro_use] pub mod headers;
132pub mod context;
133pub mod content_negotiation;
134
135pub type WebmachineCallback<T> = Box<dyn Fn(&mut WebmachineContext, &WebmachineResource) -> T + Send + Sync>;
137
138pub fn callback<T, RT>(cb: T) -> WebmachineCallback<RT>
140 where T: Fn(&mut WebmachineContext, &WebmachineResource) -> RT + Send + Sync + 'static {
141 Box::new(cb)
142}
143
144pub fn owned_vec(strings: &[&str]) -> Vec<String> {
146 strings.iter().map(|s| s.to_string()).collect()
147}
148
149pub struct WebmachineResource {
151 pub finalise_response: Option<WebmachineCallback<()>>,
154 pub render_response: WebmachineCallback<Option<Bytes>>,
156 pub available: WebmachineCallback<bool>,
160 pub known_methods: Vec<String>,
163 pub uri_too_long: WebmachineCallback<bool>,
166 pub allowed_methods: Vec<String>,
168 pub malformed_request: WebmachineCallback<bool>,
171 pub not_authorized: WebmachineCallback<Option<String>>,
175 pub forbidden: WebmachineCallback<bool>,
178 pub unsupported_content_headers: WebmachineCallback<bool>,
181 pub acceptable_content_types: Vec<String>,
184 pub valid_entity_length: WebmachineCallback<bool>,
187 pub finish_request: WebmachineCallback<()>,
190 pub options: WebmachineCallback<Option<HashMap<String, Vec<String>>>>,
193 pub produces: Vec<String>,
197 pub languages_provided: Vec<String>,
201 pub charsets_provided: Vec<String>,
205 pub encodings_provided: Vec<String>,
208 pub variances: Vec<String>,
213 pub resource_exists: WebmachineCallback<bool>,
216 pub previously_existed: WebmachineCallback<bool>,
218 pub moved_permanently: WebmachineCallback<Option<String>>,
221 pub moved_temporarily: WebmachineCallback<Option<String>>,
224 pub is_conflict: WebmachineCallback<bool>,
227 pub allow_missing_post: WebmachineCallback<bool>,
229 pub generate_etag: WebmachineCallback<Option<String>>,
232 pub last_modified: WebmachineCallback<Option<DateTime<FixedOffset>>>,
236 pub delete_resource: WebmachineCallback<Result<bool, u16>>,
242 pub post_is_create: WebmachineCallback<bool>,
247 pub process_post: WebmachineCallback<Result<bool, u16>>,
253 pub create_path: WebmachineCallback<Result<String, u16>>,
262 pub process_put: WebmachineCallback<Result<bool, u16>>,
266 pub multiple_choices: WebmachineCallback<bool>,
270 pub expires: WebmachineCallback<Option<DateTime<FixedOffset>>>
272}
273
274fn true_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
275 true
276}
277
278fn false_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
279 false
280}
281
282fn none_fn<T>(_: &mut WebmachineContext, _: &WebmachineResource) -> Option<T> {
283 None
284}
285
286impl Default for WebmachineResource {
287 fn default() -> WebmachineResource {
288 WebmachineResource {
289 finalise_response: None,
290 available: callback(true_fn),
291 known_methods: owned_vec(&["OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "CONNECT", "PATCH"]),
292 uri_too_long: callback(false_fn),
293 allowed_methods: owned_vec(&["OPTIONS", "GET", "HEAD"]),
294 malformed_request: callback(false_fn),
295 not_authorized: callback(none_fn),
296 forbidden: callback(false_fn),
297 unsupported_content_headers: callback(false_fn),
298 acceptable_content_types: owned_vec(&["application/json"]),
299 valid_entity_length: callback(true_fn),
300 finish_request: callback(|context, resource| context.response.add_cors_headers(&resource.allowed_methods)),
301 options: callback(|_, resource| Some(WebmachineResponse::cors_headers(&resource.allowed_methods))),
302 produces: vec!["application/json".to_string()],
303 languages_provided: Vec::new(),
304 charsets_provided: Vec::new(),
305 encodings_provided: vec!["identity".to_string()],
306 variances: Vec::new(),
307 resource_exists: callback(true_fn),
308 previously_existed: callback(false_fn),
309 moved_permanently: callback(none_fn),
310 moved_temporarily: callback(none_fn),
311 is_conflict: callback(false_fn),
312 allow_missing_post: callback(false_fn),
313 generate_etag: callback(none_fn),
314 last_modified: callback(none_fn),
315 delete_resource: callback(|_, _| Ok(true)),
316 post_is_create: callback(false_fn),
317 process_post: callback(|_, _| Ok(false)),
318 process_put: callback(|_, _| Ok(true)),
319 multiple_choices: callback(false_fn),
320 create_path: callback(|context, _| Ok(context.request.request_path.clone())),
321 expires: callback(none_fn),
322 render_response: callback(none_fn)
323 }
324 }
325}
326
327fn sanitise_path(path: &str) -> Vec<String> {
328 path.split("/").filter(|p| !p.is_empty()).map(|p| p.to_string()).collect()
329}
330
331fn join_paths(base: &Vec<String>, path: &Vec<String>) -> String {
332 let mut paths = base.clone();
333 paths.extend_from_slice(path);
334 let filtered: Vec<String> = paths.iter().cloned().filter(|p| !p.is_empty()).collect();
335 if filtered.is_empty() {
336 "/".to_string()
337 } else {
338 let new_path = filtered.iter().join("/");
339 if new_path.starts_with("/") {
340 new_path
341 } else {
342 "/".to_owned() + &new_path
343 }
344 }
345}
346
347const MAX_STATE_MACHINE_TRANSITIONS: u8 = 100;
348
349#[derive(Debug, Clone, PartialEq, Eq, Hash)]
350enum Decision {
351 Start,
352 End(u16),
353 A3Options,
354 B3Options,
355 B4RequestEntityTooLarge,
356 B5UnknownContentType,
357 B6UnsupportedContentHeader,
358 B7Forbidden,
359 B8Authorized,
360 B9MalformedRequest,
361 B10MethodAllowed,
362 B11UriTooLong,
363 B12KnownMethod,
364 B13Available,
365 C3AcceptExists,
366 C4AcceptableMediaTypeAvailable,
367 D4AcceptLanguageExists,
368 D5AcceptableLanguageAvailable,
369 E5AcceptCharsetExists,
370 E6AcceptableCharsetAvailable,
371 F6AcceptEncodingExists,
372 F7AcceptableEncodingAvailable,
373 G7ResourceExists,
374 G8IfMatchExists,
375 G9IfMatchStarExists,
376 G11EtagInIfMatch,
377 H7IfMatchStarExists,
378 H10IfUnmodifiedSinceExists,
379 H11IfUnmodifiedSinceValid,
380 H12LastModifiedGreaterThanUMS,
381 I4HasMovedPermanently,
382 I12IfNoneMatchExists,
383 I13IfNoneMatchStarExists,
384 I7Put,
385 J18GetHead,
386 K5HasMovedPermanently,
387 K7ResourcePreviouslyExisted,
388 K13ETagInIfNoneMatch,
389 L5HasMovedTemporarily,
390 L7Post,
391 L13IfModifiedSinceExists,
392 L14IfModifiedSinceValid,
393 L15IfModifiedSinceGreaterThanNow,
394 L17IfLastModifiedGreaterThanMS,
395 M5Post,
396 M7PostToMissingResource,
397 M16Delete,
398 M20DeleteEnacted,
399 N5PostToMissingResource,
400 N11Redirect,
401 N16Post,
402 O14Conflict,
403 O16Put,
404 O18MultipleRepresentations,
405 O20ResponseHasBody,
406 P3Conflict,
407 P11NewResource
408}
409
410impl Decision {
411 fn is_terminal(&self) -> bool {
412 match self {
413 &Decision::End(_) => true,
414 &Decision::A3Options => true,
415 _ => false
416 }
417 }
418}
419
420enum Transition {
421 To(Decision),
422 Branch(Decision, Decision)
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Hash)]
426enum DecisionResult {
427 True(String),
428 False(String),
429 StatusCode(u16)
430}
431
432impl DecisionResult {
433 fn wrap(result: bool, reason: &str) -> DecisionResult {
434 if result {
435 DecisionResult::True(format!("is: {}", reason))
436 } else {
437 DecisionResult::False(format!("is not: {}", reason))
438 }
439 }
440}
441
442lazy_static! {
443 static ref TRANSITION_MAP: HashMap<Decision, Transition> = hashmap!{
444 Decision::Start => Transition::To(Decision::B13Available),
445 Decision::B3Options => Transition::Branch(Decision::A3Options, Decision::C3AcceptExists),
446 Decision::B4RequestEntityTooLarge => Transition::Branch(Decision::End(413), Decision::B3Options),
447 Decision::B5UnknownContentType => Transition::Branch(Decision::End(415), Decision::B4RequestEntityTooLarge),
448 Decision::B6UnsupportedContentHeader => Transition::Branch(Decision::End(501), Decision::B5UnknownContentType),
449 Decision::B7Forbidden => Transition::Branch(Decision::End(403), Decision::B6UnsupportedContentHeader),
450 Decision::B8Authorized => Transition::Branch(Decision::B7Forbidden, Decision::End(401)),
451 Decision::B9MalformedRequest => Transition::Branch(Decision::End(400), Decision::B8Authorized),
452 Decision::B10MethodAllowed => Transition::Branch(Decision::B9MalformedRequest, Decision::End(405)),
453 Decision::B11UriTooLong => Transition::Branch(Decision::End(414), Decision::B10MethodAllowed),
454 Decision::B12KnownMethod => Transition::Branch(Decision::B11UriTooLong, Decision::End(501)),
455 Decision::B13Available => Transition::Branch(Decision::B12KnownMethod, Decision::End(503)),
456 Decision::C3AcceptExists => Transition::Branch(Decision::C4AcceptableMediaTypeAvailable, Decision::D4AcceptLanguageExists),
457 Decision::C4AcceptableMediaTypeAvailable => Transition::Branch(Decision::D4AcceptLanguageExists, Decision::End(406)),
458 Decision::D4AcceptLanguageExists => Transition::Branch(Decision::D5AcceptableLanguageAvailable, Decision::E5AcceptCharsetExists),
459 Decision::D5AcceptableLanguageAvailable => Transition::Branch(Decision::E5AcceptCharsetExists, Decision::End(406)),
460 Decision::E5AcceptCharsetExists => Transition::Branch(Decision::E6AcceptableCharsetAvailable, Decision::F6AcceptEncodingExists),
461 Decision::E6AcceptableCharsetAvailable => Transition::Branch(Decision::F6AcceptEncodingExists, Decision::End(406)),
462 Decision::F6AcceptEncodingExists => Transition::Branch(Decision::F7AcceptableEncodingAvailable, Decision::G7ResourceExists),
463 Decision::F7AcceptableEncodingAvailable => Transition::Branch(Decision::G7ResourceExists, Decision::End(406)),
464 Decision::G7ResourceExists => Transition::Branch(Decision::G8IfMatchExists, Decision::H7IfMatchStarExists),
465 Decision::G8IfMatchExists => Transition::Branch(Decision::G9IfMatchStarExists, Decision::H10IfUnmodifiedSinceExists),
466 Decision::G9IfMatchStarExists => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::G11EtagInIfMatch),
467 Decision::G11EtagInIfMatch => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::End(412)),
468 Decision::H7IfMatchStarExists => Transition::Branch(Decision::End(412), Decision::I7Put),
469 Decision::H10IfUnmodifiedSinceExists => Transition::Branch(Decision::H11IfUnmodifiedSinceValid, Decision::I12IfNoneMatchExists),
470 Decision::H11IfUnmodifiedSinceValid => Transition::Branch(Decision::H12LastModifiedGreaterThanUMS, Decision::I12IfNoneMatchExists),
471 Decision::H12LastModifiedGreaterThanUMS => Transition::Branch(Decision::End(412), Decision::I12IfNoneMatchExists),
472 Decision::I4HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::P3Conflict),
473 Decision::I7Put => Transition::Branch(Decision::I4HasMovedPermanently, Decision::K7ResourcePreviouslyExisted),
474 Decision::I12IfNoneMatchExists => Transition::Branch(Decision::I13IfNoneMatchStarExists, Decision::L13IfModifiedSinceExists),
475 Decision::I13IfNoneMatchStarExists => Transition::Branch(Decision::J18GetHead, Decision::K13ETagInIfNoneMatch),
476 Decision::J18GetHead => Transition::Branch(Decision::End(304), Decision::End(412)),
477 Decision::K13ETagInIfNoneMatch => Transition::Branch(Decision::J18GetHead, Decision::L13IfModifiedSinceExists),
478 Decision::K5HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::L5HasMovedTemporarily),
479 Decision::K7ResourcePreviouslyExisted => Transition::Branch(Decision::K5HasMovedPermanently, Decision::L7Post),
480 Decision::L5HasMovedTemporarily => Transition::Branch(Decision::End(307), Decision::M5Post),
481 Decision::L7Post => Transition::Branch(Decision::M7PostToMissingResource, Decision::End(404)),
482 Decision::L13IfModifiedSinceExists => Transition::Branch(Decision::L14IfModifiedSinceValid, Decision::M16Delete),
483 Decision::L14IfModifiedSinceValid => Transition::Branch(Decision::L15IfModifiedSinceGreaterThanNow, Decision::M16Delete),
484 Decision::L15IfModifiedSinceGreaterThanNow => Transition::Branch(Decision::M16Delete, Decision::L17IfLastModifiedGreaterThanMS),
485 Decision::L17IfLastModifiedGreaterThanMS => Transition::Branch(Decision::M16Delete, Decision::End(304)),
486 Decision::M5Post => Transition::Branch(Decision::N5PostToMissingResource, Decision::End(410)),
487 Decision::M7PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(404)),
488 Decision::M16Delete => Transition::Branch(Decision::M20DeleteEnacted, Decision::N16Post),
489 Decision::M20DeleteEnacted => Transition::Branch(Decision::O20ResponseHasBody, Decision::End(202)),
490 Decision::N5PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(410)),
491 Decision::N11Redirect => Transition::Branch(Decision::End(303), Decision::P11NewResource),
492 Decision::N16Post => Transition::Branch(Decision::N11Redirect, Decision::O16Put),
493 Decision::O14Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
494 Decision::O16Put => Transition::Branch(Decision::O14Conflict, Decision::O18MultipleRepresentations),
495 Decision::P3Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
496 Decision::P11NewResource => Transition::Branch(Decision::End(201), Decision::O20ResponseHasBody),
497 Decision::O18MultipleRepresentations => Transition::Branch(Decision::End(300), Decision::End(200)),
498 Decision::O20ResponseHasBody => Transition::Branch(Decision::O18MultipleRepresentations, Decision::End(204))
499 };
500}
501
502fn resource_etag_matches_header_values(
503 resource: &WebmachineResource,
504 context: &mut WebmachineContext,
505 header: &str
506) -> bool {
507 let header_values = context.request.find_header(header);
508 match (resource.generate_etag)(context, resource) {
509 Some(etag) => {
510 header_values.iter().find(|val| {
511 if val.value.starts_with("W/") {
512 val.weak_etag().unwrap() == etag
513 } else {
514 val.value == etag
515 }
516 }).is_some()
517 },
518 None => false
519 }
520}
521
522fn validate_header_date(
523 request: &WebmachineRequest,
524 header: &str,
525 context_meta: &mut Option<DateTime<FixedOffset>>
526) -> bool {
527 let header_values = request.find_header(header);
528 if let Some(date_value) = header_values.first() {
529 match DateTime::parse_from_rfc2822(&date_value.value) {
530 Ok(datetime) => {
531 *context_meta = Some(datetime.clone());
532 true
533 },
534 Err(err) => {
535 debug!("Failed to parse '{}' header value '{:?}' - {}", header, date_value, err);
536 false
537 }
538 }
539 } else {
540 false
541 }
542}
543
544fn execute_decision(
545 decision: &Decision,
546 context: &mut WebmachineContext,
547 resource: &WebmachineResource
548) -> DecisionResult {
549 match decision {
550 Decision::B10MethodAllowed => {
551 match resource.allowed_methods
552 .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()) {
553 Some(_) => DecisionResult::True("method is in the list of allowed methods".to_string()),
554 None => {
555 context.response.add_header("Allow", resource.allowed_methods
556 .iter()
557 .cloned()
558 .map(HeaderValue::basic)
559 .collect());
560 DecisionResult::False("method is not in the list of allowed methods".to_string())
561 }
562 }
563 },
564 Decision::B11UriTooLong => {
565 DecisionResult::wrap((resource.uri_too_long)(context, resource), "URI too long")
566 },
567 Decision::B12KnownMethod => DecisionResult::wrap(resource.known_methods
568 .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()).is_some(),
569 "known method"),
570 Decision::B13Available => {
571 DecisionResult::wrap((resource.available)(context, resource), "available")
572 },
573 Decision::B9MalformedRequest => {
574 DecisionResult::wrap((resource.malformed_request)(context, resource), "malformed request")
575 },
576 Decision::B8Authorized => {
577 match (resource.not_authorized)(context, resource) {
578 Some(realm) => {
579 context.response.add_header("WWW-Authenticate", vec![HeaderValue::parse_string(realm.as_str())]);
580 DecisionResult::False("is not authorized".to_string())
581 },
582 None => DecisionResult::True("is not authorized".to_string())
583 }
584 },
585 Decision::B7Forbidden => {
586 DecisionResult::wrap((resource.forbidden)(context, resource), "forbidden")
587 },
588 Decision::B6UnsupportedContentHeader => {
589 DecisionResult::wrap((resource.unsupported_content_headers)(context, resource), "unsupported content headers")
590 },
591 Decision::B5UnknownContentType => {
592 DecisionResult::wrap(context.request.is_put_or_post() && resource.acceptable_content_types
593 .iter().find(|ct| context.request.content_type().to_uppercase() == ct.to_uppercase() )
594 .is_none(), "acceptable content types")
595 },
596 Decision::B4RequestEntityTooLarge => {
597 DecisionResult::wrap(context.request.is_put_or_post() && !(resource.valid_entity_length)(context, resource),
598 "valid entity length")
599 },
600 Decision::B3Options => DecisionResult::wrap(context.request.is_options(), "options"),
601 Decision::C3AcceptExists => DecisionResult::wrap(context.request.has_accept_header(), "has accept header"),
602 Decision::C4AcceptableMediaTypeAvailable => match content_negotiation::matching_content_type(resource, &context.request) {
603 Some(media_type) => {
604 context.selected_media_type = Some(media_type);
605 DecisionResult::True("acceptable media type is available".to_string())
606 },
607 None => DecisionResult::False("acceptable media type is not available".to_string())
608 },
609 Decision::D4AcceptLanguageExists => DecisionResult::wrap(context.request.has_accept_language_header(),
610 "has accept language header"),
611 Decision::D5AcceptableLanguageAvailable => match content_negotiation::matching_language(resource, &context.request) {
612 Some(language) => {
613 if language != "*" {
614 context.selected_language = Some(language.clone());
615 context.response.add_header("Content-Language", vec![HeaderValue::parse_string(&language)]);
616 }
617 DecisionResult::True("acceptable language is available".to_string())
618 },
619 None => DecisionResult::False("acceptable language is not available".to_string())
620 },
621 Decision::E5AcceptCharsetExists => DecisionResult::wrap(context.request.has_accept_charset_header(),
622 "accept charset exists"),
623 Decision::E6AcceptableCharsetAvailable => match content_negotiation::matching_charset(resource, &context.request) {
624 Some(charset) => {
625 if charset != "*" {
626 context.selected_charset = Some(charset.clone());
627 }
628 DecisionResult::True("acceptable charset is available".to_string())
629 },
630 None => DecisionResult::False("acceptable charset is not available".to_string())
631 },
632 Decision::F6AcceptEncodingExists => DecisionResult::wrap(context.request.has_accept_encoding_header(),
633 "accept encoding exists"),
634 Decision::F7AcceptableEncodingAvailable => match content_negotiation::matching_encoding(resource, &context.request) {
635 Some(encoding) => {
636 context.selected_encoding = Some(encoding.clone());
637 if encoding != "identity" {
638 context.response.add_header("Content-Encoding", vec![HeaderValue::parse_string(&encoding)]);
639 }
640 DecisionResult::True("acceptable encoding is available".to_string())
641 },
642 None => DecisionResult::False("acceptable encoding is not available".to_string())
643 },
644 Decision::G7ResourceExists => {
645 DecisionResult::wrap((resource.resource_exists)(context, resource), "resource exists")
646 },
647 Decision::G8IfMatchExists => DecisionResult::wrap(context.request.has_header("If-Match"),
648 "match exists"),
649 Decision::G9IfMatchStarExists | &Decision::H7IfMatchStarExists => DecisionResult::wrap(
650 context.request.has_header_value("If-Match", "*"), "match star exists"),
651 Decision::G11EtagInIfMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-Match"),
652 "etag in if match"),
653 Decision::H10IfUnmodifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Unmodified-Since"),
654 "unmodified since exists"),
655 Decision::H11IfUnmodifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request, "If-Unmodified-Since", &mut context.if_unmodified_since),
656 "unmodified since valid"),
657 Decision::H12LastModifiedGreaterThanUMS => {
658 match context.if_unmodified_since {
659 Some(unmodified_since) => {
660 match (resource.last_modified)(context, resource) {
661 Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
662 "resource last modified date is greater than unmodified since"),
663 None => DecisionResult::False("resource has no last modified date".to_string())
664 }
665 },
666 None => DecisionResult::False("resource does not provide last modified date".to_string())
667 }
668 },
669 Decision::I7Put => if context.request.is_put() {
670 context.new_resource = true;
671 DecisionResult::True("is a PUT request".to_string())
672 } else {
673 DecisionResult::False("is not a PUT request".to_string())
674 },
675 Decision::I12IfNoneMatchExists => DecisionResult::wrap(context.request.has_header("If-None-Match"),
676 "none match exists"),
677 Decision::I13IfNoneMatchStarExists => DecisionResult::wrap(context.request.has_header_value("If-None-Match", "*"),
678 "none match star exists"),
679 Decision::J18GetHead => DecisionResult::wrap(context.request.is_get_or_head(),
680 "is GET or HEAD request"),
681 Decision::K7ResourcePreviouslyExisted => {
682 DecisionResult::wrap((resource.previously_existed)(context, resource), "resource previously existed")
683 },
684 Decision::K13ETagInIfNoneMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-None-Match"),
685 "ETag in if none match"),
686 Decision::L5HasMovedTemporarily => {
687 match (resource.moved_temporarily)(context, resource) {
688 Some(location) => {
689 context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
690 DecisionResult::True("resource has moved temporarily".to_string())
691 },
692 None => DecisionResult::False("resource has not moved temporarily".to_string())
693 }
694 },
695 Decision::L7Post | &Decision::M5Post | &Decision::N16Post => DecisionResult::wrap(context.request.is_post(),
696 "a POST request"),
697 Decision::L13IfModifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Modified-Since"),
698 "if modified since exists"),
699 Decision::L14IfModifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request,
700 "If-Modified-Since", &mut context.if_modified_since), "modified since valid"),
701 Decision::L15IfModifiedSinceGreaterThanNow => {
702 let datetime = context.if_modified_since.unwrap();
703 let timezone = datetime.timezone();
704 DecisionResult::wrap(datetime > Utc::now().with_timezone(&timezone),
705 "modified since greater than now")
706 },
707 Decision::L17IfLastModifiedGreaterThanMS => {
708 match context.if_modified_since {
709 Some(unmodified_since) => {
710 match (resource.last_modified)(context, resource) {
711 Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
712 "last modified greater than modified since"),
713 None => DecisionResult::False("resource has no last modified date".to_string())
714 }
715 },
716 None => DecisionResult::False("resource does not return if_modified_since".to_string())
717 }
718 },
719 Decision::I4HasMovedPermanently | &Decision::K5HasMovedPermanently => {
720 match (resource.moved_permanently)(context, resource) {
721 Some(location) => {
722 context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
723 DecisionResult::True("resource has moved permanently".to_string())
724 },
725 None => DecisionResult::False("resource has not moved permanently".to_string())
726 }
727 },
728 Decision::M7PostToMissingResource | &Decision::N5PostToMissingResource => {
729 if (resource.allow_missing_post)(context, resource) {
730 context.new_resource = true;
731 DecisionResult::True("resource allows POST to missing resource".to_string())
732 } else {
733 DecisionResult::False("resource does not allow POST to missing resource".to_string())
734 }
735 },
736 Decision::M16Delete => DecisionResult::wrap(context.request.is_delete(),
737 "a DELETE request"),
738 Decision::M20DeleteEnacted => {
739 match (resource.delete_resource)(context, resource) {
740 Ok(result) => DecisionResult::wrap(result, "resource DELETE succeeded"),
741 Err(status) => DecisionResult::StatusCode(status)
742 }
743 },
744 Decision::N11Redirect => {
745 if (resource.post_is_create)(context, resource) {
746 match (resource.create_path)(context, resource) {
747 Ok(path) => {
748 let base_path = sanitise_path(&context.request.base_path);
749 let new_path = join_paths(&base_path, &sanitise_path(&path));
750 context.request.request_path = path.clone();
751 context.response.add_header("Location", vec![HeaderValue::basic(&new_path)]);
752 DecisionResult::wrap(context.redirect, "should redirect")
753 },
754 Err(status) => DecisionResult::StatusCode(status)
755 }
756 } else {
757 match (resource.process_post)(context, resource) {
758 Ok(_) => DecisionResult::wrap(context.redirect, "processing POST succeeded"),
759 Err(status) => DecisionResult::StatusCode(status)
760 }
761 }
762 },
763 Decision::P3Conflict | &Decision::O14Conflict => {
764 DecisionResult::wrap((resource.is_conflict)(context, resource), "resource conflict")
765 },
766 Decision::P11NewResource => {
767 if context.request.is_put() {
768 match (resource.process_put)(context, resource) {
769 Ok(_) => DecisionResult::wrap(context.new_resource, "process PUT succeeded"),
770 Err(status) => DecisionResult::StatusCode(status)
771 }
772 } else {
773 DecisionResult::wrap(context.new_resource, "new resource creation succeeded")
774 }
775 },
776 Decision::O16Put => DecisionResult::wrap(context.request.is_put(), "a PUT request"),
777 Decision::O18MultipleRepresentations => {
778 DecisionResult::wrap((resource.multiple_choices)(context, resource), "multiple choices exist")
779 },
780 Decision::O20ResponseHasBody => DecisionResult::wrap(context.response.has_body(), "response has a body"),
781 _ => DecisionResult::False("default decision is false".to_string())
782 }
783}
784
785fn execute_state_machine(context: &mut WebmachineContext, resource: &WebmachineResource) {
786 let mut state = Decision::Start;
787 let mut decisions: Vec<(Decision, bool, Decision)> = Vec::new();
788 let mut loop_count = 0;
789 while !state.is_terminal() {
790 loop_count += 1;
791 if loop_count >= MAX_STATE_MACHINE_TRANSITIONS {
792 panic!("State machine has not terminated within {} transitions!", loop_count);
793 }
794 trace!("state is {:?}", state);
795 state = match TRANSITION_MAP.get(&state) {
796 Some(transition) => match transition {
797 Transition::To(decision) => {
798 trace!("Transitioning to {:?}", decision);
799 decision.clone()
800 },
801 Transition::Branch(decision_true, decision_false) => {
802 match execute_decision(&state, context, resource) {
803 DecisionResult::True(reason) => {
804 trace!("Transitioning from {:?} to {:?} as decision is true -> {}", state, decision_true, reason);
805 decisions.push((state, true, decision_true.clone()));
806 decision_true.clone()
807 },
808 DecisionResult::False(reason) => {
809 trace!("Transitioning from {:?} to {:?} as decision is false -> {}", state, decision_false, reason);
810 decisions.push((state, false, decision_false.clone()));
811 decision_false.clone()
812 },
813 DecisionResult::StatusCode(code) => {
814 let decision = Decision::End(code);
815 trace!("Transitioning from {:?} to {:?} as decision is a status code", state, decision);
816 decisions.push((state, false, decision.clone()));
817 decision.clone()
818 }
819 }
820 }
821 },
822 None => {
823 error!("Error transitioning from {:?}, the TRANSITION_MAP is mis-configured", state);
824 decisions.push((state, false, Decision::End(500)));
825 Decision::End(500)
826 }
827 }
828 }
829 trace!("Final state is {:?}", state);
830 match state {
831 Decision::End(status) => context.response.status = status,
832 Decision::A3Options => {
833 context.response.status = 204;
834 match (resource.options)(context, resource) {
835 Some(headers) => context.response.add_headers(headers),
836 None => ()
837 }
838 },
839 _ => ()
840 }
841}
842
843fn update_paths_for_resource(request: &mut WebmachineRequest, base_path: &str) {
844 request.base_path = base_path.into();
845 if request.request_path.len() > base_path.len() {
846 let request_path = request.request_path.clone();
847 let subpath = request_path.split_at(base_path.len()).1;
848 if subpath.starts_with("/") {
849 request.request_path = subpath.to_string();
850 } else {
851 request.request_path = "/".to_owned() + subpath;
852 }
853 } else {
854 request.request_path = "/".to_string();
855 }
856}
857
858fn parse_header_values(value: &str) -> Vec<HeaderValue> {
859 if value.is_empty() {
860 Vec::new()
861 } else {
862 value.split(',').map(|s| HeaderValue::parse_string(s.trim())).collect()
863 }
864}
865
866fn headers_from_http_request(headers: &HeaderMap<http::HeaderValue>) -> HashMap<String, Vec<HeaderValue>> {
867 headers.iter()
868 .map(|(name, value)| (name.to_string(), parse_header_values(value.to_str().unwrap_or_default())))
869 .collect()
870}
871
872fn decode_query(query: &str) -> String {
873 let mut chars = query.chars();
874 let mut ch = chars.next();
875 let mut result = String::new();
876
877 while ch.is_some() {
878 let c = ch.unwrap();
879 if c == '%' {
880 let c1 = chars.next();
881 let c2 = chars.next();
882 match (c1, c2) {
883 (Some(v1), Some(v2)) => {
884 let mut s = String::new();
885 s.push(v1);
886 s.push(v2);
887 let decoded: Result<Vec<u8>, _> = hex::decode(s);
888 match decoded {
889 Ok(n) => result.push(n[0] as char),
890 Err(_) => {
891 result.push('%');
892 result.push(v1);
893 result.push(v2);
894 }
895 }
896 },
897 (Some(v1), None) => {
898 result.push('%');
899 result.push(v1);
900 },
901 _ => result.push('%')
902 }
903 } else if c == '+' {
904 result.push(' ');
905 } else {
906 result.push(c);
907 }
908
909 ch = chars.next();
910 }
911
912 result
913}
914
915fn parse_query(query: &str) -> HashMap<String, Vec<String>> {
916 if !query.is_empty() {
917 query.split("&").map(|kv| {
918 if kv.is_empty() {
919 vec![]
920 } else if kv.contains("=") {
921 kv.splitn(2, "=").collect::<Vec<&str>>()
922 } else {
923 vec![kv]
924 }
925 }).fold(HashMap::new(), |mut map, name_value| {
926 if !name_value.is_empty() {
927 let name = decode_query(name_value[0]);
928 let value = if name_value.len() > 1 {
929 decode_query(name_value[1])
930 } else {
931 String::new()
932 };
933 map.entry(name).or_insert(vec![]).push(value);
934 }
935 map
936 })
937 } else {
938 HashMap::new()
939 }
940}
941
942async fn request_from_http_request(req: Request<Incoming>) -> WebmachineRequest {
943 let request_path = req.uri().path().to_string();
944 let method = req.method().to_string();
945 let query = match req.uri().query() {
946 Some(query) => parse_query(query),
947 None => HashMap::new()
948 };
949 let headers = headers_from_http_request(req.headers());
950
951 let body = match req.collect().await {
952 Ok(body) => {
953 let body = body.to_bytes();
954 if body.is_empty() {
955 None
956 } else {
957 Some(body.clone())
958 }
959 }
960 Err(err) => {
961 error!("Failed to read the request body: {}", err);
962 None
963 }
964 };
965
966 WebmachineRequest {
967 request_path,
968 base_path: "/".to_string(),
969 method,
970 headers,
971 body,
972 query
973 }
974}
975
976fn finalise_response(context: &mut WebmachineContext, resource: &WebmachineResource) {
977 if !context.response.has_header("Content-Type") {
978 let media_type = match &context.selected_media_type {
979 &Some(ref media_type) => media_type.clone(),
980 &None => "application/json".to_string()
981 };
982 let charset = match &context.selected_charset {
983 &Some(ref charset) => charset.clone(),
984 &None => "ISO-8859-1".to_string()
985 };
986 let header = HeaderValue {
987 value: media_type,
988 params: hashmap!{ "charset".to_string() => charset },
989 quote: false
990 };
991 context.response.add_header("Content-Type", vec![header]);
992 }
993
994 let mut vary_header = if !context.response.has_header("Vary") {
995 resource.variances
996 .iter()
997 .map(|h| HeaderValue::parse_string(h))
998 .collect()
999 } else {
1000 Vec::new()
1001 };
1002
1003 if resource.languages_provided.len() > 1 {
1004 vary_header.push(h!("Accept-Language"));
1005 }
1006 if resource.charsets_provided.len() > 1 {
1007 vary_header.push(h!("Accept-Charset"));
1008 }
1009 if resource.encodings_provided.len() > 1 {
1010 vary_header.push(h!("Accept-Encoding"));
1011 }
1012 if resource.produces.len() > 1 {
1013 vary_header.push(h!("Accept"));
1014 }
1015
1016 if vary_header.len() > 1 {
1017 context.response.add_header("Vary", vary_header.iter().cloned().unique().collect());
1018 }
1019
1020 if context.request.is_get_or_head() {
1021 {
1022 match (resource.generate_etag)(context, resource) {
1023 Some(etag) => context.response.add_header("ETag", vec![HeaderValue::basic(&etag).quote()]),
1024 None => ()
1025 }
1026 }
1027 {
1028 match (resource.expires)(context, resource) {
1029 Some(datetime) => context.response.add_header("Expires", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]),
1030 None => ()
1031 }
1032 }
1033 {
1034 match (resource.last_modified)(context, resource) {
1035 Some(datetime) => context.response.add_header("Last-Modified", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]),
1036 None => ()
1037 }
1038 }
1039 }
1040
1041 if context.response.body.is_none() && context.response.status == 200 && context.request.is_get() {
1042 match (resource.render_response)(context, resource) {
1043 Some(body) => context.response.body = Some(body),
1044 None => ()
1045 }
1046 }
1047
1048 match &resource.finalise_response {
1049 Some(callback) => {
1050 callback(context, resource);
1051 },
1052 None => ()
1053 }
1054
1055 debug!("Final response: {:?}", context.response);
1056}
1057
1058fn generate_http_response(context: &WebmachineContext) -> http::Result<Response<Full<Bytes>>> {
1059 let mut response = Response::builder().status(context.response.status);
1060
1061 for (header, values) in context.response.headers.clone() {
1062 let header_values = values.iter().map(|h| h.to_string()).join(", ");
1063 response = response.header(&header, &header_values);
1064 }
1065 match context.response.body.clone() {
1066 Some(body) => response.body(Full::new(body.into())),
1067 None => response.body(Full::new(Bytes::default()))
1068 }
1069}
1070
1071pub struct WebmachineDispatcher {
1073 pub routes: BTreeMap<&'static str, WebmachineResource>
1075}
1076
1077impl WebmachineDispatcher {
1078 pub async fn dispatch(&self, req: Request<Incoming>) -> http::Result<Response<Full<Bytes>>> {
1081 let mut context = self.context_from_http_request(req).await;
1082 self.dispatch_to_resource(&mut context);
1083 generate_http_response(&context)
1084 }
1085
1086 async fn context_from_http_request(&self, req: Request<Incoming>) -> WebmachineContext {
1087 let request = request_from_http_request(req).await;
1088 WebmachineContext {
1089 request,
1090 response: WebmachineResponse::default(),
1091 .. WebmachineContext::default()
1092 }
1093 }
1094
1095 fn match_paths(&self, request: &WebmachineRequest) -> Vec<String> {
1096 let request_path = sanitise_path(&request.request_path);
1097 self.routes
1098 .keys()
1099 .filter(|k| request_path.starts_with(&sanitise_path(k)))
1100 .map(|k| k.to_string())
1101 .collect()
1102 }
1103
1104 fn lookup_resource(&self, path: &str) -> Option<&WebmachineResource> {
1105 self.routes.get(path)
1106 }
1107
1108 pub fn dispatch_to_resource(&self, context: &mut WebmachineContext) {
1111 let matching_paths = self.match_paths(&context.request);
1112 let ordered_by_length: Vec<String> = matching_paths.iter()
1113 .cloned()
1114 .sorted_by(|a, b| Ord::cmp(&b.len(), &a.len())).collect();
1115 match ordered_by_length.first() {
1116 Some(path) => {
1117 update_paths_for_resource(&mut context.request, path);
1118 if let Some(resource) = self.lookup_resource(path) {
1119 execute_state_machine(context, &resource);
1120 finalise_response(context, &resource);
1121 } else {
1122 context.response.status = 404;
1123 }
1124 },
1125 None => context.response.status = 404
1126 };
1127 }
1128}
1129
1130#[cfg(test)]
1131mod tests;
1132
1133#[cfg(test)]
1134mod content_negotiation_tests;