/* * Copyright (C) 2022 Aravinth Manivannan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use actix_web::web; use actix_web::HttpRequest; use hmac::{Hmac, Mac}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use tracing::{info, warn}; use url::Url; use crate::ctx::Ctx; use crate::errors::ServiceError; use crate::errors::ServiceResult; pub type HmacSha256 = Hmac; #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct CommitPerson { pub name: String, pub email: String, pub username: String, } #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] pub struct Commit { 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 { 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 { pub admin: bool, pub push: bool, pub pull: bool, } #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] pub struct InternalTracker { 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 { 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"))] pub reference: String, pub before: String, pub after: String, pub compare_url: String, pub repository: Repository, pub pusher: Person, pub sender: Person, } impl Ctx { pub async fn process_webhook( &self, body: &web::Bytes, req: &HttpRequest, auth_token: &str, ) -> ServiceResult<()> { let headers = req.headers(); let _uuid = headers.get("X-Gitea-Delivery").unwrap(); let sig = headers.get("X-Gitea-Signature").unwrap(); let sig = hex::decode(sig).unwrap(); let event_type = headers.get("X-Gitea-Event").unwrap(); let payload: WebhookPayload = serde_json::from_slice(&body).unwrap(); let hook = self.db.get_webhook(auth_token).await?; for url in [ &payload.repository.html_url, &payload.repository.ssh_url, &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())?; mac.update(&body); mac.verify_slice(&sig[..])?; let site = self.db.get_site_from_repo_url(url).await?; self.db .webhook_link_site(auth_token, &Url::parse(&site.repo_url)?) .await?; if payload.reference.contains(&site.branch) { info!( "[webhook][forgejo/gitea] received update {:?} from {url} repository on deployed branch", event_type ); self.update_site(&site.site_secret, Some(site.branch)) .await?; } else { info!( "[webhook][forgejo/gitea] received update {:?} from {url} repository on non-deployed branch {}", event_type, payload.reference ); } return Ok(()); } } warn!( "[webhook][forgejo/gitea] stray update from {} repository", payload.repository.html_url ); Err(ServiceError::WebsiteNotFound) } }