diff --git a/Src/Common/Common.vcxitems b/Src/Common/Common.vcxitems
index 088dc66..877e49f 100644
--- a/Src/Common/Common.vcxitems
+++ b/Src/Common/Common.vcxitems
@@ -52,6 +52,7 @@
+
diff --git a/Src/Common/Common.vcxitems.filters b/Src/Common/Common.vcxitems.filters
index b56e386..a6a7e78 100644
--- a/Src/Common/Common.vcxitems.filters
+++ b/Src/Common/Common.vcxitems.filters
@@ -107,5 +107,6 @@
+
\ No newline at end of file
diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Office365.h b/Src/Common/FSecure/C3/Interfaces/Channels/Office365.h
new file mode 100644
index 0000000..72a5d0a
--- /dev/null
+++ b/Src/Common/FSecure/C3/Interfaces/Channels/Office365.h
@@ -0,0 +1,233 @@
+#pragma once
+
+#include "Common/FSecure/WinHttp/HttpClient.h"
+#include "Common/FSecure/WinHttp/HttpRequest.h"
+#include "Common/FSecure/WinHttp/WebProxy.h"
+#include "Common/FSecure/WinHttp/Constants.h"
+#include "Common/FSecure/Crypto/String.h"
+
+namespace FSecure::C3::Interfaces::Channels
+{
+ /// Abstract of using Office365 API
+ template
+ class Office365
+ {
+ public:
+ /// Public constructor.
+ /// @param arguments factory arguments.
+ Office365(ByteView arguments)
+ : m_InboundDirectionName{ arguments.Read() }
+ , m_OutboundDirectionName{ arguments.Read() }
+ , m_Username{ arguments.Read() }
+ , m_Password{ arguments.Read() }
+ , m_ClientKey{ arguments.Read() }
+ {
+ FSecure::Utils::DisallowChars({ m_InboundDirectionName, m_OutboundDirectionName }, OBF(R"(;/?:@&=+$,)"));
+
+ // Obtain proxy information and store it in the HTTP configuration.
+ if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty())
+ this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy);
+
+ RefreshAccessToken();
+ }
+
+ /// Get channel capability.
+ /// @returns ByteView view of channel capability.
+ static ByteView GetCapability();
+
+ protected:
+ /// Remove one file from server.
+ /// @param id of task.
+ void RemoveFile(std::string const& id)
+ {
+ auto webClient = HttpClient{ Convert(Derived::ItemEndpont.Decrypt() + SecureString{id}), m_ProxyConfig };
+ auto request = CreateAuthRequest(Method::DEL);
+ auto resp = webClient.Request(request);
+
+ if (resp.GetStatusCode() > 205)
+ throw std::runtime_error{ OBF("RemoveFile() Error. Task ") + id + OBF(" could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()) };
+ }
+
+ /// Removes all file from server.
+ /// @param ByteView unused.
+ /// @returns ByteVector empty vector.
+ void RemoveAllFiles()
+ {
+ auto fileList = ListData();
+ for (auto& element : fileList.at(OBF("value")))
+ RemoveFile(element.at(OBF("id")).get());
+ }
+
+ /// Requests a new access token using the refresh token
+ /// @throws std::exception if token cannot be refreshed.
+ void RefreshAccessToken()
+ {
+ try
+ {
+ //Token endpoint
+ auto webClient = HttpClient{ Convert(Derived::TokenEndpoit.Decrypt()), m_ProxyConfig };
+
+ auto request = HttpRequest{ Method::POST };
+ request.SetHeader(Header::ContentType, OBF(L"application/x-www-form-urlencoded; charset=utf-16"));
+
+ auto requestBody = SecureString{};
+ requestBody += OBF("grant_type=password");
+ requestBody += OBF("&scope=");
+ requestBody += Derived::Scope.Decrypt();
+ requestBody += OBF("&username=");
+ requestBody += m_Username.Decrypt();
+ requestBody += OBF("&password=");
+ requestBody += m_Password.Decrypt();
+ requestBody += OBF("&client_id=");
+ requestBody += m_ClientKey.Decrypt();
+
+ request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() });
+ auto resp = webClient.Request(request);
+ EvaluateResponse(resp, false);
+
+ auto data = json::parse(resp.GetData());
+ m_Token = data[OBF("access_token")].get();
+ }
+ catch (std::exception & exception)
+ {
+ throw std::runtime_error{ OBF_STR("Cannot refresh token: ") + exception.what() };
+ }
+ }
+
+ /// Check if request was successful.
+ /// @throws std::exception describing incorrect response if occurred.
+ void EvaluateResponse(WinHttp::HttpResponse const& resp, bool tryRefreshingToken = true)
+ {
+
+ if (resp.GetStatusCode() == StatusCode::OK || resp.GetStatusCode() == StatusCode::Created)
+ return;
+
+ if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time.
+ {
+ auto retryAfterHeader = resp.GetHeader(Header::RetryAfter);
+ auto delay = !retryAfterHeader.empty() ? stoul(retryAfterHeader) : FSecure::Utils::GenerateRandomValue(10, 20);
+ s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ delay };
+ throw std::runtime_error{ OBF("Too many requests") };
+ }
+
+ if (resp.GetStatusCode() == StatusCode::Unauthorized)
+ {
+ if (tryRefreshingToken)
+ RefreshAccessToken();
+ throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") };
+ }
+
+ if (resp.GetStatusCode() == StatusCode::BadRequest)
+ throw std::runtime_error{ OBF("Bad Request") };
+
+ throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) };
+ }
+
+ /// Create request using internally stored token.
+ /// @param method, request type.
+ WinHttp::HttpRequest CreateAuthRequest(WinHttp::Method method = WinHttp::Method::GET)
+ {
+ auto request = HttpRequest{ method };
+ request.SetHeader(Header::Authorization, Convert(OBF_SEC("Bearer ") + m_Token.Decrypt()));
+ return request;
+ }
+
+ /// Server will reject request send before s_TimePoint. This method will await to this point plus extra random delay.
+ /// @param min lower limit of random delay.
+ /// @param max upper limit of random delay.
+ void RateLimitDelay(std::chrono::milliseconds min, std::chrono::milliseconds max)
+ {
+ if (s_TimePoint.load() > std::chrono::steady_clock::now())
+ std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(min, max));
+ }
+
+ /// List files on server.
+ /// @param filter flags to filter data from server.
+ /// @return json server response.
+ json ListData(std::string_view filter = {})
+ {
+ auto webClient = HttpClient{ Convert(Derived::ListEndpoint.Decrypt() + SecureString{ filter }), m_ProxyConfig };
+ auto request = CreateAuthRequest();
+ auto resp = webClient.Request(request);
+ EvaluateResponse(resp);
+
+ return json::parse(resp.GetData());
+ }
+
+ /// In/Out names on the server.
+ std::string m_InboundDirectionName, m_OutboundDirectionName;
+
+ /// Username, password, client key and token for authentication.
+ Crypto::String m_Username, m_Password, m_ClientKey, m_Token;
+
+ /// Store any relevant proxy info
+ WinHttp::WebProxy m_ProxyConfig;
+
+ /// 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;
+ };
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+template
+std::atomic FSecure::C3::Interfaces::Channels::Office365::s_TimePoint = std::chrono::steady_clock::now();
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+template
+FSecure::ByteView FSecure::C3::Interfaces::Channels::Office365::GetCapability()
+{
+ return R"_(
+{
+ "create":
+ {
+ "arguments":
+ [
+ [
+ {
+ "type": "string",
+ "name": "Input ID",
+ "min": 4,
+ "randomize": true,
+ "description": "Used to distinguish packets for the channel"
+ },
+ {
+ "type": "string",
+ "name": "Output ID",
+ "min": 4,
+ "randomize": true,
+ "description": "Used to distinguish packets from the channel"
+ }
+ ],
+ {
+ "type": "string",
+ "name": "username",
+ "min": 1,
+ "description": "The O365 user"
+ },
+ {
+ "type": "string",
+ "name": "password",
+ "min": 1,
+ "description": "The user's password"
+ },
+ {
+ "type": "string",
+ "name": "Client Key/ID",
+ "min": 1,
+ "description": "The GUID of the registered application."
+ }
+ ]
+ },
+ "commands":
+ [
+ {
+ "name": "Clear channel",
+ "id": 0,
+ "description": "Clearing old files from server. May increase bandwidth",
+ "arguments": []
+ }
+ ]
+}
+)_";
+}
diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp
index 6ca3b9a..3d334cb 100644
--- a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp
+++ b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.cpp
@@ -13,35 +13,22 @@ using json = nlohmann::json;
using namespace FSecure::StringConversions;
using namespace FSecure::WinHttp;
-std::atomic FSecure::C3::Interfaces::Channels::OneDrive365RestFile::s_TimePoint = std::chrono::steady_clock::now();
-
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-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() }
-{
- FSecure::Utils::DisallowChars({ m_InboundDirectionName, m_OutboundDirectionName }, OBF(R"(;/?:@&=+$,)"));
-
- // Obtain proxy information and store it in the HTTP configuration.
- if (auto winProxy = WinTools::GetProxyConfiguration(); !winProxy.empty())
- this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy);
-
- RefreshAccessToken();
-}
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RootEndpoint = OBF("https://graph.microsoft.com/v1.0/me/drive/root:/");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::ItemEndpont = OBF("https://graph.microsoft.com/v1.0/me/drive/items/");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::ListEndpoint = OBF("https://graph.microsoft.com/v1.0/me/drive/root/children");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::TokenEndpoit = OBF("https://login.windows.net/organizations/oauth2/v2.0/token");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::OneDrive365RestFile::Scope = OBF("files.readwrite.all");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(ByteView data)
{
- if (s_TimePoint.load() > std::chrono::steady_clock::now())
- std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay));
+ RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay);
try
{
// Construct the HTTP request
- auto URLwithFilename = OBF("https://graph.microsoft.com/v1.0/me/drive/root:/") + m_OutboundDirectionName + OBF("-") + FSecure::Utils::GenerateRandomString(20) + OBF(".json") + OBF(":/content");
+ auto URLwithFilename = RootEndpoint.Decrypt().c_str() + m_OutboundDirectionName + OBF("-") + FSecure::Utils::GenerateRandomString(20) + OBF(".json") + OBF(":/content");
auto webClient = HttpClient{ Convert(URLwithFilename), m_ProxyConfig };
auto request = CreateAuthRequest(Method::PUT);
@@ -52,7 +39,7 @@ size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(B
fileData[OBF("data")] = cppcodec::base64_rfc4648::encode(&data.front(), chunkSize);
auto body = fileData.dump();
- request.SetData(OBF(L"text/plain"), { body.begin(), body.end() });
+ request.SetData(ContentType::TextPlain, { body.begin(), body.end() });
EvaluateResponse(webClient.Request(request));
return chunkSize;
@@ -67,25 +54,20 @@ size_t FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnSendToChannel(B
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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));
+ RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay);
auto packets = std::vector{};
try
{
- auto webClient = HttpClient{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children?top=1000&filter=startswith(name,'") + Convert(m_InboundDirectionName) + OBF(L"')"), m_ProxyConfig };
- auto request = CreateAuthRequest(); HttpRequest{};
- auto resp = webClient.Request(request);
- EvaluateResponse(resp);
-
- auto taskDataAsJSON = json::parse(resp.GetData());
+ auto fileList = ListData(OBF("?top=1000&filter=startswith(name,'") + m_InboundDirectionName + OBF("')"));
// First iterate over the json and populate an array of the files we want.
auto elements = std::vector{};
- for (auto& element : taskDataAsJSON.at(OBF("value")))
+ for (auto& element : fileList.at(OBF("value")))
{
//download the file
auto webClientFile = HttpClient{ Convert(element.at(OBF("@microsoft.graph.downloadUrl")).get()), m_ProxyConfig };
+ auto request = CreateAuthRequest();
auto resp = webClientFile.Request(request);
EvaluateResponse(resp);
@@ -114,82 +96,6 @@ std::vector FSecure::C3::Interfaces::Channels::OneDrive365R
return packets;
}
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RemoveFile(std::string const& id)
-{
- auto webClient = HttpClient{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/items/") + Convert(id), m_ProxyConfig };
- auto request = CreateAuthRequest(Method::DEL);
- auto resp = webClient.Request(request);
-
- if (resp.GetStatusCode() > 205)
- throw std::runtime_error{ OBF("RemoveFile() Error. Task ") + id + OBF(" could not be deleted. HTTP response:") + std::to_string(resp.GetStatusCode()) };
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RemoveAllFiles(ByteView)
-{
- auto webClient = HttpClient{ OBF(L"https://graph.microsoft.com/v1.0/me/drive/root/children"), m_ProxyConfig };
- auto request = CreateAuthRequest();
- auto resp = webClient.Request(request);
- try
- {
- EvaluateResponse(resp);
- }
- catch (const std::exception & exception)
- {
- Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + exception.what(), LogMessage::Severity::Error });
- return {};
- }
-
- // For each task (under the "value" key), extract the ID, and send a request to delete the task.
- auto taskDataAsJSON = nlohmann::json::parse(resp.GetData());
- for (auto& element : taskDataAsJSON.at(OBF("value")))
- try
- {
- RemoveFile(element.at(OBF("id")).get());
- }
- catch (const std::exception& exception)
- {
- Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + exception.what(), LogMessage::Severity::Error });
- }
-
- return {};
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::RefreshAccessToken()
-{
- try
- {
- //Token endpoint
- auto webClient = HttpClient{ OBF(L"https://login.windows.net/organizations/oauth2/v2.0/token"), m_ProxyConfig };
-
- auto request = HttpRequest{ Method::POST };
- request.SetHeader(Header::ContentType, OBF(L"application/x-www-form-urlencoded; charset=utf-16"));
-
- auto requestBody = SecureString{};
- requestBody += OBF("grant_type=password");
- requestBody += OBF("&scope=files.readwrite.all");
- requestBody += OBF("&username=");
- requestBody += m_Username.Decrypt();
- requestBody += OBF("&password=");
- requestBody += m_Password.Decrypt();
- requestBody += OBF("&client_id=");
- requestBody += m_ClientKey.Decrypt();
-
- request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() });
- auto resp = webClient.Request(request);
- EvaluateResponse(resp, false);
-
- auto data = json::parse(resp.GetData());
- m_Token = data[OBF("access_token")].get();
- }
- catch (std::exception& exception)
- {
- throw std::runtime_error{ OBF_STR("Cannot refresh token: ") + exception.what() };
- }
-}
-
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnRunCommand(ByteView command)
{
@@ -197,102 +103,16 @@ FSecure::ByteVector FSecure::C3::Interfaces::Channels::OneDrive365RestFile::OnRu
switch (command.Read())
{
case 0:
- return RemoveAllFiles(command);
+ try
+ {
+ RemoveAllFiles();
+ }
+ catch (std::exception const& e)
+ {
+ Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + e.what(), LogMessage::Severity::Error });
+ }
+ return {};
default:
return AbstractChannel::OnRunCommand(commandCopy);
}
}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-void FSecure::C3::Interfaces::Channels::OneDrive365RestFile::EvaluateResponse(WinHttp::HttpResponse const& resp, bool tryRefreshingToken)
-{
-
- if (resp.GetStatusCode() == StatusCode::OK || resp.GetStatusCode() == StatusCode::Created)
- return;
-
- if (resp.GetStatusCode() == StatusCode::TooManyRequests) // break and set sleep time.
- {
- auto retryAfterHeader = resp.GetHeader(Header::RetryAfter);
- auto delay = !retryAfterHeader.empty() ? stoul(retryAfterHeader) : FSecure::Utils::GenerateRandomValue(10, 20);
- s_TimePoint = std::chrono::steady_clock::now() + std::chrono::seconds{ delay };
- throw std::runtime_error{ OBF("Too many requests") };
- }
-
- if (resp.GetStatusCode() == StatusCode::Unauthorized)
- {
- if (tryRefreshingToken)
- RefreshAccessToken();
- throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") };
- }
-
- if (resp.GetStatusCode() == StatusCode::BadRequest)
- throw std::runtime_error{ OBF("Bad Request") };
-
- throw std::runtime_error{ OBF("Non 200 http response.") + std::to_string(resp.GetStatusCode()) };
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-FSecure::WinHttp::HttpRequest FSecure::C3::Interfaces::Channels::OneDrive365RestFile::CreateAuthRequest(WinHttp::Method method)
-{
- auto request = HttpRequest{ method };
- request.SetHeader(Header::Authorization, Convert(OBF_SEC("Bearer ") + m_Token.Decrypt()));
- return request;
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-FSecure::ByteView FSecure::C3::Interfaces::Channels::OneDrive365RestFile::GetCapability()
-{
- return R"_(
-{
- "create":
- {
- "arguments":
- [
- [
- {
- "type": "string",
- "name": "Input ID",
- "min": 4,
- "randomize": true,
- "description": "Used to distinguish packets for the channel"
- },
- {
- "type": "string",
- "name": "Output ID",
- "min": 4,
- "randomize": true,
- "description": "Used to distinguish packets from the channel"
- }
- ],
- {
- "type": "string",
- "name": "username",
- "min": 1,
- "description": "The O365 user"
- },
- {
- "type": "string",
- "name": "password",
- "min": 1,
- "description": "The user's password"
- },
- {
- "type": "string",
- "name": "Client Key/ID",
- "min": 1,
- "description": "The GUID of the registered application."
- }
- ]
- },
- "commands":
- [
- {
- "name": "Clear channel",
- "id": 0,
- "description": "Clearing old files from server. May increase bandwidth",
- "arguments": []
- }
- ]
-}
-)_";
-}
diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h
index 1f79bbe..938b53d 100644
--- a/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h
+++ b/Src/Common/FSecure/C3/Interfaces/Channels/OneDrive365RestFile.h
@@ -1,20 +1,14 @@
#pragma once
-#include "Common/FSecure/WinHttp/HttpClient.h"
-#include "Common/FSecure/WinHttp/HttpRequest.h"
-#include "Common/FSecure/WinHttp/WebProxy.h"
-#include "Common/FSecure/WinHttp/Constants.h"
-#include "Common/FSecure/Crypto/String.h"
+#include "Office365.h"
namespace FSecure::C3::Interfaces::Channels
{
- /// Implementation of the OneDrive 365 REST file Channel.
- class OneDrive365RestFile : public Channel
+ class OneDrive365RestFile : public Channel, public Office365
{
public:
- /// Public constructor.
- /// @param arguments factory arguments.
- OneDrive365RestFile(ByteView arguments);
+ /// Use Office365 constructor.
+ using Office365::Office365;
/// OnSend callback implementation.
/// @param blob data to send to Channel.
@@ -30,48 +24,17 @@ namespace FSecure::C3::Interfaces::Channels
/// @return command result.
ByteVector OnRunCommand(ByteView command) override;
- /// Get channel capability.
- /// @returns ByteView view of channel capability.
- static ByteView GetCapability();
-
/// 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 = 1000ms, s_MaxUpdateDelay = 1000ms;
- protected:
- /// Removes all file from server.
- /// @param ByteView unused.
- /// @returns ByteVector empty vector.
- ByteVector RemoveAllFiles(ByteView);
+ /// Endpoint used to add file to OneDrive
+ static Crypto::String RootEndpoint;
- /// Remove one file from server.
- /// @param id of task.
- void RemoveFile(std::string const& id);
-
- /// Requests a new access token using the refresh token
- /// @throws std::exception if token cannot be refreshed.
- void RefreshAccessToken();
-
- /// Check if request was successful.
- /// @throws std::exception describing incorrect response if occurred.
- void EvaluateResponse(WinHttp::HttpResponse const& resp, bool tryRefreshingToken = true);
-
- /// Create request using internally stored token.
- /// @param method, request type.
- WinHttp::HttpRequest CreateAuthRequest(WinHttp::Method method = WinHttp::Method::GET);
-
- /// In/Out names on the server.
- std::string m_InboundDirectionName, m_OutboundDirectionName;
-
- /// Username, password, client key and token for authentication.
- Crypto::String m_Username, m_Password, m_ClientKey, m_Token;
-
- /// Store any relevant proxy info
- WinHttp::WebProxy m_ProxyConfig;
-
-
- /// 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;
+ /// Endpoints used by Office365 methods.
+ static Crypto::String ItemEndpont;
+ static Crypto::String ListEndpoint;
+ static Crypto::String TokenEndpoit;
+ static Crypto::String Scope;
};
}
diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp
index db0ef17..527cc7f 100644
--- a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp
+++ b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.cpp
@@ -8,184 +8,62 @@
#include "Common/FSecure/WinHttp/Constants.h"
#include "Common/FSecure/WinHttp/Uri.h"
-// Namespaces.
+// Namespaces
using json = nlohmann::json;
using namespace FSecure::StringConversions;
using namespace FSecure::WinHttp;
-std::atomic FSecure::C3::Interfaces::Channels::Outlook365RestTask::s_TimePoint = std::chrono::steady_clock::now();
-
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-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())
- this->m_ProxyConfig = (winProxy == OBF(L"auto")) ? WebProxy(WebProxy::Mode::UseAutoDiscovery) : WebProxy(winProxy);
-
- RefreshAccessToken();
-}
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::ItemEndpont = OBF("https://outlook.office.com/api/v2.0/me/tasks/");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::ListEndpoint = OBF("https://outlook.office.com/api/v2.0/me/tasks");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::TokenEndpoit = OBF("https://login.windows.net/organizations/oauth2/v2.0/token/");
+FSecure::Crypto::String FSecure::C3::Interfaces::Channels::Outlook365RestTask::Scope = OBF("https://outlook.office365.com/.default");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
size_t FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnSendToChannel(ByteView data)
{
- if (s_TimePoint.load() > std::chrono::steady_clock::now())
- std::this_thread::sleep_until(s_TimePoint.load() + FSecure::Utils::GenerateRandomValue(m_MinUpdateDelay, m_MaxUpdateDelay));
+ RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay);
try
{
// Construct the HTTP request
- HttpClient webClient(OBF(L"https://outlook.office.com/api/v2.0/me/tasks"), m_ProxyConfig);
+ auto webClient = HttpClient{ Convert(ItemEndpont.Decrypt()), m_ProxyConfig };
+ auto request = CreateAuthRequest(Method::POST);
- HttpRequest request; // default request is GET
- std::string auth = "Bearer " + m_Token;
- request.SetHeader(Header::Authorization, Convert(auth));
- request.m_Method = Method::POST;
+ auto chunkSize = std::min(data.size(), 3 * 1024 * 1024); // Send max 4 MB. base64 will expand data by 4/3.
+ auto fileData = json();
+ fileData[OBF("Subject")] = m_OutboundDirectionName;
+ fileData[OBF("Body")][OBF("Content")] = cppcodec::base64_rfc4648::encode(&data.front(), data.size());
+ fileData[OBF("Body")][OBF("ContentType")] = OBF("Text");
- // 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();
+ auto body = fileData.dump();
request.SetData(ContentType::ApplicationJson, { body.begin(), body.end() });
- request.m_ContentType = L"application/json";
+ EvaluateResponse(webClient.Request(request));
- auto resp = webClient.Request(request);
-
- if (resp.GetStatusCode() != StatusCode::OK && resp.GetStatusCode() != StatusCode::Created)
- {
- 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 (resp.GetStatusCode() == StatusCode::Unauthorized)
- {
- RefreshAccessToken();
- throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") };
-
- }
- 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(resp.GetStatusCode()) };
- }
-
- return data.size();
+ return chunkSize;
}
- catch (std::exception& exception)
+ catch (std::exception & exception)
{
- Log({ OBF_SEC("Caught a std::exception when running OnSend(): ") + exception.what(), LogMessage::Severity::Warning });
+ Log({ OBF_SEC("Caught a std::exception when running OnSend(): ") + exception.what(), LogMessage::Severity::Error });
return 0u;
}
-
- return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
std::vector FSecure::C3::Interfaces::Channels::Outlook365RestTask::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));
+ RateLimitDelay(m_MinUpdateDelay, m_MaxUpdateDelay);
- std::vector packets;
+ auto packets = std::vector{};
try
{
- // Construct request to get tasks.
- // Filtered by subjects that start with m_InboundDirectionName, order by oldest first, and fetch 1000 tasks.
- // Example: https://outlook.office.com/api/v2.0/me/tasks?top=1000&filter=startswith(Subject,'C2S')&orderby=CreatedDateTime
- std::string URLwithInboundDirection = OBF("https://outlook.office.com/api/v2.0/me/tasks?top=");
- URLwithInboundDirection += OBF("1000"); // number of tasks to fetch
- 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)
+ auto fileList = ListData(OBF("?top=1000&filter=startswith(Subject,'") + m_InboundDirectionName + OBF("')&orderby=CreatedDateTime"));
- HttpClient webClient(Convert(URLwithInboundDirection), m_ProxyConfig);
+ for (auto& element : fileList.at(OBF("value")))
+ packets.emplace_back(cppcodec::base64_rfc4648::decode(element.at(OBF("Body")).at(OBF("Content")).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 (resp.GetStatusCode() != StatusCode::OK)
- {
- if (resp.GetStatusCode() == StatusCode::TooManyRequests)
- {
- s_TimePoint = std::chrono::steady_clock::now() + FSecure::Utils::GenerateRandomValue(10s, 20s);
- throw std::runtime_error{ OBF("Too many requests") };
- }
-
- if (resp.GetStatusCode() == StatusCode::Unauthorized)
- {
- RefreshAccessToken();
- throw std::runtime_error{ OBF("HTTP 401 - Token being refreshed") };
-
- }
-
- 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(resp.GetStatusCode()) };
- }
-
- auto responseData = resp.GetData();
- // Gracefully handle situation where there's an empty JSON value (e.g., a failed request)
- if (responseData.size() == 0)
- return {};
-
- // Convert response (as string_t to utf8) and parse.
- json taskDataAsJSON;
- try
- {
- taskDataAsJSON = json::parse(responseData);
- }
- catch (json::parse_error&)
- {
- Log({ OBF("Failed to parse the list of received tasks."), LogMessage::Severity::Error });
- return {};
- }
-
- for (auto& element : taskDataAsJSON.at(OBF("value")))
- {
- // Obtain subject and task ID.
- std::string subject = element.at(OBF("Subject")).get();
- std::string id = element.at(OBF("Id")).get();
-
- // Verify that the full subject and ID were obtained. If not, ignore.
- if (subject.empty() || id.empty())
- continue;
-
- // Check the direction component is at the start of subject.
- if (subject.find(m_InboundDirectionName))
- continue;
-
- try
- {
- // Send the (decoded) message's body.
- ByteVector packet = cppcodec::base64_rfc4648::decode(element.at(OBF("Body")).at(OBF("Content")).get());
- packets.emplace_back(packet);
- SCOPE_GUARD(RemoveTask(id); );
- }
- 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 task #") + id + OBF(" : ") + exception.what(), LogMessage::Severity::Error });
- }
- }
+ for (auto& element : fileList.at(OBF("value")))
+ RemoveFile(element.at(OBF("Id")));
}
catch (std::exception& exception)
{
@@ -195,170 +73,23 @@ std::vector FSecure::C3::Interfaces::Channels::Outlook365Re
return packets;
}
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-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.
- HttpClient webClient(OBF(L"https://outlook.office.com/api/v2.0/me/tasks?$top=1000"), m_ProxyConfig);
-
- 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)
- {
- 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(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")))
- RemoveTask(element.at(OBF("id")).get());
- }
- catch (std::exception& exception)
- {
- Log({ OBF_SEC("Caught a std::exception when running RemoveAllTasks(): ") + exception.what(), LogMessage::Severity::Warning });
- }
-
- return {};
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-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("')");
- HttpClient webClient(Convert(URLwithID), m_ProxyConfig);
-
- 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;
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-void FSecure::C3::Interfaces::Channels::Outlook365RestTask::RefreshAccessToken()
-{
- try
- {
- //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=https://outlook.office365.com/.default");
- requestBody += OBF("&username=");
- requestBody += m_Username;
- requestBody += OBF("&password=");
- requestBody += m_Password;
- requestBody += OBF("&client_id=");
- requestBody += m_ClientKey;
-
- request.SetData(ContentType::ApplicationXWwwFormUrlencoded, { requestBody.begin(), requestBody.end() });
- FSecure::Utils::SecureMemzero(requestBody.data(), requestBody.size());
- auto resp = webClient.Request(request);
-
- 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()) };
-
- m_Token = data["access_token"].get();
- }
- catch (std::exception& exception)
- {
- throw std::runtime_error{ OBF_STR("Cannot refresh token: ") + exception.what() };
- }
-}
-
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
FSecure::ByteVector FSecure::C3::Interfaces::Channels::Outlook365RestTask::OnRunCommand(ByteView command)
{
- auto commandCopy = command; //each read moves ByteView. CommandCoppy is needed for default.
+ auto commandCopy = command; // Each read moves ByteView. CommandCoppy is needed for default.
switch (command.Read())
{
case 0:
- return RemoveAllTasks(command);
+ try
+ {
+ RemoveAllFiles();
+ }
+ catch (std::exception const& e)
+ {
+ Log({ OBF_SEC("Caught a std::exception when running RemoveAllFiles(): ") + e.what(), LogMessage::Severity::Error });
+ }
+ return {};
default:
return AbstractChannel::OnRunCommand(commandCopy);
}
}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-FSecure::ByteView FSecure::C3::Interfaces::Channels::Outlook365RestTask::GetCapability()
-{
- return R"_(
-{
- "create":
- {
- "arguments":
- [
- [
- {
- "type": "string",
- "name": "Input ID",
- "min": 4,
- "randomize": true,
- "description": "Used to distinguish packets for the channel"
- },
- {
- "type": "string",
- "name": "Output ID",
- "min": 4,
- "randomize": true,
- "description": "Used to distinguish packets from the channel"
- }
- ],
- {
- "type": "string",
- "name": "Username",
- "min": 0,
- "description": "User with Office 365 subscription."
- },
- {
- "type": "string",
- "name": "Password",
- "min": 0,
- "description": "User password."
- },
- {
- "type": "string",
- "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."
- }
- ]
- },
- "commands":
- [
- {
- "name": "Remove all tasks",
- "id": 0,
- "description": "Clearing old tasks from server may increase bandwidth",
- "arguments": []
- }
- ]
-}
-)_";
-}
-
diff --git a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h
index a8912c3..31713c4 100644
--- a/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h
+++ b/Src/Common/FSecure/C3/Interfaces/Channels/Outlook365RestTask.h
@@ -1,17 +1,15 @@
#pragma once
-#include "Common/FSecure/WinHttp/WebProxy.h"
-#include "Common/FSecure/WinHttp/Constants.h" //< For CppRestSdk.
+#include "Office365.h"
namespace FSecure::C3::Interfaces::Channels
{
/// Implementation of the Outlook365 REST tasks Device.
- class Outlook365RestTask : public Channel
+ class Outlook365RestTask : public Channel, public Office365
{
public:
- /// Public constructor.
- /// @param arguments factory arguments.
- Outlook365RestTask(ByteView arguments);
+ /// Use Office365 constructor.
+ using Office365::Office365;
/// OnSend callback implementation.
/// @param blob data to send to Channel.
@@ -27,40 +25,15 @@ namespace FSecure::C3::Interfaces::Channels
/// @return command result.
ByteVector OnRunCommand(ByteView command) override;
- /// Get channel capability.
- /// @returns ByteView view of channel capability.
- static ByteView GetCapability();
-
/// 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 = 1000ms, s_MaxUpdateDelay = 1000ms;
- protected:
-
- /// Removes all tasks from server.
- /// @param ByteView unused.
- /// @returns ByteVector empty vector.
- ByteVector RemoveAllTasks(ByteView);
-
- /// Remove one task from server.
- /// @param id of task.
- void RemoveTask(std::string const& id);
-
- /// Requests a new access token using the refresh token
- /// @throws std::exception if token cannot be refreshed.
- void RefreshAccessToken();
-
- /// In/Out names on the server.
- std::string m_InboundDirectionName, m_OutboundDirectionName;
-
- std::string m_Username, m_Password, m_ClientKey, m_Token;
-
- /// Stores HTTP configuration (proxy, OAuth, etc).
- WinHttp::WebProxy m_ProxyConfig;
-
- /// 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;
+ /// Endpoints used by Office365 methods.
+ static Crypto::String ItemEndpont;
+ static Crypto::String ListEndpoint;
+ static Crypto::String TokenEndpoit;
+ static Crypto::String Scope;
};
}
diff --git a/Src/Common/FSecure/CppTools/StringConversions.h b/Src/Common/FSecure/CppTools/StringConversions.h
index 0bec5ef..047b1ac 100644
--- a/Src/Common/FSecure/CppTools/StringConversions.h
+++ b/Src/Common/FSecure/CppTools/StringConversions.h
@@ -34,7 +34,7 @@ namespace FSecure::StringConversions
{
/// Detect character type stored by container.
template
- using CharT = std::remove_reference_t()[0])>;
+ using CharT = std::remove_const_t()[0])>>;
/// Detect view type corresponding to container.
template