use std::ffi::c_void;
use std::rc::Weak;
use std::sync::{Arc, Mutex};
use crate::{
DesktopFrame, DesktopHttpMethod, DesktopProtocolRequest, DesktopProtocolResponse,
DesktopResponseBody, DesktopRuntime, DEFAULT_MAX_REQUEST_BYTES,
};
use anyhow::{Context, Result};
use webview2_com::Microsoft::Web::WebView2::Win32::{
ICoreWebView2, ICoreWebView2Environment, ICoreWebView2HttpRequestHeaders,
ICoreWebView2WebResourceRequest, ICoreWebView2WebResourceRequestedEventArgs,
ICoreWebView2WebResourceRequestedEventHandler, ICoreWebView2WebResourceResponse,
ICoreWebView2_22, COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL,
COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT, COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL,
};
use webview2_com::{CoTaskMemPWSTR, WebResourceRequestedEventHandler};
use windows::core::{
implement, Error as WindowsError, Interface, Result as WindowsResult, HRESULT, PWSTR,
};
use windows::Win32::Foundation::{E_FAIL, E_INVALIDARG, E_NOTIMPL, E_POINTER, S_FALSE, S_OK};
use windows::Win32::System::Com::{
ISequentialStream_Impl, IStream, IStream as WinIStream, IStream_Impl, LOCKTYPE, STATFLAG,
STATSTG, STGC, STGTY_STREAM, STREAM_SEEK, STREAM_SEEK_CUR, STREAM_SEEK_END, STREAM_SEEK_SET,
};
#[cfg(feature = "application-ipc")]
use super::ipc::WindowsIpc;
use super::{APP_ORIGIN, APP_REQUEST_FILTER};
pub(super) struct RuntimeHandlerOptions {
pub(super) tasks: Weak<super::tasks::ApplicationTasks>,
pub(super) trace: super::startup::StartupTrace,
#[cfg(feature = "application-ipc")]
pub(super) ipc: Weak<WindowsIpc>,
}
pub(super) fn register_runtime_handler(
environment: &ICoreWebView2Environment,
webview: &ICoreWebView2,
frame: &DesktopFrame,
options: RuntimeHandlerOptions,
) -> Result<ICoreWebView2WebResourceRequestedEventHandler> {
register_web_resource_filter(webview)?;
let environment = environment.clone();
let runtime = Arc::clone(&frame.runtime);
let executor = Arc::clone(&frame.executor);
let handler = WebResourceRequestedEventHandler::create(Box::new(move |_sender, args| {
if let Some(args) = args {
#[cfg(feature = "application-ipc")]
{
let request = unsafe { args.Request()? };
let uri = read_pwstr_bounded(8192, |out| unsafe { request.Uri(out) })?;
if let Some(path) = super::ipc_policy::reserved_path(&uri) {
if let Some(ipc) = options.ipc.upgrade() {
super::ipc_http::handle(&ipc, &environment, &args, path)?;
} else {
let response = create_webview_response(
&environment,
super::ipc_http::error_response(crate::ipc::IpcErrorCode::Closed),
)?;
unsafe {
args.SetResponse(&response)?;
}
}
return Ok(());
}
}
handle_web_resource_request(&environment, &runtime, &executor, &options, &args)?;
}
Ok(())
}));
let mut token = 0_i64;
unsafe { webview.add_WebResourceRequested(&handler, &mut token)? };
Ok(handler)
}
pub(super) fn app_url(url: &str) -> bool {
let Some(rest) = url.strip_prefix(APP_ORIGIN) else {
return false;
};
rest.is_empty() || rest.starts_with(['/', '?', '#'])
}
fn register_web_resource_filter(webview: &ICoreWebView2) -> Result<()> {
let webview = webview.cast::<ICoreWebView2_22>().context(
"WebUI requires WebView2 Runtime 122.0.2365.46 or later for native resource interception; update the Microsoft Edge WebView2 Runtime",
)?;
unsafe {
webview
.AddWebResourceRequestedFilterWithRequestSourceKinds(
APP_REQUEST_FILTER,
COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL,
COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL,
)
.context("Failed to register native WebUI resource interception")?;
}
Ok(())
}
fn handle_web_resource_request(
environment: &ICoreWebView2Environment,
runtime: &Arc<DesktopRuntime>,
executor: &crate::execution::ApplicationExecutor,
options: &RuntimeHandlerOptions,
args: &ICoreWebView2WebResourceRequestedEventArgs,
) -> WindowsResult<()> {
let Some(tasks) = options.tasks.upgrade() else {
return Ok(());
};
unsafe {
let request = args.Request()?;
let uri = read_pwstr_bounded(8192, |out| request.Uri(out))?;
if !app_url(&uri) {
return Ok(());
}
let method = read_pwstr_bounded(32, |out| request.Method(out))?;
let head = method.eq_ignore_ascii_case("HEAD");
let method = DesktopHttpMethod::parse(&method);
let headers = request.Headers()?;
let accept = read_header(&headers, "Accept").unwrap_or_default();
let body = read_request_body(&request)?;
let path = webui_path_from_uri(&uri);
let document = if options.trace.is_enabled() {
let mut resource_context = COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL;
args.ResourceContext(&mut resource_context)?;
resource_context == COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT
} else {
false
};
if document {
options.trace.mark("document_requested");
}
let trace = options.trace;
let runtime = Arc::clone(runtime);
let work = executor.submit(move || {
let response = runtime
.handle_request(&DesktopProtocolRequest {
method,
path: &path,
body: &body,
wants_json: accept.contains("json") || accept.contains("ndjson"),
})
.unwrap_or_else(|err| DesktopProtocolResponse::text(500, err.chain_message()));
if document {
trace.mark("document_rendered");
}
response
});
let Ok(work) = work else {
args.SetResponse(&create_webview_response(
environment,
response_for_head(
DesktopProtocolResponse::text(
503,
"Desktop application executor is unavailable",
),
head,
),
)?)?;
return Ok(());
};
let deferral = ResponseDeferral(args.GetDeferral()?);
let environment = environment.clone();
let args = args.clone();
tasks
.tasks
.spawn(async move {
let _deferral = deferral;
let response = work.await.unwrap_or_else(|_| {
DesktopProtocolResponse::text(503, "Desktop application executor closed")
});
match create_webview_response(&environment, response_for_head(response, head))
.and_then(|response| args.SetResponse(&response))
{
Ok(()) => {
if document {
trace.mark("document_delivered");
}
}
Err(error) => eprintln!("WebUI: native response delivery failed: {error}"),
}
})
.map_err(|_| WindowsError::from(E_FAIL))?;
}
struct ResponseDeferral(webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Deferral);
impl Drop for ResponseDeferral {
fn drop(&mut self) {
let _ = unsafe { self.0.Complete() };
}
}
Ok(())
}
fn response_for_head(mut response: DesktopProtocolResponse, head: bool) -> DesktopProtocolResponse {
if head {
response.body = Vec::new().into();
}
response
}
pub(super) fn read_header(headers: &ICoreWebView2HttpRequestHeaders, name: &str) -> Option<String> {
let name = CoTaskMemPWSTR::from(name);
read_pwstr_bounded(8192, |out| unsafe {
headers.GetHeader(*name.as_ref().as_pcwstr(), out)
})
.ok()
}
pub(super) fn read_pwstr_bounded(
max_units: usize,
read: impl FnOnce(*mut PWSTR) -> WindowsResult<()>,
) -> WindowsResult<String> {
let mut raw = PWSTR::null();
let result = read(&mut raw);
let _owned = CoTaskMemPWSTR::from(raw);
result?;
if raw.is_null() {
return Ok(String::new());
}
let mut len = 0;
unsafe {
while len < max_units && *raw.0.add(len) != 0 {
len += 1;
}
if *raw.0.add(len) != 0 {
return Err(WindowsError::from(E_INVALIDARG));
}
String::from_utf16(std::slice::from_raw_parts(raw.0, len))
.map_err(|_| WindowsError::from(E_INVALIDARG))
}
}
pub(super) fn read_pwstr<F>(read: F) -> WindowsResult<String>
where
F: FnOnce(*mut PWSTR) -> WindowsResult<()>,
{
let mut raw = PWSTR::null();
read(&mut raw)?;
let value = CoTaskMemPWSTR::from(raw).to_string();
Ok(value)
}
pub(super) fn webui_path_from_uri(uri: &str) -> String {
let rest = uri.strip_prefix(APP_ORIGIN).unwrap_or(uri);
if rest.is_empty() {
"/".to_string()
} else {
rest.to_string()
}
}
fn read_request_body(request: &ICoreWebView2WebResourceRequest) -> WindowsResult<Vec<u8>> {
match super::request::content(request)? {
Some(stream) => read_request_stream(stream),
None => Ok(Vec::new()),
}
}
fn read_request_stream(stream: WinIStream) -> WindowsResult<Vec<u8>> {
let mut out = Vec::new();
let max = usize::try_from(DEFAULT_MAX_REQUEST_BYTES).unwrap_or(usize::MAX);
let mut buffer = [0_u8; 16 * 1024];
loop {
let mut read = 0_u32;
let hr = unsafe {
stream.Read(
buffer.as_mut_ptr().cast::<c_void>(),
u32::try_from(buffer.len()).unwrap_or(u32::MAX),
Some(&mut read),
)
};
hr.ok()?;
if read == 0 {
break;
}
let read = usize::try_from(read).map_err(|_| WindowsError::from(E_INVALIDARG))?;
if out.len().saturating_add(read) > max {
return Err(WindowsError::from(E_INVALIDARG));
}
out.extend_from_slice(&buffer[..read]);
}
Ok(out)
}
pub(super) fn create_webview_response(
environment: &ICoreWebView2Environment,
response: DesktopProtocolResponse,
) -> WindowsResult<ICoreWebView2WebResourceResponse> {
let headers = response_headers(&response);
let reason = status_reason(response.status);
let status = i32::from(response.status);
let stream: WinIStream = MemoryStream::new(response.body).into();
let reason = CoTaskMemPWSTR::from(reason);
let headers = CoTaskMemPWSTR::from(headers.as_str());
unsafe {
environment.CreateWebResourceResponse(
&stream,
status,
*reason.as_ref().as_pcwstr(),
*headers.as_ref().as_pcwstr(),
)
}
}
fn response_headers(response: &DesktopProtocolResponse) -> String {
let reusable_asset = response.status == 200
&& matches!(&response.body, crate::DesktopResponseContent::File(_))
&& reusable_resource_type(&response.content_type);
let mut headers = String::with_capacity(response.content_type.len() + 96);
headers.push_str("Content-Type: ");
headers.push_str(&response.content_type);
headers.push_str(if reusable_asset {
"\r\nCache-Control: private, no-cache\r\nPragma: no-cache\r\n"
} else {
"\r\nCache-Control: no-store\r\nPragma: no-cache\r\n"
});
headers
}
fn reusable_resource_type(content_type: &str) -> bool {
let mime = content_type
.split_once(';')
.map_or(content_type, |(mime, _)| mime)
.trim();
matches!(
mime,
"text/css" | "text/javascript" | "application/javascript" | "application/wasm"
) || mime.starts_with("font/")
|| mime.starts_with("image/")
}
fn status_reason(status: u16) -> &'static str {
match status {
200 => "OK",
204 => "No Content",
400 => "Bad Request",
401 => "Unauthorized",
404 => "Not Found",
405 => "Method Not Allowed",
409 => "Conflict",
413 => "Payload Too Large",
429 => "Too Many Requests",
500 => "Internal Server Error",
503 => "Service Unavailable",
_ => "OK",
}
}
#[implement(IStream)]
pub(super) struct MemoryStream {
data: Arc<StreamData>,
position: Mutex<usize>,
}
impl MemoryStream {
pub(super) fn new(data: impl Into<crate::DesktopResponseContent>) -> Self {
let data = match data.into() {
crate::DesktopResponseContent::Bytes(bytes) => StreamData::Bytes(bytes),
crate::DesktopResponseContent::File(file) => {
let length = file.len();
StreamData::File {
file: Mutex::new(file),
length,
}
}
};
Self {
data: Arc::new(data),
position: Mutex::new(0),
}
}
}
enum StreamData {
Bytes(DesktopResponseBody),
File {
file: Mutex<crate::DesktopResponseFile>,
length: u64,
},
}
impl StreamData {
fn len(&self) -> usize {
match self {
Self::Bytes(bytes) => bytes.len(),
Self::File { length, .. } => usize::try_from(*length).unwrap_or(usize::MAX),
}
}
fn read(&self, position: usize, out: &mut [u8]) -> std::io::Result<usize> {
use std::io::{Read, Seek, SeekFrom};
match self {
Self::Bytes(bytes) => {
out.copy_from_slice(&bytes[position..position + out.len()]);
Ok(out.len())
}
Self::File { file, .. } => {
let mut file = file
.lock()
.map_err(|_| std::io::Error::other("desktop stream lock failed"))?;
file.seek(SeekFrom::Start(position as u64))?;
file.read_exact(out)?;
Ok(out.len())
}
}
}
}
impl ISequentialStream_Impl for MemoryStream_Impl {
fn Read(&self, pv: *mut c_void, cb: u32, pcbread: *mut u32) -> HRESULT {
let Ok(mut position) = self.position.lock() else {
return E_FAIL;
};
let remaining = self.data.len().saturating_sub(*position);
let requested = usize::try_from(cb).unwrap_or(usize::MAX);
let mut count = remaining.min(requested);
if count != 0 && pv.is_null() {
return E_POINTER;
}
if count != 0 {
unsafe {
let out = std::slice::from_raw_parts_mut(pv.cast::<u8>(), count);
match self.data.read(*position, out) {
Ok(read) => count = read,
Err(_) => return E_FAIL,
}
}
*position += count;
}
if !pcbread.is_null() {
unsafe {
*pcbread = u32::try_from(count).unwrap_or(u32::MAX);
}
}
if count < requested {
S_FALSE
} else {
S_OK
}
}
fn Write(&self, _pv: *const c_void, _cb: u32, _pcbwritten: *mut u32) -> HRESULT {
E_NOTIMPL
}
}
impl IStream_Impl for MemoryStream_Impl {
fn Seek(
&self,
dlibmove: i64,
dworigin: STREAM_SEEK,
plibnewposition: *mut u64,
) -> WindowsResult<()> {
let mut position = self
.position
.lock()
.map_err(|_| WindowsError::from(E_FAIL))?;
let base = match dworigin {
STREAM_SEEK_SET => 0_i64,
STREAM_SEEK_CUR => {
i64::try_from(*position).map_err(|_| WindowsError::from(E_INVALIDARG))?
}
STREAM_SEEK_END => {
i64::try_from(self.data.len()).map_err(|_| WindowsError::from(E_INVALIDARG))?
}
_ => return Err(WindowsError::from(E_INVALIDARG)),
};
let next = base
.checked_add(dlibmove)
.ok_or_else(|| WindowsError::from(E_INVALIDARG))?;
if next < 0 {
return Err(WindowsError::from(E_INVALIDARG));
}
*position = usize::try_from(next).map_err(|_| WindowsError::from(E_INVALIDARG))?;
if !plibnewposition.is_null() {
unsafe {
*plibnewposition =
u64::try_from(*position).map_err(|_| WindowsError::from(E_INVALIDARG))?;
}
}
Ok(())
}
fn SetSize(&self, _libnewsize: u64) -> WindowsResult<()> {
Err(WindowsError::from(E_NOTIMPL))
}
fn CopyTo(
&self,
_pstm: windows::core::Ref<IStream>,
_cb: u64,
_pcbread: *mut u64,
_pcbwritten: *mut u64,
) -> WindowsResult<()> {
Err(WindowsError::from(E_NOTIMPL))
}
fn Commit(&self, _grfcommitflags: &STGC) -> WindowsResult<()> {
Ok(())
}
fn Revert(&self) -> WindowsResult<()> {
Err(WindowsError::from(E_NOTIMPL))
}
fn LockRegion(&self, _liboffset: u64, _cb: u64, _dwlocktype: &LOCKTYPE) -> WindowsResult<()> {
Err(WindowsError::from(E_NOTIMPL))
}
fn UnlockRegion(&self, _liboffset: u64, _cb: u64, _dwlocktype: u32) -> WindowsResult<()> {
Err(WindowsError::from(E_NOTIMPL))
}
fn Stat(&self, pstatstg: *mut STATSTG, _grfstatflag: &STATFLAG) -> WindowsResult<()> {
if pstatstg.is_null() {
return Err(WindowsError::from(E_INVALIDARG));
}
unsafe {
(*pstatstg) = STATSTG::default();
(*pstatstg).r#type = STGTY_STREAM.0.cast_unsigned();
(*pstatstg).cbSize =
u64::try_from(self.data.len()).map_err(|_| WindowsError::from(E_INVALIDARG))?;
}
Ok(())
}
fn Clone(&self) -> WindowsResult<IStream> {
let position = *self
.position
.lock()
.map_err(|_| WindowsError::from(E_FAIL))?;
Ok(MemoryStream {
data: Arc::clone(&self.data),
position: Mutex::new(position),
}
.into())
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct Released(Arc<AtomicUsize>);
impl Drop for Released {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn dynamic_responses_publish_the_canonical_no_store_policy() {
assert_eq!(
response_headers(&DesktopProtocolResponse::protobuf(Vec::new())),
"Content-Type: application/x-protobuf\r\nCache-Control: no-store\r\nPragma: no-cache\r\n"
);
assert_eq!(
response_headers(&DesktopProtocolResponse::new(200, "text/css", Vec::new())),
"Content-Type: text/css\r\nCache-Control: no-store\r\nPragma: no-cache\r\n"
);
}
#[test]
fn file_assets_allow_initial_reuse_but_require_later_revalidation() {
let response = DesktopProtocolResponse::new(
200,
"text/css",
crate::DesktopResponseContent::File(crate::DesktopResponseFile::new(
tempfile::tempfile().unwrap(),
0,
)),
);
assert_eq!(
response_headers(&response),
"Content-Type: text/css\r\nCache-Control: private, no-cache\r\nPragma: no-cache\r\n"
);
let response = DesktopProtocolResponse {
status: 500,
..response
};
assert!(response_headers(&response).contains("Cache-Control: no-store\r\n"));
}
#[test]
fn only_static_browser_resources_can_enter_the_revalidation_cache() {
for content_type in [
"text/css",
"text/css; charset=utf-8",
"text/javascript",
"application/javascript",
"application/wasm",
"font/woff2",
"image/png",
] {
assert!(reusable_resource_type(content_type), "{content_type}");
}
for content_type in [
"text/html",
"text/plain",
"application/json",
"application/x-protobuf",
"application/octet-stream",
] {
let response = DesktopProtocolResponse::new(
200,
content_type,
crate::DesktopResponseContent::File(crate::DesktopResponseFile::new(
tempfile::tempfile().unwrap(),
0,
)),
);
assert!(response_headers(&response).contains("Cache-Control: no-store\r\n"));
}
}
#[test]
fn head_omits_success_and_error_bodies_but_preserves_metadata() {
for status in [200, 405, 500, 503] {
let released = Arc::new(AtomicUsize::new(0));
let response = response_for_head(
DesktopProtocolResponse::new(
status,
"application/octet-stream",
DesktopResponseBody::with_guard(vec![1, 2, 3], Released(Arc::clone(&released))),
),
true,
);
assert_eq!(response.status, status);
assert_eq!(response.content_type, "application/octet-stream");
assert!(response.body.as_bytes().unwrap().is_empty());
assert_eq!(released.load(Ordering::SeqCst), 1);
}
let response = response_for_head(DesktopProtocolResponse::text(200, "keep"), false);
assert_eq!(response.body, b"keep");
let response = response_for_head(
DesktopProtocolResponse::new(
200,
"application/octet-stream",
crate::DesktopResponseContent::File(crate::DesktopResponseFile::new(
tempfile::tempfile().unwrap(),
10,
)),
),
true,
);
assert!(response.body.as_bytes().unwrap().is_empty());
}
#[test]
fn native_stream_clones_keep_body_and_lease_until_final_release() {
let released = Arc::new(AtomicUsize::new(0));
let bytes = vec![1, 2, 3];
let pointer = bytes.as_ptr();
let memory = MemoryStream::new(DesktopResponseBody::with_guard(
bytes,
Released(Arc::clone(&released)),
));
assert!(
matches!(memory.data.as_ref(), StreamData::Bytes(body) if body.as_slice().as_ptr() == pointer)
);
let stream: IStream = memory.into();
let clone = unsafe { stream.Clone() }.unwrap();
drop(stream);
assert_eq!(released.load(Ordering::SeqCst), 0);
let mut bytes = [0_u8; 3];
let mut read = 0;
unsafe { clone.Read(bytes.as_mut_ptr().cast(), 3, Some(&mut read)) }
.ok()
.unwrap();
assert_eq!(read, 3);
assert_eq!(bytes, [1, 2, 3]);
drop(clone);
assert_eq!(released.load(Ordering::SeqCst), 1);
}
#[test]
fn request_uri_maps_to_runtime_path() {
assert_eq!(webui_path_from_uri("https://app.webui.localhost"), "/");
assert_eq!(
webui_path_from_uri("https://app.webui.localhost/contacts?view=all"),
"/contacts?view=all"
);
}
#[test]
fn native_file_stream_reads_at_independent_offsets_without_materialization() {
use std::io::{Seek, Write};
let mut file = tempfile::tempfile().unwrap();
file.write_all(b"abcdef").unwrap();
file.rewind().unwrap();
let memory = MemoryStream::new(crate::DesktopResponseContent::File(
crate::DesktopResponseFile::new(file, 6),
));
let mut bytes = [0; 3];
assert_eq!(memory.data.read(2, &mut bytes).unwrap(), 3);
assert_eq!(&bytes, b"cde");
assert_eq!(memory.data.read(0, &mut bytes).unwrap(), 3);
assert_eq!(&bytes, b"abc");
}
#[test]
fn stream_clone_preserves_cursor_and_reports_partial_eof() {
let stream: IStream = MemoryStream::new(b"abc".to_vec()).into();
let mut bytes = [0; 2];
let mut read = 0;
unsafe { stream.Read(bytes.as_mut_ptr().cast(), 2, Some(&mut read)) }
.ok()
.unwrap();
let cloned = unsafe { stream.Clone() }.unwrap();
assert_eq!(
unsafe { cloned.Read(bytes.as_mut_ptr().cast(), 2, Some(&mut read)) },
S_FALSE
);
assert_eq!(read, 1);
assert_eq!(bytes[0], b'c');
}
#[test]
fn resource_authority_excludes_external_and_lookalike_hosts() {
assert!(app_url("https://app.webui.localhost/"));
assert!(app_url("https://app.webui.localhost/resource?query=value"));
for uri in [
"https://example.com/",
"https://app.webui.localhost.example.com/",
"https://app.webui.localhost@other.example/",
"https://app.webui.localhost:444/",
"http://app.webui.localhost/",
] {
assert!(!app_url(uri), "{uri}");
}
}
}