use std::{convert::Infallible, fs, net::SocketAddr}; use config::Config; use http_body_util::{BodyExt, Full}; use hyper::{ body::{self, Body, Bytes}, server::conn::http1, service::service_fn, Method, Request, Response, StatusCode, }; use hyper_util::rt::TokioIo; use miniflux_requests::MinifluxEvent; use ring::hmac; use serenity::{all::GatewayIntents, Client}; use thiserror::Error; use tokio::net::TcpListener; use crate::discord::DiscordHolder; mod config; mod discord; mod miniflux_requests; //FIXME! const CONFIG_PATH: &'static str = "./config.json"; #[tokio::main] async fn main() -> Result<(), Box> { let config: Config = serde_json::from_slice(&fs::read(CONFIG_PATH).unwrap()).unwrap(); stderrlog::new() .module(module_path!()) .verbosity(config.log_level) .timestamp(stderrlog::Timestamp::Second) .init() .unwrap(); let mut client = Client::builder(&config.discord_token, GatewayIntents::empty()) .await .expect("Error creating Discord client"); let addr = SocketAddr::from((config.host, config.port)); let listener = TcpListener::bind(addr).await?; log::info!("Binded to {}:{}", config.host, config.port); let cache = client.cache.clone(); let http = client.http.clone(); let holder = DiscordHolder { cache, http, config }; let client_handle = tokio::task::spawn(async move { log::info!("Discord client started, should show up online now!"); client.start().await.expect("Failed starting Discord client!"); }); let loop_handle = tokio::task::spawn(async move { log::info!("Listening to TCP/HTTP connections now.."); loop { let Ok((stream, addr)) = listener.accept().await else { log::warn!("Failed to accept TCP stream"); continue; }; log::debug!("=> {}", addr); let io = TokioIo::new(stream); let holder = holder.clone(); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { let service = service_fn(|req: Request| { let holder = holder.clone(); async move { Ok::<_, Infallible>( process_webhook(req, &holder).await.unwrap_or_else(|e| e.into()), ) } }); let result = http1::Builder::new().serve_connection(io, service).await; if let Err(err) = result { log::warn!("Error serving connection {}: {:?}", addr, err); } }); } }); let (res1, res2) = tokio::join!(client_handle, loop_handle); res1.expect("Failed unwrapping client handle result!"); res2.expect("Failed unwrapping loop handle result!"); Ok(()) } async fn process_webhook( req: Request, holder: &DiscordHolder, ) -> Result>, CustomError> { let method = req.method(); if method != Method::POST { Err(CustomError::InvalidMethod(method.clone()))?; } let userid: u64 = req.uri().path().split("/").nth(1).ok_or(CustomError::IdPathNotFound)?.parse()?; if !holder.config.whitelisted_user_ids.contains(&userid) { Err(CustomError::NotWhitelistedUser(userid))?; } let headers = req.headers(); let signature_bytes = headers.get("x-miniflux-signature").ok_or(CustomError::SignatureMissing)?.as_bytes(); let signature = data_encoding::HEXLOWER.decode(signature_bytes)?; let event_type = headers.get("x-miniflux-event-type").ok_or(CustomError::EventTypeMissing)?.clone(); let upper = req.body().size_hint().upper().unwrap_or(u64::MAX); if upper > holder.config.payload_max_size { Err(CustomError::PayloadTooLarge(upper))?; } let whole_body = req.collect().await?.to_bytes(); let bytes = whole_body.iter().cloned().collect::>(); let key = hmac::Key::new(hmac::HMAC_SHA256, &holder.config.miniflux_webhook_secret.as_bytes()); let () = hmac::verify(&key, &bytes, &signature).map_err(|_| CustomError::HmacValidationError)?; let event: MinifluxEvent = serde_json::from_slice(&bytes)?; match event { MinifluxEvent::New(_) => assert!(event_type == "new_entries"), MinifluxEvent::Save(_) => assert!(event_type == "save_entry"), } log::info!("received {} from miniflux!", event_type.to_str().unwrap_or_default()); let res = holder.send(userid, event).await; if let Err(err) = res { log::error!("Error while trying to send discord message {}", err); } Ok(Response::new(Full::new(vec![].into()))) } #[derive(Error, Debug)] pub enum CustomError { #[error("id path not found")] IdPathNotFound, #[error("invalid method: {0}")] InvalidMethod(Method), #[error("parse int error: {0}")] ParseIntError(#[from] std::num::ParseIntError), #[error("{0} not allowed!")] NotWhitelistedUser(u64), #[error("signature missing")] SignatureMissing, #[error("event_type missing")] EventTypeMissing, #[error("signature decoding error: {0}")] DecodeError(#[from] data_encoding::DecodeError), #[error("payload too large ({0})")] PayloadTooLarge(u64), #[error("Hyper Error: {0}")] HyperError(#[from] hyper::Error), #[error("HMAC signature validation failed")] HmacValidationError, #[error("JSON deserialization failed: {0}")] JsonError(#[from] serde_json::Error), } impl Into>> for CustomError { fn into(self) -> Response> { match &self { CustomError::JsonError(_) => log::error!("{}", self), CustomError::HmacValidationError => log::warn!("{}", self), CustomError::HyperError(_) => log::warn!("{}", self), CustomError::PayloadTooLarge(_) => log::warn!("{}", self), CustomError::SignatureMissing => log::warn!("{}", self), CustomError::EventTypeMissing => log::warn!("{}", self), _ => log::debug!("{}", self), } let mut resp = Response::new(Full::from(Bytes::from(self.to_string()))); let status_code = match self { CustomError::IdPathNotFound => StatusCode::NOT_FOUND, CustomError::InvalidMethod(_) => StatusCode::METHOD_NOT_ALLOWED, CustomError::ParseIntError(_) => StatusCode::BAD_REQUEST, CustomError::NotWhitelistedUser(_) => StatusCode::FORBIDDEN, CustomError::SignatureMissing => StatusCode::BAD_REQUEST, CustomError::EventTypeMissing => StatusCode::BAD_REQUEST, CustomError::DecodeError(_) => StatusCode::BAD_REQUEST, CustomError::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE, CustomError::HyperError(_) => StatusCode::INTERNAL_SERVER_ERROR, CustomError::HmacValidationError => StatusCode::FORBIDDEN, CustomError::JsonError(_) => StatusCode::BAD_REQUEST, }; *resp.status_mut() = status_code; resp } }