summaryrefslogtreecommitdiff
path: root/webhook/src
diff options
context:
space:
mode:
authormatthieu <matthieu@developershouse.xyz>2021-09-03 21:59:04 +0400
committermatthieu <matthieu@developershouse.xyz>2021-09-03 21:59:04 +0400
commitd8a2d4ab8216a0ab4d7b0a0c40de474f0a409c5d (patch)
tree835ce308a20c0c01d10ba158e956bffa0e52a228 /webhook/src
parentf2e6047c21b3ee814670b17e5901d12ac52a3508 (diff)
added the base of the webhook receiver
Diffstat (limited to 'webhook/src')
-rw-r--r--webhook/src/handle.rs139
-rw-r--r--webhook/src/main.rs27
-rw-r--r--webhook/src/utils.rs59
3 files changed, 225 insertions, 0 deletions
diff --git a/webhook/src/handle.rs b/webhook/src/handle.rs
new file mode 100644
index 0000000..3a234f3
--- /dev/null
+++ b/webhook/src/handle.rs
@@ -0,0 +1,139 @@
+use std::future;
+use hyper::{HeaderMap, body::{Bytes, to_bytes}};
+use libsodium_sys::crypto_sign_ed25519_verify_detached;
+use hyper::{Body, Method, Request, Response, StatusCode};
+use hyper::service::Service;
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+use crate::utils::Settings;
+use log::info;
+
+static NOT_FOUND: &str = "
+<h1>Nova Webhook Gateway</h1>
+<p>Invalid request</p>
+";
+
+fn validate_signature (b64_public_key: &str, data: &Bytes, b64_signature: &str) -> bool {
+ // First, we need to check if the signature & private key is valid base64.
+ let signature_result = base64::decode(b64_signature);
+ let public_key_result = base64::decode(b64_public_key);
+
+ if signature_result.is_ok() && public_key_result.is_ok() {
+ // Since we now have the signatures in u8 vectors. We will initialize all the
+ // parameters for the ffi call to sodium.
+ let signature_pointer = signature_result.unwrap();
+ let private_key_pointer = public_key_result.unwrap();
+
+ let data_pointer = data.as_ptr();
+ let data_len = data.len() as u64;
+
+ // A ffi call is considered unsafe by the Rust compiler
+ // we assume all the parameters are correct for the call
+ unsafe {
+ // If the signature is valid, sodium will return 0
+ return crypto_sign_ed25519_verify_detached(
+ signature_pointer.as_ptr(),
+ data_pointer, data_len,
+ private_key_pointer.as_ptr()
+ ) == 0
+ }
+ }
+ false
+}
+
+#[cfg(test)]
+mod tests {
+ use hyper::body::Bytes;
+
+ use super::validate_signature;
+
+ #[test]
+ fn validate_signature_test() {
+ let signature = "VD7DVH1X+d2x7ExcNlA+vyiP/aPaPVEHZMmknCq7V2kO+XTGPRdHcb3SSB3hBmlm9Xq77BKj7Bcbn24jc4NwAg==";
+ let public_key = "7v4MJEc3N8sgNSMuO065HCBvChRoQWjzUD99gxYFjW8=";
+ let content = "message de test incroyable";
+ assert_eq!(validate_signature(public_key, &Bytes::from(content), signature), true);
+ }
+
+ #[test]
+ fn validate_signature_reverse_test() {
+ let signature = "zHtgMcx6iANuKbPFvG3c+vj6W84Mqb/2FWx4UnPXLJkwbEuh2u4EV/m1PEh4wv1zdjmatgTFPI4OephgE+nxAA==";
+ let public_key = "wCnuoYQ3KSyHxirsNOfRvU44/mEm8/fERt5jddxmYEQ=";
+ let content = "ceci est un test qui ne fonctionnera pas!";
+ assert_eq!(validate_signature(public_key, &Bytes::from(content), signature), false);
+ }
+}
+
+fn get_signature<'b>(headers: &'b HeaderMap) -> Option<(&'b str, &'b str)> {
+ let signature = headers.get("X-Signature-Ed25519");
+ let timestamp = headers.get("X-Signature-Timestamp");
+
+ if signature.is_some() && timestamp.is_some() {
+ return Some((
+ signature.unwrap().to_str().unwrap(),
+ timestamp.unwrap().to_str().unwrap()
+ ));
+ }
+ None
+}
+
+pub struct HandlerService {
+ pub config: Settings,
+}
+
+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 {
+ if req.method() == Method::POST {
+ let public_key = self.config.discord.public_key.clone();
+ return Box::pin (async move {
+ let headers = req.headers().clone();
+ let sig_headers = get_signature(&headers);
+ if sig_headers.is_some() {
+ let (signature, _timestamp) = sig_headers.unwrap();
+ let data = to_bytes(req.into_body()).await.unwrap();
+ info!("data: {}", public_key);
+ if validate_signature(public_key.as_str(), &data, signature) {
+ return Ok(Response::new("signature verified!".into()))
+ }
+ return Ok(Response::new("signature verification failed.".into()))
+ }
+ return Ok(Response::new("no signature specified.".into()))
+ })
+ } else {
+ return Box::pin(async {
+ let mut response = Response::new(NOT_FOUND.into());
+ let status = response.status_mut();
+ *status = StatusCode::BAD_REQUEST;
+ Ok(response)
+ })
+ }
+ }
+}
+
+
+pub struct MakeSvc {
+ pub settings: Settings
+}
+
+impl<T> Service<T> for MakeSvc {
+ type Response = HandlerService;
+ type Error = std::io::Error;
+ type Future = 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 {
+ future::ready(Ok(HandlerService { config: self.settings.clone() }))
+ }
+} \ No newline at end of file
diff --git a/webhook/src/main.rs b/webhook/src/main.rs
new file mode 100644
index 0000000..dd340e8
--- /dev/null
+++ b/webhook/src/main.rs
@@ -0,0 +1,27 @@
+use std::net::ToSocketAddrs;
+use hyper::Server;
+use log::info;
+
+use crate::handle::MakeSvc;
+extern crate log;
+
+mod handle;
+mod utils;
+
+
+#[tokio::main]
+async fn main() {
+ utils::setup_program("webhook");
+ let config = utils::Settings::new().unwrap();
+
+ let addr = format!("{}:{}", config.server.address, config.server.port)
+ .to_socket_addrs()
+ .unwrap().next().unwrap();
+
+ info!("Starting server on {}:{}", config.server.address, config.server.port);
+ let server = Server::bind(&addr).serve(MakeSvc { settings: config.clone() });
+
+ if let Err(e) = server.await {
+ eprintln!("server error: {}", e);
+ }
+}
diff --git a/webhook/src/utils.rs b/webhook/src/utils.rs
new file mode 100644
index 0000000..b41156b
--- /dev/null
+++ b/webhook/src/utils.rs
@@ -0,0 +1,59 @@
+use std::env;
+
+use config::{Config, ConfigError, Environment, File};
+use log::{info, trace};
+use serde::Deserialize;
+
+/// Executes the required configuration steps for the program,
+/// uncluding build information, Sentry and logging.
+pub fn setup_program (name: &str) {
+ // todo: this may be replaced by a more complete logger
+
+ let build_info_get = build_info();
+
+ trace!("Starting {} version {} v{} built with {} at {}",
+ name,
+ build_info_get.crate_info.name,
+ build_info_get.crate_info.version,
+ build_info_get.compiler,
+ build_info_get.timestamp
+ );
+}
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct Server {
+ pub port: u16,
+ pub address: String,
+}
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct Discord {
+ pub public_key: String,
+ pub client_id: u32,
+}
+
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct Settings {
+ pub server: Server,
+ pub discord: Discord,
+}
+
+impl Settings {
+ pub fn new() -> Result<Self, ConfigError> {
+ let mut default = Config::default();
+ default.merge(File::with_name("config/default"))?;
+ let mode = env::var("ENV").unwrap_or_else(|_| "development".into());
+ info!("Configuration Environment: {}", mode);
+
+ default.merge(File::with_name(&format!("config/{}", mode)).required(false))?;
+ default.merge(File::with_name("config/local").required(false))?;
+ default.merge(Environment::with_prefix("NOVA_GATEWAY"))?;
+
+ println!("Debug mode: {:?}", default.get_bool("debug"));
+
+ let config: Self = default.try_into().unwrap();
+
+ Ok(config)
+ }
+}