diff options
| author | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-10-31 13:08:08 -0500 |
|---|---|---|
| committer | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-10-31 13:08:08 -0500 |
| commit | a80df869a92c330fbad9695057720b11a18af901 (patch) | |
| tree | 5307272821cb3adc3f0348413a3b904290890044 /crates/chat-server/src/main.rs | |
| parent | 9139398d465f5899cc31bc4cdd54daa7392ca550 (diff) | |
Forbid Rust lints and enforce strict typed TypeScript linting
Diffstat (limited to 'crates/chat-server/src/main.rs')
| -rw-r--r-- | crates/chat-server/src/main.rs | 202 |
1 files changed, 5 insertions, 197 deletions
diff --git a/crates/chat-server/src/main.rs b/crates/chat-server/src/main.rs index 1d600b7..942a379 100644 --- a/crates/chat-server/src/main.rs +++ b/crates/chat-server/src/main.rs @@ -13,204 +13,12 @@ // 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/>. -#![doc = "Encrypted private chat server and backend maintenance commands."] +#![doc = "Checked command-line entry point for the personal infrastructure stack."] -mod http; -mod maintenance; -mod service; -mod store; +mod app; -use anyhow::{Context, Result, ensure}; -use clap::Parser; -use std::io::{Read, Write}; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::process::ExitCode; +pub use app::run; -#[derive(Debug, Parser)] -#[command(about = "Run private chat directly or behind the stack gateway")] -struct Arguments { - #[arg(long, default_value = ".")] - root: PathBuf, - #[command(flatten)] - listener: Listener, - #[arg( - long, - help = "Check the local HTTP health endpoint without opening the database" - )] - healthcheck: bool, - #[arg(long, conflicts_with_all = ["reset_owner", "owner_fingerprint", "snapshot"])] - init: bool, - #[arg(long, value_name = "FINGERPRINT", conflicts_with_all = ["owner_fingerprint", "snapshot"], help = "Permanently clear the matching owner registration and all chat history after key loss")] - reset_owner: Option<String>, - #[arg( - long, - conflicts_with = "snapshot", - help = "Print the registered owner's public fingerprint" - )] - owner_fingerprint: bool, - #[arg(long)] - snapshot: Option<PathBuf>, -} - -#[derive(Debug, clap::Args)] -struct Listener { - #[arg(long)] - bind: Option<SocketAddr>, - #[arg( - long, - help = "Run behind the stack gateway using its explicit container environment" - )] - proxy: bool, -} - -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")?; - } - stack_config::initialize_public(root) -} - -fn healthcheck(mut address: SocketAddr) -> Result<()> { - if address.ip().is_unspecified() { - address.set_ip(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)); - } - let mut socket = - std::net::TcpStream::connect_timeout(&address, std::time::Duration::from_secs(2))?; - socket.set_read_timeout(Some(std::time::Duration::from_secs(2)))?; - socket.set_write_timeout(Some(std::time::Duration::from_secs(2)))?; - socket.write_all(b"GET /healthz HTTP/1.0\r\nHost: localhost\r\n\r\n")?; - let mut response = String::new(); - socket.take(8192).read_to_string(&mut response)?; - ensure!( - response.starts_with("HTTP/1.0 200") || response.starts_with("HTTP/1.1 200"), - "Chat health endpoint failed" - ); - Ok(()) -} - -fn validate_origin( - configuration: &stack_config::Config, - bind: SocketAddr, - proxy: bool, -) -> Result<()> { - let origin = configuration.value("CHAT_ORIGIN")?; - if proxy { - let public = configuration.public_values(None)?; - let canonical = public - .get("CHAT_PUBLIC_ORIGIN") - .context("Missing public chat origin")?; - let local = format!( - "http://{}:{}", - configuration.value("CHAT_LOCAL_HOST")?, - configuration.port("LOCAL_HTTP_PORT")? - ); - ensure!( - origin == canonical || origin == local, - "Gateway chat origin must match the configured HTTPS hostname or dedicated local hostname" - ); - } else { - ensure!( - bind.ip().is_loopback() && origin == format!("http://{bind}"), - "Direct development chat requires an exact loopback HTTP origin" - ); - } - Ok(()) -} - -async fn run(arguments: Arguments) -> Result<()> { - let root = arguments.root.canonicalize()?; - if arguments.init { - return initialize(&root); - } - let configuration = if arguments.listener.proxy { - stack_config::Config::from_environment()? - } else { - stack_config::Config::load(&root)? - }; - let bind = arguments - .listener - .bind - .unwrap_or(configuration.chat_bind()?); - if arguments.healthcheck { - return healthcheck(bind); - } - 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 _server_lock = if arguments.snapshot.is_none() && !arguments.owner_fingerprint { - Some(maintenance::lock(&data)?) - } else { - None - }; - let mut store = store::Store::open(&database)?; - std::fs::set_permissions( - &database, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - )?; - if arguments.owner_fingerprint { - return maintenance::fingerprint(&store); - } - if let Some(expected) = arguments.reset_owner { - return maintenance::reset(&mut store, &expected, bind, arguments.listener.proxy); - } - if let Some(destination) = arguments.snapshot { - store.snapshot(&destination)?; - std::fs::set_permissions( - destination, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - )?; - return Ok(()); - } - let origin = configuration.value("CHAT_ORIGIN")?; - validate_origin(&configuration, bind, arguments.listener.proxy)?; - let setup = configuration - .value("CHAT_SETUP_TOKEN") - .context("Run --init to configure chat")?; - let state = service::Service::new(store, origin.to_owned(), setup.to_owned()); - let listener = tokio::net::TcpListener::bind(bind).await?; - writeln!( - std::io::stdout().lock(), - "Chat ready: {origin} (owner setup: /#setup)" - )?; - let local = (arguments.listener.proxy && origin.starts_with("http://")) - .then(|| configuration.port("LOCAL_HTTP_PORT")) - .transpose()?; - http::serve(listener, state, &root, configuration.public_values(local)?).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 - } - } +fn main() -> std::process::ExitCode { + run() } |
