From d919bad5702d757f600df9c30a303d8cd4fdd4f8 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 28 Dec 2022 13:36:30 +0530 Subject: [PATCH] feat: test webhooks --- src/api/v1/gitea.rs | 96 +++++++++++++++++++- src/ctx/api/v1/gitea.rs | 192 ++++++++++++++++++++-------------------- src/errors.rs | 16 ++++ 3 files changed, 206 insertions(+), 98 deletions(-) diff --git a/src/api/v1/gitea.rs b/src/api/v1/gitea.rs index 7fb7072..16d1dda 100644 --- a/src/api/v1/gitea.rs +++ b/src/api/v1/gitea.rs @@ -136,8 +136,10 @@ pub fn services(cfg: &mut web::ServiceConfig) { #[cfg(test)] mod tests { - use actix_web::{http::StatusCode, test}; + use actix_web::{error::ResponseError, http::StatusCode, test}; + use hmac::Mac; + use crate::ctx::api::v1::gitea::{HmacSha256, WebhookPayload}; use crate::db::GiteaWebhook; use crate::tests; use crate::*; @@ -153,6 +155,7 @@ mod tests { let (_dir, ctx) = tests::get_ctx().await; let _ = ctx.delete_user(NAME, PASSWORD).await; let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let page = ctx.add_test_site(NAME.into()).await; let cookies = get_cookie!(signin_resp); let app = get_app!(ctx).await; @@ -185,6 +188,95 @@ mod tests { check_status!(list_all_webhooks_resp, StatusCode::OK); let hooks: Vec = actix_web::test::read_body_json(list_all_webhooks_resp).await; - assert_eq!(vec![hook], hooks); + assert_eq!(vec![hook.clone()], hooks); + + let webhook_url = format!("{}?auth={}", V1_API_ROUTES.gitea.webhook, hook.auth_token); + + // test webhook + let mut webhook_payload = WebhookPayload::default(); + webhook_payload.reference = format!("refs/origin/{}", page.branch); + webhook_payload.repository.html_url = page.repo; + + let body = serde_json::to_string(&webhook_payload).unwrap(); + let body = body.as_bytes(); + let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes()) + .expect("HMAC can take key of any size"); + mac.update(&body); + let res = mac.finalize(); + let sig = res.into_bytes(); + let sig = hex::encode(&sig[..]); + + let post_to_webhook_resp = test::call_service( + &app, + post_request!(&webhook_payload, &webhook_url) + .insert_header(("X-Gitea-Delivery", "foobar213randomuuid")) + .insert_header(("X-Gitea-Signature", sig.clone())) + .insert_header(("X-Gitea-Event", "push")) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + check_status!(post_to_webhook_resp, StatusCode::OK); + + // no webhook + let fake_webhook_url = format!( + "{}?auth={}", + V1_API_ROUTES.gitea.webhook, hook.gitea_webhook_secret + ); + let body = serde_json::to_string(&webhook_payload).unwrap(); + let body = body.as_bytes(); + let mut mac = + HmacSha256::new_from_slice(b"nosecret").expect("HMAC can take key of any size"); + mac.update(&body); + let res = mac.finalize(); + let fake_sig = res.into_bytes(); + let fake_sig = hex::encode(&fake_sig[..]); + + let post_to_no_exist_webhook_resp = test::call_service( + &app, + post_request!(&webhook_payload, &fake_webhook_url) + .insert_header(("X-Gitea-Delivery", "foobar213randomuuid")) + .insert_header(("X-Gitea-Signature", fake_sig)) + .insert_header(("X-Gitea-Event", "push")) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + let err = ServiceError::WebhookNotFound; + assert_eq!(post_to_no_exist_webhook_resp.status(), err.status_code()); + let resp_err: ErrorToResponse = + actix_web::test::read_body_json(post_to_no_exist_webhook_resp).await; + assert_eq!(resp_err.error, err.to_string()); + + // no website + let mut webhook_payload = WebhookPayload::default(); + webhook_payload.reference = format!("refs/origin/{}", page.branch); + webhook_payload.repository.html_url = "foo".into(); + + let body = serde_json::to_string(&webhook_payload).unwrap(); + let body = body.as_bytes(); + let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes()) + .expect("HMAC can take key of any size"); + mac.update(&body); + let res = mac.finalize(); + let sig = res.into_bytes(); + let sig = hex::encode(&sig[..]); + + let post_to_no_website_webhook_resp = test::call_service( + &app, + post_request!(&webhook_payload, &webhook_url) + .insert_header(("X-Gitea-Delivery", "foobar213randomuuid")) + .insert_header(("X-Gitea-Signature", sig.clone())) + .insert_header(("X-Gitea-Event", "push")) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + let err = ServiceError::WebsiteNotFound; + assert_eq!(post_to_no_website_webhook_resp.status(), err.status_code()); + let resp_err: ErrorToResponse = + actix_web::test::read_body_json(post_to_no_website_webhook_resp).await; + assert_eq!(resp_err.error, err.to_string()); } } diff --git a/src/ctx/api/v1/gitea.rs b/src/ctx/api/v1/gitea.rs index 989aee0..c9219d6 100644 --- a/src/ctx/api/v1/gitea.rs +++ b/src/ctx/api/v1/gitea.rs @@ -22,127 +22,128 @@ use sha2::Sha256; use tracing::{info, warn}; use crate::ctx::Ctx; - +use crate::errors::ServiceError; use crate::errors::ServiceResult; -type HmacSha256 = Hmac; +pub type HmacSha256 = Hmac; #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct CommitPerson { - name: String, - email: String, - username: String, + pub name: String, + pub email: String, + pub username: String, } #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] pub struct Commit { - id: String, - message: String, - url: String, - author: CommitPerson, - committer: CommitPerson, - verification: serde_json::Value, - timestamp: String, - added: serde_json::Value, - removed: serde_json::Value, - modified: serde_json::Value, + pub id: String, + pub message: String, + pub url: String, + pub author: CommitPerson, + pub committer: CommitPerson, + pub verification: serde_json::Value, + pub timestamp: String, + pub added: serde_json::Value, + pub removed: serde_json::Value, + pub modified: serde_json::Value, } #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct Person { - id: usize, - login: String, - full_name: String, - email: String, - avatar_url: String, - language: String, - is_admin: bool, - last_login: String, - created: String, - restricted: bool, - active: bool, - prohibit_login: bool, - location: String, - website: String, - description: String, - visibility: String, - followers_count: usize, - following_count: usize, - starred_repos_count: usize, - username: String, + pub id: usize, + pub login: String, + pub full_name: String, + pub email: String, + pub avatar_url: String, + pub language: String, + pub is_admin: bool, + pub last_login: String, + pub created: String, + pub restricted: bool, + pub active: bool, + pub prohibit_login: bool, + pub location: String, + pub website: String, + pub description: String, + pub visibility: String, + pub followers_count: usize, + pub following_count: usize, + pub starred_repos_count: usize, + pub username: String, } #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct Permissions { - admin: bool, - push: bool, - pull: bool, + pub admin: bool, + pub push: bool, + pub pull: bool, } + #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct InternalTracker { - enable_time_tracker: bool, - allow_only_contributors_to_track_time: bool, - enable_issue_dependencies: bool, + pub enable_time_tracker: bool, + pub allow_only_contributors_to_track_time: bool, + pub enable_issue_dependencies: bool, } #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct Repository { - id: usize, - owner: Person, - name: String, - full_name: String, - description: String, - empty: bool, - private: bool, - fork: bool, - template: bool, - parent: Option, - mirror: bool, - size: usize, - html_url: String, - ssh_url: String, - clone_url: String, - original_url: String, - website: String, - stars_count: usize, - forks_count: usize, - watchers_count: usize, - open_issues_count: usize, - open_pr_counter: usize, - release_counter: usize, - default_branch: String, - archived: bool, - created_at: String, - updated_at: String, - permissions: Permissions, - has_issues: bool, - internal_tracker: InternalTracker, - has_wiki: bool, - has_pull_requests: bool, - has_projects: bool, - ignore_whitespace_conflicts: bool, - allow_merge_commits: bool, - allow_rebase: bool, - allow_rebase_explicit: bool, - allow_squash_merge: bool, - default_merge_style: String, - avatar_url: String, - internal: bool, - mirror_interval: String, - mirror_updated: String, - repo_transfer: Option, + pub id: usize, + pub owner: Person, + pub name: String, + pub full_name: String, + pub description: String, + pub empty: bool, + pub private: bool, + pub fork: bool, + pub template: bool, + pub parent: Option, + pub mirror: bool, + pub size: usize, + pub html_url: String, + pub ssh_url: String, + pub clone_url: String, + pub original_url: String, + pub website: String, + pub stars_count: usize, + pub forks_count: usize, + pub watchers_count: usize, + pub open_issues_count: usize, + pub open_pr_counter: usize, + pub release_counter: usize, + pub default_branch: String, + pub archived: bool, + pub created_at: String, + pub updated_at: String, + pub permissions: Permissions, + pub has_issues: bool, + pub internal_tracker: InternalTracker, + pub has_wiki: bool, + pub has_pull_requests: bool, + pub has_projects: bool, + pub ignore_whitespace_conflicts: bool, + pub allow_merge_commits: bool, + pub allow_rebase: bool, + pub allow_rebase_explicit: bool, + pub allow_squash_merge: bool, + pub default_merge_style: String, + pub avatar_url: String, + pub internal: bool, + pub mirror_interval: String, + pub mirror_updated: String, + pub repo_transfer: Option, } #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct WebhookPayload { #[serde(rename(serialize = "ref", deserialize = "ref"))] - reference: String, - before: String, - after: String, - compare_url: String, - repository: Repository, - pusher: Person, - sender: Person, + pub reference: String, + pub before: String, + pub after: String, + pub compare_url: String, + pub repository: Repository, + pub pusher: Person, + pub sender: Person, } impl Ctx { @@ -168,10 +169,9 @@ impl Ctx { &payload.repository.clone_url, ] { if self.db.site_with_repository_exists(url).await? { - let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes()) - .expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes())?; mac.update(&body); - mac.verify_slice(&sig[..]).unwrap(); + mac.verify_slice(&sig[..])?; let site = self.db.get_site_from_repo_url(url).await?; if payload.reference.contains(&site.branch) { @@ -195,6 +195,6 @@ impl Ctx { "[webhook][forgejo/gitea] stray update from {} repository", payload.repository.html_url ); - Ok(()) + Err(ServiceError::WebsiteNotFound) } } diff --git a/src/errors.rs b/src/errors.rs index fbebab6..42c2b96 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -28,6 +28,8 @@ use argon2_creds::errors::CredsError; use config::ConfigError as ConfigErrorInner; use derive_more::{Display, Error}; use git2::Error as GitError; +use hmac::digest::InvalidLength; +use hmac::digest::MacError; use serde::{Deserialize, Serialize}; use url::ParseError; @@ -184,6 +186,20 @@ pub enum ServiceError { WebhookNotFound, } +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(_: InvalidLength) -> ServiceError { + ServiceError::InternalServerError + } +} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(_: MacError) -> ServiceError { + ServiceError::WebhookNotFound + } +} + impl From for ServiceError { #[cfg(not(tarpaulin_include))] fn from(_: ParseError) -> ServiceError {