From 47fcc566884d7078c153cdce9fb62399fcca5d9e Mon Sep 17 00:00:00 2001 From: ops Date: Wed, 16 Sep 2026 21:54:30 +0000 Subject: Initial Commit --- contact-service/src/main.rs | 215 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 contact-service/src/main.rs (limited to 'contact-service/src') diff --git a/contact-service/src/main.rs b/contact-service/src/main.rs new file mode 100644 index 0000000..b83bbb8 --- /dev/null +++ b/contact-service/src/main.rs @@ -0,0 +1,215 @@ +// Copyright (C) 2026 Doan Luu Duc Tai +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use axum::{ + extract::{Form, State}, + response::Html, + routing::post, + Router, +}; +use chrono::Utc; +use serde::Deserialize; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::Arc; + +#[derive(Clone)] +struct AppConfig { + bind_addr: String, + cert_path: PathBuf, + inbox_dir: PathBuf, + sq_bin: PathBuf, +} + +impl AppConfig { + fn from_env() -> Self { + let home = std::env::var("HOME").unwrap_or_else(|_| "/home/ops".into()); + + Self { + bind_addr: std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:8085".into()), + cert_path: PathBuf::from( + std::env::var("CERT_PATH") + .unwrap_or_else(|_| format!("{}/stack/contact-cert.pgp", home)), + ), + inbox_dir: PathBuf::from( + std::env::var("INBOX_DIR") + .unwrap_or_else(|_| format!("{}/stack/inbox", home)), + ), + sq_bin: PathBuf::from( + std::env::var("SQ_BIN") + .unwrap_or_else(|_| format!("{}/.cargo/bin/sq", home)), + ), + } + } +} + +#[derive(Deserialize)] +struct ContactForm { + sender: Option, + message: String, +} + +#[tokio::main] +async fn main() { + let config = Arc::new(AppConfig::from_env()); + + let app = Router::new() + .route("/send-message", post(handle_message)) + .with_state(config.clone()); + + let listener = tokio::net::TcpListener::bind(&config.bind_addr) + .await + .unwrap_or_else(|_| panic!("Failed to bind to {}", config.bind_addr)); + + println!("Contact backend listening on http://{}", config.bind_addr); + println!("Using PQC Cert: {:?}", config.cert_path); + println!("Storing inbox in: {:?}", config.inbox_dir); + + axum::serve(listener, app).await.unwrap(); +} + +async fn handle_message( + State(config): State>, + Form(form): Form, +) -> Html<&'static str> { + let message = form.message.trim(); + if message.is_empty() { + return Html(ERROR_PAGE); + } + + let raw_sender = form.sender.as_deref().unwrap_or("").trim(); + let sender_display = if raw_sender.is_empty() { + "Anonymous" + } else { + raw_sender + }; + + let now = Utc::now(); + let timestamp_str = now.format("%Y-%m-%d_%H-%M-%S").to_string(); + + let sanitized_sender: String = raw_sender + .chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_') + .take(30) + .collect(); + + let filename = if sanitized_sender.is_empty() { + format!("{}.pgp", timestamp_str) + } else { + format!("{}_{}.pgp", timestamp_str, sanitized_sender) + }; + + let output_path = config.inbox_dir.join(filename); + + let payload = format!( + "=== RFC 9980 PQC MESSAGE ===\nSender: {}\nTimestamp: {}\n============================\n\n{}", + sender_display, + now.to_rfc3339(), + message + ); + + let mut child = match Command::new(&config.sq_bin) + .args([ + "encrypt", + "--for-file", + config.cert_path.to_str().unwrap_or_default(), + "--without-signature", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + Ok(proc) => proc, + Err(e) => { + eprintln!("Failed to spawn sq binary ({:?}): {}", config.sq_bin, e); + return Html(ERROR_PAGE); + } + }; + + if let Some(mut stdin) = child.stdin.take() { + if stdin.write_all(payload.as_bytes()).is_err() { + return Html(ERROR_PAGE); + } + } + + let output = match child.wait_with_output() { + Ok(out) => out, + Err(_) => return Html(ERROR_PAGE), + }; + + if !output.status.success() { + eprintln!( + "sq encryption failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + return Html(ERROR_PAGE); + } + + if std::fs::write(&output_path, &output.stdout).is_err() { + eprintln!("Failed to write to file {:?}", output_path); + return Html(ERROR_PAGE); + } + + Html(SUCCESS_PAGE) +} + +const SUCCESS_PAGE: &str = r#" + + + + + Message Sent - Doan Luu Duc Tai + + + +
+
[ JS-free ]
+
+
+
+
++------------------------------------------------------------------------------+
+| [+] Success: Message encrypted and delivered to inbox.                       |
++------------------------------------------------------------------------------+
+
+<-- Return to Home Page
+            
+
+
+ +"#; + +const ERROR_PAGE: &str = r#" + + + + Error + + + +
+[!] Error: Unable to process or encrypt message.
+
+<-- Try Again
+    
+ +"#; -- cgit v1.2.3