github: wip webhook support
This commit is contained in:
parent
0ce80677aa
commit
c24ae3e7a9
7 changed files with 607 additions and 6 deletions
|
|
@ -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> {
|
||||
|
|
|
|||
13
src/main.rs
13
src/main.rs
|
|
@ -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
70
src/webhooks/github.rs
Normal 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,
|
||||
}
|
||||
95
src/webhooks/github/signing.rs
Normal file
95
src/webhooks/github/signing.rs
Normal 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
2
src/webhooks/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
mod github;
|
||||
pub use github::github_webhook;
|
||||
Loading…
Add table
Add a link
Reference in a new issue