From 0404a14fb098d2c37da51756c2409a01ae5b8b19 Mon Sep 17 00:00:00 2001 From: "tim.carrington" Date: Fri, 8 May 2020 12:24:27 +0100 Subject: [PATCH] Update O365 channels to use the changes from SimplifyOfficeChannels as well as WinHTTP lib OneDrive now supports ordering of files - needs more testing Outlook works but needs to be commented and cleaned. --- .../Channels/OneDrive365RestFile.cpp | 364 +++++++++--------- .../Interfaces/Channels/OneDrive365RestFile.h | 17 +- .../Channels/Outlook365RestTask.cpp | 173 ++++----- .../Interfaces/Channels/Outlook365RestTask.h | 16 +- 4 files changed, 286 insertions(+), 284 deletions(-) diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp index b180703..d5833c4 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp +++ b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp @@ -1,13 +1,17 @@ #include "StdAfx.h" #include "OneDrive365RestFile.h" -#include "Common/FSecure/CppTools/Utils.h" -#include "Common/FSecure/Crypto/Base32.h" +#include "Common/FSecure/Crypto/Base64.h" #include "Common/FSecure/CppTools/ScopeGuard.h" #include "Common/json/json.hpp" +#include "Common/FSecure/CppTools/StringConversions.h" +#include "Common/FSecure/WinHttp/HttpClient.h" +#include "Common/FSecure/WinHttp/Constants.h" +#include "Common/FSecure/WinHttp/Uri.h" // Namespaces. using json = nlohmann::json; -using namespace utility::conversions; +using namespace FSecure::StringConversions; +using namespace FSecure::WinHttp; std::atomic FSecure::C3::Interfaces::Channels::OneDrive365RestFile::s_TimePoint = std::chrono::steady_clock::now(); @@ -15,31 +19,14 @@ std::atomic FSecure::C3::Interfaces::Chan FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OneDrive365RestFile(ByteView arguments) : m_InboundDirectionName{ arguments.Read()} , m_OutboundDirectionName{ arguments.Read()} + , m_username{ arguments.Read()} + , m_password{ arguments.Read()} + , m_clientKey{ arguments.Read()} { // Obtain proxy information and store it in the HTTP configuration. if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty()) - m_HttpConfig.set_proxy(winProxy == OBF(L"auto") ? web::web_proxy::use_auto_discovery : web::web_proxy(winProxy)); + this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy); - // Retrieve auth data. - std::string username, password, clientKey, clientSecret; - std::tie(username, password, clientKey, clientSecret) = arguments.Read(); - m_Password = to_utf16string(password); - FSecure::Utils::SecureMemzero(password.data(), password.size()); - - web::http::oauth2::experimental::oauth2_config oauth2Config( - to_utf16string(std::move(clientKey)), - to_utf16string(std::move(clientSecret)), - OBF(L"https://login.windows.net/common/oauth2/v2.0/authorize"), - OBF(L"https://login.windows.net/organizations/oauth2/v2.0/token"), - OBF(L""), - OBF(L"https://graph.microsoft.com/.default"), - to_utf16string(username) - ); - - // Set the above configuration in the HTTP configuration for cpprestsdk (it already includes proxy information from the code above). - m_HttpConfig.set_oauth2(std::move(oauth2Config)); - - // For simplicity access token is not a configuration parameter. Refresh token will be used to generate first access token. RefreshAccessToken(); } @@ -53,39 +40,50 @@ size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(B { // Construct the HTTP request auto URLwithFilename = OBF("https://graph.microsoft.com/v1.0/me/drive/root:/") + m_OutboundDirectionName + OBF("-") + FSecure::Utils::GenerateRandomString(20) + OBF(".txt") + OBF(":/content"); - web::http::client::http_client client(to_string_t(URLwithFilename), m_HttpConfig); - web::http::http_request request(web::http::methods::PUT); + HttpClient webClient(Convert(URLwithFilename), m_ProxyConfig); - // Data can be just sent as a text stream - request.set_body(cppcodec::base32_crockford::encode(&data.front(), data.size())); - request.headers().set_content_type(OBF(L"text/plain")); + HttpRequest request; + std::string auth = OBF("Bearer ") + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + request.m_Method = Method::PUT; + std::string dataBody = cppcodec::base64_rfc4648::encode(&data.front(), data.size()); - pplx::task task = client.request(request).then([&](web::http::http_response response) + auto now = std::chrono::high_resolution_clock::now().time_since_epoch(); + auto int_ms = std::chrono::duration_cast(now); + std::chrono::duration int_usec = int_ms; + + + json fileData; + fileData[OBF("timestamp")] = now.count(); + + fileData[OBF("data")] = dataBody; + std::string body = fileData.dump(); + + request.SetData(OBF(L"text/plain"), { body.begin(), body.end() }); + + auto resp = webClient.Request(request); + if (resp.GetStatusCode() != StatusCode::OK && resp.GetStatusCode() != StatusCode::Created) { - if (response.status_code() == web::http::status_codes::Created) - return; - - if (response.status_code() == web::http::status_codes::TooManyRequests) // break and set sleep time. + if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time. { - s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ stoul(response.headers().find(OBF(L"Retry-After"))->second) }; + s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s); throw std::runtime_error{ OBF("Too many requests") }; } - if(response.status_code() == web::http::status_codes::Unauthorized) + if (resp.GetStatusCode() == StatusCode::Unauthorized) { RefreshAccessToken(); throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; } - if (response.status_code() == web::http::status_codes::BadRequest) + if (resp.GetStatusCode() == StatusCode::BadRequest) { RefreshAccessToken(); throw std::runtime_error{ OBF("Bad Request") }; } - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(response.status_code()) }; - }); + throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; + } - task.wait(); return data.size(); } catch (std::exception& exception) @@ -96,12 +94,12 @@ size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(B } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnReceiveFromChannel() +std::vector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnReceiveFromChannel() { if (s_TimePoint.load() > std::chrono::steady_clock::now()) std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay)); - ByteVector packet; + std::vector packets; try { // Construct request to get files. @@ -109,111 +107,121 @@ FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnRe // However, this function does not appear to update in real time to reflect the status of what is actually in folders. // Therefore, we need to use a standard directory listing, and manually check the filenames to determine which files to request. // This directory listing fetches up to 1000 files. - web::http::client::http_client client(OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children?top=1000"), m_HttpConfig); - web::http::http_request request(web::http::methods::GET); + HttpClient webClient(OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children?top=1000"), m_ProxyConfig); + HttpRequest request; + std::string auth = OBF("Bearer ") + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); - pplx::task task = client.request(request).then([&](web::http::http_response response) + auto resp = webClient.Request(request); + + if (resp.GetStatusCode() != StatusCode::OK) { - if (response.status_code() == web::http::status_codes::OK) // ==200 - return response.extract_string(); - - if (response.status_code() == web::http::status_codes::TooManyRequests) // break and set sleep time. + if (resp.GetStatusCode() == StatusCode::TooManyRequests) { - s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ stoul(response.headers().find(OBF(L"Retry-After"))->second) }; + s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s); throw std::runtime_error{ OBF("Too many requests") }; } - if(response.status_code() == web::http::status_codes::Unauthorized) - { - RefreshAccessToken(); - throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; + if (resp.GetStatusCode() == StatusCode::Unauthorized) + { + RefreshAccessToken(); + throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; } - if (response.status_code() == web::http::status_codes::BadRequest) + + if (resp.GetStatusCode() == StatusCode::BadRequest) { RefreshAccessToken(); throw std::runtime_error{ OBF("Bad Request") }; } - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(response.status_code()) }; + throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; + } - }) - .then([&](pplx::task taskData) + json taskDataAsJSON; + try { - // Gracefully handle situation where there's an empty JSON value (e.g., a failed request) - if (taskData.get().empty()) - return; + taskDataAsJSON = json::parse(resp.GetData()); + } + catch (json::parse_error) + { + Log({ OBF("Failed to parse the list of received files."), LogMessage::Severity::Error }); + return {}; + } - // Convert response (as string_t to utf8) and parse. - json taskDataAsJSON; + //first iterate over the json and populate an array of the files we want. + std::vector elements; + for (auto& element : taskDataAsJSON.at(OBF("value"))) + { + // Obtain subject and task ID. + std::string filename = element.at(OBF("name")).get(); + std::string id = element.at(OBF("id")).get(); + + + // Verify that the full subject and ID were obtained. If not, ignore. + if (filename.empty() || id.empty()) + continue; + + // Check the direction component is at the start of name. + if (filename.find(m_InboundDirectionName)) + continue; + + //download the file + std::string downloadUrl = element.at(OBF("@microsoft.graph.downloadUrl")).get(); + HttpClient webClientFile(Convert(downloadUrl), m_ProxyConfig); + HttpRequest fileRequest; + + fileRequest.SetHeader(Header::Authorization, Convert(auth)); + + auto fileResp = webClientFile.Request(fileRequest); + std::string fileAsString; + + if (fileResp.GetStatusCode() == StatusCode::OK) + { + auto data = fileResp.GetData(); + fileAsString = std::string{ data.begin(), data.end() }; //We discussed bv.Read() but it wouldn't work here? + } + else + { + + Log({ OBF("Error request download url, got HTTP code ") + std::to_string(fileResp.GetStatusCode()), LogMessage::Severity::Error }); + return {}; //or continue?? + } + json j = json::parse(fileAsString); + j[OBF("id")] = id; + elements.push_back(j); + } + + //now sort and re-iterate over them. + std::sort(elements.begin(), elements.end(), [](auto const& a, auto const& b) -> bool { return a[OBF("timestamp")] < b[OBF("timestamp")]; }); + + for(auto &element : elements) + { + std::string id = element.at(OBF("id")).get(); + std::string base64Data = element.at(OBF("data")).get(); try { - taskDataAsJSON = json::parse(to_utf8string(taskData.get())); + packets.push_back(cppcodec::base64_rfc4648::decode(base64Data)); + RemoveFile(id); } - catch (json::parse_error) + catch (const cppcodec::parse_error& exception) { - Log({ OBF("Failed to parse the list of received files."), LogMessage::Severity::Error }); - return; + Log({ OBF("Error decoding task #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error }); + return {}; } - - for (auto& element : taskDataAsJSON.at(OBF("value"))) + catch (std::exception& exception) { - // Obtain subject and task ID. - std::string filename = element.at(OBF("name")).get(); - std::string id = element.at(OBF("id")).get(); - - // Verify that the full subject and ID were obtained. If not, ignore. - if (filename.empty() || id.empty()) - continue; - - // Check the direction component is at the start of name. - if (filename.find(m_InboundDirectionName)) - continue; - - web::http::client::http_client clientFile(to_string_t(element.at(OBF("@microsoft.graph.downloadUrl")).get()), m_HttpConfig); - std::string fileAsString; - - pplx::task fileRequest = clientFile.request({ web::http::methods::GET }).then([this, &fileAsString](web::http::http_response response) - { - if (response.status_code() == web::http::status_codes::OK) // ==200 - fileAsString = to_utf8string(response.extract_string().get()); - }); - - try - { - fileRequest.wait(); - } - catch (const web::http::http_exception& exception) - { - Log({ OBF_SEC("Caught a HTTP exception during OnReceive(): ") +exception.what(), LogMessage::Severity::Warning }); - continue; - } - - try - { - packet = cppcodec::base32_crockford::decode(fileAsString); - SCOPE_GUARD( RemoveFile(id); ); - return; - - } - catch (const cppcodec::parse_error& exception) - { - Log({ OBF("Error decoding task #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error }); - } - catch (std::exception& exception) - { - Log({ OBF("Caught a std::exception when processing file #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error }); - } + Log({ OBF("Caught a std::exception when processing file #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error }); + return {}; } - }); + } - task.wait(); } catch (std::exception& exception) { Log({ OBF_SEC("Caught a std::exception when running OnReceive(): ") + exception.what(), LogMessage::Severity::Warning }); } - return packet; + return packets; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -221,25 +229,24 @@ FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::Remo { try { - // Construct request. - auto client = web::http::client::http_client{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children"), m_HttpConfig }; - pplx::task task = client.request({ web::http::methods::GET }).then([this](web::http::http_response response) + HttpClient webClient(OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children"), m_ProxyConfig); + HttpRequest request; + std::string auth = OBF("Bearer ") + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + + auto resp = webClient.Request(request); + + if (resp.GetStatusCode() != StatusCode::OK) { - if (response.status_code() != web::http::status_codes::OK) - { - Log({ OBF("RemoveAllFiles() Error. Files could not be deleted. Confirm access and refresh tokens are correct."), LogMessage::Severity::Error }); - return; - } + Log({ OBF("RemoveAllFiles() Error. Files could not be deleted. Confirm access and refresh tokens are correct."), LogMessage::Severity::Error }); + return {}; + } - // Parse response (list of tasks) - auto taskDataAsJSON = nlohmann::json::parse(to_utf8string(response.extract_string().get())); + auto taskDataAsJSON = nlohmann::json::parse(resp.GetData()); - // For each task (under the "value" key), extract the ID, and send a request to delete the task. - for (auto& element : taskDataAsJSON.at(OBF("value"))) - RemoveFile(element.at(OBF("id")).get()); - }); - - task.wait(); + // For each task (under the "value" key), extract the ID, and send a request to delete the task. + for (auto& element : taskDataAsJSON.at(OBF("value"))) + RemoveFile(element.at(OBF("id")).get()); } catch (std::exception& exception) { @@ -254,42 +261,37 @@ void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RefreshAccessToken( { try { - auto oa2 = m_HttpConfig.oauth2(); - auto client = web::http::client::http_client(oa2->token_endpoint(), m_HttpConfig); - auto request = web::http::http_request(web::http::methods::POST); - request.headers().set_content_type(OBF(L"application/x-www-form-urlencoded")); + //Token endpoint + HttpClient webClient(OBF(L"https://login.windows.net/organizations/oauth2/v2.0/token"), m_ProxyConfig); + + HttpRequest request; // default request is GET + request.m_Method = Method::POST; + request.SetHeader(Header::ContentType, OBF(L"application/x-www-form-urlencoded; charset=utf-16")); + json data; + auto requestBody = ""s; requestBody += OBF("grant_type=password"); - requestBody += OBF("&scope="); - requestBody += to_utf8string(oa2->scope()); + requestBody += OBF("&scope=files.readwrite.all"); requestBody += OBF("&username="); - requestBody += to_utf8string(oa2->user_agent()); + requestBody += m_username; requestBody += OBF("&password="); - requestBody += to_utf8string(*m_Password.decrypt()); + requestBody += m_password; + //requestBody += to_utf8string(*m_Password.decrypt()); requestBody += OBF("&client_id="); - requestBody += to_utf8string(oa2->client_key()); - if (!oa2->client_secret().empty()) - { - requestBody += OBF("&client_secret="); - requestBody += to_utf8string(oa2->client_secret()); - } + requestBody += m_clientKey; - request.set_body(requestBody); + + + request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() }); FSecure::Utils::SecureMemzero(requestBody.data(), requestBody.size()); - pplx::task task = client.request(request).then([&](web::http::http_response response) - { - if (response.status_code() != web::http::status_codes::OK) - throw std::runtime_error{ OBF("Refresh access token request - non-200 status code was received: ") + std::to_string(response.status_code()) }; + auto resp = webClient.Request(request); - // If successful, parse the useful information from the response. - auto taskDataAsJSON = nlohmann::json::parse(to_utf8string(response.extract_string().get())); + if (resp.GetStatusCode() == StatusCode::OK) + data = json::parse(resp.GetData()); + else + throw std::runtime_error{ OBF("Refresh access token request - non-200 status code was received: ") + std::to_string(resp.GetStatusCode()) }; - auto tokenCopy = oa2->token(); - tokenCopy.set_access_token(to_string_t(taskDataAsJSON.at(OBF("access_token")).get())); - tokenCopy.set_expires_in(taskDataAsJSON.at(OBF("expires_in")).get()); - oa2->set_token(tokenCopy); - }); - task.wait(); + m_token = data[OBF("access_token")].get(); } catch (std::exception& exception) { @@ -297,19 +299,23 @@ void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RefreshAccessToken( } } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RemoveFile(std::string const& id) { - // There is a minor logic flaw in this part of the code, as it assumes the access token is still valid, which may not be the case. - auto URLwithID = OBF("https://graph.microsoft.com/v1.0/me/drive/items/") + id; - auto client = web::http::client::http_client{ to_string_t(URLwithID), m_HttpConfig }; - auto task = client.request({ web::http::methods::DEL }).then([&](web::http::http_response response) - { - if (response.status_code() > 205) - Log({ OBF("RemoveFile() Error. Task could not be deleted. HTTP response:") + std::to_string(response.status_code()), LogMessage::Severity::Error }); - }); + std::wstring url = OBF(L"https://graph.microsoft.com/v1.0/me/drive/items/") + Convert(id); + HttpClient webClient(url, m_ProxyConfig); + HttpRequest request; - task.wait(); + std::string auth = OBF("Bearer ") + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + request.m_Method = Method::DEL; + auto resp = webClient.Request(request); + + if (resp.GetStatusCode() > 205) + { + Log({ OBF("RemoveFile() Error. Task could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()), LogMessage::Severity::Error }); + } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -353,26 +359,21 @@ FSecure::ByteView FSecure::C3::Interfaces::Channels::OneDrive365RestFile::GetCap ], { "type": "string", - "name": "Username", + "name": "username", "min": 1, - "description": "User with Office 365 subscription." + "description": "The O365 user" }, { "type": "string", - "name": "Password", + "name": "password", "min": 1, - "description": "User password." + "description": "The user's password" }, { "type": "string", - "name": "Client key", + "name": "Client Key/ID", "min": 1, - "description": "Identifies the application (e.g. a GUID). User, or user admin must give consent for application to work in user context." - }, - { - "type": "string", - "name": "Client secret", - "description": "Leave empty if not required." + "description": "The GUID of the registered application." } ] }, @@ -388,3 +389,4 @@ FSecure::ByteView FSecure::C3::Interfaces::Channels::OneDrive365RestFile::GetCap } )_"; } + diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h index 89a5e6e..1d3a6ef 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h +++ b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h @@ -1,7 +1,7 @@ #pragma once -#include "Common/CppRestSdk/include/cpprest/http_client.h" //< For CppRestSdk. - +#include "Common/FSecure/WinHttp/WebProxy.h" +#include "Common/FSecure/WinHttp/Constants.h" //< For CppRestSdk. namespace FSecure::C3::Interfaces::Channels { /// Implementation of the OneDrive 365 REST file Channel. @@ -19,7 +19,7 @@ namespace FSecure::C3::Interfaces::Channels /// Reads a single C3 packet from Channel. /// @return packet retrieved from Channel. - ByteVector OnReceiveFromChannel(); + std::vector OnReceiveFromChannel(); /// Processes internal (C3 API) Command. /// @param command a buffer containing whole command and it's parameters. @@ -32,7 +32,7 @@ namespace FSecure::C3::Interfaces::Channels /// Values used as default for channel jitter. 30 ms if unset. Current jitter value can be changed at runtime. /// Set long delay otherwise O365 rate limit will heavily impact channel. - constexpr static std::chrono::milliseconds s_MinUpdateDelay = 3500ms, s_MaxUpdateDelay = 6500ms; + constexpr static std::chrono::milliseconds s_MinUpdateDelay = 1000ms, s_MaxUpdateDelay = 1000ms; protected: /// Removes all file from server. @@ -51,11 +51,12 @@ namespace FSecure::C3::Interfaces::Channels /// In/Out names on the server. std::string m_InboundDirectionName, m_OutboundDirectionName; - /// Stores HTTP configuration (proxy, OAuth, etc). - web::http::client::http_client_config m_HttpConfig; + /// Username, password, client key and token for authentication. + std::string m_username, m_password, m_clientKey, m_token; + + /// Store any relevant proxy info + WinHttp::WebProxy m_ProxyConfig; - /// Password for user with o365 subscription. - web::details::win32_encryption m_Password; /// Used to delay every channel instance in case of server rate limit. /// Set using information from 429 Too Many Requests header. diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp index 52e9e5b..427beb9 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp +++ b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp @@ -3,10 +3,15 @@ #include "Common/FSecure/Crypto/Base64.h" #include "Common/FSecure/CppTools/ScopeGuard.h" #include "Common/json/json.hpp" +#include "Common/FSecure/CppTools/StringConversions.h" +#include "Common/FSecure/WinHttp/HttpClient.h" +#include "Common/FSecure/WinHttp/Constants.h" +#include "Common/FSecure/WinHttp/Uri.h" // Namespaces. using json = nlohmann::json; -using namespace utility::conversions; +using namespace FSecure::StringConversions; +using namespace FSecure::WinHttp; std::atomic FSecure::C3::Interfaces::Channels::Outlook365RestTask::s_TimePoint = std::chrono::steady_clock::now(); @@ -14,31 +19,14 @@ std::atomic FSecure::C3::Interfaces::Chan FSecure::C3::Interfaces::Channels::Outlook365RestTask::Outlook365RestTask(ByteView arguments) : m_InboundDirectionName{ arguments.Read() } , m_OutboundDirectionName{ arguments.Read() } + , m_username{ arguments.Read() } + , m_Password{ arguments.Read() } + , m_clientKey{ arguments.Read() } { // Obtain proxy information and store it in the HTTP configuration. if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty()) - m_HttpConfig.set_proxy(winProxy == OBF(L"auto") ? web::web_proxy::use_auto_discovery : web::web_proxy(winProxy)); + this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy); - // Retrieve auth data. - std::string username, password, clientKey, clientSecret; - std::tie(username, password, clientKey, clientSecret) = arguments.Read(); - m_Password = to_utf16string(password); - FSecure::Utils::SecureMemzero(password.data(), password.size()); - - web::http::oauth2::experimental::oauth2_config oauth2Config( - to_utf16string(std::move(clientKey)), - to_utf16string(std::move(clientSecret)), - OBF(L"https://login.windows.net/common/oauth2/v2.0/authorize"), - OBF(L"https://login.windows.net/organizations/oauth2/v2.0/token"), - OBF(L""), - OBF(L"https://outlook.office365.com/.default"), - to_utf16string(username) - ); - - // Set the above configuration in the HTTP configuration for cpprestsdk (it already includes proxy information from the code above). - m_HttpConfig.set_oauth2(std::move(oauth2Config)); - - // For simplicity access token is not a configuration parameter. Refresh token will be used to generate first access token. RefreshAccessToken(); } @@ -51,40 +39,44 @@ size_t FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnSendToChannel(By try { // Construct the HTTP request - web::http::client::http_client client(OBF(L"https://outlook.office.com/api/v2.0/me/tasks"), m_HttpConfig); - web::http::http_request request(web::http::methods::POST); + HttpClient webClient(OBF(L"https://outlook.office.com/api/v2.0/me/tasks"), m_ProxyConfig); + + HttpRequest request; // default request is GET + std::string auth = "Bearer " + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + request.m_Method = Method::POST; // For the JSON body, take a simple approach and use only the required fields. json jsonBody; jsonBody[OBF("Subject")] = m_OutboundDirectionName; jsonBody[OBF("Body")][OBF("Content")] = cppcodec::base64_rfc4648::encode(&data.front(), data.size()); jsonBody[OBF("Body")][OBF("ContentType")] = OBF("Text"); + std::string body = jsonBody.dump(); + request.SetData(ContentType::ApplicationJson, { body.begin(), body.end() }); + request.m_ContentType = L"application/json"; - request.set_body(to_string_t(jsonBody.dump())); - request.headers().set_content_type(OBF(L"application/json")); + auto resp = webClient.Request(request); - web::http::http_response response = client.request(request).get(); - - if (response.status_code() != web::http::status_codes::Created) + if (resp.GetStatusCode() != StatusCode::OK && resp.GetStatusCode() != StatusCode::Created) { - if (response.status_code() == web::http::status_codes::TooManyRequests) // break and set sleep time. + if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time. { s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s); throw std::runtime_error{ OBF("Too many requests") }; } - if (response.status_code() == web::http::status_codes::Unauthorized) + if (resp.GetStatusCode() == StatusCode::Unauthorized) { RefreshAccessToken(); throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; } - if (response.status_code() == web::http::status_codes::BadRequest) + if (resp.GetStatusCode() == StatusCode::BadRequest) { RefreshAccessToken(); throw std::runtime_error{ OBF("Bad Request") }; } - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(response.status_code()) }; + throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; } return data.size(); @@ -94,6 +86,8 @@ size_t FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnSendToChannel(By Log({ OBF_SEC("Caught a std::exception when running OnSend(): ") + exception.what(), LogMessage::Severity::Warning }); return 0u; } + + return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -113,48 +107,50 @@ std::vector FSecure::C3::Interfaces::Channels::Outlook365Re URLwithInboundDirection += OBF("&filter=startswith(Subject,'"); // filter by subject URLwithInboundDirection += m_InboundDirectionName; // subject should contain m_InboundDirectionName URLwithInboundDirection += OBF("')&orderby=CreatedDateTime"); // order by creation date (oldest first) - web::http::client::http_client client(to_string_t(URLwithInboundDirection), m_HttpConfig); - web::http::http_request request(web::http::methods::GET); - web::http::http_response response = client.request(request).get(); + HttpClient webClient(Convert(URLwithInboundDirection), m_ProxyConfig); - if (response.status_code() != web::http::status_codes::OK) // ==200 + HttpRequest request; // default request is GET + std::string auth = "Bearer " + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + auto resp = webClient.Request(request); + + if (resp.GetStatusCode() != StatusCode::OK) { - if (response.status_code() == web::http::status_codes::TooManyRequests) // break and set sleep time. + if (resp.GetStatusCode() == StatusCode::TooManyRequests) { - s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ stoul(response.headers().find(OBF(L"Retry-After"))->second) }; + s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s); throw std::runtime_error{ OBF("Too many requests") }; } - if (response.status_code() == web::http::status_codes::Unauthorized) + if (resp.GetStatusCode() == StatusCode::Unauthorized) { RefreshAccessToken(); throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") }; } - if (response.status_code() == web::http::status_codes::BadRequest) + if (resp.GetStatusCode() == StatusCode::BadRequest) { RefreshAccessToken(); throw std::runtime_error{ OBF("Bad Request") }; } - throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(response.status_code()) }; + throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) }; } - std::string responseString = response.extract_utf8string().get(); - + auto responseData = resp.GetData(); // Gracefully handle situation where there's an empty JSON value (e.g., a failed request) - if (responseString.empty()) + if (responseData.size() == 0) return {}; // Convert response (as string_t to utf8) and parse. json taskDataAsJSON; try { - taskDataAsJSON = json::parse(responseString); + taskDataAsJSON = json::parse(responseData); } - catch (json::parse_error&) + catch (json::parse_error& err) { Log({ OBF("Failed to parse the list of received tasks."), LogMessage::Severity::Error }); return {}; @@ -202,21 +198,26 @@ std::vector FSecure::C3::Interfaces::Channels::Outlook365Re //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FSecure::ByteVector FSecure::C3::Interfaces::Channels::Outlook365RestTask::RemoveAllTasks(ByteView) { + try { // Construct request. One minor limitation of this is that it will remove 1000 tasks only (the maximum of "top"). This could be paged. - auto client = web::http::client::http_client{ OBF(L"https://outlook.office.com/api/v2.0/me/tasks?$top=1000"), m_HttpConfig }; + HttpClient webClient(OBF(L"https://outlook.office.com/api/v2.0/me/tasks?$top=1000"), m_ProxyConfig); - web::http::http_response response = client.request({ web::http::methods::GET }).get(); + HttpRequest request; // default request is GET + std::string auth = "Bearer " + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + auto resp = webClient.Request(request); - if (response.status_code() != web::http::status_codes::OK) + + if (resp.GetStatusCode() != StatusCode::OK) { Log({ OBF("RemoveAllFiles() Error. Files could not be deleted. Confirm access and refresh tokens are correct."), LogMessage::Severity::Error }); return {}; } // Parse response (list of tasks) - auto taskDataAsJSON = nlohmann::json::parse(to_utf8string(response.extract_utf8string().get())); + auto taskDataAsJSON = nlohmann::json::parse(resp.GetData()); // For each task (under the "value" key), extract the ID, and send a request to delete the task. for (auto& element : taskDataAsJSON.at(OBF("value"))) @@ -233,13 +234,21 @@ FSecure::ByteVector FSecure::C3::Interfaces::Channels::Outlook365RestTask::Remov //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void FSecure::C3::Interfaces::Channels::Outlook365RestTask::RemoveTask(std::string const& id) { + // There is a minor logic flaw in this part of the code, as it assumes the access token is still valid, which may not be the case. auto URLwithID = OBF("https://outlook.office.com/api/v2.0/me/tasks('") + id + OBF("')"); - auto client = web::http::client::http_client{ to_string_t(URLwithID), m_HttpConfig }; - web::http::http_response response = client.request({ web::http::methods::DEL }).get(); + HttpClient webClient(Convert(URLwithID), m_ProxyConfig); - if (response.status_code() > 205) - Log({ OBF("RemoveTask() Error. Task could not be deleted. HTTP response:") + std::to_string(response.status_code()), LogMessage::Severity::Error }); + HttpRequest request; // default request is GET + std::string auth = "Bearer " + m_token; + request.SetHeader(Header::Authorization, Convert(auth)); + request.m_Method = Method::DEL; + auto resp = webClient.Request(request); + + if (resp.GetStatusCode() > 205) + Log({ OBF("RemoveTask() Error. Task could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()), LogMessage::Severity::Error }); + + return; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -247,41 +256,34 @@ void FSecure::C3::Interfaces::Channels::Outlook365RestTask::RefreshAccessToken() { try { - auto oa2 = m_HttpConfig.oauth2(); - auto client = web::http::client::http_client(oa2->token_endpoint(), m_HttpConfig); - auto request = web::http::http_request(web::http::methods::POST); - request.headers().set_content_type(OBF(L"application/x-www-form-urlencoded")); + //Token endpoint + HttpClient webClient(Convert("https://login.windows.net/organizations/oauth2/v2.0/token"), m_ProxyConfig); + + HttpRequest request; // default request is GET + request.m_Method = Method::POST; + request.SetHeader(Header::ContentType, L"application/x-www-form-urlencoded; charset=utf-16");//ContentType::ApplicationXWwwFormUrlencoded); + json data; + auto requestBody = ""s; requestBody += OBF("grant_type=password"); - requestBody += OBF("&scope="); - requestBody += to_utf8string(oa2->scope()); + requestBody += OBF("&scope=https://outlook.office365.com/.default"); requestBody += OBF("&username="); - requestBody += to_utf8string(oa2->user_agent()); + requestBody += m_username; requestBody += OBF("&password="); - requestBody += to_utf8string(*m_Password.decrypt()); + requestBody += m_Password; requestBody += OBF("&client_id="); - requestBody += to_utf8string(oa2->client_key()); - if (!oa2->client_secret().empty()) - { - requestBody += OBF("&client_secret="); - requestBody += to_utf8string(oa2->client_secret()); - } + requestBody += m_clientKey; - request.set_body(requestBody); + request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() }); FSecure::Utils::SecureMemzero(requestBody.data(), requestBody.size()); + auto resp = webClient.Request(request); - web::http::http_response response = client.request(request).get(); + if (resp.GetStatusCode() == StatusCode::OK) + data = json::parse(resp.GetData()); + else + throw std::runtime_error{ OBF("Refresh access token request - non-200 status code was received: ") + std::to_string(resp.GetStatusCode()) }; - if (response.status_code() != web::http::status_codes::OK) - throw std::runtime_error{ OBF("Refresh access token request - non-200 status code was received: ") + std::to_string(response.status_code()) }; - - // If successful, parse the useful information from the response. - auto taskDataAsJSON = nlohmann::json::parse(to_utf8string(response.extract_utf8string().get())); - - auto tokenCopy = oa2->token(); - tokenCopy.set_access_token(to_string_t(taskDataAsJSON.at(OBF("access_token")).get())); - tokenCopy.set_expires_in(taskDataAsJSON.at(OBF("expires_in")).get()); - oa2->set_token(tokenCopy); + m_token = data["access_token"].get(); } catch (std::exception& exception) { @@ -330,13 +332,13 @@ FSecure::ByteView FSecure::C3::Interfaces::Channels::Outlook365RestTask::GetCapa { "type": "string", "name": "Username", - "min": 1, + "min": 0, "description": "User with Office 365 subscription." }, { "type": "string", "name": "Password", - "min": 1, + "min": 0, "description": "User password." }, { @@ -344,11 +346,6 @@ FSecure::ByteView FSecure::C3::Interfaces::Channels::Outlook365RestTask::GetCapa "name": "Client key", "min": 1, "description": "Identifies the application (e.g. a GUID). User, or user admin must give consent for application to work in user context." - }, - { - "type": "string", - "name": "Client secret", - "description": "Leave empty if not required." } ] }, diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h index 836b099..eff00f4 100644 --- a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h +++ b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h @@ -1,6 +1,7 @@ #pragma once -#include "Common/CppRestSdk/include/cpprest/http_client.h" //< For CppRestSdk. +#include "Common/FSecure/WinHttp/WebProxy.h" +#include "Common/FSecure/WinHttp/Constants.h" //< For CppRestSdk. namespace FSecure::C3::Interfaces::Channels { @@ -32,7 +33,7 @@ namespace FSecure::C3::Interfaces::Channels /// Values used as default for channel jitter. 30 ms if unset. Current jitter value can be changed at runtime. /// Set long delay otherwise O365 rate limit will heavily impact channel. - constexpr static std::chrono::milliseconds s_MinUpdateDelay = 3500ms, s_MaxUpdateDelay = 6500ms; + constexpr static std::chrono::milliseconds s_MinUpdateDelay = 1000ms, s_MaxUpdateDelay = 1000ms; protected: @@ -53,13 +54,14 @@ namespace FSecure::C3::Interfaces::Channels std::string m_InboundDirectionName, m_OutboundDirectionName; /// Stores HTTP configuration (proxy, OAuth, etc). - web::http::client::http_client_config m_HttpConfig; - - /// Password for user with o365 subscription. - web::details::win32_encryption m_Password; - + WinHttp::WebProxy m_ProxyConfig; + std::string m_username; + std::string m_Password; + std::string m_clientKey; + std::string m_token; /// Used to delay every channel instance in case of server rate limit. /// Set using information from 429 Too Many Requests header. static std::atomic s_TimePoint; }; } +