feat(core): inject invoke key on <script type="module"> (#2120)

This commit is contained in:
Lucas Fernandes Nogueira 2021-06-29 20:40:41 -03:00 committed by GitHub
parent 94a5848afb
commit f03eea9c9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 114 additions and 59 deletions

View File

@ -0,0 +1,7 @@
---
"tauri": patch
"tauri-codegen": patch
"tauri-utils": patch
---
Inject invoke key on `script` tags with `type="module"`.

View File

@ -21,3 +21,4 @@ tauri-utils = { version = "1.0.0-beta.1", path = "../tauri-utils", features = [
thiserror = "1" thiserror = "1"
walkdir = "2" walkdir = "2"
zstd = "0.9" zstd = "0.9"
kuchiki = "0.8"

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use kuchiki::traits::*;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use std::{ use std::{
@ -10,7 +11,10 @@ use std::{
fs::File, fs::File,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use tauri_utils::{assets::AssetKey, html::inject_csp}; use tauri_utils::{
assets::AssetKey,
html::{inject_csp, inject_invoke_key_token},
};
use thiserror::Error; use thiserror::Error;
use walkdir::WalkDir; use walkdir::WalkDir;
@ -170,44 +174,46 @@ impl EmbeddedAssets {
path: path.to_owned(), path: path.to_owned(),
error, error,
})?; })?;
if let Some(csp) = &options.csp { if path.extension() == Some(OsStr::new("html")) {
if path.extension() == Some(OsStr::new("html")) { let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&input).into_owned());
input = inject_csp(String::from_utf8_lossy(&input).into_owned(), csp) if let Some(csp) = &options.csp {
.as_bytes() inject_csp(&mut document, csp);
.to_vec(); }
inject_invoke_key_token(&mut document);
input = document.to_string().as_bytes().to_vec();
} else {
let is_javascript = ["js", "cjs", "mjs"]
.iter()
.any(|e| path.extension() == Some(OsStr::new(e)));
if is_javascript {
let js = String::from_utf8_lossy(&input).into_owned();
input = if [
"import{", "import*", "import ", "export{", "export*", "export ",
]
.iter()
.any(|t| js.contains(t))
{
format!(
r#"
const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
{}
"#,
js
)
.as_bytes()
.to_vec()
} else {
format!(
r#"(function () {{
const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
{}
}})()"#,
js
)
.as_bytes()
.to_vec()
};
} }
}
let is_javascript = ["js", "cjs", "mjs"]
.iter()
.any(|e| path.extension() == Some(OsStr::new(e)));
if is_javascript {
let js = String::from_utf8_lossy(&input).into_owned();
input = if [
"import{", "import*", "import ", "export{", "export*", "export ",
]
.iter()
.any(|t| js.contains(t))
{
format!(
r#"
const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
{}
"#,
js
)
.as_bytes()
.to_vec()
} else {
format!(
r#"(function () {{
const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
{}
}})()"#,
js
)
.as_bytes()
.to_vec()
};
} }
// we must canonicalize the base of our paths to allow long paths on windows // we must canonicalize the base of our paths to allow long paths on windows

View File

