hex = "0.4.3"
serde_json = { version = "1.0" }
libc = "0.2.101"
+lazy_static = "1.4.0"
[[bin]]
name = "webhook"
--- /dev/null
+use hyper::{Body, Error, 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()
+ }
+}
+use super::error::WebhookError;
use super::{signature::validate_signature, types::Interaction};
use crate::config::Config;
+use common::log::{debug, error, info};
+use common::nats_crate::Connection;
use hyper::{
body::{to_bytes, Bytes},
service::Service,
Body, Method, Request, Response, StatusCode,
};
-use common::log::{error, info, trace};
-use common::nats_crate::Connection;
use serde::{Deserialize, Serialize};
-use std::{future::Future, io::{Error, ErrorKind}, pin::Pin, str::from_utf8, sync::Arc, task::{Context, Poll}, time::Duration};
+use std::{
+ future::Future,
+ pin::Pin,
+ str::from_utf8,
+ sync::Arc,
+ task::{Context, Poll},
+ time::Duration,
+};
/// Hyper service used to handle the discord webhooks
#[derive(Clone)]
}
impl HandlerService {
- async fn check_request(&self, req: Request<Body>) -> Result<Bytes, Error> {
+ async fn check_request(&self, req: Request<Body>) -> Result<Bytes, WebhookError> {
if req.method() == Method::POST {
let headers = req.headers().clone();
let signature = headers.get("X-Signature-Ed25519");
) {
Ok(data)
} else {
- Err(Error::new(
- ErrorKind::InvalidData,
- "invalid signature specified",
- ))
+ Err(WebhookError::new(StatusCode::UNAUTHORIZED, "invalid signature"))
}
} else {
- Err(Error::new(
- ErrorKind::BrokenPipe,
- "failed to read signature",
- ))
+ Err(WebhookError::new(StatusCode::BAD_REQUEST, "failed to read signature"))
}
} else {
- Err(Error::new(ErrorKind::BrokenPipe, "unable to read body"))
+ Err(WebhookError::new(StatusCode::BAD_REQUEST, "unable to read body"))
}
} else {
- Err(Error::new(ErrorKind::InvalidData, "missing headers"))
+ Err(WebhookError::new(StatusCode::UNAUTHORIZED, "missing signature headers"))
}
} else {
- Err(Error::new(ErrorKind::InvalidData, "invalid method"))
+ 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.t {
+ 1 => {
+ info!("sending pong");
+ // a ping must be responded with another ping
+ return 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 payload =
+ serde_json::to_string(&common::payloads::CachePayload {
+ tracing: common::payloads::Tracing {
+ node_id: "".to_string(),
+ span: None,
+ },
+ operation: "".to_string(),
+ data: value,
+ })
+ .unwrap();
+
+ match self.nats.request_timeout(
+ "nova.cache.dispatch.interaction",
+ 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(_) => Err(WebhookError::new(StatusCode::BAD_REQUEST, "invalid json body")),
+ },
+
+ Err(_) => Err(WebhookError::new(StatusCode::BAD_REQUEST, "not utf-8 body")),
+ }
+ }
+ Err(error) => Err(error),
}
}
}
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
- let self_clone = self.clone();
-
+ let mut clone = self.clone();
Box::pin(async move {
- match self_clone.check_request(req).await {
- Ok(data) => {
- let value: Interaction =
- serde_json::from_str(from_utf8(&data).unwrap()).unwrap();
- trace!("received value: {:?}", value);
-
- match value.t {
- 1 => {
- info!("sending pong");
- // a ping must be responded with another ping
- return Ok(Response::builder()
- .header("Content-Type", "application/json")
- .body(serde_json::to_string(&Ping { t: 1 }).unwrap().into())
- .unwrap());
- }
- _ => {
- let payload = serde_json::to_string(&common::payloads::CachePayload {
- tracing: common::payloads::Tracing {
- node_id: "".to_string(),
- span: None,
- },
- operation: "".to_string(),
- data: value,
- })
- .unwrap();
+ let response = clone.process_request(req).await;
- match self_clone
- .nats
- .request_timeout("nova.cache.dispatch.interaction", payload, Duration::from_secs(2))
- {
- Ok(response) => Ok(Response::builder()
- .header("Content-Type", "application/json")
- .body(from_utf8(&response.data).unwrap().to_string().into())
- .unwrap()),
- Err(error) => {
- error!("failed to request nats: {}", error);
- Ok(Response::builder()
- .status(500)
- .body("an internal server error occured".to_string().into())
- .unwrap())
- }
- }
- }
- }
- }
- Err(error) => Ok(Response::builder()
- .status(StatusCode::UNAUTHORIZED)
- .body(error.to_string().into())
- .unwrap()),
+ match response {
+ Ok(r) => Ok(r),
+ Err(e) => Ok(e.into())
}
})
}
mod signature;
mod handler;
mod types;
+mod error;
+
+#[cfg(test)]
pub mod tests;
\ No newline at end of file
}
false
}
-
-#[cfg(test)]
-mod test {
- use crate::handler::signature::validate_signature;
-
-
- #[test]
- fn validate_signature_test() {
- let signature = "543ec3547d57f9ddb1ec4c5c36503ebf288ffda3da3d510764c9a49c2abb57690ef974c63d174771bdd2481de1066966f57abbec12a3ec171b9f6e2373837002";
- let public_key = "eefe0c24473737cb2035232e3b4eb91c206f0a14684168f3503f7d8316058d6f";
- let content = "message de test incroyable".as_bytes().to_vec();
- assert!(validate_signature(public_key, &content, signature))
- }
-
- #[test]
- fn validate_signature_reverse_test() {
- let signature = "543ec3547d57f9ddb1ec4c5c36503ebf288ffda3da3d510764c9a49c2abb57690ef974c63d174771bdd2481de1066966f57abbec12a3ec171b9f6e2373837002";
- let public_key = "c029eea18437292c87c62aec34e7d1bd4e38fe6126f3f7c446de6375dc666044";
- 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 = "zzz";
- let content = "ceci est un test qui ne fonctionnera pas!"
- .as_bytes()
- .to_vec();
- assert!(!validate_signature(public_key, &content, signature))
- }
-}
-fn generate_keypair() -> (
- String,
- [u8; libsodium_sys::crypto_sign_ed25519_SECRETKEYBYTES as usize],
-) {
- use libsodium_sys::crypto_sign_ed25519_keypair;
- let pk_s: String;
-
- let mut pk = [0; libsodium_sys::crypto_sign_ed25519_PUBLICKEYBYTES as usize];
- let mut sk = [0; libsodium_sys::crypto_sign_ed25519_SECRETKEYBYTES as usize];
-
- let pk_p = pk.as_mut_ptr();
- let sk_p = sk.as_mut_ptr();
-
- // generate keypair
- unsafe {
- if crypto_sign_ed25519_keypair(pk_p, sk_p) < 0 {
- panic!("keypair generation failed!");
- }
- };
-
- pk_s = hex::encode(pk);
- return (pk_s, sk);
-}
-
-fn sign_message(
- msg: Vec<u8>,
- sk: [u8; libsodium_sys::crypto_sign_ed25519_SECRETKEYBYTES as usize],
-) -> String {
- use libc::c_ulonglong;
- use libsodium_sys::crypto_sign_ed25519_detached;
-
- let len = msg.len();
- let mut signature_len: c_ulonglong = 0;
- let mut str = [0; 64];
- unsafe {
- crypto_sign_ed25519_detached(
- str.as_mut_ptr(),
- &mut signature_len,
- msg.as_ptr(),
- len as u64,
- sk.as_ptr(),
- );
- };
-
- return hex::encode(str);
-}
-
-#[tokio::test]
-async fn respond_to_pings_and_deny_invalid() {
- use crate::start;
- use common::config::test_init;
- use common::config::Settings;
- use common::log::info;
- use common::testcontainers::images::generic::GenericImage;
- use common::testcontainers::Docker;
- use hyper::{Body, Method, Request};
- use libsodium_sys::sodium_init;
- use serde_json::json;
- use std::time::Duration;
-
- test_init();
-
- unsafe {
- if sodium_init() < 0 {
- panic!("libsodium init error!");
- }
- }
-
- let (private_key, secret_key) = generate_keypair();
- let ping = json!({ "type": 1 }).to_string();
- let timestamp = "my datetime :)";
- let signature_data = [timestamp.as_bytes().to_vec(), ping.as_bytes().to_vec()].concat();
- let signature = sign_message(signature_data, secret_key);
-
- // start nats
- let docker = common::testcontainers::clients::Cli::default();
- let image = GenericImage::new("nats");
- let node = docker.run(image);
- node.start();
- let port = node.get_host_port(4222).unwrap();
-
- let settings: Settings<crate::config::Config> = common::config::Settings {
- config: crate::config::Config {
- server: crate::config::ServerSettings {
- port: 5003,
- address: "0.0.0.0".to_string(),
- },
- discord: crate::config::Discord {
- public_key: private_key,
- client_id: 0,
- },
- },
- monitoring: common::monitoring::MonitoringConfiguration {
- enabled: false,
- address: None,
- port: None,
- },
- nats: common::nats::NatsConfiguration {
- client_cert: None,
- root_cert: None,
- jetstream_api_prefix: None,
- max_reconnects: None,
- reconnect_buffer_size: None,
- tls: None,
- client_name: None,
- tls_required: None,
- host: format!("localhost:{}", port),
- },
- };
-
- let nats: common::nats_crate::Connection = settings.nats.clone().into();
- // start the server
- tokio::task::spawn(start(settings));
- tokio::time::sleep(Duration::from_secs(1)).await;
-
- let req = Request::builder()
- .method(Method::POST)
- .uri("http://localhost:5003/")
- .header("X-Signature-Ed25519", signature)
- .header("X-Signature-Timestamp", timestamp)
- .body(Body::from(ping.clone()))
- .expect("request builder");
- let client = hyper::client::Client::new();
- let result = client.request(req).await.unwrap();
- assert!(result.status() == 200);
-
- let req = Request::builder()
- .method(Method::POST)
- .uri("http://localhost:5003/")
- .header("X-Signature-Ed25519", "inva&lid signature :)")
- .header("X-Signature-Timestamp", timestamp)
- .body(Body::from(ping.clone()))
- .expect("request builder");
- let client = hyper::client::Client::new();
- let result = client.request(req).await.unwrap();
- assert!(result.status() == 401);
-
- // setup nats mock listener
- let sub = nats.subscribe("nova.cache.dispatch.interaction").unwrap();
-
- let ping = json!({ "type": 0 }).to_string();
- let timestamp = "my datetime :)";
- let signature_data = [timestamp.as_bytes().to_vec(), ping.as_bytes().to_vec()].concat();
- let signature = sign_message(signature_data, secret_key);
-
- // we must timeout
- let req = Request::builder()
- .method(Method::POST)
- .uri("http://localhost:5003/")
- .header("X-Signature-Ed25519", signature.clone())
- .header("X-Signature-Timestamp", timestamp)
- .body(Body::from(ping.clone()))
- .expect("request builder");
- let client = hyper::client::Client::new();
- let result = client.request(req).await.unwrap();
- assert!(result.status() == 500);
-
- sub.with_handler(move |msg| {
- info!("Received {}", &msg);
- msg.respond("ok :)").unwrap();
- Ok(())
- });
-
- let req = Request::builder()
- .method(Method::POST)
- .uri("http://localhost:5003/")
- .header("X-Signature-Ed25519", signature.clone())
- .header("X-Signature-Timestamp", timestamp)
- .body(Body::from(ping.clone()))
- .expect("request builder");
- let client = hyper::client::Client::new();
- let result = client.request(req).await.unwrap();
- assert!(result.status() == 200);
-}
--- /dev/null
+use std::time::Duration;
+
+use crate::{
+ config::Config,
+ handler::tests::utils::{generate_keypair, sign_message},
+ start,
+};
+use common::{config::test_init, nats_crate::Connection};
+use common::{
+ config::Settings,
+ log::info,
+ testcontainers::{clients::Cli, images::generic::GenericImage, Container, Docker},
+};
+use hyper::{Body, Method, Request};
+use lazy_static::{__Deref, lazy_static};
+use serde_json::json;
+
+lazy_static! {
+ static ref DOCKER: Cli = Cli::default();
+
+ static ref NATS_CONTAINER: Container<'static, Cli, GenericImage> = {
+ test_init();
+ let image: GenericImage = GenericImage::new("nats");
+ let container = DOCKER.run(image);
+ container.start();
+ container.get_host_port(4222).unwrap();
+ container
+ };
+
+ static ref KEYPAIR: (String, [u8; 64]) = {
+ generate_keypair()
+ };
+
+ static ref SETTINGS: Settings<Config> = {
+ let port = NATS_CONTAINER.get_host_port(4222).unwrap();
+ common::config::Settings {
+ config: crate::config::Config {
+ server: crate::config::ServerSettings {
+ port: 5003,
+ address: "0.0.0.0".to_string(),
+ },
+ discord: crate::config::Discord {
+ public_key: KEYPAIR.0.clone(),
+ client_id: 0,
+ },
+ },
+ redis: common::redis::RedisConfiguration {
+ url: "".to_string(),
+ },
+ monitoring: common::monitoring::MonitoringConfiguration {
+ enabled: false,
+ address: None,
+ port: None,
+ },
+ nats: common::nats::NatsConfiguration {
+ client_cert: None,
+ root_cert: None,
+ jetstream_api_prefix: None,
+ max_reconnects: None,
+ reconnect_buffer_size: None,
+ tls: None,
+ client_name: None,
+ tls_required: None,
+ host: format!("localhost:{}", port),
+ },
+ }
+ };
+
+ static ref TASK: () = {
+ std::thread::spawn(|| {
+ let r = tokio::runtime::Runtime::new().unwrap();
+ r.spawn(async { start(SETTINGS.clone()).await });
+ loop {}
+ });
+ std::thread::sleep(Duration::from_secs(1));
+ };
+}
+
+#[tokio::test]
+async fn respond_to_pings() {
+ let _ = NATS_CONTAINER.deref();
+ let _ = TASK.deref();
+ let ping = json!({ "type": 1 }).to_string();
+ let timestamp = "my datetime :)";
+ let signature_data = [timestamp.as_bytes().to_vec(), ping.as_bytes().to_vec()].concat();
+ let signature = sign_message(signature_data, KEYPAIR.1);
+
+ let req = Request::builder()
+ .method(Method::POST)
+ .uri("http://localhost:5003/")
+ .header("X-Signature-Ed25519", signature)
+ .header("X-Signature-Timestamp", timestamp)
+ .body(Body::from(ping.clone()))
+ .expect("request builder");
+ let client = hyper::client::Client::new();
+ let result = client.request(req).await.unwrap();
+
+ assert!(result.status() == 200);
+}
+
+#[tokio::test]
+async fn deny_invalid_signatures() {
+ let _ = NATS_CONTAINER.deref();
+ let _ = TASK.deref();
+ let ping = json!({ "type": 1 }).to_string();
+ let timestamp = "my datetime :)";
+
+ let req = Request::builder()
+ .method(Method::POST)
+ .uri("http://localhost:5003/")
+ .header("X-Signature-Ed25519", "inva&lid signature :)")
+ .header("X-Signature-Timestamp", timestamp)
+ .body(Body::from(ping.clone()))
+ .expect("request builder");
+ let client = hyper::client::Client::new();
+ let result = client.request(req).await.unwrap();
+ assert!(result.status() == 401);
+}
+
+#[tokio::test]
+async fn response_500_when_no_nats_response() {
+ let _ = NATS_CONTAINER.deref();
+ let _ = TASK.deref();
+ let ping = json!({ "type": 0 }).to_string();
+ let timestamp = "my datetime :)";
+ let signature_data = [timestamp.as_bytes().to_vec(), ping.as_bytes().to_vec()].concat();
+ let signature = sign_message(signature_data, KEYPAIR.1);
+
+ // we must timeout
+ let req = Request::builder()
+ .method(Method::POST)
+ .uri("http://localhost:5003/")
+ .header("X-Signature-Ed25519", signature)
+ .header("X-Signature-Timestamp", timestamp)
+ .body(Body::from(ping.clone()))
+ .expect("request builder");
+
+ let client = hyper::client::Client::new();
+ let result = client.request(req).await.unwrap();
+ assert!(result.status() == 500);
+}
+
+#[tokio::test]
+async fn respond_from_nats_response() {
+ let _ = NATS_CONTAINER.deref();
+ let _ = TASK.deref();
+ let nats: Connection = SETTINGS.clone().nats.into();
+ let sub = nats.subscribe("nova.cache.dispatch.interaction").unwrap();
+ let ping = json!({ "type": 0 }).to_string();
+ let timestamp = "my datetime :)";
+ let signature_data = [timestamp.as_bytes().to_vec(), ping.as_bytes().to_vec()].concat();
+ let signature = sign_message(signature_data, KEYPAIR.1);
+
+ sub.with_handler(move |msg| {
+ info!("Received {}", &msg);
+ msg.respond("ok :)").unwrap();
+ Ok(())
+ });
+
+ let req = Request::builder()
+ .method(Method::POST)
+ .uri("http://localhost:5003/")
+ .header("X-Signature-Ed25519", signature)
+ .header("X-Signature-Timestamp", timestamp)
+ .body(Body::from(ping.clone()))
+ .expect("request builder");
+ let client = hyper::client::Client::new();
+ let result = client.request(req).await.unwrap();
+ assert!(result.status() == 200);
+}
+
+#[tokio::test]
+async fn response_400_when_invalid_json_body() {
+ let _ = TASK.deref();
+ let ping = "{".to_string();
+ let timestamp = "my datetime :)";
+ let signature_data = [timestamp.as_bytes().to_vec(), ping.as_bytes().to_vec()].concat();
+ let signature = sign_message(signature_data, KEYPAIR.1);
+
+ let req = Request::builder()
+ .method(Method::POST)
+ .uri("http://localhost:5003/")
+ .header("X-Signature-Ed25519", signature)
+ .header("X-Signature-Timestamp", timestamp)
+ .body(Body::from(ping.clone()))
+ .expect("request builder");
+ let client = hyper::client::Client::new();
+ let result = client.request(req).await.unwrap();
+ assert!(result.status() == 400);
+}
+
+
+#[tokio::test]
+async fn response_400_when_invalid_utf8_body() {
+ let _ = TASK.deref();
+ // invalid 2 octet sequence
+ let ping = vec![0xc3, 0x28];
+
+ let timestamp = "my datetime :)";
+ let signature_data = [timestamp.as_bytes().to_vec(), ping.to_vec()].concat();
+ let signature = sign_message(signature_data, KEYPAIR.1);
+
+ let req = Request::builder()
+ .method(Method::POST)
+ .uri("http://localhost:5003/")
+ .header("X-Signature-Ed25519", signature)
+ .header("X-Signature-Timestamp", timestamp)
+ .body(Body::from(ping.clone()))
+ .expect("request builder");
+ let client = hyper::client::Client::new();
+ let result = client.request(req).await.unwrap();
+ assert!(result.status() == 400);
+}
-pub mod handler;
\ No newline at end of file
+pub mod handler_integration;
+pub mod signature;
+pub mod utils;
+pub mod handler;
--- /dev/null
+use crate::handler::signature::validate_signature;
+
+
+#[test]
+fn validate_signature_test() {
+ let signature = "543ec3547d57f9ddb1ec4c5c36503ebf288ffda3da3d510764c9a49c2abb57690ef974c63d174771bdd2481de1066966f57abbec12a3ec171b9f6e2373837002";
+ let public_key = "eefe0c24473737cb2035232e3b4eb91c206f0a14684168f3503f7d8316058d6f";
+ let content = "message de test incroyable".as_bytes().to_vec();
+ assert!(validate_signature(public_key, &content, signature))
+}
+
+#[test]
+fn validate_signature_reverse_test() {
+ let signature = "543ec3547d57f9ddb1ec4c5c36503ebf288ffda3da3d510764c9a49c2abb57690ef974c63d174771bdd2481de1066966f57abbec12a3ec171b9f6e2373837002";
+ let public_key = "c029eea18437292c87c62aec34e7d1bd4e38fe6126f3f7c446de6375dc666044";
+ 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 = "zzz";
+ 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
--- /dev/null
+pub fn generate_keypair() -> (
+ String,
+ [u8; libsodium_sys::crypto_sign_ed25519_SECRETKEYBYTES as usize],
+) {
+ use libsodium_sys::crypto_sign_ed25519_keypair;
+ let pk_s: String;
+
+ let mut pk = [0; libsodium_sys::crypto_sign_ed25519_PUBLICKEYBYTES as usize];
+ let mut sk = [0; libsodium_sys::crypto_sign_ed25519_SECRETKEYBYTES as usize];
+
+ let pk_p = pk.as_mut_ptr();
+ let sk_p = sk.as_mut_ptr();
+
+ // generate keypair
+ unsafe {
+ if crypto_sign_ed25519_keypair(pk_p, sk_p) < 0 {
+ panic!("keypair generation failed!");
+ }
+ };
+
+ pk_s = hex::encode(pk);
+ return (pk_s, sk);
+}
+
+pub fn sign_message(
+ msg: Vec<u8>,
+ sk: [u8; libsodium_sys::crypto_sign_ed25519_SECRETKEYBYTES as usize],
+) -> String {
+ use libc::c_ulonglong;
+ use libsodium_sys::crypto_sign_ed25519_detached;
+
+ let len = msg.len();
+ let mut signature_len: c_ulonglong = 0;
+ let mut str = [0; 64];
+ unsafe {
+ crypto_sign_ed25519_detached(
+ str.as_mut_ptr(),
+ &mut signature_len,
+ msg.as_ptr(),
+ len as u64,
+ sk.as_ptr(),
+ );
+ };
+
+ return hex::encode(str);
+}
\ No newline at end of file
}\r
\r
async fn start(settings: Settings<Config>) {\r
+ \r
let addr = format!(\r
"{}:{}",\r
settings.config.server.address, settings.config.server.port\r
});\r
\r
if let Err(e) = server.await {\r
- panic!("server error: {}", e);\r
+ error!("server error: {}", e);\r
}\r
}\r