//! The hook and native listener share ceremony state without exposing the native listener to guests. use sausage::{ api::{Api, Listener, Options as ApiOptions}, cred::{Caller, Cred}, exec::{Command, Exec, Options}, process::Runtime, world::World, }; use std::{ io::{Read, Write}, sync::Arc, }; #[path = "hook_guest.rs"] mod hook_guest; #[tokio::test] async fn one_api_for_pages_and_guest_requests() -> anyhow::Result<()> { use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey}; let dir = tempfile::tempdir()?; let world = World::open(dir.path())?; std::fs::create_dir_all(dir.path().join("world/bin"))?; std::fs::copy( hook_guest::component(), dir.path().join("world/bin/guest.wasm"), )?; let cred = Arc::new(Cred::open(&world.host_dir)?); let origin = "http://localhost:8081"; let api = Arc::new(Api::open( cred.clone(), &world.host_dir, "localhost", origin, )?); let runtime = Runtime::new_with_hooks( world, cred, sausage::process::default_script(), Some(api.clone()), )?; let listener = Listener::start_with_api( api.clone(), ApiOptions { api_listen: "127.0.0.1:0".parse()?, api_rp_id: "localhost".into(), api_origin: origin.into(), }, )?; runtime .network_policy() .deny_host_port(listener.address().port()); let response = sausage::api::handle( &api, Caller { program: "/bin/guest.wasm".into(), pid: 1, conversations: vec![], }, http::Request::post("http://host.sausage/cred/passkeys/register/start") .body(bytes::Bytes::from_static(b"{\"label\":\"shared\"}"))?, ) .await; assert_eq!(response.status(), 200); let challenge: serde_json::Value = serde_json::from_slice(response.body())?; let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true)); let registration = authenticator.do_registration( url::Url::parse(origin)?, serde_json::from_value(challenge["options"].clone())?, )?; let body = serde_json::to_vec( &serde_json::json!({"challenge_id":challenge["id"], "response": registration}), )?; let mut connection = std::net::TcpStream::connect(listener.address())?; write!( connection, "POST /cred/passkeys/register/finish HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: {}\r\n\r\n", body.len() )?; connection.write_all(&body)?; let mut reply = String::new(); connection.read_to_string(&mut reply)?; assert!(reply.starts_with("HTTP/1.1 200")); for (uri, expected) in [ ("http://host.sausage/cred/passkeys".to_owned(), "shared"), (format!("http://{}/whoami", listener.address()), "error"), ] { let mut out = Vec::new(); let code = runtime .exec( Command::Argv(vec!["/bin/guest.wasm".into(), uri, "GET".into()]), Options { cwd: "/".into(), timeout: Some(std::time::Duration::from_secs(30)), ..Default::default() }, &mut |_, b| out.extend_from_slice(b), ) .await?; assert_eq!(code, 0); assert!(String::from_utf8(out)?.contains(expected)); } Ok(()) }