From 23dfc4fa61ca7b308f3c13161bbe7fe9a9401ec7 Mon Sep 17 00:00:00 2001 From: Mo8it Date: Sat, 24 Dec 2022 23:26:12 +0100 Subject: [PATCH] Add extractors, use inner instead of 0, replace new with build --- Cargo.toml | 1 - src/config.rs | 2 +- src/errors.rs | 14 +++++--- src/extractors.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++ src/mailer.rs | 2 +- src/main.rs | 7 ++-- src/routes.rs | 51 ++++++---------------------- src/states.rs | 26 +++++++++----- 8 files changed, 130 insertions(+), 59 deletions(-) create mode 100644 src/extractors.rs diff --git a/Cargo.toml b/Cargo.toml index 1f1ff36..554e2f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/config.rs b/src/config.rs index dbe3b74..f0b0814 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,7 @@ pub struct Config { } impl Config { - pub fn new() -> Result { + pub fn build() -> Result { let config_file_var = "GWC_CONFIG_FILE"; let config_path = env::var(config_file_var) .with_context(|| format!("Environment variable {config_file_var} missing!"))?; diff --git a/src/errors.rs b/src/errors.rs index ee91a77..24b30f3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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 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()), + } } } diff --git a/src/extractors.rs b/src/extractors.rs new file mode 100644 index 0000000..d3d1fdf --- /dev/null +++ b/src/extractors.rs @@ -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, +} + +#[async_trait] +impl FromRequestParts for ReceivedSignature +where + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { + 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::::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 FromRequest for ValidatedBody +where + Bytes: FromRequest, + B: Send + 'static, + S: Send + Sync + states::ConfigState, +{ + type Rejection = AppError; + + async fn from_request(req: Request, state: &S) -> Result { + 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 }) + } +} diff --git a/src/mailer.rs b/src/mailer.rs index 2603792..719d9f1 100644 --- a/src/mailer.rs +++ b/src/mailer.rs @@ -15,7 +15,7 @@ pub struct Mailer { } impl Mailer { - pub fn new(config: &mut config::Config) -> Result { + pub fn build(config: &mut config::Config) -> Result { let creds = Credentials::new( mem::take(&mut config.email_server.email), mem::take(&mut config.email_server.password), diff --git a/src/main.rs b/src/main.rs index 908256b..fa313d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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)?)?; diff --git a/src/routes.rs b/src/routes.rs index b20f582..8561276 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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>, + State(db): State>, query: Query, ) -> Result { 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::::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>, - State(config_state): State>, + State(db): State>, + State(state_config): State>, State(mailer): State>, - headers: HeaderMap, - body: Bytes, + body: extractors::ValidatedBody, ) -> Result { 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(); diff --git a/src/states.rs b/src/states.rs index 1427e5a..e27e4db 100644 --- a/src/states.rs +++ b/src/states.rs @@ -9,20 +9,20 @@ pub struct DB { } impl DB { - pub fn new() -> Result { + pub fn build() -> Result { Ok(Self { pool: db::establish_connection_pool()?, }) } } -pub struct Config { +pub struct StateConfig { pub secret: Vec, pub base_url: String, pub hooks: Vec, } -impl From for Config { +impl From for StateConfig { fn from(config: config::Config) -> Self { Self { secret: config.secret.as_bytes().to_owned(), @@ -32,7 +32,7 @@ impl From 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, + pub config: Arc, pub mailer: Arc, pub db: Arc, } impl AppState { - pub fn new(config: config::Config, mailer: mailer::Mailer) -> Result { + pub fn build(config: config::Config, mailer: mailer::Mailer) -> Result { 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; +} + +impl ConfigState for AppState { + fn config(&self) -> Arc { + self.config.clone() + } +}