1
0
Fork 0
mirror of https://codeberg.org/Mo8it/git-webhook-client synced 2024-10-18 07:22:39 +00:00

Use template and replace expect with match

This commit is contained in:
Mo 2022-10-21 04:54:57 +02:00
parent 6f1b94e4f7
commit c270cd91db
10 changed files with 203 additions and 120 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
/Cargo.lock /Cargo.lock
/config.json /config.json
/db/ /db/
/scripts/
/target/ /target/

View file

@ -17,6 +17,7 @@ diesel = { version = "2.0.2", features = [
hex = "0.4.3" hex = "0.4.3"
hmac = "0.12.1" hmac = "0.12.1"
rocket = "0.5.0-rc.2" rocket = "0.5.0-rc.2"
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85" serde_json = "1.0.87"
sha2 = "0.10.6" sha2 = "0.10.6"

6
Rocket.toml Normal file
View file

@ -0,0 +1,6 @@
[default]
template_dir = "templates"
[release]
address = "0.0.0.0"
port = 80

View file

@ -10,23 +10,30 @@ use crate::schema::hooklog;
pub type DBPool = Pool<ConnectionManager<SqliteConnection>>; pub type DBPool = Pool<ConnectionManager<SqliteConnection>>;
pub fn establish_connection_pool() -> DBPool { pub fn establish_connection_pool() -> Result<DBPool, String> {
let database_url = let database_url = match env::var("DATABASE_URL") {
env::var("DATABASE_URL").expect("Environment variable DATABASE_URL missing!"); Ok(url) => url,
Err(_) => return Err("Environment variable DATABASE_URL missing!".to_string()),
};
let manager = ConnectionManager::<SqliteConnection>::new(&database_url); let manager = ConnectionManager::<SqliteConnection>::new(&database_url);
Pool::builder() match Pool::builder().build(manager) {
.build(manager) Ok(pool) => Ok(pool),
.expect("Could not build database connection pool!") Err(_) => Err("Could not build database connection pool!".to_string()),
}
} }
fn get_conn(pool: &DBPool) -> PooledConnection<ConnectionManager<SqliteConnection>> { fn get_conn(
pool.get() pool: &DBPool,
.expect("Can not get a connection from the database pool!") ) -> Result<PooledConnection<ConnectionManager<SqliteConnection>>, String> {
match pool.get() {
Ok(pool) => Ok(pool),
Err(_) => Err("Could not get database pool!".to_string()),
}
} }
pub fn add_hook_log(pool: &DBPool, hook: &Hook, output: &Output) -> i32 { pub fn add_hook_log(pool: &DBPool, hook: &Hook, output: &Output) -> Result<i32, String> {
let conn = &mut get_conn(pool); let conn = &mut get_conn(pool)?;
let command_with_args = hook.command.to_owned() + " " + &hook.args.join(" "); let command_with_args = hook.command.to_owned() + " " + &hook.args.join(" ");
@ -35,32 +42,45 @@ pub fn add_hook_log(pool: &DBPool, hook: &Hook, output: &Output) -> i32 {
repo_url: &hook.repo_url, repo_url: &hook.repo_url,
command_with_args: &command_with_args, command_with_args: &command_with_args,
current_dir: &hook.current_dir, current_dir: &hook.current_dir,
stdout: std::str::from_utf8(&output.stdout).expect("Can not convert stdout to str!"), stdout: match std::str::from_utf8(&output.stdout) {
stderr: std::str::from_utf8(&output.stderr).expect("Can not convert stderr to str!"), Ok(s) => s,
Err(_) => return Err("Can not convert stdout to str!".to_string()),
},
stderr: match std::str::from_utf8(&output.stderr) {
Ok(s) => s,
Err(_) => return Err("Can not convert stderr to str!".to_string()),
},
status_code: output.status.code(), status_code: output.status.code(),
}; };
let result: HookLog = diesel::insert_into(hooklog::table) let result = diesel::insert_into(hooklog::table)
.values(&new_hook_log) .values(&new_hook_log)
.get_result(conn) .get_result::<HookLog>(conn);
.expect("Error saving hook log!");
result.id match result {
Ok(hook_log) => Ok(hook_log.id),
Err(e) => Err(e.to_string()),
}
} }
pub fn get_hook_log(pool: &DBPool, id: i32) -> HookLog { pub fn get_hook_log(pool: &DBPool, id: i32) -> Result<HookLog, String> {
let conn = &mut get_conn(pool); // id=0 not allowed!
if id >= 0 { let conn = &mut get_conn(pool)?;
hooklog::dsl::hooklog
.find(id) let hl: Result<HookLog, diesel::result::Error>;
.first(conn)
.expect("No hook log exists for this id!") if id > 0 {
hl = hooklog::dsl::hooklog.find(id).first(conn);
} else { } else {
hooklog::dsl::hooklog hl = hooklog::dsl::hooklog
.order(hooklog::dsl::id.desc()) .order(hooklog::dsl::id.desc())
.offset((-id - 1).into()) .offset((-id - 1).into())
.first(conn) .first(conn);
.expect("No hook log exists for this negative id!") }
match hl {
Ok(hl) => Ok(hl),
Err(e) => Err(e.to_string()),
} }
} }

View file

@ -11,60 +11,93 @@ pub struct Repo<'r> {
pub clone_url: &'r str, pub clone_url: &'r str,
} }
#[derive(Debug)]
pub enum RepoDataGuardError {
PayloadTooLarge,
MissingSignature,
MoreThatOneSignature,
InvalidSignature,
Io(std::io::Error),
}
#[rocket::async_trait] #[rocket::async_trait]
impl<'r> FromData<'r> for Repo<'r> { impl<'r> FromData<'r> for Repo<'r> {
type Error = RepoDataGuardError; type Error = String;
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
let payload = match data.open(Limits::JSON).into_bytes().await { let payload = match data.open(Limits::JSON).into_bytes().await {
Ok(payload) if payload.is_complete() => payload.into_inner(), Ok(payload) if payload.is_complete() => payload.into_inner(),
Ok(_) => { Ok(_) => {
return Outcome::Failure((Status::PayloadTooLarge, Self::Error::PayloadTooLarge)) return Outcome::Failure((Status::PayloadTooLarge, "Payload too large".to_string()))
} }
Err(e) => return Outcome::Failure((Status::InternalServerError, Self::Error::Io(e))), Err(e) => return Outcome::Failure((Status::InternalServerError, e.to_string())),
}; };
let mut received_signatures = req.headers().get("X-GITEA-SIGNATURE"); let mut received_signatures = req.headers().get("X-GITEA-SIGNATURE");
let received_signature = match received_signatures.next() { let received_signature = match received_signatures.next() {
Some(signature) => { Some(signature) => match hex::decode(signature) {
hex::decode(signature).expect("Can not hex decode the received signature!") Ok(signature) => signature,
Err(_) => {
return Outcome::Failure((
Status::BadRequest,
"Can not hex decode the received signature!".to_string(),
))
}
},
None => {
return Outcome::Failure((Status::BadRequest, "Missing signature!".to_string()))
} }
None => return Outcome::Failure((Status::BadRequest, Self::Error::MissingSignature)),
}; };
if received_signatures.next().is_some() { if received_signatures.next().is_some() {
return Outcome::Failure((Status::BadRequest, Self::Error::MoreThatOneSignature)); return Outcome::Failure((
Status::BadRequest,
"Received more than one signature!".to_string(),
));
} }
let config_state = req let config_state = match req.rocket().state::<states::Config>() {
.rocket() Some(state) => state,
.state::<states::Config>() None => {
.expect("Can not get the config state!"); return Outcome::Failure((
Status::BadRequest,
"Can not get the config state!".to_string(),
))
}
};
if !is_valid_signature(&config_state.secret, &received_signature, &payload) { if !is_valid_signature(&config_state.secret, &received_signature, &payload) {
return Outcome::Failure((Status::BadRequest, Self::Error::InvalidSignature)); return Outcome::Failure((Status::BadRequest, "Invalid signature!".to_string()));
} }
let json: Value = let json: Value = match serde_json::from_slice(&payload) {
serde_json::from_slice(&payload).expect("Can not parse payload into JSON!"); Ok(json) => json,
let repo = json Err(_) => {
.get("repository") return Outcome::Failure((
.expect("Can not get the repository value from the payload!"); Status::BadRequest,
let clone_url = repo "Can not parse payload into JSON!".to_string(),
.get("clone_url") ))
.expect("Can not get value clone_url from repository in the payload!") }
.as_str() };
.expect("The value of clone_url from repository in the payload is not a string!") let repo = match json.get("repository") {
.to_string(); Some(repo) => repo,
None => {
return Outcome::Failure((
Status::BadRequest,
"Can not get the repository value from the payload!".to_string(),
))
}
};
let clone_url = match repo.get("clone_url") {
Some(url) => url,
None => {
return Outcome::Failure((
Status::BadRequest,
"Can not get value clone_url from repository in the payload!".to_string(),
))
}
};
let clone_url = match clone_url.as_str() {
Some(url) => url.to_string(),
None => {
return Outcome::Failure((
Status::BadRequest,
"The value of clone_url from repository in the payload is not a string!"
.to_string(),
))
}
};
let clone_url = request::local_cache!(req, clone_url); let clone_url = request::local_cache!(req, clone_url);
@ -73,8 +106,13 @@ impl<'r> FromData<'r> for Repo<'r> {
} }
fn is_valid_signature(secret: &[u8], received_signature: &[u8], payload: &[u8]) -> bool { fn is_valid_signature(secret: &[u8], received_signature: &[u8], payload: &[u8]) -> bool {
let mut mac = let mut mac = match Hmac::<Sha256>::new_from_slice(secret) {
Hmac::<Sha256>::new_from_slice(secret).expect("Can not generate a mac from the secret!"); Ok(mac) => mac,
Err(_) => {
println!("Can not generate a mac from the secret!");
return false;
}
};
mac.update(payload); mac.update(payload);
let expected_signature = mac.finalize().into_bytes(); let expected_signature = mac.finalize().into_bytes();

View file

@ -6,6 +6,8 @@ mod routes;
mod schema; mod schema;
mod states; mod states;
use rocket_dyn_templates::Template;
#[rocket::launch] #[rocket::launch]
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build()
@ -13,4 +15,5 @@ fn rocket() -> _ {
.mount("/api", rocket::routes![routes::trigger]) .mount("/api", rocket::routes![routes::trigger])
.manage(states::DB::new()) .manage(states::DB::new())
.manage(states::Config::new()) .manage(states::Config::new())
.attach(Template::fairing())
} }

View file

@ -1,8 +1,9 @@
use diesel::prelude::{Insertable, Queryable}; use diesel::prelude::{Insertable, Queryable};
use serde::Serialize;
use crate::schema::hooklog; use crate::schema::hooklog;
#[derive(Queryable)] #[derive(Queryable, Serialize)]
pub struct HookLog { pub struct HookLog {
pub id: i32, pub id: i32,
pub datetime: String, pub datetime: String,

View file

@ -1,4 +1,6 @@
use rocket::response::status::BadRequest;
use rocket::{get, post, State}; use rocket::{get, post, State};
use rocket_dyn_templates::Template;
use std::process::Command; use std::process::Command;
use crate::db; use crate::db;
@ -6,51 +8,25 @@ use crate::guards;
use crate::states; use crate::states;
#[get("/?<id>")] #[get("/?<id>")]
pub fn index(db_state: &State<states::DB>, id: Option<i32>) -> String { pub fn index(
db_state: &State<states::DB>,
id: Option<i32>,
) -> Result<Template, BadRequest<String>> {
let id = match id { let id = match id {
Some(id) => id, Some(id) => id,
None => -1, None => -1,
}; };
let hook_log = db::get_hook_log(&db_state.pool, id); if id == 0 {
return Err(BadRequest(Some("id=0 not allowed!".to_string())));
format!(
"Hook log id:
{}
Datetime:
{}
Repository url:
{}
Command with arguments:
{}
Current directory of the command:
{}
Standard output:
{}
Standard error:
{}
Status code:
{}
",
hook_log.id,
hook_log.datetime,
hook_log.repo_url,
hook_log.command_with_args,
hook_log.current_dir,
hook_log.stdout,
hook_log.stderr,
match hook_log.status_code {
Some(code) => code.to_string(),
None => String::from("None"),
} }
)
let hook_log = match db::get_hook_log(&db_state.pool, id) {
Ok(hl) => hl,
Err(e) => return Err(BadRequest(Some(e))),
};
Ok(Template::render("hook_log", hook_log))
} }
#[post("/trigger", format = "json", data = "<repo>")] #[post("/trigger", format = "json", data = "<repo>")]
@ -58,19 +34,32 @@ pub fn trigger(
db_state: &State<states::DB>, db_state: &State<states::DB>,
config_state: &State<states::Config>, config_state: &State<states::Config>,
repo: guards::Repo, repo: guards::Repo,
) -> String { ) -> Result<String, BadRequest<String>> {
let hook = config_state.get_hook(repo.clone_url).expect(&format!( let hook = match config_state.get_hook(repo.clone_url) {
Some(hook) => hook,
None => {
return Err(BadRequest(Some(format!(
"No matching repository with url {} in the configuration file.", "No matching repository with url {} in the configuration file.",
repo.clone_url repo.clone_url
)); ))))
}
};
let output = Command::new(&hook.command) let output = match Command::new(&hook.command)
.args(&hook.args) .args(&hook.args)
.current_dir(&hook.current_dir) .current_dir(&hook.current_dir)
.output() .output()
.expect("Can not run the hook command!"); {
Ok(output) => output,
let new_hook_log_id = db::add_hook_log(&db_state.pool, hook, &output); Err(_) => {
return Err(BadRequest(Some(
format!("{}/?id={}", config_state.base_url, new_hook_log_id) "Can not run the hook command!".to_string(),
)))
}
};
match db::add_hook_log(&db_state.pool, hook, &output) {
Ok(new_hook_log_id) => Ok(format!("{}/?id={}", config_state.base_url, new_hook_log_id)),
Err(e) => Err(BadRequest(Some(e))),
}
} }

View file

@ -7,9 +7,10 @@ pub struct DB {
impl DB { impl DB {
pub fn new() -> Self { pub fn new() -> Self {
let pool = db::establish_connection_pool(); match db::establish_connection_pool() {
Ok(pool) => Self { pool },
Self { pool } Err(e) => panic!("Could not establish database pool: {}", e),
}
} }
} }

23
templates/hook_log.tera Normal file
View file

@ -0,0 +1,23 @@
Hook log id:
{{ id }}
Datetime:
{{ datetime }}
Repository url:
{{ repo_url }}
Command with arguments:
{{ command_with_args }}
Current directory of the command:
{{ current_dir }}
Standard output:
{{ stdout }}
Standard error:
{{ stderr }}
Status code:
{{ status_code }}