lydell/elm-watch

Support different client and server ports

chazsconi opened this issue · 9 comments

This is related to #39 which solved my problem for SSL hand off being done by an external server (An nginx Ingress in Kubernetes in this case).

I was able to have elm-watch listening on port 443 (but serving HTTP not HTTPS) as this port was not being used by anything else as the nginx was running elsewhere.

However, in another use case I have, I am running everything on my laptop - I use a local haproxy to do the SSL handoff. This of course must listen on port 443 thus not allowing elm-watch to also listen on this port.

Therefore I would really like to be able to configure the elm-watch client port to be 443 but set the server port to something else. In haproxy I would then rewrite /elm-watch to the server port.

I know that you are not keen on extra configuration options, so perhaps this could be done via an environment variable either to change the client port or the server port?

Hi again!

I’m missing something – if you’re running everything on your laptop, why can’t you use like any random port for elm-watch then? Why does it need to be behind the proxy?

Hi - thanks for responding so quickly!

I didn't try using a random port before as I wanted to avoid the self-signed certificate as I already had a valid certificate for the domain which is handled by `haproxy and does the SSL handoff.

However, I have now tried with a random port and this work on Firefox after accepting the self-signed cert from elm-watch.

However, it does not work in Chrome (which is my preferred browser for development) even after accepting the self-signed cert. I think this is because Chrome only accepts the certificate for the current page, and going to a different URL on the same domain or even just refreshing the page requires re-accepting the self-signed cert every time.

I just keep getting logs like this in the browser console:

elm.js:988 WebSocket connection to 'wss://foo.bar.com:53853/elm-watch?elmWatchVersion=1.1.0&targetName=Foo&elmCompiledTimestamp=1667594645328' failed:

The Chrome version is 107.0.5304.87 on an M1 Mac.

Oh, you don’t use localhost? I only tested Chrome (and other browsers) with localhost. Good to know. Edit: I added 127.0.0.1 foo.bar.com to /etc/hosts (on macOS) and went to https://foo.bar.com:8001/ in Chrome, and went through the self-signed certificate stuff. elm-watch then connected as expected. I used the example to test it. So I’m not sure why it didn’t work for you?

Do you have to files on your computer – like a .key and a .crt – for haproxy? I’ve been waiting for someone with this setup to see if an idea I’ve had makes sense. I have been thinking of adding configuration in elm-watch.json that lets you point out those two files so elm-watch can use the same certificate for its SSL stuff. Does that make sense?

Finally, I’m curious why you are running HTTPS locally. We used to do that at work, because we thought it was good to have the local environment be as similar as possible as the production environment. However, we noticed that the HTTPS part never caught any bugs or anything for us, so we tried switching to HTTP and everything got simpler (save for having to conditionally set the Secure flag of two session cookies).

I'll test the example later to see if this works for me.

However, to answer your other questions:

Do you have to files on your computer – like a .key and a .crt – for haproxy?

Yes, I have the Let's Encrypt .key and .crt files in a ssl/le-certs folder on my laptop that is referenced by haproxy. Similar to what you tried, I also set the domain to point to 127.0.0.1 in /etc/hosts

It's a pretty simple haproxy setup - something like:

frontend https-in
  bind *:443 ssl crt ssl/le-certs

  # I would like to use the following line, but would 
  # need the to configure elm-watch server to run on port 4001
  # use_backend elm-watch if { path_reg ^/elm-watch }

  use_backend phoenix  if { path_reg ^/ }

backend phoenix 
  server node1 0.0.0.0:4000

# backend elm-watch
#   server node 1 0.0.0.0:4001

I have been thinking of adding configuration in elm-watch.json that lets you point out those two files so elm-watch can use the same certificate for its SSL stuff. Does that make sense?

I'm not sure. I know that you do support SSL hand-off in elm-watch currently, but do you think it is necessary? As you say in "What elm-watch is not", it is "not a file server" and "Let elm-watch excel at compiling Elm quickly and reliably, and own the rest of the stack yourself."

If a user is using HTTPS for development they will need to have the file server/web server that they use for the non-Elm stuff do SSL hand off anyway, so maybe it's simpler to just let them do this in one place with a proxy like nginx or haproxy. (Although of course to do this, you need to be able to configure elm-watch to run on port 443 in the client and a different port on the server :) )

