//! The host HTTP listener and console startup share one owned API and credentials store. use super::Api; use crate::cred::{Caller, Cred}; 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 api = Arc::new(Api::open( cred, host_dir, &options.api_rp_id, &options.api_origin, )?); Self::start_with_api(api, options) } pub fn start_with_api(api: Arc, 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 thread = std::thread::spawn(move || { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("API runtime"); 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(runtime.block_on(super::handle( &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(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 with one API for guests and host pages. 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 api = Arc::new(Api::open( cred.clone(), &world.host_dir, &options.api_rp_id, &options.api_origin, )?); let runtime = crate::process::Runtime::new_with_hooks( crate::world::World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), }, cred.clone(), crate::process::default_script(), Some(api.clone()), )?; let (sender, receiver) = crate::harness::input::channel(); api.attach_harness(sender.clone()); let listener = Listener::start_with_api(api, options)?; runtime .network_policy() .deny_host_port(listener.address().port()); 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_with_input( world, Arc::new(runtime), cred, sender, receiver, )) }) .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; drop(listener); result }