use hmac::{Hmac, Mac}; use rusqlite::{params, Connection}; use serde_json::Value; use sha2::Sha256; use std::{ collections::HashMap, env, io::Read, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tiny_http::{Header, Method, Request, Response, Server}; fn header<'a>(request: &'a Request, name: &str) -> Result<&'a str, u16> { let values: Vec<_> = request .headers() .iter() .filter(|header| header.field.as_str().as_str().eq_ignore_ascii_case(name)) .collect(); if values.len() != 1 { return Err(400); } Ok(values[0].value.as_str()) } fn verify_signature( raw_body: &[u8], timestamp: &str, signature: &str, secret: &str, ) -> Result<(), u16> { let digest = signature.strip_prefix("v1=").ok_or(400_u16)?; if digest.len() != 64 || !digest .bytes() .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) { return Err(400); } let mut mac = Hmac::::new_from_slice(secret.as_bytes()).map_err(|_| 400_u16)?; mac.update(format!("v1.{timestamp}.").as_bytes()); mac.update(raw_body); mac.verify_slice(&hex::decode(digest).map_err(|_| 400_u16)?) .map_err(|_| 400_u16) } fn receive( request: &mut Request, secrets: &HashMap, subscription_id: &str, database: &str, ) -> Result<(), u16> { if request.method() != &Method::Post { return Err(405); } let timestamp = header(request, "X-ServiceApp-Timestamp")?.to_owned(); let event_id = header(request, "X-ServiceApp-Event-Id")?.to_owned(); let signature = header(request, "X-ServiceApp-Signature")?.to_owned(); let secret = secrets .get(header(request, "X-ServiceApp-Key-Id")?) .ok_or(400_u16)?; if timestamp.is_empty() || timestamp.len() > 20 || !timestamp.bytes().all(|b| b.is_ascii_digit()) { return Err(400); } let seconds = timestamp.parse::().map_err(|_| 400_u16)?; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| 503_u16)? .as_secs(); if now.abs_diff(seconds) > 300 { return Err(400); } if request .headers() .iter() .any(|h| h.field.equiv("Transfer-Encoding")) { return Err(400); } let length = header(request, "Content-Length")? .parse::() .map_err(|_| 400_u16)?; if !(1..=65536).contains(&length) { return Err(413); } if !header(request, "Content-Type")? .split(';') .next() .unwrap_or("") .trim() .eq_ignore_ascii_case("application/json") { return Err(415); } let mut raw_body = Vec::new(); request .as_reader() .take(65537) .read_to_end(&mut raw_body) .map_err(|_| 400_u16)?; if raw_body.len() != length { return Err(400); } verify_signature(&raw_body, ×tamp, &signature, secret)?; let event: Value = serde_json::from_slice(&raw_body).map_err(|_| 400_u16)?; let uuid = event_id.len() == 36 && event_id.bytes().enumerate().all(|(i, b)| { if [8, 13, 18, 23].contains(&i) { b == b'-' } else { b.is_ascii_hexdigit() } }); if !uuid || event["event_id"].as_str() != Some(event_id.as_str()) || event["schema_version"].as_u64() != Some(1) || !event["resource"].is_object() { return Err(400); } let connection = Connection::open(database).map_err(|_| 503_u16)?; connection .busy_timeout(Duration::from_secs(5)) .map_err(|_| 503_u16)?; connection .execute_batch( "CREATE TABLE IF NOT EXISTS inbox ( subscription_id TEXT NOT NULL, event_id TEXT NOT NULL, body BLOB NOT NULL, state TEXT NOT NULL DEFAULT 'pending', PRIMARY KEY (subscription_id, event_id))", ) .map_err(|_| 503_u16)?; connection .execute( "INSERT INTO inbox (subscription_id, event_id, body) VALUES (?1, ?2, ?3) ON CONFLICT(subscription_id, event_id) DO NOTHING", params![subscription_id, event_id, raw_body], ) .map_err(|_| 503_u16)?; Ok(()) } fn main() -> Result<(), Box> { let secrets: HashMap = serde_json::from_str(&env::var("SERVICEAPP_WEBHOOK_SECRETS")?)?; let subscription_id = env::var("SERVICEAPP_WEBHOOK_SUBSCRIPTION_ID")?; if secrets.is_empty() || secrets .iter() .any(|(key, secret)| key.is_empty() || secret.is_empty()) || subscription_id.is_empty() { return Err("Укажите секреты и ID подписки".into()); } let database = env::var("SERVICEAPP_WEBHOOK_DB").unwrap_or_else(|_| "webhook-inbox.sqlite3".into()); let server = Server::http("127.0.0.1:8080")?; for mut request in server.incoming_requests() { let status = match receive(&mut request, &secrets, &subscription_id, &database) { Ok(()) => 204, Err(status) => status, }; let mut response = Response::empty(status); if status == 503 { response.add_header(Header::from_bytes("Retry-After", "2").unwrap()); } let _ = request.respond(response); } Ok(()) }