refactor(core): use webview's URI schemes for IPC (#7170)

Co-authored-by: chip <chip@chip.sh>
This commit is contained in:
Lucas Fernandes Nogueira
2023-08-10 06:12:38 -07:00
committed by GitHub
parent 85efd0ae43
commit fbeb5b9185
80 changed files with 2088 additions and 1168 deletions

5
.changes/channel-rust.md Normal file
View File

@@ -0,0 +1,5 @@
---
"tauri": patch:enhance
---
Added `Channel::new` allowing communication from a mobile plugin with Rust.

View File

@@ -0,0 +1,6 @@
---
"tauri": patch:enhance
"tauri-utils": patch:enhance
---
Use custom protocols on the IPC implementation to enhance performance.

6
.changes/ipc-refactor.md Normal file
View File

@@ -0,0 +1,6 @@
---
"tauri": patch:breaking
"tauri-macros": patch:breaking
---
Moved `tauri::api::ipc` to `tauri::ipc` and refactored all types.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
Removed the `linux-protocol-headers` feature (now always enabled) and added `linux-ipc-protocol`.

View File

@@ -0,0 +1,5 @@
---
"tauri-runtime-wry": patch:breaking
---
Removed the `linux-headers` feature (now always enabled) and added `linux-protocol-body`.

6
.changes/migrate-csp.md Normal file
View File

@@ -0,0 +1,6 @@
---
"tauri-cli": patch:enhance
"@tauri-apps/cli": patch:enhance
---
Update migrate command to update the configuration CSP to include `ipc:` on the `connect-src` directive, needed by the new IPC using custom protocols.

View File

@@ -50,7 +50,7 @@ jobs:
clippy:
- { args: '', key: 'empty' }
- {
args: '--features compression,wry,linux-protocol-headers,isolation,custom-protocol,system-tray,test',
args: '--features compression,wry,isolation,custom-protocol,system-tray,test',
key: 'all'
}
- { args: '--features custom-protocol', key: 'custom-protocol' }

View File

@@ -72,7 +72,7 @@ jobs:
key: no-default
}
- {
args: --features compression,wry,linux-protocol-headers,isolation,custom-protocol,system-tray,test,
args: --features compression,wry,isolation,custom-protocol,system-tray,test,
key: all
}
@@ -98,6 +98,11 @@ jobs:
workspaces: core -> ../target
save-if: ${{ matrix.features.key == 'all' }}
- name: Downgrade crates with MSRV conflict
# The --precise flag can only be used once per invocation.
run: |
cargo update -p time --precise 0.3.23
- name: test
uses: actions-rs/cargo@v1
with:

View File

@@ -211,7 +211,7 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
use #root::command::private::*;
// prevent warnings when the body is a `compile_error!` or if the command has no arguments
#[allow(unused_variables)]
let #root::Invoke { message: #message, resolver: #resolver } = $invoke;
let #root::ipc::Invoke { message: #message, resolver: #resolver } = $invoke;
#body
}};

View File

@@ -48,4 +48,4 @@ macos-private-api = [
"tauri-runtime/macos-private-api"
]
objc-exception = [ "wry/objc-exception" ]
linux-headers = [ ]
linux-protocol-body = [ "wry/linux-body", "webkit2gtk/v2_40" ]

View File

