Skip to content

Commit

Permalink
Adds github webhook integration
Browse files Browse the repository at this point in the history
When push event is recieved, pulls

requires github token
  • Loading branch information
Jerboa-app committed Jun 1, 2024
1 parent 51ec7d2 commit 9f41c85
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 27 deletions.
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,14 @@ pub struct GitAuthConfig
/// - ```remote```: the url (public or private)
/// - ```branch```: the tracked branch
/// - ```auth```: if present either ssh key or passphrase will be used
/// - ```checkout_schedule```: schedule for checking for new commits on [GitConfig::branch]
/// - ```remote_webhook_token```: optional webhook token to recieve push events
#[derive(Clone, Serialize, Deserialize)]
pub struct GitConfig
{
pub remote: String,
pub branch: String,
pub remote_webhook_token: Option<String>,
pub checkout_schedule: Option<String>,
pub auth: Option<GitAuthConfig>
}
Expand Down
50 changes: 44 additions & 6 deletions src/integrations/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl From<std::io::Error> for GitError
/// Attempt to clone a remote repo from a [crate::config::GitConfig]
pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError>
{
if let GitConfig{auth: Some(_), remote: _, checkout_schedule: _, branch: _} = config
if let GitConfig{auth: Some(_), remote: _, checkout_schedule: _, branch: _, remote_webhook_token: _} = config
{
let auth = config.auth.clone().unwrap();
let result = match &auth.key_path
Expand Down Expand Up @@ -87,7 +87,7 @@ pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError
Ok(repo) => Ok(repo),
Err(e) =>
{
crate::debug(format!("Error {} while cloning (authenticated) repo at {}", e, config.remote), None);
crate::debug(format!("Error {} while cloning (authenticated) repo at {}", e, config.remote), Some("GIT"));
Err(GitError::from(e))
}
}
Expand All @@ -99,7 +99,7 @@ pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError
Ok(repo) => Ok(repo),
Err(e) =>
{
crate::debug(format!("Error {} while cloning (pub) repo at {}", e, config.remote), None);
crate::debug(format!("Error {} while cloning (pub) repo at {}", e, config.remote), Some("GIT"));
Err(GitError::from(e))
}
}
Expand Down Expand Up @@ -139,7 +139,7 @@ pub fn clean_and_clone(dir: &str, config: GitConfig) -> Result<Repository, GitEr

/// Fast forward pull from the repository, makes no attempt to resolve
/// if a fast foward is not possible
pub fn fast_forward_pull(repo: Repository, branch: &str) -> Result<(), GitError>
pub fn fast_forward_pull(repo: Repository, branch: &str) -> Result<Option<HeadInfo>, GitError>
{
// modified from https://stackoverflow.com/questions/58768910/how-to-perform-git-pull-with-the-rust-git2-crate
repo.find_remote("origin")?.fetch(&[branch], None, None)?;
Expand All @@ -150,18 +150,56 @@ pub fn fast_forward_pull(repo: Repository, branch: &str) -> Result<(), GitError>

if analysis.is_up_to_date()
{
Ok(())
Ok(None)
}
else if analysis.is_fast_forward()
{
let refname = format!("refs/heads/{}", branch);
let mut reference = repo.find_reference(&refname)?;
reference.set_target(fetch_commit.id(), "Fast-Forward")?;
repo.set_head(&refname)?;
Ok(repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?)
repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
Ok(head_info(&repo))
}
else
{
Err(GitError{why: "Cannot fastforward".to_owned()})
}
}

pub struct HeadInfo
{
pub hash: git2::Oid,
pub author: String,
pub datetime: String
}

pub fn head_info(repo: &Repository) -> Option<HeadInfo>
{
let head = match repo.head()
{
Ok(h) => match h.target()
{
Some(h) => h,
None => return None
},
Err(_) => return None
};

match repo.find_commit(head)
{
Ok(c) =>
{
Some
(
HeadInfo
{
hash: c.id(),
author: c.author().to_string(),
datetime: format!("{:?}", c.time())
}
)
},
Err(_) => None
}
}
62 changes: 50 additions & 12 deletions src/integrations/git/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use axum::async_trait;
use chrono::{DateTime, Utc};
use cron::Schedule;
use git2::Repository;

use tokio::sync::Mutex;

use crate::{config::{Config, CONFIG_PATH}, task::{next_job_time, schedule_from_option, Task}};
use crate::{config::{Config, CONFIG_PATH}, integrations::discord::post::try_post, task::{next_job_time, schedule_from_option, Task}};

use super::{clean_and_clone, fast_forward_pull};
use super::{clean_and_clone, fast_forward_pull, HeadInfo};

pub struct GitRefreshTask
{
Expand All @@ -34,18 +35,13 @@ impl GitRefreshTask
schedule
}
}
}

