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" }
|
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"] }
|
||||||
|
|
|
@ -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!"))?;
|
||||||
|
|
|
@ -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
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 {
|
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),
|
||||||
|
|
|
@ -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)?)?;
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue