1
0
Fork 0
mirror of https://codeberg.org/Mo8it/git-webhook-client synced 2024-11-21 11:06:32 +00:00

Add extractors, use inner instead of 0, replace new with build

This commit is contained in:
Mo 2022-12-24 23:26:12 +01:00
parent 0aba8b543f
commit 23dfc4fa61
8 changed files with 130 additions and 59 deletions

View file

@ -13,7 +13,6 @@ askama = { git = "https://github.com/djc/askama.git" }
askama_axum = { git = "https://github.com/djc/askama.git", package = "askama_axum" } askama_axum = { git = "https://github.com/djc/askama.git", package = "askama_axum" }
axum = { version = "0.6", default-features = false, features = ["http1", "tokio", "macros", "query"] } axum = { version = "0.6", default-features = false, features = ["http1", "tokio", "macros", "query"] }
axum-extra = { version = "0.4", features = ["spa"] } axum-extra = { version = "0.4", features = ["spa"] }
bytes = "1.3"
chrono = { version = "0.4", default-features = false, features = ["clock"] } chrono = { version = "0.4", default-features = false, features = ["clock"] }
diesel = { version = "2.0", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35", "without-deprecated"] } diesel = { version = "2.0", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35", "without-deprecated"] }
diesel_migrations = { version = "2.0.0", features = ["sqlite"] } diesel_migrations = { version = "2.0.0", features = ["sqlite"] }

View file

@ -50,7 +50,7 @@ pub struct Config {
} }
impl Config { impl Config {
pub fn new() -> Result<Self> { pub fn build() -> Result<Self> {
let config_file_var = "GWC_CONFIG_FILE"; let config_file_var = "GWC_CONFIG_FILE";
let config_path = env::var(config_file_var) let config_path = env::var(config_file_var)
.with_context(|| format!("Environment variable {config_file_var} missing!"))?; .with_context(|| format!("Environment variable {config_file_var} missing!"))?;

View file

@ -4,24 +4,28 @@ use axum::{
}; };
use tracing::error; use tracing::error;
pub struct AppError(anyhow::Error); pub struct AppError {
inner: anyhow::Error,
}
impl IntoResponse for AppError { impl IntoResponse for AppError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
error!("{:?}", self.0); error!("{:?}", self.inner);
(StatusCode::BAD_REQUEST, format!("{:?}", self.0)).into_response() (StatusCode::BAD_REQUEST, format!("{:?}", self.inner)).into_response()
} }
} }
impl From<anyhow::Error> for AppError { impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self { fn from(err: anyhow::Error) -> Self {
Self(err) Self { inner: err }
} }
} }
impl From<&str> for AppError { impl From<&str> for AppError {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Self(anyhow::Error::msg(s.to_string())) Self {
inner: anyhow::Error::msg(s.to_string()),
}
} }
} }

86
src/extractors.rs Normal file
View file

@ -0,0 +1,86 @@
use anyhow::Context;
use axum::{
async_trait,
body::Bytes,
extract::{FromRequest, FromRequestParts},
http::request::{Parts, Request},
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::{errors::AppError, states};
pub struct ReceivedSignature {
pub inner: Vec<u8>,
}
#[async_trait]
impl<S> FromRequestParts<S> for ReceivedSignature
where
S: Send + Sync,
{
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
let mut received_signatures = parts.headers.get_all("X-GITEA-SIGNATURE").iter();
let received_signature = received_signatures.next().context("Missing signature!")?;
if received_signatures.next().is_some() {
return Err("Received more than one signature!".into());
}
let received_signature = hex::decode(received_signature)
.context("Can not hex decode the received signature!")?;
Ok(ReceivedSignature {
inner: received_signature.to_vec(),
})
}
}
impl ReceivedSignature {
pub async fn is_valid(self, secret: &[u8], body: &[u8]) -> bool {
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
.expect("Can not generate a mac from the secret!");
mac.update(body);
let expected_signature = mac.finalize().into_bytes();
self.inner[..] == expected_signature[..]
}
}
pub struct ValidatedBody {
pub inner: Bytes,
}
#[async_trait]
impl<S, B> FromRequest<S, B> for ValidatedBody
where
Bytes: FromRequest<S, B>,
B: Send + 'static,
S: Send + Sync + states::ConfigState,
{
type Rejection = AppError;
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
let (mut parts, body) = req.into_parts();
let received_signature = ReceivedSignature::from_request_parts(&mut parts, state).await?;
let req = Request::from_parts(parts, body);
let body = Bytes::from_request(req, state)
.await
.map_err(|_| "Can not extract body as Bytes!")?;
let state_config = state.config();
if !received_signature
.is_valid(&state_config.secret, &body)
.await
{
return Err("Invalid signature!".into());
}
Ok(Self { inner: body })
}
}