@@ -3085,17 +3085,14 @@ fn create_webview<T: UserEvent>(
webview_attributes,
uri_scheme_protocols,
mut window_builder,
ipc_handler,
label,
ipc_handler,
url,
menu_ids,
#[cfg(target_os = "android")]
on_webview_created,
..
} = pending;
let webview_id_map = context.webview_id_map.clone();
#[cfg(windows)]
let proxy = context.proxy.clone();
let window_event_listeners = WindowEventListeners::default();
@@ -3108,6 +3105,8 @@ fn create_webview<T: UserEvent>(
#[cfg(windows)]
let window_theme = window_builder.inner.window.preferred_theme;
#[cfg(windows)]
let proxy = context.proxy.clone();
#[cfg(target_os = "macos")]
{
@@ -3130,7 +3129,7 @@ fn create_webview<T: UserEvent>(
};
let window = window_builder.inner.build(event_loop).unwrap();
webview_id_map.insert(window.id(), window_id);
context.webview_id_map.insert(window.id(), window_id);
if window_builder.center {
let _ = center_window(&window, window.inner_size());
@@ -3157,17 +3156,18 @@ fn create_webview<T: UserEvent>(
}
#[cfg(windows)]
if let Some(additional_browser_args) = webview_attributes.additional_browser_args {
webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args);
}
{
if let Some(additional_browser_args) = webview_attributes.additional_browser_args {
webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args);
}
#[cfg(windows)]
if let Some(theme) = window_theme {
webview_builder = webview_builder.with_theme(match theme {
WryTheme::Dark => wry::webview::Theme::Dark,
WryTheme::Light => wry::webview::Theme::Light,
_ => wry::webview::Theme::Light,
});
if let Some(theme) = window_theme {
webview_builder = webview_builder.with_theme(match theme {
WryTheme::Dark => wry::webview::Theme::Dark,
WryTheme::Light => wry::webview::Theme::Light,
_ => wry::webview::Theme::Light,
});
}
}
if let Some(handler) = ipc_handler {
@@ -3178,6 +3178,7 @@ fn create_webview<T: UserEvent>(
handler,
));
}
for (scheme, protocol) in uri_scheme_protocols {
webview_builder = webview_builder.with_custom_protocol(scheme, move |wry_request| {
protocol(&HttpRequestWrapper::from(wry_request).0)
@@ -3254,7 +3255,7 @@ fn create_webview<T: UserEvent>(
unsafe {
controller.add_GotFocus(
&FocusChangedEventHandler::create(Box::new(move |_, _| {
let _ = proxy_.send_event(Message::Webview(
let _ = proxy.send_event(Message::Webview(
window_id,
WebviewMessage::WebviewEvent(WebviewEvent::Focused(true)),
));
@@ -3267,7 +3268,7 @@ fn create_webview<T: UserEvent>(
unsafe {
controller.add_LostFocus(
&FocusChangedEventHandler::create(Box::new(move |_, _| {
let _ = proxy.send_event(Message::Webview(
let _ = proxy_.send_event(Message::Webview(
window_id,
WebviewMessage::WebviewEvent(WebviewEvent::Focused(false)),
));

View File

@@ -8,7 +8,8 @@
* isolation frame -> main frame = isolation message
*/
;(async function () {
;
(async function () {
/**
* Sends the message to the isolation frame.
* @param {any} message
@@ -38,34 +39,52 @@
* @return {Promise<{nonce: number[], payload: number[]}>}
*/
async function encrypt(data) {
let algorithm = Object.create(null)
const algorithm = Object.create(null)
algorithm.name = 'AES-GCM'
algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
let encoder = new TextEncoder()
let payloadRaw = encoder.encode(__RAW_stringify_ipc_message_fn__(data))
const encoder = new TextEncoder()
const encoded = encoder.encode(__RAW_process_ipc_message_fn__(data).data)
return window.crypto.subtle
.encrypt(algorithm, aesGcmKey, payloadRaw)
.encrypt(algorithm, aesGcmKey, encoded)
.then((payload) => {
let result = Object.create(null)
const result = Object.create(null)
result.nonce = Array.from(new Uint8Array(algorithm.iv))
result.payload = Array.from(new Uint8Array(payload))
return result
})
}
/**
* Detects if a message event is a valid isolation message.
*
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation message
* @return {boolean} - if the event was a valid isolation message
*/
function isIsolationMessage(data) {
if (typeof data === 'object' && typeof data.payload === 'object') {
const keys = data.payload ? Object.keys(data.payload) : []
return (
keys.length > 0 &&
keys.every((key) => key === 'nonce' || key === 'payload')
)
}
return false
}
/**
* Detect if a message event is a valid isolation payload.
*
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation payload
* @return boolean
*/
function isIsolationPayload(event) {
function isIsolationPayload(data) {
return (
typeof event.data === 'object' &&
'callback' in event.data &&
'error' in event.data
typeof data === 'object' &&
'callback' in data &&
'error' in data &&
!isIsolationMessage(data)
)
}
@@ -74,7 +93,7 @@
* @param {MessageEvent<any>} event
*/
async function payloadHandler(event) {
if (!isIsolationPayload(event)) {
if (!isIsolationPayload(event.data)) {
return
}
@@ -85,8 +104,13 @@
data = await window.__TAURI_ISOLATION_HOOK__(data)
}
const encrypted = await encrypt(data)
sendMessage(encrypted)
const message = Object.create(null)
message.cmd = data.cmd
message.callback = data.callback
message.error = data.error
message.options = data.options
message.payload = await encrypt(data.payload)
sendMessage(message)
}
window.addEventListener('message', payloadHandler, false)

View File

@@ -96,16 +96,14 @@ impl Keys {
}
/// Decrypts a message using the generated keys.
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<String, Error> {
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<Vec<u8>, Error> {
let RawIsolationPayload { nonce, payload } = raw;
let nonce: [u8; 12] = nonce.as_ref().try_into()?;
let bytes = self
self
.aes_gcm
.key
.decrypt(Nonce::from_slice(&nonce), payload.as_ref())
.map_err(|_| self::Error::Aes)?;
String::from_utf8(bytes).map_err(Into::into)
.map_err(|_| self::Error::Aes)
}
}
@@ -116,11 +114,11 @@ pub struct RawIsolationPayload<'a> {
payload: Cow<'a, [u8]>,
}
impl<'a> TryFrom<&'a str> for RawIsolationPayload<'a> {
impl<'a> TryFrom<&'a Vec<u8>> for RawIsolationPayload<'a> {
type Error = Error;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
serde_json::from_str(value).map_err(Into::into)
fn try_from(value: &'a Vec<u8>) -> Result<Self, Self::Error> {
serde_json::from_slice(value).map_err(Into::into)
}
}
@@ -141,9 +139,9 @@ pub struct IsolationJavascriptCodegen {
pub struct IsolationJavascriptRuntime<'a> {
/// The key used on the Rust backend and the Isolation Javascript
pub runtime_aes_gcm_key: &'a [u8; 32],
/// The function that stringifies a IPC message.
/// The function that processes the IPC message.
#[raw]
pub stringify_ipc_message_fn: &'a str,
pub process_ipc_message_fn: &'a str,
}
#[cfg(test)]

View File

@@ -61,6 +61,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "json", "st
bytes = { version = "1", features = [ "serde" ] }
raw-window-handle = "0.5"
glob = "0.3"
mime = "0.3"
data-url = { version = "0.2", optional = true }
serialize-to-javascript = "=0.1.1"
infer = { version = "0.9", optional = true }
@@ -118,7 +119,7 @@ test = [ ]
compression = [ "tauri-macros/compression", "tauri-utils/compression" ]
wry = [ "tauri-runtime-wry" ]
objc-exception = [ "tauri-runtime-wry/objc-exception" ]
linux-protocol-headers = [ "tauri-runtime-wry/linux-headers", "webkit2gtk/v2_36" ]
linux-ipc-protocol = [ "tauri-runtime-wry/linux-protocol-body", "webkit2gtk/v2_40" ]
isolation = [ "tauri-utils/isolation", "tauri-macros/isolation" ]
custom-protocol = [ "tauri-macros/custom-protocol" ]
native-tls = [ "reqwest/native-tls" ]

View File

@@ -50,6 +50,11 @@ fn main() {
alias("desktop", !mobile);
alias("mobile", mobile);
alias(
"ipc_custom_protocol",
target_os != "android" && (target_os != "linux" || has_feature("linux-ipc-protocol")),
);
let checked_features_out_path = Path::new(&var("OUT_DIR").unwrap()).join("checked_features");
std::fs::write(
checked_features_out_path,

View File

@@ -14,6 +14,7 @@ class Invoke(
val callback: Long,
val error: Long,
private val sendResponse: (callback: Long, data: PluginResult?) -> Unit,
private val sendChannelData: (channelId: Long, data: PluginResult) -> Unit,
val data: JSObject) {
fun resolve(data: JSObject?) {
@@ -205,6 +206,6 @@ class Invoke(
fun getChannel(name: String): Channel? {
val channelDef = getString(name, "")
val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: return null
return Channel(callback) { res -> sendResponse(callback, PluginResult(res)) }
return Channel(callback) { res -> sendChannelData(callback, PluginResult(res)) }
}
}

View File

@@ -85,15 +85,6 @@ class PluginManager(val activity: AppCompatActivity) {
}
}
@JniMethod
fun postIpcMessage(webView: WebView, pluginId: String, command: String, data: JSObject, callback: Long, error: Long) {
val invoke = Invoke(callback, command, callback, error, { fn, result ->
webView.evaluateJavascript("window['_$fn']($result)", null)
}, data)
dispatchPluginMessage(invoke, pluginId)
}
@JniMethod
fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) {
val successId = 0L
@@ -107,6 +98,8 @@ class PluginManager(val activity: AppCompatActivity) {
error = result
}
handlePluginResponse(id, success?.toString(), error?.toString())
}, { channelId, payload ->
sendChannelData(channelId, payload.toString())
}, data)
dispatchPluginMessage(invoke, pluginId)
@@ -140,4 +133,5 @@ class PluginManager(val activity: AppCompatActivity) {
}
private external fun handlePluginResponse(id: Int, success: String?, error: String?)
private external fun sendChannelData(id: Long, data: String)
}

View File

@@ -6,8 +6,8 @@ public class Channel {
public let id: UInt64
let handler: (JsonValue) -> Void
public init(callback: UInt64, handler: @escaping (JsonValue) -> Void) {
self.id = callback
public init(id: UInt64, handler: @escaping (JsonValue) -> Void) {
self.id = id
self.handler = handler
}

View File

@@ -8,76 +8,92 @@ import UIKit
let CHANNEL_PREFIX = "__CHANNEL__:"
@objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer {
public var dictionaryRepresentation: NSDictionary {
return data as NSDictionary
}
public var dictionaryRepresentation: NSDictionary {
return data as NSDictionary
}
public static var jsDateFormatter: ISO8601DateFormatter = {
return ISO8601DateFormatter()
}()
public static var jsDateFormatter: ISO8601DateFormatter = {
return ISO8601DateFormatter()
}()
public var command: String
var callback: UInt64
var error: UInt64
public var data: JSObject
var sendResponse: (UInt64, JsonValue?) -> Void
public var data: JSObject
var sendResponse: (UInt64, JsonValue?) -> Void
var sendChannelData: (UInt64, JsonValue) -> Void
public init(command: String, callback: UInt64, error: UInt64, sendResponse: @escaping (UInt64, JsonValue?) -> Void, data: JSObject?) {
public init(
command: String, callback: UInt64, error: UInt64,
sendResponse: @escaping (UInt64, JsonValue?) -> Void,
sendChannelData: @escaping (UInt64, JsonValue) -> Void, data: JSObject?
) {
self.command = command
self.callback = callback
self.error = error
self.data = data ?? [:]
self.sendResponse = sendResponse
}
self.data = data ?? [:]
self.sendResponse = sendResponse
self.sendChannelData = sendChannelData
}
public func resolve() {
sendResponse(callback, nil)
}
public func resolve() {
sendResponse(callback, nil)
}
public func resolve(_ data: JsonObject) {
resolve(.dictionary(data))
}
public func resolve(_ data: JsonObject) {
resolve(.dictionary(data))
}
public func resolve(_ data: JsonValue) {
sendResponse(callback, data)
}
public func resolve(_ data: JsonValue) {
sendResponse(callback, data)
}
public func reject(_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil) {
let payload: NSMutableDictionary = ["message": message, "code": code ?? "", "error": error ?? ""]
if let data = data {
switch data {
case .dictionary(let dict):
for entry in dict {
payload[entry.key] = entry.value
}
}
}
sendResponse(self.error, .dictionary(payload as! JsonObject))
}
public func reject(
_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil
) {
let payload: NSMutableDictionary = [
"message": message, "code": code ?? "", "error": error ?? "",
]
if let data = data {
switch data {
case .dictionary(let dict):
for entry in dict {
payload[entry.key] = entry.value
}
}
}
sendResponse(self.error, .dictionary(payload as! JsonObject))
}
public func unimplemented() {
unimplemented("not implemented")
}
public func unimplemented() {
unimplemented("not implemented")
}
public func unimplemented(_ message: String) {
sendResponse(error, .dictionary(["message": message]))
}
public func unimplemented(_ message: String) {
sendResponse(error, .dictionary(["message": message]))
}
public func unavailable() {
unavailable("not available")
}
public func unavailable() {
unavailable("not available")
}
public func unavailable(_ message: String) {
sendResponse(error, .dictionary(["message": message]))
}
public func unavailable(_ message: String) {
sendResponse(error, .dictionary(["message": message]))
}
public func getChannel(_ key: String) -> Channel? {
let channelDef = getString(key, "")
guard let callback = UInt64(channelDef.components(separatedBy: CHANNEL_PREFIX)[1]) else {
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
if components.count < 2 {
return nil
}
return Channel(callback: callback, handler: { (res: JsonValue) -> Void in
self.sendResponse(callback, res)
})
guard let channelId = UInt64(components[1]) else {
return nil
}
return Channel(
id: channelId,
handler: { (res: JsonValue) -> Void in
self.sendChannelData(channelId, res)
})
}
}

View File

@@ -2,145 +2,147 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import SwiftRs
import Foundation
import SwiftRs
import UIKit
import WebKit
import os.log
class PluginHandle {
var instance: Plugin
var loaded = false
var instance: Plugin
var loaded = false
init(plugin: Plugin) {
instance = plugin
}
init(plugin: Plugin) {
instance = plugin
}
}
public class PluginManager {
static let shared: PluginManager = PluginManager()
public var viewController: UIViewController?
var plugins: [String: PluginHandle] = [:]
var ipcDispatchQueue = DispatchQueue(label: "ipc")
public var isSimEnvironment: Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
static let shared: PluginManager = PluginManager()
public var viewController: UIViewController?
var plugins: [String: PluginHandle] = [:]
var ipcDispatchQueue = DispatchQueue(label: "ipc")
public var isSimEnvironment: Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
public func assetUrl(fromLocalURL url: URL?) -> URL? {
guard let inputURL = url else {
return nil
}
public func assetUrl(fromLocalURL url: URL?) -> URL? {
guard let inputURL = url else {
return nil
}
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
}
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
}
func onWebviewCreated(_ webview: WKWebView) {
for (_, handle) in plugins {
if (!handle.loaded) {
handle.instance.load(webview: webview)
}
}
}
func onWebviewCreated(_ webview: WKWebView) {
for (_, handle) in plugins {
if !handle.loaded {
handle.instance.load(webview: webview)
}
}
}
func load<P: Plugin>(name: String, plugin: P, config: JSObject, webview: WKWebView?) {
func load<P: Plugin>(name: String, plugin: P, config: JSObject, webview: WKWebView?) {
plugin.setConfig(config)
let handle = PluginHandle(plugin: plugin)
if let webview = webview {
handle.instance.load(webview: webview)
handle.loaded = true
}
plugins[name] = handle
}
let handle = PluginHandle(plugin: plugin)
if let webview = webview {
handle.instance.load(webview: webview)
handle.loaded = true
}
plugins[name] = handle
}
func invoke(name: String, invoke: Invoke) {
if let plugin = plugins[name] {
ipcDispatchQueue.async {
let selectorWithThrows = Selector(("\(invoke.command):error:"))
if plugin.instance.responds(to: selectorWithThrows) {
var error: NSError? = nil
withUnsafeMutablePointer(to: &error) {
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
unsafeBitCast(methodIMP, to: (@convention(c)(Any?, Selector, Invoke, OpaquePointer) -> Void).self)(plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
}
if let error = error {
invoke.reject("\(error)")
// TODO: app crashes without this leak
let _ = Unmanaged.passRetained(error)
}
} else {
let selector = Selector(("\(invoke.command):"))
if plugin.instance.responds(to: selector) {
plugin.instance.perform(selector, with: invoke)
} else {
invoke.reject("No command \(invoke.command) found for plugin \(name)")
}
}
}
} else {
invoke.reject("Plugin \(name) not initialized")
}
}
func invoke(name: String, invoke: Invoke) {
if let plugin = plugins[name] {
ipcDispatchQueue.async {
let selectorWithThrows = Selector(("\(invoke.command):error:"))
if plugin.instance.responds(to: selectorWithThrows) {
var error: NSError? = nil
withUnsafeMutablePointer(to: &error) {
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
unsafeBitCast(
methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)(
plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
}
if let error = error {
invoke.reject("\(error)")
// TODO: app crashes without this leak
let _ = Unmanaged.passRetained(error)
}
} else {
let selector = Selector(("\(invoke.command):"))
if plugin.instance.responds(to: selector) {
plugin.instance.perform(selector, with: invoke)
} else {
invoke.reject("No command \(invoke.command) found for plugin \(name)")
}
}
}
} else {
invoke.reject("Plugin \(name) not initialized")
}
}
}
extension PluginManager: NSCopying {
public func copy(with zone: NSZone? = nil) -> Any {
return self
}
public func copy(with zone: NSZone? = nil) -> Any {
return self
}
}
@_cdecl("register_plugin")
func registerPlugin(name: SRString, plugin: NSObject, config: NSDictionary?, webview: WKWebView?) {
PluginManager.shared.load(
name: name.toString(),
plugin: plugin as! Plugin,
PluginManager.shared.load(
name: name.toString(),
plugin: plugin as! Plugin,
config: JSTypes.coerceDictionaryToJSObject(config ?? [:], formattingDatesAsStrings: true)!,
webview: webview
)
)
}
@_cdecl("on_webview_created")
func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
PluginManager.shared.viewController = viewController
PluginManager.shared.onWebviewCreated(webview)
PluginManager.shared.viewController = viewController
PluginManager.shared.onWebviewCreated(webview)
}
@_cdecl("post_ipc_message")
func postIpcMessage(webview: WKWebView, name: SRString, command: SRString, data: NSDictionary, callback: UInt64, error: UInt64) {
let invoke = Invoke(command: command.toString(), callback: callback, error: error, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
var payloadJson: String
do {
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
webview.evaluateJavaScript("window['_\(fn)'](\(payloadJson))")
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
}
@_cdecl("run_plugin_method")
@_cdecl("run_plugin_command")
func runCommand(
id: Int,
name: SRString,
command: SRString,
data: NSDictionary,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void
id: Int,
name: SRString,
command: SRString,
data: NSDictionary,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void,
sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer<CChar>) -> Void
) {
let callbackId: UInt64 = 0
let errorId: UInt64 = 1
let invoke = Invoke(command: command.toString(), callback: callbackId, error: errorId, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
let success = fn == callbackId
var payloadJson: String = ""
do {
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
let invoke = Invoke(
command: command.toString(), callback: callbackId, error: errorId,
sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
let success = fn == callbackId
var payloadJson: String = ""
do {
try payloadJson =
payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
},
sendChannelData: { (id: UInt64, payload: JsonValue) -> Void in
var payloadJson: String = ""
do {
try payloadJson =
payload.jsonRepresentation() ?? "`Failed to serialize payload`"
} catch {
payloadJson = "`\(error)`"
}
sendChannelData(id, payloadJson)
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
}

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,15 @@
})
}
const osName = __TEMPLATE_os_name__
window.__TAURI__.convertFileSrc = function convertFileSrc(filePath, protocol = 'asset') {
const path = encodeURIComponent(filePath)
return osName === 'windows' || osName === 'android'
? `https://${protocol}.localhost/${path}`
: `${protocol}://localhost/${path}`
}
window.__TAURI__.transformCallback = function transformCallback(
callback,
once
@@ -48,7 +57,7 @@
}
}
window.__TAURI_INVOKE__ = function invoke(cmd, args = {}) {
window.__TAURI_INVOKE__ = function invoke(cmd, payload = {}, options) {
return new Promise(function (resolve, reject) {
var callback = window.__TAURI__.transformCallback(function (r) {
resolve(r)
@@ -59,19 +68,13 @@
delete window[`_${callback}`]
}, true)
if (typeof cmd === 'string') {
args.cmd = cmd
} else if (typeof cmd === 'object') {
args = cmd
} else {
return reject(new Error('Invalid argument type.'))
}
const action = () => {
window.__TAURI_IPC__({
...args,
cmd,
callback,
error: error
error,
payload,
options
})
}
if (window.__TAURI_IPC__) {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
;(function () {
const processIpcMessage = __RAW_process_ipc_message_fn__
const osName = __TEMPLATE_os_name__
const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__
const useCustomProtocol = __TEMPLATE_use_custom_protocol__
Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {
value: (message) => {
const { cmd, callback, error, payload, options } = message
// use custom protocol for IPC if the flag is set to true, the command is the fetch data command or when not on Linux/Android
if (useCustomProtocol || cmd === fetchChannelDataCommand || (osName !== 'linux' && osName !== 'android')) {
const { contentType, data } = processIpcMessage(payload)
fetch(window.__TAURI__.convertFileSrc(cmd, 'ipc'), {
method: 'POST',
body: data,
headers: {
'Content-Type': contentType,
'Tauri-Callback': callback,
'Tauri-Error': error,
...options?.headers
}
}).then((response) => {
const cb = response.ok ? callback : error
// we need to split here because on Android the content-type gets duplicated
switch ((response.headers.get('content-type') || '').split(',')[0]) {
case 'application/json':
return response.json().then((r) => [cb, r])
case 'text/plain':
return response.text().then((r) => [cb, r])
default:
return response.arrayBuffer().then((r) => [cb, r])
}
}).then(([cb, data]) => {
if (window[`_${cb}`]) {
window[`_${cb}`](data)
} else {
console.warn(`[TAURI] Couldn't find callback id {cb} in window. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)
}
})
} else {
// otherwise use the postMessage interface
const { data } = processIpcMessage({ cmd, callback, error, options, ...payload })
window.ipc.postMessage(data)
}
}
})
})()

View File

@@ -33,11 +33,14 @@
* @return {boolean} - if the event was a valid isolation message
*/
function isIsolationMessage(event) {
return (
typeof event.data === 'object' &&
'nonce' in event.data &&
'payload' in event.data
)
if (typeof event.data === 'object' && typeof event.data.payload === 'object') {
const keys = Object.keys(event.data.payload)
return (
keys.length > 0 &&
keys.every((key) => key === 'nonce' || key === 'payload')
)
}
return false
}
/**
@@ -47,7 +50,12 @@
* @return {boolean} - if the data is able to transform into an isolation payload
*/
function isIsolationPayload(data) {
return typeof data === 'object' && 'callback' in data && 'error' in data
return (
typeof data === 'object' &&
'callback' in data &&
'error' in data &&
!isIsolationMessage(data)
)
}
/**

View File

@@ -0,0 +1,31 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// this is a function and not an iife so use it carefully
(function (message) {
if (message instanceof ArrayBuffer || ArrayBuffer.isView(message) || Array.isArray(message)) {
return {
contentType: 'application/octet-stream',
data: message
}
} else {
const data = JSON.stringify(message, (_k, val) => {
if (val instanceof Map) {
let o = {};
val.forEach((v, k) => o[k] = v);
return o;
} else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') {
return `__CHANNEL__:${val.id}`
} else {
return val;
}
})
return {
contentType: 'application/json',
data
}
}
})

View File

@@ -1,17 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
(function (message) {
return JSON.stringify(message, (_k, val) => {
if (val instanceof Map) {
let o = {};
val.forEach((v, k) => o[k] = v);
return o;
} else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') {
return `__CHANNEL__:${val.id}`
} else {
return val;
}
})
})

View File

@@ -4,8 +4,6 @@
//! The Tauri API interface.
pub mod ipc;
mod error;
pub use error::{Error, Result};

View File

@@ -6,10 +6,10 @@
pub(crate) mod tray;
use crate::{
api::ipc::CallbackFn,
command::{CommandArg, CommandItem},
hooks::{
window_invoke_responder, InvokeHandler, InvokeResponder, OnPageLoad, PageLoadPayload, SetupHook,
ipc::{
channel::ChannelDataIpcQueue, CallbackFn, Invoke, InvokeError, InvokeHandler, InvokeResponder,
InvokeResponse,
},
manager::{Asset, CustomProtocol, WindowManager},
plugin::{Plugin, PluginStore},
@@ -23,14 +23,16 @@ use crate::{
sealed::{ManagerBase, RuntimeOrDispatch},
utils::config::Config,
utils::{assets::Assets, Env},
Context, DeviceEventFilter, EventLoopMessage, Icon, Invoke, InvokeError, InvokeResponse, Manager,
Monitor, Runtime, Scopes, StateManager, Theme, Window,
Context, DeviceEventFilter, EventLoopMessage, Icon, Manager, Monitor, Runtime, Scopes,
StateManager, Theme, Window,
};
#[cfg(feature = "protocol-asset")]
use crate::scope::FsScope;
use raw_window_handle::HasRawDisplayHandle;
use serde::Deserialize;
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
use tauri_macros::default_runtime;
use tauri_runtime::window::{
dpi::{PhysicalPosition, PhysicalSize},
@@ -55,6 +57,24 @@ pub(crate) type GlobalMenuEventListener<R> = Box<dyn Fn(WindowMenuEvent<R>) + Se
pub(crate) type GlobalWindowEventListener<R> = Box<dyn Fn(GlobalWindowEvent<R>) + Send + Sync>;
#[cfg(all(desktop, feature = "system-tray"))]
type SystemTrayEventListener<R> = Box<dyn Fn(&AppHandle<R>, tray::SystemTrayEvent) + Send + Sync>;
/// A closure that is run when the Tauri application is setting up.
pub type SetupHook<R> =
Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
/// A closure that is run once every time a window is created and loaded.
pub type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload) + Send + Sync + 'static;
/// The payload for the [`OnPageLoad`] hook.
#[derive(Debug, Clone, Deserialize)]
pub struct PageLoadPayload {
url: String,
}
impl PageLoadPayload {
/// The page URL.
pub fn url(&self) -> &str {
&self.url
}
}
/// Api exposed on the `ExitRequested` event.
#[derive(Debug)]
@@ -799,7 +819,7 @@ pub struct Builder<R: Runtime> {
invoke_handler: Box<InvokeHandler<R>>,
/// The JS message responder.
pub(crate) invoke_responder: Arc<InvokeResponder<R>>,
invoke_responder: Option<Arc<InvokeResponder<R>>>,
/// The script that initializes the `window.__TAURI_POST_MESSAGE__` function.
invoke_initialization_script: String,
@@ -847,6 +867,17 @@ pub struct Builder<R: Runtime> {
device_event_filter: DeviceEventFilter,
}
#[derive(Template)]
#[default_template("../scripts/ipc-protocol.js")]
struct InvokeInitializationScript<'a> {
/// The function that processes the IPC message.
#[raw]
process_ipc_message_fn: &'a str,
os_name: &'a str,
fetch_channel_data_command: &'a str,
use_custom_protocol: bool,
}
impl<R: Runtime> Builder<R> {
/// Creates a new App builder.
pub fn new() -> Self {
@@ -855,9 +886,16 @@ impl<R: Runtime> Builder<R> {
runtime_any_thread: false,
setup: Box::new(|_| Ok(())),
invoke_handler: Box::new(|_| false),
invoke_responder: Arc::new(window_invoke_responder),
invoke_initialization_script:
format!("Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {{ value: (message) => window.ipc.postMessage({}(message)) }})", crate::manager::STRINGIFY_IPC_MESSAGE_FN),
invoke_responder: None,
invoke_initialization_script: InvokeInitializationScript {
process_ipc_message_fn: crate::manager::PROCESS_IPC_MESSAGE_FN,
os_name: std::env::consts::OS,
fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND,
use_custom_protocol: cfg!(ipc_custom_protocol),
}
.render_default(&Default::default())
.unwrap()
.into_string(),
on_page_load: Box::new(|_, _| ()),
pending_windows: Default::default(),
plugins: PluginStore::default(),
@@ -916,14 +954,14 @@ impl<R: Runtime> Builder<R> {
/// The `responder` is a function that will be called when a command has been executed and must send a response to the JS layer.
///
/// The `initialization_script` is a script that initializes `window.__TAURI_POST_MESSAGE__`.
/// That function must take the `message: object` argument and send it to the backend.
/// That function must take the `(message: object, options: object)` arguments and send it to the backend.
#[must_use]
pub fn invoke_system<F>(mut self, initialization_script: String, responder: F) -> Self
where
F: Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
F: Fn(Window<R>, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
{
self.invoke_initialization_script = initialization_script;
self.invoke_responder = Arc::new(responder);
self.invoke_responder.replace(Arc::new(responder));
self
}
@@ -1368,6 +1406,9 @@ impl<R: Runtime> Builder<R> {
asset_protocol: FsScope::for_fs_api(&app, &app.config().tauri.security.asset_protocol.scope)?,
});
app.manage(ChannelDataIpcQueue::default());
app.handle.plugin(crate::ipc::channel::plugin())?;
#[cfg(windows)]
{
if let crate::utils::config::WebviewInstallMode::FixedRuntime { path } = &app

View File

@@ -7,11 +7,14 @@
//! You usually don't need to create these items yourself. These are created from [command](../attr.command.html)
//! attribute macro along the way and used by [`crate::generate_handler`] macro.
use crate::hooks::InvokeError;
use crate::InvokeMessage;
use crate::Runtime;
use serde::de::Visitor;
use serde::{Deserialize, Deserializer};
use crate::{
ipc::{InvokeBody, InvokeError, InvokeMessage},
Runtime,
};
use serde::{
de::{Error, Visitor},
Deserialize, Deserializer,
};
/// Represents a custom command.
pub struct CommandItem<'a, R: Runtime> {
@@ -62,8 +65,6 @@ impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D {
macro_rules! pass {
($fn:ident, $($arg:ident: $argt:ty),+) => {
fn $fn<V: Visitor<'de>>(self, $($arg: $argt),*) -> Result<V::Value, Self::Error> {
use serde::de::Error;
if self.key.is_empty() {
return Err(serde_json::Error::custom(format!(
"command {} has an argument with no name with a non-optional value",
@@ -71,14 +72,24 @@ macro_rules! pass {
)))
}
match self.message.payload.get(self.key) {
Some(value) => value.$fn($($arg),*),
None => {
match &self.message.payload {
InvokeBody::Raw(_body) => {
Err(serde_json::Error::custom(format!(
"command {} missing required key {}",
"command {} expected a value for key {} but the IPC call used a bytes payload",
self.name, self.key
)))
}
InvokeBody::Json(v) => {
match v.get(self.key) {
Some(value) => value.$fn($($arg),*),
None => {
Err(serde_json::Error::custom(format!(
"command {} missing required key {}",
self.name, self.key
)))
}
}
}
}
}
}
@@ -111,9 +122,15 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
pass!(deserialize_byte_buf, visitor: V);
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
match self.message.payload.get(self.key) {
Some(value) => value.deserialize_option(visitor),
None => visitor.visit_none(),
match &self.message.payload {
InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!(
"command {} expected a value for key {} but the IPC call used a bytes payload",
self.name, self.key
))),
InvokeBody::Json(v) => match v.get(self.key) {
Some(value) => value.deserialize_option(visitor),
None => visitor.visit_none(),
},
}
}
@@ -155,46 +172,47 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
/// Nothing in this module is considered stable.
#[doc(hidden)]
pub mod private {
use crate::{InvokeError, InvokeResolver, Runtime};
use crate::{
ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse},
Runtime,
};
use futures_util::{FutureExt, TryFutureExt};
use serde::Serialize;
use serde_json::Value;
use std::future::Future;
// ===== impl Serialize =====
// ===== impl IpcResponse =====
pub struct SerializeTag;
pub struct ResponseTag;
pub trait SerializeKind {
pub trait ResponseKind {
#[inline(always)]
fn blocking_kind(&self) -> SerializeTag {
SerializeTag
fn blocking_kind(&self) -> ResponseTag {
ResponseTag
}
#[inline(always)]
fn async_kind(&self) -> SerializeTag {
SerializeTag
fn async_kind(&self) -> ResponseTag {
ResponseTag
}
}
impl<T: Serialize> SerializeKind for &T {}
impl<T: IpcResponse> ResponseKind for &T {}
impl SerializeTag {
impl ResponseTag {
#[inline(always)]
pub fn block<R, T>(self, value: T, resolver: InvokeResolver<R>)
where
R: Runtime,
T: Serialize,
T: IpcResponse,
{
resolver.respond(Ok(value))
}
#[inline(always)]
pub fn future<T>(self, value: T) -> impl Future<Output = Result<Value, InvokeError>>
pub fn future<T>(self, value: T) -> impl Future<Output = Result<InvokeBody, InvokeError>>
where
T: Serialize,
T: IpcResponse,
{
std::future::ready(serde_json::to_value(value).map_err(InvokeError::from_serde_json))
std::future::ready(value.body().map_err(InvokeError::from_error))
}
}
@@ -214,14 +232,14 @@ pub mod private {
}
}
impl<T: Serialize, E: Into<InvokeError>> ResultKind for Result<T, E> {}
impl<T: IpcResponse, E: Into<InvokeError>> ResultKind for Result<T, E> {}
impl ResultTag {
#[inline(always)]
pub fn block<R, T, E>(self, value: Result<T, E>, resolver: InvokeResolver<R>)
where
R: Runtime,
T: Serialize,
T: IpcResponse,
E: Into<InvokeError>,
{
resolver.respond(value.map_err(Into::into))
@@ -231,20 +249,20 @@ pub mod private {
pub fn future<T, E>(
self,
value: Result<T, E>,
) -> impl Future<Output = Result<Value, InvokeError>>
) -> impl Future<Output = Result<InvokeBody, InvokeError>>
where
T: Serialize,
T: IpcResponse,
E: Into<InvokeError>,
{
std::future::ready(
value
.map_err(Into::into)
.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)),
.and_then(|value| value.body().map_err(InvokeError::from_error)),
)
}
}
// ===== Future<Output = impl Serialize> =====
// ===== Future<Output = impl IpcResponse> =====
pub struct FutureTag;
@@ -254,16 +272,16 @@ pub mod private {
FutureTag
}
}
impl<T: Serialize, F: Future<Output = T>> FutureKind for &F {}
impl<T: IpcResponse, F: Future<Output = T>> FutureKind for &F {}
impl FutureTag {
#[inline(always)]
pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<Value, InvokeError>>
pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
where
T: Serialize,
T: IpcResponse,
F: Future<Output = T> + Send + 'static,
{
value.map(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json))
value.map(|value| value.body().map_err(InvokeError::from_error))
}
}
@@ -278,19 +296,22 @@ pub mod private {
}
}
impl<T: Serialize, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind for F {}
impl<T: IpcResponse, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind
for F
{
}
impl ResultFutureTag {
#[inline(always)]
pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<Value, InvokeError>>
pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
where
T: Serialize,
T: IpcResponse,
E: Into<InvokeError>,
F: Future<Output = Result<T, E>> + Send,
{
value.err_into().map(|result| {
result.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json))
})
value
.err_into()
.map(|result| result.and_then(|value| value.body().map_err(InvokeError::from_error)))
}
}
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{api::ipc::CallbackFn, command, Manager, Result, Runtime, Window};
use crate::{command, ipc::CallbackFn, Manager, Result, Runtime, Window};
use serde::{Deserialize, Deserializer};
use serde_json::Value as JsonValue;
use tauri_runtime::window::is_label_valid;

View File

@@ -9,7 +9,7 @@ use swift_rs::{swift, SRString, SwiftArg};
use std::{
ffi::c_void,
os::raw::{c_char, c_int},
os::raw::{c_char, c_int, c_ulonglong},
};
type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char);
@@ -23,20 +23,24 @@ impl<'a> SwiftArg<'a> for PluginMessageCallback {
}
}
swift!(pub fn post_ipc_message(
webview: *const c_void,
name: &SRString,
method: &SRString,
data: *const c_void,
callback: usize,
error: usize
));
swift!(pub fn run_plugin_method(
type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char);
pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn);
impl<'a> SwiftArg<'a> for ChannelSendDataCallback {
type ArgType = ChannelSendDataCallbackFn;
unsafe fn as_arg(&'a self) -> Self::ArgType {
self.0
}
}
swift!(pub fn run_plugin_command(
id: i32,
name: &SRString,
method: &SRString,
data: *const c_void,
callback: PluginMessageCallback
callback: PluginMessageCallback,
send_channel_data_callback: ChannelSendDataCallback
));
swift!(pub fn register_plugin(
name: &SRString,
@@ -108,7 +112,7 @@ unsafe fn add_json_value_to_array(array: id, value: &JsonValue) {
let () = msg_send![array, addObject: number];
}
JsonValue::String(val) => {
let () = msg_send![array, addObject: NSString::new(&val)];
let () = msg_send![array, addObject: NSString::new(val)];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
@@ -130,7 +134,7 @@ unsafe fn add_json_value_to_array(array: id, value: &JsonValue) {
}
unsafe fn add_json_entry_to_dictionary(data: id, key: &str, value: &JsonValue) {
let key = NSString::new(&key);
let key = NSString::new(key);
match value {
JsonValue::Null => {
let null: id = msg_send![class!(NSNull), null];
@@ -154,7 +158,7 @@ unsafe fn add_json_entry_to_dictionary(data: id, key: &str, value: &JsonValue) {
let () = msg_send![data, setObject:number forKey: key];
}
JsonValue::String(val) => {
let () = msg_send![data, setObject:NSString::new(&val) forKey: key];
let () = msg_send![data, setObject:NSString::new(val) forKey: key];
}
JsonValue::Array(val) => {
let nsarray: id = msg_send![class!(NSMutableArray), alloc];

View File

@@ -0,0 +1,151 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use serde::{Deserialize, Serialize, Serializer};
use crate::{
command,
command::{CommandArg, CommandItem},
plugin::{Builder as PluginBuilder, TauriPlugin},
Manager, Runtime, State, Window,
};
use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response};
pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__";
// TODO: ideally this const references CHANNEL_PLUGIN_NAME
pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch";
pub(crate) const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id";
/// Maps a channel id to a pending data that must be send to the JavaScript side via the IPC.
#[derive(Default, Clone)]
pub struct ChannelDataIpcQueue(pub(crate) Arc<Mutex<HashMap<u32, InvokeBody>>>);
/// An IPC channel.
#[derive(Clone)]
pub struct Channel {
id: u32,
on_message: Arc<dyn Fn(InvokeBody) -> crate::Result<()> + Send + Sync>,
}
impl Serialize for Channel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.id))
}
}
impl Channel {
/// Creates a new channel with the given message handler.
pub fn new<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
on_message: F,
) -> Self {
Self::_new(rand::random(), on_message)
}
pub(crate) fn _new<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
id: u32,
on_message: F,
) -> Self {
#[allow(clippy::let_and_return)]
let channel = Self {
id,
on_message: Arc::new(on_message),
};
#[cfg(mobile)]
crate::plugin::mobile::register_channel(channel.clone());
channel
}
pub(crate) fn from_ipc<R: Runtime>(window: Window<R>, callback: CallbackFn) -> Self {
Channel::_new(callback.0, move |body| {
let data_id = rand::random();
window
.state::<ChannelDataIpcQueue>()
.0
.lock()
.unwrap()
.insert(data_id, body);
window.eval(&format!(
"__TAURI_INVOKE__('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': {data_id} }} }}).then(window['_' + {}]).catch(console.error)",
callback.0
))
})
}
pub(crate) fn load_from_ipc<R: Runtime>(
window: Window<R>,
value: impl AsRef<str>,
) -> Option<Self> {
value
.as_ref()
.split_once(IPC_PAYLOAD_PREFIX)
.and_then(|(_prefix, id)| id.parse().ok())
.map(|callback_id| Self::from_ipc(window, CallbackFn(callback_id)))
}
/// The channel identifier.
pub fn id(&self) -> u32 {
self.id
}
/// Sends the given data through the channel.
pub fn send<T: IpcResponse>(&self, data: T) -> crate::Result<()> {
let body = data.body()?;
(self.on_message)(body)
}
}
impl<'de, R: Runtime> CommandArg<'de, R> for Channel {
/// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`].
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
let name = command.name;
let arg = command.key;
let window = command.message.window();
let value: String =
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
Channel::load_from_ipc(window, &value).ok_or_else(|| {
InvokeError::from_anyhow(anyhow::anyhow!(
"invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format"
))
})
}
}
#[command(root = "crate")]
fn fetch(
request: Request<'_>,
cache: State<'_, ChannelDataIpcQueue>,
) -> Result<Response, &'static str> {
if let Some(id) = request
.headers()
.get(CHANNEL_ID_HEADER_NAME)
.and_then(|v| v.to_str().ok())
.and_then(|id| id.parse().ok())
{
if let Some(data) = cache.0.lock().unwrap().remove(&id) {
Ok(Response::new(data))
} else {
Err("data not found")
}
} else {
Err("missing channel id header")
}
}
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new(CHANNEL_PLUGIN_NAME)
.invoke_handler(crate::generate_handler![fetch])
.build()
}

View File

@@ -2,82 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Types and functions related to Inter Procedure Call(IPC).
//!
//! This module includes utilities to send messages to the JS layer of the webview.
use serde::{Deserialize, Serialize};
use serde::Serialize;
use serde_json::value::RawValue;
pub use serialize_to_javascript::Options as SerializeOptions;
use serialize_to_javascript::Serialized;
use tauri_macros::default_runtime;
use crate::{
command::{CommandArg, CommandItem},
InvokeError, Runtime, Window,
};
const CHANNEL_PREFIX: &str = "__CHANNEL__:";
/// An IPC channel.
#[default_runtime(crate::Wry, wry)]
pub struct Channel<R: Runtime> {
id: CallbackFn,
window: Window<R>,
}
impl<R: Runtime> Clone for Channel<R> {
fn clone(&self) -> Self {
Self {
id: self.id,
window: self.window.clone(),
}
}
}
impl Serialize for Channel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{CHANNEL_PREFIX}{}", self.id.0))
}
}
impl<R: Runtime> Channel<R> {
/// Sends the given data through the channel.
pub fn send<S: Serialize>(&self, data: &S) -> crate::Result<()> {
let js = format_callback(self.id, data)?;
self.window.eval(&js)
}
}
impl<'de, R: Runtime> CommandArg<'de, R> for Channel<R> {
/// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`].
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
let name = command.name;
let arg = command.key;
let window = command.message.window();
let value: String =
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
if let Some(callback_id) = value
.split_once(CHANNEL_PREFIX)
.and_then(|(_prefix, id)| id.parse().ok())
{
return Ok(Channel {
id: CallbackFn(callback_id),
window,
});
}
Err(InvokeError::from_anyhow(anyhow::anyhow!(
"invalid channel value `{value}`, expected a string in the `{CHANNEL_PREFIX}ID` format"
)))
}
}
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct CallbackFn(pub usize);
use super::CallbackFn;
/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490).
///
@@ -111,22 +40,9 @@ const MIN_JSON_PARSE_LEN: usize = 10_240;
/// 1. `serde_json`'s ability to correctly escape and format json into a string.
/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
/// character to end a string that was opened with it.
///
/// # Examples
///
/// ```
/// use tauri::api::ipc::{serialize_js_with, SerializeOptions};
/// #[derive(serde::Serialize)]
/// struct Foo {
/// bar: String,
/// }
/// let foo = Foo { bar: "x".repeat(20_000).into() };
/// let value = serialize_js_with(&foo, SerializeOptions::default(), |v| format!("console.log({v})")).unwrap();
/// assert_eq!(value, format!("console.log(JSON.parse('{{\"bar\":\"{}\"}}'))", foo.bar));
/// ```
pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
value: &T,
options: SerializeOptions,
options: serialize_to_javascript::Options,
cb: F,
) -> crate::api::Result<String> {
// get a raw &str representation of a serialized json value.
@@ -161,80 +77,13 @@ pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
Ok(return_val)
}
/// Transforms & escapes a JSON value.
///
/// This is a convenience function for [`serialize_js_with`], simply allocating the result to a String.
///
/// For usage in functions where performance is more important than code readability, see [`serialize_js_with`].
///
/// # Examples
/// ```rust,no_run
/// use tauri::{Manager, api::ipc::serialize_js};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Foo {
/// bar: String,
/// }
///
/// #[derive(Serialize)]
/// struct Bar {
/// baz: u32,
/// }
///
/// tauri::Builder::default()
/// .setup(|app| {
/// let window = app.get_window("main").unwrap();
/// window.eval(&format!(
/// "console.log({}, {})",
/// serialize_js(&Foo { bar: "bar".to_string() }).unwrap(),
/// serialize_js(&Bar { baz: 0 }).unwrap()),
/// )?;
/// Ok(())
/// });
/// ```
pub fn serialize_js<T: Serialize>(value: &T) -> crate::api::Result<String> {
serialize_js_with(value, Default::default(), |v| v.into())
}
/// Formats a function name and argument to be evaluated as callback.
///
/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
/// than 10 KiB with `JSON.parse('...')`.
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
///
/// # Examples
/// - With string literals:
/// ```
/// use tauri::api::ipc::{CallbackFn, format_callback};
/// // callback with a string argument
/// let cb = format_callback(CallbackFn(12345), &"the string response").unwrap();
/// assert!(cb.contains(r#"window["_12345"]("the string response")"#));
/// ```
///
/// - With types implement [`serde::Serialize`]:
/// ```
/// use tauri::api::ipc::{CallbackFn, format_callback};
/// use serde::Serialize;
///
/// // callback with large JSON argument
/// #[derive(Serialize)]
/// struct MyResponse {
/// value: String
/// }
///
/// let cb = format_callback(
/// CallbackFn(6789),
/// &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap()
/// }).expect("failed to serialize");
///
/// assert!(cb.contains(r#"window["_6789"](JSON.parse('{"value":"XXXXXXXXX"#));
/// ```
pub fn format_callback<T: Serialize>(
function_name: CallbackFn,
arg: &T,
) -> crate::api::Result<String> {
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::api::Result<String> {
serialize_js_with(arg, Default::default(), |arg| {
format!(
r#"
@@ -258,42 +107,33 @@ pub fn format_callback<T: Serialize>(
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
///
/// Note that the callback strings are automatically generated by the `invoke` helper.
///
/// # Examples
/// ```
/// use tauri::api::ipc::{CallbackFn, format_callback_result};
/// let res: Result<u8, &str> = Ok(5);
/// let cb = format_callback_result(res, CallbackFn(145), CallbackFn(0)).expect("failed to format");
/// assert!(cb.contains(r#"window["_145"](5)"#));
///
/// let res: Result<&str, &str> = Err("error message here");
/// let cb = format_callback_result(res, CallbackFn(2), CallbackFn(1)).expect("failed to format");
/// assert!(cb.contains(r#"window["_1"]("error message here")"#));
/// ```
// TODO: better example to explain
pub fn format_callback_result<T: Serialize, E: Serialize>(
pub fn format_result<T: Serialize, E: Serialize>(
result: Result<T, E>,
success_callback: CallbackFn,
error_callback: CallbackFn,
) -> crate::api::Result<String> {
match result {
Ok(res) => format_callback(success_callback, &res),
Err(err) => format_callback(error_callback, &err),
Ok(res) => format(success_callback, &res),
Err(err) => format(error_callback, &err),
}
}
#[cfg(test)]
mod test {
use crate::api::ipc::*;
use super::*;
use quickcheck::{Arbitrary, Gen};
use quickcheck_macros::quickcheck;
impl Arbitrary for CallbackFn {
fn arbitrary(g: &mut Gen) -> CallbackFn {
CallbackFn(usize::arbitrary(g))
CallbackFn(u32::arbitrary(g))
}
}
fn serialize_js<T: Serialize>(value: &T) -> crate::api::Result<String> {
serialize_js_with(value, Default::default(), |v| v.into())
}
#[test]
fn test_serialize_js() {
assert_eq!(serialize_js(&()).unwrap(), "null");
@@ -346,7 +186,7 @@ mod test {
#[quickcheck]
fn qc_formatting(f: CallbackFn, a: String) -> bool {
// call format callback
let fc = format_callback(f, &a).unwrap();
let fc = format(f, &a).unwrap();
fc.contains(&format!(
r#"window["_{}"](JSON.parse('{}'))"#,
f.0,
@@ -358,11 +198,10 @@ mod test {
))
}
// check arbitrary strings in format_callback_result
// check arbitrary strings in format_result
#[quickcheck]
fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
let resp =
format_callback_result(result.clone(), c, ec).expect("failed to format callback result");
let resp = format_result(result.clone(), c, ec).expect("failed to format callback result");
let (function, value) = match result {
Ok(v) => (c, v),
Err(e) => (ec, e),

View File

@@ -2,77 +2,156 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
api::ipc::{format_callback, format_callback_result, CallbackFn},
app::App,
Runtime, StateManager, Window,
};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serialize_to_javascript::{default_template, Template};
use std::{future::Future, sync::Arc};
//! Types and functions related to Inter Procedure Call(IPC).
//!
//! This module includes utilities to send messages to the JS layer of the webview.
use std::sync::Arc;
use futures_util::Future;
use http::HeaderMap;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
pub use serialize_to_javascript::Options as SerializeOptions;
use tauri_macros::default_runtime;
/// A closure that is run when the Tauri application is setting up.
pub type SetupHook<R> =
Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
use crate::{
command::{CommandArg, CommandItem},
Runtime, StateManager, Window,
};
pub(crate) mod channel;
#[cfg(not(ipc_custom_protocol))]
pub(crate) mod format_callback;
pub(crate) mod protocol;
pub use channel::Channel;
/// A closure that is run every time Tauri receives a message it doesn't explicitly handle.
pub type InvokeHandler<R> = dyn Fn(Invoke<R>) -> bool + Send + Sync + 'static;
/// A closure that is responsible for respond a JS message.
pub type InvokeResponder<R> =
dyn Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
dyn Fn(Window<R>, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
type OwnedInvokeResponder<R> =
dyn Fn(Window<R>, String, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
/// A closure that is run once every time a window is created and loaded.
pub type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload) + Send + Sync + 'static;
// todo: why is this derive broken but the output works manually?
#[derive(Template)]
#[default_template("../scripts/ipc.js")]
pub(crate) struct IpcJavascript<'a> {
pub(crate) isolation_origin: &'a str,
/// Possible values of an IPC payload.
#[derive(Debug, Clone)]
pub enum InvokeBody {
/// Json payload.
Json(JsonValue),
/// Bytes payload.
Raw(Vec<u8>),
}
#[cfg(feature = "isolation")]
#[derive(Template)]
#[default_template("../scripts/isolation.js")]
pub(crate) struct IsolationJavascript<'a> {
pub(crate) isolation_src: &'a str,
pub(crate) style: &'a str,
}
/// The payload for the [`OnPageLoad`] hook.
#[derive(Debug, Clone, Deserialize)]
pub struct PageLoadPayload {
url: String,
}
impl PageLoadPayload {
/// The page URL.
pub fn url(&self) -> &str {
&self.url
impl Default for InvokeBody {
fn default() -> Self {
Self::Json(Default::default())
}
}
/// The payload used on the IPC invoke.
#[derive(Debug, Deserialize)]
pub struct InvokePayload {
/// The invoke command.
pub cmd: String,
/// The success callback.
pub callback: CallbackFn,
/// The error callback.
pub error: CallbackFn,
/// The payload of the message.
#[serde(flatten)]
pub inner: JsonValue,
impl From<JsonValue> for InvokeBody {
fn from(value: JsonValue) -> Self {
Self::Json(value)
}
}
impl From<Vec<u8>> for InvokeBody {
fn from(value: Vec<u8>) -> Self {
Self::Raw(value)
}
}
impl IpcResponse for InvokeBody {
fn body(self) -> crate::Result<InvokeBody> {
Ok(self)
}
}
impl InvokeBody {
#[allow(dead_code)]
pub(crate) fn into_json(self) -> JsonValue {
match self {
Self::Json(v) => v,
Self::Raw(v) => {
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
}
}
}
/// Attempts to deserialize the invoke body.
pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
match self {
InvokeBody::Json(v) => serde_json::from_value(v),
InvokeBody::Raw(v) => serde_json::from_slice(&v),
}
}
}
/// The IPC request.
#[derive(Debug)]
pub struct Request<'a> {
body: &'a InvokeBody,
headers: &'a HeaderMap,
}
impl<'a> Request<'a> {
/// The request body.
pub fn body(&self) -> &InvokeBody {
self.body
}
/// Thr request headers.
pub fn headers(&self) -> &HeaderMap {
self.headers
}
}
impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> {
/// Returns the invoke [`Request`].
fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
Ok(Self {
body: command.message.payload(),
headers: command.message.headers(),
})
}
}
/// Marks a type as a response to an IPC call.
pub trait IpcResponse {
/// Resolve the IPC response body.
fn body(self) -> crate::Result<InvokeBody>;
}
impl<T: Serialize> IpcResponse for T {
fn body(self) -> crate::Result<InvokeBody> {
serde_json::to_value(self)
.map(Into::into)
.map_err(Into::into)
}
}
/// The IPC request.
pub struct Response {
body: InvokeBody,
}
impl IpcResponse for Response {
fn body(self) -> crate::Result<InvokeBody> {
Ok(self.body)
}
}
impl Response {
/// Defines a response with the given body.
pub fn new(body: impl Into<InvokeBody>) -> Self {
Self { body: body.into() }
}
}
/// The message and resolver given to a custom command.
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct Invoke<R: Runtime> {
/// The message passed.
pub message: InvokeMessage<R>,
@@ -83,12 +162,12 @@ pub struct Invoke<R: Runtime> {
/// Error response from an [`InvokeMessage`].
#[derive(Debug)]
pub struct InvokeError(JsonValue);
pub struct InvokeError(pub JsonValue);
impl InvokeError {
/// Create an [`InvokeError`] as a string of the [`serde_json::Error`] message.
/// Create an [`InvokeError`] as a string of the [`std::error::Error`] message.
#[inline(always)]
pub fn from_serde_json(error: serde_json::Error) -> Self {
pub fn from_error<E: std::error::Error>(error: E) -> Self {
Self(JsonValue::String(error.to_string()))
}
@@ -104,7 +183,7 @@ impl<T: Serialize> From<T> for InvokeError {
fn from(value: T) -> Self {
serde_json::to_value(value)
.map(Self)
.unwrap_or_else(Self::from_serde_json)
.unwrap_or_else(Self::from_error)
}
}
@@ -119,31 +198,20 @@ impl From<crate::Error> for InvokeError {
#[derive(Debug)]
pub enum InvokeResponse {
/// Resolve the promise.
Ok(JsonValue),
Ok(InvokeBody),
/// Reject the promise.
Err(InvokeError),
}
impl InvokeResponse {
/// Turn a [`InvokeResponse`] back into a serializable result.
#[inline(always)]
pub fn into_result(self) -> Result<JsonValue, JsonValue> {
match self {
Self::Ok(v) => Ok(v),
Self::Err(e) => Err(e.0),
}
}
}
impl<T: Serialize> From<Result<T, InvokeError>> for InvokeResponse {
impl<T: IpcResponse, E: Into<InvokeError>> From<Result<T, E>> for InvokeResponse {
#[inline]
fn from(result: Result<T, InvokeError>) -> Self {
fn from(result: Result<T, E>) -> Self {
match result {
Ok(ok) => match serde_json::to_value(ok) {
Ok(ok) => match ok.body() {
Ok(value) => Self::Ok(value),
Err(err) => Self::Err(InvokeError::from_serde_json(err)),
Err(err) => Self::Err(InvokeError::from_error(err)),
},
Err(err) => Self::Err(err),
Err(err) => Self::Err(err.into()),
}
}
}
@@ -156,9 +224,10 @@ impl From<InvokeError> for InvokeResponse {
/// Resolver of a invoke message.
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct InvokeResolver<R: Runtime> {
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
cmd: String,
pub(crate) callback: CallbackFn,
pub(crate) error: CallbackFn,
}
@@ -167,6 +236,8 @@ impl<R: Runtime> Clone for InvokeResolver<R> {
fn clone(&self) -> Self {
Self {
window: self.window.clone(),
responder: self.responder.clone(),
cmd: self.cmd.clone(),
callback: self.callback,
error: self.error,
}
@@ -174,9 +245,17 @@ impl<R: Runtime> Clone for InvokeResolver<R> {
}
impl<R: Runtime> InvokeResolver<R> {
pub(crate) fn new(window: Window<R>, callback: CallbackFn, error: CallbackFn) -> Self {
pub(crate) fn new(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
cmd: String,
callback: CallbackFn,
error: CallbackFn,
) -> Self {
Self {
window,
responder,
cmd,
callback,
error,
}
@@ -185,43 +264,67 @@ impl<R: Runtime> InvokeResolver<R> {
/// Reply to the invoke promise with an async task.
pub fn respond_async<T, F>(self, task: F)
where
T: Serialize,
T: IpcResponse,
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
{
crate::async_runtime::spawn(async move {
Self::return_task(self.window, task, self.callback, self.error).await;
Self::return_task(
self.window,
self.responder,
task,
self.cmd,
self.callback,
self.error,
)
.await;
});
}
/// Reply to the invoke promise with an async task which is already serialized.
pub fn respond_async_serialized<F>(self, task: F)
where
F: Future<Output = Result<JsonValue, InvokeError>> + Send + 'static,
F: Future<Output = Result<InvokeBody, InvokeError>> + Send + 'static,
{
crate::async_runtime::spawn(async move {
let response = match task.await {
Ok(ok) => InvokeResponse::Ok(ok),
Err(err) => InvokeResponse::Err(err),
};
Self::return_result(self.window, response, self.callback, self.error)
Self::return_result(
self.window,
self.responder,
response,
self.cmd,
self.callback,
self.error,
)
});
}
/// Reply to the invoke promise with a serializable value.
pub fn respond<T: Serialize>(self, value: Result<T, InvokeError>) {
Self::return_result(self.window, value.into(), self.callback, self.error)
pub fn respond<T: IpcResponse>(self, value: Result<T, InvokeError>) {
Self::return_result(
self.window,
self.responder,
value.into(),
self.cmd,
self.callback,
self.error,
)
}
/// Resolve the invoke promise with a value.
pub fn resolve<T: Serialize>(self, value: T) {
Self::return_result(self.window, Ok(value).into(), self.callback, self.error)
pub fn resolve<T: IpcResponse>(self, value: T) {
self.respond(Ok(value))
}
/// Reject the invoke promise with a value.
pub fn reject<T: Serialize>(self, value: T) {
Self::return_result(
self.window,
Result::<(), _>::Err(value.into()).into(),
self.responder,
Result::<(), _>::Err(value).into(),
self.cmd,
self.callback,
self.error,
)
@@ -229,7 +332,14 @@ impl<R: Runtime> InvokeResolver<R> {
/// Reject the invoke promise with an [`InvokeError`].
pub fn invoke_error(self, error: InvokeError) {
Self::return_result(self.window, error.into(), self.callback, self.error)
Self::return_result(
self.window,
self.responder,
error.into(),
self.cmd,
self.callback,
self.error,
)
}
/// Asynchronously executes the given task
@@ -239,52 +349,56 @@ impl<R: Runtime> InvokeResolver<R> {
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
pub async fn return_task<T, F>(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
task: F,
cmd: String,
success_callback: CallbackFn,
error_callback: CallbackFn,
) where
T: Serialize,
T: IpcResponse,
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
{
let result = task.await;
Self::return_closure(window, || result, success_callback, error_callback)
Self::return_closure(
window,
responder,
|| result,
cmd,
success_callback,
error_callback,
)
}
pub(crate) fn return_closure<T: Serialize, F: FnOnce() -> Result<T, InvokeError>>(
pub(crate) fn return_closure<T: IpcResponse, F: FnOnce() -> Result<T, InvokeError>>(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
f: F,
cmd: String,
success_callback: CallbackFn,
error_callback: CallbackFn,
) {
Self::return_result(window, f().into(), success_callback, error_callback)
Self::return_result(
window,
responder,
f().into(),
cmd,
success_callback,
error_callback,
)
}
pub(crate) fn return_result(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
response: InvokeResponse,
cmd: String,
success_callback: CallbackFn,
error_callback: CallbackFn,
) {
(window.invoke_responder())(window, response, success_callback, error_callback);
(responder)(window, cmd, response, success_callback, error_callback);
}
}
pub fn window_invoke_responder<R: Runtime>(
window: Window<R>,
response: InvokeResponse,
success_callback: CallbackFn,
error_callback: CallbackFn,
) {
let callback_string =
match format_callback_result(response.into_result(), success_callback, error_callback) {
Ok(callback_string) => callback_string,
Err(e) => format_callback(error_callback, &e.to_string())
.expect("unable to serialize response string to json"),
};
let _ = window.eval(&callback_string);
}
/// An invoke message.
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
@@ -296,7 +410,9 @@ pub struct InvokeMessage<R: Runtime> {
/// The IPC command.
pub(crate) command: String,
/// The JSON argument passed on the invoke message.
pub(crate) payload: JsonValue,
pub(crate) payload: InvokeBody,
/// The request headers.
pub(crate) headers: HeaderMap,
}
impl<R: Runtime> Clone for InvokeMessage<R> {
@@ -306,6 +422,7 @@ impl<R: Runtime> Clone for InvokeMessage<R> {
state: self.state.clone(),
command: self.command.clone(),
payload: self.payload.clone(),
headers: self.headers.clone(),
}
}
}
@@ -316,13 +433,15 @@ impl<R: Runtime> InvokeMessage<R> {
window: Window<R>,
state: Arc<StateManager>,
command: String,
payload: JsonValue,
payload: InvokeBody,
headers: HeaderMap,
) -> Self {
Self {
window,
state,
command,
payload,
headers,
}
}
@@ -346,7 +465,7 @@ impl<R: Runtime> InvokeMessage<R> {
/// A reference to the payload the invoke received.
#[inline(always)]
pub fn payload(&self) -> &JsonValue {
pub fn payload(&self) -> &InvokeBody {
&self.payload
}
@@ -361,4 +480,14 @@ impl<R: Runtime> InvokeMessage<R> {
pub fn state_ref(&self) -> &StateManager {
&self.state
}
/// The request headers.
#[inline(always)]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
}
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct CallbackFn(pub u32);

View File

@@ -0,0 +1,275 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use http::{
header::{ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN},
HeaderValue, Method, StatusCode,
};
use crate::{
manager::WindowManager,
runtime::http::{Request as HttpRequest, Response as HttpResponse},
window::{InvokeRequest, UriSchemeProtocolHandler},
Runtime,
};
use super::{CallbackFn, InvokeBody, InvokeResponse};
const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback";
const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
#[cfg(not(ipc_custom_protocol))]
pub fn message_handler<R: Runtime>(
manager: WindowManager<R>,
) -> crate::runtime::webview::WebviewIpcHandler<crate::EventLoopMessage, R> {
Box::new(move |window, request| handle_ipc_message(request, &manager, &window.label))
}
pub fn get<R: Runtime>(manager: WindowManager<R>, label: String) -> UriSchemeProtocolHandler {
Box::new(move |request| {
let mut response = match *request.method() {
Method::POST => {
let (mut response, content_type) = match handle_ipc_request(request, &manager, &label) {
Ok(data) => match data {
InvokeResponse::Ok(InvokeBody::Json(v)) => (
HttpResponse::new(serde_json::to_vec(&v)?.into()),
mime::APPLICATION_JSON,
),
InvokeResponse::Ok(InvokeBody::Raw(v)) => {
(HttpResponse::new(v.into()), mime::APPLICATION_OCTET_STREAM)
}
InvokeResponse::Err(e) => {
let mut response = HttpResponse::new(serde_json::to_vec(&e.0)?.into());
response.set_status(StatusCode::BAD_REQUEST);
(response, mime::TEXT_PLAIN)
}
},
Err(e) => {
let mut response = HttpResponse::new(e.as_bytes().to_vec().into());
response.set_status(StatusCode::BAD_REQUEST);
(response, mime::TEXT_PLAIN)
}
};
response.set_mimetype(Some(content_type.essence_str().into()));
response
}
Method::OPTIONS => {
let mut r = HttpResponse::new(Vec::new().into());
r.headers_mut().insert(
ACCESS_CONTROL_ALLOW_HEADERS,
HeaderValue::from_static("Content-Type, Tauri-Callback, Tauri-Error, Tauri-Channel-Id"),
);
r
}
_ => {
let mut r = HttpResponse::new(
"only POST and OPTIONS are allowed"
.as_bytes()
.to_vec()
.into(),
);
r.set_status(StatusCode::METHOD_NOT_ALLOWED);
r.set_mimetype(Some(mime::TEXT_PLAIN.essence_str().into()));
r
}
};
response
.headers_mut()
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
Ok(response)
})
}
#[cfg(not(ipc_custom_protocol))]
fn handle_ipc_message<R: Runtime>(message: String, manager: &WindowManager<R>, label: &str) {
if let Some(window) = manager.get_window(label) {
use serde::{Deserialize, Deserializer};
pub(crate) struct HeaderMap(http::HeaderMap);
impl<'de> Deserialize<'de> for HeaderMap {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map = std::collections::HashMap::<String, String>::deserialize(deserializer)?;
let mut headers = http::HeaderMap::default();
for (key, value) in map {
if let (Ok(key), Ok(value)) = (
http::HeaderName::from_bytes(key.as_bytes()),
http::HeaderValue::from_str(&value),
) {
headers.insert(key, value);
} else {
return Err(serde::de::Error::custom(format!(
"invalid header `{key}` `{value}`"
)));
}
}
Ok(Self(headers))
}
}
#[derive(Deserialize)]
struct RequestOptions {
headers: HeaderMap,
}
#[derive(Deserialize)]
struct Message {
cmd: String,
callback: CallbackFn,
error: CallbackFn,
#[serde(flatten)]
payload: serde_json::Value,
options: Option<RequestOptions>,
}
#[allow(unused_mut)]
let mut invoke_message: Option<crate::Result<Message>> = None;
#[cfg(feature = "isolation")]
{
#[derive(serde::Deserialize)]
struct IsolationMessage<'a> {
cmd: String,
callback: CallbackFn,
error: CallbackFn,
#[serde(flatten)]
payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>,
options: Option<RequestOptions>,
}
if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
invoke_message.replace(
serde_json::from_str::<IsolationMessage<'_>>(&message)
.map_err(Into::into)
.and_then(|message| {
Ok(Message {
cmd: message.cmd,
callback: message.callback,
error: message.error,
payload: serde_json::from_slice(&crypto_keys.decrypt(message.payload)?)?,
options: message.options,
})
}),
);
}
}
match invoke_message
.unwrap_or_else(|| serde_json::from_str::<Message>(&message).map_err(Into::into))
{
Ok(message) => {
let _ = window.on_message(InvokeRequest {
cmd: message.cmd,
callback: message.callback,
error: message.error,
body: message.payload.into(),
headers: message.options.map(|o| o.headers.0).unwrap_or_default(),
});
}
Err(e) => {
let _ = window.eval(&format!(
r#"console.error({})"#,
serde_json::Value::String(e.to_string())
));
}
}
}
}
fn handle_ipc_request<R: Runtime>(
request: &HttpRequest,
manager: &WindowManager<R>,
label: &str,
) -> std::result::Result<InvokeResponse, String> {
if let Some(window) = manager.get_window(label) {
// TODO: consume instead
#[allow(unused_mut)]
let mut body = request.body().clone();
let cmd = request
.uri()
.strip_prefix("ipc://localhost/")
.map(|c| c.to_string())
// the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
// where `$P` is not `localhost/*`
// in this case the IPC call is considered invalid
.unwrap_or_else(|| "".to_string());
let cmd = percent_encoding::percent_decode(cmd.as_bytes())
.decode_utf8_lossy()
.to_string();
// the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it
#[cfg(all(feature = "isolation", ipc_custom_protocol))]
if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
.and_then(|raw| crypto_keys.decrypt(raw))
.map_err(|e| e.to_string())?;
}
let callback = CallbackFn(
request
.headers()
.get(TAURI_CALLBACK_HEADER_NAME)
.ok_or("missing Tauri-Callback header")?
.to_str()
.map_err(|_| "Tauri callback header value must be a string")?
.parse()
.map_err(|_| "Tauri callback header value must be a numeric string")?,
);
let error = CallbackFn(
request
.headers()
.get(TAURI_ERROR_HEADER_NAME)
.ok_or("missing Tauri-Error header")?
.to_str()
.map_err(|_| "Tauri error header value must be a string")?
.parse()
.map_err(|_| "Tauri error header value must be a numeric string")?,
);
let content_type = request
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.map(|mime| mime.parse())
.unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM))
.map_err(|_| "unknown content type")?;
let body = if content_type == mime::APPLICATION_OCTET_STREAM {
body.into()
} else if content_type == mime::APPLICATION_JSON {
if cfg!(ipc_custom_protocol) {
serde_json::from_slice::<serde_json::Value>(&body)
.map_err(|e| e.to_string())?
.into()
} else {
// the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it
serde_json::Value::Object(Default::default()).into()
}
} else {
return Err(format!("content type {content_type} is not implemented"));
};
let payload = InvokeRequest {
cmd,
callback,
error,
body,
headers: request.headers().clone(),
};
let rx = window.on_message(payload);
Ok(rx.recv().unwrap())
} else {
Err("window not found".into())
}
}

View File

@@ -31,7 +31,7 @@ fn json_to_java<'a, R: Runtime>(
}
JsonValue::String(val) => (
"Ljava/lang/Object;",
JObject::from(env.new_string(&val)?).into(),
JObject::from(env.new_string(val)?).into(),
),
JsonValue::Array(val) => {
let js_array_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSArray")?;
@@ -60,7 +60,7 @@ fn json_to_java<'a, R: Runtime>(
data,
"put",
format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"),
&[env.new_string(&key)?.into(), val],
&[env.new_string(key)?.into(), val],
)?;
}

View File

@@ -16,7 +16,7 @@
//! - **test**: Enables the [`test`] module exposing unit test helpers.
//! - **dox**: Internal feature to generate Rust documentation without linking on Linux.
//! - **objc-exception**: Wrap each msg_send! in a @try/@catch and panics if an exception is caught, preventing Objective-C from unwinding into Rust.
//! - **linux-protocol-headers**: Enables headers support for custom protocol requests on Linux. Requires webkit2gtk v2.36 or above.
//! - **linux-ipc-protocol**: Use custom protocol for faster IPC on Linux. Requires webkit2gtk v2.40 or above.
//! - **isolation**: Enables the isolation pattern. Enabled by default if the `tauri > pattern > use` config option is set to `isolation` on the `tauri.conf.json` file.
//! - **custom-protocol**: Feature managed by the Tauri CLI. When enabled, Tauri assumes a production environment instead of a development one.
//! - **devtools**: Enables the developer tools (Web inspector) and [`Window::open_devtools`]. Enabled by default on debug builds.
@@ -81,7 +81,7 @@ pub mod async_runtime;
pub mod command;
mod error;
mod event;
mod hooks;
pub mod ipc;
mod manager;
mod pattern;
pub mod plugin;
@@ -120,23 +120,31 @@ macro_rules! android_binding {
handlePluginResponse,
[i32, JString, JString],
);
::tauri::wry::application::android_fn!(
app_tauri,
plugin,
PluginManager,
sendChannelData,
[i64, JString],
);
// this function is a glue between PluginManager.kt > handlePluginResponse and Rust
#[allow(non_snake_case)]
pub unsafe fn handlePluginResponse(
env: JNIEnv,
_: JClass,
id: i32,
success: JString,
error: JString,
) {
pub fn handlePluginResponse(env: JNIEnv, _: JClass, id: i32, success: JString, error: JString) {
::tauri::handle_android_plugin_response(env, id, success, error);
}
// this function is a glue between PluginManager.kt > sendChannelData and Rust
#[allow(non_snake_case)]
pub fn sendChannelData(env: JNIEnv, _: JClass, id: i64, data: JString) {
::tauri::send_channel_data(env, id, data);
}
};
}
#[cfg(all(feature = "wry", target_os = "android"))]
#[doc(hidden)]
pub use plugin::mobile::handle_android_plugin_response;
pub use plugin::mobile::{handle_android_plugin_response, send_channel_data};
#[cfg(all(feature = "wry", target_os = "android"))]
#[doc(hidden)]
pub use tauri_runtime_wry::wry;
@@ -180,10 +188,6 @@ pub use {
App, AppHandle, AssetResolver, Builder, CloseRequestApi, GlobalWindowEvent, RunEvent,
WindowEvent,
},
self::hooks::{
Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokePayload, InvokeResolver,
InvokeResponder, InvokeResponse, OnPageLoad, PageLoadPayload, SetupHook,
},
self::manager::Asset,
self::runtime::{
webview::WebviewAttributes,

View File

@@ -11,36 +11,32 @@ use std::{
};
use serde::Serialize;
use serde_json::Value as JsonValue;
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
use url::Url;
use tauri_macros::default_runtime;
use tauri_utils::debug_eprintln;
#[cfg(feature = "isolation")]
use tauri_utils::pattern::isolation::RawIsolationPayload;
use tauri_utils::{
assets::{AssetKey, CspHash},
config::{Csp, CspDirectiveSources},
html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
};
use crate::app::{GlobalMenuEventListener, WindowMenuEvent};
use crate::hooks::IpcJavascript;
#[cfg(feature = "isolation")]
use crate::hooks::IsolationJavascript;
use crate::pattern::PatternJavascript;
use crate::{
app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
app::{
AppHandle, GlobalMenuEventListener, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad,
PageLoadPayload, WindowMenuEvent,
},
event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload},
ipc::{Invoke, InvokeHandler, InvokeResponder},
pattern::PatternJavascript,
plugin::PluginStore,
runtime::{
http::{
MimeType, Request as HttpRequest, Response as HttpResponse,
ResponseBuilder as HttpResponseBuilder,
},
webview::{WebviewIpcHandler, WindowBuilder},
webview::WindowBuilder,
window::{dpi::PhysicalSize, DetachedWindow, FileDropEvent, PendingWindow},
},
utils::{
@@ -48,7 +44,8 @@ use crate::{
config::{AppUrl, Config, WindowUrl},
PackageInfo,
},
Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
window::{UriSchemeProtocolHandler, WebResourceRequestHandler},
Context, EventLoopMessage, Icon, Manager, Pattern, Runtime, Scopes, StateManager, Window,
WindowEvent,
};
@@ -70,8 +67,8 @@ const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
const MENU_EVENT: &str = "tauri://menu";
pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
include_str!("../scripts/stringify-ipc-message-fn.js");
pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
include_str!("../scripts/process-ipc-message-fn.js");
// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
// and we do not get a secure context without the custom protocol that proxies to the dev server
@@ -79,6 +76,20 @@ pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
#[cfg(feature = "isolation")]
#[derive(Template)]
#[default_template("../scripts/isolation.js")]
pub(crate) struct IsolationJavascript<'a> {
pub(crate) isolation_src: &'a str,
pub(crate) style: &'a str,
}
#[derive(Template)]
#[default_template("../scripts/ipc.js")]
pub(crate) struct IpcJavascript<'a> {
pub(crate) isolation_origin: &'a str,
}
#[derive(Default)]
/// Spaced and quoted Content-Security-Policy hash values.
struct CspHashStrings {
@@ -229,7 +240,7 @@ pub struct InnerWindowManager<R: Runtime> {
/// Window event listeners to all windows.
window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
/// Responder for invoke calls.
invoke_responder: Arc<InvokeResponder<R>>,
invoke_responder: Option<Arc<InvokeResponder<R>>>,
/// The script that initializes the invoke system.
invoke_initialization_script: String,
/// Application pattern.
@@ -302,7 +313,7 @@ impl<R: Runtime> WindowManager<R> {
state: StateManager,
window_event_listeners: Vec<GlobalWindowEventListener<R>>,
(menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
(invoke_responder, invoke_initialization_script): (Arc<InvokeResponder<R>>, String),
(invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
) -> Self {
// generate a random isolation key at runtime
#[cfg(feature = "isolation")]
@@ -353,7 +364,7 @@ impl<R: Runtime> WindowManager<R> {
}
/// The invoke responder.
pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
pub(crate) fn invoke_responder(&self) -> Option<Arc<InvokeResponder<R>>> {
self.inner.invoke_responder.clone()
}
@@ -506,6 +517,14 @@ impl<R: Runtime> WindowManager<R> {
registered_scheme_protocols.push("tauri".into());
}
if !registered_scheme_protocols.contains(&"ipc".into()) {
pending.register_uri_scheme_protocol(
"ipc",
crate::ipc::protocol::get(self.clone(), pending.label.clone()),
);
registered_scheme_protocols.push("ipc".into());
}
#[cfg(feature = "protocol-asset")]
if !registered_scheme_protocols.contains(&"asset".into()) {
let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
@@ -538,7 +557,7 @@ impl<R: Runtime> WindowManager<R> {
let asset = String::from_utf8_lossy(asset.as_ref());
let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
runtime_aes_gcm_key: &aes_gcm_key,
stringify_ipc_message_fn: STRINGIFY_IPC_MESSAGE_FN,
process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN,
};
match template.render(asset.as_ref(), &Default::default()) {
Ok(asset) => HttpResponseBuilder::new()
@@ -567,43 +586,6 @@ impl<R: Runtime> WindowManager<R> {
Ok(pending)
}
fn prepare_ipc_handler(&self) -> WebviewIpcHandler<EventLoopMessage, R> {
let manager = self.clone();
Box::new(move |window, #[allow(unused_mut)] mut request| {
if let Some(window) = manager.get_window(&window.label) {
#[cfg(feature = "isolation")]
if let Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
match RawIsolationPayload::try_from(request.as_str())
.and_then(|raw| crypto_keys.decrypt(raw))
{
Ok(json) => request = json,
Err(e) => {
let error: crate::Error = e.into();
let _ = window.eval(&format!(
r#"console.error({})"#,
JsonValue::String(error.to_string())
));
return;
}
}
}
match serde_json::from_str::<InvokePayload>(&request) {
Ok(message) => {
let _ = window.on_message(message);
}
Err(e) => {
let error: crate::Error = e.into();
let _ = window.eval(&format!(
r#"console.error({})"#,
JsonValue::String(error.to_string())
));
}
}
}
})
}
pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
let assets = &self.inner.assets;
if path.ends_with('/') {
@@ -625,7 +607,7 @@ impl<R: Runtime> WindowManager<R> {
let asset_response = assets
.get(&path.as_str().into())
.or_else(|| {
eprintln!("Asset `{path}` not found; fallback to {path}.html");
debug_eprintln!("Asset `{path}` not found; fallback to {path}.html");
let fallback = format!("{}.html", path.as_str()).into();
let asset = assets.get(&fallback);
asset_path = fallback;
@@ -687,15 +669,11 @@ impl<R: Runtime> WindowManager<R> {
}
}
#[allow(clippy::type_complexity)]
fn prepare_uri_scheme_protocol(
&self,
window_origin: &str,
web_resource_request_handler: Option<
Box<dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync>,
>,
) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
{
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
) -> UriSchemeProtocolHandler {
#[cfg(all(dev, mobile))]
let url = {
let mut url = self.get_url().as_str().to_string();
@@ -740,7 +718,6 @@ impl<R: Runtime> WindowManager<R> {
#[cfg(all(dev, mobile))]
let mut response = {
use reqwest::StatusCode;
let decoded_path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
@@ -762,7 +739,7 @@ impl<R: Runtime> WindowManager<R> {
Ok(r) => {
let mut response_cache_ = response_cache.lock().unwrap();
let mut response = None;
if r.status() == StatusCode::NOT_MODIFIED {
if r.status() == http::StatusCode::NOT_MODIFIED {
response = response_cache_.get(&url);
}
let response = if let Some(r) = response {
@@ -846,6 +823,12 @@ impl<R: Runtime> WindowManager<R> {
freeze_prototype: &'a str,
}
#[derive(Template)]
#[default_template("../scripts/core.js")]
struct CoreJavascript<'a> {
os_name: &'a str,
}
let bundle_script = if with_global_tauri {
include_str!("../scripts/bundle.global.js")
} else {
@@ -872,7 +855,11 @@ impl<R: Runtime> WindowManager<R> {
"window['_' + window.__TAURI__.transformCallback(cb) ]".into()
)
),
core_script: include_str!("../scripts/core.js"),
core_script: &CoreJavascript {
os_name: std::env::consts::OS,
}
.render_default(&Default::default())?
.into_string(),
event_initialization_script: &self.event_initialization_script(),
plugin_initialization_script,
freeze_prototype,
@@ -923,7 +910,7 @@ mod test {
StateManager::new(),
Default::default(),
Default::default(),
(std::sync::Arc::new(|_, _, _, _| ()), "".into()),
(None, "".into()),
);
#[cfg(custom_protocol)]
@@ -1089,7 +1076,10 @@ impl<R: Runtime> WindowManager<R> {
#[allow(clippy::redundant_clone)]
app_handle.clone(),
)?;
pending.ipc_handler = Some(self.prepare_ipc_handler());
#[cfg(not(ipc_custom_protocol))]
{
pending.ipc_handler = Some(crate::ipc::protocol::message_handler(self.clone()));
}
// in `Windows`, we need to force a data_directory
// but we do respect user-specification

View File

@@ -5,8 +5,10 @@
//! The Tauri plugin extension to expand Tauri functionality.
use crate::{
utils::config::PluginConfig, AppHandle, Invoke, InvokeHandler, PageLoadPayload, RunEvent,
Runtime, Window,
app::PageLoadPayload,
ipc::{Invoke, InvokeHandler},
utils::config::PluginConfig,
AppHandle, RunEvent, Runtime, Window,
};
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;

View File

@@ -4,7 +4,7 @@
use super::{PluginApi, PluginHandle};
use crate::Runtime;
use crate::{ipc::Channel, AppHandle, Runtime};
#[cfg(target_os = "android")]
use crate::{
runtime::RuntimeHandle,
@@ -12,7 +12,7 @@ use crate::{
};
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use serde::{de::DeserializeOwned, Serialize};
use std::{
collections::HashMap,
@@ -20,11 +20,13 @@ use std::{
sync::{mpsc::channel, Mutex},
};
type PendingPluginCallHandler =
Box<dyn FnOnce(Result<serde_json::Value, serde_json::Value>) + Send + 'static>;
type PluginResponse = Result<serde_json::Value, serde_json::Value>;
type PendingPluginCallHandler = Box<dyn FnOnce(PluginResponse) + Send + 'static>;
static PENDING_PLUGIN_CALLS: OnceCell<Mutex<HashMap<i32, PendingPluginCallHandler>>> =
OnceCell::new();
static CHANNELS: OnceCell<Mutex<HashMap<u32, Channel>>> = OnceCell::new();
/// Possible errors when invoking a plugin.
#[derive(Debug, thiserror::Error)]
@@ -42,6 +44,17 @@ pub enum PluginInvokeError {
/// Failed to deserialize response.
#[error("failed to deserialize response: {0}")]
CannotDeserializeResponse(serde_json::Error),
/// Failed to serialize request payload.
#[error("failed to serialize payload: {0}")]
CannotSerializePayload(serde_json::Error),
}
pub(crate) fn register_channel(channel: Channel) {
CHANNELS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(channel.id(), channel);
}
/// Glue between Rust and the Kotlin code that sends the plugin response back.
@@ -86,6 +99,26 @@ pub fn handle_android_plugin_response(
}
}
/// Glue between Rust and the Kotlin code that sends the channel data.
#[cfg(target_os = "android")]
pub fn send_channel_data(
env: jni::JNIEnv<'_>,
channel_id: i64,
data_str: jni::objects::JString<'_>,
) {
let data: serde_json::Value =
serde_json::from_str(env.get_string(data_str).unwrap().to_str().unwrap()).unwrap();
if let Some(channel) = CHANNELS
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&(channel_id as u32))
{
let _ = channel.send(data);
}
}
/// Error response from the Kotlin and Swift backends.
#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)]
pub struct ErrorResponse<T = ()> {
@@ -236,81 +269,22 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
}
impl<R: Runtime> PluginHandle<R> {
/// Executes the given mobile method.
pub fn run_mobile_plugin<T: serde::de::DeserializeOwned>(
/// Executes the given mobile command.
pub fn run_mobile_plugin<T: DeserializeOwned>(
&self,
method: impl AsRef<str>,
payload: impl serde::Serialize,
command: impl AsRef<str>,
payload: impl Serialize,
) -> Result<T, PluginInvokeError> {
#[cfg(target_os = "ios")]
{
self.run_ios_plugin(method, payload).map_err(Into::into)
}
#[cfg(target_os = "android")]
{
self.run_android_plugin(method, payload).map_err(Into::into)
}
}
// Executes the given iOS method.
#[cfg(target_os = "ios")]
fn run_ios_plugin<T: serde::de::DeserializeOwned>(
&self,
method: impl AsRef<str>,
payload: impl serde::Serialize,
) -> Result<T, PluginInvokeError> {
use std::{
ffi::CStr,
os::raw::{c_char, c_int},
};
let id: i32 = rand::random();
let (tx, rx) = channel();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(
id,
Box::new(move |arg| {
tx.send(arg).unwrap();
}),
);
unsafe {
extern "C" fn plugin_method_response_handler(
id: c_int,
success: c_int,
payload: *const c_char,
) {
let payload = unsafe {
assert!(!payload.is_null());
CStr::from_ptr(payload)
};
if let Some(handler) = PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.remove(&id)
{
let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap();
handler(if success == 1 {
Ok(payload)
} else {
Err(payload)
});
}
}
crate::ios::run_plugin_method(
id,
&self.name.into(),
&method.as_ref().into(),
crate::ios::json_to_dictionary(&serde_json::to_value(payload).unwrap()) as _,
crate::ios::PluginMessageCallback(plugin_method_response_handler),
);
}
run_command(
self.name,
&self.handle,
command,
serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?,
move |response| {
tx.send(response).unwrap();
},
)?;
let response = rx.recv().unwrap();
match response {
@@ -322,89 +296,154 @@ impl<R: Runtime> PluginHandle<R> {
),
}
}
// Executes the given Android method.
#[cfg(target_os = "android")]
fn run_android_plugin<T: serde::de::DeserializeOwned>(
&self,
method: impl AsRef<str>,
payload: impl serde::Serialize,
) -> Result<T, PluginInvokeError> {
use jni::{errors::Error as JniError, objects::JObject, JNIEnv};
fn run<R: Runtime>(
id: i32,
plugin: &'static str,
method: String,
payload: &serde_json::Value,
runtime_handle: &R::Handle,
env: JNIEnv<'_>,
activity: JObject<'_>,
) -> Result<(), JniError> {
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
let plugin_manager = env
.call_method(
activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;
env.call_method(
plugin_manager,
"runCommand",
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
&[
id.into(),
env.new_string(plugin)?.into(),
env.new_string(&method)?.into(),
data,
],
)?;
Ok(())
}
let handle = match self.handle.runtime() {
RuntimeOrDispatch::Runtime(r) => r.handle(),
RuntimeOrDispatch::RuntimeHandle(h) => h,
_ => unreachable!(),
};
let id: i32 = rand::random();
let plugin_name = self.name;
let method = method.as_ref().to_string();
let payload = serde_json::to_value(payload).unwrap();
let handle_ = handle.clone();
let (tx, rx) = channel();
let tx_ = tx.clone();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(
id,
Box::new(move |arg| {
tx.send(Ok(arg)).unwrap();
}),
);
handle.run_on_android_context(move |env, activity, _webview| {
if let Err(e) = run::<R>(id, plugin_name, method, &payload, &handle_, env, activity) {
tx_.send(Err(e)).unwrap();
}
});
let response = rx.recv().unwrap()?;
match response {
Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse),
Err(r) => Err(
serde_json::from_value::<ErrorResponse>(r)
.map(Into::into)
.map_err(PluginInvokeError::CannotDeserializeResponse)?,
),
}
}
}
#[cfg(target_os = "ios")]
pub(crate) fn run_command<R: Runtime, C: AsRef<str>, F: FnOnce(PluginResponse) + Send + 'static>(
name: &str,
_handle: &AppHandle<R>,
command: C,
payload: serde_json::Value,
handler: F,
) -> Result<(), PluginInvokeError> {
use std::{
ffi::CStr,
os::raw::{c_char, c_int, c_ulonglong},
};
let id: i32 = rand::random();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(id, Box::new(handler));
unsafe {
extern "C" fn plugin_command_response_handler(
id: c_int,
success: c_int,
payload: *const c_char,
) {
let payload = unsafe {
assert!(!payload.is_null());
CStr::from_ptr(payload)
};
if let Some(handler) = PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.remove(&id)
{
let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap();
handler(if success == 1 {
Ok(payload)
} else {
Err(payload)
});
}
}
extern "C" fn send_channel_data_handler(id: c_ulonglong, payload: *const c_char) {
let payload = unsafe {
assert!(!payload.is_null());
CStr::from_ptr(payload)
};
if let Some(channel) = CHANNELS
.get_or_init(Default::default)
.lock()
.unwrap()
.get(&(id as u32))
{
let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap();
let _ = channel.send(payload);
}
}
crate::ios::run_plugin_command(
id,
&name.into(),
&command.as_ref().into(),
crate::ios::json_to_dictionary(&payload) as _,
crate::ios::PluginMessageCallback(plugin_command_response_handler),
crate::ios::ChannelSendDataCallback(send_channel_data_handler),
);
}
Ok(())
}
#[cfg(target_os = "android")]
pub(crate) fn run_command<
R: Runtime,
C: AsRef<str>,
F: FnOnce(PluginResponse) + Send + Clone + 'static,
>(
name: &str,
handle: &AppHandle<R>,
command: C,
payload: serde_json::Value,
handler: F,
) -> Result<(), PluginInvokeError> {
use jni::{errors::Error as JniError, objects::JObject, JNIEnv};
fn run<R: Runtime>(
id: i32,
plugin: &str,
command: String,
payload: &serde_json::Value,
runtime_handle: &R::Handle,
env: JNIEnv<'_>,
activity: JObject<'_>,
) -> Result<(), JniError> {
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
let plugin_manager = env
.call_method(
activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;
env.call_method(
plugin_manager,
"runCommand",
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
&[
id.into(),
env.new_string(plugin)?.into(),
env.new_string(&command)?.into(),
data,
],
)?;
Ok(())
}
let handle = match handle.runtime() {
RuntimeOrDispatch::Runtime(r) => r.handle(),
RuntimeOrDispatch::RuntimeHandle(h) => h,
_ => unreachable!(),
};
let id: i32 = rand::random();
let plugin_name = name.to_string();
let command = command.as_ref().to_string();
let handle_ = handle.clone();
PENDING_PLUGIN_CALLS
.get_or_init(Default::default)
.lock()
.unwrap()
.insert(id, Box::new(handler.clone()));
handle.run_on_android_context(move |env, activity, _webview| {
if let Err(e) = run::<R>(id, &plugin_name, command, &payload, &handle_, env, activity) {
handler(Err(e.to_string().into()));
}
});
Ok(())
}

View File

@@ -168,9 +168,10 @@ impl Scope {
mod tests {
use super::RemoteDomainAccessScope;
use crate::{
api::ipc::CallbackFn,
ipc::CallbackFn,
test::{assert_ipc_response, mock_app, MockRuntime},
App, InvokePayload, Manager, Window, WindowBuilder,
window::InvokeRequest,
App, Manager, Window, WindowBuilder,
};
const PLUGIN_NAME: &str = "test";
@@ -188,7 +189,7 @@ mod tests {
(app, window)
}
fn path_is_absolute_payload() -> InvokePayload {
fn path_is_absolute_request() -> InvokeRequest {
let callback = CallbackFn(0);
let error = CallbackFn(1);
@@ -198,23 +199,25 @@ mod tests {
serde_json::Value::String(std::env::current_dir().unwrap().display().to_string()),
);
InvokePayload {
InvokeRequest {
cmd: "plugin:path|is_absolute".into(),
callback,
error,
inner: serde_json::Value::Object(payload),
body: serde_json::Value::Object(payload).into(),
headers: Default::default(),
}
}
fn plugin_test_payload() -> InvokePayload {
fn plugin_test_request() -> InvokeRequest {
let callback = CallbackFn(0);
let error = CallbackFn(1);
InvokePayload {
InvokeRequest {
cmd: format!("plugin:{PLUGIN_NAME}|doSomething"),
callback,
error,
inner: Default::default(),
body: Default::default(),
headers: Default::default(),
}
}
@@ -227,7 +230,7 @@ mod tests {
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
path_is_absolute_payload(),
path_is_absolute_request(),
Err(&crate::window::ipc_scope_not_found_error_message(
"main",
"https://tauri.app/",
@@ -244,7 +247,7 @@ mod tests {
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
path_is_absolute_payload(),
path_is_absolute_request(),
Err(&crate::window::ipc_scope_window_error_message("main")),
);
}
@@ -258,7 +261,7 @@ mod tests {
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
path_is_absolute_payload(),
path_is_absolute_request(),
Err(&crate::window::ipc_scope_domain_error_message(
"https://tauri.app/",
)),
@@ -277,25 +280,25 @@ mod tests {
]);
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
assert_ipc_response(&window, path_is_absolute_request(), Ok(true));
window.navigate("https://blog.tauri.app".parse().unwrap());
assert_ipc_response(
&window,
path_is_absolute_payload(),
path_is_absolute_request(),
Err(&crate::window::ipc_scope_domain_error_message(
"https://blog.tauri.app/",
)),
);
window.navigate("https://sub.tauri.app".parse().unwrap());
assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
assert_ipc_response(&window, path_is_absolute_request(), Ok(true));
window.window.label = "test".into();
window.navigate("https://dev.tauri.app".parse().unwrap());
assert_ipc_response(
&window,
path_is_absolute_payload(),
path_is_absolute_request(),
Err(&crate::window::ipc_scope_not_found_error_message(
"test",
"https://dev.tauri.app/",
@@ -310,7 +313,7 @@ mod tests {
.add_plugin("path")]);
window.navigate("https://tauri.app/inner/path".parse().unwrap());
assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
assert_ipc_response(&window, path_is_absolute_request(), Ok(true));
}
#[test]
@@ -322,7 +325,7 @@ mod tests {
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
path_is_absolute_payload(),
path_is_absolute_request(),
Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
);
}
@@ -336,7 +339,7 @@ mod tests {
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
plugin_test_payload(),
plugin_test_request(),
Err(&format!("plugin {PLUGIN_NAME} not found")),
);
}
@@ -350,7 +353,7 @@ mod tests {
window.navigate("https://tauri.app".parse().unwrap());
assert_ipc_response(
&window,
plugin_test_payload(),
plugin_test_request(),
Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
);
}

View File

@@ -4,7 +4,8 @@
use crate::{
command::{CommandArg, CommandItem},
InvokeError, Runtime,
ipc::InvokeError,
Runtime,
};
use state::TypeMap;

View File

@@ -42,11 +42,12 @@
//! // in this case we'll run the my_cmd command with no arguments
//! tauri::test::assert_ipc_response(
//! &window,
//! tauri::InvokePayload {
//! tauri::window::InvokeRequest {
//! cmd: "my_cmd".into(),
//! callback: tauri::api::ipc::CallbackFn(0),
//! error: tauri::api::ipc::CallbackFn(1),
//! inner: serde_json::Value::Null,
//! callback: tauri::ipc::CallbackFn(0),
//! error: tauri::ipc::CallbackFn(1),
//! body: serde_json::Value::Null.into(),
//! headers: Default::default(),
//! },
//! Ok(())
//! );
@@ -59,21 +60,19 @@
mod mock_runtime;
pub use mock_runtime::*;
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::{
borrow::Cow,
collections::HashMap,
fmt::Debug,
hash::{Hash, Hasher},
sync::{
mpsc::{channel, Sender},
Arc, Mutex,
},
sync::Arc,
};
use crate::hooks::window_invoke_responder;
use crate::{api::ipc::CallbackFn, App, Builder, Context, InvokePayload, Manager, Pattern, Window};
use crate::{
ipc::{CallbackFn, InvokeResponse},
window::InvokeRequest,
App, Builder, Context, Pattern, Window,
};
use tauri_utils::{
assets::{AssetKey, Assets, CspHash},
config::{Config, PatternKind, TauriConfig},
@@ -92,8 +91,6 @@ impl Hash for IpcKey {
}
}
struct Ipc(Mutex<HashMap<IpcKey, Sender<std::result::Result<JsonValue, JsonValue>>>>);
/// An empty [`Assets`] implementation.
pub struct NoopAsset {
csp_hashes: Vec<CspHash<'static>>,
@@ -166,20 +163,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
/// }
/// ```
pub fn mock_builder() -> Builder<MockRuntime> {
let mut builder = Builder::<MockRuntime>::new().manage(Ipc(Default::default()));
builder.invoke_responder = Arc::new(|window, response, callback, error| {
let window_ = window.clone();
let ipc = window_.state::<Ipc>();
let mut ipc_ = ipc.0.lock().unwrap();
if let Some(tx) = ipc_.remove(&IpcKey { callback, error }) {
tx.send(response.into_result()).unwrap();
} else {
window_invoke_responder(window, response, callback, error)
}
});
builder
Builder::<MockRuntime>::new()
}
/// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`].
@@ -222,11 +206,12 @@ pub fn mock_app() -> App<MockRuntime> {
/// // run the `ping` command and assert it returns `pong`
/// tauri::test::assert_ipc_response(
/// &window,
/// tauri::InvokePayload {
/// tauri::window::InvokeRequest {
/// cmd: "ping".into(),
/// callback: tauri::api::ipc::CallbackFn(0),
/// error: tauri::api::ipc::CallbackFn(1),
/// inner: serde_json::Value::Null,
/// callback: tauri::ipc::CallbackFn(0),
/// error: tauri::ipc::CallbackFn(1),
/// body: serde_json::Value::Null.into(),
/// headers: Default::default(),
/// },
/// // the expected response is a success with the "pong" payload
/// // we could also use Err("error message") here to ensure the command failed
@@ -237,18 +222,17 @@ pub fn mock_app() -> App<MockRuntime> {
/// ```
pub fn assert_ipc_response<T: Serialize + Debug>(
window: &Window<MockRuntime>,
payload: InvokePayload,
request: InvokeRequest,
expected: Result<T, T>,
) {
let callback = payload.callback;
let error = payload.error;
let ipc = window.state::<Ipc>();
let (tx, rx) = channel();
ipc.0.lock().unwrap().insert(IpcKey { callback, error }, tx);
window.clone().on_message(payload).unwrap();
let rx = window.clone().on_message(request);
let response = rx.recv().unwrap();
assert_eq!(
rx.recv().unwrap(),
match response {
InvokeResponse::Ok(b) => Ok(b.into_json()),
InvokeResponse::Err(e) => Err(e.0),
},
expected
.map(|e| serde_json::to_value(e).unwrap())
.map_err(|e| serde_json::to_value(e).unwrap())

View File

@@ -6,6 +6,7 @@
pub(crate) mod menu;
use http::HeaderMap;
pub use menu::{MenuEvent, MenuHandle};
pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState};
use url::Url;
@@ -13,11 +14,12 @@ use url::Url;
#[cfg(target_os = "macos")]
use crate::TitleBarStyle;
use crate::{
api::ipc::CallbackFn,
app::AppHandle,
command::{CommandArg, CommandItem},
event::{Event, EventHandler},
hooks::{InvokePayload, InvokeResponder},
ipc::{
CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver, InvokeResponse,
},
manager::WindowManager,
runtime::{
http::{Request as HttpRequest, Response as HttpResponse},
@@ -32,8 +34,7 @@ use crate::{
sealed::ManagerBase,
sealed::RuntimeOrDispatch,
utils::config::{WindowConfig, WindowEffectsConfig, WindowUrl},
EventLoopMessage, Invoke, InvokeError, InvokeMessage, InvokeResolver, Manager, PageLoadPayload,
Runtime, Theme, WindowEvent,
EventLoopMessage, Manager, Runtime, Theme, WindowEvent,
};
#[cfg(desktop)]
use crate::{
@@ -56,11 +57,16 @@ use std::{
fmt,
hash::{Hash, Hasher},
path::PathBuf,
sync::{Arc, Mutex},
sync::{
mpsc::{sync_channel, Receiver},
Arc, Mutex,
},
};
pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send;
pub(crate) type UriSchemeProtocolHandler =
Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>;
#[derive(Clone, Serialize)]
struct WindowCreatedEvent {
@@ -772,6 +778,21 @@ struct JsEventListenerKey {
pub event: String,
}
/// The IPC invoke request.
#[derive(Debug)]
pub struct InvokeRequest {
/// The invoke command.
pub cmd: String,
/// The success callback.
pub callback: CallbackFn,
/// The error callback.
pub error: CallbackFn,
/// The body of the request.
pub body: InvokeBody,
/// The request headers.
pub headers: HeaderMap,
}
// TODO: expand these docs since this is a pretty important type
/// A webview window managed by Tauri.
///
@@ -967,10 +988,6 @@ impl<R: Runtime> Window<R> {
WindowBuilder::<'a, R>::new(manager, label.into(), url)
}
pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
self.manager.invoke_responder()
}
/// The current window's dispatcher.
pub(crate) fn dispatcher(&self) -> R::Dispatcher {
self.window.dispatcher.clone()
@@ -1662,14 +1679,17 @@ impl<R: Runtime> Window<R> {
}
fn is_local_url(&self, current_url: &Url) -> bool {
self.manager.get_url().make_relative(current_url).is_some() || {
let protocol_url = self.manager.protocol_url();
current_url.scheme() == protocol_url.scheme() && current_url.domain() == protocol_url.domain()
}
self.manager.get_url().make_relative(current_url).is_some()
|| {
let protocol_url = self.manager.protocol_url();
current_url.scheme() == protocol_url.scheme()
&& current_url.domain() == protocol_url.domain()
}
|| (cfg!(dev) && current_url.domain() == Some("tauri.localhost"))
}
/// Handles this window receiving an [`InvokeMessage`].
pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> {
/// Handles this window receiving an [`InvokeRequest`].
pub fn on_message(self, request: InvokeRequest) -> Receiver<InvokeResponse> {
let manager = self.manager.clone();
let current_url = self.url();
let is_local = self.is_local_url(&current_url);
@@ -1691,39 +1711,108 @@ impl<R: Runtime> Window<R> {
}
}
};
match payload.cmd.as_str() {
"__initialized" => {
let payload: PageLoadPayload = serde_json::from_value(payload.inner)?;
manager.run_on_page_load(self, payload);
}
_ => {
let message = InvokeMessage::new(
self.clone(),
manager.state(),
payload.cmd.to_string(),
payload.inner,
);
#[allow(clippy::redundant_clone)]
let resolver = InvokeResolver::new(self.clone(), payload.callback, payload.error);
let mut invoke = Invoke { message, resolver };
if !is_local && scope.is_none() {
invoke.resolver.reject(scope_not_found_error_message);
return Ok(());
}
let (tx, rx) = sync_channel(1);
if payload.cmd.starts_with("plugin:") {
if !is_local {
let command = invoke.message.command.replace("plugin:", "");
let plugin_name = command.split('|').next().unwrap().to_string();
if !scope
.map(|s| s.plugins().contains(&plugin_name))
.unwrap_or(true)
let custom_responder = self.manager.invoke_responder();
let resolver = InvokeResolver::new(
self.clone(),
Arc::new(
#[allow(unused_variables)]
move |window: Window<R>, cmd, response, callback, error| {
#[cfg(not(ipc_custom_protocol))]
{
use crate::ipc::{
format_callback::{
format as format_callback, format_result as format_callback_result,
},
Channel,
};
use serde_json::Value as JsonValue;
// the channel data command is the only command that uses a custom protocol on Linux
if custom_responder.is_none() && cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
{
invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
return Ok(());
fn responder_eval<R: Runtime>(
window: &Window<R>,
js: crate::api::Result<String>,
error: CallbackFn,
) {
let eval_js = match js {
Ok(js) => js,
Err(e) => format_callback(error, &e.to_string())
.expect("unable to serialize response error string to json"),
};
let _ = window.eval(&eval_js);
}
match &response {
InvokeResponse::Ok(InvokeBody::Json(v)) => {
if matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) {
let _ = Channel::from_ipc(window.clone(), callback).send(v);
} else {
responder_eval(
&window,
format_callback_result(Result::<_, ()>::Ok(v), callback, error),
error,
)
}
}
InvokeResponse::Ok(InvokeBody::Raw(v)) => {
let _ =
Channel::from_ipc(window.clone(), callback).send(InvokeBody::Raw(v.clone()));
}
InvokeResponse::Err(e) => responder_eval(
&window,
format_callback_result(Result::<(), _>::Err(&e.0), callback, error),
error,
),
}
}
}
if let Some(responder) = &custom_responder {
(responder)(window, cmd, &response, callback, error);
}
let _ = tx.send(response);
},
),
request.cmd.clone(),
request.callback,
request.error,
);
match request.cmd.as_str() {
"__initialized" => match request.body.deserialize() {
Ok(payload) => {
manager.run_on_page_load(self, payload);
resolver.resolve(());
}
Err(e) => resolver.reject(e.to_string()),
},
_ => {
#[cfg(mobile)]
let app_handle = self.app_handle.clone();
let message = InvokeMessage::new(
self,
manager.state(),
request.cmd.to_string(),
request.body,
request.headers,
);
let mut invoke = Invoke {
message,
resolver: resolver.clone(),
};
if !is_local && scope.is_none() {
invoke.resolver.reject(scope_not_found_error_message);
} else if request.cmd.starts_with("plugin:") {
let command = invoke.message.command.replace("plugin:", "");
let mut tokens = command.split('|');
// safe to unwrap: split always has a least one item
@@ -1733,101 +1822,59 @@ impl<R: Runtime> Window<R> {
.map(|c| c.to_string())
.unwrap_or_else(String::new);
if !(is_local
|| plugin == crate::ipc::channel::CHANNEL_PLUGIN_NAME
|| scope
.map(|s| s.plugins().contains(&plugin.into()))
.unwrap_or(true))
{
invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
return rx;
}
let command = invoke.message.command.clone();
let resolver = invoke.resolver.clone();
#[cfg(mobile)]
let message = invoke.message.clone();
#[allow(unused_mut)]
let mut handled = manager.extend_api(plugin, invoke);
#[cfg(target_os = "ios")]
#[cfg(mobile)]
{
if !handled {
handled = true;
let plugin = plugin.to_string();
let (callback, error) = (resolver.callback, resolver.error);
self.with_webview(move |webview| {
unsafe {
crate::ios::post_ipc_message(
webview.inner() as _,
&plugin.as_str().into(),
&heck::ToLowerCamelCase::to_lower_camel_case(message.command.as_str())
.as_str()
.into(),
crate::ios::json_to_dictionary(&message.payload) as _,
callback.0,
error.0,
)
};
})?;
}
}
#[cfg(target_os = "android")]
{
if !handled {
handled = true;
fn load_channels<R: Runtime>(payload: &serde_json::Value, window: &Window<R>) {
if let serde_json::Value::Object(map) = payload {
for v in map.values() {
if let serde_json::Value::String(s) = v {
if s.starts_with(crate::ipc::channel::IPC_PAYLOAD_PREFIX) {
crate::ipc::Channel::load_from_ipc(window.clone(), s);
}
}
}
}
}
let payload = message.payload.into_json();
// initialize channels
load_channels(&payload, &message.window);
let resolver_ = resolver.clone();
let runtime_handle = self.app_handle.runtime_handle.clone();
let plugin = plugin.to_string();
self.with_webview(move |webview| {
webview.jni_handle().exec(move |env, activity, webview| {
use jni::{
errors::Error as JniError,
objects::JObject,
JNIEnv,
};
fn handle_message<R: Runtime>(
plugin: &str,
runtime_handle: &R::Handle,
message: InvokeMessage<R>,
(callback, error): (CallbackFn, CallbackFn),
env: JNIEnv<'_>,
activity: JObject<'_>,
webview: JObject<'_>,
) -> Result<(), JniError> {
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, &message.payload)?;
let plugin_manager = env
.call_method(
activity,
"getPluginManager",
"()Lapp/tauri/plugin/PluginManager;",
&[],
)?
.l()?;
env.call_method(
plugin_manager,
"postIpcMessage",
"(Landroid/webkit/WebView;Ljava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;JJ)V",
&[
webview.into(),
env.new_string(plugin)?.into(),
env.new_string(&heck::ToLowerCamelCase::to_lower_camel_case(message.command.as_str()))?.into(),
data,
(callback.0 as i64).into(),
(error.0 as i64).into(),
],
)?;
Ok(())
}
if let Err(e) = handle_message(
&plugin,
&runtime_handle,
message,
(resolver_.callback, resolver_.error),
env,
activity,
webview,
) {
resolver_.reject(format!("failed to reach Android layer: {e}"));
}
});
})?;
if let Err(e) = crate::plugin::mobile::run_command(
plugin,
&app_handle,
message.command,
payload,
move |response| match response {
Ok(r) => resolver_.resolve(r),
Err(e) => resolver_.reject(e),
},
) {
resolver.reject(e.to_string());
return rx;
}
}
}
@@ -1836,7 +1883,6 @@ impl<R: Runtime> Window<R> {
}
} else {
let command = invoke.message.command.clone();
let resolver = invoke.resolver.clone();
let handled = manager.run_invoke_handler(invoke);
if !handled {
resolver.reject(format!("Command {command} not found"));
@@ -1845,7 +1891,7 @@ impl<R: Runtime> Window<R> {
}
}
Ok(())
rx
}
/// Evaluates JavaScript on this window.

View File

@@ -15,7 +15,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
window.__TAURI_ISOLATION_HOOK__ = (payload, options) => {
return payload
}

View File

@@ -3243,9 +3243,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b87728a671df8520c274fa9bed48d7384f5a965ef2fc364f01a942f6ff1ae6d2"
checksum = "436fb014010f6c87561125b14add8a6091354681f190bb9ffeb42819af9218a4"
dependencies = [
"bitflags",
"cairo-rs",
@@ -3329,6 +3329,7 @@ dependencies = [
"jni",
"libc",
"log",
"mime",
"objc",
"once_cell",
"percent-encoding",

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use tauri::command;
#[derive(Debug, Deserialize)]
@@ -17,8 +17,15 @@ pub fn log_operation(event: String, payload: Option<String>) {
log::info!("{} {:?}", event, payload);
}
#[command]
pub fn perform_request(endpoint: String, body: RequestBody) -> String {
println!("{} {:?}", endpoint, body);
"message response".into()
#[derive(Serialize)]
pub struct ApiResponse {
message: String,
}
#[command]
pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse {
println!("{} {:?}", endpoint, body);
ApiResponse {
message: "message response".into(),
}
}

View File

@@ -12,7 +12,7 @@ mod cmd;
mod tray;
use serde::Serialize;
use tauri::{window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl};
use tauri::{ipc::Channel, window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl};
use tauri_plugin_sample::{PingRequest, SampleExt};
#[derive(Clone, Serialize)]
@@ -66,6 +66,10 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
let value = Some("test".to_string());
let response = app.sample().ping(PingRequest {
value: value.clone(),
on_event: Channel::new(|event| {
println!("got channel event: {:?}", event);
Ok(())
}),
});
log::info!("got response: {:?}", response);
if let Ok(res) = response {

View File

@@ -17,6 +17,11 @@ class ExamplePlugin(private val activity: Activity): Plugin(activity) {
@Command
fun ping(invoke: Invoke) {
val onEvent = invoke.getChannel("onEvent")
val event = JSObject()
event.put("kind", "ping")
onEvent?.send(event)
val value = invoke.getString("value") ?: ""
val ret = JSObject()
ret.put("value", implementation.pong(value))

View File

@@ -2,19 +2,22 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import SwiftRs
import Tauri
import UIKit
import WebKit
import Tauri
import SwiftRs
class ExamplePlugin: Plugin {
@objc public func ping(_ invoke: Invoke) throws {
let value = invoke.getString("value")
invoke.resolve(["value": value as Any])
}
@objc public func ping(_ invoke: Invoke) throws {
let onEvent = invoke.getChannel("onEvent")
onEvent?.send(["kind": "ping"])
let value = invoke.getString("value")
invoke.resolve(["value": value as Any])
}
}
@_cdecl("init_plugin_sample")
func initPlugin() -> Plugin {
return ExamplePlugin()
return ExamplePlugin()
}

View File

@@ -17,8 +17,14 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
/// A helper class to access the sample APIs.
pub struct Sample<R: Runtime>(AppHandle<R>);
#[derive(serde::Serialize)]
struct Event {
kind: &'static str,
}
impl<R: Runtime> Sample<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
let _ = payload.on_event.send(Event { kind: "ping" });
Ok(PingResponse {
value: payload.value,
})

View File

@@ -3,10 +3,13 @@
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
use tauri::ipc::Channel;
#[derive(Debug, Serialize)]
#[derive(Serialize)]
pub struct PingRequest {
pub value: Option<String>,
#[serde(rename = "onEvent")]
pub on_event: Channel,
}
#[derive(Debug, Clone, Default, Deserialize)]

View File

@@ -26,7 +26,11 @@
"name": "theme",
"takesValue": true,
"description": "App theme",
"possibleValues": ["light", "dark", "system"]
"possibleValues": [
"light",
"dark",
"system"
]
},
{
"short": "v",
@@ -88,7 +92,10 @@
"security": {
"csp": {
"default-src": "'self' customprotocol: asset:",
"font-src": ["https://fonts.gstatic.com"],
"connect-src": "ipc: https://ipc.localhost",
"font-src": [
"https://fonts.gstatic.com"
],
"img-src": "'self' asset: https://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
},
@@ -96,8 +103,13 @@
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
"deny": ["$APPDATA/db/*.stronghold"]
"allow": [
"$APPDATA/db/**",
"$RESOURCE/**"
],
"deny": [
"$APPDATA/db/*.stronghold"
]
}
}
},

View File

@@ -18,7 +18,8 @@
window.__TAURI__
.invoke(commandName, args)
.then((response) => {
result.innerText = `Ok(${response})`
const val = response instanceof ArrayBuffer ? new TextDecoder().decode(response) : response
result.innerText = `Ok(${val})`
})
.catch((error) => {
result.innerText = `Err(${error})`
@@ -28,6 +29,7 @@
const container = document.querySelector('#container')
const commands = [
{ name: 'borrow_cmd' },
{ name: 'raw_request' },
{ name: 'window_label' },
{ name: 'simple_command' },
{ name: 'stateful_command' },

View File

@@ -9,7 +9,11 @@ mod commands;
use commands::{cmd, invoke, message, resolver};
use serde::Deserialize;
use tauri::{command, State, Window};
use tauri::{
command,
ipc::{Request, Response},
State, Window,
};
#[derive(Debug)]
pub struct MyState {
@@ -213,6 +217,12 @@ fn borrow_cmd_async(the_argument: &str) -> &str {
the_argument
}
#[command]
fn raw_request(request: Request<'_>) -> Response {
println!("{:?}", request);
Response::new(include_bytes!("./README.md").to_vec())
}
fn main() {
tauri::Builder::default()
.manage(MyState {
@@ -222,6 +232,7 @@ fn main() {
.invoke_handler(tauri::generate_handler![
borrow_cmd,
borrow_cmd_async,
raw_request,
window_label,
force_async,
force_async_with_result,

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["index.html"],
"devPath": ["index.html"],
"distDir": [
"index.html"
],
"devPath": [
"index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": "",
"withGlobalTauri": true
@@ -47,7 +51,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["index.html"],
"devPath": ["index.html"],
"distDir": [
"index.html"
],
"devPath": [
"index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
@@ -46,7 +50,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
console.log('hook', payload)
window.__TAURI_ISOLATION_HOOK__ = (payload, options) => {
console.log('hook', payload, options)
return payload
}

View File

@@ -60,7 +60,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["index.html"],
"devPath": ["index.html"],
"distDir": [
"index.html"
],
"devPath": [
"index.html"
],
"withGlobalTauri": true
},
"package": {
@@ -43,7 +47,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -47,7 +47,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["index.html"],
"devPath": ["index.html"],
"distDir": [
"index.html"
],
"devPath": [
"index.html"
],
"withGlobalTauri": true
},
"package": {
@@ -27,7 +31,7 @@
"category": "DeveloperTool"
},
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["../index.html"],
"devPath": ["../index.html"],
"distDir": [
"../index.html"
],
"devPath": [
"../index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": "",
"withGlobalTauri": true
@@ -23,7 +27,9 @@
"../../.icons/icon.icns",
"../../.icons/icon.ico"
],
"resources": ["assets/*"],
"resources": [
"assets/*"
],
"externalBin": [],
"copyright": "",
"category": "DeveloperTool",
@@ -47,7 +53,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -44,7 +44,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["index.html"],
"devPath": ["index.html"],
"distDir": [
"index.html"
],
"devPath": [
"index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": "",
"withGlobalTauri": true
@@ -47,7 +51,7 @@
}
],
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -47,7 +47,7 @@
}
],
"security": {
"csp": "default-src 'self'; media-src stream: https://stream.localhost asset: https://asset.localhost",
"csp": "default-src 'self' ipc:; media-src stream: https://stream.localhost asset: https://asset.localhost",
"assetProtocol": {
"scope": ["**/test_video.mp4"]
}

View File

@@ -1,8 +1,12 @@
{
"$schema": "../../../core/tauri-config-schema/schema.json",
"build": {
"distDir": ["src/index.html"],
"devPath": ["src/index.html"],
"distDir": [
"src/index.html"
],
"devPath": [
"src/index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
@@ -46,7 +50,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -41,10 +41,10 @@ interface IPCMessage {
* })
*
* test("mocked command", () => {
* mockIPC((cmd, args) => {
* mockIPC((cmd, payload) => {
* switch (cmd) {
* case "add":
* return (args.a as number) + (args.b as number);
* return (payload.a as number) + (payload.b as number);
* default:
* break;
* }
@@ -64,7 +64,7 @@ interface IPCMessage {
* })
*
* test("mocked command", () => {
* mockIPC((cmd, args) => {
* mockIPC((cmd, payload) => {
* if(cmd === "get_data") {
* return fetch("https://example.com/data.json")
* .then((response) => response.json())
@@ -78,19 +78,19 @@ interface IPCMessage {
* @since 1.0.0
*/
export function mockIPC(
cb: (cmd: string, args: Record<string, unknown>) => any | Promise<any>
cb: (cmd: string, payload: Record<string, unknown>) => any | Promise<any>
): void {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
window.__TAURI_IPC__ = async ({
cmd,
callback,
error,
...args
payload
}: IPCMessage) => {
try {
// @ts-expect-error The function key is dynamic and therefore not typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
window[`_${callback}`](await cb(cmd, args))
window[`_${callback}`](await cb(cmd, payload))
} catch (err) {
// @ts-expect-error The function key is dynamic and therefore not typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call

View File

@@ -43,17 +43,6 @@ enum BaseDirectory {
Template
}
declare global {
interface Window {
__TAURI__: {
path: {
__sep: string
__delimiter: string
}
}
}
}
/**
* Returns the path to the suggested directory for your app's config files.
* Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the value [`tauri.bundle.identifier`](https://tauri.app/v1/api/config/#bundleconfig.identifier) is configured in `tauri.conf.json`.

View File

@@ -11,12 +11,17 @@
/** @ignore */
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Window {
__TAURI_IPC__: (message: any) => void
ipc: {
postMessage: (args: string) => void
__TAURI__: {
path: {
__sep: string
__delimiter: string
}
convertFileSrc: (src: string, protocol: string) => string
}
__TAURI_IPC__: (message: any) => void
}
}
@@ -125,7 +130,14 @@ async function addPluginListener<T>(
*
* @since 1.0.0
*/
type InvokeArgs = Record<string, unknown>
type InvokeArgs = Record<string, unknown> | number[] | ArrayBuffer | Uint8Array
/**
* @since 2.0.0
*/
interface InvokeOptions {
headers: Headers | Record<string, string>
}
/**
* Sends a message to the backend.
@@ -137,11 +149,16 @@ type InvokeArgs = Record<string, unknown>
*
* @param cmd The command name.
* @param args The optional arguments to pass to the command.
* @param options The request options.
* @return A promise resolving or rejecting to the backend response.
*
* @since 1.0.0
*/
async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
async function invoke<T>(
cmd: string,
args: InvokeArgs = {},
options?: InvokeOptions
): Promise<T> {
return new Promise((resolve, reject) => {
const callback = transformCallback((e: T) => {
resolve(e)
@@ -156,7 +173,8 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
cmd,
callback,
error,
...args
payload: args,
options
})
})
}
@@ -164,7 +182,7 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
/**
* Convert a device file path to an URL that can be loaded by the webview.
* Note that `asset:` and `https://asset.localhost` must be added to [`tauri.security.csp`](https://tauri.app/v1/api/config/#securityconfig.csp) in `tauri.conf.json`.
* Example CSP value: `"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"` to use the asset protocol on image sources.
* Example CSP value: `"csp": "default-src 'self' ipc: https://ipc.localhost; img-src 'self' asset: https://asset.localhost"` to use the asset protocol on image sources.
*
* Additionally, `asset` must be added to [`tauri.allowlist.protocol`](https://tauri.app/v1/api/config/#allowlistconfig.protocol)
* in `tauri.conf.json` and its access scope must be defined on the `assetScope` array on the same `protocol` object.
@@ -192,13 +210,10 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
* @since 1.0.0
*/
function convertFileSrc(filePath: string, protocol = 'asset'): string {
const path = encodeURIComponent(filePath)
return navigator.userAgent.includes('Windows')
? `https://${protocol}.localhost/${path}`
: `${protocol}://localhost/${path}`
return window.__TAURI__.convertFileSrc(filePath, protocol)
}
export type { InvokeArgs }
export type { InvokeArgs, InvokeOptions }
export {
transformCallback,

View File

@@ -43,7 +43,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -5,7 +5,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::fs::read;
use tauri::{command, path::BaseDirectory, AppHandle, Manager, Runtime};
use tauri::{command, ipc::Response, path::BaseDirectory, AppHandle, Manager, Runtime};
#[command]
fn app_should_close(exit_code: i32) {
@@ -13,13 +13,13 @@ fn app_should_close(exit_code: i32) {
}
#[command]
async fn read_file<R: Runtime>(app: AppHandle<R>) -> Result<Vec<u8>, String> {
async fn read_file<R: Runtime>(app: AppHandle<R>) -> Result<Response, String> {
let path = app
.path()
.resolve(".tauri_3mb.json", BaseDirectory::Home)
.map_err(|e| e.to_string())?;
let contents = read(&path).map_err(|e| e.to_string())?;
Ok(contents)
Ok(Response::new(contents))
}
fn main() {

View File

@@ -43,7 +43,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -43,7 +43,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}

View File

@@ -47,6 +47,13 @@ fn migrate_config(config: &mut Value) -> Result<()> {
process_allowlist(tauri_config, &mut plugins, allowlist)?;
}
if let Some(security) = tauri_config
.get_mut("security")
.and_then(|c| c.as_object_mut())
{
process_security(security)?;
}
// cli
if let Some(cli) = tauri_config.remove("cli") {
process_cli(&mut plugins, cli)?;
@@ -64,6 +71,44 @@ fn migrate_config(config: &mut Value) -> Result<()> {
Ok(())
}
fn process_security(security: &mut Map<String, Value>) -> Result<()> {
// migrate CSP: add `ipc:` to `connect-src`
if let Some(csp_value) = security.remove("csp") {
let csp = if csp_value.is_null() {
csp_value
} else {
let mut csp: tauri_utils_v1::config::Csp = serde_json::from_value(csp_value)?;
match &mut csp {
tauri_utils_v1::config::Csp::Policy(csp) => {
if csp.contains("connect-src") {
*csp = csp.replace("connect-src", "connect-src ipc: https://ipc.localhost");
} else {
*csp = format!("{csp}; connect-src ipc: https://ipc.localhost");
}
}
tauri_utils_v1::config::Csp::DirectiveMap(csp) => {
if let Some(connect_src) = csp.get_mut("connect-src") {
if !connect_src.contains("ipc: https://ipc.localhost") {
connect_src.push("ipc: https://ipc.localhost");
}
} else {
csp.insert(
"connect-src".into(),
tauri_utils_v1::config::CspDirectiveSources::List(vec![
"ipc: https://ipc.localhost".to_string()
]),
);
}
}
}
serde_json::to_value(csp)?
};
security.insert("csp".into(), csp);
}
Ok(())
}
fn process_allowlist(
tauri_config: &mut Map<String, Value>,
plugins: &mut Map<String, Value>,
@@ -142,8 +187,19 @@ fn process_updater(
#[cfg(test)]
mod test {
fn migrate(original: &serde_json::Value) -> serde_json::Value {
let mut migrated = original.clone();
super::migrate_config(&mut migrated).expect("failed to migrate config");
if let Err(e) = serde_json::from_value::<tauri_utils::config::Config>(migrated.clone()) {
panic!("migrated config is not valid: {e}");
}
migrated
}
#[test]
fn migrate() {
fn migrate_full() {
let original = serde_json::json!({
"tauri": {
"bundle": {
@@ -199,16 +255,14 @@ mod test {
"http": {
"scope": ["http://localhost:3003/"]
}
},
"security": {
"csp": "default-src: 'self' tauri:"
}
}
});
let mut migrated = original.clone();
super::migrate_config(&mut migrated).expect("failed to migrate config");
if let Err(e) = serde_json::from_value::<tauri_utils::config::Config>(migrated.clone()) {
panic!("migrated config is not valid: {e}");
}
let migrated = migrate(&original);
// bundle > updater
assert_eq!(
@@ -268,5 +322,105 @@ mod test {
migrated["tauri"]["security"]["assetProtocol"]["scope"],
original["tauri"]["allowlist"]["protocol"]["assetScope"]
);
// security CSP
assert_eq!(
migrated["tauri"]["security"]["csp"],
format!(
"{}; connect-src ipc: https://ipc.localhost",
original["tauri"]["security"]["csp"].as_str().unwrap()
)
);
}
#[test]
fn migrate_csp_object() {
let original = serde_json::json!({
"tauri": {
"security": {
"csp": {
"default-src": ["self", "tauri:"]
}
}
}
});
let migrated = migrate(&original);
assert_eq!(
migrated["tauri"]["security"]["csp"]["default-src"],
original["tauri"]["security"]["csp"]["default-src"]
);
assert!(migrated["tauri"]["security"]["csp"]["connect-src"]
.as_array()
.expect("connect-src isn't an array")
.contains(&"ipc: https://ipc.localhost".into()));
}
#[test]
fn migrate_csp_existing_connect_src_string() {
let original = serde_json::json!({
"tauri": {
"security": {
"csp": {
"default-src": ["self", "tauri:"],
"connect-src": "self"
}
}
}
});
let migrated = migrate(&original);
assert_eq!(
migrated["tauri"]["security"]["csp"]["default-src"],
original["tauri"]["security"]["csp"]["default-src"]
);
assert_eq!(
migrated["tauri"]["security"]["csp"]["connect-src"]
.as_str()
.expect("connect-src isn't a string"),
format!(
"{} ipc: https://ipc.localhost",
original["tauri"]["security"]["csp"]["connect-src"]
.as_str()
.unwrap()
)
);
}
#[test]
fn migrate_csp_existing_connect_src_array() {
let original = serde_json::json!({
"tauri": {
"security": {
"csp": {
"default-src": ["self", "tauri:"],
"connect-src": ["self", "asset:"]
}
}
}
});
let migrated = migrate(&original);
assert_eq!(
migrated["tauri"]["security"]["csp"]["default-src"],
original["tauri"]["security"]["csp"]["default-src"]
);
let migrated_connect_src = migrated["tauri"]["security"]["csp"]["connect-src"]
.as_array()
.expect("connect-src isn't an array");
let original_connect_src = original["tauri"]["security"]["csp"]["connect-src"]
.as_array()
.unwrap();
assert!(
migrated_connect_src
.iter()
.zip(original_connect_src.iter())
.all(|(a, b)| a == b),
"connect-src migration failed"
);
}
}

View File

@@ -50,7 +50,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: https://ipc.localhost"
}
}
}
}