Взаимодействие сайта в браузере и локально запущенной программы

в 18:24, , рубрики: .net, angular, bouncycastle, C#, HTTPS, nancy, TypeScript, Разработка веб-сайтов

Иногда возникает необходимость передать данные между работающим в браузере приложением и программой, выполняющейся на той же системе, на которой запущен браузер. Это может понадобиться, например, если нам нужно поработать с оборудованием, подключенным локально. Считывателем смарт-карт, аппаратным ключом шифрования и так далее.

Взаимодействие сайта в браузере и локально запущенной программы - 1

Картинка отсюда

Первыми приходят в голову три способа решить эту задачу:

  1. Обойтись средствами браузеров, или написать плагины к ним
  2. Организовать обмен данными через backend, выступающий в роли посредника
  3. Добавить в программу HTTP-сервис, и обращаться к ней напрямую из браузера

Третий пункт выглядит хорошо, позволяет убрать авторизацию в программе, не требует вообще никакого пользовательского интерфейса. Попробуем его реализовать, написав программу на C# под .NET Framework 4. Так как речь пойдет о .NET, решение будет только для Windows (XP и новее). Веб-приложение сделаем на angular.

Почему не 1 и 2?

Первый пункт точно принесёт много боли, поддерживать браузеры придётся отдельно, сделать в плагинах для браузера можно далеко не всё. Всё же, теоретически, поработать со смарт-картами через плагины возможно. Но нужен способ проще.

Второй пункт просто реализовать, но для этой схемы придется делать авторизацию не только на сайте, но и в локальном приложении. Значит понадобится какой-никакой, но интерфейс, при смене пароля потребуется повторная авторизация и в программе. Плюс в корпоративных сетях будут дополнительные проблемы с сетью, у них часто доступ в интернет реализован через прокси-серверы с суровой фильтрацией и авторизацией, для настройки прокси тоже придётся делать интерфейс, не всегда можно отделаться автоматическим определением настроек. Далёкому от IT пользователю будет сложнее с этим работать, создадим больше работы техподдержке. Конечно, можно формировать установочный пакет индивидуально для каждого пользователя, чтобы убрать необходимость первичной авторизации, но это только добавит проблем.

При чем тут HTTPS?

Когда сайт работает по HTTPS, браузеры блокируют загрузку активного содержимого с помощью HTTP. Однако, по логике вещей, запрос к локальной машине по HTTP браузеры должны считать безопасным, и не должны блокировать его. Это оказалось не совсем так.
Таблица показывает результаты небольшого исследования поведения браузеров на платформе Windows:

Firefox 65 Chrome 72 IE 11
http://localhost/ ❌ Blocked loading mixed active content ❌ Error: Access is denied 0x8007005
http://127.0.0.1/ ❌ Error: Access is denied 0x8007005
https://localhost/ ❌ SEC_ERROR_UNKNOWN_ISSUER

В таблице приведено поведение браузеров при попытке сделать запрос по соответствующему адресу. Браузеры на движке Chromium ведут себя аналогично Chrome, а поведение Edge 44 аналогично поведению IE 11. Для HTTPS выпущен валидный сертификат, подписанный самоподписанным корневым сертификатом. Поведение для https://127.0.0.1 и https://localhost одинаковое, просто для 127.0.0.1 тогда нужно тоже выпускать сертификат, а сертификаты для IP адресов редко встречаются, так что опустим этот момент.

В Chrome всё работает. Chrome и IE используют системное хранилище сертификатов, поэтому в них работает и HTTPS. Firefox использует собственное хранилище сертификатов, поэтому не доверяет нашему самоподписанному сертификату. Firefox и IE не доверяют имени localhost, и это правильно, ведь никто не гарантирует, что оно разрешится в 127.0.0.1 (хотя они могли просто это проверить, как делает Chrome).

Главная проблема: IE не даёт обратиться к программе по HTTP. Значит возни с сертификатами нам не избежать.

Для работы с браузерами также потребуется указывать в программе правильные заголовки Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers (CORS).

SSL сертификат

Можно сделать DNS-запись для своего домена, например local.example.com, которая будет разрешаться в 127.0.0.1. Выпустить для этого домена SSL сертификат, распространять его вместе с программой. Придётся распространять закрытый ключ этого сертификата вместе с программой. Это совершенно не годится. А сертификат в программе еще и нужно будет обновлять.

