summaryrefslogtreecommitdiff
path: root/contact-service/src/main.rs
diff options
context:
space:
mode:
authorops <ops@localhost>2026-09-16 21:54:30 +0000
committerops <ops@localhost>2026-09-16 21:57:23 +0000
commit47fcc566884d7078c153cdce9fb62399fcca5d9e (patch)
tree98aba4327069d588d4134c326297b733628fc200 /contact-service/src/main.rs
Initial Commit
Diffstat (limited to 'contact-service/src/main.rs')
-rw-r--r--contact-service/src/main.rs215
1 files changed, 215 insertions, 0 deletions
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 <doan@luuduct.ai>
+//
+// 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 <https://www.gnu.org/licenses/>.
+
+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<String>,
+ 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<Arc<AppConfig>>,
+ Form(form): Form<ContactForm>,
+) -> 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#"<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <title>Message Sent - Doan Luu Duc Tai</title>
+ <link rel="stylesheet" href="/styles.css">
+</head>
+<body>
+ <div id="page">
+ <div class="badge">[ JS-free ]</div>
+ <div id="content">
+ <pre>
+<a href="/" class="logo">
+ ____ ___ _ _ _
+| _ \ / _ \ / \ | \ | |
+| | | | | | |/ _ \ | \| |
+| |_| | |_| / ___ \| |\ |
+|____/ \___/_/ \_\_| \_| LUU DUC TAI
+</a>
+
++------------------------------------------------------------------------------+
+| [+] Success: Message encrypted and delivered to inbox. |
++------------------------------------------------------------------------------+
+
+<a href="/"><-- Return to Home Page</a>
+ </pre>
+ </div>
+ </div>
+</body>
+</html>"#;
+
+const ERROR_PAGE: &str = r#"<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Error</title>
+ <link rel="stylesheet" href="/styles.css">
+</head>
+<body>
+ <pre>
+[!] Error: Unable to process or encrypt message.
+
+<a href="/message.html"><-- Try Again</a>
+ </pre>
+</body>
+</html>"#;