#[async_trait]
impl Task for GitRefreshTask
{
async fn run(&mut self) -> Result<(), crate::task::TaskError>
pub fn pull(config: &Config) -> Option<HeadInfo>
{
let _ = self.lock.lock().await;
let config = Config::load_or_default(CONFIG_PATH);

if config.git.is_some()
{
let git = config.git.unwrap();
let git = config.git.clone().unwrap();
let path = Path::new(&config.content.path);
if path.is_dir()
{
Expand All @@ -54,7 +50,7 @@ impl Task for GitRefreshTask
Ok(repo) => fast_forward_pull(repo, &git.branch),
Err(e) =>
{
crate::debug(format!("{}, {:?} is not a git repo", e, path), None);
crate::debug(format!("{}, {:?} is not a git repo", e, path), Some("GIT"));
match clean_and_clone(&config.content.path, git.clone())
{
Ok(repo) => fast_forward_pull(repo, &git.branch),
Expand All @@ -65,10 +61,52 @@ impl Task for GitRefreshTask

if result.is_err()
{
crate::debug(format!("{:?}", result.err()), None);
crate::debug(format!("{:?}", result.err()), Some("GIT"));
}
else
{
return result.unwrap()
}
}
}
None
}

pub async fn notify_pull(info: Option<HeadInfo>, config: &Config)
{
match info
{
Some(info) =>
{
let msg = format!
(
"Checked out new commit for {}:\n {}\n {}\n {}",
config.domain,
info.hash,
info.author,
info.datetime
);
crate::debug(msg.clone(), Some("GIT"));
try_post
(
config.notification_endpoint.clone(),
&msg
).await;
},
None => {}
}

}
}

#[async_trait]
impl Task for GitRefreshTask
{
async fn run(&mut self) -> Result<(), crate::task::TaskError>
{
let _ = self.lock.lock().await;
let config = Config::load_or_default(CONFIG_PATH);
GitRefreshTask::notify_pull(GitRefreshTask::pull(&config), &config).await;

self.schedule = schedule_from_option(config.stats.save_schedule.clone());

Expand Down
100 changes: 100 additions & 0 deletions src/integrations/github/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use axum::{body::Bytes, http::{HeaderMap, Request}, middleware::Next, response::{IntoResponse, Response}};
use regex::Regex;
use reqwest::StatusCode;

use crate::config::{Config, CONFIG_PATH};

use super::git::refresh::GitRefreshTask;

pub async fn filter_github<B>
(
headers: HeaderMap,
request: Request<B>,
next: Next<B>
) -> Result<Response, StatusCode>
where B: axum::body::HttpBody<Data = Bytes>
{
let user_agent = match std::str::from_utf8(headers["user-agent"].as_bytes())
{
Ok(u) => u,
Err(_) =>
{
return Ok(next.run(request).await)
}
};

if Regex::new(r"GitHub-Hookshot").unwrap().captures(user_agent).is_some()
{

let authentic = is_authentic(&headers, request).await;
if authentic != StatusCode::ACCEPTED
{
return Ok(authentic.into_response());
}

crate::debug("Authentic github event".to_string(), Some("GITHUB"));

if !headers.contains_key("x-github-event")
{
return Ok(StatusCode::BAD_REQUEST.into_response());
}

match std::str::from_utf8(headers["x-github-event"].as_bytes())
{
Ok(s) =>
{
if s.to_lowercase() == "push"
{
let config = Config::load_or_default(CONFIG_PATH);
GitRefreshTask::notify_pull(GitRefreshTask::pull(&config), &config).await;
}
},
Err(e) =>
{
crate::debug(format!("Invalid utf8 in x-github-event, {}", e), Some("GITHUB"));
return Ok(StatusCode::BAD_REQUEST.into_response());
}
}

return Ok(StatusCode::ACCEPTED.into_response());
}
else
{
return Ok(next.run(request).await)
}
}

async fn is_authentic<B>(headers: &HeaderMap, request: Request<B>) -> StatusCode
where B: axum::body::HttpBody<Data = Bytes>
{
let config = Config::load_or_default(CONFIG_PATH);

let token = if config.git.is_some()
{
config.git.unwrap().remote_webhook_token
}
else
{
None
};

if token.is_none()
{
return StatusCode::METHOD_NOT_ALLOWED;
}

let body = request.into_body();
let bytes = match body.collect().await {
Ok(collected) => collected.to_bytes(),
Err(_) => {
return StatusCode::BAD_REQUEST
}
};

super::is_authentic
(
&headers, "x-hub-signature-256",
token.unwrap(),
&bytes
)
}
1 change: 1 addition & 0 deletions src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use regex::Regex;
use crate::util::{read_bytes, dump_bytes};

pub mod discord;
pub mod github;
pub mod git;
pub mod webhook;

Expand Down
Loading

0 comments on commit 9f41c85

Please sign in to comment.