// 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
    
"#;