use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Instant;
use super::config::Config;
use super::listen::serve;
use super::local_signin::{Local, NoLys, link};
use super::lys::LysSignIn;
use super::signin::{Door, Issuer};
use crate::client::network::Trust;
const CONFIG_BYTES: u64 = 1_048_576;
const USAGE: &str = "usage: haematite door [--check] --config <absolute door.json>";
pub fn dispatch(args: impl Iterator<Item = String>) -> ExitCode {
let started = Instant::now();
let (path, checking) = match arguments(args) {
Ok(arguments) => arguments,
Err(message) => {
eprintln!("{message}");
return ExitCode::FAILURE;
}
};
match run_config(&path, checking, started) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("{message}");
if checking {
ExitCode::from(78)
} else {
ExitCode::FAILURE
}
}
}
}
pub fn run(args: impl Iterator<Item = String>) -> Result<(), String> {
let started = Instant::now();
let (path, checking) = arguments(args)?;
run_config(&path, checking, started)
}
fn run_config(path: &Path, checking: bool, started: Instant) -> Result<(), String> {
let config = load(path, checking)?;
let networked = config
.stores
.values()
.any(|store| matches!(store, super::config::Store::Network(_)));
let trust = if networked {
Trust::system()?
} else {
Trust::mozilla()?
};
let reached = super::check::stores(&config, &trust);
let say = |line: &String| {
if checking {
println!("{line}");
} else {
eprintln!("haematite door: {line}");
}
};
let reached = match reached {
Ok(lines) => lines,
Err((lines, refused)) => {
lines.iter().for_each(say);
return Err(refused);
}
};
reached.iter().for_each(say);
if checking {
super::check::configuration(&config)?;
println!(
"door_check_ok: {} children, {} stores",
config.children.len(),
config.stores.len()
);
return Ok(());
}
if let Some(lys) = &config.lys {
let door = Arc::new(Door::new(LysSignIn::connect(lys)?));
let assistant = super::assistant_socket::Listener::start(&door, &config)
.map_err(|error| format!("{}: {}", error.code, error.message))?;
open(&config, &trust, &door, started, None)?;
drop(assistant);
return Ok(());
}
let owner = crate::serve::contracts::local_owner()
.map_err(|failure| format!("{}: {}", failure.code, failure.message))?;
let (local, key) =
Local::new(owner).map_err(|failure| format!("{}: {}", failure.code, failure.message))?;
let door = Arc::new(Door::at_keyboard(NoLys, local));
open(&config, &trust, &door, started, Some(&key))
}
fn open<I: Issuer>(
config: &Config,
trust: &Trust,
door: &Arc<Door<I>>,
started: Instant,
key: Option<&str>,
) -> Result<(), String> {
let children = super::children::Children::start(&config.children)?;
let site = Arc::new(config.site(trust)?);
let limits = config.limits();
let listener = TcpListener::bind(config.listen)
.map_err(|error| format!("the door could not listen on {}: {error}", config.listen))?;
let listening = listener
.local_addr()
.map_err(|error| format!("the door could not read its listen address: {error}"))?;
eprintln!(
"haematite door: listening on {listening} for {} elapsed_ms={}",
config.origin,
started.elapsed().as_millis()
);
if let Some(key) = key {
let mut out = std::io::stdout().lock();
writeln!(out, "{}", link(&config.origin, key))
.and_then(|()| out.flush())
.map_err(|error| format!("the door could not print its sign-in link: {error}"))?;
}
serve(&listener, door, &site, &limits);
drop(children);
Ok(())
}
fn arguments(mut args: impl Iterator<Item = String>) -> Result<(PathBuf, bool), String> {
let mut checking = false;
let mut path = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--check" if !checking => checking = true,
"--config" if path.is_none() => path = Some(PathBuf::from(args.next().ok_or(USAGE)?)),
_ => return Err(USAGE.to_owned()),
}
}
let path = path.ok_or(USAGE)?;
if !path.is_absolute() {
return Err(USAGE.to_owned());
}
Ok((path, checking))
}
fn load(path: &Path, checking: bool) -> Result<Config, String> {
let file = super::config_upgrade::open_source(path)?;
let mut bytes = Vec::new();
file.take(CONFIG_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| format!("the door configuration could not be read: {error}"))?;
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > CONFIG_BYTES {
return Err("the door configuration exceeds its byte limit".to_owned());
}
let plan = super::config_upgrade::plan(path, &bytes)?;
if checking {
super::config_upgrade::check_apply(path, &plan)?;
} else {
super::config_upgrade::apply(path, &bytes, &plan)?;
}
let config = Config::parse(&plan.bytes)?;
if checking && !plan.names.is_empty() {
println!(
"door_config_upgrade_needed: give_up_after=3 for {}",
plan.names.join(", ")
);
}
Ok(config)
}