接入桌面壳受控站内导航
Tauri 桌面壳支持 navigation.openNativePage 的同源 H5 路由跳转 补充桌面壳导航白名单校验和运行时能力检查 更新宿主壳方案和项目共享决策记录
This commit is contained in:
@@ -4,6 +4,7 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tauri::Manager;
|
||||
use tauri::Url;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
@@ -77,6 +78,7 @@ fn capabilities() -> Vec<&'static str> {
|
||||
"host.getRuntime",
|
||||
"share.open",
|
||||
"share.setTarget",
|
||||
"navigation.openNativePage",
|
||||
"app.openExternalUrl",
|
||||
"app.setTitle",
|
||||
"clipboard.writeText",
|
||||
@@ -166,6 +168,21 @@ fn normalize_external_url(raw_url: &str) -> Option<String> {
|
||||
Some(url.to_string())
|
||||
}
|
||||
|
||||
fn normalize_native_page_url(raw_url: &str) -> Option<Url> {
|
||||
let url = raw_url.trim();
|
||||
if url.is_empty() || url.chars().any(char::is_control) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_url = Url::parse(WEB_APP_ORIGIN).ok()?;
|
||||
let normalized_url = base_url.join(url).ok()?;
|
||||
if normalized_url.scheme() != "https" || normalized_url.origin() != base_url.origin() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(normalized_url)
|
||||
}
|
||||
|
||||
fn normalize_window_title(raw_title: &str) -> Option<String> {
|
||||
let title = raw_title.trim();
|
||||
if title.is_empty() || title.chars().any(char::is_control) {
|
||||
@@ -179,7 +196,11 @@ fn normalize_export_file_name(raw_file_name: &str) -> String {
|
||||
let mut file_name = String::new();
|
||||
let mut last_was_space = false;
|
||||
|
||||
for character in raw_file_name.trim().chars().take(EXPORT_FILE_NAME_MAX_LENGTH) {
|
||||
for character in raw_file_name
|
||||
.trim()
|
||||
.chars()
|
||||
.take(EXPORT_FILE_NAME_MAX_LENGTH)
|
||||
{
|
||||
if character.is_control()
|
||||
|| matches!(
|
||||
character,
|
||||
@@ -377,6 +398,29 @@ async fn host_bridge_request(
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
}
|
||||
}
|
||||
"navigation.openNativePage" => {
|
||||
let url = match required_string_payload(&request, "url")
|
||||
.ok()
|
||||
.and_then(normalize_native_page_url)
|
||||
{
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return failed(
|
||||
request.id,
|
||||
"invalid_request",
|
||||
"url must use an allowed same-origin H5 route",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match app.get_webview_window("main") {
|
||||
Some(window) => match window.navigate(url) {
|
||||
Ok(()) => ok(request.id, json!(true)),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id, "host_error", "main window not found"),
|
||||
}
|
||||
}
|
||||
"clipboard.writeText" => {
|
||||
let text = match required_string_payload(&request, "text") {
|
||||
Ok(text) => text,
|
||||
@@ -530,6 +574,10 @@ mod tests {
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("share.setTarget")));
|
||||
assert!(result["capabilities"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("navigation.openNativePage")));
|
||||
assert!(result["capabilities"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -590,6 +638,39 @@ mod tests {
|
||||
assert_eq!(normalize_external_url("/relative/path"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_page_url_normalization_allows_same_origin_routes() {
|
||||
assert_eq!(
|
||||
normalize_native_page_url("/works/detail?work=PZ-1")
|
||||
.expect("same-origin route")
|
||||
.as_str(),
|
||||
"https://app.genarrative.world/works/detail?work=PZ-1"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_native_page_url("works/detail?work=PZ-1")
|
||||
.expect("relative route")
|
||||
.as_str(),
|
||||
"https://app.genarrative.world/works/detail?work=PZ-1"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_native_page_url("https://app.genarrative.world/works/detail?work=PZ-1")
|
||||
.expect("absolute same-origin route")
|
||||
.as_str(),
|
||||
"https://app.genarrative.world/works/detail?work=PZ-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_page_url_normalization_rejects_unsafe_routes() {
|
||||
assert_eq!(normalize_native_page_url("https://example.com/works"), None);
|
||||
assert_eq!(normalize_native_page_url("//example.com/works"), None);
|
||||
assert_eq!(normalize_native_page_url("javascript:alert(1)"), None);
|
||||
assert_eq!(
|
||||
normalize_native_page_url("https://app.genarrative.world/\nnext"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_title_normalization_requires_visible_text() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "npm --prefix ../.. run dev:web",
|
||||
"beforeBuildCommand": "npm --prefix ../.. run build:raw && npm run typecheck",
|
||||
"devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,share.open,share.setTarget,app.openExternalUrl,app.setTitle,clipboard.writeText,file.exportText",
|
||||
"devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,clipboard.writeText,file.exportText",
|
||||
"frontendDist": "../../../dist"
|
||||
},
|
||||
"app": {
|
||||
@@ -14,7 +14,7 @@
|
||||
{
|
||||
"create": false,
|
||||
"label": "main",
|
||||
"url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,share.open,share.setTarget,app.openExternalUrl,app.setTitle,clipboard.writeText,file.exportText",
|
||||
"url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,clipboard.writeText,file.exportText",
|
||||
"title": "Genarrative",
|
||||
"width": 1280,
|
||||
"height": 820,
|
||||
|
||||
Reference in New Issue
Block a user