pijul-hooks 0.1.2

Tools to handle webhooks from nest.pijul.com
Documentation
//! # Hooks for the Pijul Nest
//!
//! This crate can be used to write web servers that can receive
//! requests from nest.pijul.com. It takes care of all the
//! necessary authentication and parsing.
//!
//! Here is an example client and server:
//!
//! ```
//! extern crate futures;
//! use futures::Future;
//! use pijul_hooks::*;
//! tokio::run(futures::lazy(move || {
//!     let secret = "ce sera notre petit secret";
//!     let port = 9812;
//!     let addr = ([127, 0, 0, 1], port).into();
//!     let make_service = move || {
//!         hyper::service::service_fn(move |req| {
//!             parse_request(req, &secret).map(|hook| {
//!                 hook.unwrap();
//!                 let ok = hyper::Body::from("Ok");
//!                 hyper::Response::new(ok)
//!             })
//!         })
//!     };
//!     let (tx, rx) = futures::sync::oneshot::channel::<()>();
//!     let server = hyper::Server::bind(&addr).serve(make_service);
//!     hyper::rt::spawn(server.with_graceful_shutdown(rx).map_err(|e| {
//!         eprintln!("server error: {}", e);
//!     }));
//!     (Hook {
//!         url: format!("http://!127.0.0.1:{}/", port).parse().unwrap(),
//!         secret: secret.to_string(),
//!     })
//!     .run(&HookContent::Discussion {
//!         repository_owner: "owner".to_string(),
//!         repository_name: "name".to_string(),
//!         discussion_number: 1234,
//!         title: "title".to_string(),
//!         author: "author".to_string(),
//!     })
//!     .map_err(|e| eprintln!("error: {:?}", e))
//!     .map(|_| tx.send(()).unwrap())
//! }))
//! ```

#[macro_use]
extern crate serde_derive;
extern crate futures;
extern crate hex;
extern crate hyper;
extern crate openssl;
extern crate reqwest;
extern crate serde_json;
#[cfg(tests)]
extern crate tokio;
use futures::future::Either;
use futures::{Future, Stream};

#[derive(Debug, Serialize, Deserialize)]
pub enum HookContent {
    Discussion {
        repository_owner: String,
        repository_name: String,
        discussion_number: u32,
        title: String,
        author: String,
    },
    NewPatches {
        repository_owner: String,
        repository_name: String,
        pusher: String,
        patches: Vec<Patch>,
    },
    PatchesApplied {
        repository_owner: String,
        repository_name: String,
        merged_by: String,
        title: String,
        author: String,
        patches: Vec<Patch>,
    },
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Patch {
    pub hash: String,
    pub authors: Vec<String>,
    pub url: String,
    pub name: String,
}

#[derive(Debug)]
pub struct Hook {
    pub url: reqwest::Url,
    pub secret: String,
}

impl Hook {
    /// Send a hook contents to the URL described by `self`, signed
    /// with the secret in `self`.
    pub fn run(
        self,
        contents: &HookContent,
    ) -> impl Future<Item = reqwest::async::Response, Error = reqwest::Error> {
        use hex::encode;
        let body = serde_json::to_string(contents).unwrap();
        let host = if let (Some(host), Some(port)) = (self.url.host(), self.url.port()) {
            Some(format!("{}:{}", host, port))
        } else {
            None
        };
        let client = reqwest::async::Client::new().post(self.url);
        let client = if let Some(ref host) = host {
            client.header(reqwest::header::HOST, host.as_str())
        } else {
            client
        };
        let signature = {
            use openssl::hash::MessageDigest;
            use openssl::pkey::PKey;
            use openssl::sign::Signer;
            let pkey = PKey::hmac(self.secret.as_bytes()).unwrap();
            let mut signer = Signer::new(MessageDigest::sha256(), &pkey).unwrap();
            signer.update(body.as_bytes()).unwrap();
            signer.sign_to_vec().unwrap()
        };
        println!("sig: {:?}", signature);
        let s = "sha256=".to_string() + &encode(&signature);
        let client = client.header("X-Nest-Event-Signature", s.as_str());

        client.body(body).send()
    }
}

/// Handle a hook request, verifying the signature. The future
/// returned by this function yields `None` if the signature is
/// invalid.
pub fn parse_request(
    req: hyper::Request<hyper::Body>,
    secret: &str,
) -> impl Future<Item = Option<HookContent>, Error = hyper::Error> {
    use hex::decode;
    use openssl::hash::MessageDigest;
    use openssl::pkey::PKey;
    use openssl::sign::Signer;
    let pkey = PKey::hmac(secret.as_bytes()).unwrap();
    let (digest, sig) = if let Some(sig) = req.headers().get("X-Nest-Event-Signature") {
        if let Ok(sig) = std::str::from_utf8(sig.as_bytes()) {
            let mut sp = sig.split("=");
            match (sp.next(), sp.next().and_then(|sig| decode(sig).ok())) {
                (Some("sha256"), Some(sig)) => (MessageDigest::sha256(), sig),
                _ => return Either::B(futures::finished(None)),
            }
        } else {
            return Either::B(futures::finished(None));
        }
    } else {
        return Either::B(futures::finished(None));
    };
    println!("received: {:?}", sig);
    Either::A(req.into_body().concat2().map(move |body| {
        let mut signer = Signer::new(digest, &pkey).unwrap();
        signer.update(body.as_ref()).unwrap();
        if let Ok(hmac) = signer.sign_to_vec() {
            if openssl::memcmp::eq(&hmac, &sig) {
                if let Ok(s) = std::str::from_utf8(&body) {
                    return serde_json::from_str(s).ok();
                }
            }
        }
        None
    }))
}