Доброго времени суток всем!
В этой заметке я постараюсь рассмотреть проблематику зашифрованного соединения с Web-сервером (HTTPS) и такого же соединения с Comet-сервером с использованием WebSocket (WSS) при использовании самоподписанных сертификатов, а так же варианты для решения данной проблемы.
В данный момент времени я занимаюсь разработкой Web UI к некоему продукту компании, который будет использоваться внутри клиентских корпоративных сетей.
Изначальные условия: доступ к Web UI прямо по IP адресу сервера, желательно использование стандартных портов 80/443, чтобы облегчить деплоймент, ОС сервера — Windows Server 2012.
Comet-сервер нужен нам для организации связи между неким сервисом, который формирует отчеты по заданным параметрам, и непосредственно Web UI, через который клиент отправляет запрос, получает статус о выполнении и завершении задания, а так же для передачи разных сервисных сообщений о событиях (например — пользователь вошел в систему, данные пользователя были изменены администратором и т.п.).
Для Comet-сервера и бэкенда Web UI мы используем Node.JS.
Вообще опыта использования Node.JS у меня особого нет, но как оказалось подобные задачи делать на нем весьма удобно — не вдаваясь в детали на само написание первой версии с глобальным бродкастом сообщений между всеми участниками без «комнат» или «каналов» у меня ушло буквально пару часов. И большая куча времени на инвестигирование модулей Node.JS, на которых можно реализовать WebSocket сервер (Socket.IO, WS) — тут время ушло на исследование стыковки C# серверного компонента и Comet-сервера. Socket.IO нам не подошел — в версии > 1 там реализовано много «поверх», решили по разным причинам использовать более нативную реализацию и взяли модуль WS.
Скажу сразу, что Comet-сервер имеет два интерфейса: первый это обычный HTTP интерфейс (внутренний с авторизацией запросов), по которому происходит авторизация пользователей и компонентов, отправка команд серверу; второй это непосредственно WebSocket интерфейс (WS), к которому подключаются авторизованные пользователи и компоненты для получения сообщений о событиях в системе.
В связке HTTP+WS все естественно работает на «ура» и никаких проблем, но по заданию нам нужен SSL, и соответственно HTTPS+WSS.
Создать самоподписанный сертификат при помощи OpenSSL дело несложное, прикрутить к Web UI тоже буквально пара строчек, к интерфейсу Comet тоже просто.
И вот тут началось самое интересное.
Как все знают, при использовании самоподписанных сертификатов любой браузер выдает предупреждение и просит пользователя подтвердить, что он доверяет этому сертификату.
Итак, мы идем на тестовый Web UI с эмуляцией клиента по адресу
Из четырех тестируемых браузеров — IE, Firefox, Opera, Chrome — второй раз не спрашивают подтверждения на другой порт по тому же адресу Chrome и Opera, IE и Firefox запрос молча обрубают с выдачей ошибки в консоль.
Поскольку скорее всего у клиента будет IE (все-таки Windows это массовая ОС у корпоративных клиентов), то это большая проблема.
Самое простое решение, которое приходит в голову — это проксировать запросы. В Linux я бы поставил Nginx и проксировал через него, но у нас Windows. Взял уже имеющийся у меня Apache 2.2, настроил SSL и дал вот такую директиву:
То есть запросы на /wss/ пробрасываем к WS интерфейсу, все остальное — на Web UI.
Не заработало. Гугление и прочее выдало что надо Apache 2.4 с модулем mod_proxy_wstunnel. Поставил, подключил — все прекрасно, Web UI выдает запрос один раз, WSS соединение устанавливается без проблем.
Но Apache в данном получается все-таки лишним компонентом, и несколько портит производительность, которую дает асинхронное приложение на Node.JS.
Решил попробовать написать прокси-сервер на Node.JS, не вышло. Если вкратце, то одновременные запросы на любой один порт с поднятием WS и HTTPS интерфейсов HTTPS интерфейс в упор не ловит запросов по WSS протоколу. Совсем. Проксировать отдельно HTTPS на внутренний HTTP и отдельно WSS на внутренний WS проксирует, а вот поймать урл при запросе
Хорошо, вернемся к первому варианту и двум портам с подтверждением сертификатов.
В Node.JS WSS сервер создается на основе HTTPS-сервера и умеет отдавать некий контент при прямом запросе
Ради смеха решил попробовать iframe. В iframe выдается предупреждение… И никаких кнопок для пользователя чтобы подтвердить сертификат. Да и в любом случае я никак не могу отследить, подтвердил там чего пользователь или нет, чтобы потом установить соединение. Прописать в техтребованиях, чтобы пользователь обязательно сходил на два адреса? Тоже как-то некрасиво… AJAX-запрос на другой порт по тому же адресу вообще не работает из-за внутренней безопасности — для Javascript это два разных сайта.
Оказалось несколько бредовым, но весьма рабочим. Итак, нам надо два раза подтвердить сертификат. А что если мы попросим пользователя сначала сходить на HTTPS интерфейс WSS-сервера, а потом после подтверждения сделаем редирект на основной WebUI?
Работает! При запросе
NB! Выше я ничего не написал про доменные имена. Конечно, их можно использовать, чтобы получить нормальные сертификаты, но у меня есть определенные ограничения в виде корпоративной сети, из которой далеко не всегда есть доступ к центру сертификации, выдавшему сертификат. Да и требовать заводить доменные имена во внутренней сети для продакшена в моем случае это лишняя головная боль. Еще один сервер для Web UI на Linux, например, с проксей в виде Nginx в силу разных причин мы тоже не рассматриваем.
Для решения данной проблемы нашлось два пути:
Буду рад любым комментариям, может есть еще какие решения.
для Node.JS все-таки получилось сделать проксирование WS запросов (я брал node-http-proxy) по части урла (спасибо xdenser) средствами самого Node.JS на событие Upgrade, причем проксирование оптимально добавить прямо в http-сервер Web UI, чтобы не плодить компоненты, примерно таким образом:
В этой заметке я постараюсь рассмотреть проблематику зашифрованного соединения с Web-сервером (HTTPS) и такого же соединения с Comet-сервером с использованием WebSocket (WSS) при использовании самоподписанных сертификатов, а так же варианты для решения данной проблемы.
В данный момент времени я занимаюсь разработкой Web UI к некоему продукту компании, который будет использоваться внутри клиентских корпоративных сетей.
Изначальные условия: доступ к Web UI прямо по IP адресу сервера, желательно использование стандартных портов 80/443, чтобы облегчить деплоймент, ОС сервера — Windows Server 2012.
Comet-сервер нужен нам для организации связи между неким сервисом, который формирует отчеты по заданным параметрам, и непосредственно Web UI, через который клиент отправляет запрос, получает статус о выполнении и завершении задания, а так же для передачи разных сервисных сообщений о событиях (например — пользователь вошел в систему, данные пользователя были изменены администратором и т.п.).
Для Comet-сервера и бэкенда Web UI мы используем Node.JS.
Вообще опыта использования Node.JS у меня особого нет, но как оказалось подобные задачи делать на нем весьма удобно — не вдаваясь в детали на само написание первой версии с глобальным бродкастом сообщений между всеми участниками без «комнат» или «каналов» у меня ушло буквально пару часов. И большая куча времени на инвестигирование модулей Node.JS, на которых можно реализовать WebSocket сервер (Socket.IO, WS) — тут время ушло на исследование стыковки C# серверного компонента и Comet-сервера. Socket.IO нам не подошел — в версии > 1 там реализовано много «поверх», решили по разным причинам использовать более нативную реализацию и взяли модуль WS.
Скажу сразу, что Comet-сервер имеет два интерфейса: первый это обычный HTTP интерфейс (внутренний с авторизацией запросов), по которому происходит авторизация пользователей и компонентов, отправка команд серверу; второй это непосредственно WebSocket интерфейс (WS), к которому подключаются авторизованные пользователи и компоненты для получения сообщений о событиях в системе.
В связке HTTP+WS все естественно работает на «ура» и никаких проблем, но по заданию нам нужен SSL, и соответственно HTTPS+WSS.
Создать самоподписанный сертификат при помощи OpenSSL дело несложное, прикрутить к Web UI тоже буквально пара строчек, к интерфейсу Comet тоже просто.
И вот тут началось самое интересное.
Как все знают, при использовании самоподписанных сертификатов любой браузер выдает предупреждение и просит пользователя подтвердить, что он доверяет этому сертификату.
Итак, мы идем на тестовый Web UI с эмуляцией клиента по адресу
127.0.0.1
, подтверждаем сертификат, далаем попытку соединения по адресу wss://127.0.0.1:4433/
— и получаем ошибку соединения. Потому что для адреса 127.0.0.1 и порта 4433 сертификат надо подтверждать второй раз. Браузер при этом ну никаких дополнительных сообщений о том, что сертификат нужно подтвердить, не выдает. Просто молча сбрасывает запрос и все, он даже до сервера не доходит. Из четырех тестируемых браузеров — IE, Firefox, Opera, Chrome — второй раз не спрашивают подтверждения на другой порт по тому же адресу Chrome и Opera, IE и Firefox запрос молча обрубают с выдачей ошибки в консоль.
Поскольку скорее всего у клиента будет IE (все-таки Windows это массовая ОС у корпоративных клиентов), то это большая проблема.
Решение 1
Самое простое решение, которое приходит в голову — это проксировать запросы. В Linux я бы поставил Nginx и проксировал через него, но у нас Windows. Взял уже имеющийся у меня Apache 2.2, настроил SSL и дал вот такую директиву:
ProxyPass /wss/ ws://127.0.0.1:8095/
ProxyPass / http://127.0.0.1:8080/
То есть запросы на /wss/ пробрасываем к WS интерфейсу, все остальное — на Web UI.
Не заработало. Гугление и прочее выдало что надо Apache 2.4 с модулем mod_proxy_wstunnel. Поставил, подключил — все прекрасно, Web UI выдает запрос один раз, WSS соединение устанавливается без проблем.
Но Apache в данном получается все-таки лишним компонентом, и несколько портит производительность, которую дает асинхронное приложение на Node.JS.
Решил попробовать написать прокси-сервер на Node.JS, не вышло. Если вкратце, то одновременные запросы на любой один порт с поднятием WS и HTTPS интерфейсов HTTPS интерфейс в упор не ловит запросов по WSS протоколу. Совсем. Проксировать отдельно HTTPS на внутренний HTTP и отдельно WSS на внутренний WS проксирует, а вот поймать урл при запросе
wss://127.0.0.1/wss/
— никак.Хорошо, вернемся к первому варианту и двум портам с подтверждением сертификатов.
В Node.JS WSS сервер создается на основе HTTPS-сервера и умеет отдавать некий контент при прямом запросе
127.0.0.1:4433
, чем я и решил воспользоваться:var ws = require('ws');
var https = require('https');
var fs = require('fs');
var processRequest = function( req, res ) {
res.statusCode = 200;
res.end('Hello, world!' + "\n");
};
var options = {
key: fs.readFileSync(PathToKey),
cert: fs.readFileSync(PathToCert)
};
var cometApp = https.createServer(options, processRequest).listen(4433, function() {
console.log('Comet server [WSS] on *:4433');
});
var cometServerOptions = {
server: cometApp,
verifyClient: function(request) {
// Some client verifying
return true;
}
};
var cometServer = new ws.Server(cometServerOptions);
cometServer.on('connection', function(socket) {
socket.on('message', function(data) {
console.log('New message received');
}
});
});
Ради смеха решил попробовать iframe. В iframe выдается предупреждение… И никаких кнопок для пользователя чтобы подтвердить сертификат. Да и в любом случае я никак не могу отследить, подтвердил там чего пользователь или нет, чтобы потом установить соединение. Прописать в техтребованиях, чтобы пользователь обязательно сходил на два адреса? Тоже как-то некрасиво… AJAX-запрос на другой порт по тому же адресу вообще не работает из-за внутренней безопасности — для Javascript это два разных сайта.
Решение 2
Оказалось несколько бредовым, но весьма рабочим. Итак, нам надо два раза подтвердить сертификат. А что если мы попросим пользователя сначала сходить на HTTPS интерфейс WSS-сервера, а потом после подтверждения сделаем редирект на основной WebUI?
var ws = require('ws');
var https = require('https');
var fs = require('fs');
var processRequest = function( req, res ) {
res.setHeader("Location", redirectUrl);
res.statusCode = 302;
res.end();
};
var options = {
key: fs.readFileSync(PathToKey),
cert: fs.readFileSync(PathToCert)
};
var cometApp = https.createServer(options, processRequest).listen(4433, function() {
console.log('Comet server [WSS] on *:4433');
});
var cometServerOptions = {
server: cometApp,
verifyClient: function(request) {
// Some client verifying
return true;
}
};
var cometServer = new ws.Server(cometServerOptions);
cometServer.on('connection', function(socket) {
socket.on('message', function(data) {
console.log('New message received');
}
});
});
Работает! При запросе
127.0.0.1:4433/
меня теперь просит подтвердить сертификат, потом автоматически перебрасывает в Web UI 127.0.0.1
, где тоже просит подтвердить сертификат, после чего связка HTTPS на одном + WSS на другом порту прекрасно работает и больше ничего не спрашивает.NB! Выше я ничего не написал про доменные имена. Конечно, их можно использовать, чтобы получить нормальные сертификаты, но у меня есть определенные ограничения в виде корпоративной сети, из которой далеко не всегда есть доступ к центру сертификации, выдавшему сертификат. Да и требовать заводить доменные имена во внутренней сети для продакшена в моем случае это лишняя головная боль. Еще один сервер для Web UI на Linux, например, с проксей в виде Nginx в силу разных причин мы тоже не рассматриваем.
Выводы
Для решения данной проблемы нашлось два пути:
- Проксирование с прямого адреса по части url (при помощи Apache или Nginx)
- Редирект с HTTPS-интерфейса WS-сервера на основной Web UI с подтверждением сертификатов два раза
Буду рад любым комментариям, может есть еще какие решения.
UPDATE
для Node.JS все-таки получилось сделать проксирование WS запросов (я брал node-http-proxy) по части урла (спасибо xdenser) средствами самого Node.JS на событие Upgrade, причем проксирование оптимально добавить прямо в http-сервер Web UI, чтобы не плодить компоненты, примерно таким образом:
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({});
function proxyWebsocketResponse(req, res, head) {
try {
var hostname = req.headers.host.split(":")[0];
var pathname = url.parse(req.url).pathname;
if (pathname === config.comet.websocket.path) {
var options = {
target: 'ws://' + config.comet.websocket.host + ':' + config.comet.websocket.port + '/',
ws: true
};
proxy.ws(req, res, head, options);
proxy.on('error', function(e) {
console.log('WebSocket error: ' + e.message);
res.end();
});
} else {
res.statusCode = 501;
res.end('Not Implemented');
}
} catch (e) {
console.log('Error: ' + e.message);
}
}
httpServer.addListener('upgrade', proxyWebsocketResponse);