From 4903cfd4842ddda5152d8b1cc8a2b572fda3299b Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 7 Feb 2023 15:13:10 -0600 Subject: [PATCH] AzureConnection: remove our dependency on cpprestsdk (#14776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request removes, in full, our dependency on cpprestsdk. This allows us to shed 500KiB-1.2MiB from our package off the top and enables the following future investments: - Removal of the App CRT forwarders to save an additional ~500KiB - Switching over to the HybridCRT and removing our dependency on _any CRT_. cpprest was built on my dev box two or so years ago, and is in _utter_ violation of our compliance guidelines on SBOM et al. In addition, this change allows us to use the proxy server configured in Windows Settings. I did this in four steps (represented roughly by the individual commits): 1. Switch from cpprest's http_client/json to Windows.Web.Http and Windows.Data.Json 2. Switch from websocketpp to winhttp.dll's WebSocket implementation¹ 3. Remove all remaining utility classes 4. Purge all dependencies from all projects and scripts on cpprest. I also took this opportunity to add a feature flag that allows Dev builds to run AzureConnection in-process. ¹ Windows.Networking.Sockets' API is so unergonomic that it was simply infeasible (and also _horrible_) to use it. ## Validation Steps I've run the Azure Connection quite a bit inproc. Closes #4575. Might be related to #5977, #11714, and with the user agent thing maybe #14403. --- .github/actions/spelling/allow/apis.txt | 5 + .github/actions/spelling/expect/expect.txt | 3 - build/config/ESRPSigning_Terminal.json | 45 -- build/pipelines/release.yml | 2 +- .../build-console-compliance-job.yml | 2 +- build/scripts/Test-WindowsTerminalPackage.ps1 | 5 - dep/nuget/packages.config | 1 - .../WindowExe/WindowExe.vcxproj | 20 - src/cascadia/TerminalApp/TerminalPage.cpp | 11 +- src/cascadia/TerminalConnection/AzureClient.h | 22 +- .../TerminalConnection/AzureConnection.cpp | 425 +++++++++--------- .../TerminalConnection/AzureConnection.h | 42 +- .../TerminalConnection.vcxproj | 36 -- src/cascadia/TerminalConnection/pch.h | 6 + src/common.nugetversions.targets | 6 - src/features.xml | 10 + 16 files changed, 293 insertions(+), 348 deletions(-) diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index e0cc755009..ce36d8d2a2 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -55,6 +55,8 @@ GETMOUSEHOVERTIME Hashtable HIGHCONTRASTON HIGHCONTRASTW +hinternet +HINTERNET hotkeys href hrgn @@ -214,6 +216,8 @@ Viewbox virtualalloc wcsstr wcstoui +WDJ +winhttp winmain winsta winstamin @@ -221,6 +225,7 @@ wmemcmp wpc WSF wsregex +WWH wwinmain xchg XDocument diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 9c7ef11a7d..0c46903dbe 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -311,7 +311,6 @@ CPLINFO cplusplus CPPCORECHECK cppcorecheckrules -cpprest cpprestsdk cppwinrt CProc @@ -1437,7 +1436,6 @@ PPEB ppf ppguid ppidl -pplx PPROC PPROCESS ppropvar @@ -2114,7 +2112,6 @@ WDDMCONSOLECONTEXT wdm webpage websites -websockets wekyb wex wextest diff --git a/build/config/ESRPSigning_Terminal.json b/build/config/ESRPSigning_Terminal.json index 715848a42c..01780c2df3 100644 --- a/build/config/ESRPSigning_Terminal.json +++ b/build/config/ESRPSigning_Terminal.json @@ -67,51 +67,6 @@ } ] } - }, - { - // THIRD PARTY SOFTWARE - "MatchedPath": [ - "cpprest*.dll" - ], - "SigningInfo": { - "Operations": [ - { - "KeyCode": "CP-231522", - "OperationSetCode": "SigntoolSign", - "Parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "Microsoft" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-231522", - "OperationSetCode": "SigntoolVerify", - "Parameters": [], - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - } } ] } diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml index 2f3c6992c5..2fd0f8757d 100644 --- a/build/pipelines/release.yml +++ b/build/pipelines/release.yml @@ -64,7 +64,7 @@ parameters: variables: MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\MakeAppx.exe' - TerminalInternalPackageVersion: "0.0.7" + TerminalInternalPackageVersion: "0.0.8" # If we are building a branch called "release-*", change the NuGet suffix # to "preview". If we don't do that, XES will set the suffix to "release1" # because it truncates the value after the first period. diff --git a/build/pipelines/templates/build-console-compliance-job.yml b/build/pipelines/templates/build-console-compliance-job.yml index d5042cdb17..0f885cfcce 100644 --- a/build/pipelines/templates/build-console-compliance-job.yml +++ b/build/pipelines/templates/build-console-compliance-job.yml @@ -144,7 +144,7 @@ jobs: inputs: TargetPattern: guardianGlob # See https://aka.ms/gdn-globs for how to do match patterns - AnalyzeTargetGlob: $(Build.SourcesDirectory)\bin\**\*.dll;$(Build.SourcesDirectory)\bin\**\*.exe;-:file|**\Microsoft.UI.Xaml.dll;-:file|**\Microsoft.Toolkit.Win32.UI.XamlHost.dll;-:file|**\vcruntime*.dll;-:file|**\vcomp*.dll;-:file|**\vccorlib*.dll;-:file|**\vcamp*.dll;-:file|**\msvcp*.dll;-:file|**\concrt*.dll;-:file|**\TerminalThemeHelpers*.dll;-:file|**\cpprest*.dll + AnalyzeTargetGlob: $(Build.SourcesDirectory)\bin\**\*.dll;$(Build.SourcesDirectory)\bin\**\*.exe;-:file|**\Microsoft.UI.Xaml.dll;-:file|**\Microsoft.Toolkit.Win32.UI.XamlHost.dll;-:file|**\vcruntime*.dll;-:file|**\vcomp*.dll;-:file|**\vccorlib*.dll;-:file|**\vcamp*.dll;-:file|**\msvcp*.dll;-:file|**\concrt*.dll;-:file|**\TerminalThemeHelpers*.dll continueOnError: true # Set XES_SERIALPOSTBUILDREADY to run Security and Compliance task once per build diff --git a/build/scripts/Test-WindowsTerminalPackage.ps1 b/build/scripts/Test-WindowsTerminalPackage.ps1 index aa9cca6d10..a33329c26c 100644 --- a/build/scripts/Test-WindowsTerminalPackage.ps1 +++ b/build/scripts/Test-WindowsTerminalPackage.ps1 @@ -96,11 +96,6 @@ Try { Throw "Failed to find App.xbf (TerminalApp project) in resources.pri" } - If (($null -eq (Get-Item "$AppxPackageRootPath\cpprest142_2_10.dll" -EA:Ignore)) -And - ($null -eq (Get-Item "$AppxPackageRootPath\cpprest142_2_10d.dll" -EA:Ignore))) { - Throw "Failed to find cpprest142_2_10.dll -- check the WAP packaging project" - } - If (($null -eq (Get-Item "$AppxPackageRootPath\wtd.exe" -EA:Ignore)) -And ($null -eq (Get-Item "$AppxPackageRootPath\wt.exe" -EA:Ignore))) { Throw "Failed to find wt.exe/wtd.exe -- check the WAP packaging project" diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index 0a62482b48..935d004a72 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -5,7 +5,6 @@ - diff --git a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj index e17243bfc6..de4c7a3435 100644 --- a/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj +++ b/scratch/ScratchIslandApp/WindowExe/WindowExe.vcxproj @@ -197,26 +197,6 @@ - - - - <_TerminalConnectionDlls Include="$(OpenConsoleCommonOutDir)\TerminalConnection\*.dll" /> - - - $(ProjectName) - BuiltProjectOutputGroup - %(Filename)%(Extension) - - - - <_WindowsTerminalExe Include="$(OpenConsoleCommonOutDir)\WindowsTerminal\*.exe" /> diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index cf4f0140a2..94e9865e63 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1247,10 +1247,17 @@ namespace winrt::TerminalApp::implementation if (connectionType == TerminalConnection::AzureConnection::ConnectionType() && TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) { - // TODO GH#4661: Replace this with directly using the AzCon when our VT is better std::filesystem::path azBridgePath{ wil::GetModuleFileNameW(nullptr) }; azBridgePath.replace_filename(L"TerminalAzBridge.exe"); - connection = TerminalConnection::ConptyConnection(); + if constexpr (Feature_AzureConnectionInProc::IsEnabled()) + { + connection = TerminalConnection::AzureConnection{}; + } + else + { + connection = TerminalConnection::ConptyConnection{}; + } + auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.wstring(), L".", L"Azure", diff --git a/src/cascadia/TerminalConnection/AzureClient.h b/src/cascadia/TerminalConnection/AzureClient.h index a036212555..f8ca9c0823 100644 --- a/src/cascadia/TerminalConnection/AzureClient.h +++ b/src/cascadia/TerminalConnection/AzureClient.h @@ -3,8 +3,6 @@ #pragma once -#include "cpprest/json.h" - namespace Microsoft::Terminal::Azure { class AzureException : public std::runtime_error @@ -12,14 +10,24 @@ namespace Microsoft::Terminal::Azure std::wstring _code; public: - static bool IsErrorPayload(const web::json::value& errorObject) + static bool IsErrorPayload(const winrt::Windows::Data::Json::JsonObject& errorObject) { - return errorObject.has_string_field(L"error"); + if (!errorObject.HasKey(L"error")) + { + return false; + } + + if (errorObject.GetNamedValue(L"error").ValueType() != winrt::Windows::Data::Json::JsonValueType::String) + { + return false; + } + + return true; } - AzureException(const web::json::value& errorObject) : - runtime_error(til::u16u8(errorObject.at(L"error_description").as_string())), // surface the human-readable description as .what() - _code(errorObject.at(L"error").as_string()) + AzureException(const winrt::Windows::Data::Json::JsonObject& errorObject) : + runtime_error(til::u16u8(errorObject.GetNamedString(L"error_description"))), // surface the human-readable description as .what() + _code(errorObject.GetNamedString(L"error")) { } diff --git a/src/cascadia/TerminalConnection/AzureConnection.cpp b/src/cascadia/TerminalConnection/AzureConnection.cpp index f04e08bd74..3844979b48 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.cpp +++ b/src/cascadia/TerminalConnection/AzureConnection.cpp @@ -15,22 +15,24 @@ #include "winrt/Windows.System.UserProfile.h" #include "../../types/inc/Utils.hpp" +#include "winrt/Windows.Web.Http.Filters.h" + using namespace ::Microsoft::Console; using namespace ::Microsoft::Terminal::Azure; - -using namespace web; -using namespace web::http; -using namespace web::http::client; -using namespace web::websockets::client; using namespace winrt::Windows::Security::Credentials; -static constexpr int CurrentCredentialVersion = 1; -static constexpr auto PasswordVaultResourceName = L"Terminal"; -static constexpr auto HttpUserAgent = L"Terminal/0.0"; +static constexpr int CurrentCredentialVersion = 2; +static constexpr std::wstring_view PasswordVaultResourceName = L"Terminal"; +static constexpr std::wstring_view HttpUserAgent = L"Mozilla/5.0 (Windows NT 10.0) Terminal/1.0"; static constexpr int USER_INPUT_COLOR = 93; // yellow - the color of something the user can type static constexpr int USER_INFO_COLOR = 97; // white - the color of clarifying information +using namespace winrt::Windows::Foundation; +namespace WDJ = ::winrt::Windows::Data::Json; +namespace WSS = ::winrt::Windows::Storage::Streams; +namespace WWH = ::winrt::Windows::Web::Http; + static constexpr winrt::guid AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62 } }; static inline std::wstring _colorize(const unsigned int colorCode, const std::wstring_view text) @@ -115,6 +117,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - creates the output thread (where we will do the authentication and actually connect to Azure) void AzureConnection::Start() { + _httpClient = winrt::Windows::Web::Http::HttpClient{}; + _httpClient.DefaultRequestHeaders().UserAgent().TryParseAdd(HttpUserAgent); // Create our own output handling thread // Each connection needs to make sure to drain the output from its backing host. _hOutputThread.reset(CreateThread( @@ -126,7 +130,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { return pInstance->_OutputThread(); } - return gsl::narrow_cast(E_INVALIDARG); + return gsl::narrow(E_INVALIDARG); }, this, 0, @@ -183,12 +187,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation if (_state == AzureState::TermConnected) { - // If we're connected, we don't need to do any fun input shenanigans. - websocket_outgoing_message msg; - const auto str = winrt::to_string(data); - msg.set_utf8_message(str); - - _cloudShellSocket.send(msg).get(); + auto buff{ winrt::to_string(data) }; + WinHttpWebSocketSend(_webSocket.get(), WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, buff.data(), gsl::narrow(buff.size())); return; } @@ -238,16 +238,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } else // We only transition to Connected when we've established the websocket. { - // Initialize client - http_client terminalClient(_cloudShellUri); + auto uri{ fmt::format(L"{}terminals/{}/size?cols={}&rows={}&version=2019-01-01", _cloudShellUri, _terminalID, columns, rows) }; - // Initialize the request - http_request terminalRequest(L"POST"); - terminalRequest.set_request_uri(fmt::format(L"terminals/{}/size?cols={}&rows={}&version=2019-01-01", _terminalID, columns, rows)); - terminalRequest.set_body(json::value::null()); + WWH::HttpStringContent content{ + L"", + WSS::UnicodeEncoding::Utf8, + // LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'" + L"application/json" + }; // Send the request (don't care about the response) - (void)_SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest); + std::ignore = _SendRequestReturningJson(uri, content); } } CATCH_LOG(); @@ -264,7 +265,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation if (_state == AzureState::TermConnected) { // Close the websocket connection - _cloudShellSocket.close(); + std::ignore = WinHttpWebSocketClose(_webSocket.get(), WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0); // throw away the error + _webSocket.reset(); + _socketConnectionHandle.reset(); + _socketSessionHandle.reset(); } if (_hOutputThread) @@ -287,44 +291,46 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - tenant - the unparsed tenant // Return value: // - a tuple containing the ID and display name of the tenant. - static Tenant _crackTenant(const json::value& jsonTenant) + static Tenant _crackTenant(const WDJ::IJsonValue& value) { + auto jsonTenant{ value.GetObjectW() }; + Tenant tenant{}; - if (jsonTenant.has_string_field(L"tenantID")) + if (jsonTenant.HasKey(L"tenantID")) { // for compatibility with version 1 credentials - tenant.ID = jsonTenant.at(L"tenantID").as_string(); + tenant.ID = jsonTenant.GetNamedString(L"tenantID"); } else { // This one comes in off the wire - tenant.ID = jsonTenant.at(L"tenantId").as_string(); + tenant.ID = jsonTenant.GetNamedString(L"tenantId"); } - if (jsonTenant.has_string_field(L"displayName")) + if (jsonTenant.HasKey(L"displayName")) { - tenant.DisplayName = jsonTenant.at(L"displayName").as_string(); + tenant.DisplayName = jsonTenant.GetNamedString(L"displayName"); } - if (jsonTenant.has_string_field(L"defaultDomain")) + if (jsonTenant.HasKey(L"defaultDomain")) { - tenant.DefaultDomain = jsonTenant.at(L"defaultDomain").as_string(); + tenant.DefaultDomain = jsonTenant.GetNamedString(L"defaultDomain"); } return tenant; } - static void _packTenant(json::value& jsonTenant, const Tenant& tenant) + static void _packTenant(const WDJ::JsonObject& jsonTenant, const Tenant& tenant) { - jsonTenant[L"tenantId"] = json::value::string(tenant.ID); + jsonTenant.SetNamedValue(L"tenantId", WDJ::JsonValue::CreateStringValue(tenant.ID)); if (tenant.DisplayName.has_value()) { - jsonTenant[L"displayName"] = json::value::string(*tenant.DisplayName); + jsonTenant.SetNamedValue(L"displayName", WDJ::JsonValue::CreateStringValue(*tenant.DisplayName)); } if (tenant.DefaultDomain.has_value()) { - jsonTenant[L"defaultDomain"] = json::value::string(*tenant.DefaultDomain); + jsonTenant.SetNamedValue(L"defaultDomain", WDJ::JsonValue::CreateStringValue(*tenant.DefaultDomain)); } } @@ -383,35 +389,42 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation case AzureState::TermConnected: { _transitionToState(ConnectionState::Connected); + while (true) { - // Read from websocket - pplx::task msgT; - try + WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType{}; + DWORD read{}; + THROW_IF_WIN32_ERROR(WinHttpWebSocketReceive(_webSocket.get(), _buffer.data(), gsl::narrow(_buffer.size()), &read, &bufferType)); + + switch (bufferType) { - msgT = _cloudShellSocket.receive(); - msgT.wait(); + case WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE: + case WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE: + { + const auto result{ til::u8u16(std::string_view{ _buffer.data(), read }, _u16Str, _u8State) }; + if (FAILED(result)) + { + // EXIT POINT + _transitionToState(ConnectionState::Failed); + return gsl::narrow(result); + } + + if (_u16Str.empty()) + { + continue; + } + + // Pass the output to our registered event handlers + _TerminalOutputHandlers(_u16Str); + break; } - catch (...) - { - // Websocket has been closed; consider it a graceful exit? - // This should result in our termination. + case WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE: + // EXIT POINT if (_transitionToState(ConnectionState::Closed)) { - // End the output thread. return S_FALSE; } } - - auto msg = msgT.get(); - auto msgStringTask = msg.extract_string(); - auto msgString = msgStringTask.get(); - - // Convert to hstring - const auto hstr = winrt::to_hstring(msgString); - - // Pass the output to our registered event handlers - _TerminalOutputHandlers(hstr); } return S_OK; } @@ -449,25 +462,29 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation _tenantList.clear(); for (const auto& entry : credList) { - auto nameJson = json::value::parse(entry.UserName().c_str()); - std::optional credentialVersion; - if (nameJson.has_integer_field(U("ver"))) + try { - credentialVersion = nameJson.at(U("ver")).as_integer(); + auto nameJson = WDJ::JsonObject::Parse(entry.UserName()); + std::optional credentialVersion; + if (nameJson.HasKey(L"ver")) + { + credentialVersion = static_cast(nameJson.GetNamedNumber(L"ver")); + } + + if (!credentialVersion.has_value() || credentialVersion.value() != CurrentCredentialVersion) + { + // ignore credentials that aren't from the latest credential revision + vault.Remove(entry); + oldVersionEncountered = true; + continue; + } + + auto newTenant{ _tenantList.emplace_back(_crackTenant(nameJson)) }; + + _WriteStringWithNewline(_formatTenant(numTenants, newTenant)); + numTenants++; } - - if (!credentialVersion.has_value() || credentialVersion.value() != CurrentCredentialVersion) - { - // ignore credentials that aren't from the latest credential revision - vault.Remove(entry); - oldVersionEncountered = true; - continue; - } - - auto newTenant{ _tenantList.emplace_back(_crackTenant(nameJson)) }; - - _WriteStringWithNewline(_formatTenant(numTenants, newTenant)); - numTenants++; + CATCH_LOG(); } if (!numTenants) @@ -533,11 +550,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // User wants to login with one of the saved connection settings auto desiredCredential = credList.GetAt(selectedTenant); desiredCredential.RetrievePassword(); - auto passWordJson = json::value::parse(desiredCredential.Password().c_str()); + auto passWordJson = WDJ::JsonObject::Parse(desiredCredential.Password()); _currentTenant = til::at(_tenantList, selectedTenant); // we already unpacked the name info, so we should just use it - _accessToken = passWordJson.at(L"accessToken").as_string(); - _refreshToken = passWordJson.at(L"refreshToken").as_string(); - _expiry = std::stoi(passWordJson.at(L"expiry").as_string()); + _setAccessToken(passWordJson.GetNamedString(L"accessToken")); + _refreshToken = passWordJson.GetNamedString(L"refreshToken"); + _expiry = std::stoi(winrt::to_string(passWordJson.GetNamedString(L"expiry"))); const auto t1 = std::chrono::system_clock::now(); const auto timeNow = std::chrono::duration_cast(t1.time_since_epoch()).count(); @@ -577,17 +594,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const auto deviceCodeResponse = _GetDeviceCode(); // Print the message and store the device code, polling interval and expiry - const auto message = winrt::to_hstring(deviceCodeResponse.at(L"message").as_string().c_str()); + const auto message{ deviceCodeResponse.GetNamedString(L"message") }; _WriteStringWithNewline(message); _WriteStringWithNewline(RS_(L"AzureCodeExpiry")); - const auto devCode = deviceCodeResponse.at(L"device_code").as_string(); - const auto pollInterval = std::stoi(deviceCodeResponse.at(L"interval").as_string()); - const auto expiresIn = std::stoi(deviceCodeResponse.at(L"expires_in").as_string()); + const auto devCode = deviceCodeResponse.GetNamedString(L"device_code"); + const auto pollInterval = std::stoi(winrt::to_string(deviceCodeResponse.GetNamedString(L"interval"))); + const auto expiresIn = std::stoi(winrt::to_string(deviceCodeResponse.GetNamedString(L"expires_in"))); // Wait for user authentication and obtain the access/refresh tokens auto authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn); - _accessToken = authenticatedResponse.at(L"access_token").as_string(); - _refreshToken = authenticatedResponse.at(L"refresh_token").as_string(); + _setAccessToken(authenticatedResponse.GetNamedString(L"access_token")); + _refreshToken = authenticatedResponse.GetNamedString(L"refresh_token"); // Get the tenants and the required tenant id _PopulateTenantList(); @@ -696,19 +713,18 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - Helper function to parse the preferred shell type from user settings returned by cloud console API. // We need this function because the field might be missing in the settings // created with old versions of cloud console API. - std::optional AzureConnection::_ParsePreferredShellType(const web::json::value& settingsResponse) + winrt::hstring AzureConnection::_ParsePreferredShellType(const WDJ::JsonObject& settingsResponse) { - if (settingsResponse.has_object_field(L"properties")) + if (settingsResponse.HasKey(L"properties")) { - const auto userSettings = settingsResponse.at(L"properties"); - if (userSettings.has_string_field(L"preferredShellType")) + const auto userSettings = settingsResponse.GetNamedObject(L"properties"); + if (userSettings.HasKey(L"preferredShellType")) { - const auto preferredShellTypeValue = userSettings.at(L"preferredShellType"); - return preferredShellTypeValue.as_string(); + return userSettings.GetNamedString(L"preferredShellType"); } } - return std::nullopt; + return L"pwsh"; } // Method description: @@ -717,7 +733,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation { // Get user's cloud shell settings const auto settingsResponse = _GetCloudShellUserSettings(); - if (settingsResponse.has_field(L"error")) + if (settingsResponse.HasKey(L"error")) { _WriteStringWithNewline(RS_(L"AzureNoCloudAccount")); _transitionToState(ConnectionState::Failed); @@ -732,12 +748,36 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // Request for a terminal for said cloud shell const auto shellType = _ParsePreferredShellType(settingsResponse); _WriteStringWithNewline(RS_(L"AzureRequestingTerminal")); - const auto socketUri = _GetTerminal(shellType.value_or(L"pwsh")); + const auto socketUri = _GetTerminal(shellType); _TerminalOutputHandlers(L"\r\n"); - // Step 8: connecting to said terminal - const auto connReqTask = _cloudShellSocket.connect(socketUri); - connReqTask.wait(); + //// Step 8: connecting to said terminal + { + wil::unique_winhttp_hinternet sessionHandle, connectionHandle, requestHandle, socketHandle; + Uri parsedUri{ socketUri }; + sessionHandle.reset(WinHttpOpen(HttpUserAgent.data(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, 0)); + THROW_LAST_ERROR_IF(!sessionHandle); + + connectionHandle.reset(WinHttpConnect(sessionHandle.get(), parsedUri.Host().c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0)); + THROW_LAST_ERROR_IF(!connectionHandle); + + requestHandle.reset(WinHttpOpenRequest(connectionHandle.get(), L"GET", parsedUri.Path().c_str(), nullptr, nullptr, nullptr, WINHTTP_FLAG_SECURE)); + THROW_LAST_ERROR_IF(!requestHandle); + + THROW_IF_WIN32_BOOL_FALSE(WinHttpSetOption(requestHandle.get(), WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0)); +#pragma warning(suppress : 26477) // WINHTTP_NO_ADDITIONAL_HEADERS expands to NULL rather than nullptr (who would have thought?) + THROW_IF_WIN32_BOOL_FALSE(WinHttpSendRequest(requestHandle.get(), WINHTTP_NO_ADDITIONAL_HEADERS, 0, nullptr, 0, 0, 0)); + THROW_IF_WIN32_BOOL_FALSE(WinHttpReceiveResponse(requestHandle.get(), nullptr)); + + socketHandle.reset(WinHttpWebSocketCompleteUpgrade(requestHandle.get(), 0)); + THROW_LAST_ERROR_IF(!socketHandle); + + requestHandle.reset(); // We no longer need the request once we've upgraded it. + // We have to keep the socket session and connection handles. + _socketSessionHandle = std::move(sessionHandle); + _socketConnectionHandle = std::move(connectionHandle); + _webSocket = std::move(socketHandle); + } _state = AzureState::TermConnected; @@ -752,58 +792,52 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // Method description: // - helper function to send requests with default headers and extract responses as json values // Arguments: - // - a http_client - // - a http_request for the client to send + // - the URI + // - optional body content + // - an optional HTTP method (defaults to POST if content is present, GET otherwise) // Return value: // - the response from the server as a json value - json::value AzureConnection::_SendRequestReturningJson(http_client& theClient, http_request theRequest) + WDJ::JsonObject AzureConnection::_SendRequestReturningJson(std::wstring_view uri, const WWH::IHttpContent& content, WWH::HttpMethod method) { - auto& headers{ theRequest.headers() }; - headers.add(L"User-Agent", HttpUserAgent); - headers.add(L"Accept", L"application/json"); + if (!method) + { + method = content == nullptr ? WWH::HttpMethod::Get() : WWH::HttpMethod::Post(); + } - json::value jsonResult; - const auto responseTask = theClient.request(theRequest); - responseTask.wait(); - const auto response = responseTask.get(); - const auto responseJsonTask = response.extract_json(); - responseJsonTask.wait(); - jsonResult = responseJsonTask.get(); + WWH::HttpRequestMessage request{ method, Uri{ uri } }; + request.Content(content); + + auto headers{ request.Headers() }; + headers.Accept().TryParseAdd(L"application/json"); + + const auto response{ _httpClient.SendRequestAsync(request).get() }; + const auto string{ response.Content().ReadAsStringAsync().get() }; + const auto jsonResult{ WDJ::JsonObject::Parse(string) }; THROW_IF_AZURE_ERROR(jsonResult); return jsonResult; } - // Method description: - // - helper function to send _authenticated_ requests with json bodies whose responses are expected - // to be json. builds on _SendRequestReturningJson. - // Arguments: - // - the http_request - json::value AzureConnection::_SendAuthenticatedRequestReturningJson(http_client& theClient, http_request theRequest) + void AzureConnection::_setAccessToken(std::wstring_view accessToken) { - auto& headers{ theRequest.headers() }; - headers.add(L"Authorization", L"Bearer " + _accessToken); - - return _SendRequestReturningJson(theClient, std::move(theRequest)); + _accessToken = accessToken; + _httpClient.DefaultRequestHeaders().Authorization(WWH::Headers::HttpCredentialsHeaderValue{ L"Bearer", _accessToken }); } // Method description: // - helper function to start the device code flow // Return value: // - the response to the device code flow initiation - json::value AzureConnection::_GetDeviceCode() + WDJ::JsonObject AzureConnection::_GetDeviceCode() { - // Initialize the client - http_client loginClient(_loginUri); - - // Initialize the request - http_request commonRequest(L"POST"); - commonRequest.set_request_uri(L"common/oauth2/devicecode"); - const auto body{ fmt::format(L"client_id={}&resource={}", AzureClientID, _wantedResource) }; - commonRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded"); - - // Send the request and receive the response as a json value - return _SendRequestReturningJson(loginClient, commonRequest); + auto uri{ fmt::format(L"{}common/oauth2/devicecode", _loginUri) }; + WWH::HttpFormUrlEncodedContent content{ + std::unordered_map{ + { winrt::hstring{ L"client_id" }, winrt::hstring{ AzureClientID } }, + { winrt::hstring{ L"resource" }, winrt::hstring{ _wantedResource } }, + } + }; + return _SendRequestReturningJson(uri, content); } // Method description: @@ -815,14 +849,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // Return value: // - if authentication is done successfully, then return the response from the server // - else, throw an exception - json::value AzureConnection::_WaitForUser(const utility::string_t deviceCode, int pollInterval, int expiresIn) + WDJ::JsonObject AzureConnection::_WaitForUser(const winrt::hstring& deviceCode, int pollInterval, int expiresIn) { - // Initialize the client - http_client pollingClient(_loginUri); - - // Continuously send a poll request until the user authenticates - const auto body{ fmt::format(L"grant_type=device_code&resource={}&client_id={}&code={}", _wantedResource, AzureClientID, deviceCode) }; - const auto requestUri = L"common/oauth2/token"; + auto uri{ fmt::format(L"{}common/oauth2/token", _loginUri) }; + WWH::HttpFormUrlEncodedContent content{ + std::unordered_map{ + { winrt::hstring{ L"grant_type" }, winrt::hstring{ L"device_code" } }, + { winrt::hstring{ L"client_id" }, winrt::hstring{ AzureClientID } }, + { winrt::hstring{ L"resource" }, winrt::hstring{ _wantedResource } }, + { winrt::hstring{ L"code" }, deviceCode }, + } + }; // use a steady clock here so it's not impacted by local time discontinuities const auto tokenExpiry{ std::chrono::steady_clock::now() + std::chrono::seconds(expiresIn) }; @@ -837,14 +874,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation break; } - http_request pollRequest(L"POST"); - pollRequest.set_request_uri(requestUri); - pollRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded"); - try { - auto response{ _SendRequestReturningJson(pollingClient, pollRequest) }; + auto response = _SendRequestReturningJson(uri, content); _WriteStringWithNewline(RS_(L"AzureSuccessfullyAuthenticated")); + // Got a valid response: we're done return response; } @@ -859,7 +893,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } // uncaught exceptions bubble up to the caller } - return json::value::null(); + return nullptr; } // Method description: @@ -868,16 +902,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - the response which contains a list of the user's Azure tenants void AzureConnection::_PopulateTenantList() { - // Initialize the client - http_client tenantClient(_resourceUri); - - // Initialize the request - http_request tenantRequest(L"GET"); - tenantRequest.set_request_uri(L"tenants?api-version=2020-01-01"); + auto uri{ fmt::format(L"{}tenants?api-version=2020-01-01", _resourceUri) }; // Send the request and return the response as a json value - auto tenantResponse{ _SendAuthenticatedRequestReturningJson(tenantClient, tenantRequest) }; - auto tenantList{ tenantResponse.at(L"value").as_array() }; + auto tenantResponse{ _SendRequestReturningJson(uri, nullptr) }; + auto tenantList{ tenantResponse.GetNamedArray(L"value") }; _tenantList.clear(); std::transform(tenantList.begin(), tenantList.end(), std::back_inserter(_tenantList), _crackTenant); @@ -889,82 +918,73 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - the response with the new tokens void AzureConnection::_RefreshTokens() { - // Initialize the client - http_client refreshClient(_loginUri); - - // Initialize the request - http_request refreshRequest(L"POST"); - refreshRequest.set_request_uri(_currentTenant->ID + L"/oauth2/token"); - const auto body{ fmt::format(L"client_id={}&resource={}&grant_type=refresh_token&refresh_token={}", AzureClientID, _wantedResource, _refreshToken) }; - refreshRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded"); + auto uri{ fmt::format(L"{}{}/oauth2/token", _loginUri, _currentTenant->ID) }; + WWH::HttpFormUrlEncodedContent content{ + std::unordered_map{ + { winrt::hstring{ L"grant_type" }, winrt::hstring{ L"refresh_token" } }, + { winrt::hstring{ L"client_id" }, winrt::hstring{ AzureClientID } }, + { winrt::hstring{ L"resource" }, winrt::hstring{ _wantedResource } }, + { winrt::hstring{ L"refresh_token" }, winrt::hstring{ _refreshToken } }, + } + }; // Send the request and return the response as a json value - auto refreshResponse{ _SendRequestReturningJson(refreshClient, refreshRequest) }; - _accessToken = refreshResponse.at(L"access_token").as_string(); - _refreshToken = refreshResponse.at(L"refresh_token").as_string(); - _expiry = std::stoi(refreshResponse.at(L"expires_on").as_string()); + auto refreshResponse{ _SendRequestReturningJson(uri, content) }; + _setAccessToken(refreshResponse.GetNamedString(L"access_token")); + _refreshToken = refreshResponse.GetNamedString(L"refresh_token"); + _expiry = std::stoi(winrt::to_string(refreshResponse.GetNamedString(L"expires_on"))); } // Method description: // - helper function to get the user's cloud shell settings // Return value: // - the user's cloud shell settings - json::value AzureConnection::_GetCloudShellUserSettings() + WDJ::JsonObject AzureConnection::_GetCloudShellUserSettings() { - // Initialize client - http_client settingsClient(_resourceUri); - - // Initialize request - http_request settingsRequest(L"GET"); - settingsRequest.set_request_uri(L"providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2018-10-01"); - - return _SendAuthenticatedRequestReturningJson(settingsClient, settingsRequest); + auto uri{ fmt::format(L"{}providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2020-04-01-preview", _resourceUri) }; + return _SendRequestReturningJson(uri, nullptr); } // Method description: // - helper function to request for a cloud shell // Return value: // - the uri for the cloud shell - utility::string_t AzureConnection::_GetCloudShell() + winrt::hstring AzureConnection::_GetCloudShell() { - // Initialize client - http_client cloudShellClient(_resourceUri); + auto uri{ fmt::format(L"{}providers/Microsoft.Portal/consoles/default?api-version=2020-04-01-preview", _resourceUri) }; - // Initialize request - http_request shellRequest(L"PUT"); - shellRequest.set_request_uri(L"providers/Microsoft.Portal/consoles/default?api-version=2018-10-01"); - // { "properties": { "osType": "linux" } } - auto body = json::value::object({ { U("properties"), json::value::object({ { U("osType"), json::value::string(U("linux")) } }) } }); - shellRequest.set_body(body); + WWH::HttpStringContent content{ + LR"-({"properties": {"osType": "linux"}})-", + WSS::UnicodeEncoding::Utf8, + L"application/json" + }; - // Send the request and get the response as a json value - const auto cloudShell = _SendAuthenticatedRequestReturningJson(cloudShellClient, shellRequest); + const auto cloudShell = _SendRequestReturningJson(uri, content, WWH::HttpMethod::Put()); // Return the uri - return cloudShell.at(L"properties").at(L"uri").as_string() + L"/"; + return winrt::hstring{ std::wstring{ cloudShell.GetNamedObject(L"properties").GetNamedString(L"uri") } + L"/" }; } // Method description: // - helper function to request for a terminal // Return value: // - the uri for the terminal - utility::string_t AzureConnection::_GetTerminal(utility::string_t shellType) + winrt::hstring AzureConnection::_GetTerminal(const winrt::hstring& shellType) { - // Initialize client - http_client terminalClient(_cloudShellUri); + auto uri{ fmt::format(L"{}terminals?cols={}&rows={}&version=2019-01-01&shell={}", _cloudShellUri, _initialCols, _initialRows, shellType) }; - // Initialize the request - http_request terminalRequest(L"POST"); - terminalRequest.set_request_uri(fmt::format(L"terminals?cols={}&rows={}&version=2019-01-01&shell={}", _initialCols, _initialRows, shellType)); - // LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'" - terminalRequest.set_body(json::value::null()); + WWH::HttpStringContent content{ + L"", + WSS::UnicodeEncoding::Utf8, + // LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'" + L"application/json" + }; - // Send the request and get the response as a json value - const auto terminalResponse = _SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest); - _terminalID = terminalResponse.at(L"id").as_string(); + const auto terminalResponse = _SendRequestReturningJson(uri, content); + _terminalID = terminalResponse.GetNamedString(L"id"); // Return the uri - return terminalResponse.at(L"socketUri").as_string(); + return terminalResponse.GetNamedString(L"socketUri"); } // Method description: @@ -972,16 +992,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation // - we store the display name, tenant ID, access/refresh tokens, and token expiry void AzureConnection::_StoreCredential() { - json::value userName; - userName[U("ver")] = CurrentCredentialVersion; + WDJ::JsonObject userName; + userName.SetNamedValue(L"ver", WDJ::JsonValue::CreateNumberValue(CurrentCredentialVersion)); _packTenant(userName, *_currentTenant); - json::value passWord; - passWord[U("accessToken")] = json::value::string(_accessToken); - passWord[U("refreshToken")] = json::value::string(_refreshToken); - passWord[U("expiry")] = json::value::string(std::to_wstring(_expiry)); + + WDJ::JsonObject passWord; + passWord.SetNamedValue(L"accessToken", WDJ::JsonValue::CreateStringValue(_accessToken)); + passWord.SetNamedValue(L"refreshToken", WDJ::JsonValue::CreateStringValue(_refreshToken)); + passWord.SetNamedValue(L"expiry", WDJ::JsonValue::CreateStringValue(std::to_wstring(_expiry))); PasswordVault vault; - PasswordCredential newCredential{ PasswordVaultResourceName, userName.serialize(), passWord.serialize() }; + PasswordCredential newCredential{ PasswordVaultResourceName, userName.Stringify(), passWord.Stringify() }; vault.Add(newCredential); } diff --git a/src/cascadia/TerminalConnection/AzureConnection.h b/src/cascadia/TerminalConnection/AzureConnection.h index 22c62699ca..cedd76757a 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.h +++ b/src/cascadia/TerminalConnection/AzureConnection.h @@ -5,9 +5,6 @@ #include "AzureConnection.g.h" -#include -#include -#include #include #include @@ -56,30 +53,30 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void _RunStoreState(); void _RunConnectState(); - const utility::string_t _loginUri{ U("https://login.microsoftonline.com/") }; - const utility::string_t _resourceUri{ U("https://management.azure.com/") }; - const utility::string_t _wantedResource{ U("https://management.core.windows.net/") }; + static constexpr std::wstring_view _loginUri{ L"https://login.microsoftonline.com/" }; + static constexpr std::wstring_view _resourceUri{ L"https://management.azure.com/" }; + static constexpr std::wstring_view _wantedResource{ L"https://management.core.windows.net/" }; const int _expireLimit{ 2700 }; - utility::string_t _accessToken; - utility::string_t _refreshToken; + winrt::hstring _accessToken; + winrt::hstring _refreshToken; int _expiry{ 0 }; - utility::string_t _cloudShellUri; - utility::string_t _terminalID; + winrt::hstring _cloudShellUri; + winrt::hstring _terminalID; std::vector<::Microsoft::Terminal::Azure::Tenant> _tenantList; std::optional<::Microsoft::Terminal::Azure::Tenant> _currentTenant; void _WriteStringWithNewline(const std::wstring_view str); void _WriteCaughtExceptionRecord(); - web::json::value _SendRequestReturningJson(web::http::client::http_client& theClient, web::http::http_request theRequest); - web::json::value _SendAuthenticatedRequestReturningJson(web::http::client::http_client& theClient, web::http::http_request theRequest); - web::json::value _GetDeviceCode(); - web::json::value _WaitForUser(utility::string_t deviceCode, int pollInterval, int expiresIn); + winrt::Windows::Data::Json::JsonObject _SendRequestReturningJson(std::wstring_view uri, const winrt::Windows::Web::Http::IHttpContent& content = nullptr, winrt::Windows::Web::Http::HttpMethod method = nullptr); + void _setAccessToken(std::wstring_view accessToken); + winrt::Windows::Data::Json::JsonObject _GetDeviceCode(); + winrt::Windows::Data::Json::JsonObject _WaitForUser(const winrt::hstring& deviceCode, int pollInterval, int expiresIn); void _PopulateTenantList(); void _RefreshTokens(); - web::json::value _GetCloudShellUserSettings(); - utility::string_t _GetCloudShell(); - utility::string_t _GetTerminal(utility::string_t shellType); + winrt::Windows::Data::Json::JsonObject _GetCloudShellUserSettings(); + winrt::hstring _GetCloudShell(); + winrt::hstring _GetTerminal(const winrt::hstring& shellType); void _StoreCredential(); void _RemoveCredentials(); @@ -95,9 +92,16 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation std::optional _ReadUserInput(InputMode mode); - web::websockets::client::websocket_client _cloudShellSocket; + winrt::Windows::Web::Http::HttpClient _httpClient{ nullptr }; + wil::unique_winhttp_hinternet _socketSessionHandle; + wil::unique_winhttp_hinternet _socketConnectionHandle; + wil::unique_winhttp_hinternet _webSocket; - static std::optional _ParsePreferredShellType(const web::json::value& settingsResponse); + til::u8state _u8State{}; + std::wstring _u16Str; + std::array _buffer{}; + + static winrt::hstring _ParsePreferredShellType(const winrt::Windows::Data::Json::JsonObject& settingsResponse); }; } diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index 5668399a40..3f7ee84729 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -11,7 +11,6 @@ true - true @@ -99,39 +98,4 @@ - - - - - - - - - diff --git a/src/cascadia/TerminalConnection/pch.h b/src/cascadia/TerminalConnection/pch.h index 847f8971bc..124397ca3b 100644 --- a/src/cascadia/TerminalConnection/pch.h +++ b/src/cascadia/TerminalConnection/pch.h @@ -24,8 +24,14 @@ #include "winrt/Windows.Security.Credentials.h" #include "winrt/Windows.Foundation.Collections.h" +#include "winrt/Windows.Web.Http.h" +#include "winrt/Windows.Web.Http.Headers.h" +#include "winrt/Windows.Data.Json.h" #include +#include +#include + #include TRACELOGGING_DECLARE_PROVIDER(g_hTerminalConnectionProvider); #include diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 78c37d8633..04cda21acb 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -43,9 +43,6 @@ - - - @@ -79,9 +76,6 @@ - - - diff --git a/src/features.xml b/src/features.xml index e26c217d3b..13925a6615 100644 --- a/src/features.xml +++ b/src/features.xml @@ -145,4 +145,14 @@ AlwaysDisabled + + Feature_AzureConnectionInProc + Host the AzureConnection inside Terminal rather than via TerminalAzBridge + 4661 + AlwaysDisabled + + Dev + + +