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

LiveReload doesn't work when TLS is enabled #2859

Closed
isaacs opened this issue Apr 19, 2022 · 12 comments
Closed

LiveReload doesn't work when TLS is enabled #2859

isaacs opened this issue Apr 19, 2022 · 12 comments

Comments

@isaacs
Copy link

isaacs commented Apr 19, 2022

What version of Remix are you using?

1.3.5

Steps to Reproduce

  1. Create a server with TLS keys

  2. Attach remix route handler to it

  3. Load site

  4. Error appears in console:

    WebSocket connection to 'wss://moxy.cow-augmented.ts.net:8002/socket' failed: An SSL error has occurred and a secure connection to the server cannot be made.
    
  5. Change files, observe that they are not reloaded live. (Server rebuilds, but the ws push doesn't work.)

Expected Behavior

Expect that WebSocket.Server would be able to listen over https somehow.

Actual Behavior

WebSocket connection to 'wss://moxy.cow-augmented.ts.net:8002/socket' failed: An SSL error has occurred and a secure connection to the server cannot be made.

Tracked it down to this bit in ./node_modules/@remix-run/dev/cli/commands.js

  let wss = new WebSocket__default["default"].Server({
    port: config$1.devServerPort
  });

It appears that the only way for a ws server to speak tls is to attach to an existing server.

Something like this:

  const server = config.devServerCert && config.devServerKey ? https.createServer({
    cert: config.devServerCert,
    key: config.devServerKey,
  }) : http.createServer()
  server.listen(config.devServerPort)
  let wss = new WebSocket.Server({ server });
@mstaicu
Copy link

mstaicu commented Apr 20, 2022

@isaacs this might help. I'm running a different setup than yours, but I still serve the client via HTTPS and the live reload server has to connect via wss due to https being used as the protocol

Heres the actual full client deployment file

apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      automountServiceAccountToken: false
      containers:
        - name: client
          image: pidgeon/client-remix
          envFrom:
            - secretRef:
                name: dotenv
          # ----------------------------------------------------------------------------------------
          #
          # Development only
          #
          # We use init containers to download the root certificate authority certificate,
          # that was used to sign the intermediate and leaf certificate of Traefik,
          # and mount the certificate to the client's container as NODE_EXTRA_CA_CERTS
          #
          # We need to do this because otherwise any HTTPS requests issued from the client to Traefik will fail
          # ----------------------------------------------------------------------------------------
          env:
            - name: NODE_EXTRA_CA_CERTS
              value: /certs/pebble-root-ca.pem
            - name: REMIX_DEV_SERVER_WS_PORT
              value: "3001"
          volumeMounts:
            - name: certs
              mountPath: /certs
      initContainers:
        # ------------------------------------------
        #
        # Stage 1: Wait for Pebble to be available
        #
        # ------------------------------------------
        - name: client-wait-for-pebble
          image: alpine/curl
          command: ["/bin/sh", "-c"]
          args:
            [
              'while [[ "$(curl --insecure -s -o /dev/null -w %{http_code} https://pebble:15000/roots/0)" != "200" ]]; do echo "Waiting for Pebble..."; sleep 5; done',
            ]
        # ----------------------------------------------------------------------------------------
        #
        # Stage 2: Get the root CA used to sign Traefik's leaf and intermediate certificate
        #
        # ----------------------------------------------------------------------------------------
        - name: client-get-root-certs
          image: alpine/curl
          command: ["/bin/sh", "-c"]
          args:
            [
              "curl --insecure -s -o /certs/pebble-root-ca.pem https://pebble:15000/roots/0",
            ]
          volumeMounts:
            - name: certs
              mountPath: /certs
      volumes:
        - name: certs
          emptyDir: {}

For my case, I use init containers to extend the certificate authorities, via NODE_EXTRA_CA_CERTS, of the container that runs my Remix.Run client, which allows me to be able to perform HTTPS requests to my ingress controller, which handles all the routing in my cluster, but it also completes the authority chain of the certificates that I'm using. Thus I don't need to provide any extra properties to my live reload server. Just some food for thought

MichaelDeBoey pushed a commit to isaacs/remix that referenced this issue Jun 15, 2022
@tadeaspetak
Copy link

Being able to run a dev server on an https domain (localhost, lvh.me) would be wonderful 👌

@EBruchet
Copy link

EBruchet commented Aug 3, 2022

Is there any chance of the above commit being accepted in the near future? Using https configuration locally with an Express server setup is blocking access to the wss:// for hot reloading at the moment.

Not sure if there's another known workaround.

@kiliman
Copy link
Collaborator

kiliman commented Aug 3, 2022

Have you tried simply including the <LiveReload/> component in your project and removing the protocol check (i.e., always using ws:).

export const LiveReload =
process.env.NODE_ENV !== "development"
? () => null
: function LiveReload({
port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002),
nonce = undefined,
}: {
port?: number;
/**
* @deprecated this property is no longer relevant.
*/
nonce?: string;
}) {
let js = String.raw;
return (
<script
nonce={nonce}
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: js`
function remixLiveReloadConnect(config) {
let protocol = location.protocol === "https:" ? "wss:" : "ws:";
let host = location.hostname;
let socketPath = protocol + "//" + host + ":" + ${String(
port
)} + "/socket";
let ws = new WebSocket(socketPath);
ws.onmessage = (message) => {
let event = JSON.parse(message.data);
if (event.type === "LOG") {
console.log(event.message);
}
if (event.type === "RELOAD") {
console.log("💿 Reloading window ...");
window.location.reload();
}
};
ws.onopen = () => {
if (config && typeof config.onOpen === "function") {
config.onOpen();
}
};
ws.onclose = (error) => {
console.log("Remix dev asset server web socket closed. Reconnecting...");
setTimeout(
() =>
remixLiveReloadConnect({
onOpen: () => window.location.reload(),
}),
1000
);
};
ws.onerror = (error) => {
console.log("Remix dev asset server web socket error:");
console.error(error);
};
}
remixLiveReloadConnect();
`,
}}
/>
);
};

