Мы продолжаем рассказывать про миграцию мобильного сервиса в ASP.NET Core и Docker. В этой статье будет идти речь про модуль WCF-клиента, упомянутый в предыдущей статье, NTLM-авторизацию и другие проблемы при его миграции. Сейчас расскажем, почему нам пришлось немного изучить анатомию и пощупать .NET Core изнутри.  Первым делом мы настроили дебаг в docker-образ и локально запустили сервис в windows-контейнере.  При попытке отправки запроса в WCF-сервис получили весьма витиеватую ошибку: System.ServiceModel.Security.MessageSecurityException: The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate TlRMTVNTUAACAAAAEAAQADgAA... Методом проб вышли на то, что в креденшиалах сервиса требуется указывать Domain. Смешно, что можно указать любое значение, лишь бы не null — тогда работает.   Всё, теперь запросы ходят, теперь в Windows-контейнере дела ок. Едем дальше. Переключившись на сборку в Linux-контейнер, ради интереса убрали значение Domain — и оно работает.  Первая проблема при отправке запросов в WCF связана с SSL. Ругается так: System.Net.Http.CurlException SSL peer certificate or SSH remote key was not OK Что означает: нет доверия к сертификату. Если бы WCF-сервис присылал не только конечный сертификат, но и все промежуточные, проблемы бы не было. Как решили: 1. Выкачиваем промежуточные сертификаты. Например, в Chrome открываем ссылку и идем в F12 во вкладку Security. Дальше View Certificate → Certification Path. Для каждого сертификата открываем View Certificate и на вкладке Details по кнопке Copy To File сохраняем Base-64 encoded сертификаты в директорию проекта. Расширение файлов нужно поменять на .crt. 2. Дописываем в Dockerfile новый слой. Для дебага и экспериментов можно и просто временно отключить SSL-валидацию: Самое полезное и важное, что мы узнали, получив CurlException — то, что для сетевых запросов используется libcurl. Вкусная часть ждала нас впереди.  Теперь дорогу преградил такой эксепшн MessageSecurityException The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate, NTLM'. Меняем Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows на HttpClientCredentialType.Ntlm Ошибка несколько изменилась, но легче не стало: MessageSecurityException The HTTP request is unauthorized with client authentication scheme 'Ntlm'. The authentication header received from the server was 'Negotiate, NTLM'. Давайте убедимся, что мы не столкнулись с еще одной docker-related особенностью наподобие значения Domain. Запускаем сервис в виртуалке с Ubuntu LTS. И по делу: на чистом Linux ошибка ровно та же самая, что в контейнере, а значит, проблема железно кроется в реализации WCF-клиента. Пробуем зайти с другой стороны. Из командной строки запускаем: 1. curl -v --negotiate — завершается ошибкой 2. curl -v --ntlm — все хорошо, запрос работает Здесь самое время вспомнить список официально поддерживаемых фич в WCF. В искомой строчке написано, что Core на Linux не умеет в NTLM. Но это никак не клеется с тем, что curl-то умеет, и было бы странно не реализовать настолько популярный вариант. Из комментария в интернете узнаем, что Negotiate Negotiate-у рознь: в определенных реализациях есть поддержка fallback Kerberos→NTLM (повсеместно на винде), а в других — нет. Curl из последних, и Negotiate становится препятствием. Все это наводит на мысль, что HttpClient может не учитывать этот нюанс, а значит, надежда на победу есть. И вот здесь нельзя не порадоваться за новый Microsoft за их решение открыть код миру. В сорцах находим ключик CURLHANDLER_DEBUG_VERBOSE=true, который нам расcкажет, чем занимается libcurl в момент выполнения WCF-запросов. В логах видим уже знакомую ошибку gss_init_sec_context() failed и для HttpClientCredentialType.Windows, и для HttpClientCredentialType.Ntlm. Теперь понятно, что WCF-клиент не реагирует на переключение с Windows-авторизации на NTLM и пытается использовать Negotiate в обоих случаях. Скорее всего, это происходит из-за двойного хедера WWW-Authentication 'Negotiate, NTLM', который присылает WCF сервис, и поскольку Negotiate является более сильной авторизацией, то он и используется.  Из мануала libcurl вкурили, что тип авторизации задается через опцию CURLOPT_HTTPAUTH. По этому следу мы вышли на таблицу выбора авторизации: Атрибуты static readonly выглядят особенно соблазнительно, поскольку это означает, что достаточно поиграться при помощи Reflection со значениями в таблице на старте сервиса, и при HTTP-запросах не будет никакого оверхеда. Добавили в Program.cs следующий код: Здесь мы прибиваем гвоздями соответствие между "Negotiate" и CURLAUTH.NTLM. Вуаля, теперь запросы срабатывают успешно. Мы не остановились на достигнутом. Если внимательно посмотреть на логи, то видно, что один WCF запрос-ответ включает в себя несколько запросов-ответов HTTP, и один из ответов стабильно возвращается с Bad Request. В чем дело? Для ошибочного запроса используется метод HEAD. И действительно, такое же поведение легко эмулируется с curl -I. В libcurl это соответствует опции CURLOPTION_NOBODY. В corefx эта опция используется при отправке HttpMethod.Head реквестов.
Поднимаемся по стеку выше в WCF. Видим, что в методе SendPreauthenticationHeadRequestIfNeeded отправляется HEAD запрос для авторизации, а все ошибки просто игнорируются: Здесь явно напрашивается флаг, подобный HttpClientHandler.PreAuthenticate, чтобы не запускать запрос, заранее обреченный на 400. Раз уж о нас не позаботились, значит, будем резать. Метод SendPreauthenticationHeadRequestIfNeeded асинхронный, поэтому его патчинг может привести к красноглазию в слишком раннем возрасте. Если оглядеться по сторонам, то можно заметить простой и неприхотливый метод AuthenticationSchemeMayRequireResend. Очевидно, если он будет возвращать всегда false, то и не будет запускаться SendPreauthenticationHeadRequestIfNeeded. Приступаем к операции.  Добавляем в решение новый проект WcfPreauthPatch. Теперь ставим Cecil, при помощи которого полезем в IL-код. Нужна бета-версия, чтобы работало под .NET Core. Install-Package Mono.Cecil -Version 0.10.0-beta7 -ProjectName WcfPreauthPatch Код такой: В Dockerfile допишем Запускаем сервис и убеждаемся, что в логах одним запросом стало меньше. WCF-клиент в .NET Core доставил нам немало хлопот.  На github уже есть обсуждение поднятых в статье проблем и вопросов: 1. Negotiate/NTLM https://github.com/dotnet/corefx/issues/9533
https://github.com/dotnet/corefx/issues/9234 2. Preauthentication-запрос https://github.com/dotnet/wcf/issues/2433 Однако, как мы увидели, эти проблемы не решены полностью. Надеемся, что наши 5 копеек в обсуждении добавят процессу новый оборот. При осутствии интеграции с docker патчинг можно запускать как postbuild target.  Существуют NTLM-прокси, например, CNTLM. Альтернативный путь с настройкой NTLM-прокси внутри контейнера также имеет перспективы и более универсален, а готовый настроенный образ будет достоен выкладки на Docker Hub. Гипотетически можно попробовать править хедер авторизации WWW-Authentication, который приходит от WCF-сервиса. Нужно переопределить поведение WCF-клиента через IEndpointBehavior и метод AfterReceiveReply. Однако, это сработает только в случае, если preauthentication запрос выключен, т.к. AfterReceiveReply его не поймает. Если вы используете/имеете доступ к HttpClient, то вот ссылочка на workaround для похожей проблемы с NTLM.  Пропатчить CurlHandler при помощи Сecil не получится: System.Net.Http.dll — это mixed mode assembly (т.е. либа с managed и native кодом), и такой вариант в Cecil пока что не поддерживается. Подмена указателя на метод в рантайме, описанная в статье, не работает, не пытайтесь.  Создаем цифровые продукты