use serde_json;
use hmac::{Hmac, Mac};
use sha1;
use providers::prelude::*;
use utils;
use common::prelude::*;
lazy_static! {
static ref GITHUB_EVENTS: Vec<&'static str> = vec![
"commit_comment", "create", "delete", "deployment",
"deployment_status", "fork", "gollum", "issue_comment", "issues",
"label", "member", "membership", "milestone", "organization",
"page_build", "project_card", "project_column", "project", "public",
"pull_reques_review_comment", "pull_request_review", "pull_request",
"push", "repository", "release", "status", "team", "team_add", "watch",
];
static ref GITHUB_HEADERS: Vec<&'static str> = vec![
"X-GitHub-Event",
"X-Hub-Signature",
"X-GitHub-Delivery",
];
}
#[derive(Deserialize)]
struct PushEvent<'src> {
#[serde(rename = "ref")]
git_ref: &'src str,
head_commit: PushCommit<'src>,
}
#[derive(Deserialize)]
struct PushCommit<'src> {
id: &'src str,
}
#[derive(Debug, Deserialize)]
pub struct GitHubProvider {
secret: Option<String>,
events: Option<Vec<String>>,
}
impl ProviderTrait for GitHubProvider {
fn new(input: &str) -> Result<GitHubProvider> {
let inst: GitHubProvider = serde_json::from_str(input)?;
if let Some(ref events) = inst.events {
for event in events {
if !GITHUB_EVENTS.contains(&event.as_ref()) {
return Err(ErrorKind::ProviderGitHubInvalidEventName(
event.clone()
).into());
}
}
}
Ok(inst)
}
fn validate(&self, request: &Request) -> RequestType {
let req;
if let Request::Web(ref inner) = *request {
req = inner;
} else {
return RequestType::Invalid;
}
for header in GITHUB_HEADERS.iter() {
if !req.headers.contains_key(*header) {
return RequestType::Invalid;
}
}
if let Some(ref secret) = self.secret {
let signature = &req.headers["X-Hub-Signature"];
if !verify_signature(secret, &req.body, signature) {
return RequestType::Invalid;
}
}
let event = &req.headers["X-GitHub-Event"];
if !(GITHUB_EVENTS.contains(&event.as_ref()) || *event == "ping") {
return RequestType::Invalid;
}
if let Some(ref events) = self.events {
if !(events.contains(event) || *event == "ping") {
return RequestType::Invalid;
}
}
if serde_json::from_str::<serde_json::Value>(&req.body).is_err() {
return RequestType::Invalid;
}
if event == "ping" {
return RequestType::Ping;
}
RequestType::ExecuteHook
}
fn build_env(&self, r: &Request, b: &mut EnvBuilder) -> Result<()> {
let req;
if let Request::Web(ref inner) = *r {
req = inner;
} else {
return Ok(());
}
b.add_env("EVENT", &req.headers["X-GitHub-Event"]);
b.add_env("DELIVERY_ID", &req.headers["X-GitHub-Delivery"]);
let event = &req.headers["X-GitHub-Event"];
if self.events.as_ref().and_then(|e| Some(e.contains(event))).unwrap_or(false) {
if *event == "push" {
let parsed: PushEvent = serde_json::from_str(&req.body)?;
b.add_env("PUSH_REF", parsed.git_ref);
b.add_env("PUSH_HEAD", parsed.head_commit.id);
}
}
Ok(())
}
}
fn verify_signature(secret: &str, payload: &str, raw_signature: &str) -> bool {
type HmacSha1 = Hmac<sha1::Sha1>;
if !raw_signature.contains('=') {
return false;
}
let splitted: Vec<&str> = raw_signature.split('=').collect();
let algorithm = &splitted[0];
let hex_signature = splitted
.iter()
.skip(1)
.cloned()
.collect::<Vec<&str>>()
.join("=");
let signature = if let Ok(converted) = utils::from_hex(&hex_signature) {
converted
} else {
return false;
};
if *algorithm != "sha1" {
return false;
}
let mut mac = HmacSha1::new_varkey(secret.as_bytes()).unwrap();
mac.input(payload.as_bytes());
mac.verify(&signature).is_ok()
}
#[cfg(test)]
mod tests {
use utils::testing::*;
use requests::RequestType;
use web::WebRequest;
use providers::ProviderTrait;
use scripts::EnvBuilder;
use super::{verify_signature, GitHubProvider, GITHUB_EVENTS};
#[test]
fn test_new() {
for right in &[
r#"{}"#,
r#"{"secret": "abcde"}"#,
r#"{"events": ["push", "fork"]}"#,
r#"{"secret": "abcde", "events": ["push", "fork"]}"#,
] {
assert!(GitHubProvider::new(right).is_ok(), right.to_string());
}
for wrong in &[
r#"{"secret": 12345}"#,
r#"{"secret": true}"#,
r#"{"events": 12345}"#,
r#"{"events": true}"#,
r#"{"events": {}}"#,
r#"{"events": [12345]}"#,
r#"{"events": [true]}"#,
r#"{"events": ["invalid_event"]}"#,
] {
assert!(GitHubProvider::new(wrong).is_err(), wrong.to_string());
}
}
#[test]
fn test_request_type() {
let provider = GitHubProvider::new("{}").unwrap();
macro_rules! assert_req_type {
($provider:expr, $event:expr, $expected:expr) => {
let mut request = dummy_web_request();
let _ = request.headers.insert(
"X-GitHub-Event".into(),
$event.to_string(),
);
let _ = request.headers.insert(
"X-GitHub-Delivery".into(),
"12345".into(),
);
let _ = request.headers.insert(
"X-Hub-Signature".into(),
"invalid".into(),
);
request.body = "{}".into();
assert_eq!($provider.validate(&request.into()), $expected);
};
}
assert_req_type!(provider, "ping", RequestType::Ping);
for event in GITHUB_EVENTS.iter() {
assert_req_type!(provider, event, RequestType::ExecuteHook);
}
}
#[test]
fn test_build_env() {
let mut req = dummy_web_request();
req.headers.insert("X-GitHub-Event".into(), "ping".into());
req.headers.insert("X-GitHub-Delivery".into(), "12345".into());
let provider = GitHubProvider::new("{}").unwrap();
let mut b = EnvBuilder::dummy();
provider.build_env(&req.into(), &mut b).unwrap();
assert_eq!(b.dummy_data().env, hashmap! {
"EVENT".into() => "ping".into(),
"DELIVERY_ID".into() => "12345".into(),
});
assert_eq!(b.dummy_data().files, hashmap!());
}
fn dummy_push_event_request(event: &str) -> WebRequest {
let mut req = dummy_web_request();
req.headers.insert("X-GitHub-Delivery".into(), "12345".into());
req.headers.insert("X-GitHub-Event".into(), event.into());
req.body = ::serde_json::to_string(&json!({
"ref": "refs/heads/master",
"head_commit": json!({
"id": "deadbeef",
}),
})).unwrap();
req
}
#[test]
fn test_build_env_event_push_wrong_event() {
let req = dummy_push_event_request("ping");
let provider = GitHubProvider::new(
r#"{"events": ["create", "push"]}"#
).unwrap();
let mut b = EnvBuilder::dummy();
provider.build_env(&req.into(), &mut b).unwrap();
assert_eq!(b.dummy_data().env.get("PUSH_REF"), None);
assert_eq!(b.dummy_data().env.get("PUSH_HEAD"), None);
}
#[test]
fn test_build_env_event_push_no_whitelist() {
let req = dummy_push_event_request("push");
let provider = GitHubProvider::new("{}").unwrap();
let mut b = EnvBuilder::dummy();
provider.build_env(&req.into(), &mut b).unwrap();
assert_eq!(b.dummy_data().env.get("PUSH_REF"), None);
assert_eq!(b.dummy_data().env.get("PUSH_HEAD"), None);
}
#[test]
fn test_build_env_event_push_correct() {
let req = dummy_push_event_request("push");
let provider = GitHubProvider::new(r#"{"events": ["push"]}"#).unwrap();
let mut b = EnvBuilder::dummy();
provider.build_env(&req.into(), &mut b).unwrap();
assert_eq!(
b.dummy_data().env.get("PUSH_REF"), Some(&"refs/heads/master".into())
);
assert_eq!(
b.dummy_data().env.get("PUSH_HEAD"), Some(&"deadbeef".into())
);
}
#[test]
fn test_verify_signature() {
for signature in &[
"invalid", "invalid=invalid", "sha1=g", "sha1=e75efc0f29bf50c23f99b30b86f7c78fdaf5f11d",
] {
assert!(
!verify_signature("secret", "payload", signature),
signature.to_string()
);
}
assert!(verify_signature(
"secret",
"payload",
"sha1=f75efc0f29bf50c23f99b30b86f7c78fdaf5f11d"
));
}
}