Files
sausage/host/tests/it/api_transport.rs
T

102 lines
4.0 KiB
Rust

//! 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<tiny_http::Server>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl Listener {
pub fn start(cred: Arc<Cred>, host_dir: &Path, options: Options) -> anyhow::Result<Self> {
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<Api>, options: Options) -> anyhow::Result<Self> {
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();
}
}
}