use std::{ffi::c_void, rc::Rc};
use webview2_com::Microsoft::Web::WebView2::Win32::{
ICoreWebView2Environment, ICoreWebView2HttpRequestHeaders, ICoreWebView2WebResourceRequest,
ICoreWebView2WebResourceRequestedEventArgs,
};
use windows::core::Result as WindowsResult;
use super::{
ipc::WindowsIpc,
ipc_body::read_body,
ipc_policy::error,
protocol::{create_webview_response, read_pwstr_bounded},
};
use crate::{
ipc::{wire::WireError, IpcError, IpcErrorCode, OwnedIpcHttpRequest},
DesktopHttpMethod, DesktopProtocolResponse,
};
struct OnDrop<F: FnOnce()>(Option<F>);
impl<F: FnOnce()> Drop for OnDrop<F> {
fn drop(&mut self) {
if let Some(complete) = self.0.take() {
complete();
}
}
}
pub(super) fn error_response(code: IpcErrorCode) -> DesktopProtocolResponse {
let status = match code {
IpcErrorCode::PermissionDenied => 401,
IpcErrorCode::NotReady | IpcErrorCode::Navigated => 409,
IpcErrorCode::PayloadTooLarge => 413,
IpcErrorCode::Overloaded => 429,
IpcErrorCode::Closed | IpcErrorCode::Transport => 503,
_ => 400,
};
let body = WireError {
code: code.as_str().into(),
message: code.as_str().into(),
help: "reload the trusted document or reduce IPC work".into(),
application_code: String::new(),
}
.encode_to_vec();
DesktopProtocolResponse::new(status, "application/x-protobuf", body)
}
pub(super) fn handle(
ipc: &Rc<WindowsIpc>,
environment: &ICoreWebView2Environment,
args: &ICoreWebView2WebResourceRequestedEventArgs,
path: &str,
) -> WindowsResult<()> {
let navigation = ipc.navigation();
unsafe {
args.SetResponse(&create_webview_response(
environment,
error_response(IpcErrorCode::Overloaded),
)?)?;
}
let request = match prepare(ipc, args, path) {
Ok(request) => request,
Err(err) => {
unsafe {
args.SetResponse(&create_webview_response(
environment,
error_response(err.code),
)?)?;
}
return Ok(());
}
};
let deferral = unsafe { args.GetDeferral()? };
let completion = OnDrop(Some(move || {
let _ = unsafe { deferral.Complete() };
}));
let bridge = ipc.bridge.clone();
let weak = Rc::downgrade(ipc);
let args = args.clone();
let environment = environment.clone();
let result = ipc.spawn(async move {
let _completion = completion;
let response = bridge
.submit(request)
.await
.unwrap_or_else(|err| error_response(err.code));
let Some(ipc) = weak.upgrade() else {
return;
};
if !ipc.current(navigation) {
return;
}
if let Ok(response) = create_webview_response(&environment, response) {
let _ = unsafe { args.SetResponse(&response) };
}
});
if let Err(err) = result {
if err.code != IpcErrorCode::Overloaded {
ipc.transport_failed(err.code);
}
}
Ok(())
}
fn prepare(
ipc: &WindowsIpc,
args: &ICoreWebView2WebResourceRequestedEventArgs,
path: &str,
) -> Result<OwnedIpcHttpRequest, IpcError> {
if let Some(code) = ipc.document.borrow().request_error() {
return Err(error(code));
}
let max_bytes = ipc.bridge.max_request_body_bytes(path)?;
if path.len() > 128 || path.contains(['?', '#']) {
return Err(error(IpcErrorCode::InvalidFrame));
}
let request = unsafe { args.Request() }.map_err(|_| error(IpcErrorCode::Transport))?;
let method = read_pwstr_bounded(16, |out| unsafe { request.Method(out) })
.map_err(|_| error(IpcErrorCode::InvalidFrame))?;
let method = DesktopHttpMethod::parse(&method);
let headers = unsafe { request.Headers() }.map_err(|_| error(IpcErrorCode::Transport))?;
if header(&headers, "Origin", 256)?.is_some_and(|origin| origin != super::APP_ORIGIN) {
return Err(error(IpcErrorCode::PermissionDenied));
}
let token = header(&headers, "X-WebUI-Ipc-Session", 32)?
.ok_or_else(|| error(IpcErrorCode::PermissionDenied))?;
if super::ipc_policy::nonce(&token).is_none() {
return Err(error(IpcErrorCode::PermissionDenied));
}
let claimed = header(&headers, "Content-Length", 20)?
.map(|value| {
value
.parse::<usize>()
.map_err(|_| error(IpcErrorCode::InvalidFrame))
})
.transpose()?;
let mut input_permit = None;
let body = request_body(&request, max_bytes, claimed, |bytes| {
if let Some(permit) = &mut input_permit {
crate::ipc::IpcInputPermit::try_grow(permit, bytes)
} else {
input_permit = Some(ipc.bridge.reserve_input(bytes)?);
Ok(())
}
})?;
let input_permit = match input_permit {
Some(permit) => permit,
None => ipc.bridge.reserve_input(0)?,
};
Ok(OwnedIpcHttpRequest {
navigation: ipc.navigation(),
method,
path: path.into(),
token,
body,
input_permit,
})
}
fn header(
headers: &ICoreWebView2HttpRequestHeaders,
name: &str,
max: usize,
) -> Result<Option<String>, IpcError> {
let name = webview2_com::CoTaskMemPWSTR::from(name);
let mut present = windows::core::BOOL::default();
unsafe { headers.Contains(*name.as_ref().as_pcwstr(), &mut present) }
.map_err(|_| error(IpcErrorCode::Transport))?;
if !present.as_bool() {
return Ok(None);
}
read_pwstr_bounded(max, |out| unsafe {
headers.GetHeader(*name.as_ref().as_pcwstr(), out)
})
.map(Some)
.map_err(|_| error(IpcErrorCode::InvalidFrame))
}
fn request_body(
request: &ICoreWebView2WebResourceRequest,
max_bytes: usize,
claimed: Option<usize>,
reserve: impl FnMut(usize) -> Result<(), IpcError>,
) -> Result<Vec<u8>, IpcError> {
let stream = super::request::content(request).map_err(|_| error(IpcErrorCode::Transport))?;
read_body(
max_bytes,
claimed,
|buffer| {
let Some(stream) = &stream else {
return Ok(0);
};
let mut count = 0;
let size =
u32::try_from(buffer.len()).map_err(|_| error(IpcErrorCode::PayloadTooLarge))?;
unsafe { stream.Read(buffer.as_mut_ptr().cast::<c_void>(), size, Some(&mut count)) }
.ok()
.map_err(|_| error(IpcErrorCode::Transport))?;
usize::try_from(count).map_err(|_| error(IpcErrorCode::Transport))
},
reserve,
)
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::super::ipc_body::check_claim;
use super::*;
use crate::ipc::DEFAULT_MAX_IPC_PAYLOAD_BYTES as MAX_BODY;
use std::cell::Cell;
#[test]
fn body_cap_and_reservation_are_based_on_actual_bytes() {
for size in [0, 1, MAX_BODY, MAX_BODY + 1] {
let mut remaining = size;
let reserved = Cell::new(0);
let result = read_body(
MAX_BODY,
None,
|buffer| {
let count = remaining.min(buffer.len());
remaining -= count;
buffer[..count].fill(7);
Ok(count)
},
|bytes| {
reserved.set(reserved.get() + bytes);
Ok(())
},
);
if size <= MAX_BODY {
let body = result.unwrap();
assert_eq!(body.len(), size);
assert_eq!(body.capacity(), reserved.get());
} else {
assert_eq!(result.unwrap_err().code, IpcErrorCode::PayloadTooLarge);
}
assert!(reserved.get() <= MAX_BODY);
}
}
#[test]
fn failed_credit_does_not_grow_or_read_again() {
let reads = Cell::new(0);
let result = read_body(
MAX_BODY,
None,
|buffer| {
reads.set(reads.get() + 1);
buffer[0] = 1;
Ok(1)
},
|_| Err(error(IpcErrorCode::Overloaded)),
);
assert_eq!(result.unwrap_err().code, IpcErrorCode::Overloaded);
assert_eq!(reads.get(), 1);
}
#[test]
fn content_length_cannot_hide_or_inflate_actual_body() {
assert!(check_claim(MAX_BODY, None, MAX_BODY).is_ok());
assert!(check_claim(MAX_BODY, Some(0), 0).is_ok());
assert!(check_claim(MAX_BODY, Some(10), 10).is_ok());
assert_eq!(
check_claim(MAX_BODY, Some(0), 1).unwrap_err().code,
IpcErrorCode::InvalidFrame
);
assert_eq!(
check_claim(MAX_BODY, Some(10), 9).unwrap_err().code,
IpcErrorCode::InvalidFrame
);
assert_eq!(
check_claim(MAX_BODY, Some(1), MAX_BODY + 1)
.unwrap_err()
.code,
IpcErrorCode::PayloadTooLarge
);
assert_eq!(
check_claim(MAX_BODY, Some(MAX_BODY + 1), 0)
.unwrap_err()
.code,
IpcErrorCode::PayloadTooLarge
);
}
#[test]
fn invalid_stream_byte_count_fails_without_slicing_or_reserving() {
let result = read_body(
MAX_BODY,
None,
|buffer| Ok(buffer.len() + 1),
|_| panic!("invalid stream must not reserve"),
);
assert_eq!(result.unwrap_err().code, IpcErrorCode::PayloadTooLarge);
}
#[test]
fn cancelled_or_late_completion_runs_cleanup_once() {
let completed = Cell::new(0);
let completion = OnDrop(Some(|| completed.set(completed.get() + 1)));
let future = async move {
let _completion = completion;
std::future::pending::<()>().await;
};
drop(future); assert_eq!(completed.get(), 1);
let completion = OnDrop(Some(|| completed.set(completed.get() + 1)));
drop(completion);
assert_eq!(completed.get(), 2);
}
}