From e6b350a1d48ca0490b7a3447bb8d0a4235514b76 Mon Sep 17 00:00:00 2001 From: Doan Luu Duc Tai Date: Fri, 30 Oct 2026 21:39:57 -0500 Subject: Implement experimental file-based encrypted private chat Add fixed-suite Rust/WASI identity files, signed challenge authentication, ciphertext-only SQLite storage, a minimal LibreJS-compatible interface, and consistent backup snapshots. Keep production unchanged and document the local workflow, limits, source distribution, and browser recovery tests. --- crates/chat-server/src/main.rs | 129 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 crates/chat-server/src/main.rs (limited to 'crates/chat-server/src/main.rs') diff --git a/crates/chat-server/src/main.rs b/crates/chat-server/src/main.rs new file mode 100644 index 0000000..6c7f4e1 --- /dev/null +++ b/crates/chat-server/src/main.rs @@ -0,0 +1,129 @@ +// 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 . + +#![doc = "Local experimental chat server with ciphertext-only durable storage."] + +mod http; +mod service; +mod store; + +use anyhow::{Context, Result, ensure}; +use clap::Parser; +use std::io::Write; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::process::ExitCode; + +#[derive(Debug, Parser)] +#[command(about = "Run the experimental private chat locally; production is not enabled")] +struct Arguments { + #[arg(long, default_value = ".")] + root: PathBuf, + #[arg(long, default_value = "127.0.0.1:9080")] + bind: SocketAddr, + #[arg(long)] + init: bool, + #[arg(long)] + snapshot: Option, +} + +fn initialize(root: &std::path::Path) -> Result<()> { + let env = root.join(".env"); + let existing = if env.exists() { + std::fs::read_to_string(&env)? + } else { + String::new() + }; + ensure!( + !existing + .lines() + .any(|line| line.starts_with("CHAT_SETUP_TOKEN=")), + "CHAT_SETUP_TOKEN already exists" + ); + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&env)?; + std::fs::set_permissions(&env, std::os::unix::fs::PermissionsExt::from_mode(0o600))?; + writeln!(file, "\nCHAT_SETUP_TOKEN={}", service::token()?)?; + if !existing + .lines() + .any(|line| line.starts_with("CHAT_ORIGIN=")) + { + writeln!(file, "CHAT_ORIGIN=http://127.0.0.1:9080")?; + } + Ok(()) +} + +async fn run(arguments: Arguments) -> Result<()> { + let root = arguments.root.canonicalize()?; + if arguments.init { + return initialize(&root); + } + let data = root.join("var/data/chat"); + std::fs::create_dir_all(&data)?; + std::fs::set_permissions(&data, std::os::unix::fs::PermissionsExt::from_mode(0o700))?; + let database = data.join("chat.sqlite"); + let store = store::Store::open(&database)?; + std::fs::set_permissions( + &database, + std::os::unix::fs::PermissionsExt::from_mode(0o600), + )?; + if let Some(destination) = arguments.snapshot { + store.snapshot(&destination)?; + std::fs::set_permissions( + destination, + std::os::unix::fs::PermissionsExt::from_mode(0o600), + )?; + return Ok(()); + } + ensure!( + arguments.bind.ip().is_loopback(), + "Experimental server must bind to loopback" + ); + let settings = dotenvy::from_path_iter(root.join(".env"))? + .collect::, _>>()?; + let origin = settings + .get("CHAT_ORIGIN") + .context("Run --init to configure chat")?; + ensure!( + origin == &format!("http://{}", arguments.bind), + "This development server requires CHAT_ORIGIN to match its loopback bind address" + ); + let setup = settings + .get("CHAT_SETUP_TOKEN") + .context("Missing CHAT_SETUP_TOKEN")?; + let state = service::Service::new(store, origin.clone(), setup.clone()); + let listener = tokio::net::TcpListener::bind(arguments.bind).await?; + writeln!( + std::io::stdout().lock(), + "Experimental chat: {origin} (owner setup: /#setup)" + )?; + http::serve(listener, state, &root).await +} + +#[tokio::main] +async fn main() -> ExitCode { + match run(Arguments::parse()).await { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + drop(writeln!( + std::io::stderr().lock(), + "Chat server failed: {error:#}" + )); + ExitCode::FAILURE + } + } +} -- cgit v1.2.3