use super::{
ipc::WindowsIpc,
ipc_policy::{self, Control, HelloCall, MAX_CONTROL_UNITS},
protocol::read_pwstr_bounded,
};
#[cfg(feature = "local-server")]
use crate::ipc::native_data::{DataRequest, NativeData, MAX_MESSAGE_UNITS};
use crate::ipc::{Admission, IpcError, IpcErrorCode, NativeControl, SessionInfo};
use serde_json::Value;
use std::rc::Rc;
use webview2_com::{
CoTaskMemPWSTR, ExecuteScriptCompletedHandler,
Microsoft::Web::WebView2::Win32::ICoreWebView2WebMessageReceivedEventArgs,
};
use windows::{
core::{Error as WindowsError, Result as WindowsResult},
Win32::Foundation::E_FAIL,
};
impl WindowsIpc {
pub(super) fn publish_retirement(&self, code: IpcErrorCode) {
let outgoing = {
let document = self.document.borrow();
document
.proof
.as_ref()
.filter(|proof| document.accepts(proof))
.cloned()
.zip(document.generation)
};
let Some((proof, generation)) = outgoing else {
return;
};
let control = NativeControl::Closed { generation, code };
let script = match crate::native_ipc::control_script(&proof, control) {
Ok(script) => CoTaskMemPWSTR::from(script.as_str()),
Err(error) => {
eprintln!("WebUI: could not encode document retirement control: {error}");
return;
}
};
let completion = ExecuteScriptCompletedHandler::create(Box::new(|result, _| {
if let Err(error) = result {
eprintln!("WebUI: document retirement delivery failed: {error}");
}
Ok(())
}));
if let Err(error) = unsafe {
self.webview
.ExecuteScript(*script.as_ref().as_pcwstr(), &completion)
} {
eprintln!("WebUI: could not queue document retirement: {error}");
}
}
pub fn message(
self: &Rc<Self>,
args: &ICoreWebView2WebMessageReceivedEventArgs,
) -> WindowsResult<()> {
let source = read_pwstr_bounded(8192, |out| unsafe { args.Source(out) })?;
if !self.trusted_url(&source) || !self.trusted_source() {
return Ok(());
}
#[cfg(feature = "local-server")]
if self.is_local() {
let Ok(raw) = read_pwstr_bounded(MAX_MESSAGE_UNITS, |out| unsafe {
args.WebMessageAsJson(out)
}) else {
return Ok(());
};
if raw.len() > MAX_CONTROL_UNITS {
if let Ok(data) = serde_json::from_str::<DataRequest>(&raw) {
self.data_message(data);
}
return Ok(());
}
if let Ok(data) = serde_json::from_str::<DataRequest>(&raw) {
self.data_message(data);
return Ok(());
}
}
let Ok(raw) = read_pwstr_bounded(MAX_CONTROL_UNITS, |out| unsafe {
args.WebMessageAsJson(out)
}) else {
return Ok(());
};
if raw.len() > MAX_CONTROL_UNITS {
return Ok(());
}
let Ok(control) = serde_json::from_str::<Control>(&raw) else {
return Ok(());
};
match control {
Control::Disconnect { generation, token } => {
let Some(generation) = ipc_policy::decimal(&generation) else {
return Ok(());
};
if ipc_policy::nonce(&token).is_some()
&& self.document.borrow().generation == Some(generation)
&& self
.bridge
.disconnect_authenticated(generation, &token)
.is_ok()
{
self.disconnect_document();
}
}
control => {
if let Some(call) = control.into_hello(self.is_local()) {
self.hello(call);
}
}
}
Ok(())
}
#[cfg(feature = "local-server")]
fn data_message(self: &Rc<Self>, request: DataRequest) {
let (navigation, session) = {
let document = self.document.borrow();
let Some(proof) = document
.proof
.as_ref()
.filter(|proof| document.accepts(proof))
else {
return;
};
let (Some(generation), Some(token), Some(limits)) = (
document.generation,
document.token.as_ref(),
document.limits.as_ref(),
) else {
return;
};
(
proof.navigation,
SessionInfo {
generation,
token: token.clone(),
limits: limits.clone(),
},
)
};
let weak = Rc::downgrade(self);
let cursor = Rc::clone(&self.data);
let bridge = self.bridge.clone();
let result = self.spawn(async move {
let response =
NativeData::exchange(&cursor, bridge, navigation, session, request).await;
let Some(ipc) = weak
.upgrade()
.filter(|ipc| ipc.current(navigation) && ipc.trusted_source())
else {
return;
};
if ipc.arm_data_deadline().is_err() {
ipc.transport_failed(IpcErrorCode::Transport);
return;
}
if let Ok(json) = serde_json::to_vec(&response) {
if json.len() <= MAX_MESSAGE_UNITS && ipc.post_json(&json).is_err() {
ipc.transport_failed(IpcErrorCode::Transport);
}
} else {
ipc.transport_failed(IpcErrorCode::Transport);
}
});
if result.is_err() {
self.transport_failed(IpcErrorCode::Overloaded);
}
}
fn hello(self: &Rc<Self>, call: HelloCall) {
if !self.document.borrow_mut().begin_hello(&call.proof) {
return;
}
match super::ipc_deadline::HelloDeadline::start(self.hwnd, self.wake.cookie, &call) {
Ok(deadline) => *self.hello_deadline.borrow_mut() = Some(deadline),
Err(error) => {
self.transport_failed(error.code);
return;
}
}
let admission = Admission {
hello: call.hello.clone(),
proof: call.proof.clone(),
};
let bridge = self.bridge.clone();
let weak = Rc::downgrade(self);
let result = self.spawn(async move {
let result = bridge.admit(admission).await;
let Some(ipc) = weak.upgrade() else {
return;
};
ipc.expire_hello(ipc.wake.cookie);
if !ipc.document.borrow().accepts(&call.proof) {
if let Ok(session) = result {
let _ = ipc
.bridge
.disconnect_authenticated(session.generation, &session.token);
}
return;
}
ipc.cancel_hello_deadline();
ipc.document.borrow_mut().hello_pending = false;
ipc.hello_reply(&call, result);
});
if let Err(error) = result {
self.transport_failed(error.code);
}
}
pub(super) fn hello_reply(&self, call: &HelloCall, result: Result<SessionInfo, IpcError>) {
let proof = &call.proof;
if !self.document.borrow().accepts(proof) {
if let Ok(session) = result {
let _ = self
.bridge
.disconnect_authenticated(session.generation, &session.token);
}
return;
}
let response = if self.is_local() {
crate::native_ipc::hello_reply_json_local(call, &result)
} else {
crate::native_ipc::hello_reply_json(call, &result)
};
if let Ok(session) = &result {
let mut document = self.document.borrow_mut();
document.generation = Some(session.generation);
document.token = Some(session.token.clone());
document.max_frame_bytes = Some(session.limits.max_frame_bytes);
document.limits = Some(session.limits.clone());
}
if response
.map_err(|_| ())
.and_then(|response| self.post_json(&response).map_err(|_| ()))
.is_err()
{
if let Ok(session) = result {
let _ = self
.bridge
.disconnect_authenticated(session.generation, &session.token);
}
if self.document.borrow().accepts(proof) {
self.transport_failed(IpcErrorCode::Transport);
}
}
}
pub(super) fn post(&self, message: &Value) -> WindowsResult<()> {
let json = message.to_string();
self.post_json(json.as_bytes())
}
fn post_json(&self, json: &[u8]) -> WindowsResult<()> {
let json = std::str::from_utf8(json).map_err(|_| WindowsError::from(E_FAIL))?;
let json = CoTaskMemPWSTR::from(json);
unsafe {
self.webview
.PostWebMessageAsJson(*json.as_ref().as_pcwstr())
}
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
#[test]
fn success_and_failure_echo_all_proof_fields_without_payload_arrays() {
let proof = crate::ipc::DocumentActivation {
navigation: 9,
document_nonce: [1; 16],
challenge: [2; 16],
};
let session = SessionInfo {
generation: 7,
token: "03".repeat(16),
limits: crate::ipc::IpcLimits::default(),
};
let error = ipc_policy::error(IpcErrorCode::DeadlineExceeded);
let call = HelloCall {
call_id: "1".into(),
proof,
hello: crate::ipc::Hello {
wire_version: 2,
contract_name: "test".into(),
contract_major: 1,
schema_hash: "0".repeat(64),
},
};
for result in [Ok(session), Err(error)] {
let response: Value = serde_json::from_slice(
&crate::native_ipc::hello_reply_json(&call, &result).unwrap(),
)
.unwrap();
assert_eq!(response["kind"], "helloResult");
assert_eq!(response["callId"], "1");
assert_eq!(response["navigation"], "9");
assert_eq!(response["documentNonce"], "01".repeat(16));
assert_eq!(response["challenge"], "02".repeat(16));
if result.is_ok() {
assert_eq!(response["generation"], "7");
assert!(response.get("error").is_none());
} else {
assert_eq!(response["error"]["code"], "deadline-exceeded");
assert!(response.get("token").is_none());
}
}
}
}