Although haproxy lets you put all your .key and .crt files in the same folder, I expect different web servers/proxies rely on different setups, e.g. putting the files in different folders, joining the files, or using .pem files instead. With Let's Encrypt certs you need to renew them frequently, so a user will need to regularly update the files in the correct format and location for their own web server/proxy and elm-watch (although of course they could script this).

I’m curious why you are running HTTPS locally

I have found that browser features increasingly only work with HTTPS and so this is the only option for testing in a development environment. These include:

  • Copy to clipboard (using navigator.clipboard)
  • Service workers
  • Push notifications

I have also read that Chrome (although I cannot find the original source of the story from Google :( ) will eventually stop supporting HTTP completely (although I suppose for localhostit will always be allowed). I suspect that sites with self-signed certs will also not be supported in the future - so you won't be able to click the "Advanced" button and agree to continue. I think this is already the case for real certs that have expired, which up to a few months ago, allowed you to still click the "Advanced" button.

In my use case I cannot use localhost (even if I wasn't testing the browser HTTPS only features) as I have a couple of microsites running on different domains with a single sign on between them.

If a user is using HTTPS for development they will need to have the file server/web server that they use for the non-Elm stuff do SSL hand off anyway, so maybe it's simpler to just let them do this in one place with a proxy like nginx or haproxy.

Yeah, I’m not sure what’s simpler either. elm-watch currently contains a hardcoded certificate, so being able to point out two files is not far from what we have today. And that might be easier to do for end users than proxying? Supporting different ports for backend and frontend also sounds easy (I think), but from my experience with Docker it’s easy to confuse oneself with port mappings (which port is used where).

I have found that browser features increasingly only work with HTTPS and so this is the only option for testing in a development environment.

FYI: As far as I understand, http://localhost is an exception. I know for sure that clipboard and service workers work there, but I haven’t tested push notifications.


Either way, I’m not sure which path I want to take here and don’t feel like thinking more about it right now, so I’m going to let this one sit for a while. Maybe some other HTTPS-locally user pops up and gives more insight.

For now, you can use a hack like this:

<script>
window.WebSocket = class HackWebSocket extends WebSocket {
  constructor(urlParam) {
    const url = new URL(urlParam);
    if (looksLikeElmWatch(url)) {
      url.port = "1234";
    }
    super(url);
  }
}

function looksLikeElmWatch(url) {
  return url.port === "4321";
  // or return url.pathname === "/elm-watch";
}
</script>

Ok, one more question: If I were to add cert configuration to elm-watch.json, would it look something like this for you then?

{
  "sslCertificate": {
    "cert": "./ssl/le-certs/foo.crt",
    "key": "./ssl/le-certs/foo.key"
  },
  "targets": {}
}

In other words, the paths are relative to the elm-watch.json file, and should work for any contributor, not just on your machine. (Config in elm-watch.json should be correct for everyone; things that differ for different contributors go in environment variables (ELM_WATCH_OPEN_EDITOR) or can be changed via the browser UI (compilation mode, UI position, error overlay).)

I checked Node.js, Go, Ruby, Python, nginx, Apache, and they all take two files as input. So far, I’ve only found HAProxy that concatenates them. And even in that case, couldn’t you “just” keep three files on disk?

For now, you can use a hack like this:

<script>
window.WebSocket = class HackWebSocket extends WebSocket {
  constructor(urlParam) {
    const url = new URL(urlParam);
    if (looksLikeElmWatch(url)) {
      url.port = "1234";
    }
    super(url);
  }
}

function looksLikeElmWatch(url) {
  return url.port === "4321";
  // or return url.pathname === "/elm-watch";
}
</script>

This works great - thanks for the tip!

Ok, one more question: If I were to add cert configuration to elm-watch.json, would it look something like this for you then?

{
  "sslCertificate": {
    "cert": "./ssl/le-certs/foo.crt",
    "key": "./ssl/le-certs/foo.key"
  },
  "targets": {}
}

Yes, this would be perfect.

So far, I’ve only found HAProxy that concatenates them. And even in that case, couldn’t you “just” keep three files on disk?

In fact HAProxy doesn't concatenate them - it just expects all the files in the same folder.

Just some extra info for anyone else using a setup with HAProxy: To prevent elm-watch from frequently reloading, you will need to add something like this to your HAProxy config in the backend section for elm-watch

timeout tunnel 1h