@EBruchet
Copy link

EBruchet commented Aug 3, 2022

I've been using <LiveReload /> in the project (worth noting it works great without this HTTPS config) but unfortunately tweaking the LiveReload component directly produces the following error

Uncaught DOMException: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.

Additionally as this is an organisation repository, editing the library files directly probably wouldn't be a maintainable approach going forward.

Appreciate the suggestion though 🙇‍♂️

@kiliman
Copy link
Collaborator

kiliman commented Aug 3, 2022

First, I wasn't saying to edit the Remix version, but to copy it into your project. It's a very straightforward component. I have custom LiveReload as well.

However, it doesn't look like you can connect to an insecure web socket from a secure route anyway, so this is a moot point.

Is there a reason you need HTTPS on your local machine? Have you considered running a reverse proxy like nginx in front of your Remix app? That way Remix doesn't care about HTTPS, only your browser.

@petomalina
Copy link

I made this change for gitpod.io env, and it started working flawlessly.

  • First commit is just copying the component into a custom one and importing instead of the official one
  • Second commit adds an expression to see whether I am running on gitpod.io and changes the URL

You can do the same easily with the protocols :)

@bderblatter-qualtrics
Copy link

@kiliman I need HTTPS for Marketo which requires a custom domain for certain functions to work (for security reasons). If I use a custom domain for localhost without HTTPS on my computer the page won't load because it is not secure.

@kiliman
Copy link
Collaborator

kiliman commented Aug 18, 2022

@petomalina I had to do a similar thing for CodeSandbox. If you look at my LiveReload component, you'll see that I had to change the URL since CSB encodes the port in the host name.
https://codesandbox.io/s/remix-starter-template-xs4z6?file=/app/components/LiveReload.tsx

@eladchen
Copy link

eladchen commented Oct 13, 2022

Doesn't seem like #4123 or #4107 will be merged anytime soon... so I solved it using the attached snippet.

The below connects to the web socket created when calling "remix watch" command, and spins up an
additional web socket that uses an https server instance.

Once a message to the remix web socket is sent, it is then sent to the https one.

const { WebSocket } = require("ws");

const httpsServer = // ... [some code that spins up an https server with express]

if (NODE_ENV !== "production") {
    const connectToRemixSocket = (cb, attempts = 0) => {
        const remixSocket = new WebSocket(`ws://127.0.0.1:8002`);

        remixSocket.once("open", () => {
            console.log("Connected to remix dev socket");

            cb(null, remixSocket);
        });

        remixSocket.once("error", (error) => {
            if (attempts < 3) {
                setTimeout(() => {
                    connectToRemixSocket(cb, attempts += 1);
                }, 1000);
            }
            else {
                cb(error, null);
            }
        });
    };

    connectToRemixSocket((error, remixSocket) => {
        if (error) {
            throw error;
        }

        const customSocket = new WebSocket.Server({ server: httpsServer });

        remixSocket.on("message", (message) => {
            customSocket.clients.forEach((client) => {
                if (client.readyState === WebSocket.OPEN) {
                    client.send(message.toString());
                }
            });
        });
    });
}

NOTE:
The above example is missing the https server portion.
Just make sure to set the live reload component port to the same port as the https server.

app/root.tsx:

<body>
    ....
    {/* assuming the https server is listening on port 2001 */}
    <LiveReload port={2001} />
</body>

@pcattori
Copy link
Contributor

pcattori commented Jul 5, 2023

The new v2 dev server now fully supports TLS, as described in the docs.

@pcattori pcattori closed this as completed Jul 5, 2023
@lopugit
Copy link

lopugit commented Mar 24, 2024

Use Vite and you don't need LiveReload 🙏💯🙌

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

Successfully merging a pull request may close this issue.