@ -2,17 +2,55 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use html5ever::{ use html5ever::{interface::QualName, namespace_url, ns, LocalName};
interface::QualName, use kuchiki::{Attribute, ExpandedName, NodeRef};
namespace_url, ns,
tendril::{fmt::UTF8, NonAtomic, Tendril}, /// Injects the invoke key token to each script on the document.
LocalName, ///
}; /// The invoke key token is replaced at runtime with the actual invoke key value.
use kuchiki::{traits::*, Attribute, ExpandedName, NodeRef}; pub fn inject_invoke_key_token(document: &mut NodeRef) {
let mut targets = vec![];
if let Ok(scripts) = document.select("script") {
for target in scripts {
targets.push(target);
}
for target in targets {
let node = target.as_node();
let element = node.as_element().unwrap();
let attrs = element.attributes.borrow();
// if the script is external (has `src`) or its type is not "module", we won't inject the token
if attrs.get("src").is_some() || attrs.get("type") != Some("module") {
continue;
}
let replacement_node = NodeRef::new_element(
QualName::new(None, ns!(html), "script".into()),
element
.attributes
.borrow()
.clone()
.map
.into_iter()
.collect::<Vec<_>>(),
);
let script = node.text_contents();
replacement_node.append(NodeRef::new_text(format!(
r#"
const __TAURI_INVOKE_KEY__ = __TAURI__INVOKE_KEY_TOKEN__;
{}
"#,
script
)));
node.insert_after(replacement_node);
node.detach();
}
}
}
/// Injects a content security policy to the HTML. /// Injects a content security policy to the HTML.
pub fn inject_csp<H: Into<Tendril<UTF8, NonAtomic>>>(html: H, csp: &str) -> String { pub fn inject_csp(document: &mut NodeRef, csp: &str) {
let document = kuchiki::parse_html().one(html);
if let Ok(ref head) = document.select_first("head") { if let Ok(ref head) = document.select_first("head") {
head.as_node().append(create_csp_meta_tag(csp)); head.as_node().append(create_csp_meta_tag(csp));
} else { } else {
@ -23,7 +61,6 @@ pub fn inject_csp<H: Into<Tendril<UTF8, NonAtomic>>>(html: H, csp: &str) -> Stri
head.append(create_csp_meta_tag(csp)); head.append(create_csp_meta_tag(csp));
document.prepend(head); document.prepend(head);
} }
document.to_string()
} }
fn create_csp_meta_tag(csp: &str) -> NodeRef { fn create_csp_meta_tag(csp: &str) -> NodeRef {
@ -50,6 +87,7 @@ fn create_csp_meta_tag(csp: &str) -> NodeRef {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use kuchiki::traits::*;
#[test] #[test]
fn csp() { fn csp() {
let htmls = vec![ let htmls = vec![
@ -57,10 +95,11 @@ mod tests {
"<html></html>".to_string(), "<html></html>".to_string(),
]; ];
for html in htmls { for html in htmls {
let mut document = kuchiki::parse_html().one(html);
let csp = "default-src 'self'; img-src https://*; child-src 'none';"; let csp = "default-src 'self'; img-src https://*; child-src 'none';";
let new = super::inject_csp(html, csp); super::inject_csp(&mut document, csp);
assert_eq!( assert_eq!(
new, document.to_string(),
format!( format!(
r#"<html><head><meta content="{}" http-equiv="Content-Security-Policy"></head><body></body></html>"#, r#"<html><head><meta content="{}" http-equiv="Content-Security-Policy"></head><body></body></html>"#,
csp csp

View File

@ -459,6 +459,7 @@ impl<P: Params> WindowManager<P> {
}; };
let is_javascript = let is_javascript =
path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs"); path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
let is_html = path.ends_with(".html");
let asset_response = assets let asset_response = assets
.get(&path) .get(&path)
@ -471,16 +472,17 @@ impl<P: Params> WindowManager<P> {
.map(Cow::into_owned); .map(Cow::into_owned);
match asset_response { match asset_response {
Ok(asset) => { Ok(asset) => {
if is_javascript { if is_javascript || is_html {
let js = String::from_utf8_lossy(&asset).into_owned(); let contents = String::from_utf8_lossy(&asset).into_owned();
Ok( Ok(
js.replacen( contents
"__TAURI__INVOKE_KEY_TOKEN__", .replacen(
&manager.generate_invoke_key().to_string(), "__TAURI__INVOKE_KEY_TOKEN__",
1, &manager.generate_invoke_key().to_string(),
) 1,
.as_bytes() )
.to_vec(), .as_bytes()
.to_vec(),
) )
} else { } else {
Ok(asset) Ok(asset)