github: wip webhook support

This commit is contained in:
Antoine Martin 2021-09-12 02:16:54 +02:00
parent 0ce80677aa
commit c24ae3e7a9
7 changed files with 607 additions and 6 deletions

View file

@ -22,7 +22,7 @@ pub struct Prololo {
impl Prololo {
/// Creates a new [`Prololo`] bot and builds a [`matrix_sdk::Client`] using the provided
/// [`Config`].
/// [`ProloloConfig`].
///
/// The [`Client`] is only initialized, not ready to be used yet.
pub fn new(config: ProloloConfig) -> anyhow::Result<Self> {

View file

@ -3,6 +3,7 @@ use std::io::BufReader;
use std::path::PathBuf;
use clap::Clap;
use rocket::routes;
mod bot;
use bot::Prololo;
@ -10,15 +11,18 @@ use bot::Prololo;
mod config;
use config::ProloloConfig;
mod webhooks;
use webhooks::github_webhook;
#[derive(Clap)]
#[clap(version = "0.1")]
struct Opts {
/// File where session information will be saved
/// Configuration file for prololo
#[clap(short, long, parse(from_os_str))]
config: PathBuf,
}
#[tokio::main]
#[rocket::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
@ -28,7 +32,8 @@ async fn main() -> anyhow::Result<()> {
let prololo = Prololo::new(config)?;
prololo.init().await?;
prololo.run().await;
tokio::spawn(async move { prololo.run().await });
Ok(())
let rocket = rocket::build().mount("/", routes![github_webhook]);
rocket.launch().await.map_err(|err| anyhow::anyhow!(err))
}

70
src/webhooks/github.rs Normal file
View file

@ -0,0 +1,70 @@
use anyhow::anyhow;
use rocket::{
http::Status,
request::{FromRequest, Outcome},
Request,
};
use serde::Deserialize;
mod signing;
use signing::SignedGitHubPayload;
use tracing::info;
const X_GITHUB_EVENT: &str = "X-GitHub-Event";
struct GitHubSecret(String);
#[rocket::post("/api/webhooks/github", data = "<payload>")]
pub fn github_webhook(event: GitHubEventType, payload: SignedGitHubPayload) -> &'static str {
info!(
"received event {:?} with signed payload:\n{}",
event, payload.0
);
"OK"
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GitHubEventType {
Create,
Issues,
IssueComment,
Push,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for GitHubEventType {
type Error = anyhow::Error;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let event_types = request.headers().get(X_GITHUB_EVENT).collect::<Vec<_>>();
if event_types.len() != 1 {
return Outcome::Failure((
Status::BadRequest,
anyhow!("request header needs exactly one event type"),
));
}
let event_type = event_types[0];
match serde_json::from_str::<GitHubEventType>(event_type) {
Ok(ev_type) => Outcome::Success(ev_type),
Err(e) => Outcome::Failure((Status::BadRequest, anyhow!(e))),
}
}
}
enum GitHubEvent {
Create { ref_type: RefType },
Issues,
IssueComment,
Push,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum RefType {
Branch,
Tag,
}

View file

@ -0,0 +1,95 @@
use std::io;
use std::ops::{Deref, DerefMut};
use anyhow::anyhow;
use rocket::{
data::{ByteUnit, FromData, Outcome},
http::{ContentType, Status},
Data, Request, State,
};
use crate::webhooks::github::GitHubSecret;
const X_GITHUB_SIGNATURE: &str = "X-Hub-Signature-256";
fn validate_signature(secret: &str, signature: &str, data: &str) -> bool {
use hmac::{Hmac, Mac, NewMac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("this should never fail");
mac.update(data.as_bytes());
match hex::decode(signature) {
Ok(bytes) => mac.verify(&bytes).is_ok(),
Err(_) => false,
}
}
pub struct SignedGitHubPayload(pub String);
// FIXME: probably not needed
impl Deref for SignedGitHubPayload {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
impl DerefMut for SignedGitHubPayload {
fn deref_mut(&mut self) -> &mut String {
&mut self.0
}
}
const LIMIT: ByteUnit = ByteUnit::Mebibyte(1);
// Tracking issue for chaining Data guards to avoid reimplementing all this:
// https://github.com/SergioBenitez/Rocket/issues/775
#[rocket::async_trait]
impl<'r> FromData<'r> for SignedGitHubPayload {
type Error = anyhow::Error;
async fn from_data(request: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
let json_ct = ContentType::new("application", "json");
if request.content_type() != Some(&json_ct) {
return Outcome::Failure((Status::BadRequest, anyhow!("wrong content type")));
}
let signatures = request
.headers()
.get(X_GITHUB_SIGNATURE)
.collect::<Vec<_>>();
if signatures.len() != 1 {
return Outcome::Failure((
Status::BadRequest,
anyhow!("request header needs exactly one signature"),
));
}
let size_limit = request.limits().get("json").unwrap_or(LIMIT);
let content = match data.open(size_limit).into_string().await {
Ok(s) if s.is_complete() => s.into_inner(),
Ok(_) => {
let eof = io::ErrorKind::UnexpectedEof;
return Outcome::Failure((
Status::PayloadTooLarge,
io::Error::new(eof, "data limit exceeded").into(),
));
}
Err(e) => return Outcome::Failure((Status::BadRequest, e.into())),
};
let signature = signatures[0];
let secret = request.guard::<&State<GitHubSecret>>().await.unwrap();
if !validate_signature(&secret.0, signature, &content) {
return Outcome::Failure((Status::BadRequest, anyhow!("couldn't verify signature")));
}
Outcome::Success(SignedGitHubPayload(content))
}
}

2
src/webhooks/mod.rs Normal file
View file

@ -0,0 +1,2 @@
mod github;
pub use github::github_webhook;