summaryrefslogtreecommitdiff
path: root/crates/chat-server/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/chat-server/src/main.rs')
-rw-r--r--crates/chat-server/src/main.rs202
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()
}