diff --git a/Cargo.lock b/Cargo.lock index 6bf4410..382262b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,27 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" +[[package]] +name = "async-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.51" @@ -45,6 +66,26 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281" +dependencies = [ + "autocfg", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -77,6 +118,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "binascii" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" + [[package]] name = "bitflags" version = "1.3.2" @@ -174,6 +221,17 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" +[[package]] +name = "cookie" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.1" @@ -199,6 +257,16 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -231,6 +299,39 @@ dependencies = [ "const-oid", ] +[[package]] +name = "devise" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c7580b072f1c8476148f16e0a0d5dedddab787da98d86c5082c5e9ed8ab595" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2" +dependencies = [ + "devise_core", + "quote", +] + +[[package]] +name = "devise_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841ef46f4787d9097405cac4e70fb8644fc037b526e8c14054247c0263c400d0" +dependencies = [ + "bitflags", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -296,6 +397,20 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +[[package]] +name = "figment" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "fnv" version = "1.0.7" @@ -440,6 +555,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "winapi", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -474,6 +602,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "gloo-timers" version = "0.2.1" @@ -533,6 +667,22 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "http" version = "0.2.4" @@ -635,6 +785,12 @@ dependencies = [ "unindent", ] +[[package]] +name = "inlinable_string" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3094308123a0e9fd59659ce45e22de9f53fc1d2ac6e1feb9fef988e4f76cad77" + [[package]] name = "instant" version = "0.1.10" @@ -723,6 +879,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "loom" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2111607c723d7857e0d8299d5ce7a0bf4b844d3e44f8de136b13da513eaf8fc4" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", +] + [[package]] name = "lru" version = "0.6.6" @@ -848,6 +1017,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "multer" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "408327e2999b839cd1af003fc01b2019a6c10a1361769542203f6fedc5179680" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "mime", + "spin", + "tokio", + "tokio-util", + "twoway", + "version_check", +] + [[package]] name = "native-tls" version = "0.2.8" @@ -986,6 +1175,29 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" +[[package]] +name = "pear" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1102,15 +1314,33 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "prololo-reborn" version = "0.1.0" dependencies = [ "anyhow", "clap", + "hex", + "hmac", "matrix-sdk", + "rocket", "serde", + "serde_json", "serde_yaml", + "sha2", "tokio", "tracing", "tracing-subscriber", @@ -1216,6 +1446,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300f2a835d808734ee295d45007adacb9ebb29dd3ae2424acfa17930cae541da" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c38e3aecd2b21cb3959637b883bb3714bc7e43f0268b9a29d3743ee3e55cdd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.5.4" @@ -1283,6 +1533,88 @@ dependencies = [ "winreg", ] +[[package]] +name = "rocket" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a71c18c42a0eb15bf3816831caf0dad11e7966f2a41aaf486a701979c4dd1f2" +dependencies = [ + "async-stream", + "async-trait", + "atomic", + "atty", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot", + "pin-project-lite", + "rand 0.8.4", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "state", + "tempfile", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "ubyte", + "version_check", + "yansi", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66f5fa462f7eb958bba8710c17c5d774bbbd59809fa76fb1957af7e545aea8bb" +dependencies = [ + "devise", + "glob", + "indexmap", + "proc-macro2", + "quote", + "rocket_http", + "syn", + "unicode-xid", +] + +[[package]] +name = "rocket_http" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c8b7d512d2fcac2316ebe590cde67573844b99e6cc9ee0f53375fa16e25ebd" +dependencies = [ + "cookie", + "either", + "http", + "hyper", + "indexmap", + "log", + "memchr", + "mime", + "parking_lot", + "pear", + "percent-encoding", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec", + "stable-pattern", + "state", + "time", + "tokio", + "uncased", +] + [[package]] name = "ruma" version = "0.4.0" @@ -1516,6 +1848,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -1532,6 +1870,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1702,6 +2046,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" + [[package]] name = "spki" version = "0.4.0" @@ -1711,6 +2061,15 @@ dependencies = [ "der", ] +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" +dependencies = [ + "memchr", +] + [[package]] name = "standback" version = "0.2.17" @@ -1720,6 +2079,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "state" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5" +dependencies = [ + "loom", +] + [[package]] name = "stdweb" version = "0.4.20" @@ -1941,6 +2309,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.8" @@ -2061,12 +2440,47 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "twoway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" +dependencies = [ + "memchr", + "unchecked-index", +] + [[package]] name = "typenum" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +[[package]] +name = "ubyte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42756bb9e708855de2f8a98195643dff31a97f0485d90d8467b39dc24be9e8fe" +dependencies = [ + "serde", +] + +[[package]] +name = "uncased" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicode-bidi" version = "0.3.6" @@ -2287,6 +2701,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" + [[package]] name = "zeroize" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 2a6d91c..9f5603f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,12 @@ resolver = "2" [dependencies] anyhow = "1.0" -serde = "1.0" +hex = "0.4" +hmac = "0.11" +serde = { version = "1.0", features = [ "derive" ] } +serde_json = "1.0" serde_yaml = "0.8" +sha2 = "0.9" tokio = { version = "1.0", features = [ "full" ] } tracing-subscriber = "0.2" tracing = "0.1" @@ -25,3 +29,8 @@ features = [ "native-tls" ] version = "3.0.0-beta.4" default-features = false features = ["cargo", "derive", "std"] + +[dependencies.rocket] +version = "0.5.0-rc.1" +# don't need private-cookies +default-features = false diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 8da7ecc..3aa47d1 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -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 { diff --git a/src/main.rs b/src/main.rs index 64dacf2..33e9315 100644 --- a/src/main.rs +++ b/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)) } diff --git a/src/webhooks/github.rs b/src/webhooks/github.rs new file mode 100644 index 0000000..83c20a2 --- /dev/null +++ b/src/webhooks/github.rs @@ -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 = "")] +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 { + let event_types = request.headers().get(X_GITHUB_EVENT).collect::>(); + 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::(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, +} diff --git a/src/webhooks/github/signing.rs b/src/webhooks/github/signing.rs new file mode 100644 index 0000000..32c550e --- /dev/null +++ b/src/webhooks/github/signing.rs @@ -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; + + 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::>(); + 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>().await.unwrap(); + + if !validate_signature(&secret.0, signature, &content) { + return Outcome::Failure((Status::BadRequest, anyhow!("couldn't verify signature"))); + } + + Outcome::Success(SignedGitHubPayload(content)) + } +} diff --git a/src/webhooks/mod.rs b/src/webhooks/mod.rs new file mode 100644 index 0000000..f8096f5 --- /dev/null +++ b/src/webhooks/mod.rs @@ -0,0 +1,2 @@ +mod github; +pub use github::github_webhook;