feat: add support for adding CSP nonces (#1348)

This commit is contained in:
Greg Johnston 2023-07-14 16:37:18 -04:00 committed by GitHub
parent 77580401da
commit 8e68699435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 336 additions and 96 deletions

View File

@ -19,3 +19,6 @@ serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"
tracing = "0.1.37"
[features]
nonce = ["leptos/nonce"]

View File

@ -733,6 +733,8 @@ fn provide_contexts(
provide_context(cx, res_options);
provide_context(cx, req.clone());
provide_server_redirect(cx, move |path| redirect(cx, path));
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce(cx);
}
fn leptos_corrected_path(req: &HttpRequest) -> String {
@ -797,8 +799,11 @@ async fn build_stream_response(
// wait for any blocking resources to load before pulling metadata
let first_app_chunk = stream.next().await.unwrap_or_default();
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })

View File

@ -19,6 +19,9 @@ leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
tokio-util = {version = "0.7.7", features = ["rt"] }
tokio-util = { version = "0.7.7", features = ["rt"] }
tracing = "0.1.37"
once_cell = "1.17"
[features]
nonce = ["leptos/nonce"]

View File

@ -693,8 +693,11 @@ async fn forward_stream(
let mut shell = Box::pin(bundle);
let first_app_chunk = shell.next().await.unwrap_or_default();
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
_ = tx.send(head).await;
_ = tx.send(first_app_chunk).await;
@ -828,6 +831,8 @@ fn provide_contexts(
provide_context(cx, extractor);
provide_context(cx, default_res_options);
provide_server_redirect(cx, move |path| redirect(cx, path));
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce(cx);
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries

View File

@ -1,18 +1,18 @@
use futures::{Stream, StreamExt};
use leptos::{use_context, RuntimeId, ScopeId};
use leptos::{nonce::use_nonce, use_context, RuntimeId, Scope, ScopeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
extern crate tracing;
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn autoreload(options: &LeptosOptions) -> String {
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
<script crossorigin=""{nonce_str}>(function () {{
{}
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
@ -42,6 +42,8 @@ fn autoreload(options: &LeptosOptions) -> String {
false => "".to_string(),
}
}
#[deprecated = "Use html_parts_separated."]
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn html_parts(
options: &LeptosOptions,
@ -58,7 +60,7 @@ pub fn html_parts(
wasm_output_name.push_str("_bg");
}
let leptos_autoreload = autoreload(options);
let leptos_autoreload = autoreload("".into(), options);
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
@ -80,11 +82,17 @@ pub fn html_parts(
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn html_parts_separated(
cx: Scope,
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let nonce = use_nonce(cx);
let nonce = nonce
.as_ref()
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
@ -94,7 +102,7 @@ pub fn html_parts_separated(
wasm_output_name.push_str("_bg");
}
let leptos_autoreload = autoreload(options);
let leptos_autoreload = autoreload(&nonce, options);
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
@ -109,9 +117,9 @@ pub fn html_parts_separated(
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
{head}
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
<script type="module"{nonce}>import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
@ -133,8 +141,11 @@ pub async fn build_async_response(
}
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
// in async, we load the meta content *now*, after the suspenses have resolved
let meta = use_context::<MetaContext>(cx);

View File

@ -19,3 +19,6 @@ leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
[features]
nonce = ["leptos/nonce"]

View File

@ -655,8 +655,11 @@ async fn forward_stream(
mut tx: Sender<String>,
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
@ -793,6 +796,8 @@ fn provide_contexts(
provide_context(cx, req_parts);
provide_context(cx, default_res_options);
provide_server_redirect(cx, move |path| redirect(cx, path));
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce(cx);
}
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries

View File

@ -58,6 +58,7 @@ serde-lite = ["leptos_reactive/serde-lite"]
miniserde = ["leptos_reactive/miniserde"]
rkyv = ["leptos_reactive/rkyv"]
tracing = ["leptos_macro/tracing"]
nonce = ["leptos_dom/nonce"]
[package.metadata.cargo-all-features]
denylist = [

View File

@ -164,9 +164,10 @@ pub use leptos_dom::{
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
html, log, math, mount_to, mount_to_body, nonce, svg, warn, window,
Attribute, Class, CollectView, Errors, Fragment, HtmlElement,
IntoAttribute, IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef,
Property, View,
};
/// Types to make it easier to handle errors in your application.

View File

@ -9,10 +9,12 @@ description = "DOM operations for the Leptos web framework."
[dependencies]
async-recursion = "1"
base64 = { version = "0.21", optional = true }
cfg-if = "1"
drain_filter_polyfill = "0.1"
educe = "0.4"
futures = "0.3"
getrandom = { version = "0.2", optional = true }
html-escape = "0.2"
indexmap = "2"
itertools = "0.10"
@ -22,6 +24,7 @@ server_fn = { workspace = true }
once_cell = "1"
pad-adapter = "0.1"
paste = "1"
rand = { version = "0.8", optional = true }
rustc-hash = "1.1.0"
serde_json = "1"
smallvec = "1"
@ -29,6 +32,9 @@ tracing = "0.1"
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.31"
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
leptos = { path = "../leptos" }
@ -161,6 +167,7 @@ default = []
web = ["leptos_reactive/csr"]
ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
[package.metadata.cargo-all-features]
denylist = ["nightly"]

View File

@ -18,6 +18,8 @@ mod logging;
mod macro_helpers;
pub mod math;
mod node_ref;
/// Utilities for exporting nonces to be used for a Content Security Policy.
pub mod nonce;
pub mod ssr;
pub mod ssr_in_order;
pub mod svg;

158
leptos_dom/src/nonce.rs Normal file
View File

@ -0,0 +1,158 @@
use crate::{Attribute, IntoAttribute};
use leptos_reactive::{use_context, Scope};
use std::{fmt::Display, ops::Deref};
/// A nonce a cryptographic nonce ("number used once") which can be
/// used by Content Security Policy to determine whether or not a given
/// resource will be allowed to load.
///
/// When the `nonce` feature is enabled on one of the server integrations,
/// a nonce is generated during server rendering and added to all inline
/// scripts used for HTML streaming and resource loading.
///
/// The nonce being used during the current server response can be
/// accessed using [`use_nonce`](use_nonce).
///
/// ```rust,ignore
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce(cx)
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Nonce(pub(crate) String);
impl Deref for Nonce {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Nonce {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl IntoAttribute for Nonce {
fn into_attribute(self, _cx: Scope) -> Attribute {
Attribute::String(self.0.into())
}
fn into_attribute_boxed(self: Box<Self>, _cx: Scope) -> Attribute {
Attribute::String(self.0.into())
}
}
impl IntoAttribute for Option<Nonce> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.0.into()))
}
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.0.into()))
}
}
/// Accesses the nonce that has been generated during the current
/// server response. This can be added to inline `<script>` and
/// `<style>` tags for compatibility with a Content Security Policy.
///
/// ```rust,ignore
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce(cx)
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
pub fn use_nonce(cx: Scope) -> Option<Nonce> {
use_context::<Nonce>(cx)
}
#[cfg(all(feature = "ssr", feature = "nonce"))]
pub use generate::*;
#[cfg(all(feature = "ssr", feature = "nonce"))]
mod generate {
use super::Nonce;
use base64::{
alphabet,
engine::{self, general_purpose},
Engine,
};
use leptos_reactive::{provide_context, Scope};
use rand::{thread_rng, RngCore};
const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
&alphabet::URL_SAFE,
general_purpose::NO_PAD,
);
impl Nonce {
/// Generates a new nonce from 16 bytes (128 bits) of random data.
pub fn new() -> Self {
let mut thread_rng = thread_rng();
let mut bytes = [0; 16];
thread_rng.fill_bytes(&mut bytes);
Nonce(NONCE_ENGINE.encode(bytes))
}
}
impl Default for Nonce {
fn default() -> Self {
Self::new()
}
}
/// Generates a nonce and provides it during server rendering.
pub fn provide_nonce(cx: Scope) {
provide_context(cx, Nonce::new())
}
}

View File

@ -212,6 +212,9 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
}
});
let cx = Scope { runtime, id: scope };
let nonce_str = crate::nonce::use_nonce(cx)
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let mut blocking_fragments = FuturesUnordered::new();
let fragments = FuturesUnordered::new();
@ -230,91 +233,110 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
let stream = futures::stream::once(
// HTML for the view function and script to store resources
async move {
let resolvers = format!(
"<script>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
{
let nonce_str = nonce_str.clone();
async move {
let resolvers = format!(
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
if replace_blocks {
let mut blocks = Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
if replace_blocks {
let mut blocks =
Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close =
format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) =
shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell =
format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments = fragments_to_chunks(
nonce_str.clone(),
blocking_fragments,
);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close = format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) =
shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell = format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments =
fragments_to_chunks(blocking_fragments);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
}
},
)
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
.chain(ooo_body_stream_recurse(
cx,
nonce_str,
fragments,
serializers,
));
(stream, runtime, scope)
}
fn ooo_body_stream_recurse(
cx: Scope,
nonce_str: String,
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> Pin<Box<dyn Stream<Item = String>>> {
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
let fragments = fragments_to_chunks(fragments);
let fragments = fragments_to_chunks(nonce_str.clone(), fragments);
// stream data for each Resource as it resolves
let resources = render_serializers(serializers);
let resources = render_serializers(nonce_str.clone(), serializers);
Box::pin(
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
fragments.chain(resources).chain(
futures::stream::once(async move {
let pending = cx.pending_fragments();
if !pending.is_empty() {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
futures::stream::once({
async move {
let pending = cx.pending_fragments();
if !pending.is_empty() {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<
Box<dyn Future<Output = (String, String)>>,
>);
}
Box::pin(ooo_body_stream_recurse(
cx,
nonce_str.clone(),
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} else {
Box::pin(futures::stream::once(async move {
Default::default()
}))
}
Box::pin(ooo_body_stream_recurse(
cx,
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} else {
Box::pin(futures::stream::once(async move {
Default::default()
}))
}
})
.flatten(),
@ -327,13 +349,14 @@ fn ooo_body_stream_recurse(
instrument(level = "trace", skip_all,)
)]
fn fragments_to_chunks(
nonce_str: String,
fragments: impl Stream<Item = (String, String)>,
) -> impl Stream<Item = String> {
fragments.map(|(fragment_id, html)| {
fragments.map(move |(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
<script{nonce_str}>
var id = "{fragment_id}";
var open = undefined;
var close = undefined;
@ -679,13 +702,14 @@ pub(crate) fn to_kebab_case(name: &str) -> String {
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_serializers(
nonce_str: String,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> impl Stream<Item = String> {
serializers.map(|(id, json)| {
serializers.map(move |(id, json)| {
let id = serde_json::to_string(&id).unwrap();
let json = json.replace('<', "\\u003c");
format!(
r#"<script>
r#"<script{nonce_str}>
var val = {json:?};
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)

View File

@ -124,24 +124,33 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
handle_chunks(cx, tx, remaining_chunks).await;
});
let stream = futures::stream::once(async move {
let prefix = prefix_rx.await.expect("to receive prefix");
format!(
r#"
let nonce = crate::nonce::use_nonce(cx);
let nonce_str = nonce
.as_ref()
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let stream = futures::stream::once({
let nonce_str = nonce_str.clone();
async move {
let prefix = prefix_rx.await.expect("to receive prefix");
format!(
r#"
{prefix}
<script>
<script{nonce_str}>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
)
}
})
.chain(rx)
.chain(
futures::stream::once(async move {
let serializers = cx.serialization_resolvers();
render_serializers(serializers)
render_serializers(nonce_str, serializers)
})
.flatten(),
);

View File

@ -1,5 +1,5 @@
use crate::use_head;
use leptos::*;
use leptos::{nonce::use_nonce, *};
use std::borrow::Cow;
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
@ -109,6 +109,7 @@ pub fn Link(
.attr("title", title)
.attr("type", type_)
.attr("blocking", blocking)
.attr("nonce", use_nonce(cx))
}
});

View File

@ -1,5 +1,5 @@
use crate::use_head;
use leptos::*;
use leptos::{nonce::use_nonce, *};
use std::borrow::Cow;
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
@ -85,6 +85,7 @@ pub fn Script(
.attr("src", src)
.attr("type", type_)
.attr("blocking", blocking)
.attr("nonce", use_nonce(cx))
}
});
let builder_el = if let Some(children) = children {

View File

@ -1,5 +1,5 @@
use crate::use_head;
use leptos::*;
use leptos::{nonce::use_nonce, *};
use std::borrow::Cow;
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
@ -57,6 +57,7 @@ pub fn Style(
.attr("nonce", nonce)
.attr("title", title)
.attr("blocking", blocking)
.attr("nonce", use_nonce(cx))
}
});
let builder_el = if let Some(children) = children {