IE не будет доверять самоподписанному SSL сертификату, его надо подписать доверенным корневым сертификатом (а он может быть и самоподписанный).

Можно сгенерировать корневой сертификат и SSL сертификат и распространять их с программой, добавляя в локальное хранилище сертификатов. Выглядит небезопасно. И тоже может возникнуть необходимость отозвать или обновить сертификат. Поэтому, сертификаты с ключами будем генерировать прямо на компьютере пользователя при первом запуске программы.

Создание сертификатов в C#

Для .NET есть библиотека BouncyCastle, умеющая всё что нам нужно. Единственная проблема – для добавления сертификата в хранилище придётся запросить повышение прав. Также повышенные права понадобятся, чтобы с помощью netsh закрепить сертификат за определённым портом в системе.

netsh http add sslcert ipport=0.0.0.0:{PORT} certhash={certThumbprint}

В примере эту работу делает метод RegisterSslOnPort в классе SslHelper.

HTTP-сервис в программе на C#

Для создания легковесного HTTP(S)-сервера воспользуемся библиотекой Nancy. Nancy – это легкий веб-фреймворк для .NET, простой и удобный в работе. Про него много написано, в том числе и на Хабре. Благодаря модулю Nancy.SelfHosting мы можем хостить наше приложение без использования IIS.

Для примера создадим endpoint, занимающийся сложением двух чисел. Важно тут установить правильные заголовки CORS, иначе браузер не будет выполнять запрос к нашему API.

NancyModule

public class CalcNancyModule : NancyModule
{
    public CalcNancyModule()
    {
        //настроим заголовки, без этого не будет работать
        After.AddItemToEndOfPipeline((ctx) => ctx.Response
            .WithHeader("Access-Control-Allow-Origin", GetOrigin(ctx))
            .WithHeader("Access-Control-Allow-Methods", "POST,GET")
            .WithHeader("Access-Control-Allow-Headers", "Accept, Origin, Content-type"));

        Get["/Calc"] = _ =>
        {
            //здесь вернём строку с номером версии .NET сборки
        };
        Get["/Calc/Add"] = _ =>
        {
            //а здесь вернём сумму двух параметров запроса (num1 + num2)
        };
    }
    private string GetOrigin(NancyContext ctx)
    {
        //возвращаем Origin, с которого пришёл запрос
        //ТОЛЬКО для тестовых целей
        //в реальных приложениях нужно возвращать адрес веб-приложения
        //например https://app.example.com
        return ctx.Request?.Headers["Origin"]?.FirstOrDefault() ?? "";
    }
}

Добавим инициализацию Nancy в наше приложение, и мы готовы к бою.

Инициализация Nancy

var hostConfigs = new HostConfiguration();
hostConfigs.UrlReservations.CreateAutomatically = true;
hostConfigs.RewriteLocalhost = false;

var uris = new Uri[]
{
    new Uri($"http://localhost:{HTTPPORT}"),
    new Uri($"http://127.0.0.1:{HTTPPORT}"),
    new Uri($"https://localhost:{HTTPSPORT}")
};

using (var host = new NancyHost(hostConfigs, uris)) 
{
    host.Start();
}

При первом запуске нужно сгенерировать сертификаты и поместить их в хранилище, запросив при этом соответствующие права. Для этих манипуляций служит класс SslHelper, в котором единственный публичный метод CheckOrCreateCertificates делает эту работу. В качестве параметров ему передаются SubjectName сертификатов. Метод проверяет нет ли нужных сертификатов и системе, если нет — создаёт их.

Для симуляции тяжелой работы и долгих задержек в примере добавим Thread.Sleep(1000) в вызовы нашего API.

На этом приложение готово к запуску, перейдём к вебу.

Веб-приложение

Как понятно из таблицы поведения браузеров, каким-то одним эндпоинтом обойтись не получится, придётся использовать как минимум два:

В веб-приложении нам нужно определить, если мы в IE (или Edge) – использовать HTTPS, если нет – HTTP. Можно сделать надёжнее и не выяснять в каком мы браузере, а просто попробовать выполнить запрос к методу GET /Calc нашего API, если запрос успешен – работаем, если нет – пробуем другой протокол.

