IIS As Reverse Proxy To WebSocket Server

Adrian Jenkins
9 min readOct 4, 2022

In this article we will use IIS as a reverse proxy to a WebSocket Server.

Prerequisites:

Architecture

Client => Reverse Proxy (IIS) => Backend (Node.js)

IIS will act as a reverse proxy to forward requests from “localhost” OR “reverseproxy.com” to “localhost:8080”.

In IIS we will have a site called “ReverseProxy” with bindings for HTTP & HTTPS “localhost” + HTTP & HTTPS “reverseproxy.com”.

For “reverseproxy.com” we will fake an “A” Record in “C:\Windows\System32\drivers\etc\hosts” file.

127.0.0.1 reverseproxy.com

So, if client request “localhost” or “reverseproxy.com” IIS should forward it to “http://localhost:8080" that will shown an “index.html” from where the WebSocket connection will be created.

WebSocket endpoints:

  • “ws://localhost:8080”
  • “wss://reverseproxy.com”

What is WebSocket?

WebSocket is bidirectional, a full-duplex protocol that is used in the same scenario of client-server communication, unlike HTTP it starts from ws:// or wss://. It is a stateful protocol, which means the connection between client and server will keep alive until it is terminated by either party (client or server). After closing the connection by either of the client and server, the connection is terminated from both ends.

I do want to make a clarification regarding “[…] starts from ws:// or wss://”. We need to understand every WebSocket request starts as a HTTP or HTTPS request. If all succeeds, the same HTTP or HTTPS connection will be upgraded to a WebSocket connection.

WebSocket protocol needs at least HTTP/1.1

What is “ws://” and “wss://”

We just said that every WebSocket request starts as an HTTP or HTTPS request.

So:

“ws://” happens over HTTP.

“wss://” happens over HTTPS.

Understanding A WebSocket Request

WebSocket is not a protocol on its own. It is a protocol on top of HTTP protocol.

Actually, if you manually type in the Browser “ws://<whatever>” you will receive “ERR_UNKNOWN_URL_SCHEME”.

When you make a WebSocket request, it first makes a HTTP request with three special headers:

  • Connection: “Upgrade” — signals that the client would like to change the protocol.
  • Sec-WebSocket-Key: XXX — Uunique base-64 encoded key that the client selects. The value that the client sends to the server will in-turn be used by the server to create yet another key and sent back as a Sec-WebSocket-Accept response header.
  • Upgrade: “websocket” — requested protocol is “websocket”.
  • Sec-WebSocket-Version — WebSocket protocol version
  • Sec-WebSocket-Extensions [optional]: means that the browser supports data compression.

Now, the Server knows this request wants to be upgraded to WebSocket connection. If all goes well, it will do just that, and it will switch protocols (101) and upgrade this same connection to “ws://” of “wss://”.

In JavaScript you create a WebSocket connection with

let socket = new WebSocket(URL);

For example, if you browse to this site: WebSocket (javascript.info) open DevTools and locate the following SS

You can type in the console:

