use std::collections::BTreeSet;
use std::fmt;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{Duration, SystemTime};
use tauri::{AppHandle, Manager, Runtime, Webview};
pub const OWNER_DECISION: &str = "Every Right app has absolutely NO access to DevTools: blocked at each and every level that is possible, for the app, its shell and its own UI. The single exception is ViewRight: it opens HTML documents, and DevTools must be available INSIDE that opened HTML content only (the content frame), never for ViewRight's own UI or shell.";
pub const CONTENT_ONLY_MARKER: &str = "RIGHTKIT-SHELL-DEVTOOLS-CONTENT-ONLY-COMPILED";
#[cfg(feature = "content-devtools")]
#[used]
static CONTENT_ONLY_MARKER_BYTES: &[u8] = b"RIGHTKIT-SHELL-DEVTOOLS-CONTENT-ONLY-COMPILED";
pub const DEV_INSPECT_MARKER: &str = "RIGHTKIT-SHELL-DEV-INSPECT-COMPILED";
pub const DEV_INSPECT_COMPILED: bool = cfg!(feature = "dev-inspect");
#[cfg(feature = "dev-inspect")]
#[used]
static DEV_INSPECT_MARKER_BYTES: [u8; DEV_INSPECT_MARKER.len()] = {
let src = DEV_INSPECT_MARKER.as_bytes();
let mut out = [0u8; DEV_INSPECT_MARKER.len()];
let mut i = 0;
while i < src.len() {
out[i] = src[i];
i += 1;
}
out
};
#[cfg(feature = "dev-inspect")]
#[inline(never)]
fn keep_dev_inspect_marker() -> u8 {
unsafe { std::ptr::read_volatile(DEV_INSPECT_MARKER_BYTES.as_ptr()) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DevToolsPolicy {
#[default]
Denied,
ContentOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebviewDecision {
Deny,
AllowContent,
AllowDev,
}
#[derive(Debug, Clone)]
pub struct ContentWebview {
pub label: String,
pub window_label: String,
pub capabilities: Vec<String>,
pub app_acl_manifest: bool,
}
impl ContentWebview {
pub fn new(label: &str, window_label: &str) -> Self {
Self {
label: label.to_owned(),
window_label: window_label.to_owned(),
capabilities: Vec::new(),
app_acl_manifest: false,
}
}
pub fn capability(mut self, json: &str) -> Self {
self.capabilities.push(json.to_owned());
self
}
pub fn app_acl_manifest(mut self, declared: bool) -> Self {
self.app_acl_manifest = declared;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DevToolsError {
RemoteDebuggingArgument { arg: String },
BrowserPolicyInjects { source: String, switch: String },
BrowserRuntimeRedirected { source: String, folder: String },
RegistryUnreadable { detail: String },
RemoteDebugPortLive { path: PathBuf },
NotContentOnly,
ContentDevtoolsNotCompiled,
InvalidLabel { label: String },
MainWebview { label: String },
ContentHasIpc { label: String, capability: String },
CapabilitiesUnreadable { detail: String },
AppAclManifestMissing { label: String },
NotAContentWebview { label: String },
NotInstalled,
WebviewMissing { label: String },
NativePolicyNotApplied { label: String, detail: String },
}
impl DevToolsError {
pub fn code(&self) -> &'static str {
match self {
Self::RemoteDebuggingArgument { .. } => "remote_debugging_argument",
Self::BrowserPolicyInjects { .. } => "browser_policy_injects",
Self::BrowserRuntimeRedirected { .. } => "browser_runtime_redirected",
Self::RegistryUnreadable { .. } => "registry_unreadable",
Self::RemoteDebugPortLive { .. } => "remote_debug_port_live",
Self::NotContentOnly => "not_content_only",
Self::ContentDevtoolsNotCompiled => "content_devtools_not_compiled",
Self::InvalidLabel { .. } => "invalid_label",
Self::MainWebview { .. } => "main_webview",
Self::ContentHasIpc { .. } => "content_has_ipc",
Self::CapabilitiesUnreadable { .. } => "capabilities_unreadable",
Self::AppAclManifestMissing { .. } => "app_acl_manifest_missing",
Self::NotAContentWebview { .. } => "not_a_content_webview",
Self::NotInstalled => "not_installed",
Self::WebviewMissing { .. } => "webview_missing",
Self::NativePolicyNotApplied { .. } => "native_policy_not_applied",
}
}
}
impl fmt::Display for DevToolsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "rightkit-tauri-shell devtools policy: ")?;
match self {
Self::RemoteDebuggingArgument { arg } => write!(f, "refusing to start, the process was launched with a debugging argument ({arg})"),
Self::BrowserPolicyInjects { source, switch } => write!(f, "refusing to create a webview, {source} injects a debugging browser argument ({switch}); an application cannot override a machine policy"),
Self::BrowserRuntimeRedirected { source, folder } => write!(f, "refusing to create a webview, {source} makes WebView2 start a browser from `{folder}` instead of the installed runtime; an application cannot override a machine policy"),
Self::RegistryUnreadable { detail } => write!(f, "refusing to create a webview, the WebView2 policy registry could not be read ({detail})"),
Self::RemoteDebugPortLive { path } => write!(f, "refusing to run, a remote-debugging port is live ({})", path.display()),
Self::NotContentOnly => write!(f, "content webviews are registered but the policy is Denied; select DevToolsPolicy::ContentOnly explicitly"),
Self::ContentDevtoolsNotCompiled => write!(f, "ContentOnly needs the rightkit-tauri-shell `content-devtools` feature"),
Self::InvalidLabel { label } => write!(f, "`{label}` is not a valid webview label"),
Self::MainWebview { label } => write!(f, "`{label}` is the app UI webview; it can never be a content webview"),
Self::ContentHasIpc { label, capability } => write!(f, "content webview `{label}` is covered by capability `{capability}`, which grants app IPC; content webviews need no capability at all"),
Self::CapabilitiesUnreadable { detail } => write!(f, "a capability document could not be read ({detail})"),
Self::AppAclManifestMissing { label } => write!(f, "content webview `{label}` requires an application ACL manifest for the app's own commands (otherwise local content can call every app command)"),
Self::NotAContentWebview { label } => write!(f, "`{label}` is not a registered content webview"),
Self::NotInstalled => write!(f, "the shell is not installed"),
Self::WebviewMissing { label } => write!(f, "webview `{label}` does not exist"),
Self::NativePolicyNotApplied { label, detail } => write!(f, "refusing webview `{label}`, DevTools could not be disabled in it ({detail})"),
}
}
}
impl std::error::Error for DevToolsError {}
impl From<DevToolsError> for crate::ShellError {
fn from(e: DevToolsError) -> Self {
crate::ShellError::PermissionDenied(e.to_string())
}
}
fn unrev(s: &str) -> String {
s.chars().rev().collect()
}
fn forbidden_switch_names() -> Vec<String> {
[
"trop-gniggubed-etomer",
"epip-gniggubed-etomer",
"sserdda-gniggubed-etomer",
"snigiro-wolla-etomer",
"sbat-rof-slootved-nepo-otua",
"gniggubedetomerpdwslootvedegdesm",
]
.iter()
.map(|s| unrev(s))
.collect()
}
fn scrubbed_env_names() -> Vec<String> {
[
"STNEMUGRA_RESWORB_LANOITIDDA_2WEIVBEW",
"REGGUBED_TPIRCS_ROF_EPIP_2WEIVBEW",
]
.iter()
.map(|s| unrev(s))
.collect()
}
pub fn forbidden_switches_in(args: &str) -> Vec<String> {
let names = forbidden_switch_names();
let mut out = Vec::new();
for raw in args.split_whitespace() {
let token = raw.trim_matches(|c| c == '"' || c == '\'');
let Some(rest) = token.strip_prefix('-') else {
continue;
};
let rest = rest.trim_start_matches('-');
let (name, value) = rest.split_once('=').unwrap_or((rest, ""));
let name = name.to_ascii_lowercase();
let hit = names.contains(&name)
|| (name == "js-flags" && value.to_ascii_lowercase().contains("inspect"));
if hit {
out.push(token.to_owned());
}
}
out
}
pub fn argv_violations(args: &[String]) -> Vec<String> {
args.iter().flat_map(|a| forbidden_switches_in(a)).collect()
}
pub fn env_vars_to_scrub<I, S>(names: I) -> Vec<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let wanted = scrubbed_env_names();
names
.into_iter()
.filter(|n| wanted.iter().any(|w| w.eq_ignore_ascii_case(n.as_ref())))
.map(|n| n.as_ref().to_owned())
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegValue {
pub name: String,
pub value: String,
}
const REG_TEXT_TYPES: [u32; 3] = [1, 2, 7];
pub fn decode_reg_text(kind: u32, data: &[u8]) -> Option<String> {
if !REG_TEXT_TYPES.contains(&kind) {
return None;
}
let units: Vec<u16> = data
.as_chunks::<2>()
.0
.iter()
.map(|pair| u16::from_le_bytes(*pair))
.collect();
let text = String::from_utf16_lossy(&units);
Some(
text.split('\0')
.filter(|part| !part.is_empty())
.collect::<Vec<_>>()
.join(" "),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyKeyOpen {
Opened,
Absent,
Unreadable(u32),
}
impl PolicyKeyOpen {
pub fn from_status(status: u32) -> Self {
match status {
0 => Self::Opened,
2 => Self::Absent,
other => Self::Unreadable(other),
}
}
}
pub fn policy_violations(values: &[RegValue], source: &str) -> Vec<DevToolsError> {
values
.iter()
.flat_map(|v| {
forbidden_switches_in(&v.value)
.into_iter()
.map(move |switch| DevToolsError::BrowserPolicyInjects {
source: format!("{source} (value `{}`)", v.name),
switch,
})
})
.collect()
}
pub fn policy_value_applies(name: &str, exe_name: &str) -> bool {
let name = name.trim();
exe_name.is_empty() || name == "*" || name.eq_ignore_ascii_case(exe_name)
}
pub fn runtime_folder_env_name() -> String {
unrev("REDLOF_ELBATUCEXE_RESWORB_2WEIVBEW")
}
pub fn runtime_folder_env_violation(value: Option<&str>) -> Option<DevToolsError> {
let folder = value?.trim();
if folder.is_empty() {
return None;
}
Some(DevToolsError::BrowserRuntimeRedirected {
source: format!("the environment variable {}", runtime_folder_env_name()),
folder: folder.to_owned(),
})
}
pub fn runtime_folder_violations(
values: &[RegValue],
source: &str,
exe_name: &str,
) -> Vec<DevToolsError> {
values
.iter()
.filter(|v| !v.value.trim().is_empty() && policy_value_applies(&v.name, exe_name))
.map(|v| DevToolsError::BrowserRuntimeRedirected {
source: format!("{source} (value `{}`)", v.name),
folder: v.value.trim().to_owned(),
})
.collect()
}
pub fn expand_env_refs(text: &str, env: impl Fn(&str) -> Option<String>) -> String {
let mut out = String::new();
let mut rest = text;
while let Some(open) = rest.find('%') {
out.push_str(&rest[..open]);
let after = &rest[open + 1..];
match after.find('%') {
Some(close) if close > 0 => match env(&after[..close]) {
Some(value) => {
out.push_str(&value);
rest = &after[close + 1..];
}
None => {
out.push('%');
rest = after;
}
},
_ => {
out.push('%');
rest = after;
}
}
}
out.push_str(rest);
out
}
pub fn policy_user_data_dirs(
values: &[RegValue],
exe_name: &str,
env: impl Fn(&str) -> Option<String>,
) -> Vec<PathBuf> {
values
.iter()
.filter(|v| !v.value.trim().is_empty() && policy_value_applies(&v.name, exe_name))
.map(|v| PathBuf::from(expand_env_refs(v.value.trim(), &env)))
.collect()
}
static EXTRA_USER_DATA_DIRS: std::sync::Mutex<Vec<PathBuf>> = std::sync::Mutex::new(Vec::new());
pub fn register_user_data_dir(dir: impl Into<PathBuf>) {
let dir = dir.into();
let mut dirs = EXTRA_USER_DATA_DIRS
.lock()
.unwrap_or_else(|poison| poison.into_inner());
if !dirs.contains(&dir) {
dirs.push(dir);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GuardReport {
pub scrubbed_env: Vec<String>,
}
static GUARD: OnceLock<Result<GuardReport, DevToolsError>> = OnceLock::new();
static PROCESS_START: OnceLock<SystemTime> = OnceLock::new();
pub fn guard_process_start() -> Result<GuardReport, DevToolsError> {
GUARD.get_or_init(run_guard).clone()
}
fn run_guard() -> Result<GuardReport, DevToolsError> {
let _ = PROCESS_START.set(SystemTime::now());
if DEV_INSPECT_COMPILED {
return Ok(announce_dev_inspect());
}
let argv: Vec<String> = std::env::args_os()
.skip(1)
.map(|a| a.to_string_lossy().into_owned())
.collect();
if let Some(arg) = argv_violations(&argv).into_iter().next() {
return Err(DevToolsError::RemoteDebuggingArgument { arg });
}
let present: Vec<String> = std::env::vars_os()
.map(|(k, _)| k.to_string_lossy().into_owned())
.collect();
let scrubbed_env = env_vars_to_scrub(present.iter());
for name in &scrubbed_env {
std::env::remove_var(name);
}
#[cfg(windows)]
{
if let Some(err) = registry_policy_violations()?.into_iter().next() {
return Err(err);
}
let folder = std::env::var_os(runtime_folder_env_name());
let folder = folder.as_ref().map(|value| value.to_string_lossy());
if let Some(err) = runtime_folder_env_violation(folder.as_deref()) {
return Err(err);
}
let exe = current_exe_name();
for (source, values) in read_policy("BrowserExecutableFolder")? {
if let Some(err) = runtime_folder_violations(&values, source, &exe)
.into_iter()
.next()
{
return Err(err);
}
}
let env = |name: &str| std::env::var(name).ok();
for (_, values) in read_policy("UserDataFolder")? {
for dir in policy_user_data_dirs(&values, &exe, env) {
register_user_data_dir(dir);
}
}
if let Some(dir) = std::env::var_os(unrev("REDLOF_ATAD_RESU_2WEIVBEW")) {
if !dir.is_empty() {
register_user_data_dir(PathBuf::from(dir));
}
}
}
Ok(GuardReport { scrubbed_env })
}
#[cfg(feature = "dev-inspect")]
fn announce_dev_inspect() -> GuardReport {
let kept = keep_dev_inspect_marker();
eprintln!(
"RIGHTKIT SHELL DEVELOPER INSPECTION ACTIVE: this build was compiled with the rightkit-tauri-shell `dev-inspect` \
feature (marker byte {kept:#04x}). Every webview is inspectable, debugging switches and WebView2 \
environment overrides are not scrubbed, and no remote-debugging refusal or port tripwire runs. \
A build like this can never be released; right-release refuses it."
);
GuardReport::default()
}
#[cfg(not(feature = "dev-inspect"))]
fn announce_dev_inspect() -> GuardReport {
GuardReport::default()
}
#[cfg(windows)]
mod registry {
use super::{decode_reg_text, PolicyKeyOpen, RegValue};
pub(super) const HKEY_CURRENT_USER: isize = 0x8000_0001_u32 as i32 as isize;
pub(super) const HKEY_LOCAL_MACHINE: isize = 0x8000_0002_u32 as i32 as isize;
pub(super) const VIEW_64: u32 = 0x0100;
pub(super) const VIEW_32: u32 = 0x0200;
const KEY_READ: u32 = 0x0002_0019;
const ERROR_MORE_DATA: u32 = 234;
const ERROR_NO_MORE_ITEMS: u32 = 259;
const NAME_UNITS: usize = 16_384;
#[link(name = "advapi32")]
unsafe extern "system" {
fn RegOpenKeyExW(
key: isize,
sub_key: *const u16,
options: u32,
access: u32,
result: *mut isize,
) -> u32;
fn RegEnumValueW(
key: isize,
index: u32,
name: *mut u16,
name_len: *mut u32,
reserved: *mut u32,
kind: *mut u32,
data: *mut u8,
data_len: *mut u32,
) -> u32;
fn RegCloseKey(key: isize) -> u32;
}
struct Key(isize);
impl Drop for Key {
fn drop(&mut self) {
unsafe { RegCloseKey(self.0) };
}
}
pub(super) fn text_values(
root: isize,
path: &str,
view: u32,
) -> Result<Option<Vec<RegValue>>, String> {
let wide: Vec<u16> = path.encode_utf16().chain([0]).collect();
let mut handle: isize = 0;
let status = unsafe { RegOpenKeyExW(root, wide.as_ptr(), 0, KEY_READ | view, &mut handle) };
match PolicyKeyOpen::from_status(status) {
PolicyKeyOpen::Opened => {}
PolicyKeyOpen::Absent => return Ok(None),
PolicyKeyOpen::Unreadable(code) => {
return Err(format!("open failed with Win32 error {code}"))
}
}
let key = Key(handle);
let mut values = Vec::new();
let mut name = vec![0u16; NAME_UNITS];
let mut data = vec![0u8; 4096];
let mut index = 0u32;
loop {
let mut name_len = name.len() as u32;
let mut data_len = data.len() as u32;
let mut kind = 0u32;
let status = unsafe {
RegEnumValueW(
key.0,
index,
name.as_mut_ptr(),
&mut name_len,
std::ptr::null_mut(),
&mut kind,
data.as_mut_ptr(),
&mut data_len,
)
};
match status {
0 => {
if let Some(value) = decode_reg_text(kind, &data[..data_len as usize]) {
values.push(RegValue {
name: String::from_utf16_lossy(&name[..name_len as usize]),
value,
});
}
index += 1;
}
ERROR_MORE_DATA => {
let needed = (data_len as usize).max(data.len() * 2);
data.resize(needed, 0);
}
ERROR_NO_MORE_ITEMS => return Ok(Some(values)),
other => {
return Err(format!(
"reading value {index} failed with Win32 error {other}"
))
}
}
}
}
}
#[cfg(windows)]
fn registry_policy_violations() -> Result<Vec<DevToolsError>, DevToolsError> {
let mut out = Vec::new();
for (source, values) in read_policy("AdditionalBrowserArguments")? {
out.extend(policy_violations(&values, source));
}
Ok(out)
}
#[cfg(windows)]
fn current_exe_name() -> String {
std::env::current_exe()
.ok()
.and_then(|path| {
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
})
.unwrap_or_default()
}
#[cfg(windows)]
fn read_policy(policy: &str) -> Result<Vec<(&'static str, Vec<RegValue>)>, DevToolsError> {
let path = format!(r"Software\Policies\Microsoft\Edge\WebView2\{policy}");
let path = path.as_str();
let keys = [
(
"HKLM policy (64-bit view)",
registry::HKEY_LOCAL_MACHINE,
registry::VIEW_64,
),
(
"HKLM policy (32-bit view)",
registry::HKEY_LOCAL_MACHINE,
registry::VIEW_32,
),
(
"HKCU policy",
registry::HKEY_CURRENT_USER,
registry::VIEW_64,
),
];
let mut out = Vec::new();
for (source, root, view) in keys {
match registry::text_values(root, path, view) {
Ok(None) => {}
Ok(Some(values)) => out.push((source, values)),
Err(detail) => {
return Err(DevToolsError::RegistryUnreadable {
detail: format!("{source}, {path}: {detail}"),
})
}
}
}
Ok(out)
}
#[cfg(windows)]
#[doc(hidden)]
pub fn read_user_key_text_values(path: &str) -> Result<Option<Vec<RegValue>>, String> {
registry::text_values(registry::HKEY_CURRENT_USER, path, registry::VIEW_64)
}
pub const DEBUG_PORT_FILE: &str = "DevToolsActivePort";
pub fn is_live_debug_port_file(modified: SystemTime, process_start: SystemTime) -> bool {
modified + Duration::from_secs(2) >= process_start
}
pub fn live_debug_port_file(dirs: &[PathBuf], process_start: SystemTime) -> Option<PathBuf> {
for dir in dirs {
for candidate in [
dir.join(DEBUG_PORT_FILE),
dir.join("EBWebView").join(DEBUG_PORT_FILE),
] {
if let Ok(meta) = std::fs::metadata(&candidate) {
let modified = meta.modified().unwrap_or_else(|_| SystemTime::now());
if is_live_debug_port_file(modified, process_start) {
return Some(candidate);
}
}
}
}
None
}
pub fn verify_not_remote_debuggable<R: Runtime>(app: &AppHandle<R>) -> Result<(), DevToolsError> {
if DEV_INSPECT_COMPILED {
return Ok(());
}
#[cfg(windows)]
{
let start = PROCESS_START.get().copied().unwrap_or_else(SystemTime::now);
let mut dirs: Vec<PathBuf> = Vec::new();
if let Ok(d) = app.path().app_local_data_dir() {
dirs.push(d);
}
if let Ok(d) = app.path().app_data_dir() {
dirs.push(d);
}
dirs.extend(
EXTRA_USER_DATA_DIRS
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.iter()
.cloned(),
);
if let Some(path) = live_debug_port_file(&dirs, start) {
return Err(DevToolsError::RemoteDebugPortLive { path });
}
}
#[cfg(not(windows))]
let _ = app;
Ok(())
}
pub fn verify_after_first_navigation<R: Runtime>(app: &AppHandle<R>) -> Result<(), DevToolsError> {
static DONE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if DONE.swap(true, std::sync::atomic::Ordering::SeqCst) {
return Ok(());
}
verify_not_remote_debuggable(app)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DevToolsGate {
policy: DevToolsPolicy,
main_label: String,
content: BTreeSet<String>,
dev_inspect: bool,
}
impl DevToolsGate {
pub fn denied(main_label: &str) -> Self {
Self {
policy: DevToolsPolicy::Denied,
main_label: main_label.to_owned(),
content: BTreeSet::new(),
dev_inspect: false,
}
}
pub fn is_dev_inspect(&self) -> bool {
self.dev_inspect
}
pub fn policy(&self) -> DevToolsPolicy {
self.policy
}
pub fn content_labels(&self) -> Vec<String> {
if self.dev_inspect {
return vec!["*".to_owned()];
}
self.content.iter().cloned().collect()
}
pub fn decide(&self, label: &str) -> WebviewDecision {
if self.dev_inspect {
return WebviewDecision::AllowDev;
}
if self.policy == DevToolsPolicy::ContentOnly
&& label != self.main_label
&& self
.content
.iter()
.any(|pattern| glob_match(pattern, label))
{
WebviewDecision::AllowContent
} else {
WebviewDecision::Deny
}
}
}
fn valid_label(label: &str) -> bool {
!label.is_empty()
&& label
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '/' | ':' | '_'))
}
fn valid_content_label(label: &str) -> bool {
match label.strip_suffix('*') {
Some(prefix) => valid_label(prefix),
None => valid_label(label),
}
}
pub fn patterns_may_overlap(a: &str, b: &str) -> bool {
let head = |p: &str| match p.find(['*', '?']) {
Some(i) => (p[..i].to_owned(), true),
None => (p.to_owned(), false),
};
let (ha, wa) = head(a);
let (hb, wb) = head(b);
match (wa, wb) {
(false, false) => ha == hb,
(true, false) => hb.starts_with(&ha) && glob_match(a, b),
(false, true) => ha.starts_with(&hb) && glob_match(b, a),
(true, true) => ha.starts_with(&hb) || hb.starts_with(&ha),
}
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
let (mut pi, mut ti) = (0usize, 0usize);
let (mut star, mut mark) = (None::<usize>, 0usize);
while ti < t.len() {
if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == '*' {
star = Some(pi);
mark = ti;
pi += 1;
} else if let Some(s) = star {
pi = s + 1;
mark += 1;
ti = mark;
} else {
return false;
}
}
while pi < p.len() && p[pi] == '*' {
pi += 1;
}
pi == p.len()
}
fn patterns(value: &serde_json::Value, key: &str) -> Vec<String> {
value
.get(key)
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
}
pub fn capability_granting_ipc(
doc: &str,
webview_label: &str,
window_label: &str,
) -> Result<Option<String>, DevToolsError> {
let parsed: serde_json::Value =
serde_json::from_str(doc).map_err(|e| DevToolsError::CapabilitiesUnreadable {
detail: e.to_string(),
})?;
let docs: Vec<&serde_json::Value> = match &parsed {
serde_json::Value::Array(items) => items.iter().collect(),
other => vec![other],
};
for cap in docs {
let applies = patterns(cap, "webviews")
.iter()
.any(|p| patterns_may_overlap(p, webview_label))
|| patterns(cap, "windows")
.iter()
.any(|p| glob_match(p, window_label));
let grants = cap
.get("permissions")
.and_then(|v| v.as_array())
.is_some_and(|a| !a.is_empty());
if applies && grants {
let id = cap
.get("identifier")
.and_then(|v| v.as_str())
.unwrap_or("<unnamed>");
return Ok(Some(id.to_owned()));
}
}
Ok(None)
}
pub fn build_gate(
policy: DevToolsPolicy,
main_label: &str,
registrations: &[ContentWebview],
devtools_compiled: bool,
) -> Result<DevToolsGate, DevToolsError> {
build_gate_with(policy, main_label, registrations, devtools_compiled, false)
}
pub fn build_gate_with(
policy: DevToolsPolicy,
main_label: &str,
registrations: &[ContentWebview],
devtools_compiled: bool,
dev_inspect: bool,
) -> Result<DevToolsGate, DevToolsError> {
let devtools_compiled = devtools_compiled || dev_inspect;
if registrations.is_empty() {
return Ok(DevToolsGate {
policy,
main_label: main_label.to_owned(),
content: BTreeSet::new(),
dev_inspect,
});
}
if policy != DevToolsPolicy::ContentOnly {
return Err(DevToolsError::NotContentOnly);
}
if !devtools_compiled {
return Err(DevToolsError::ContentDevtoolsNotCompiled);
}
let mut content = BTreeSet::new();
for reg in registrations {
if !valid_content_label(®.label) || !valid_label(®.window_label) {
return Err(DevToolsError::InvalidLabel {
label: reg.label.clone(),
});
}
if reg.label == main_label || glob_match(®.label, main_label) {
return Err(DevToolsError::MainWebview {
label: reg.label.clone(),
});
}
if !reg.app_acl_manifest {
return Err(DevToolsError::AppAclManifestMissing {
label: reg.label.clone(),
});
}
for doc in ®.capabilities {
if let Some(capability) = capability_granting_ipc(doc, ®.label, ®.window_label)? {
return Err(DevToolsError::ContentHasIpc {
label: reg.label.clone(),
capability,
});
}
}
content.insert(reg.label.clone());
}
Ok(DevToolsGate {
policy,
main_label: main_label.to_owned(),
content,
dev_inspect,
})
}
pub fn open_content_devtools<R: Runtime>(
app: &AppHandle<R>,
label: &str,
) -> Result<(), DevToolsError> {
let gate = app
.try_state::<DevToolsGate>()
.ok_or(DevToolsError::NotInstalled)?;
if gate.decide(label) == WebviewDecision::Deny {
return Err(DevToolsError::NotAContentWebview {
label: label.to_owned(),
});
}
#[cfg(any(feature = "content-devtools", feature = "dev-inspect"))]
{
let webview = app
.get_webview(label)
.ok_or_else(|| DevToolsError::WebviewMissing {
label: label.to_owned(),
})?;
webview.open_devtools();
Ok(())
}
#[cfg(not(any(feature = "content-devtools", feature = "dev-inspect")))]
Err(DevToolsError::ContentDevtoolsNotCompiled)
}
pub trait DevToolsSwitch {
fn set(&mut self, enabled: bool) -> Result<(), String>;
fn get(&self) -> Result<bool, String>;
}
pub fn enforce_switch(
label: &str,
decision: WebviewDecision,
switch: Option<&mut dyn DevToolsSwitch>,
) -> Result<(), DevToolsError> {
let deny = decision == WebviewDecision::Deny;
let refuse = |detail: String| DevToolsError::NativePolicyNotApplied {
label: label.to_owned(),
detail,
};
let Some(switch) = switch else {
return if deny {
Err(refuse("the webview has no settings object".to_owned()))
} else {
Ok(())
};
};
let set = switch.set(!deny);
if !deny {
return Ok(());
}
set.map_err(|e| refuse(format!("the setting was rejected: {e}")))?;
match switch.get() {
Ok(false) => Ok(()),
Ok(true) => Err(refuse(
"the setting is still on after it was turned off".to_owned(),
)),
Err(e) => Err(refuse(format!("the setting could not be read back: {e}"))),
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct KeyRules {
pub devtools: bool,
pub reload: bool,
pub history: bool,
pub zoom: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct KeyModifiers {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
}
pub fn is_blocked_accelerator(virtual_key: u32, held: KeyModifiers, rules: KeyRules) -> bool {
let KeyModifiers { ctrl, shift, alt } = held;
match virtual_key {
0x7B => rules.devtools,
0x49 | 0x4A | 0x43 => rules.devtools && ctrl && shift,
0x55 => rules.devtools && ctrl,
0x74 | 0xA8 => rules.reload,
0x52 => rules.reload && ctrl,
0x25 | 0x27 => rules.history && alt,
0xA6 | 0xA7 => rules.history,
0xBB | 0xBD | 0x30 | 0x6B | 0x6D | 0x60 => rules.zoom && ctrl,
_ => false,
}
}
pub fn apply_native<R: Runtime>(
webview: &Webview<R>,
decision: WebviewDecision,
) -> Result<(), DevToolsError> {
#[cfg(any(
debug_assertions,
feature = "content-devtools",
feature = "dev-inspect"
))]
if decision == WebviewDecision::Deny {
webview.close_devtools();
}
#[cfg(target_os = "macos")]
macos::apply(webview, decision);
#[cfg(windows)]
windows_impl::apply(webview, decision)?;
#[cfg(not(any(target_os = "macos", windows)))]
let _ = (webview, decision);
Ok(())
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
use objc2::msg_send;
use objc2::runtime::AnyObject;
use objc2_foundation::{ns_string, NSNumber};
pub fn apply<R: Runtime>(webview: &Webview<R>, decision: WebviewDecision) {
let allow = decision != WebviewDecision::Deny;
let _ = webview.with_webview(move |platform| unsafe {
let wk = &*(platform.inner() as *mut AnyObject);
let responds: bool = msg_send![wk, respondsToSelector: objc2::sel!(setInspectable:)];
if responds {
let (): () = msg_send![wk, setInspectable: allow];
}
let config: objc2::rc::Retained<AnyObject> = msg_send![wk, configuration];
let prefs: objc2::rc::Retained<AnyObject> = msg_send![&config, preferences];
let flag = NSNumber::numberWithBool(allow);
let (): () = msg_send![&prefs, setValue: Some(&*flag), forKey: ns_string!("developerExtrasEnabled")];
});
}
}
#[cfg(windows)]
pub(crate) use windows_impl::set_hardening_keys;
#[cfg(windows)]
mod windows_impl {
use super::*;
use crate::winapi::webview2_com::Microsoft::Web::WebView2::Win32::{
ICoreWebView2ContextMenuItem, ICoreWebView2ContextMenuItemCollection,
ICoreWebView2Settings, ICoreWebView2_11, COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN,
COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN,
};
use crate::winapi::webview2_com::{
AcceleratorKeyPressedEventHandler, ContextMenuRequestedEventHandler,
};
use crate::winapi::windows::core::{Interface, PWSTR};
use std::collections::HashMap;
use std::sync::Mutex;
use tauri::webview::PlatformWebview;
#[link(name = "ole32")]
unsafe extern "system" {
fn CoTaskMemFree(block: *const core::ffi::c_void);
}
#[link(name = "user32")]
unsafe extern "system" {
fn GetKeyState(virtual_key: i32) -> i16;
}
const VK_SHIFT: i32 = 0x10;
const VK_CONTROL: i32 = 0x11;
const VK_MENU: i32 = 0x12;
static KEY_RULES: Mutex<Option<HashMap<String, KeyRules>>> = Mutex::new(None);
fn update_key_rules(label: &str, change: impl FnOnce(&mut KeyRules)) {
let mut rules = KEY_RULES
.lock()
.unwrap_or_else(|poison| poison.into_inner());
change(
rules
.get_or_insert_with(HashMap::new)
.entry(label.to_owned())
.or_default(),
);
}
fn key_rules(label: &str) -> KeyRules {
let rules = KEY_RULES
.lock()
.unwrap_or_else(|poison| poison.into_inner());
rules
.as_ref()
.and_then(|map| map.get(label).copied())
.unwrap_or_default()
}
pub(crate) fn set_hardening_keys(label: &str, reload: bool, history: bool, zoom: bool) {
update_key_rules(label, |rules| {
rules.reload = reload;
rules.history = history;
rules.zoom = zoom;
});
}
fn key_is_down(virtual_key: i32) -> bool {
unsafe { GetKeyState(virtual_key) < 0 }
}
static HANDLERS: Mutex<Option<HashMap<String, Registered>>> = Mutex::new(None);
type Registered = (Option<i64>, Option<i64>);
fn is_inspect_item(item: &ICoreWebView2ContextMenuItem) -> bool {
let mut name = PWSTR::null();
unsafe {
if item.Name(&mut name).is_err() || name.is_null() {
return false;
}
let text = name.to_string().unwrap_or_default();
CoTaskMemFree(name.0 as *const core::ffi::c_void);
text.to_ascii_lowercase().contains("inspect")
}
}
unsafe fn retain_items(
items: &ICoreWebView2ContextMenuItemCollection,
remove: impl Fn(&ICoreWebView2ContextMenuItem) -> bool,
) -> usize {
let mut count: u32 = 0;
if items.Count(&mut count).is_err() {
return 0;
}
let mut i = count;
while i > 0 {
i -= 1;
if let Ok(item) = items.GetValueAtIndex(i) {
if remove(&item) {
let _ = items.RemoveValueAtIndex(i);
}
}
}
let mut left: u32 = 0;
let _ = items.Count(&mut left);
left as usize
}
struct LiveSwitch<'a>(&'a ICoreWebView2Settings);
impl DevToolsSwitch for LiveSwitch<'_> {
fn set(&mut self, enabled: bool) -> Result<(), String> {
unsafe { self.0.SetAreDevToolsEnabled(enabled) }.map_err(|e| e.to_string())
}
fn get(&self) -> Result<bool, String> {
let mut enabled = Default::default();
unsafe { self.0.AreDevToolsEnabled(&mut enabled) }.map_err(|e| e.to_string())?;
Ok(enabled.as_bool())
}
}
pub fn apply<R: Runtime>(
webview: &Webview<R>,
decision: WebviewDecision,
) -> Result<(), DevToolsError> {
let label = webview.label().to_owned();
let refused = webview.clone();
let (report, reported) = std::sync::mpsc::channel();
let on_main = label.clone();
let dispatched = webview.with_webview(move |platform| {
let outcome = apply_on_main(&platform, &on_main, decision);
if let Err(error) = &outcome {
eprintln!("{error}");
std::thread::spawn(move || {
let _ = refused.close();
});
}
let _ = report.send(outcome);
});
if let Err(error) = dispatched {
if decision != WebviewDecision::Deny {
return Ok(());
}
let error = DevToolsError::NativePolicyNotApplied {
label,
detail: format!("the webview could not be reached: {error}"),
};
eprintln!("{error}");
let _ = webview.close();
return Err(error);
}
reported.try_recv().unwrap_or(Ok(()))
}
fn apply_on_main(
platform: &PlatformWebview,
label: &str,
decision: WebviewDecision,
) -> Result<(), DevToolsError> {
let allow = decision != WebviewDecision::Deny;
let dev = decision == WebviewDecision::AllowDev;
unsafe {
let controller = platform.controller();
let core = controller.CoreWebView2().ok();
let settings = core.as_ref().and_then(|core| core.Settings().ok());
let mut live = settings.as_ref().map(LiveSwitch);
enforce_switch(
label,
decision,
live.as_mut().map(|live| live as &mut dyn DevToolsSwitch),
)?;
let Some(core) = core else {
return Ok(());
};
let wv11 = core.cast::<ICoreWebView2_11>().ok();
let mut handlers = HANDLERS.lock().unwrap_or_else(|poison| poison.into_inner());
let (old_menu, old_keys) = handlers
.get_or_insert_with(HashMap::new)
.remove(label)
.unwrap_or((None, None));
if let (Some(wv11), Some(token)) = (&wv11, old_menu) {
let _ = wv11.remove_ContextMenuRequested(token);
}
if let Some(token) = old_keys {
let _ = controller.remove_AcceleratorKeyPressed(token);
}
update_key_rules(label, |rules| rules.devtools = !allow);
let mut keys = None;
{
let label = label.to_owned();
let handler =
AcceleratorKeyPressedEventHandler::create(Box::new(move |_sender, args| {
let Some(args) = args else { return Ok(()) };
let mut kind = Default::default();
args.KeyEventKind(&mut kind)?;
if kind != COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN
&& kind != COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN
{
return Ok(());
}
let mut key: u32 = 0;
args.VirtualKey(&mut key)?;
let held = KeyModifiers {
ctrl: key_is_down(VK_CONTROL),
shift: key_is_down(VK_SHIFT),
alt: key_is_down(VK_MENU),
};
if is_blocked_accelerator(key, held, key_rules(&label)) {
args.SetHandled(true)?;
}
Ok(())
}));
let mut token: i64 = 0;
if controller
.add_AcceleratorKeyPressed(&handler, &mut token)
.is_ok()
{
keys = Some(token);
}
}
let mut menu = None;
if let (false, Some(wv11)) = (dev, &wv11) {
let handler =
ContextMenuRequestedEventHandler::create(Box::new(move |_sender, args| {
let Some(args) = args else { return Ok(()) };
let items = args.MenuItems()?;
if allow {
let left = retain_items(&items, |it| !is_inspect_item(it));
if left == 0 {
args.SetHandled(true)?;
}
} else {
retain_items(&items, is_inspect_item);
}
Ok(())
}));
let mut token: i64 = 0;
if wv11.add_ContextMenuRequested(&handler, &mut token).is_ok() {
menu = Some(token);
}
}
if let Some(map) = handlers.as_mut() {
map.insert(label.to_owned(), (menu, keys));
}
Ok(())
}
}
}