mirror of
https://codeberg.org/Mo8it/git-webhook-client
synced 2024-11-21 11:06:32 +00:00
Use template and replace expect with match
This commit is contained in:
parent
6f1b94e4f7
commit
c270cd91db
10 changed files with 203 additions and 120 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
/config.json
|
/config.json
|
||||||
/db/
|
/db/
|
||||||
|
/scripts/
|
||||||
/target/
|
/target/
|
||||||
|
|
|
@ -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
6
Rocket.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[default]
|
||||||
|
template_dir = "templates"
|
||||||
|
|
||||||
|
[release]
|
||||||
|
address = "0.0.0.0"
|
||||||
|
port = 80
|
74
src/db.rs
74
src/db.rs
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
106
src/guards.rs
106
src/guards.rs
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
23
templates/hook_log.tera
Normal 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 }}
|
Loading…
Reference in a new issue