1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Implementation of Hyper's service-related traits for constructor and handler
//!
//! In hyper, `Constructor` can be served using `Server::bind(&addr).serve(constructor)`.
//!
//! Example:
//!
//! ```
//! extern crate rifling;
//! extern crate hyper;
//!
//! use rifling::Constructor;
//!
//! let _ = hyper::Server::bind(&"0.0.0.0:4567".parse().unwrap()).serve(Constructor::new());
//! ```

use futures::stream::Stream;
use futures::{future, Future};
use hyper::service::{NewService, Service};
use hyper::{Body, Error, Request, Response, StatusCode};

use super::Constructor;
use super::ContentType;
use super::Delivery;
use super::Handler;

/// Get Option<String> typed header value from HeaderMap<HeaderValue> of hyper.
macro_rules! hyper_get_header_value {
    ($headers:expr, $key:expr) => {
        if let Some(value) = $headers.get($key) {
            if let Ok(inner) = value.to_str() {
                Some(String::from(inner.clone()))
            } else {
                None
            }
        } else {
            None
        }
    };
}

/// Implement `NewService` trait to `Constructor`
impl NewService for Constructor {
    type ReqBody = Body;
    type ResBody = Body;
    type Error = Error;
    type Service = Handler;
    type Future = Box<Future<Item = Self::Service, Error = Self::InitError> + Send>;
    type InitError = Error;

    /// Create a new handler to handle the service
    fn new_service(&self) -> Self::Future {
        debug!("Creating new service");
        Box::new(future::ok(Handler::from(self)))
    }
}

/// Implement `Service` struct from `Hyper` to `Handler`
impl Service for Handler {
    type ReqBody = Body;
    type ResBody = Body;
    type Error = Error;
    type Future = Box<Future<Item = Response<Body>, Error = Error> + Send + 'static>;

    /// Handle the request
    fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
        fn response(status_code: StatusCode, body: &'static str) -> Response<Body> {
            Response::builder()
                .status(status_code)
                .body(body.into())
                .unwrap()
        }
        let headers = req.headers();
        let (event, executor) =
            if let Some(event_string) = hyper_get_header_value!(&headers, "X-Github-Event") {
                (
                    Some(event_string.clone()),
                    self.get_hooks(event_string.as_str()),
                )
            } else {
                // Invalid payload without a event header
                return Box::new(future::ok(response(
                    StatusCode::ACCEPTED,
                    "Invalid payload",
                )));
            };
        if executor.is_empty() {
            // No matched hook found
            return Box::new(future::ok(response(
                StatusCode::ACCEPTED,
                "No matched hook configured",
            )));
        }
        let id = hyper_get_header_value!(&headers, "X-Github-Delivery");
        let signature = hyper_get_header_value!(&headers, "X-Hub-Signature");
        let content_type = hyper_get_header_value!(&headers, "content-type");
        if content_type.is_none() {
            // No valid content-type header found
            return Box::new(future::ok(response(
                StatusCode::ACCEPTED,
                "Invalid payload",
            )));
        }
        Box::new(
            req.into_body()
                .concat2()
                .map(move |chunk| String::from_utf8(chunk.to_vec()).ok())
                .and_then(move |request_body| {
                    let content_type: ContentType = match content_type.unwrap().as_str() {
                        "application/x-www-form-urlencoded" => ContentType::URLENCODED,
                        _ => ContentType::JSON, // Default
                    };
                    if request_body.is_some() {
                        let delivery =
                            Delivery::new(id, event, signature, content_type, request_body);
                        debug!("Received delivery: {:#?}", &delivery);
                        executor.run(delivery);
                        future::ok(response(StatusCode::OK, "OK"))
                    } else {
                        future::ok(response(StatusCode::ACCEPTED, "Invalid payload"))
                    }
                }),
        )
    }
}