diff options
| author | matthieu <matthieu@developershouse.xyz> | 2021-09-03 21:59:04 +0400 |
|---|---|---|
| committer | matthieu <matthieu@developershouse.xyz> | 2021-09-03 21:59:04 +0400 |
| commit | d8a2d4ab8216a0ab4d7b0a0c40de474f0a409c5d (patch) | |
| tree | 835ce308a20c0c01d10ba158e956bffa0e52a228 /webhook/src | |
| parent | f2e6047c21b3ee814670b17e5901d12ac52a3508 (diff) | |
added the base of the webhook receiver
Diffstat (limited to 'webhook/src')
| -rw-r--r-- | webhook/src/handle.rs | 139 | ||||
| -rw-r--r-- | webhook/src/main.rs | 27 | ||||
| -rw-r--r-- | webhook/src/utils.rs | 59 |
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) + } +} |
