Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trunk 0.21.6 causes CORS related issues when used with tauri, which causes CSS attributes not being applied #941

Closed
hrzlgnm opened this issue Jan 20, 2025 · 14 comments · Fixed by #942

Comments

@hrzlgnm
Copy link

hrzlgnm commented Jan 20, 2025

What I could observe so far:

After updating trunk to 0.21.6, all styles used in were somehow not applied. I can observe the following errors in developer tools, when enabled in debug mode:

Refused to load http://localhost:1420/styles-8594f9560b2c9a3f.css because it does not appear in the style-src directive of the Content Security Policy.

Steps to reproduce:

cargo install --locked [email protected] --no-default-features --features=rustls
cargo install --locked tauri-cli
cargo install create-tauri-app --locked
cargo create-tauri-app
cargo create-tauri-app -m cargo -t leptos -y my-app
cd my-app
cargo --locked  tauri dev

Expect something similar to the following screenshot to be shown:

Image

Actual result:

Image

The issue is reproducible on linux and on windows.

In the meantime I went back to 0.21.5 which works fine, and doesn't cause those issues.

@tikitko
Copy link

tikitko commented Jan 20, 2025

It could be related to #934.
My final index.html contains the placeholder {{__TRUNK_NONCE__}} instead of a random number. Could you check?
<link nonce="{{__TRUNK NONCE__}}" as=fetch crossorigin href=/blog-ui-c181e37c8d294b36_bg.wasm integrity=sha384-UeNBIll/9aCxGJJNeoPefx+qzfF9XsMlUw5E/8cj/+4d15S6CwhDaaNWUBCBdgjU rel=preload type=application/wasm>

@hrzlgnm
Copy link
Author

hrzlgnm commented Jan 20, 2025

I had a look, and the final index.html in my-app/dist/
contains the placeholder at multiple locations:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Tauri + Leptos App</title>
    <link rel="stylesheet" href="/styles-8594f9560b2c9a3f.css" integrity="sha384&#x2D;LDfuJQTxLxRuEDdPgbqdtWGmnRQovfvNZuMp&#x2F;F1Ds&#x2F;8UBwItKMXhlpp3ghq7IX7O"/>
    
    
<script type="module" nonce="{{__TRUNK NONCE__}}">
import init, * as bindings from '/my-app-ui-196185f181692e60.js';
const wasm = await init({ module_or_path: '/my-app-ui-196185f181692e60_bg.wasm' });


window.wasmBindings = bindings;


dispatchEvent(new CustomEvent("TrunkApplicationStarted", {detail: {wasm}}));

</script>
  <link rel="modulepreload" nonce="{{__TRUNK NONCE__}}" href="/my-app-ui-196185f181692e60.js" crossorigin="anonymous" integrity="sha384-rxudMjVa+yAnfjnpHmwNWAlp/jSCTM9XEaMgRL2VrWHuW2R38mWNrRz3WVUYfL+F"><link rel="preload" nonce="{{__TRUNK NONCE__}}" href="/my-app-ui-196185f181692e60_bg.wasm" crossorigin="anonymous" integrity="sha384-ppqX7J+rRF3sVGuknVh5JnGHrJA6cVi91GlzbzXC3JKgfKk6N1rlYTQYEMkGBuK2" as="fetch" type="application/wasm"></head>
  <body><script nonce="{{__TRUNK NONCE__}}">"use strict";

