add gitea sig header check, add Json errors, begin AppState
This commit is contained in:
+63
-23
@@ -1,16 +1,28 @@
|
||||
use axum::response::{Response, IntoResponse};
|
||||
use axum::{Json, Router};
|
||||
use anyhow::anyhow;
|
||||
use axum::body::to_bytes;
|
||||
use axum::extract::FromRequest;
|
||||
use axum::routing::{post, get};
|
||||
use reqwest::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use serde_json::Value;
|
||||
use sha2::Sha256;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use crate::env::{EnvConfig};
|
||||
use crate::errors::AppError;
|
||||
use crate::gitea::WebhookType;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub async fn start_api(config: EnvConfig) -> anyhow::Result<()> {
|
||||
let app = Router::new().route("/", get(root)).route("/webhook", post(webhook));
|
||||
let listerner = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.http_port)).await?;
|
||||
const MAX_WEBHOOK_BODY_SIZE: usize = 1024 * 1024; // 1 Mo
|
||||
|
||||
pub async fn start(app_state: AppState) -> anyhow::Result<()> {
|
||||
let http_port = app_state.config.http_port;
|
||||
|
||||
let app = Router::new()
|
||||
.with_state(app_state)
|
||||
.route("/", get(root))
|
||||
.route("/webhook", post(webhook));
|
||||
let listerner = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", http_port)).await?;
|
||||
|
||||
axum::serve(listerner, app)
|
||||
.await
|
||||
@@ -21,11 +33,12 @@ async fn root() -> &'static str {
|
||||
"Hi, i'm Herald :)"
|
||||
}
|
||||
|
||||
async fn webhook(WebhookExtract(wb): WebhookExtract) -> Response {
|
||||
match wb {
|
||||
async fn webhook(WebhookExtract(wb): WebhookExtract) -> Result<Response, AppError> {
|
||||
Ok(match wb {
|
||||
WebhookType::Review(id, _) => format!("Received {} pr id", id),
|
||||
_ => String::from("Nothing to see :/")
|
||||
}.into_response()
|
||||
_ => String::from("Nothing to see :/"),
|
||||
}
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub struct WebhookExtract(pub WebhookType);
|
||||
@@ -34,21 +47,48 @@ impl<S> FromRequest<S> for WebhookExtract
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Response;
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request(
|
||||
req: axum::extract::Request,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let Json(value) = Json::<Value>::from_request(req, state)
|
||||
async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let headers = req.headers();
|
||||
let sig_header = headers
|
||||
.get("x-gitea-signature")
|
||||
.ok_or(AppError::WebHookSigHeaderNotFoundErr)?
|
||||
.to_str()
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.to_string();
|
||||
|
||||
let body = req.into_body();
|
||||
let body_bytes = to_bytes(body, MAX_WEBHOOK_BODY_SIZE)
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
|
||||
let webhook = WebhookType::try_from(value)
|
||||
.map_err(|e| {
|
||||
(StatusCode::UNPROCESSABLE_ENTITY, e.to_string()).into_response()
|
||||
})?;
|
||||
let Json(value) =
|
||||
Json::<Value>::from_bytes(&body_bytes).map_err(|_| AppError::MalformedJsonErr)?;
|
||||
let webhook = WebhookType::try_from(value)?;
|
||||
|
||||
check_sig_header(sig_header.as_bytes(), &body_bytes)?;
|
||||
|
||||
Ok(WebhookExtract(webhook))
|
||||
}
|
||||
}
|
||||
|
||||
fn check_sig_header(sig_header: &[u8], body: &[u8]) -> Result<(), AppError> {
|
||||
let sig_header_decoded = hex::decode(sig_header).map_err(|err| anyhow!(err))?;
|
||||
|
||||
let webhook_sig_header_secret =
|
||||
std::env::var("WEBHOOK_SIG_HEADER_SECRET").map_err(|err| anyhow!(err))?;
|
||||
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(&webhook_sig_header_secret.into_bytes())
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
|
||||
mac.update(body);
|
||||
|
||||
let generated_hmac = mac.finalize().into_bytes();
|
||||
let check_result: bool = generated_hmac.ct_eq(&sig_header_decoded).into();
|
||||
|
||||
match check_result {
|
||||
true => Ok(()),
|
||||
false => Err(AppError::WebHookSigHeaderInvalidErr),
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,5 +1,13 @@
|
||||
use crate::gitea::WebhookType;
|
||||
use crate::{env::EnvConfig, gitea::WebhookType};
|
||||
|
||||
pub async fn exec(webhook: WebhookType) {
|
||||
pub struct Bot {
|
||||
config: EnvConfig,
|
||||
}
|
||||
|
||||
}
|
||||
impl Bot {
|
||||
pub fn new(config: EnvConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn exec(&self, webhook: WebhookType) {}
|
||||
}
|
||||
|
||||
+20
-3
@@ -1,19 +1,36 @@
|
||||
use anyhow::anyhow;
|
||||
use dotenvy::dotenv;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EnvConfig {
|
||||
pub http_port: u16,
|
||||
pub webhook_secret: String,
|
||||
pub open_router_api_key: String,
|
||||
pub bot_name: String,
|
||||
}
|
||||
|
||||
pub fn load_config() -> anyhow::Result<EnvConfig> {
|
||||
dotenv().ok();
|
||||
|
||||
let http_port = std::env::var("HTTP_PORT")?.parse()?;
|
||||
let bot_name = std::env::var("BOT_NAME")?;
|
||||
let http_port = try_get_env("HTTP_PORT")?.parse()?;
|
||||
let bot_name = try_get_env("BOT_NAME")?;
|
||||
let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?;
|
||||
let open_router_api_key = try_get_env("OPEN_ROUTER_API_KEY")?;
|
||||
|
||||
Ok(EnvConfig {
|
||||
http_port,
|
||||
webhook_secret,
|
||||
bot_name,
|
||||
open_router_api_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_get_env(key: &str) -> anyhow::Result<String> {
|
||||
let env = std::env::var(key)?;
|
||||
|
||||
if env.trim().len() == 0 {
|
||||
return Err(anyhow!(format!("env var {} is empty", env)));
|
||||
}
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use axum::response::IntoResponse;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Malformed Json")]
|
||||
MalformedJsonErr,
|
||||
|
||||
#[error("Json not contains mandatory fields")]
|
||||
BadJsonStructErr,
|
||||
|
||||
#[error("WebHook sig header not found")]
|
||||
WebHookSigHeaderNotFoundErr,
|
||||
|
||||
#[error("WebHook sig header is invalid")]
|
||||
WebHookSigHeaderInvalidErr,
|
||||
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
AppError::MalformedJsonErr => (StatusCode::BAD_REQUEST, "Malformed Json"),
|
||||
AppError::BadJsonStructErr => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Json not contains mandatory fields",
|
||||
),
|
||||
AppError::WebHookSigHeaderNotFoundErr => {
|
||||
(StatusCode::BAD_REQUEST, "WebHook sig header not found")
|
||||
}
|
||||
AppError::WebHookSigHeaderInvalidErr => {
|
||||
(StatusCode::BAD_REQUEST, "WebHook sig header is invalid")
|
||||
}
|
||||
AppError::Other(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -1,17 +1,27 @@
|
||||
use anyhow::anyhow;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::errors::AppError;
|
||||
|
||||
pub enum WebhookType {
|
||||
Review(u64, String)
|
||||
Review(u64, String),
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for WebhookType {
|
||||
type Error = anyhow::Error;
|
||||
type Error = AppError;
|
||||
|
||||
fn try_from(json: Value) -> Result<Self, Self::Error> {
|
||||
let pull_request = json.get("pull_request");
|
||||
let comment = json.get("comment");
|
||||
let action = json
|
||||
.get("action")
|
||||
.ok_or(anyhow!("action not found"))?
|
||||
.as_str()
|
||||
.ok_or(anyhow!("error while action"))?;
|
||||
|
||||
if action != "created" {
|
||||
return Err(AppError::BadJsonStructErr);
|
||||
}
|
||||
|
||||
if let (Some(pull_request), Some(comment)) = (pull_request, comment) {
|
||||
let comment_body = comment
|
||||
@@ -30,6 +40,6 @@ impl TryFrom<Value> for WebhookType {
|
||||
return Ok(WebhookType::Review(pr_id, comment_body));
|
||||
}
|
||||
|
||||
anyhow::bail!("unknow webhook type")
|
||||
Err(AppError::BadJsonStructErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -1,11 +1,24 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{bot::Bot, state::AppState};
|
||||
|
||||
mod api;
|
||||
mod env;
|
||||
mod gitea;
|
||||
mod bot;
|
||||
mod env;
|
||||
mod errors;
|
||||
mod gitea;
|
||||
mod state;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let config = env::load_config()?;
|
||||
|
||||
api::start_api(config).await
|
||||
let app_state = AppState {
|
||||
bot: Arc::new(Mutex::new(Bot::new(config.clone()))),
|
||||
config: config,
|
||||
};
|
||||
|
||||
api::start(app_state).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{bot::Bot, env::EnvConfig};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub bot: Arc<Mutex<Bot>>,
|
||||
pub config: EnvConfig,
|
||||
}
|
||||
Reference in New Issue
Block a user