diff options
| author | ops <ops@localhost> | 2026-09-16 21:54:30 +0000 |
|---|---|---|
| committer | ops <ops@localhost> | 2026-09-16 21:57:23 +0000 |
| commit | 47fcc566884d7078c153cdce9fb62399fcca5d9e (patch) | |
| tree | 98aba4327069d588d4134c326297b733628fc200 /contact-service/src/main.rs | |
Initial Commit
Diffstat (limited to 'contact-service/src/main.rs')
| -rw-r--r-- | contact-service/src/main.rs | 215 |
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>"#; |
