//! The host HTTP listener and console startup with one shared credentials store. use super::{Api, respond}; use crate::cred::{Caller, Cred, entry::EntryKey, webauthn::Passkeys}; use std::{net::SocketAddr, path::Path, sync::Arc}; #[derive(Clone, Debug, clap::Args)] pub struct Options { /// Address of the host's approval, enrollment, and secret-entry listener. #[arg(long, default_value = "127.0.0.1:8081")] pub api_listen: SocketAddr, /// WebAuthn relying-party ID for the host pages. #[arg(long, default_value = "localhost")] pub api_rp_id: String, /// Browser origin of the host pages, not the world's origin. #[arg(long, default_value = "http://localhost:8081")] pub api_origin: String, } pub struct Listener { server: Arc, thread: Option>, } impl Listener { pub fn start(cred: Arc, host_dir: &Path, options: Options) -> anyhow::Result { let server = Arc::new( tiny_http::Server::http(options.api_listen) .map_err(|_| anyhow::anyhow!("host API address unavailable"))?, ); let serving = server.clone(); let host_dir = host_dir.to_owned(); let (ready, started) = std::sync::mpsc::sync_channel(1); let thread = std::thread::spawn(move || { let setup = || -> anyhow::Result<_> { Ok(( rusqlite::Connection::open(host_dir.join("cred.sqlite"))?, EntryKey::open(&host_dir.join("entry.key"))?, )) }; let serve = || -> anyhow::Result<()> { let (db, entry) = setup()?; let passkeys = Passkeys::new(&db, &options.api_rp_id, &options.api_origin)?; let api = Api::new(cred, passkeys, entry); let _ = ready.send(true); for mut request in serving.incoming_requests() { let response = (|| -> anyhow::Result<_> { let mut body = Vec::new(); request.as_reader().read_to_end(&mut body)?; let request = http::Request::builder() .method(request.method().as_str()) .uri(request.url()) .body(bytes::Bytes::from(body))?; Ok(respond( &api, Caller { program: "/host/browser".into(), pid: std::process::id(), conversations: Vec::new(), }, request, )) })(); let response = response.unwrap_or_else(|_| { http::Response::builder() .status(400) .header("content-type", "application/json") .body(bytes::Bytes::from_static( b"{\"error\":\"invalid_request\",\"message\":\"invalid_request\"}", )) .expect("static response") }); let content_type = response.headers()["content-type"].as_bytes(); let reply = tiny_http::Response::from_data(response.body().to_vec()) .with_status_code(response.status().as_u16()) .with_header( tiny_http::Header::from_bytes(b"Content-Type", content_type) .expect("static header"), ); let _ = request.respond(reply); } Ok(()) }; if serve().is_err() { let _ = ready.send(false); } }); if !started.recv().unwrap_or(false) { let _ = thread.join(); anyhow::bail!("host API initialization failed"); } Ok(Self { server, thread: Some(thread), }) } pub fn address(&self) -> SocketAddr { self.server.server_addr().to_ip().expect("TCP listener") } } impl Drop for Listener { fn drop(&mut self) { self.server.unblock(); if let Some(thread) = self.thread.take() { let _ = thread.join(); } } } /// Preserve the console lifecycle while sharing Cred with the host page listener. pub async fn run_with_userland( path: &Path, userland: Option<&Path>, options: Options, ) -> anyhow::Result<()> { let world = crate::world::World::open_with_userland(path, userland)?; let cred = Arc::new(Cred::open(&world.host_dir)?); let _listener = Listener::start(cred.clone(), &world.host_dir, options)?; let runtime = crate::process::Runtime::new(crate::world::World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), })?; let services = crate::process::supervise(runtime.clone()); let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { tokio::runtime::Builder::new_current_thread() .enable_all() .build()? .block_on(crate::harness::run_console(world, Arc::new(runtime), cred)) }) .await .map_err(anyhow::Error::from) .and_then(|result| result) .map_err(|error| { let mut text = error.to_string(); for key in ["PI_PROXY_BASE_URL", "PI_PROXY_API_KEY"] { if let Ok(value) = std::env::var(key) && !value.is_empty() { text = text.replace(&value, "[redacted]"); } } anyhow::anyhow!(text) }); services.shutdown().await; result }