(function () {

    const address = '{{__TRUNK_ADDRESS__}}';
    const base = '{{__TRUNK_WS_BASE__}}';
    let protocol = '';
    protocol =
        protocol
            ? protocol
            : window.location.protocol === 'https:'
                ? 'wss'
                : 'ws';
    const url = protocol + '://' + address + base + '.well-known/trunk/ws';

    class Overlay {
        constructor() {
            // create an overlay
            this._overlay = document.createElement("div");
            const style = this._overlay.style;
            style.height = "100vh";
            style.width = "100vw";
            style.position = "fixed";
            style.top = "0";
            style.left = "0";
            style.backgroundColor = "rgba(222, 222, 222, 0.5)";
            style.fontFamily = "sans-serif";
            // not sure that's the right approach
            style.zIndex = "1000000";
            style.backdropFilter = "blur(1rem)";

            const container = document.createElement("div");
            // center it
            container.style.position = "absolute";
            container.style.top = "30%";
            container.style.left = "15%";
            container.style.maxWidth = "85%";

            this._title = document.createElement("div");
            this._title.innerText = "Build failure";
            this._title.style.paddingBottom = "2rem";
            this._title.style.fontSize = "2.5rem";

            this._message = document.createElement("div");
            this._message.style.whiteSpace = "pre-wrap";

            const icon= document.createElement("div");
            icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#dc3545" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg>';
            this._title.prepend(icon);

            container.append(this._title, this._message);
            this._overlay.append(container);

            this._inject();
            window.setInterval(() => {
                this._inject();
            }, 250);
        }

        set reason(reason) {
            this._message.textContent = reason;
        }

        _inject() {
            if (!this._overlay.isConnected) {
                // prepend it
                document.body?.prepend(this._overlay);
            }
        }

    }

    class Client {
        constructor(url) {
            this.url = url;
            this.poll_interval = 5000;
            this._overlay = null;
        }

        start() {
            const ws = new WebSocket(this.url);
            ws.onmessage = (ev) => {
                const msg = JSON.parse(ev.data);
                switch (msg.type) {
                    case "reload":
                        this.reload();
                        break;
                    case "buildFailure":
                        this.buildFailure(msg.data)
                        break;
                }
            };
            ws.onclose = () => this.onclose();
        }

        onclose() {
            window.setTimeout(
                () => {
                    // when we successfully reconnect, we'll force a
                    // reload (since we presumably lost connection to
                    // trunk due to it being killed, so it will have
                    // rebuilt on restart)
                    const ws = new WebSocket(this.url);
                    ws.onopen = () => window.location.reload();
                    ws.onclose = () => this.onclose();
                },
                this.poll_interval);
        }

        reload() {
            window.location.reload();
        }

        buildFailure({reason}) {
            // also log the console
            console.error("Build failed:", reason);

            console.debug("Overlay", this._overlay);

            if (!this._overlay) {
                this._overlay = new Overlay();
            }
            this._overlay.reason = reason;
        }
    }

    new Client(url).start();

})()
</script></body>
</html>

When build with trunk 0.21.5, the index.html contains the following:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Tauri + Leptos App</title>
    <link rel="stylesheet" href="/styles-8594f9560b2c9a3f.css" integrity="sha384&#x2D;LDfuJQTxLxRuEDdPgbqdtWGmnRQovfvNZuMp&#x2F;F1Ds&#x2F;8UBwItKMXhlpp3ghq7IX7O"/>
    
    
<script type="module" nonce="jlMPf6PCM5FY8bkSfqmSnw==">
import init, * as bindings from '/my-app-ui-196185f181692e60.js';
const wasm = await init({ module_or_path: '/my-app-ui-196185f181692e60_bg.wasm' });


window.wasmBindings = bindings;


dispatchEvent(new CustomEvent("TrunkApplicationStarted", {detail: {wasm}}));

</script>
  <link rel="modulepreload" href="/my-app-ui-196185f181692e60.js" crossorigin=anonymous integrity="sha384-rxudMjVa+yAnfjnpHmwNWAlp/jSCTM9XEaMgRL2VrWHuW2R38mWNrRz3WVUYfL+F"><link rel="preload" href="/my-app-ui-196185f181692e60_bg.wasm" crossorigin=anonymous integrity="sha384-ppqX7J+rRF3sVGuknVh5JnGHrJA6cVi91GlzbzXC3JKgfKk6N1rlYTQYEMkGBuK2" as="fetch" type="application/wasm"></head>
  <body><script>"use strict";

