open external links in native shells
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserView, BrowserWindow, nativeImage, session } from "electron"
|
||||
import { app, BrowserView, BrowserWindow, nativeImage, session, shell } from "electron"
|
||||
import { existsSync } from "fs"
|
||||
import { dirname, join } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
@@ -89,6 +89,56 @@ function loadLoadingScreen(window: BrowserWindow) {
|
||||
})
|
||||
}
|
||||
|
||||
function getAllowedRendererOrigins(): string[] {
|
||||
const origins = new Set<string>()
|
||||
const rendererCandidates = [currentCliUrl, process.env.VITE_DEV_SERVER_URL, process.env.ELECTRON_RENDERER_URL]
|
||||
for (const candidate of rendererCandidates) {
|
||||
if (!candidate) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
origins.add(new URL(candidate).origin)
|
||||
} catch (error) {
|
||||
console.warn("[cli] failed to parse origin for", candidate, error)
|
||||
}
|
||||
}
|
||||
return Array.from(origins)
|
||||
}
|
||||
|
||||
function shouldOpenExternally(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return true
|
||||
}
|
||||
const allowedOrigins = getAllowedRendererOrigins()
|
||||
return !allowedOrigins.includes(parsed.origin)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function setupNavigationGuards(window: BrowserWindow) {
|
||||
const handleExternal = (url: string) => {
|
||||
shell.openExternal(url).catch((error) => console.error("[cli] failed to open external URL", url, error))
|
||||
}
|
||||
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (shouldOpenExternally(url)) {
|
||||
handleExternal(url)
|
||||
return { action: "deny" }
|
||||
}
|
||||
return { action: "allow" }
|
||||
})
|
||||
|
||||
window.webContents.on("will-navigate", (event, url) => {
|
||||
if (shouldOpenExternally(url)) {
|
||||
event.preventDefault()
|
||||
handleExternal(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let cachedPreloadPath: string | null = null
|
||||
function getPreloadPath() {
|
||||
if (cachedPreloadPath && existsSync(cachedPreloadPath)) {
|
||||
@@ -153,6 +203,8 @@ function createWindow() {
|
||||
},
|
||||
})
|
||||
|
||||
setupNavigationGuards(mainWindow)
|
||||
|
||||
if (isMac) {
|
||||
mainWindow.webContents.session.setSpellCheckerEnabled(false)
|
||||
}
|
||||
|
||||
207
packages/tauri-app/Cargo.lock
generated
207
packages/tauri-app/Cargo.lock
generated
@@ -80,6 +80,79 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix 1.1.2",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix 1.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
@@ -91,6 +164,30 @@ dependencies = [
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix 1.1.2",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -191,6 +288,19 @@ dependencies = [
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
@@ -382,6 +492,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"thiserror 1.0.69",
|
||||
"which",
|
||||
]
|
||||
@@ -1357,6 +1468,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1659,6 +1776,25 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -2316,6 +2452,18 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2386,6 +2534,12 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -2538,6 +2692,17 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -2570,6 +2735,20 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
@@ -3725,6 +3904,28 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.2",
|
||||
"open",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
"windows",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.9.1"
|
||||
@@ -5216,8 +5417,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
|
||||
@@ -19,3 +19,4 @@ which = "4"
|
||||
libc = "0.2"
|
||||
tauri-plugin-dialog = "2"
|
||||
dirs = "5"
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:allow-open"
|
||||
"dialog:allow-open",
|
||||
"opener:allow-default-urls"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ mod cli_manager;
|
||||
use cli_manager::{CliProcessManager, CliStatus};
|
||||
use serde_json::json;
|
||||
use tauri::menu::Menu;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri::plugin::Builder as PluginBuilder;
|
||||
use tauri::webview::Webview;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
@@ -32,9 +36,38 @@ fn is_dev_mode() -> bool {
|
||||
cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok()
|
||||
}
|
||||
|
||||
fn should_allow_internal(url: &Url) -> bool {
|
||||
match url.scheme() {
|
||||
"tauri" | "asset" | "file" => true,
|
||||
"http" | "https" => matches!(url.host_str(), Some("127.0.0.1" | "localhost")),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn intercept_navigation<R: Runtime>(webview: &Webview<R>, url: &Url) -> bool {
|
||||
if should_allow_internal(url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Err(err) = webview
|
||||
.app_handle()
|
||||
.opener()
|
||||
.open_url(url.as_str(), None::<&str>)
|
||||
{
|
||||
eprintln!("[tauri] failed to open external link {}: {}", url, err);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let navigation_guard = PluginBuilder::new("external-link-guard")
|
||||
.on_navigation(|webview, url| intercept_navigation(webview, url))
|
||||
.build();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(navigation_guard)
|
||||
.manage(AppState {
|
||||
manager: CliProcessManager::new(),
|
||||
})
|
||||
@@ -45,10 +78,7 @@ fn main() {
|
||||
let manager = app.state::<AppState>().manager.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = manager.start(app_handle.clone(), dev_mode) {
|
||||
let _ = app_handle.emit(
|
||||
"cli:error",
|
||||
json!({"message": err.to_string()}),
|
||||
);
|
||||
let _ = app_handle.emit("cli:error", json!({"message": err.to_string()}));
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
@@ -59,9 +89,21 @@ fn main() {
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| {
|
||||
match event {
|
||||
tauri::RunEvent::ExitRequested { .. } => {
|
||||
.run(|app_handle, event| match event {
|
||||
tauri::RunEvent::ExitRequested { .. } => {
|
||||
let app = app_handle.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(state) = app.try_state::<AppState>() {
|
||||
let _ = state.manager.stop();
|
||||
}
|
||||
app.exit(0);
|
||||
});
|
||||
}
|
||||
tauri::RunEvent::WindowEvent {
|
||||
event: tauri::WindowEvent::Destroyed,
|
||||
..
|
||||
} => {
|
||||
if app_handle.webview_windows().len() <= 1 {
|
||||
let app = app_handle.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(state) = app.try_state::<AppState>() {
|
||||
@@ -70,19 +112,8 @@ fn main() {
|
||||
app.exit(0);
|
||||
});
|
||||
}
|
||||
tauri::RunEvent::WindowEvent { event: tauri::WindowEvent::Destroyed, .. } => {
|
||||
if app_handle.webview_windows().len() <= 1 {
|
||||
let app = app_handle.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(state) = app.try_state::<AppState>() {
|
||||
let _ = state.manager.stop();
|
||||
}
|
||||
app.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user