View file

@ -15,7 +15,7 @@ pub struct Mailer {
} }
impl Mailer { impl Mailer {
pub fn new(config: &mut config::Config) -> Result<Self> { pub fn build(config: &mut config::Config) -> Result<Self> {
let creds = Credentials::new( let creds = Credentials::new(
mem::take(&mut config.email_server.email), mem::take(&mut config.email_server.email),
mem::take(&mut config.email_server.password), mem::take(&mut config.email_server.password),

View file

@ -1,6 +1,7 @@
mod config; mod config;
mod db; mod db;
mod errors; mod errors;
mod extractors;
mod logging; mod logging;
mod mailer; mod mailer;
mod models; mod models;
@ -22,8 +23,8 @@ use std::{
use tracing::info; use tracing::info;
async fn init() -> Result<()> { async fn init() -> Result<()> {
let mut config = config::Config::new()?; let mut config = config::Config::build()?;
let mailer = mailer::Mailer::new(&mut config)?; let mailer = mailer::Mailer::build(&mut config)?;
let address = config.socket_address.address; let address = config.socket_address.address;
let socket_address = SocketAddr::new( let socket_address = SocketAddr::new(
@ -35,7 +36,7 @@ async fn init() -> Result<()> {
let _tracing_gurad = logging::init_logger(&config.logging); let _tracing_gurad = logging::init_logger(&config.logging);
let app_state = states::AppState::new(config, mailer)?; let app_state = states::AppState::build(config, mailer)?;
db::run_migrations(&mut db::get_conn(&app_state.db.pool)?)?; db::run_migrations(&mut db::get_conn(&app_state.db.pool)?)?;

View file

@ -2,18 +2,14 @@ use anyhow::Context;
use askama_axum::IntoResponse; use askama_axum::IntoResponse;
use axum::{ use axum::{
extract::{Query, State}, extract::{Query, State},
http::header::HeaderMap,
response::Response, response::Response,
}; };
use bytes::Bytes;
use hmac::{Hmac, Mac};
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use sha2::Sha256;
use std::{process::Command, sync::Arc, thread}; use std::{process::Command, sync::Arc, thread};
use tracing::{error, info}; use tracing::{error, info};
use crate::{db, errors, mailer, states, templates}; use crate::{db, errors, extractors, mailer, states, templates};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct IndexQuery { pub struct IndexQuery {
@ -21,7 +17,7 @@ pub struct IndexQuery {
} }
pub async fn index( pub async fn index(
State(db_state): State<Arc<states::DB>>, State(db): State<Arc<states::DB>>,
query: Query<IndexQuery>, query: Query<IndexQuery>,
) -> Result<Response, errors::AppError> { ) -> Result<Response, errors::AppError> {
let id = match query.id { let id = match query.id {
@ -30,7 +26,7 @@ pub async fn index(
None => -1, None => -1,
}; };
let hook_log = db::get_hook_log(&db_state.pool, id)?; let hook_log = db::get_hook_log(&db.pool, id)?;
info!("Viewed hook log with id: {}", hook_log.id); info!("Viewed hook log with id: {}", hook_log.id);
@ -39,41 +35,16 @@ pub async fn index(
Ok(template.into_response()) Ok(template.into_response())
} }
async fn is_valid_signature(secret: &[u8], received_signature: &[u8], body: &[u8]) -> bool {
let mut mac =
Hmac::<Sha256>::new_from_slice(secret).expect("Can not generate a mac from the secret!");
mac.update(body);
let expected_signature = mac.finalize().into_bytes();
received_signature[..] == expected_signature[..]
}
pub async fn trigger( pub async fn trigger(
State(db_state): State<Arc<states::DB>>, State(db): State<Arc<states::DB>>,
State(config_state): State<Arc<states::Config>>, State(state_config): State<Arc<states::StateConfig>>,
State(mailer): State<Arc<mailer::Mailer>>, State(mailer): State<Arc<mailer::Mailer>>,
headers: HeaderMap, body: extractors::ValidatedBody,
body: Bytes,
) -> Result<Response, errors::AppError> { ) -> Result<Response, errors::AppError> {
info!("Trigger called"); info!("Trigger called");
let mut received_signatures = headers.get_all("X-GITEA-SIGNATURE").iter();
let received_signature = received_signatures.next().context("Missing signature!")?;
let received_signature =
hex::decode(received_signature).context("Can not hex decode the received signature!")?;
if received_signatures.next().is_some() {
return Err("Received more than one signature!".into());
}
if !is_valid_signature(&config_state.secret, &received_signature, &body).await {
return Err("Invalid signature!".into());
}
let json: Value = let json: Value =
serde_json::from_slice(&body).context("Can not parse the request body into JSON!")?; serde_json::from_slice(&body.inner).context("Can not parse the request body into JSON!")?;
let repo = json let repo = json
.get("repository") .get("repository")
@ -87,13 +58,13 @@ pub async fn trigger(
.as_str() .as_str()
.context("The value of clone_url from repository in the request body is not a string!")?; .context("The value of clone_url from repository in the request body is not a string!")?;
let hook = config_state.get_hook(clone_url).with_context(|| { let hook = state_config.get_hook(clone_url).with_context(|| {
format!("No matching repository with url {clone_url} in the configuration file.") format!("No matching repository with url {clone_url} in the configuration file.")
})?; })?;
let hook_log_id = db::add_hook_log(&db_state.pool, hook)?.id; let hook_log_id = db::add_hook_log(&db.pool, hook)?.id;
let hook_log_link = format!("{}/?id={}", config_state.base_url, hook_log_id); let hook_log_link = format!("{}/?id={}", state_config.base_url, hook_log_id);
{ {
// Spawn and detach a thread that runs the command and fills the output in the log. // Spawn and detach a thread that runs the command and fills the output in the log.
@ -103,7 +74,7 @@ pub async fn trigger(
let command = hook.command.clone(); let command = hook.command.clone();
let args = hook.args.clone(); let args = hook.args.clone();
let current_dir = hook.current_dir.clone(); let current_dir = hook.current_dir.clone();
let db_pool = db_state.pool.clone(); let db_pool = db.pool.clone();
let clone_url = clone_url.to_string(); let clone_url = clone_url.to_string();
let hook_name = hook.name.clone(); let hook_name = hook.name.clone();
let hook_log_link = hook_log_link.clone(); let hook_log_link = hook_log_link.clone();

View file

@ -9,20 +9,20 @@ pub struct DB {
} }
impl DB { impl DB {
pub fn new() -> Result<Self> { pub fn build() -> Result<Self> {
Ok(Self { Ok(Self {
pool: db::establish_connection_pool()?, pool: db::establish_connection_pool()?,
}) })
} }
} }
pub struct Config { pub struct StateConfig {
pub secret: Vec<u8>, pub secret: Vec<u8>,
pub base_url: String, pub base_url: String,
pub hooks: Vec<config::Hook>, pub hooks: Vec<config::Hook>,
} }
impl From<config::Config> for Config { impl From<config::Config> for StateConfig {
fn from(config: config::Config) -> Self { fn from(config: config::Config) -> Self {
Self { Self {
secret: config.secret.as_bytes().to_owned(), secret: config.secret.as_bytes().to_owned(),
@ -32,7 +32,7 @@ impl From<config::Config> for Config {
} }
} }
impl Config { impl StateConfig {
pub fn get_hook(&self, clone_url: &str) -> Option<&config::Hook> { pub fn get_hook(&self, clone_url: &str) -> Option<&config::Hook> {
self.hooks.iter().find(|&hook| hook.repo_url == clone_url) self.hooks.iter().find(|&hook| hook.repo_url == clone_url)
} }
@ -40,17 +40,27 @@ impl Config {
#[derive(Clone, FromRef)] #[derive(Clone, FromRef)]
pub struct AppState { pub struct AppState {
pub config: Arc<Config>, pub config: Arc<StateConfig>,
pub mailer: Arc<mailer::Mailer>, pub mailer: Arc<mailer::Mailer>,
pub db: Arc<DB>, pub db: Arc<DB>,
} }
impl AppState { impl AppState {
pub fn new(config: config::Config, mailer: mailer::Mailer) -> Result<Self> { pub fn build(config: config::Config, mailer: mailer::Mailer) -> Result<Self> {
Ok(Self { Ok(Self {
config: Arc::new(Config::from(config)), config: Arc::new(StateConfig::from(config)),
mailer: Arc::new(mailer), mailer: Arc::new(mailer),
db: Arc::new(DB::new()?), db: Arc::new(DB::build()?),
}) })
} }
} }
pub trait ConfigState {
fn config(&self) -> Arc<StateConfig>;
}
impl ConfigState for AppState {
fn config(&self) -> Arc<StateConfig> {
self.config.clone()
}
}