(function () {

    const address = '{{__TRUNK_ADDRESS__}}';
    const base = '{{__TRUNK_WS_BASE__}}';
    let protocol = '';
    protocol =
        protocol
            ? protocol
            : window.location.protocol === 'https:'
                ? 'wss'
                : 'ws';
    const url = protocol + '://' + address + base + '.well-known/trunk/ws';

    class Overlay {
        constructor() {
            // create an overlay
            this._overlay = document.createElement("div");
            const style = this._overlay.style;
            style.height = "100vh";
            style.width = "100vw";
            style.position = "fixed";
            style.top = "0";
            style.left = "0";
            style.backgroundColor = "rgba(222, 222, 222, 0.5)";
            style.fontFamily = "sans-serif";
            // not sure that's the right approach
            style.zIndex = "1000000";
            style.backdropFilter = "blur(1rem)";

            const container = document.createElement("div");
            // center it
            container.style.position = "absolute";
            container.style.top = "30%";
            container.style.left = "15%";
            container.style.maxWidth = "85%";

            this._title = document.createElement("div");
            this._title.innerText = "Build failure";
            this._title.style.paddingBottom = "2rem";
            this._title.style.fontSize = "2.5rem";

            this._message = document.createElement("div");
            this._message.style.whiteSpace = "pre-wrap";

            const icon= document.createElement("div");
            icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#dc3545" viewBox="0 0 16 16"><path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/></svg>';
            this._title.prepend(icon);

            container.append(this._title, this._message);
            this._overlay.append(container);

            this._inject();
            window.setInterval(() => {
                this._inject();
            }, 250);
        }

        set reason(reason) {
            this._message.textContent = reason;
        }

        _inject() {
            if (!this._overlay.isConnected) {
                // prepend it
                document.body?.prepend(this._overlay);
            }
        }

    }

    class Client {
        constructor(url) {
            this.url = url;
            this.poll_interval = 5000;
            this._overlay = null;
        }

        start() {
            const ws = new WebSocket(this.url);
            ws.onmessage = (ev) => {
                const msg = JSON.parse(ev.data);
                switch (msg.type) {
                    case "reload":
                        this.reload();
                        break;
                    case "buildFailure":
                        this.buildFailure(msg.data)
                        break;
                }
            };
            ws.onclose = this.onclose;
        }

        onclose() {
            window.setTimeout(
                () => {
                    // when we successfully reconnect, we'll force a
                    // reload (since we presumably lost connection to
                    // trunk due to it being killed, so it will have
                    // rebuilt on restart)
                    const ws = new WebSocket(this.url);
                    ws.onopen = () => window.location.reload();
                    ws.onclose = this.onclose;
                },
                this.poll_interval);
        }

        reload() {
            window.location.reload();
        }

        buildFailure({reason}) {
            // also log the console
            console.error("Build failed:", reason);

            console.debug("Overlay", this._overlay);

            if (!this._overlay) {
                this._overlay = new Overlay();
            }
            this._overlay.reason = reason;
        }
    }

    new Client(url).start();

})()
</script></body>
</html>

@hrzlgnm
Copy link
Author

hrzlgnm commented Jan 21, 2025

According to changes in #934 there is a config option to disable nonce generation, but unfortunately it didn't make it into the documentation, and there is no example entry in the top level Trunk.toml. I guess when building an app that later loads the HTML from the disc or other means of embedding, the nonce generation makes no sense, as there is no trunk process running in that case, to replace those placeholders. I'm not sure whether it makes sense to have a nonce generation enabled when developing locally, as the html page might not be loaded through trunk in that case either. I'm not sure how the tauri app loads the html page in that case. I'll investigate further tomorrow. This might have some implications for tauri apps using leptos. As those would need to update their Trunk.toml to include disabling nonce generation.

@bicarlsen
Copy link
Contributor

I'm running into the issue as well.

@beliefsky
Copy link

[build]
create_nonce = false

@ctron
Copy link
Collaborator

ctron commented Jan 21, 2025

Thanks for reporting and analyzing this. Indeed, that seems to be introduced by #934. I think the problem is:

let create_nonce = build.create_nonce.then_some(build.nonce_placeholder);

I think the solution would be to make this Option<String>, and then don't set a default value (or None). And then use a random value in case create_nonce is true. This would:

  • Generate a random nonce as before
  • Allow disabling the nonce generation (enabled by default)
  • Allow a user to provide an override value

@ctron
Copy link
Collaborator

ctron commented Jan 21, 2025

Ok, just peeking at bit on how to use CSP nonces …

