Websockets обратный прокси-сервер в IIS 8

Я пытаюсь подключиться к серверу websockets (websockify) через обратный прокси-сервер в IIS. Сервер IIS и веб-серверы находятся на одном физическом сервере (Windows Server 2012 R2, IIS 8.5, ARR 3, Websockets включены). Я видел несколько вопросов об этом, и он предположил, что это должно работать с IIS 8 и ARR 3, но пока никаких реальных решений пока нет. У меня есть некоторый опыт работы с обратными прокси-серверами http/https в IIS, но это моя первая попытка работать с веб-сайтами.

Например:

Оригинальный URL: WS://10.2.1.10/websockify

Обратный прокси должен перевести это на: WS://10.2.1.10: 5901/websockify

Слишком общее правило выборки в web.config:

<rewrite>
     <rules>               
        <rule name="WS reverse proxy" stopProcessing="true"> 
          <match url="(.*)" />
          <conditions> <add input="{CACHE_URL}" pattern="^(.+)://" /> 
          </conditions> 
          <action type="Rewrite" url="{C:1}://10.2.1.10:5901/websockify"/>       
        </rule>
     </rules>
</rewrite>

В случае неудачной трассировки запроса URL-адрес переводится, но по какой-то причине он не доходит до сервера websocket в 10.2.1.10:5901.

Конечной целью является включение noVNC/websockify для обеспечения доступа клиента на основе браузера к нескольким серверам VNC в сети. Любая помощь в понимании того, как обращать прокси-сервер, приветствуется.

Ответ 1

Я пытался выполнить то же самое на IIS 8.5 с ARR 3.0, и в конце концов обнаружил проблему. Согласно Microsoft Erez Benari, это возможно:

Для поддержки WebSocket в IIS должна быть установлена функция WebSocket, но не требуется никаких других настроек или действий. Установите функцию с помощью диспетчера серверов, добавьте роли и компоненты, и после ее завершения ARR 3.0 будет обрабатывать запросы соответствующим образом.

В качестве теста я настроил сервер Node.js для WebSocket:

const WebSocketServer = require('ws');
const wss = new WebSocketServer({ port: 3011 });
function sendWSMessage(msg) {
    wss.clients.forEach((client) => {
        client.send(msg);
    });
}
setInterval(function() {
    sendWSMessage('hello client');
}, 3000);

Вместе с простой тестовой страницей:

var websock = new WebSocket('ws://localhost:3011');
websock.onmessage = function (event) {
    console.log(event.data);
};
websock.onopen = function (event) {
  websock.send("hello server"); 
};

Затем я установил обратный прокси-сервер ARR на своем локальном компьютере, добавив в файл web.config каталог "wstest" на локальном хосте:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
    <rewrite>
        <rules>
            <rule name="WebSocketTestRule" stopProcessing="true">
                <match url=".*" />
                <conditions>
                    <add input="{CACHE_URL}" pattern="^(.+)://" />
                </conditions>
                <action type="Rewrite" url="{C:1}://localhost:3011/" />
            </rule>
        </rules>
    </rewrite>
</system.webServer>
</configuration>

Это должно перенаправить весь трафик для //localhost/wstest на сервер Node.js через порт 3011. Сервер Node работает, когда я напрямую подключаюсь к нему через ws://localhost:3011. Когда я пытаюсь подключиться через прокси-сервер через ws://localhost/wstest, запрос проходит к серверу Node.js, происходит обновление и соединение устанавливается.

Chrome отправляет:

GET ws://localhost/wstest HTTP/1.1
Host: localhost
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: file://
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.84 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
Sec-WebSocket-Key: 4ufu8nAOj7cKndASs4EX9w==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Сервер Node.js получает:

cache-control: no-cache
connection: upgrade
pragma: no-cache
upgrade: Websocket
accept-encoding: gzip, deflate, sdch
accept-language: en-US,en;q=0.8
host: localhost:3011
max-forwards: 10
user-agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.84 Safari/537.36
origin: file://
sec-websocket-version: 13
sec-websocket-key: fBkTwAS9d/unXYKDE3+Jjg==
sec-websocket-extensions: permessage-deflate; client_max_window_bits
x-original-url: /wstest
x-forwarded-for: [::1]:54499
x-arr-log-id: a0b27458-9231-491d-b74b-07ae5a01c300

Сервер Node.js отвечает:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-Websocket-Accept: yep8mgQACAc93oGIk8Azde4WSXk=
Sec-WebSocket-Extensions: permessage-deflate

И наконец Chrome получает:

HTTP/1.1 101 Switching Protocols
Upgrade: Websocket
Sec-WebSocket-Accept: CBSM8dzuDoDG0OrJC28nIqaw/sI=
Sec-WebSocket-Extensions: permessage-deflate
X-Powered-By: ARR/3.0
Connection: Upgrade
X-Powered-By: ASP.NET
Date: Fri, 10 Jun 2016 21:16:16 GMT
EndTime: 17:16:16.148
ReceivedBytes: 0
SentBytes: 0

Так что теперь они связаны. Все это выглядит хорошо, единственное заметное отличие состоит в том, что Sec-WebSocket-Key и Sec-WebSocket-Accept изменяются в обоих направлениях либо IIS, либо прокси-сервером ARR.

Но... ни один фрейм WebSocket не сможет пройти через прокси! Когда Chrome получает положительный отзыв о своем запросе на обновление, он отправляет свой фрейм сообщения WebSocket, а затем сидит и ждет сообщений от сервера. Сервер Node.js отправляет свои фреймы, и никаких ошибок не возникает, но они никогда не принимаются Chrome. Сообщение, отправленное Chrome, никогда не принимается Node.js. Похоже, что ARR/IIS отбрасывает кадры WebSocket в обоих направлениях.

Обратите внимание, как Chrome сообщает серверу, что он поддерживает расширение permessage-deflate, которое является расширением WebSocket для сжатия каждого сообщения. Сервер отвечает, что он также поддерживает permessage-deflate, поэтому, когда браузер и сервер отправляют свои сообщения друг другу, они используют это расширение сжатия. ОДНАКО, парень посередине, ARR, очевидно, не поддерживает это сжатие! Отключив поддержку permessage-deflate на сервере, фактические фреймы WebSocket теперь могут без проблем проходить через прокси:

const wss = new WebSocketServer({ port: 3011, perMessageDeflate: false });

Я думаю, что проблема в том, что ARR 3.0 не поддерживает заголовок Sec-Websocket-Extensions, поэтому он позволяет заголовку просто проходить. Но разрешать согласование этого заголовка между клиентом и сервером неверно, поскольку ARR не участвует в согласовании и не может сообщить двум сторонам, что он не поддерживает передачу сжатых сообщений. Надеемся, что когда-нибудь ARR сможет правильно обрабатывать расширения, договариваясь между собой и клиентом, а затем выполняя отдельное согласование между собой и сервером. В его нынешнем виде клиент и сервер просто договариваются друг с другом, что приводит к этой ошибке.

Ответ 2

Как указано в @jowo, IIS8/8.5, похоже, не поддерживают Sec-Websocket-Extensions. При этом обходной путь, который я применил, - это просто переписать переменную сервера, о которой идет речь. Сначала добавьте HTTP_SEC_WEBSOCKET_EXTENSIONS в список разрешенных серверов, затем добавьте его в свое правило:

<serverVariables><set name="HTTP_SEC_WEBSOCKET_EXTENSIONS" value="" /></serverVariables>

Таким образом, получатель не получит проблемное разрешение-дефляция:)