Всё это нужно только если веб-приложение само использует HTTPS, потому что при использовании протокола HTTP, браузеры не накладывают ограничений на запросы, нужны только правильные заголовки CORS.

В angular – приложении создадим сервис InteractionService, который будет выполнять проверку доступности локального эндпоинта сначала по HTTP, потом по HTTPS. Проверку выполняет метод checkAvailability, а результат проверки доступен при подписке на переменную available$ типа BehaviorSubject с начальным значением false.

Работу по сложению чисел поместим в компонент AppComponent. При нажатии кнопки “Calculate”, веб-приложение делает запрос к GET /Calc/Add?num1={num1}&num2={num2}. Ответ или ошибка отображается в поле Result.

При отладке, даже по HTTPS, можно не заметить проблем, так как домен для запросов будет один и тот же – localhost. Поэтому тестировать приложение нужно с другим доменным именем.
Чтобы максимально упростить работу по развертыванию веб-приложения воспользуемся сервисом https://stackblitz.com, это веб-IDE для angular и не только, со вкусом VSCode. Готовое приложение доступно по ссылке.

А поковырять код можно здесь.

В интерактивном режиме на stackblitz приложение не будет работать, нужно открыть его в отдельной приватной вкладке, или в другом браузере по адресу https://angular-pfwfrm.stackblitz.io.

Как попробовать?

Веб-приложение удобно запустить с помощью stackblitz, просто перейдя по ссылке https://angular-pfwfrm.stackblitz.io.

Можно запустить веб-приложение локально.

Для этого нужно

для этого нужно клонировать репозиторий:

git clone https://github.com/jdtcn/InteractionExample.git
cd InteractionExample

в папке AngularWebApp нужно выполнить команды:

npm install
ng serve  --ssl true

Веб-приложение будет доступно по адресу https://localhost:4200/

Локальное приложение можно либо скомпилировать из примера (открыть CsClientApp.sln из папки CsClientApp) с помощью Visual Studio и запустить, либо использовать скрипт для программы LINQPad.

Если вы .NET-разработчик, и не пользуетесь LINQPad, обязательно почитайте про него, незаменимая вещь в разработке. Чтобы запустить пример, нужно открыть в LINQPad’e скрипт (первый раз нужно запустить LINQPad с правами администратора, чтобы установились сертификаты), и установить nuget-пакеты BouncyCastle, Nancy, Nancy.Hosting.Self, потом запустить скрипт. После этого можно нажать кнопку “Calculate” в веб-приложении, и получить результат выполнения операции.

Безопасность

Важно правильно формировать заголовки CORS в реальном приложении, чтобы злодеи с других сайтов не могли обратиться к нашей программе. Если же у злодея есть возможность работать с привилегиями пользователя на его компьютере и обходить проверку CORS, значит он и так сможет сделать всё, что может делать наша программа.

В любом случае, программа должна работать с минимальными правами, а если делает что-то чувствительное с документами, надо добавить в неё запросы на подтверждение операций.

Вывод

Несложная на вид задача на деле оказалась довольно объемной, да еще и требующей дополнительных костылей для работы с сертификатами.

Этот подход хорошо себя показал в реальном приложении. Конечно, чтобы использовать код из примера, нужно добавить нормальную обработку ошибок.

Повышение привилегий удобно запрашивать при установке программы, с помощью InnoSetup это легко сделать, и передать при первом запуске программе нужный атрибут. Также перед установкой удобно проверять наличие .NET 4, и устанавливать его, если не установлен.

Никто на Virustotal на эту программу не реагирует, а хотелось бы! Зато если собрать установочный пакет в InnoSetup, пара третьесортных антивирусов начинает на него срабатывать. Помогает от этого избавиться подписывание установщика с помощью code signing certificate.

Автоматическое обновление программы здесь оставлено за кадром, но оно точно не будет лишним в реальном приложении. Для управления автоматическим обновлением хорошо подходит Squirrel. Еще с помощью squirrel удобно удалить наши сертификаты из системы при удалении программы.

Код примера выложен на GitHub.

Спасибо за внимание!

Автор: sergeyZ

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js