diff --git a/.gitignore b/.gitignore index ea8c4bf..cf417ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +/config.json diff --git a/Cargo.lock b/Cargo.lock index 6a0cbd8..8ee0d9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,10 +395,14 @@ dependencies = [ ] [[package]] -name = "gitea-webhook" +name = "git-webhook-client" version = "0.1.0" dependencies = [ + "hmac", "rocket", + "serde", + "serde_json", + "sha2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f11aa96..cdb95de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "gitea-webhook" +name = "git-webhook-client" version = "0.1.0" authors = ["Mo Bitar "] edition = "2021" @@ -7,4 +7,8 @@ readme = "README.adoc" license-file = "LICENSE" [dependencies] +hmac = "0.12.1" rocket = "0.5.0-rc.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.85" +sha2 = "0.10.6" diff --git a/LICENSE.txt b/LICENSE.txt index 265bb11..c8d0868 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -629,7 +629,7 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - gitea-webhook + Git webhook client Copyright (C) 2022 Mo Bitar This program is free software: you can redistribute it and/or modify diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..238fdf6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,98 @@ +#[macro_use] +extern crate rocket; + +use rocket::data::{self, Data, FromData, Limits, Outcome, ToByteUnit}; +use rocket::http::Status; +use rocket::outcome::Outcome::{Failure, Success}; +use rocket::request::{self, Request}; + +use hmac::{Hmac, Mac}; +use sha2::Sha256; + +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::BufReader; + +const CONFIG_FILENAME: &str = "config.json"; + +struct SignatureDataGuard; + +fn is_valid_signature(received_signature: &str, payload: &Vec) -> bool { + let config_file = File::open(CONFIG_FILENAME).unwrap(); + let config_reader = BufReader::new(config_file); + let config: Config = serde_json::from_reader(config_reader).unwrap(); + + type HmacSha256 = Hmac; + let mut mac = HmacSha256::new_from_slice(config.secret.as_bytes()).unwrap(); + mac.update(payload); + let expected_signature = mac.finalize().into_bytes(); + + received_signature.as_bytes()[..] == expected_signature[..] +} + +#[derive(Debug)] +enum Error { + PayloadTooLarge, + MissingSignature, + MoreThatOneSignature, + InvalidSignature, + Io(std::io::Error), +} + +#[rocket::async_trait] +impl<'r> FromData<'r> for SignatureDataGuard { + type Error = Error; + + async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> { + let payload = match data.open(256.bytes()).into_bytes().await { + Ok(payload) if payload.is_complete() => payload.into_inner(), + Ok(_) => return Failure((Status::PayloadTooLarge, Error::PayloadTooLarge)), + Err(e) => return Failure((Status::InternalServerError, Error::Io(e))), + }; + + let mut received_signatures = req.headers().get("X-GITEA-SIGNATURE"); + let received_signature = match received_signatures.next() { + Some(signature) => signature, + None => return Outcome::Failure((Status::BadRequest, Error::MissingSignature)), + }; + + if received_signatures.next().is_some() { + return Outcome::Failure((Status::BadRequest, Error::MoreThatOneSignature)); + } + + if !is_valid_signature(received_signature, &payload) { + return Outcome::Failure((Status::BadRequest, Error::InvalidSignature)); + } + + Outcome::Success(SignatureDataGuard) + } +} + +#[derive(Deserialize)] +struct Hook { + repo_url: String, + commands: Vec, +} + +#[derive(Deserialize)] +struct Config { + secret: String, + hooks: Vec, +} + +#[get("/")] +fn index() -> &'static str { + "Hello, world!" +} + +#[post("/trigger", data = "")] +fn trigger(payload: SignatureDataGuard) -> &'static str { + "Sensitive data." +} + +#[launch] +fn rocket() -> _ { + rocket::build() + .mount("/", routes![index]) + .mount("/api", routes![trigger]) +}