1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
//! Webview hardening: no stray webview context menu, reload / zoom /
//! history keys blocked, drag-drop cannot navigate, external links leave for the system browser.
//!
//! Three layers, because no single one covers every engine:
//! 1. an initialisation script (all frames by default) that cancels the DOM events;
//! 2. native webview settings (WKWebView preferences, WebView2 settings) for what JavaScript
//! cannot cancel (Cmd+Opt+I inspector, WebView2 accelerator keys, pinch zoom, swipe);
//! 3. the Rust navigation hook (`NavPolicy`) that catches whatever navigates anyway.
use serde::Serialize;
use tauri::{Runtime, Webview};
use crate::devtools::WebviewDecision;
/// Frame scope for the full shell plugin's DOM hardening script.
/// Native webview settings & Rust navigation policy are configured separately.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum HardeningScript {
/// Inject into every frame, including document iframes (legacy default).
#[default]
AllFrames,
/// Inject into the top frame of each webview only.
TopFrameOnly,
/// Do not inject the DOM hardening script.
Off,
}
#[derive(Debug, Clone)]
pub struct Hardening {
/// Cancel `contextmenu` outside designated surfaces.
pub block_context_menu: bool,
/// Keep the native Cut/Copy/Paste menu inside inputs, textareas and contenteditable
/// (HeardRight behaviour). Mark any other surface with `data-rk-native-menu`.
pub allow_editable_menu: bool,
/// Block F5 and Cmd/Ctrl+R (+Shift) reload shortcuts only. Print and view-source have
/// their own switches below, so an app can keep reload blocked without losing them.
pub block_reload: bool,
/// Block Cmd/Ctrl+P (print). Split from `block_reload`: MailRight needs print while
/// reload stays blocked.
pub block_print: bool,
/// Block Cmd/Ctrl+U (view source) outside editable surfaces. Inside an input, textarea or
/// contenteditable the same chord is the underline command and is never cancelled, so a
/// compose editor keeps underline while view-source stays blocked elsewhere. Inspector
/// shortcuts (F12, Cmd/Ctrl+Shift+I/J/C, Cmd+Opt+I/J/C) are not governed by this switch:
/// the DevTools layer always cancels them outside registered content webviews.
pub block_view_source: bool,
/// Block Cmd/Ctrl +/-/0, ctrl+wheel and trackpad pinch zoom.
pub block_zoom: bool,
/// Block Alt+Left/Right, Cmd+[ / ], browser back/forward keys and mouse buttons 4/5.
pub block_history_nav: bool,
/// A dropped file must never navigate the webview; only `data-rk-dropzone` accepts drops.
pub block_drop_navigation: bool,
/// Route http(s)/mailto link clicks and `window.open` to the system browser.
pub intercept_external_links: bool,
}
impl Default for Hardening {
fn default() -> Self {
Self {
block_context_menu: true,
allow_editable_menu: true,
block_reload: true,
// Compatible defaults: `block_reload` used to cover print and view-source too, so
// the split switches default on and `Hardening::default()` blocks exactly what it
// blocked before.
block_print: true,
block_view_source: true,
block_zoom: true,
block_history_nav: true,
block_drop_navigation: true,
intercept_external_links: true,
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ScriptConfig {
block_context_menu: bool,
allow_editable_menu: bool,
block_reload: bool,
block_print: bool,
block_view_source: bool,
block_zoom: bool,
block_history_nav: bool,
content_devtools_labels: Vec<String>,
block_drop_navigation: bool,
intercept_external_links: bool,
}
const SCRIPT_BODY: &str = include_str!("harden.js");
impl Hardening {
/// The initialisation script for this configuration (idempotent per document). DevTools
/// shortcuts are always cancelled; see [`Hardening::script_for`] for content webviews.
pub fn script(&self) -> String {
self.script_for(&[])
}
/// Like [`Hardening::script`], for an app whose `DevToolsPolicy::ContentOnly` registered the
/// webviews in `content_labels`. In those webviews the script leaves the context menu and the
/// DevTools shortcuts alone (the native layer decides what they may do); everywhere else
/// F12, Ctrl+Shift+I/J/C and Cmd+Opt+I/J/C/U are cancelled.
pub fn script_for(&self, content_labels: &[String]) -> String {
let cfg = ScriptConfig {
block_context_menu: self.block_context_menu,
allow_editable_menu: self.allow_editable_menu,
block_reload: self.block_reload,
block_print: self.block_print,
block_view_source: self.block_view_source,
block_zoom: self.block_zoom,
block_history_nav: self.block_history_nav,
content_devtools_labels: content_labels.to_vec(),
block_drop_navigation: self.block_drop_navigation,
intercept_external_links: self.intercept_external_links,
};
let json = serde_json::to_string(&cfg).expect("static config serialises");
SCRIPT_BODY.replace("__RK_SHELL_CONFIG__", &json)
}
}
/// Apply the native (non-DOM, non-DevTools) hardening to a freshly created webview. The DevTools
/// settings are applied separately and unconditionally by [`crate::devtools::apply_native`];
/// `decision` only tells this layer to leave a content webview's context menu to that layer.
pub fn apply_native<R: Runtime>(webview: &Webview<R>, h: &Hardening, decision: WebviewDecision) {
#[cfg(target_os = "macos")]
macos::apply(webview, h);
#[cfg(windows)]
windows_impl::apply(webview, h, decision);
#[cfg(not(windows))]
let _ = decision;
#[cfg(not(any(target_os = "macos", windows)))]
let _ = (webview, h);
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
use objc2::msg_send;
use objc2::runtime::AnyObject;
pub fn apply<R: Runtime>(webview: &Webview<R>, h: &Hardening) {
let zoom_off = h.block_zoom;
let nav_off = h.block_history_nav;
let _ = webview.with_webview(move |platform| unsafe {
// SAFETY: Tauri hands over the live WKWebView for this webview; used only inside this
// main-thread closure.
let wk = &*(platform.inner() as *mut AnyObject);
if zoom_off {
let (): () = msg_send![wk, setAllowsMagnification: false];
}
if nav_off {
let (): () = msg_send![wk, setAllowsBackForwardNavigationGestures: false];
}
});
}
}
#[cfg(windows)]
mod windows_impl {
use super::*;
use crate::winapi::webview2_com::ContextMenuRequestedEventHandler;
use crate::winapi::webview2_com::Microsoft::Web::WebView2::Win32::{
ICoreWebView2Settings5, ICoreWebView2Settings6, ICoreWebView2_11,
};
use crate::winapi::windows::core::Interface;
use crate::winapi::windows::core::BOOL;
/// WebView2 side of the same policy. NOT compiled on this (macOS) host: compile and verify on
/// the Windows agent with `rightkit cargo check -p rightkit-tauri-shell` (see API.md).
pub fn apply<R: Runtime>(webview: &Webview<R>, h: &Hardening, decision: WebviewDecision) {
let h = h.clone();
let content = decision != WebviewDecision::Deny;
// F5, Ctrl+R, Alt+Left and the zoom keys are stopped one by one by the shell's single key
// handler (the DevTools layer adds it); browser keys are never turned off as a whole, so
// print and find keep working.
crate::devtools::set_hardening_keys(
webview.label(),
h.block_reload,
h.block_history_nav,
h.block_zoom,
);
let _ = webview.with_webview(move |platform| unsafe {
let Ok(core) = platform.controller().CoreWebView2() else {
return;
};
let Ok(settings) = core.Settings() else {
return;
};
let _ = settings.SetIsStatusBarEnabled(false);
if h.block_zoom {
let _ = settings.SetIsZoomControlEnabled(false);
if let Ok(s5) = settings.cast::<ICoreWebView2Settings5>() {
let _ = s5.SetIsPinchZoomEnabled(false);
}
}
if h.block_history_nav {
if let Ok(s6) = settings.cast::<ICoreWebView2Settings6>() {
let _ = s6.SetIsSwipeNavigationEnabled(false);
}
}
if h.block_context_menu && !content {
// Content webviews: the DevTools layer owns their menu. Keep default menus only so the editable-field menu can survive; every other
// target is marked handled (no menu shown).
let allow_editable = h.allow_editable_menu;
if let Ok(wv11) = core.cast::<ICoreWebView2_11>() {
let handler =
ContextMenuRequestedEventHandler::create(Box::new(move |_sender, args| {
let Some(args) = args else { return Ok(()) };
let target = args.ContextMenuTarget()?;
let mut editable = BOOL(0);
let _ = target.IsEditable(&mut editable);
if !(allow_editable && editable.as_bool()) {
args.SetHandled(true)?;
}
Ok(())
}));
let mut token: i64 = 0;
let _ = wv11.add_ContextMenuRequested(&handler, &mut token);
}
}
});
}
}