The nonce must be unique for each HTTP response

https://content-security-policy.com/nonce/

So, indeed it should be a placeholder. However, I think it makes sense to set the create_nonce default value to false.

ctron added a commit to ctron/trunk that referenced this issue Jan 21, 2025
As the nonce should be unique per request, it doesn't make sense to
enable this by default, as that requires additional work on the serving
side.

On the other side, having a (static) random value isn't correct either.

So we keep the current logic, but disable nonce generation by default,
making it opt-in.

Closes trunk-rs#941
ctron added a commit to ctron/trunk that referenced this issue Jan 21, 2025
As the nonce should be unique per request, it doesn't make sense to
enable this by default, as that requires additional work on the serving
side.

On the other side, having a (static) random value isn't correct either.

So we keep the current logic, but disable nonce generation by default,
making it opt-in.

Closes trunk-rs#941
@ctron
Copy link
Collaborator

ctron commented Jan 21, 2025

It would be great if any of you could try out PR #942 to verify the fix.

You can install it using:

cargo install --git https://github.com/ctron/trunk trunk --branch hotfix/fix_941_1

@hrzlgnm
Copy link
Author

hrzlgnm commented Jan 21, 2025

It would be great if any of you could try out PR #942 to verify the fix.

You can install it using:

cargo install --git https://github.com/ctron/trunk trunk --branch hotfix/fix_941_1

Tried it out and indeed, that fixes the issue.

@bicarlsen
Copy link
Contributor

Patch is working for me, too. Thanks!

ctron added a commit to ctron/trunk that referenced this issue Jan 21, 2025
As the nonce should be unique per request, it doesn't make sense to
enable this by default, as that requires additional work on the serving
side.

On the other side, having a (static) random value isn't correct either.

So we keep the current logic, but disable nonce generation by default,
making it opt-in.

Closes trunk-rs#941
github-merge-queue bot pushed a commit that referenced this issue Jan 21, 2025
As the nonce should be unique per request, it doesn't make sense to
enable this by default, as that requires additional work on the serving
side.

On the other side, having a (static) random value isn't correct either.

So we keep the current logic, but disable nonce generation by default,
making it opt-in.

Closes #941
@ctron
Copy link
Collaborator

ctron commented Jan 21, 2025

The build for the release 0.21.7 is running now: https://github.com/trunk-rs/trunk/actions/runs/12889480915

@ensc
Copy link
Contributor

ensc commented Jan 21, 2025

I guess when building an app that later loads the HTML from the disc or other means of embedding, the nonce generation makes no sense,

#934 was written for this purpose: when application defines a CSP, all <link> and <script> elements must have a nonce. Examining all possible locations is a non-trivial task so that a placeholder ({{__TRUNK NONCE__}}) is added which can be replaced trivially (str.replace(...)) with an actual, random nonce.

The existence of the nonce itself should not matter (e.g. older trunk versions added already a static, difficulty to replace nonce in one element); only drawback is a slightly increased size of index.html.

Problem in this issue is probably the CSP generation (which was added and enabled by default also in #934). I do not know what tauri is doing; perhaps it adds manually some style or script or so to index.html which fails then to load due to the missing nonce.

as there is no trunk process running in that case,

I does not matter whether it is an own application or trunk. Replacement operation has been implemented in trunk and it is trivial to do this in custom applications.

I'm not sure whether it makes sense to have a nonce generation enabled when developing locally, as the html page might not be loaded through trunk in that case either.

imo it is a good idea to make the developing environment similar to the production one and detect CSP related problems early.

As those would need to update their Trunk.toml to include disabling nonce generation.

[serve]
disable_csp: true

might be an alternative way

@ctron
Copy link
Collaborator

ctron commented Jan 21, 2025

I think that the previous behavior, being able to insert that placeholder (or any other value) should still be possible. I agree that it would be great to have some default, random, generated nonce for trunk serve.

@hrzlgnm
Copy link
Author

hrzlgnm commented Jan 21, 2025

I'm not agains having those options, but in an application where there is no external access to any websites at all, only local html and and some wasm and some js glue code to talk to a backend that only does local networking CSP is totally out of the window for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants