diff options
| author | MatthieuCoder <matthieu@matthieu-dev.xyz> | 2022-12-31 17:07:30 +0400 |
|---|---|---|
| committer | MatthieuCoder <matthieu@matthieu-dev.xyz> | 2022-12-31 17:07:30 +0400 |
| commit | 65652932f77ce194a10cbc8dd42f3064e2c1a132 (patch) | |
| tree | 4ca18a9317c4e561e917e9dd0cf39b695b43bc34 /exes/webhook/src | |
| parent | a16bafdf5b0ec52fa0d73458597eee7c34ea5e7b (diff) | |
updates and bazel removal
Diffstat (limited to 'exes/webhook/src')
| -rw-r--r-- | exes/webhook/src/config.rs | 19 | ||||
| -rw-r--r-- | exes/webhook/src/handler/error.rs | 36 | ||||
| -rw-r--r-- | exes/webhook/src/handler/handler.rs | 176 | ||||
| -rw-r--r-- | exes/webhook/src/handler/make_service.rs | 34 | ||||
| -rw-r--r-- | exes/webhook/src/handler/mod.rs | 7 | ||||
| -rw-r--r-- | exes/webhook/src/handler/signature.rs | 41 | ||||
| -rw-r--r-- | exes/webhook/src/handler/tests/handler.rs | 0 | ||||
| -rw-r--r-- | exes/webhook/src/handler/tests/mod.rs | 2 | ||||
| -rw-r--r-- | exes/webhook/src/handler/tests/signature.rs | 33 | ||||
| -rw-r--r-- | exes/webhook/src/main.rs | 45 |
10 files changed, 393 insertions, 0 deletions
diff --git a/exes/webhook/src/config.rs b/exes/webhook/src/config.rs new file mode 100644 index 0000000..a054d33 --- /dev/null +++ b/exes/webhook/src/config.rs @@ -0,0 +1,19 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ServerSettings { + pub port: u16, + pub address: String, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Discord { + pub public_key: String, + pub client_id: u32, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Config { + pub server: ServerSettings, + pub discord: Discord, +} diff --git a/exes/webhook/src/handler/error.rs b/exes/webhook/src/handler/error.rs new file mode 100644 index 0000000..d4fee07 --- /dev/null +++ b/exes/webhook/src/handler/error.rs @@ -0,0 +1,36 @@ +use hyper::{header::ToStrError, Body, Response, StatusCode}; + +pub struct WebhookError { + pub code: StatusCode, + pub message: String, +} + +impl WebhookError { + pub fn new(code: StatusCode, message: &str) -> WebhookError { + WebhookError { + code, + message: message.to_string(), + } + } +} + +impl Into<Response<Body>> for WebhookError { + fn into(self) -> Response<Body> { + Response::builder() + .status(self.code) + .body(self.message.into()) + .unwrap() + } +} + +impl From<hyper::Error> for WebhookError { + fn from(_: hyper::Error) -> Self { + WebhookError::new(StatusCode::BAD_REQUEST, "invalid request") + } +} + +impl From<ToStrError> for WebhookError { + fn from(_: ToStrError) -> Self { + WebhookError::new(StatusCode::BAD_REQUEST, "invalid request") + } +} diff --git a/exes/webhook/src/handler/handler.rs b/exes/webhook/src/handler/handler.rs new file mode 100644 index 0000000..3294c22 --- /dev/null +++ b/exes/webhook/src/handler/handler.rs @@ -0,0 +1,176 @@ +use super::error::WebhookError; +use super::signature::validate_signature; +use crate::config::Config; +use ed25519_dalek::PublicKey; +use hyper::{ + body::{to_bytes, Bytes}, + service::Service, + Body, Method, Request, Response, StatusCode, +}; +use serde::{Deserialize, Serialize}; +use shared::nats_crate::Connection; +use shared::{ + log::{debug, error}, + payloads::{CachePayload, DispatchEventTagged, Tracing}, +}; +use std::{ + future::Future, + pin::Pin, + str::from_utf8, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; +use twilight_model::gateway::event::{DispatchEvent}; +use twilight_model::{ + application::interaction::{Interaction, InteractionType}, + gateway::payload::incoming::InteractionCreate, +}; + +/// Hyper service used to handle the discord webhooks +#[derive(Clone)] +pub struct HandlerService { + pub config: Arc<Config>, + pub nats: Arc<Connection>, + pub public_key: Arc<PublicKey>, +} + +impl HandlerService { + async fn check_request(&self, req: Request<Body>) -> Result<Bytes, WebhookError> { + if req.method() == Method::POST { + let signature = if let Some(sig) = req.headers().get("X-Signature-Ed25519") { + sig.to_owned() + } else { + return Err(WebhookError::new( + StatusCode::BAD_REQUEST, + "missing signature header", + )); + }; + + let timestamp = if let Some(timestamp) = req.headers().get("X-Signature-Timestamp") { + timestamp.to_owned() + } else { + return Err(WebhookError::new( + StatusCode::BAD_REQUEST, + "missing timestamp header", + )); + }; + let data = to_bytes(req.into_body()).await?; + + if validate_signature( + &self.public_key, + &[timestamp.as_bytes().to_vec(), data.to_vec()].concat(), + signature.to_str()?, + ) { + Ok(data) + } else { + Err(WebhookError::new( + StatusCode::UNAUTHORIZED, + "invalid signature", + )) + } + } else { + Err(WebhookError::new(StatusCode::NOT_FOUND, "not found")) + } + } + + async fn process_request( + &mut self, + req: Request<Body>, + ) -> Result<Response<Body>, WebhookError> { + match self.check_request(req).await { + Ok(data) => { + let utf8 = from_utf8(&data); + match utf8 { + Ok(data) => match serde_json::from_str::<Interaction>(data) { + Ok(value) => { + match value.kind { + InteractionType::Ping => Ok(Response::builder() + .header("Content-Type", "application/json") + .body(serde_json::to_string(&Ping { t: 1 }).unwrap().into()) + .unwrap()), + _ => { + debug!("calling nats"); + // this should hopefully not fail ? + + let data = CachePayload { + tracing: Tracing { + node_id: "".to_string(), + span: None, + }, + data: DispatchEventTagged { + data: DispatchEvent::InteractionCreate(Box::new( + InteractionCreate(value), + )), + }, + }; + + let payload = serde_json::to_string(&data).unwrap(); + + match self.nats.request_timeout( + "nova.cache.dispatch.INTERACTION_CREATE", + payload, + Duration::from_secs(2), + ) { + Ok(response) => Ok(Response::builder() + .header("Content-Type", "application/json") + .body(Body::from(response.data)) + .unwrap()), + + Err(error) => { + error!("failed to request nats: {}", error); + Err(WebhookError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to request nats", + )) + } + } + } + } + } + + Err(error) => { + error!("invalid json body: {}", error); + Err(WebhookError::new( + StatusCode::BAD_REQUEST, + "invalid json body", + )) + } + }, + + Err(_) => Err(WebhookError::new(StatusCode::BAD_REQUEST, "not utf-8 body")), + } + } + Err(error) => Err(error), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Ping { + #[serde(rename = "type")] + t: i32, +} + +/// Implementation of the service +impl Service<Request<Body>> for HandlerService { + type Response = Response<Body>; + type Error = hyper::Error; + type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; + + fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request<Body>) -> Self::Future { + let mut clone = self.clone(); + Box::pin(async move { + let response = clone.process_request(req).await; + + match response { + Ok(r) => Ok(r), + Err(e) => Ok(e.into()), + } + }) + } +} diff --git a/exes/webhook/src/handler/make_service.rs b/exes/webhook/src/handler/make_service.rs new file mode 100644 index 0000000..2774917 --- /dev/null +++ b/exes/webhook/src/handler/make_service.rs @@ -0,0 +1,34 @@ +use super::handler::HandlerService; +use crate::config::Config; +use hyper::service::Service; +use shared::nats_crate::Connection; +use std::{ + future::{ready, Ready}, + sync::Arc, + task::{Context, Poll}, +}; +use ed25519_dalek::PublicKey; + +pub struct MakeSvc { + pub settings: Arc<Config>, + pub nats: Arc<Connection>, + pub public_key: Arc<PublicKey> +} + +impl<T> Service<T> for MakeSvc { + type Response = HandlerService; + type Error = std::io::Error; + type Future = Ready<Result<Self::Response, Self::Error>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + Ok(()).into() + } + + fn call(&mut self, _: T) -> Self::Future { + ready(Ok(HandlerService { + config: self.settings.clone(), + nats: self.nats.clone(), + public_key: self.public_key.clone() + })) + } +} diff --git a/exes/webhook/src/handler/mod.rs b/exes/webhook/src/handler/mod.rs new file mode 100644 index 0000000..20a977a --- /dev/null +++ b/exes/webhook/src/handler/mod.rs @@ -0,0 +1,7 @@ +mod error; +mod handler; +pub mod make_service; +mod signature; + +#[cfg(test)] +pub mod tests; diff --git a/exes/webhook/src/handler/signature.rs b/exes/webhook/src/handler/signature.rs new file mode 100644 index 0000000..3dc4373 --- /dev/null +++ b/exes/webhook/src/handler/signature.rs @@ -0,0 +1,41 @@ +use shared::prometheus::{Counter, HistogramVec, labels, opts, register_counter, register_histogram_vec}; +use ed25519_dalek::PublicKey; +use ed25519_dalek::Verifier; +use ed25519_dalek::Signature; +use std::convert::TryInto; + +lazy_static::lazy_static! { + static ref SIGNATURE_TIME_HISTOGRAM: HistogramVec = register_histogram_vec!( + "nova_webhook_signature_time", + "The time taken by the signature verification", + &["signature"] + ).unwrap(); + + static ref SIGNATURE_COUNTER: Counter = register_counter!(opts!( + "nova_webhook_signatures_verify", + "number of signatures verification issued by the service", + labels! {"handler" => "webhook_main"} + )).unwrap(); +} + +fn demo<T, const N: usize>(v: Vec<T>) -> [T; N] { + v.try_into() + .unwrap_or_else(|v: Vec<T>| panic!("Expected a Vec of length {} but it was {}", N, v.len())) +} + +pub fn validate_signature(public_key: &PublicKey, data: &Vec<u8>, hex_signature: &str) -> bool { + SIGNATURE_COUNTER.inc(); + let timer = SIGNATURE_TIME_HISTOGRAM.with_label_values(&["webhook_main"]).start_timer(); + + let signature_result = hex::decode(hex_signature); + + let mut result = false; + if let Ok(signature) = signature_result { + let sig = Signature::from(demo(signature)); + + result = public_key.verify(data, &sig).is_ok(); + } + + timer.observe_duration(); + result +} diff --git a/exes/webhook/src/handler/tests/handler.rs b/exes/webhook/src/handler/tests/handler.rs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/exes/webhook/src/handler/tests/handler.rs diff --git a/exes/webhook/src/handler/tests/mod.rs b/exes/webhook/src/handler/tests/mod.rs new file mode 100644 index 0000000..cf7f558 --- /dev/null +++ b/exes/webhook/src/handler/tests/mod.rs @@ -0,0 +1,2 @@ +pub mod signature; +pub mod handler; diff --git a/exes/webhook/src/handler/tests/signature.rs b/exes/webhook/src/handler/tests/signature.rs new file mode 100644 index 0000000..490143b --- /dev/null +++ b/exes/webhook/src/handler/tests/signature.rs @@ -0,0 +1,33 @@ +use crate::handler::signature::validate_signature; +use ed25519_dalek::PublicKey; + +#[test] +fn validate_signature_test() { + let signature = "543ec3547d57f9ddb1ec4c5c36503ebf288ffda3da3d510764c9a49c2abb57690ef974c63d174771bdd2481de1066966f57abbec12a3ec171b9f6e2373837002"; + let content = "message de test incroyable".as_bytes().to_vec(); + let public_key = PublicKey::from_bytes(&hex::decode("eefe0c24473737cb2035232e3b4eb91c206f0a14684168f3503f7d8316058d6f").unwrap()).unwrap(); + + assert!(validate_signature(&public_key, &content, signature)) +} + +#[test] +fn validate_signature_reverse_test() { + let signature = "543ec3547d57f9ddb1ec4c5c36503ebf288ffda3da3d510764c9a49c2abb57690ef974c63d174771bdd2481de1066966f57abbec12a3ec171b9f6e2373837002"; + let public_key = PublicKey::from_bytes(&hex::decode("c029eea18437292c87c62aec34e7d1bd4e38fe6126f3f7c446de6375dc666044").unwrap()).unwrap(); + + let content = "ceci est un test qui ne fonctionnera pas!" + .as_bytes() + .to_vec(); + assert!(!validate_signature(&public_key, &content, signature)) +} + +#[test] +fn invalid_hex() { + let signature = "zzz"; + let public_key = PublicKey::from_bytes(&hex::decode("c029eea18437292c87c62aec34e7d1bd4e38fe6126f3f7c446de6375dc666044").unwrap()).unwrap(); + + let content = "ceci est un test qui ne fonctionnera pas!" + .as_bytes() + .to_vec(); + assert!(!validate_signature(&public_key, &content, signature)) +}
\ No newline at end of file diff --git a/exes/webhook/src/main.rs b/exes/webhook/src/main.rs new file mode 100644 index 0000000..9527d0f --- /dev/null +++ b/exes/webhook/src/main.rs @@ -0,0 +1,45 @@ +use std::{net::ToSocketAddrs, sync::Arc}; +mod config; +mod handler; +use crate::handler::make_service::MakeSvc; + +use crate::config::Config; +use shared::config::Settings; +use shared::log::{error, info}; +use ed25519_dalek::PublicKey; +use hyper::Server; + +#[tokio::main] +async fn main() { + let settings: Settings<Config> = Settings::new("webhook").unwrap(); + start(settings).await; +} + +async fn start(settings: Settings<Config>) { + let addr = format!( + "{}:{}", + settings.config.server.address, settings.config.server.port + ) + .to_socket_addrs() + .unwrap() + .next() + .unwrap(); + + info!( + "Starting server on {}:{}", + settings.config.server.address, settings.config.server.port + ); + + let config = Arc::new(settings.config); + let public_key = + Arc::new(PublicKey::from_bytes(&hex::decode(&config.discord.public_key).unwrap()).unwrap()); + let server = Server::bind(&addr).serve(MakeSvc { + settings: config, + nats: Arc::new(settings.nats.into()), + public_key: public_key, + }); + + if let Err(e) = server.await { + error!("server error: {}", e); + } +} |
