terminal/src/cascadia/TerminalConnection/AzureConnection.cpp
Michael Niksa 7478248564
Add names to threads to make debugging a slight bit easier (#9801)
Add names to threads to make debugging a slight bit easier.

## PR Checklist
* [x] Closes personal todo item.
* [x] I work here.
* [x] Tested manually.

## Detailed Description of the Pull Request / Additional comments
Thread descriptions show up as names in both the Visual Studio debugger, WinDBG debugger, and Windows Performance Analyzer. This makes it faster and easier to identify threads of interest in our processes.

## Validation Steps Performed
* [x] Checked threads were named in OpenConsole.exe running in classic conhost window mode under VS debug
* [x] Checked threads were named in OpenConsole.exe running in conpty mode under VS debug
* [x] Checked threads were named in WindowsTerminal.exe (for a few of the threads around connections)
* [x] Checked that we could also see it in WinDBG
2021-04-14 10:56:52 +00:00

1014 lines
39 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "AzureConnection.h"
#include "AzureClientID.h"
#include <sstream>
#include <cstdlib>
#include <LibraryResources.h>
#include <unicode.hpp>
#include "AzureConnection.g.cpp"
#include "winrt/Windows.System.UserProfile.h"
#include "../../types/inc/Utils.hpp"
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 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
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)
{
return fmt::format(L"\x1b[{0}m{1}\x1b[m", colorCode, text);
}
// Takes N resource names, loads the first one as a format string, and then
// loads all the remaining ones into the %s arguments in the first one after
// colorizing them in the USER_INPUT_COLOR.
// This is intended to be used to drop UserEntry resources into an existing string.
template<typename... Args>
static inline std::wstring _formatResWithColoredUserInputOptions(const std::wstring_view resourceKey, Args&&... args)
{
return fmt::format(std::wstring_view{ GetLibraryResourceString(resourceKey) }, (_colorize(USER_INPUT_COLOR, GetLibraryResourceString(args)))...);
}
static inline std::wstring _formatTenant(int tenantNumber, const Tenant& tenant)
{
return fmt::format(std::wstring_view{ RS_(L"AzureIthTenant") },
_colorize(USER_INPUT_COLOR, std::to_wstring(tenantNumber)),
_colorize(USER_INFO_COLOR, tenant.DisplayName.value_or(std::wstring{ RS_(L"AzureUnknownTenantName") })),
tenant.DefaultDomain.value_or(tenant.ID)); // use the domain name if possible, ID if not.
}
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
winrt::guid AzureConnection::ConnectionType() noexcept
{
return AzureConnectionType;
}
// This function exists because the clientID only gets added by the release pipelines
// and is not available on local builds, so we want to be able to make sure we don't
// try to make an Azure connection if its a local build
bool AzureConnection::IsAzureConnectionAvailable() noexcept
{
return (AzureClientID != L"0");
}
AzureConnection::AzureConnection(const uint32_t initialRows, const uint32_t initialCols) :
_initialRows{ initialRows },
_initialCols{ initialCols },
_expiry{}
{
}
// Method description:
// - helper that will write an unterminated string (generally, from a resource) to the output stream.
// Arguments:
// - str: the string to write.
void AzureConnection::_WriteStringWithNewline(const std::wstring_view str)
{
_TerminalOutputHandlers(str + L"\r\n");
}
// Method description:
// - helper that prints exception information to the output stream.
// Arguments:
// - [IMPLICIT] the current exception context
void AzureConnection::_WriteCaughtExceptionRecord()
{
try
{
throw;
}
catch (const std::exception& runtimeException)
{
// This also catches the AzureException, which has a .what()
_TerminalOutputHandlers(_colorize(91, til::u8u16(std::string{ runtimeException.what() })));
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - creates the output thread (where we will do the authentication and actually connect to Azure)
void AzureConnection::Start()
{
// Create our own output handling thread
// Each connection needs to make sure to drain the output from its backing host.
_hOutputThread.reset(CreateThread(
nullptr,
0,
[](LPVOID lpParameter) noexcept {
AzureConnection* const pInstance = static_cast<AzureConnection*>(lpParameter);
if (pInstance)
{
return pInstance->_OutputThread();
}
return gsl::narrow_cast<DWORD>(E_INVALIDARG);
},
this,
0,
nullptr));
THROW_LAST_ERROR_IF_NULL(_hOutputThread);
LOG_IF_FAILED(SetThreadDescription(_hOutputThread.get(), L"AzureConnection Output Thread"));
_transitionToState(ConnectionState::Connecting);
}
std::optional<std::wstring> AzureConnection::_ReadUserInput(InputMode mode)
{
std::unique_lock<std::mutex> inputLock{ _inputMutex };
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
return std::nullopt;
}
_currentInputMode = mode;
_TerminalOutputHandlers(L"> \x1b[92m"); // Make prompted user input green
_inputEvent.wait(inputLock, [this, mode]() {
return _currentInputMode != mode || _isStateAtOrBeyond(ConnectionState::Closing);
});
_TerminalOutputHandlers(L"\x1b[m");
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
return std::nullopt;
}
std::wstring readInput{};
_userInput.swap(readInput);
return readInput;
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - handles the different possible inputs in the different states
// Arguments:
// the user's input
void AzureConnection::WriteInput(hstring const& data)
{
// We read input while connected AND connecting.
if (!_isStateOneOf(ConnectionState::Connected, ConnectionState::Connecting))
{
return;
}
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();
return;
}
std::lock_guard<std::mutex> lock{ _inputMutex };
if (data.size() > 0 && (gsl::at(data, 0) == UNICODE_BACKSPACE || gsl::at(data, 0) == UNICODE_DEL)) // BS or DEL
{
if (_userInput.size() > 0)
{
_userInput.pop_back();
_TerminalOutputHandlers(L"\x08 \x08"); // overstrike the character with a space
}
}
else
{
_TerminalOutputHandlers(data); // echo back
switch (_currentInputMode)
{
case InputMode::Line:
if (data.size() > 0 && gsl::at(data, 0) == UNICODE_CARRIAGERETURN)
{
_TerminalOutputHandlers(L"\r\n"); // we probably got a \r, so we need to advance to the next line.
_currentInputMode = InputMode::None; // toggling the mode indicates completion
_inputEvent.notify_one();
break;
}
[[fallthrough]];
default:
std::copy(data.cbegin(), data.cend(), std::back_inserter(_userInput));
break;
}
}
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - resizes the terminal
// Arguments:
// - the new rows/cols values
void AzureConnection::Resize(uint32_t rows, uint32_t columns)
try
{
if (!_isConnected())
{
_initialRows = rows;
_initialCols = columns;
}
else // We only transition to Connected when we've established the websocket.
{
// Initialize client
http_client terminalClient(_cloudShellUri);
// 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());
// Send the request (don't care about the response)
(void)_SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest);
}
}
CATCH_LOG();
// Method description:
// - ascribes to the ITerminalConnection interface
// - closes the websocket connection and the output thread
void AzureConnection::Close()
{
if (_transitionToState(ConnectionState::Closing))
{
_inputEvent.notify_all();
if (_state == AzureState::TermConnected)
{
// Close the websocket connection
auto closedTask = _cloudShellSocket.close();
closedTask.wait();
}
if (_hOutputThread)
{
// Tear down our output thread
WaitForSingleObject(_hOutputThread.get(), INFINITE);
_hOutputThread.reset();
}
_transitionToState(ConnectionState::Closed);
}
}
// Method description:
// - This method returns a tenant's ID and display name (if one is available).
// If there is no display name, a placeholder is returned in its stead.
// Arguments:
// - tenant - the unparsed tenant
// Return value:
// - a tuple containing the ID and display name of the tenant.
static Tenant _crackTenant(const json::value& jsonTenant)
{
Tenant tenant{};
if (jsonTenant.has_string_field(L"tenantID"))
{
// for compatibility with version 1 credentials
tenant.ID = jsonTenant.at(L"tenantID").as_string();
}
else
{
// This one comes in off the wire
tenant.ID = jsonTenant.at(L"tenantId").as_string();
}
if (jsonTenant.has_string_field(L"displayName"))
{
tenant.DisplayName = jsonTenant.at(L"displayName").as_string();
}
if (jsonTenant.has_string_field(L"defaultDomain"))
{
tenant.DefaultDomain = jsonTenant.at(L"defaultDomain").as_string();
}
return tenant;
}
static void _packTenant(json::value& jsonTenant, const Tenant& tenant)
{
jsonTenant[L"tenantId"] = json::value::string(tenant.ID);
if (tenant.DisplayName.has_value())
{
jsonTenant[L"displayName"] = json::value::string(*tenant.DisplayName);
}
if (tenant.DefaultDomain.has_value())
{
jsonTenant[L"defaultDomain"] = json::value::string(*tenant.DefaultDomain);
}
}
// Method description:
// - this is the output thread, where we initiate the connection to Azure
// and establish a websocket connection
// Return value:
// - return status
DWORD AzureConnection::_OutputThread()
{
while (true)
{
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
// If we enter a new state while closing, just bail.
return S_FALSE;
}
try
{
switch (_state)
{
// Initial state, check if the user has any stored connection settings and allow them to login with those
// or allow them to login with a different account or allow them to remove the saved settings
case AzureState::AccessStored:
{
_RunAccessState();
break;
}
// User has no saved connection settings or has opted to login with a different account
// Azure authentication happens here
case AzureState::DeviceFlow:
{
_RunDeviceFlowState();
break;
}
// User has multiple tenants in their Azure account, they need to choose which one to connect to
case AzureState::TenantChoice:
{
_RunTenantChoiceState();
break;
}
// Ask the user if they want to save these connection settings for future logins
case AzureState::StoreTokens:
{
_RunStoreState();
break;
}
// Connect to Azure, we only get here once we have everything we need (tenantID, accessToken, refreshToken)
case AzureState::TermConnecting:
{
_RunConnectState();
break;
}
// We are connected, continuously read from the websocket until its closed
case AzureState::TermConnected:
{
_transitionToState(ConnectionState::Connected);
while (true)
{
// Read from websocket
pplx::task<websocket_incoming_message> msgT;
try
{
msgT = _cloudShellSocket.receive();
msgT.wait();
}
catch (...)
{
// Websocket has been closed; consider it a graceful exit?
// This should result in our termination.
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;
}
}
}
catch (...)
{
_WriteCaughtExceptionRecord();
_transitionToState(ConnectionState::Failed);
return E_FAIL;
}
}
}
// Method description:
// - helper function to get the stored credentials (if any) and let the user choose what to do next
void AzureConnection::_RunAccessState()
{
bool oldVersionEncountered = false;
auto vault = PasswordVault();
winrt::Windows::Foundation::Collections::IVectorView<PasswordCredential> credList;
// FindAllByResource throws an exception if there are no credentials stored under the given resource so we wrap it in a try-catch block
try
{
credList = vault.FindAllByResource(PasswordVaultResourceName);
}
catch (...)
{
// No credentials are stored, so start the device flow
_state = AzureState::DeviceFlow;
return;
}
int numTenants{ 0 };
_tenantList.clear();
for (const auto& entry : credList)
{
auto nameJson = json::value::parse(entry.UserName().c_str());
std::optional<int> credentialVersion;
if (nameJson.has_integer_field(U("ver")))
{
credentialVersion = nameJson.at(U("ver")).as_integer();
}
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 (!numTenants)
{
if (oldVersionEncountered)
{
_WriteStringWithNewline(RS_(L"AzureOldCredentialsFlushedMessage"));
}
// No valid up-to-date credentials were found, so start the device flow
_state = AzureState::DeviceFlow;
return;
}
_WriteStringWithNewline(RS_(L"AzureEnterTenant"));
_WriteStringWithNewline(_formatResWithColoredUserInputOptions(USES_RESOURCE(L"AzureNewLogin"), USES_RESOURCE(L"AzureUserEntry_NewLogin")));
_WriteStringWithNewline(_formatResWithColoredUserInputOptions(USES_RESOURCE(L"AzureRemoveStored"), USES_RESOURCE(L"AzureUserEntry_RemoveStored")));
int selectedTenant{ -1 };
do
{
auto maybeTenantSelection = _ReadUserInput(InputMode::Line);
if (!maybeTenantSelection.has_value())
{
return;
}
const auto& tenantSelection = maybeTenantSelection.value();
if (tenantSelection == RS_(L"AzureUserEntry_RemoveStored"))
{
// User wants to remove the stored settings
_RemoveCredentials();
_state = AzureState::DeviceFlow;
return;
}
else if (tenantSelection == RS_(L"AzureUserEntry_NewLogin"))
{
// User wants to login with a different account
_state = AzureState::DeviceFlow;
return;
}
else
{
try
{
selectedTenant = std::stoi(tenantSelection);
if (selectedTenant < 0 || selectedTenant >= numTenants)
{
_WriteStringWithNewline(RS_(L"AzureNumOutOfBoundsError"));
continue; // go 'round again
}
break;
}
catch (...)
{
// suppress exceptions in conversion
}
}
// if we got here, we didn't break out of the loop early and need to go 'round again
_WriteStringWithNewline(_formatResWithColoredUserInputOptions(USES_RESOURCE(L"AzureInvalidAccessInput"), USES_RESOURCE(L"AzureUserEntry_NewLogin"), USES_RESOURCE(L"AzureUserEntry_RemoveStored")));
} while (true);
// 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());
_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());
const auto t1 = std::chrono::system_clock::now();
const auto timeNow = std::chrono::duration_cast<std::chrono::seconds>(t1.time_since_epoch()).count();
// Check if the token is close to expiring and refresh if so
if (timeNow + _expireLimit > _expiry)
{
try
{
_RefreshTokens();
// Store the updated tokens under the same username
_StoreCredential();
}
catch (const AzureException& e)
{
if (e.GetCode() == ErrorCodes::InvalidGrant)
{
_WriteCaughtExceptionRecord();
vault.Remove(desiredCredential);
// Delete this credential and try again.
_state = AzureState::AccessStored;
return;
}
throw; // rethrow. we couldn't handle this error.
}
}
// We have everything we need, so go ahead and connect
_state = AzureState::TermConnecting;
}
// Method description:
// - helper function to start the device code flow (required for authentication to Azure)
void AzureConnection::_RunDeviceFlowState()
{
// Initiate device code flow
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());
_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());
// Wait for user authentication and obtain the access/refresh tokens
json::value authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn);
_accessToken = authenticatedResponse.at(L"access_token").as_string();
_refreshToken = authenticatedResponse.at(L"refresh_token").as_string();
// Get the tenants and the required tenant id
_PopulateTenantList();
if (_tenantList.size() == 0)
{
_WriteStringWithNewline(RS_(L"AzureNoTenants"));
_transitionToState(ConnectionState::Failed);
return;
}
else if (_tenantList.size() == 1)
{
_currentTenant = til::at(_tenantList, 0);
// We have to refresh now that we have the tenantID
_RefreshTokens();
_state = AzureState::StoreTokens;
}
else
{
_state = AzureState::TenantChoice;
}
}
// Method description:
// - helper function to list the user's tenants and let them decide which tenant they wish to connect to
void AzureConnection::_RunTenantChoiceState()
{
auto numTenants = gsl::narrow<int>(_tenantList.size());
for (int i = 0; i < numTenants; i++)
{
_WriteStringWithNewline(_formatTenant(i, til::at(_tenantList, i)));
}
_WriteStringWithNewline(RS_(L"AzureEnterTenant"));
int selectedTenant{ -1 };
do
{
auto maybeTenantSelection = _ReadUserInput(InputMode::Line);
if (!maybeTenantSelection.has_value())
{
return;
}
const auto& tenantSelection = maybeTenantSelection.value();
try
{
selectedTenant = std::stoi(tenantSelection);
if (selectedTenant < 0 || selectedTenant >= numTenants)
{
_WriteStringWithNewline(RS_(L"AzureNumOutOfBoundsError"));
continue;
}
break;
}
catch (...)
{
// suppress exceptions in conversion
}
// if we got here, we didn't break out of the loop early and need to go 'round again
_WriteStringWithNewline(RS_(L"AzureNonNumberError"));
} while (true);
_currentTenant = til::at(_tenantList, selectedTenant);
// We have to refresh now that we have the tenantID
_RefreshTokens();
_state = AzureState::StoreTokens;
}
// Method description:
// - helper function to ask the user if they wish to store their credentials
void AzureConnection::_RunStoreState()
{
_WriteStringWithNewline(_formatResWithColoredUserInputOptions(USES_RESOURCE(L"AzureStorePrompt"), USES_RESOURCE(L"AzureUserEntry_Yes"), USES_RESOURCE(L"AzureUserEntry_No")));
// Wait for user input
do
{
auto maybeStoreCredentials = _ReadUserInput(InputMode::Line);
if (!maybeStoreCredentials.has_value())
{
return;
}
const auto& storeCredentials = maybeStoreCredentials.value();
if (storeCredentials == RS_(L"AzureUserEntry_Yes"))
{
_StoreCredential();
_WriteStringWithNewline(RS_(L"AzureTokensStored"));
break;
}
else if (storeCredentials == RS_(L"AzureUserEntry_No"))
{
break; // we're done, but the user wants nothing.
}
// if we got here, we didn't break out of the loop early and need to go 'round again
_WriteStringWithNewline(_formatResWithColoredUserInputOptions(USES_RESOURCE(L"AzureInvalidStoreInput"), USES_RESOURCE(L"AzureUserEntry_Yes"), USES_RESOURCE(L"AzureUserEntry_No")));
} while (true);
_state = AzureState::TermConnecting;
}
// Method description:
// - 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<utility::string_t> AzureConnection::_ParsePreferredShellType(const web::json::value& settingsResponse)
{
if (settingsResponse.has_object_field(L"properties"))
{
const auto userSettings = settingsResponse.at(L"properties");
if (userSettings.has_string_field(L"preferredShellType"))
{
const auto preferredShellTypeValue = userSettings.at(L"preferredShellType");
return preferredShellTypeValue.as_string();
}
}
return std::nullopt;
}
// Method description:
// - helper function to connect the user to the Azure cloud shell
void AzureConnection::_RunConnectState()
{
// Get user's cloud shell settings
const auto settingsResponse = _GetCloudShellUserSettings();
if (settingsResponse.has_field(L"error"))
{
_WriteStringWithNewline(RS_(L"AzureNoCloudAccount"));
_transitionToState(ConnectionState::Failed);
return;
}
// Request for a cloud shell
_WriteStringWithNewline(RS_(L"AzureRequestingCloud"));
_cloudShellUri = _GetCloudShell();
_WriteStringWithNewline(RS_(L"AzureSuccess"));
// 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"));
_TerminalOutputHandlers(L"\r\n");
// Step 8: connecting to said terminal
const auto connReqTask = _cloudShellSocket.connect(socketUri);
connReqTask.wait();
_state = AzureState::TermConnected;
std::wstring queuedUserInput{};
std::swap(_userInput, queuedUserInput);
if (queuedUserInput.size() > 0)
{
WriteInput(static_cast<winrt::hstring>(queuedUserInput)); // send the user's queued up input back through
}
}
// 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
// Return value:
// - the response from the server as a json value
json::value AzureConnection::_SendRequestReturningJson(http_client& theClient, http_request theRequest)
{
auto& headers{ theRequest.headers() };
headers.add(L"User-Agent", HttpUserAgent);
headers.add(L"Accept", L"application/json");
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();
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)
{
auto& headers{ theRequest.headers() };
headers.add(L"Authorization", L"Bearer " + _accessToken);
return _SendRequestReturningJson(theClient, std::move(theRequest));
}
// Method description:
// - helper function to start the device code flow
// Return value:
// - the response to the device code flow initiation
json::value 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);
}
// Method description:
// - helper function to wait for the user to authenticate using their web browser
// Arguments:
// - the device code that would have been received when authentication was initiated
// - the polling interval duration
// - the duration the code is still valid for
// 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)
{
// 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";
// 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) };
while (std::chrono::steady_clock::now() < tokenExpiry)
{
std::this_thread::sleep_for(std::chrono::seconds(pollInterval));
// User might close the tab while we wait for them to authenticate, this case handles that
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
// We're going down, there's no valid user for us to return
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) };
_WriteStringWithNewline(RS_(L"AzureSuccessfullyAuthenticated"));
// Got a valid response: we're done
return response;
}
catch (const AzureException& e)
{
if (e.GetCode() == ErrorCodes::AuthorizationPending)
{
// Handle the "auth pending" exception by retrying.
continue;
}
throw;
} // uncaught exceptions bubble up to the caller
}
return json::value::null();
}
// Method description:
// - helper function to acquire the user's Azure tenants
// Return value:
// - 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");
// Send the request and return the response as a json value
auto tenantResponse{ _SendAuthenticatedRequestReturningJson(tenantClient, tenantRequest) };
auto tenantList{ tenantResponse.at(L"value").as_array() };
_tenantList.clear();
std::transform(tenantList.begin(), tenantList.end(), std::back_inserter(_tenantList), _crackTenant);
}
// Method description:
// - helper function to refresh the access/refresh tokens
// Return value:
// - 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");
// 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());
}
// Method description:
// - helper function to get the user's cloud shell settings
// Return value:
// - the user's cloud shell settings
json::value 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);
}
// Method description:
// - helper function to request for a cloud shell
// Return value:
// - the uri for the cloud shell
utility::string_t AzureConnection::_GetCloudShell()
{
// Initialize client
http_client cloudShellClient(_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);
// Send the request and get the response as a json value
const auto cloudShell = _SendAuthenticatedRequestReturningJson(cloudShellClient, shellRequest);
// Return the uri
return cloudShell.at(L"properties").at(L"uri").as_string() + 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)
{
// Initialize client
http_client terminalClient(_cloudShellUri);
// 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());
// Send the request and get the response as a json value
const auto terminalResponse = _SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest);
_terminalID = terminalResponse.at(L"id").as_string();
// Return the uri
return terminalResponse.at(L"socketUri").as_string();
}
// Method description:
// - helper function to store the credentials
// - we store the display name, tenant ID, access/refresh tokens, and token expiry
void AzureConnection::_StoreCredential()
{
json::value userName;
userName[U("ver")] = 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));
PasswordVault vault;
PasswordCredential newCredential{ PasswordVaultResourceName, userName.serialize(), passWord.serialize() };
vault.Add(newCredential);
}
// Method description:
// - helper function to remove all stored credentials
void AzureConnection::_RemoveCredentials()
{
PasswordVault vault;
winrt::Windows::Foundation::Collections::IVectorView<PasswordCredential> credList;
// FindAllByResource throws an exception if there are no credentials stored under the given resource so we wrap it in a try-catch block
try
{
credList = vault.FindAllByResource(PasswordVaultResourceName);
}
catch (...)
{
// No credentials are stored, so just return
_WriteStringWithNewline(RS_(L"AzureNoTokens"));
return;
}
for (const auto& cred : credList)
{
try
{
vault.Remove(cred);
}
CATCH_LOG();
}
_WriteStringWithNewline(RS_(L"AzureTokensRemoved"));
}
}