remix icon indicating copy to clipboard operation
remix copied to clipboard

LiveReload doesn't work when TLS is enabled

Open isaacs opened this issue 2 years ago • 10 comments

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 });

isaacs avatar Apr 19 '22 05:04 isaacs

@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

mstaicu avatar Apr 20 '22 17:04 mstaicu

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

tadeaspetak avatar Aug 01 '22 14:08 tadeaspetak

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.

EBruchet avatar Aug 03 '22 11:08 EBruchet

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

https://github.com/remix-run/remix/blob/40a7a390063836607c76aad9754b7881f8c82303/packages/remix-react/components.tsx#L1498-L1561

kiliman avatar Aug 03 '22 14:08 kiliman

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 🙇‍♂️

EBruchet avatar Aug 03 '22 14:08 EBruchet

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.

kiliman avatar Aug 03 '22 14:08 kiliman

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 :)

petomalina avatar Aug 17 '22 15:08 petomalina

@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.

bderblatter-qualtrics avatar Aug 18 '22 21:08 bderblatter-qualtrics

@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

kiliman avatar Aug 18 '22 22:08 kiliman

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>

eladchen avatar Oct 13 '22 11:10 eladchen

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

pcattori avatar Jul 05 '23 20:07 pcattori

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

lopugit avatar Mar 24 '24 09:03 lopugit