let socket = new WebSocket(“wss://javascript.info/article/websocket/chat/ws”);

As soon as you hit “enter” it will make the connection and then, you can send a message with

socket.send(message);

IIS As Reverse Proxy:

Need prerequisites!

IIS

As the aim of this article is to explain IIS as reverse proxy for WebSocket, I will revise IIS prerequisites here:

  • ARR installed.
  • URL Rewrite installed
  • WebSocket Protocol installed and enabled (“system.webServer/webSocket/enabled = True”)

IIS — Site

Create a site in IIS called “ReverseProxy”. This site will act as a reverse proxy and forward traffic to “http://localhost:8080”, where our WebSocket server will be.

The site can be pointing to an empty folder, does not matter. Remember, this site will just act as a proxy.

The idea is to forward requests from “reverseproxy.com” or “localhost” to “localhost:8080”. Therefore, we need to add those bindings to our site:

To generate the certificate, you can run the next command in PS:

New-SelfSignedCertificate -DnsName “reverseproxy.com” -CertStoreLocation “cert:\LocalMachine\My”

For localhost you can use the “IIS Express Development Certificate”.

IIS-ARR

Now, go at Server level in IIS > ARR and ensure “Enable proxy” is checked.

This will make the whole URL available for us to rewrite.

Without ARR and “Enable proxy” we can only rewrite the path of a given URL.

Example:

  • “http://something/car1/year” rewrite to => “/cars/brands/years/year/2022.html”.

Externally, client will see “http://something/car1/year”, but on the server it will go to “http://something/cars/brands/years/year/2022.html”

After “Enable proxy” we have access to the full URL, so we can completely rewrite it.

IIS — URL Rewrite

I want to create a rule as simple as possible.

We will rewrite every request to “reverseproxy.com” OR “localhost” to “http://localhost:8080”.

Go to URL Rewrite module at Server Level and create the following rule:

I have read multiple discussions that IIS does not handle well the “Sec-WebSocket-Extensions: permessage-deflate”, however I have not had any issues whatsoever. If you face that issue, rewrite that header with an empty value. I will add those articles below.

Code Section

Server.js:

  • Create HTTP server.
  • If requested path is “/”, then show “index.html”.
  • Create WebSocket Server by passing that HTTP server. Remember WebSocket uses same HTTP connection.
  • On “request” event to log messaged received from client and we will have a function that pings the client every 20 seconds.

Index.html:

  • Establish a WebSocket connection.
  • If “host” (in request Headers) is localhost — does not matter if it is HTTP or HTTPS, then => “ws://localhost:8080”.
  • If “host (in request Headers) is “reverseproxy.com”, does not matter if it is HTTP or HTTPS, then => “wss://reverseproxy.com”
  • On “close” event, log the code in the console.log.
  • Append received messages from WebSocket server in HTML.
  • WebSocket endpoints: “ws://localhost:8080” | “wss://reverseproxy.com”

Testing

Request to “https://localhost"

Client => “https://localhost” => ARR redirect to => “http://localhost:8080” => shows => “index.html” => open WebSocket connection to => “ws://localhost:8080”.

In Fiddler we can see the initial request, then it gets tunnel to “localhost:8080” and “index.html” creates a connection to “ws://localhost:8080”.

If we take a closer look at the WebSocket connection, we can clearly see how it was a GET request to “http://localhost:8080” with needed WebSocket headers that it got upgraded to a WebSocket connection.

HTML view:

In IIS Logs we will see a 200.

2022–09–29 16:15:32 ::1 GET / X-ARR-CACHE-HIT=0&X-ARR-LOG-ID=9ed47fd8–70d1–493c-b843-fde938419d71&SERVER-STATUS=200 80 — ::1 HTTP/1.1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/106.0.0.0+Safari/537.36–200 0 0 62

Should not IIS Log 101? We can clearly see 101 in Fiddler, and we know connection is being established as we are seeing the messages in the html.

This is because IIS never made a WebSocket connection. As far as it is concerned, it got a request, forwarded to “localhost:8080” and that gave back a html file.

Now, this html file will establish a WebSocket connection to “localhost:8080” (Node.js) and will start pinging every 20 seconds and appending/updating those messages into the html file.

The WebSocket conversation is between the html file, so to speak, and the WebSocket server. Messages are only being updated through the same connection into the html file.

Request to “http://reverseproxy".

Client => “http://reverseproxy.com” => ARR redirect to => “http://localhost:8080” => shows => “index.html” => open WebSocket connection to => “wss://reverseproxy.com”.

This “wss://reverseproxy.com” follows the following flow:

(Remember this next request has upgrade header)

“https://reverseproxy.com” => ARR redirect to => “http://localhost:8080” => acknowledge and accept request to switch protocol => WebSocket connection get established.

Fiddler:

Request and response headers of the WebSocket connection:

In FREB we will see two requests. The first one for the html file:

Client => http://reverseproxy.com => redirected to http://localhost:8080 => shows “index.html”

And then, the second request is for “wss://reverseproxy.com” that, by now we know that it will start as an “HTTPS” request.

HTTPS request to “https://reverseproxy.com" with upgrade to header => forwarded to “localhost:8080” => Server accepts it and switch protocol.

In IIS Logs we should see two requests:

2022–09–30 20:53:35 127.0.0.1 GET / X-ARR-CACHE-HIT=0&X-ARR-LOG-ID=ad279eb5–5553–4609–8c40–4f529aee77f7&SERVER-STATUS=200 443–127.0.0.1 HTTP/2 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/105.0.0.0+Safari/537.36+Edg/105.0.1343.53–200 0 0 22

2022–09–30 20:53:41 127.0.0.1 GET / X-ARR-CACHE-HIT=0&X-ARR-LOG-ID=a3614585-e908–40f2–9435-e3c23a2a6e31&SERVER-STATUS=101 443–127.0.0.1 HTTP/1.1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/105.0.0.0+Safari/537.36+Edg/105.0.1343.53–101 0 64 5808

This time, we are using the same HTTPS connection that we passed through the IIS with upgrade header. As it passed through IIS, we will FREB and logs for 101 status codes.

In Summary:

  • The primary application of WebSocket in the industry is to make the browser richer, such that communication between a browser and a server can be bi-directional.
  • “ws” happens over HTTP while.
  • “wss” happens over HTTPS.
  • WebSocket protocol requires at least HTTP/1.1 version.
  • Every WebSocket request starts as a HTTP or HTTPS (wss) request.
  • A WebSocket request is a HTTP request sent with special headers: “Connection”, “Sec-WebSocket-Key”, “Upgrade” and “Sec-WebSocket-Version”.
  • In order for IIS to support WebSocket you must have “WebSocket Protocol” installed and enabled (“system.webServer/webSocket/enabled = True”).

Resources:

--

--