-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Sergio Castaño Arteaga <[email protected]>
- Loading branch information
Showing
15 changed files
with
794 additions
and
338 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[package] | ||
name = "dco2-aws-lambda" | ||
description = "DCO2 AWS Lambda" | ||
version.workspace = true | ||
license.workspace = true | ||
edition.workspace = true | ||
rust-version.workspace = true | ||
|
||
[dependencies] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
fn main() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
[package] | ||
name = "dco2-server" | ||
description = "DCO2 HTTP server" | ||
version.workspace = true | ||
license.workspace = true | ||
edition.workspace = true | ||
rust-version.workspace = true | ||
|
||
[dependencies] | ||
anyhow = { workspace = true } | ||
axum = { workspace = true } | ||
clap = { workspace = true } | ||
dco2 = { path = "../dco2" } | ||
figment = { workspace = true } | ||
hex = { workspace = true } | ||
hmac = { workspace = true } | ||
serde = { workspace = true } | ||
sha2 = { workspace = true } | ||
tokio = { workspace = true } | ||
tower = { workspace = true } | ||
tower-http = { workspace = true } | ||
tracing = { workspace = true } | ||
tracing-subscriber = { workspace = true } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
//! This module defines some types to represent the server configuration. | ||
use anyhow::Result; | ||
use dco2::github::AppConfig; | ||
use figment::{ | ||
providers::{Env, Format, Serialized, Yaml}, | ||
Figment, | ||
}; | ||
use serde::{Deserialize, Serialize}; | ||
use std::path::PathBuf; | ||
|
||
/// Server configuration. | ||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] | ||
pub(crate) struct Config { | ||
pub github_app: AppConfig, | ||
pub log_format: LogFormat, | ||
pub server_addr: String, | ||
} | ||
|
||
impl Config { | ||
/// Create a new Config instance. | ||
pub(crate) fn new(config_file: &Option<PathBuf>) -> Result<Self> { | ||
let mut figment = Figment::new() | ||
.merge(Serialized::default("log_format", "json")) | ||
.merge(Serialized::default("server_addr", "localhost:9000")); | ||
if let Some(config_file) = config_file { | ||
figment = figment.merge(Yaml::file(config_file)); | ||
} | ||
figment.merge(Env::prefixed("DCO2_").split("__")).extract().map_err(Into::into) | ||
} | ||
} | ||
|
||
/// Format to use in logs. | ||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] | ||
#[serde(rename_all = "snake_case")] | ||
pub(crate) enum LogFormat { | ||
Json, | ||
Pretty, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
//! This module defines the router and handlers used to process HTTP requests. | ||
use anyhow::{format_err, Error, Result}; | ||
use axum::{ | ||
body::Bytes, | ||
extract::{FromRef, State}, | ||
http::{HeaderMap, StatusCode}, | ||
response::IntoResponse, | ||
routing::{get, post}, | ||
Router, | ||
}; | ||
use dco2::{ | ||
dco, | ||
github::{DynGHClient, Event, EventError, EVENT_ID_HEADER, SIGNATURE_HEADER}, | ||
}; | ||
use hmac::{Hmac, Mac}; | ||
use sha2::Sha256; | ||
use tower::ServiceBuilder; | ||
use tower_http::trace::TraceLayer; | ||
use tracing::{debug, error, instrument}; | ||
|
||
/// Router's state. | ||
#[derive(Clone, FromRef)] | ||
struct RouterState { | ||
gh_client: DynGHClient, | ||
webhook_secret: WebhookSecret, | ||
} | ||
|
||
/// Type alias to represent a webhook secret. | ||
pub type WebhookSecret = String; | ||
|
||
/// Setup HTTP server router. | ||
pub fn setup_router(gh_client: DynGHClient, webhook_secret: &str) -> Router { | ||
// Setup router's state | ||
let state = RouterState { | ||
gh_client, | ||
webhook_secret: webhook_secret.to_string(), | ||
}; | ||
|
||
// Setup router | ||
Router::new() | ||
.route("/health-check", get(health_check)) | ||
.route("/webhook/github", post(event)) | ||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) | ||
.with_state(state) | ||
} | ||
|
||
/// Handler that takes care of health check requests. | ||
async fn health_check() -> impl IntoResponse { | ||
StatusCode::OK | ||
} | ||
|
||
/// Handler that processes webhook events from GitHub. | ||
#[instrument(fields(event_id), skip_all, err(Debug))] | ||
async fn event( | ||
State(gh_client): State<DynGHClient>, | ||
State(webhook_secret): State<WebhookSecret>, | ||
headers: HeaderMap, | ||
body: Bytes, | ||
) -> impl IntoResponse { | ||
// Record event_id as part of the current span | ||
if let Some(event_id) = headers.get(EVENT_ID_HEADER) { | ||
tracing::Span::current().record("event_id", event_id.to_str().unwrap_or_default()); | ||
} | ||
|
||
// Verify request signature | ||
if verify_signature(webhook_secret.as_bytes(), &headers, &body).is_err() { | ||
return Err((StatusCode::BAD_REQUEST, "no valid signature found".to_string())); | ||
} | ||
|
||
// Parse event from request payload | ||
let event = match Event::try_from((&headers, &body)) { | ||
Ok(event) => event, | ||
Err(err @ (EventError::MissingHeader | EventError::InvalidPayload)) => { | ||
return Err((StatusCode::BAD_REQUEST, err.to_string())) | ||
} | ||
Err(EventError::UnsupportedEvent) => return Ok(()), | ||
}; | ||
|
||
// Process event and run DCO check | ||
if let Err(err) = dco::process_event(gh_client, &event).await { | ||
error!(?err, "error processing event"); | ||
return Err((StatusCode::INTERNAL_SERVER_ERROR, String::new())); | ||
} | ||
debug!("event processed successfully"); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Verify that the signature provided in the webhook request is valid. | ||
pub fn verify_signature(secret: &[u8], headers: &HeaderMap, body: &[u8]) -> Result<()> { | ||
if let Some(signature) = headers | ||
.get(SIGNATURE_HEADER) | ||
.and_then(|s| s.to_str().ok()) | ||
.and_then(|s| s.strip_prefix("sha256=")) | ||
.and_then(|s| hex::decode(s).ok()) | ||
{ | ||
let mut mac = Hmac::<Sha256>::new_from_slice(secret)?; | ||
mac.update(body.as_ref()); | ||
mac.verify_slice(&signature[..]).map_err(Error::new) | ||
} else { | ||
Err(format_err!("no valid signature found")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#![warn(clippy::all, clippy::pedantic)] | ||
#![allow(clippy::doc_markdown, clippy::similar_names)] | ||
|
||
use anyhow::{Context, Result}; | ||
use clap::Parser; | ||
use config::{Config, LogFormat}; | ||
use dco2::github::GHClientOctorust; | ||
use handlers::setup_router; | ||
use std::{path::PathBuf, sync::Arc}; | ||
use tokio::{net::TcpListener, signal}; | ||
use tracing::{error, info}; | ||
use tracing_subscriber::EnvFilter; | ||
|
||
mod config; | ||
mod handlers; | ||
|
||
#[derive(Debug, Parser)] | ||
#[clap(author, version, about)] | ||
struct Args { | ||
/// Config file path | ||
#[clap(short, long)] | ||
config_file: Option<PathBuf>, | ||
} | ||
|
||
#[tokio::main] | ||
async fn main() -> Result<()> { | ||
// Setup configuration | ||
let args = Args::parse(); | ||
let cfg = Config::new(&args.config_file).context("error setting up configuration")?; | ||
|
||
// Setup logging | ||
if std::env::var_os("RUST_LOG").is_none() { | ||
std::env::set_var("RUST_LOG", "dco2_server=debug,dco2=debug"); | ||
} | ||
let ts = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()); | ||
match cfg.log_format { | ||
LogFormat::Json => ts.json().init(), | ||
LogFormat::Pretty => ts.init(), | ||
}; | ||
|
||
// Setup GitHub client | ||
let gh_client = Arc::new(GHClientOctorust::new(&cfg.github_app)?); | ||
|
||
// Setup and launch HTTP server | ||
let router = setup_router(gh_client, &cfg.github_app.webhook_secret); | ||
let listener = TcpListener::bind(&cfg.server_addr).await?; | ||
info!("server started"); | ||
info!(%cfg.server_addr, "listening"); | ||
if let Err(err) = axum::serve(listener, router).with_graceful_shutdown(shutdown_signal()).await { | ||
error!(?err, "server error"); | ||
return Err(err.into()); | ||
} | ||
info!("server stopped"); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Return a future that will complete when the program is asked to stop via a | ||
/// ctrl+c or terminate signal. | ||
async fn shutdown_signal() { | ||
// Setup signal handlers | ||
let ctrl_c = async { | ||
signal::ctrl_c().await.expect("failed to install ctrl+c signal handler"); | ||
}; | ||
|
||
#[cfg(unix)] | ||
let terminate = async { | ||
signal::unix::signal(signal::unix::SignalKind::terminate()) | ||
.expect("failed to install terminate signal handler") | ||
.recv() | ||
.await; | ||
}; | ||
|
||
#[cfg(not(unix))] | ||
let terminate = std::future::pending::<()>(); | ||
|
||
// Wait for any of the signals | ||
tokio::select! { | ||
() = ctrl_c => {}, | ||
() = terminate => {}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[package] | ||
name = "dco2" | ||
description = "DCO2 core library" | ||
version.workspace = true | ||
license.workspace = true | ||
edition.workspace = true | ||
rust-version.workspace = true | ||
|
||
[dependencies] | ||
anyhow = { workspace = true } | ||
askama = { workspace = true } | ||
async-trait = { workspace = true } | ||
bytes = { workspace = true } | ||
chrono = { workspace = true } | ||
http = { workspace = true } | ||
octorust = { workspace = true } | ||
pem = { workspace = true } | ||
serde = { workspace = true } | ||
serde_json = { workspace = true } | ||
thiserror = { workspace = true } | ||
tracing = { workspace = true } |
Oops, something went wrong.