//! The host HTTP listener and console startup share one owned API and credentials store. use sausage::api::Api; use sausage::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:8090")] 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:8090")] 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 mut builder = http::Request::builder() .method(request.method().as_str()) .uri(request.url()); for header in request.headers() { builder = builder.header(header.field.as_str().as_str(), header.value.as_str()); } let request = builder.body(bytes::Bytes::from(body))?; Ok(runtime.block_on(sausage::api::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(); } } }