summaryrefslogtreecommitdiff
path: root/exes/webhook/src
diff options
context:
space:
mode:
authorMatthieuCoder <matthieu@matthieu-dev.xyz>2022-12-31 17:07:30 +0400
committerMatthieuCoder <matthieu@matthieu-dev.xyz>2022-12-31 17:07:30 +0400
commit65652932f77ce194a10cbc8dd42f3064e2c1a132 (patch)
tree4ca18a9317c4e561e917e9dd0cf39b695b43bc34 /exes/webhook/src
parenta16bafdf5b0ec52fa0d73458597eee7c34ea5e7b (diff)
updates and bazel removal
Diffstat (limited to 'exes/webhook/src')
-rw-r--r--exes/webhook/src/config.rs19
-rw-r--r--exes/webhook/src/handler/error.rs36
-rw-r--r--exes/webhook/src/handler/handler.rs176
-rw-r--r--exes/webhook/src/handler/make_service.rs34
-rw-r--r--exes/webhook/src/handler/mod.rs7
-rw-r--r--exes/webhook/src/handler/signature.rs41
-rw-r--r--exes/webhook/src/handler/tests/handler.rs0
-rw-r--r--exes/webhook/src/handler/tests/mod.rs2
-rw-r--r--exes/webhook/src/handler/tests/signature.rs33
-rw-r--r--exes/webhook/src/main.rs45
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);
+ }
+}