Merge v3 with the generic web server in the shared workspace
This commit is contained in:
@@ -56,7 +56,7 @@ async fn page(api: &Api, path: &str) -> Result<String> {
|
||||
);
|
||||
let text = std::str::from_utf8(response.body())?;
|
||||
assert!(!text.contains(SECRET));
|
||||
assert!(text.contains("/app.js"));
|
||||
assert!(text.contains("app.js"));
|
||||
Ok(text.into())
|
||||
}
|
||||
fn item(cred: &Cred, name: &str) -> Result<cred::items::Item> {
|
||||
@@ -338,7 +338,11 @@ async fn encrypt(api: &Api, public_key: Value) -> Result<Envelope> {
|
||||
..Default::default()
|
||||
});
|
||||
runtime.execute_script("browser.js","globalThis.crypto = Deno.core.loadExtScript('ext:deno_crypto/00_crypto.js').crypto; globalThis.TextEncoder = Deno.core.loadExtScript('ext:deno_web/08_text_encoding.js').TextEncoder; globalThis.document = {querySelector: () => ({addEventListener() {}})};")?;
|
||||
runtime.execute_script("app.js", std::str::from_utf8(script.body())?.to_string())?;
|
||||
runtime.execute_script(
|
||||
"app.js",
|
||||
std::str::from_utf8(script.body())?
|
||||
.replace("import.meta.url", "'http://localhost:8080/api/app.js'"),
|
||||
)?;
|
||||
runtime.execute_script("encrypt.js",format!("globalThis.done = encrypt({public_key},'{SECRET}').then(result => globalThis.envelope = JSON.stringify(result.envelope));"))?;
|
||||
runtime.run_event_loop(Default::default()).await?;
|
||||
let value = runtime.execute_script("result.js", "envelope")?;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
//! Host pages remain reachable. World config replaces listener flags on the CLI.
|
||||
use super::api_transport::{Listener, Options};
|
||||
// Host pages remain reachable. World config replaces listener flags on the CLI.
|
||||
use anyhow::Result;
|
||||
use sausage::{
|
||||
api::{Listener, Options},
|
||||
cred::Cred,
|
||||
};
|
||||
use sausage::cred::Cred;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
sync::Arc,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//! 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
-26
@@ -19,13 +19,13 @@ async fn request(port: u16, path: &str, body: Option<&str>) -> anyhow::Result<(u
|
||||
let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
|
||||
let method = if body.is_some() { "POST" } else { "GET" };
|
||||
let body = body.unwrap_or("");
|
||||
stream.write_all(format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",body.len()).as_bytes()).await?;
|
||||
stream.write_all(format!("{method} {path} HTTP/1.1\r\nHost: localhost:{port}\r\nOrigin: http://localhost:{port}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",body.len()).as_bytes()).await?;
|
||||
let mut bytes = Vec::new();
|
||||
stream.read_to_end(&mut bytes).await?;
|
||||
let end = bytes
|
||||
.windows(4)
|
||||
.position(|s| s == b"\r\n\r\n")
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing HTTP headers"))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing HTTP headers for {path}"))?
|
||||
+ 4;
|
||||
let header = std::str::from_utf8(&bytes[..end])?;
|
||||
let status = header.split_whitespace().nth(1).unwrap().parse()?;
|
||||
@@ -33,7 +33,7 @@ async fn request(port: u16, path: &str, body: Option<&str>) -> anyhow::Result<(u
|
||||
}
|
||||
#[test]
|
||||
fn page_rendering() -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland/chat");
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland/web");
|
||||
let page = std::fs::read_to_string(source.join("index.html"))?;
|
||||
let script = page
|
||||
.split_once("<script>")
|
||||
@@ -79,7 +79,7 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
"--release",
|
||||
"--locked",
|
||||
"-p",
|
||||
"sausage-chat",
|
||||
"sausage-web",
|
||||
"-p",
|
||||
"sausage-init",
|
||||
"--target",
|
||||
@@ -96,30 +96,36 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
let units = bundle.path().join("etc/systemd/system");
|
||||
std::fs::create_dir_all(units.join("multi-user.target.wants"))?;
|
||||
std::fs::copy(
|
||||
source.join("userland/chat/chat.service"),
|
||||
units.join("chat.service"),
|
||||
source.join("userland/web/web.service"),
|
||||
units.join("web.service"),
|
||||
)?;
|
||||
std::fs::write(units.join("multi-user.target.wants/chat.service"), "")?;
|
||||
std::fs::write(units.join("multi-user.target.wants/web.service"), "")?;
|
||||
std::fs::create_dir_all(bundle.path().join("bin"))?;
|
||||
for binary in ["chat", "init"] {
|
||||
for binary in ["web", "init"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
bundle.path().join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
crate::common::copy_tree(
|
||||
&source.join("userland/chat"),
|
||||
&bundle.path().join("src/sausage/userland/chat"),
|
||||
&source.join("userland/web"),
|
||||
&bundle.path().join("src/sausage/userland/web"),
|
||||
)?;
|
||||
let www = bundle.path().join("srv/www");
|
||||
std::fs::create_dir_all(&www)?;
|
||||
std::fs::copy(
|
||||
source.join("userland/web/index.html"),
|
||||
www.join("index.html"),
|
||||
)?;
|
||||
let dir = tempfile::tempdir()?;
|
||||
drop(World::open_with_userland(dir.path(), Some(bundle.path()))?);
|
||||
let port = std::net::TcpListener::bind("127.0.0.1:0")?
|
||||
.local_addr()?
|
||||
.port();
|
||||
let seeded = dir.path().join("world/etc/systemd/system/chat.service");
|
||||
let seeded = dir.path().join("world/etc/systemd/system/web.service");
|
||||
let unit = std::fs::read_to_string(&seeded)?.replace(
|
||||
"ExecStart=/bin/chat.wasm",
|
||||
&format!("ExecStart=/bin/chat.wasm --listen 127.0.0.1:{port}"),
|
||||
"ExecStart=/bin/web.wasm",
|
||||
&format!("ExecStart=/bin/web.wasm --listen 127.0.0.1:{port}"),
|
||||
);
|
||||
std::fs::write(&seeded, &unit)?;
|
||||
let world = World::open_with_userland(dir.path(), Some(bundle.path()))?;
|
||||
@@ -130,12 +136,12 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
);
|
||||
assert!(
|
||||
dir.path()
|
||||
.join("world/src/sausage/userland/chat/Cargo.toml")
|
||||
.join("world/src/sausage/userland/web/Cargo.toml")
|
||||
.is_file()
|
||||
);
|
||||
let cred = Arc::new(Cred::open(&world.host_dir)?);
|
||||
let caller = Caller {
|
||||
program: "/bin/chat.wasm".into(),
|
||||
program: "/bin/web.wasm".into(),
|
||||
pid: 1,
|
||||
conversations: vec!["1".into()],
|
||||
};
|
||||
@@ -173,7 +179,7 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
cred.clone(),
|
||||
&world.host_dir,
|
||||
"localhost",
|
||||
"http://localhost:8090",
|
||||
"http://localhost:8080",
|
||||
)?);
|
||||
let (sender, receiver) = input::channel();
|
||||
api.attach_harness(sender.clone());
|
||||
@@ -216,8 +222,17 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
tokio::task::yield_now().await;
|
||||
};
|
||||
assert!(String::from_utf8_lossy(&page).contains("Sausage chat"));
|
||||
std::fs::write(dir.path().join("world/srv/www/index.html"), "Live edit")?;
|
||||
assert_eq!(request(port, "/", None).await?.1, b"Live edit");
|
||||
assert_eq!(request(port, "/missing", None).await?.0, 404);
|
||||
let (status, ndjson) = request(port, "/api/cargo", Some("{}")).await?;
|
||||
assert_eq!(status, 200);
|
||||
assert!(std::str::from_utf8(&ndjson)?.lines().count() >= 2);
|
||||
for line in std::str::from_utf8(&ndjson)?.lines() {
|
||||
let _: serde_json::Value = serde_json::from_str(line)?;
|
||||
}
|
||||
let root = loop {
|
||||
let (status, body) = request(port, "/api/conversations", None).await?;
|
||||
let (status, body) = request(port, "/api/harness/conversations", None).await?;
|
||||
let value: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
if status == 200
|
||||
&& let Some(id) = value["conversations"][0]["id"].as_u64()
|
||||
@@ -236,7 +251,7 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
request(
|
||||
port,
|
||||
&format!("/api/conversations/{root}/submit"),
|
||||
&format!("/api/harness/conversations/{root}/submit"),
|
||||
Some(&submit)
|
||||
)
|
||||
.await?
|
||||
@@ -247,7 +262,7 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let (status, body) = request(
|
||||
port,
|
||||
&format!("/api/conversations/{root}/entries?after=0"),
|
||||
&format!("/api/harness/conversations/{root}/entries?after=0"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -268,18 +283,109 @@ async fn seeded_chat_case(open_connection: bool) -> anyhow::Result<()> {
|
||||
anyhow::ensure!(std::time::Instant::now() < deadline, "Reply did not appear");
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
let (status, body) = request(port, "/api/credential-requests", None).await?;
|
||||
let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
|
||||
stream.write_all(format!("GET /api/harness/conversations/{root}/stream?after=0 HTTP/1.1\r\nHost: localhost:{port}\r\nConnection: close\r\n\r\n").as_bytes()).await?;
|
||||
let mut streamed = Vec::new();
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
let mut chunk = [0; 4096];
|
||||
let count = stream.read(&mut chunk).await?;
|
||||
anyhow::ensure!(count > 0, "Stream closed before replay");
|
||||
streamed.extend_from_slice(&chunk[..count]);
|
||||
if String::from_utf8_lossy(&streamed).contains("Chat test reply") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await??;
|
||||
assert!(String::from_utf8_lossy(&streamed).contains("application/x-ndjson"));
|
||||
drop(stream);
|
||||
let (status, body) = request(port, "/api/harness/credential-requests", None).await?;
|
||||
assert_eq!(status, 200);
|
||||
let value: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
assert!(
|
||||
value["requests"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|r| r["id"] == pending
|
||||
&& r["approvalUrl"] == format!("http://localhost:8090/approve/{pending}"))
|
||||
value["requests"].as_array().unwrap().iter().any(
|
||||
|r| r["id"] == pending && r["approvalUrl"] == format!("/api/approve/{pending}")
|
||||
)
|
||||
);
|
||||
assert_eq!(request(port, "/api/unknown", None).await?.0, 404);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&request(port, "/api/register", None).await?.1)
|
||||
.contains("Register a passkey")
|
||||
);
|
||||
use sausage::cred::webauthn::Challenge;
|
||||
use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey};
|
||||
use webauthn_rs::prelude::{CreationChallengeResponse, RequestChallengeResponse};
|
||||
let origin = url::Url::parse(&format!("http://localhost:{port}"))?;
|
||||
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
|
||||
let (status, start) = request(
|
||||
port,
|
||||
"/api/cred/passkeys/register/start",
|
||||
Some(r#"{"label":"Mounted browser"}"#),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(status, 200, "{}", String::from_utf8_lossy(&start));
|
||||
let start: Challenge<CreationChallengeResponse> = serde_json::from_slice(&start)?;
|
||||
let registration = authenticator.do_registration(origin.clone(), start.options)?;
|
||||
let finish =
|
||||
serde_json::json!({"challenge_id":start.id,"response":registration}).to_string();
|
||||
assert_eq!(
|
||||
request(port, "/api/cred/passkeys/register/finish", Some(&finish))
|
||||
.await?
|
||||
.0,
|
||||
200
|
||||
);
|
||||
assert_eq!(
|
||||
request(
|
||||
port,
|
||||
"/api/cred/passkeys/register/start",
|
||||
Some(r#"{"label":"Ungated"}"#)
|
||||
)
|
||||
.await?
|
||||
.0,
|
||||
403
|
||||
);
|
||||
assert!(
|
||||
String::from_utf8_lossy(
|
||||
&request(port, &format!("/api/approve/{pending}"), None)
|
||||
.await?
|
||||
.1
|
||||
)
|
||||
.contains("Approve")
|
||||
);
|
||||
let choice = r#"{"decision":"approve","overrides":{}}"#;
|
||||
assert_ne!(
|
||||
request(
|
||||
port,
|
||||
&format!("/api/cred/requests/{pending}/decide"),
|
||||
Some(choice)
|
||||
)
|
||||
.await?
|
||||
.0,
|
||||
200
|
||||
);
|
||||
let challenge: Challenge<RequestChallengeResponse> = serde_json::from_slice(
|
||||
&request(
|
||||
port,
|
||||
&format!("/api/cred/requests/{pending}/challenge"),
|
||||
Some(choice),
|
||||
)
|
||||
.await?
|
||||
.1,
|
||||
)?;
|
||||
let assertion = authenticator.do_authentication(origin, challenge.options)?;
|
||||
let decision = serde_json::json!({"challenge_id":challenge.id,"assertion":assertion,"decision":"approve","overrides":{}}).to_string();
|
||||
assert_eq!(
|
||||
request(
|
||||
port,
|
||||
&format!("/api/cred/requests/{pending}/decide"),
|
||||
Some(&decision)
|
||||
)
|
||||
.await?
|
||||
.0,
|
||||
200
|
||||
);
|
||||
sender.send(Input::Stop)?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ pub fn write_config(dir: &Path, standing: &str, roster: serde_json::Value) -> an
|
||||
serde_yaml_ng::from_str(include_str!("../../../config/config.yaml"))?;
|
||||
config["standing"] = serde_json::json!(standing);
|
||||
config["roster"] = roster;
|
||||
config["listener"]["address"] = serde_json::json!("127.0.0.1:0");
|
||||
let folder = dir.join("world/etc/sausage");
|
||||
std::fs::create_dir_all(&folder)?;
|
||||
std::fs::write(
|
||||
|
||||
@@ -78,7 +78,6 @@ async fn init_seeds_config_tools_and_one_source_commit() -> anyhow::Result<()> {
|
||||
let world = World::open(&destination)?;
|
||||
assert!(destination.join("world/bin/git").exists());
|
||||
let config = sausage::harness::config::Config::read(&world.root).await?;
|
||||
assert_eq!(config.listener.origin, "http://localhost:8090");
|
||||
assert_eq!(config.proxy.base_url, "https://proxy.test");
|
||||
assert_eq!(config.proxy.api_key, "fixture-key");
|
||||
let source = destination.join("world/src/sausage");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The hook and native listener share ceremony state without exposing the native listener to guests.
|
||||
use super::api_transport::{Listener, Options as ApiOptions};
|
||||
// The hook and native listener share ceremony state without exposing the native listener to guests.
|
||||
use sausage::{
|
||||
api::{Api, Listener, Options as ApiOptions},
|
||||
api::Api,
|
||||
cred::{Caller, Cred},
|
||||
exec::{Command, Exec, Options},
|
||||
process::Runtime,
|
||||
@@ -56,6 +57,7 @@ async fn one_api_for_pages_and_guest_requests() -> anyhow::Result<()> {
|
||||
conversations: vec![],
|
||||
},
|
||||
http::Request::post("http://host.sausage/cred/passkeys/register/start")
|
||||
.header("origin", origin)
|
||||
.body(bytes::Bytes::from_static(b"{\"label\":\"shared\"}"))?,
|
||||
)
|
||||
.await;
|
||||
@@ -72,7 +74,7 @@ async fn one_api_for_pages_and_guest_requests() -> anyhow::Result<()> {
|
||||
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",
|
||||
"POST /cred/passkeys/register/finish HTTP/1.1\r\nHost: localhost\r\nOrigin: {origin}\r\nConnection: close\r\nContent-Length: {}\r\n\r\n",
|
||||
body.len()
|
||||
)?;
|
||||
connection.write_all(&body)?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Host integration tests share one executable. Modules retain their original test registrations.
|
||||
mod api;
|
||||
mod api_auth;
|
||||
mod api_transport;
|
||||
mod api_listener;
|
||||
mod automations;
|
||||
mod builder;
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ fn bundle() -> anyhow::Result<PathBuf> {
|
||||
|| !bundle.join("etc/sausage/config.yaml").exists()
|
||||
|| std::fs::read_to_string(&stamp).ok().as_deref() != Some(key.as_str())
|
||||
{
|
||||
if stamp.exists() {
|
||||
std::fs::remove_file(&stamp)?;
|
||||
if bundle.exists() {
|
||||
std::fs::remove_dir_all(&bundle)?;
|
||||
}
|
||||
assert!(
|
||||
std::process::Command::new("nu")
|
||||
|
||||
+10
-6
@@ -1,8 +1,9 @@
|
||||
//! P2 exit rehearsal: native host pages, software passkey, durable bash tools, and real SSH Git push.
|
||||
use super::api_transport::{Listener, Options};
|
||||
// P2 exit rehearsal: native host pages, software passkey, durable bash tools, and real SSH Git push.
|
||||
use anyhow::{Result, ensure};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use sausage::{
|
||||
api::{Api, Listener, Options},
|
||||
api::Api,
|
||||
cred::{Cred, entry::Envelope, webauthn::Challenge},
|
||||
world::World,
|
||||
};
|
||||
@@ -49,8 +50,8 @@ impl Browser {
|
||||
connection.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||
write!(
|
||||
connection,
|
||||
"{method} {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
bytes.len()
|
||||
"{method} {path} HTTP/1.1\r\nHost: localhost\r\nOrigin: {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
self.origin, bytes.len()
|
||||
)?;
|
||||
connection.write_all(&bytes)?;
|
||||
let mut response = String::new();
|
||||
@@ -146,7 +147,10 @@ impl Browser {
|
||||
..Default::default()
|
||||
});
|
||||
runtime.execute_script("browser.js", "globalThis.crypto = Deno.core.loadExtScript('ext:deno_crypto/00_crypto.js').crypto; globalThis.TextEncoder = Deno.core.loadExtScript('ext:deno_web/08_text_encoding.js').TextEncoder; globalThis.document = {querySelector: () => ({addEventListener() {}})};")?;
|
||||
runtime.execute_script("app.js", script)?;
|
||||
runtime.execute_script(
|
||||
"app.js",
|
||||
script.replace("import.meta.url", "'http://localhost:8080/api/app.js'"),
|
||||
)?;
|
||||
runtime.execute_script("entry.js", format!("encrypt({},{}).then(value => globalThis.envelope = JSON.stringify(value.envelope))", public["public_key"], serde_json::to_string(secret)?))?;
|
||||
runtime.run_event_loop(Default::default()).await?;
|
||||
let result = runtime.execute_script("result.js", "envelope")?;
|
||||
@@ -386,7 +390,7 @@ async fn request_approve_fetch_and_ssh_push() -> Result<()> {
|
||||
ensure!(
|
||||
browser
|
||||
.raw("GET", "/register", Value::Null)?
|
||||
.contains("/app.js"),
|
||||
.contains("src=\"app.js\""),
|
||||
"registration page missing"
|
||||
);
|
||||
let start: Challenge<CreationChallengeResponse> = serde_json::from_value(browser.json(
|
||||
|
||||
Reference in New Issue
Block a user