mirror of
https://github.com/microsoft/terminal.git
synced 2026-02-12 21:25:50 +00:00
az: rework error handling
* _RequestHelper no longer eats exceptions. * Delete the "no internet" error message. * Wrap exceptions coming out of Azure API in a well-known type. * Catch by type. * Extract error codes for known failures (keep polling, invalid grant). * When we get an Invalid Grant, dispose of the cached refresh token and force the user to log in again. * Catch all printable exceptions and print them. * Remove the NoConnect state completely -- just bail out when an exception hits the toplevel of the output thread. * Move 3x logic into _RefreshTokens and pop exceptions out of it. * Begin abstracting into AzureClient
This commit is contained in:
committed by
Dustin Howett
parent
37e62dabd5
commit
61f06e7aa5
46
src/cascadia/TerminalConnection/AzureClient.h
Normal file
46
src/cascadia/TerminalConnection/AzureClient.h
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cpprest/json.h"
|
||||
|
||||
namespace Microsoft::Terminal::Azure
|
||||
{
|
||||
class AzureException : public std::runtime_error
|
||||
{
|
||||
std::wstring _code;
|
||||
|
||||
public:
|
||||
static bool IsErrorPayload(const web::json::value& errorObject)
|
||||
{
|
||||
return errorObject.has_string_field(L"error");
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring_view GetCode() const noexcept
|
||||
{
|
||||
return _code;
|
||||
}
|
||||
};
|
||||
|
||||
namespace ErrorCodes
|
||||
{
|
||||
static constexpr std::wstring_view AuthorizationPending{ L"authorization_pending" };
|
||||
static constexpr std::wstring_view InvalidGrant{ L"invalid_grant" };
|
||||
}
|
||||
}
|
||||
|
||||
#define THROW_IF_AZURE_ERROR(payload) \
|
||||
do \
|
||||
{ \
|
||||
if (AzureException::IsErrorPayload((payload))) \
|
||||
{ \
|
||||
throw AzureException((payload)); \
|
||||
} \
|
||||
} while (0)
|
||||
@@ -16,9 +16,13 @@
|
||||
|
||||
#include "AzureConnection.g.cpp"
|
||||
|
||||
#include "AzureClient.h"
|
||||
|
||||
#include "winrt/Windows.System.UserProfile.h"
|
||||
#include "../../types/inc/Utils.hpp"
|
||||
|
||||
using namespace ::Microsoft::Console;
|
||||
using namespace ::Microsoft::Terminal::Azure;
|
||||
|
||||
using namespace utility;
|
||||
using namespace web;
|
||||
@@ -40,7 +44,7 @@ static constexpr auto HttpUserAgent = L"Terminal/0.0";
|
||||
{ \
|
||||
return E_FAIL; \
|
||||
} \
|
||||
} while (0, 0)
|
||||
} while (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
|
||||
@@ -91,6 +95,27 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
_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)
|
||||
@@ -366,17 +391,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
case AzureState::NoConnect:
|
||||
{
|
||||
_WriteStringWithNewline(RS_(L"AzureInternetOrServerIssue"));
|
||||
_transitionToState(ConnectionState::Failed);
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
_state = AzureState::NoConnect;
|
||||
_WriteCaughtExceptionRecord();
|
||||
_transitionToState(ConnectionState::Failed);
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,12 +521,24 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
// Check if the token is close to expiring and refresh if so
|
||||
if (timeNow + _expireLimit > _expiry)
|
||||
{
|
||||
const auto refreshResponse = _RefreshTokens();
|
||||
_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());
|
||||
// Store the updated tokens under the same username
|
||||
_StoreCredential();
|
||||
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 S_FALSE;
|
||||
}
|
||||
throw; // rethrow. we couldn't handle this error.
|
||||
}
|
||||
}
|
||||
|
||||
// We have everything we need, so go ahead and connect
|
||||
@@ -532,17 +565,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
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;
|
||||
try
|
||||
{
|
||||
authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
_WriteStringWithNewline(RS_(L"AzureExitStr"));
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
json::value authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn);
|
||||
_accessToken = authenticatedResponse.at(L"access_token").as_string();
|
||||
_refreshToken = authenticatedResponse.at(L"refresh_token").as_string();
|
||||
|
||||
@@ -562,11 +585,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
std::tie(_tenantID, _displayName) = _crackTenant(chosenTenant);
|
||||
|
||||
// We have to refresh now that we have the tenantID
|
||||
const auto refreshResponse = _RefreshTokens();
|
||||
_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());
|
||||
|
||||
_RefreshTokens();
|
||||
_state = AzureState::StoreTokens;
|
||||
}
|
||||
else
|
||||
@@ -626,11 +645,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
std::tie(_tenantID, _displayName) = _crackTenant(chosenTenant);
|
||||
|
||||
// We have to refresh now that we have the tenantID
|
||||
const auto refreshResponse = _RefreshTokens();
|
||||
_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());
|
||||
|
||||
_RefreshTokens();
|
||||
_state = AzureState::StoreTokens;
|
||||
return S_OK;
|
||||
}
|
||||
@@ -724,19 +739,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
json::value AzureConnection::_RequestHelper(http_client theClient, http_request theRequest)
|
||||
{
|
||||
json::value jsonResult;
|
||||
try
|
||||
{
|
||||
const auto responseTask = theClient.request(theRequest);
|
||||
responseTask.wait();
|
||||
const auto response = responseTask.get();
|
||||
const auto responseJsonTask = response.extract_json();
|
||||
responseJsonTask.wait();
|
||||
jsonResult = responseJsonTask.get();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
_WriteStringWithNewline(RS_(L"AzureInternetOrServerIssue"));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -776,7 +786,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
// Continuously send a poll request until the user authenticates
|
||||
const auto body = hstring() + L"grant_type=device_code&resource=" + _wantedResource + L"&client_id=" + AzureClientID + L"&code=" + deviceCode;
|
||||
const auto requestUri = L"common/oauth2/token";
|
||||
json::value responseJson;
|
||||
for (int count = 0; count < expiresIn / pollInterval; count++)
|
||||
{
|
||||
// User might close the tab while we wait for them to authenticate, this case handles that
|
||||
@@ -784,28 +793,32 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
{
|
||||
throw "Tab closed.";
|
||||
}
|
||||
|
||||
http_request pollRequest(L"POST");
|
||||
pollRequest.set_request_uri(requestUri);
|
||||
pollRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
|
||||
|
||||
responseJson = _RequestHelper(pollingClient, pollRequest);
|
||||
|
||||
if (responseJson.has_field(L"error"))
|
||||
{
|
||||
Sleep(pollInterval * 1000); // Sleep takes arguments in milliseconds
|
||||
continue; // Still waiting for authentication
|
||||
}
|
||||
else
|
||||
try
|
||||
{
|
||||
auto response{ _RequestHelper(pollingClient, pollRequest) };
|
||||
_WriteStringWithNewline(RS_(L"AzureSuccessfullyAuthenticated"));
|
||||
break; // Authentication is done, break from loop
|
||||
// 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.
|
||||
Sleep(pollInterval * 1000);
|
||||
continue;
|
||||
}
|
||||
throw;
|
||||
} // uncaught exceptions bubble up to the caller
|
||||
}
|
||||
if (responseJson.has_field(L"error"))
|
||||
{
|
||||
throw "Time out.";
|
||||
}
|
||||
return responseJson;
|
||||
|
||||
throw "Time out.";
|
||||
return json::value::null();
|
||||
}
|
||||
|
||||
// Method description:
|
||||
@@ -830,7 +843,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
// - helper function to refresh the access/refresh tokens
|
||||
// Return value:
|
||||
// - the response with the new tokens
|
||||
json::value AzureConnection::_RefreshTokens()
|
||||
void AzureConnection::_RefreshTokens()
|
||||
{
|
||||
// Initialize the client
|
||||
http_client refreshClient(_loginUri);
|
||||
@@ -843,7 +856,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
refreshRequest.headers().add(L"User-Agent", HttpUserAgent);
|
||||
|
||||
// Send the request and return the response as a json value
|
||||
return _RequestHelper(refreshClient, refreshRequest);
|
||||
auto refreshResponse{ _RequestHelper(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:
|
||||
|
||||
@@ -40,7 +40,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
StoreTokens,
|
||||
TermConnecting,
|
||||
TermConnected,
|
||||
NoConnect
|
||||
};
|
||||
|
||||
AzureState _state{ AzureState::AccessStored };
|
||||
@@ -69,11 +68,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
||||
utility::string_t _terminalID;
|
||||
|
||||
void _WriteStringWithNewline(const std::wstring_view str);
|
||||
void _WriteCaughtExceptionRecord();
|
||||
web::json::value _RequestHelper(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);
|
||||
web::json::value _GetTenants();
|
||||
web::json::value _RefreshTokens();
|
||||
void _RefreshTokens();
|
||||
web::json::value _GetCloudShellUserSettings();
|
||||
utility::string_t _GetCloudShell();
|
||||
utility::string_t _GetTerminal(utility::string_t shellType);
|
||||
|
||||
@@ -123,15 +123,15 @@
|
||||
<data name="AzureEnterTenant" xml:space="preserve">
|
||||
<value>Please enter the desired tenant number.</value>
|
||||
</data>
|
||||
<data name="AzureNewLogin" xml:space="default">
|
||||
<data name="AzureNewLogin" xml:space="preserve">
|
||||
<value>Enter {0} to login with a new account</value>
|
||||
<comment>{0} will be replaced with the resource from AzureUserEntry_NewLogin; it is intended to be a single-character shorthand for "new account"</comment>
|
||||
</data>
|
||||
<data name="AzureRemoveStored" xml:space="default">
|
||||
<data name="AzureRemoveStored" xml:space="preserve">
|
||||
<value>Enter {0} to remove the above saved connection settings.</value>
|
||||
<comment>{0} will be replaced with the resource from AzureUserEntry_RemoveStored; it is intended to be a single-character shorthand for "remove stored"</comment>
|
||||
</data>
|
||||
<data name="AzureInvalidAccessInput" xml:space="default">
|
||||
<data name="AzureInvalidAccessInput" xml:space="preserve">
|
||||
<value>Please enter a valid number to access the stored connection settings, {0} to log in with a new account, or {1} to remove the saved connection settings.</value>
|
||||
<comment>{0} will be replaced with the resource from AzureUserEntry_NewLogin, and {1} will be replaced with AzureUserEntry_RemoveStored. This is an error message, used after AzureNewLogin/AzureRemoveStored if the user enters an invalid value.</comment>
|
||||
</data>
|
||||
@@ -148,11 +148,11 @@
|
||||
<value>You have not set up your cloud shell account yet. Please go to https://shell.azure.com to set it up.</value>
|
||||
<comment>{Locked="https://shell.azure.com"} This URL should not be localized. Everything else should.</comment>
|
||||
</data>
|
||||
<data name="AzureStorePrompt" xml:space="default">
|
||||
<data name="AzureStorePrompt" xml:space="preserve">
|
||||
<value>Do you want to save these connection settings for future logins? [{0}/{1}]</value>
|
||||
<comment>{0} and {1} will be replaced with AzureUserEntry_Yes and AzureUserEntry_No. They are single-character shorthands for "yes" and "no" in this language.</comment>
|
||||
</data>
|
||||
<data name="AzureInvalidStoreInput" xml:space="default">
|
||||
<data name="AzureInvalidStoreInput" xml:space="preserve">
|
||||
<value>Please enter {0} or {1}</value>
|
||||
<comment>{0} and {1} will be replaced with AzureUserEntry_Yes and AzureUserEntry_No. This resource will be used as an error response after AzureStorePrompt.</comment>
|
||||
</data>
|
||||
@@ -180,16 +180,13 @@
|
||||
<data name="AzureAuthString" xml:space="preserve">
|
||||
<value>Authenticated.</value>
|
||||
</data>
|
||||
<data name="AzureInternetOrServerIssue" xml:space="preserve">
|
||||
<value>Could not connect to Azure. You may not have internet or the server might be down.</value>
|
||||
</data>
|
||||
<data name="AzureOldCredentialsFlushedMessage" xml:space="preserve">
|
||||
<value>Authentication parameters changed. You'll need to log in again.</value>
|
||||
</data>
|
||||
<data name="AzureUnknownTenantName" xml:space="preserve">
|
||||
<value><unknown tenant name></value>
|
||||
</data>
|
||||
<data name="AzureIthTenant" xml:space="default">
|
||||
<data name="AzureIthTenant" xml:space="preserve">
|
||||
<value>Tenant {0}: {1} ({2})</value>
|
||||
<comment>{0} is the tenant's number, which the user will enter to connect to the tenant. {1} is the tenant's display name, which will be meaningful for the user. {2} is the tenant's internal ID number.</comment>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user