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:
parent
0aba8b543f
commit
23dfc4fa61
8 changed files with 130 additions and 59 deletions
|
@ -13,7 +13,6 @@ askama = { git = "https://github.com/djc/askama.git" }
|
|||
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-extra = { version = "0.4", features = ["spa"] }
|
||||
bytes = "1.3"
|
||||
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_migrations = { version = "2.0.0", features = ["sqlite"] }
|
||||
|
|
|
@ -50,7 +50,7 @@ pub struct Config {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self> {
|
||||
pub fn build() -> Result<Self> {
|
||||
let config_file_var = "GWC_CONFIG_FILE";
|
||||
let config_path = env::var(config_file_var)
|
||||
.with_context(|| format!("Environment variable {config_file_var} missing!"))?;
|
||||
|
|
|
@ -4,24 +4,28 @@ use axum::{
|
|||
};
|
||||
use tracing::error;
|
||||
|
||||
pub struct AppError(anyhow::Error);
|
||||
pub struct AppError {
|
||||
inner: anyhow::Error,
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
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 {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self(err)
|
||||
Self { inner: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for AppError {
|
||||
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
86
src/extractors.rs
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ pub struct 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(
|
||||
mem::take(&mut config.email_server.email),
|
||||
mem::take(&mut config.email_server.password),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod config;
|
||||
mod db;
|
||||
mod errors;
|
||||
mod extractors;
|
||||
mod logging;
|
||||
mod mailer;
|
||||
mod models;
|
||||
|
@ -22,8 +23,8 @@ use std::{
|
|||
use tracing::info;
|
||||
|
||||
async fn init() -> Result<()> {
|
||||
let mut config = config::Config::new()?;
|
||||
let mailer = mailer::Mailer::new(&mut config)?;
|
||||
let mut config = config::Config::build()?;
|
||||
let mailer = mailer::Mailer::build(&mut config)?;
|
||||
|
||||
let address = config.socket_address.address;
|
||||
let socket_address = SocketAddr::new(
|
||||
|
@ -35,7 +36,7 @@ async fn init() -> Result<()> {
|
|||
|
||||
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)?)?;
|
||||
|
||||
|
|
|
@ -2,18 +2,14 @@ use anyhow::Context;
|
|||
use askama_axum::IntoResponse;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::header::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use sha2::Sha256;
|
||||
use std::{process::Command, sync::Arc, thread};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{db, errors, mailer, states, templates};
|
||||
use crate::{db, errors, extractors, mailer, states, templates};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct IndexQuery {
|
||||
|
@ -21,7 +17,7 @@ pub struct IndexQuery {
|
|||
}
|
||||
|
||||
pub async fn index(
|
||||
State(db_state): State<Arc<states::DB>>,
|
||||
State(db): State<Arc<states::DB>>,
|
||||
query: Query<IndexQuery>,
|
||||
) -> Result<Response, errors::AppError> {
|
||||
let id = match query.id {
|
||||
|
@ -30,7 +26,7 @@ pub async fn index(
|
|||
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);
|
||||
|
||||
|
@ -39,41 +35,16 @@ pub async fn index(
|
|||
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(
|
||||
State(db_state): State<Arc<states::DB>>,
|
||||
State(config_state): State<Arc<states::Config>>,
|
||||
State(db): State<Arc<states::DB>>,
|
||||
State(state_config): State<Arc<states::StateConfig>>,
|
||||
State(mailer): State<Arc<mailer::Mailer>>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
body: extractors::ValidatedBody,
|
||||
) -> Result<Response, errors::AppError> {
|
||||
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 =
|
||||
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
|
||||
.get("repository")
|
||||
|
@ -87,13 +58,13 @@ pub async fn trigger(
|
|||
.as_str()
|
||||
.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.")
|
||||
})?;
|
||||
|
||||
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.
|
||||
|
@ -103,7 +74,7 @@ pub async fn trigger(
|
|||
let command = hook.command.clone();
|
||||
let args = hook.args.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 hook_name = hook.name.clone();
|
||||
let hook_log_link = hook_log_link.clone();
|
||||
|
|
|
@ -9,20 +9,20 @@ pub struct DB {
|
|||
}
|
||||
|
||||
impl DB {
|
||||
pub fn new() -> Result<Self> {
|
||||
pub fn build() -> Result<Self> {
|
||||
Ok(Self {
|
||||
pool: db::establish_connection_pool()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub struct StateConfig {
|
||||
pub secret: Vec<u8>,
|
||||
pub base_url: String,
|
||||
pub hooks: Vec<config::Hook>,
|
||||
}
|
||||
|
||||
impl From<config::Config> for Config {
|
||||
impl From<config::Config> for StateConfig {
|
||||
fn from(config: config::Config) -> Self {
|
||||
Self {
|
||||
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> {
|
||||
self.hooks.iter().find(|&hook| hook.repo_url == clone_url)
|
||||
}
|
||||
|
@ -40,17 +40,27 @@ impl Config {
|
|||
|
||||
#[derive(Clone, FromRef)]
|
||||
pub struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub config: Arc<StateConfig>,
|
||||
pub mailer: Arc<mailer::Mailer>,
|
||||
pub db: Arc<DB>,
|
||||
}
|
||||
|
||||
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 {
|
||||
config: Arc::new(Config::from(config)),
|
||||
config: Arc::new(StateConfig::from(config)),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue