diff --git a/client-lite/test/string_ops_tests.cpp b/client-lite/test/string_ops_tests.cpp index 45add727..662cd856 100644 --- a/client-lite/test/string_ops_tests.cpp +++ b/client-lite/test/string_ops_tests.cpp @@ -1,5 +1,6 @@ #include "test_common.h" +#include "do_test_helpers.h" #include "string_ops.h" #include "test_helpers.h" @@ -37,7 +38,7 @@ static void StringPartitionTester(const std::string& left, const std::string& ri size_t numExpectedParts = 0; if (expectedStr1) { ++numExpectedParts; } if (expectedStr2) { ++numExpectedParts; } - auto fnLogStr = test_scope_exit([&]() + auto fnLogStr = dotest::util::scope_exit([&]() { std::cout << "Test string: \"" << testString << "\"\n"; if (parts.size() > 0) std::cout << "Partition1: \"" << parts[0] << "\"\n"; diff --git a/client-lite/test/test_helpers.h b/client-lite/test/test_helpers.h index 1fd64c75..4428da12 100644 --- a/client-lite/test/test_helpers.h +++ b/client-lite/test/test_helpers.h @@ -10,66 +10,3 @@ class TestHelpers // Disallow creating an instance of this object TestHelpers() {} }; - -template -class test_lambda_call -{ -public: - test_lambda_call(const test_lambda_call&) = delete; - test_lambda_call& operator=(const test_lambda_call&) = delete; - test_lambda_call& operator=(test_lambda_call&& other) = delete; - - explicit test_lambda_call(TLambda&& lambda) noexcept : _lambda(std::move(lambda)) - { - static_assert(std::is_same::value, "scope_exit lambdas must not have a return value"); - static_assert(!std::is_lvalue_reference::value && !std::is_rvalue_reference::value, - "scope_exit should only be directly used with a lambda"); - } - - test_lambda_call(test_lambda_call&& other) noexcept : - _lambda(std::move(other._lambda)), - _fCall(other._fCall) - { - other._fCall = false; - } - - ~test_lambda_call() noexcept - { - reset(); - } - - // Ensures the scope_exit lambda will not be called - void release() noexcept - { - _fCall = false; - } - - // Executes the scope_exit lambda immediately if not yet run; ensures it will not run again - void reset() noexcept - { - if (_fCall) - { - _fCall = false; - _lambda(); - } - } - - // Returns true if the scope_exit lambda is still going to be executed - explicit operator bool() const noexcept - { - return _fCall; - } - -protected: - TLambda _lambda; - bool _fCall { true }; -}; - -// Returns an object that executes the given lambda when destroyed. -// Capture the object with 'auto'; use reset() to execute the lambda early or release() to avoid -// execution. Exceptions thrown in the lambda will fail-fast; use scope_exit_log to avoid. -template -inline auto test_scope_exit(TLambda&& lambda) noexcept -{ - return test_lambda_call(std::forward(lambda)); -} diff --git a/common/lib-dotestutil/do_test_helpers.h b/common/lib-dotestutil/do_test_helpers.h index 247a5fce..645f574f 100644 --- a/common/lib-dotestutil/do_test_helpers.h +++ b/common/lib-dotestutil/do_test_helpers.h @@ -65,5 +65,68 @@ class DOTestException : public std::exception const char* what() const noexcept override { return _msg.c_str(); } }; +template +class lambda_call +{ +public: + lambda_call(const lambda_call&) = delete; + lambda_call& operator=(const lambda_call&) = delete; + lambda_call& operator=(lambda_call&& other) = delete; + + explicit lambda_call(TLambda&& lambda) noexcept : + _lambda(std::move(lambda)) + { + static_assert(std::is_same::value, "scope_exit lambdas must not have a return value"); + static_assert(!std::is_lvalue_reference::value && !std::is_rvalue_reference::value, + "scope_exit should only be directly used with a lambda"); + } + + lambda_call(lambda_call&& other) noexcept : + _lambda(std::move(other._lambda)), + _fCall(other._fCall) + { + other._fCall = false; + } + + ~lambda_call() noexcept + { + reset(); + } + + // Ensures the scope_exit lambda will not be called + void release() noexcept + { + _fCall = false; + } + + // Executes the scope_exit lambda immediately if not yet run; ensures it will not run again + void reset() noexcept + { + if (_fCall) + { + _fCall = false; + _lambda(); + } + } + + // Returns true if the scope_exit lambda is still going to be executed + explicit operator bool() const noexcept + { + return _fCall; + } + +protected: + TLambda _lambda; + bool _fCall { true }; +}; + +// Returns an object that executes the given lambda when destroyed. +// Capture the object with 'auto'; use reset() to execute the lambda early or release() to avoid execution. +template +inline auto scope_exit(TLambda&& lambda) noexcept +{ + return lambda_call(std::forward(lambda)); +} + } // namespace util } // namespace dotest diff --git a/sdk-cpp/CMakeLists.txt b/sdk-cpp/CMakeLists.txt index b53833a0..f977ee39 100644 --- a/sdk-cpp/CMakeLists.txt +++ b/sdk-cpp/CMakeLists.txt @@ -26,16 +26,6 @@ fixup_compile_options_for_arm() # Include external libraries here find_package(Boost COMPONENTS filesystem system REQUIRED) -# Cpprest Issues: -# 1. v2.10.10 min version (see required PR link below). cpprestsdk does not seem to support specifying this through cmake. -# https://github.com/microsoft/cpprestsdk/pull/1019/files -# -# 2. Installing libcpprest-dev via apt installs cpprest's cmake config files to a non-default cmake search path before v2.10.9 -# See: https://github.com/microsoft/cpprestsdk/issues/686 -# This issue has been patched but has not made its way to the stable branch for Ubuntu -# Since we are statically linking to v2.10.16 we no longer need to worry about the above as cpprest is patched to provide the proper package configuration metadata -find_package(cpprestsdk CONFIG REQUIRED) - if (DO_BUILD_TESTS) add_subdirectory(tests) endif() @@ -63,7 +53,6 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") "src/internal/rest" "src/internal/util" "src/internal" - ${docs_common_includes} ${include_directories_for_arm} ) target_compile_definitions(${DO_SDK_LIB_NAME} PRIVATE @@ -76,7 +65,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") DO_PLUGIN_APT_BIN_NAME="${DO_PLUGIN_APT_BIN_NAME}" ) target_link_libraries(${DO_SDK_LIB_NAME} - PRIVATE doversion cpprestsdk::cpprest + PRIVATE doversion PUBLIC ${Boost_LIBRARIES} ) diff --git a/sdk-cpp/src/do_download.cpp b/sdk-cpp/src/do_download.cpp index 26f65551..b07b7d72 100644 --- a/sdk-cpp/src/do_download.cpp +++ b/sdk-cpp/src/do_download.cpp @@ -107,7 +107,7 @@ void download::download_url_to_path(const std::string& uri, const std::string& d oneShotDownload.start(); download_status status; - bool timedOut; + bool timedOut = false; do { if (isCancelled) diff --git a/sdk-cpp/src/internal/rest/download_rest.cpp b/sdk-cpp/src/internal/rest/download_rest.cpp index a65e8a32..97e6c6d1 100644 --- a/sdk-cpp/src/internal/rest/download_rest.cpp +++ b/sdk-cpp/src/internal/rest/download_rest.cpp @@ -1,35 +1,34 @@ - #include "download_rest.h" +#include #include -#include - #include "do_exceptions_internal.h" #include "do_exceptions.h" #include "do_http_client.h" +#include "do_url_encode.h" namespace msdo = microsoft::deliveryoptimization; -namespace cpprest_util_conv = utility::conversions; using namespace std::chrono_literals; // NOLINT(build/namespaces) const int g_maxNumRetryAttempts = 3; +const char* const g_downloadUriPart = "download"; + namespace microsoft::deliveryoptimization::details { + CDownloadRest::CDownloadRest(const std::string& uri, const std::string& downloadFilePath) { - web::uri_builder builder(g_downloadUriPart); - builder.append_path(U("create")); - - builder.append_query(U("Uri"), cpprest_util_conv::to_string_t(uri)); - builder.append_query(U("DownloadFilePath"), cpprest_util_conv::to_string_t(downloadFilePath)); + std::stringstream url; + url << g_downloadUriPart << "/create" << "?Uri=" << Url::EncodeDataString(uri) << "&DownloadFilePath=" + << Url::EncodeDataString(downloadFilePath); for (int retryAttempts = 0; retryAttempts < g_maxNumRetryAttempts; retryAttempts++) { try { - const auto respBody = CHttpClient::GetInstance().SendRequest(web::http::methods::POST, builder.to_string()); + const auto respBody = CHttpClient::GetInstance().SendRequest(HttpRequest::POST, url.str()); _id = respBody.get("Id"); return; } @@ -78,24 +77,23 @@ void CDownloadRest::Abort() msdo::download_status CDownloadRest::GetStatus() { - web::uri_builder builder(g_downloadUriPart); - builder.append_path(U("getstatus")); - builder.append_query(U("Id"), cpprest_util_conv::to_string_t(_id)); + std::stringstream url; + url << g_downloadUriPart << "/getstatus" << "?Id=" << _id; - const auto respBody = CHttpClient::GetInstance().SendRequest(web::http::methods::GET, builder.to_string()); + const auto respBody = CHttpClient::GetInstance().SendRequest(HttpRequest::GET, url.str()); uint64_t bytesTotal = respBody.get("BytesTotal"); uint64_t bytesTransferred = respBody.get("BytesTransferred"); int32_t errorCode = respBody.get("ErrorCode"); int32_t extendedErrorCode = respBody.get("ExtendedErrorCode"); - static const std::map stateMap = - {{ U("Created"), download_state::created }, - { U("Transferring"), download_state::transferring }, - { U("Transferred"), download_state::transferred }, - { U("Finalized"), download_state::finalized }, - { U("Aborted"), download_state::aborted }, - { U("Paused"), download_state::paused }}; + static const std::map stateMap = + {{ "Created", download_state::created }, + { "Transferring", download_state::transferring }, + { "Transferred", download_state::transferred }, + { "Finalized", download_state::finalized }, + { "Aborted", download_state::aborted }, + { "Paused", download_state::paused }}; download_state status = download_state::created; auto it = stateMap.find(respBody.get("Status")); @@ -114,11 +112,9 @@ msdo::download_status CDownloadRest::GetStatus() void CDownloadRest::_DownloadOperationCall(const std::string& type) { - web::uri_builder builder(g_downloadUriPart); - builder.append_path(cpprest_util_conv::to_string_t(type)); - builder.append_query(U("Id"), cpprest_util_conv::to_string_t(_id)); - - (void)CHttpClient::GetInstance().SendRequest(web::http::methods::POST, builder.to_string()); + std::stringstream url; + url << g_downloadUriPart << '/' << type << "?Id=" << _id; + (void)CHttpClient::GetInstance().SendRequest(HttpRequest::POST, url.str()); } } // namespace microsoft::deliveryoptimization::details diff --git a/sdk-cpp/src/internal/util/do_http_client.cpp b/sdk-cpp/src/internal/util/do_http_client.cpp index db1ddfa4..0fce5aea 100644 --- a/sdk-cpp/src/internal/util/do_http_client.cpp +++ b/sdk-cpp/src/internal/util/do_http_client.cpp @@ -1,20 +1,61 @@ - #include "do_http_client.h" -#include -#include -#include -#include +#include +#include +#include +#include -#include "do_exceptions_internal.h" #include "do_exceptions.h" +#include "do_exceptions_internal.h" +#include "do_http_message.h" #include "do_port_finder.h" -const utility::string_t g_downloadUriPart(U("/download")); +namespace net = boost::asio; // from +using tcp = net::ip::tcp; // from namespace microsoft::deliveryoptimization::details { +class CHttpClientImpl +{ +public: + ~CHttpClientImpl() + { + if (_socket.is_open()) + { + // Gracefully close the socket + boost::system::error_code ec; + _socket.shutdown(tcp::socket::shutdown_both, ec); + } + } + + boost::system::error_code Connect(ushort port) + { + tcp::resolver resolver{_ioc}; + const auto endpoints = resolver.resolve({ "127.0.0.1", std::to_string(port) }); + boost::system::error_code ec; + boost::asio::connect(_socket, endpoints, ec); + return ec; + } + + std::pair GetResponse(HttpRequest::Method method, const std::string& url) + { + HttpRequest request{method, url}; + request.Serialize(_socket); + + HttpResponse response; + response.Deserialize(_socket); + + return {response.StatusCode(), response.ExtractJsonBody()}; + } + +private: + net::io_service _ioc; + net::ip::tcp::socket _socket{_ioc}; +}; + +CHttpClient::~CHttpClient() = default; + CHttpClient& CHttpClient::GetInstance() { static CHttpClient myInstance; @@ -23,32 +64,30 @@ CHttpClient& CHttpClient::GetInstance() void CHttpClient::_InitializeDOConnection(bool launchClientFirst) { - std::unique_lock lock(_mutex); - const auto port = CPortFinder::GetDOPort(launchClientFirst); - const auto url = "http://127.0.0.1:" + port + "/"; - _httpClient = std::make_unique(url); -} - -void g_ThrowIfHttpError(const web::http::http_response& resp) -{ - if (resp.status_code() != 200) + const auto port = std::strtoul(CPortFinder::GetDOPort(launchClientFirst).data(), nullptr, 10); + auto spImpl = std::make_unique(); + auto ec = spImpl->Connect(gsl::narrow(port)); + if (ec) { - web::json::object respBody = resp.extract_json().get().as_object(); - - int32_t ErrorCode = respBody[U("ErrorCode")].as_integer(); - - ThrowException(ErrorCode); + // TODO(shishirb) Log the actual error when logging is available + ThrowException(microsoft::deliveryoptimization::errc::no_service); } + + std::unique_lock lock(_mutex); + _httpClientImpl = std::move(spImpl); } -boost::property_tree::ptree CHttpClient::SendRequest(const web::http::method& method, const utility::string_t& url, bool retry) +boost::property_tree::ptree CHttpClient::SendRequest(HttpRequest::Method method, const std::string& url, bool retry) { - web::http::http_response response; + auto responseStatusCode = 0u; + boost::property_tree::ptree responseBodyJson; try { - response = _httpClient->request(method, url).get(); + std::unique_lock lock(_mutex); + auto pClient = static_cast(_httpClientImpl.get()); + std::tie(responseStatusCode, responseBodyJson) = pClient->GetResponse(method, url); } - catch (const web::http::http_exception& e) + catch (const boost::system::system_error& e) { if (retry) { @@ -56,15 +95,16 @@ boost::property_tree::ptree CHttpClient::SendRequest(const web::http::method& me return SendRequest(method, url, false); } - ThrowException(e.error_code()); + ThrowException(e.code().value()); } - g_ThrowIfHttpError(response); + if (responseStatusCode != 200) + { + auto agentErrorCode = responseBodyJson.get_optional("ErrorCode"); + ThrowException(agentErrorCode ? *agentErrorCode : -1); + } - std::stringstream ss(response.extract_utf8string().get()); - boost::property_tree::ptree returnValue; - boost::property_tree::read_json(ss, returnValue); - return returnValue; + return responseBodyJson; } CHttpClient::CHttpClient() diff --git a/sdk-cpp/src/internal/util/do_http_client.h b/sdk-cpp/src/internal/util/do_http_client.h index db82ed68..ac7d2384 100644 --- a/sdk-cpp/src/internal/util/do_http_client.h +++ b/sdk-cpp/src/internal/util/do_http_client.h @@ -1,34 +1,27 @@ #pragma once #include - #include -#include -#include - +#include "do_http_message.h" #include "do_noncopyable.h" -namespace web::http::client -{ -class http_client; -} - -extern const utility::string_t g_downloadUriPart; - namespace microsoft::deliveryoptimization::details { +class CHttpClientImpl; + class CHttpClient : CDONoncopyable { public: + ~CHttpClient(); static CHttpClient& GetInstance(); - - boost::property_tree::ptree SendRequest(const web::http::method& method, const utility::string_t& url, bool retry = true); + boost::property_tree::ptree SendRequest(HttpRequest::Method method, const std::string& url, bool retry = true); private: CHttpClient(); void _InitializeDOConnection(bool launchClientFirst = false); mutable std::mutex _mutex; - std::unique_ptr _httpClient; + std::unique_ptr _httpClientImpl; }; + } // namespace microsoft::deliveryoptimization::details diff --git a/sdk-cpp/src/internal/util/do_http_message.cpp b/sdk-cpp/src/internal/util/do_http_message.cpp new file mode 100644 index 00000000..9e596348 --- /dev/null +++ b/sdk-cpp/src/internal/util/do_http_message.cpp @@ -0,0 +1,60 @@ +#include "do_http_message.h" + +#include +#include +#include +#include "do_http_parser.h" + +namespace net = boost::asio; // from + +namespace microsoft::deliveryoptimization::details +{ + +HttpRequest::HttpRequest(Method method, const std::string& url) : + _method(method), + _url(url.data()) +{ +} + +void HttpRequest::Serialize(boost::asio::ip::tcp::socket& socket) const +{ + std::stringstream request; + const char* pVerb = (_method == Method::GET) ? "GET" : "POST"; + request << pVerb << ' ' << _url << ' ' << "HTTP/1.1\r\n"; + request << "Host: 127.0.0.1\r\n"; + request << "User-Agent: DO-SDK-CPP\r\n"; + request << "\r\n"; + + const auto req = request.str(); + // std::cout << "Sending request:\n" << req << std::endl; // uncomment for debugging + net::write(socket, net::buffer(req.data(), req.size())); +} + +void HttpResponse::Deserialize(boost::asio::ip::tcp::socket& socket) +{ + HttpResponseParser parser{_statusCode, _contentLength, _body}; + std::vector readBuf(1024); + do + { + auto bytesRead = socket.read_some(net::buffer(readBuf.data(), readBuf.size())); + parser.OnData(readBuf.data(), bytesRead); + } while (!parser.Done()); +} + +boost::property_tree::ptree HttpResponse::ExtractJsonBody() +{ + boost::property_tree::ptree responseBodyJson; + if (_body.rdbuf()->in_avail() > 0) + { + try + { + boost::property_tree::read_json(_body, responseBodyJson); + } + catch (...) + { + } + } + return responseBodyJson; +} + +} // microsoft::deliveryoptimization::details diff --git a/sdk-cpp/src/internal/util/do_http_message.h b/sdk-cpp/src/internal/util/do_http_message.h new file mode 100644 index 00000000..10ca4461 --- /dev/null +++ b/sdk-cpp/src/internal/util/do_http_message.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +namespace microsoft::deliveryoptimization::details +{ + +// Credit: Code takes a little inspiration from Boost.Beast. Too bad it is not available on Ubuntu 18.04. +class HttpRequest +{ +public: + enum Method + { + GET, + POST + }; + + HttpRequest(Method method, const std::string& url); + void Serialize(boost::asio::ip::tcp::socket& socket) const; + +private: + Method _method; + const char* _url; +}; + +class HttpResponse +{ +public: + void Deserialize(boost::asio::ip::tcp::socket& socket); + + unsigned int StatusCode() const { return _statusCode; } + boost::property_tree::ptree ExtractJsonBody(); + +private: + unsigned int _statusCode { 0 }; + size_t _contentLength { 0 }; + std::stringstream _body; +}; + +} // microsoft::deliveryoptimization::details diff --git a/sdk-cpp/src/internal/util/do_http_parser.cpp b/sdk-cpp/src/internal/util/do_http_parser.cpp new file mode 100644 index 00000000..35d55ae6 --- /dev/null +++ b/sdk-cpp/src/internal/util/do_http_parser.cpp @@ -0,0 +1,150 @@ +#include "do_http_parser.h" + +// #include +#include +#include +#include "do_exceptions.h" +#include "do_exceptions_internal.h" + +namespace microsoft::deliveryoptimization::details +{ + +HttpResponseParser::HttpResponseParser(unsigned int& statusCodeBuf, size_t& contentLenBuf, std::stringstream& bodyBuf) : + _statusCode(statusCodeBuf), + _contentLength(contentLenBuf), + _body(bodyBuf) +{ + // Agent response is always with a JSON body, small in size. + _responseBuf.reserve(2048); +} + +void HttpResponseParser::OnData(const char* pData, size_t cb) +{ + if ((_responseBuf.size() + cb) > _responseBuf.capacity()) + { + ThrowException(microsoft::deliveryoptimization::errc::unexpected); + } + _responseBuf.insert(_responseBuf.end(), pData, pData + cb); + + while (_ParseBuf()) + { + } +} + +// Returns true if more processing can be done, false if processing is done or cannot continue until more data is received. +bool HttpResponseParser::_ParseBuf() +{ + const auto oldState = _state; + switch (_state) + { + case ParserState::StatusLine: + { + auto itCR = _FindCRLF(_responseBuf.begin()); + if (itCR != _responseBuf.end()) + { + std::string statusLine{_responseBuf.begin(), itCR}; + // std::cout << "Status line: " << statusLine << std::endl; + + std::regex rxStatusLine{"[hHtTpP/1\\.]+ (\\d+) [a-zA-Z0-9 ]+"}; + std::cmatch matches; + if (!std::regex_match(statusLine.data(), matches, rxStatusLine)) + { + ThrowException(microsoft::deliveryoptimization::errc::unexpected); + } + + _statusCode = static_cast(std::strtoul(matches[1].str().data(), nullptr, 10)); + // std::cout << "Result: " << _statusCode << std::endl; + + _state = ParserState::Fields; + _itParseFrom = itCR + 2; + } + break; + } + + case ParserState::Fields: + { + while (_ParseNextField()) + { + } + break; + } + + case ParserState::Body: + { + if (_contentLength == 0) + { + _state = ParserState::Complete; + } + else + { + const auto availableBodySize = gsl::narrow(std::distance(_itParseFrom, _responseBuf.end())); + // Agent response is a JSON body, couple hundred bytes at max. Read everything at once. + if (availableBodySize == _contentLength) + { + _body.write(&(*_itParseFrom), _contentLength); + // std::cout << "Body: " << _body.str() << std::endl; + _state = ParserState::Complete; + _itParseFrom = _responseBuf.end(); + } + } + break; + } + + case ParserState::Complete: + break; + } + + return (oldState != _state); +} + +// Returns true if there are more fields to process, false otherwise +bool HttpResponseParser::_ParseNextField() +{ + // Find \r\n, look for Content-Length. + auto itCR = _FindCRLF(_itParseFrom); + if (itCR == _responseBuf.end()) + { + return false; // need more data + } + + if (_itParseFrom == itCR) + { + _state = ParserState::Body; // empty field == end of headers + _itParseFrom = itCR + 2; + return false; + } + + std::string field{_itParseFrom, itCR}; + // std::cout << "Field: " << field << std::endl; + if (field.find("Content-Length") != std::string::npos) + { + std::regex rxContentLength{".*:[ ]*(\\d+).*"}; + std::cmatch matches; + if (!std::regex_match(field.data(), matches, rxContentLength)) + { + ThrowException(microsoft::deliveryoptimization::errc::unexpected); + } + + _contentLength = static_cast(std::strtoul(matches[1].str().data(), nullptr, 10)); + // std::cout << "Body size: " << _contentLength << std::endl; + } + // else, field not interesting + _itParseFrom = itCR + 2; + return true; +} + +std::vector::iterator HttpResponseParser::_FindCRLF(std::vector::iterator itStart) +{ + auto itCR = std::find(itStart, _responseBuf.end(), '\r'); + if (itCR == _responseBuf.end() || (itCR + 1) == _responseBuf.end()) + { + return _responseBuf.end(); // need more data + } + if (*(itCR + 1) != '\n') + { + ThrowException(microsoft::deliveryoptimization::errc::unexpected); + } + return itCR; +} + +} // microsoft::deliveryoptimization::details diff --git a/sdk-cpp/src/internal/util/do_http_parser.h b/sdk-cpp/src/internal/util/do_http_parser.h new file mode 100644 index 00000000..85d26d19 --- /dev/null +++ b/sdk-cpp/src/internal/util/do_http_parser.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +namespace microsoft::deliveryoptimization::details +{ + +// Very limited parsing abilities, just enough to support the Agent's responses. +// Credit: Code takes a little inspiration from Boost.Beast. Too bad it is not available on Ubuntu 18.04. +class HttpResponseParser +{ +public: + HttpResponseParser(unsigned int& statusCodeBuf, size_t& contentLenBuf, std::stringstream& bodyBuf); + + void OnData(const char* pData, size_t cb); + + bool Done() const noexcept + { + return (_state == ParserState::Complete); + } + +private: + bool _ParseBuf(); + bool _ParseNextField(); + std::vector::iterator _FindCRLF(std::vector::iterator itStart); + + // Parsing info + enum class ParserState + { + StatusLine, + Fields, + Body, + Complete + }; + + ParserState _state { ParserState::StatusLine }; + std::vector _responseBuf; + std::vector::iterator _itParseFrom; + + unsigned int& _statusCode; + size_t& _contentLength; + std::stringstream& _body; +}; + +} // microsoft::deliveryoptimization::details diff --git a/sdk-cpp/src/internal/util/do_port_finder.cpp b/sdk-cpp/src/internal/util/do_port_finder.cpp index b2945f0d..fee9b725 100644 --- a/sdk-cpp/src/internal/util/do_port_finder.cpp +++ b/sdk-cpp/src/internal/util/do_port_finder.cpp @@ -17,7 +17,7 @@ const int32_t g_maxNumPortFileReadAttempts = 3; namespace microsoft::deliveryoptimization::details { -std::string g_DiscoverDOPort() +static std::string g_DiscoverDOPort() { const std::string runtimeDirectory = GetRuntimeDirectory(); if (!boost::filesystem::exists(runtimeDirectory)) diff --git a/sdk-cpp/src/internal/util/do_url_encode.h b/sdk-cpp/src/internal/util/do_url_encode.h new file mode 100644 index 00000000..e2f8d908 --- /dev/null +++ b/sdk-cpp/src/internal/util/do_url_encode.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace microsoft::deliveryoptimization::details +{ + +class Url +{ +public: + // Unreserved characters are those that are allowed in a URI but do not have a reserved purpose + static bool IsUnreserved(int c) + { + return isalnum(static_cast(c)) || c == '-' || c == '.' || c == '_' || c == '~'; + } + + static std::string EncodeDataString(const std::string& input) + { + // Credits: cpprestsdk + // https://github.com/microsoft/cpprestsdk/blob/master/Release/src/uri/uri.cpp + + const char* const hex = "0123456789ABCDEF"; + std::string encoded; + for (auto c : input) + { + // for utf8 encoded string, char ASCII can be greater than 127. + const int ch = static_cast(c); + if (!IsUnreserved(ch)) + { + encoded.push_back('%'); + encoded.push_back(hex[(ch >> 4) & 0xF]); + encoded.push_back(hex[ch & 0xF]); + } + else + { + encoded.push_back(static_cast(ch)); + } + } + return encoded; + } + +}; + +} // namespace microsoft::deliveryoptimization::details diff --git a/sdk-cpp/tests/download_tests.cpp b/sdk-cpp/tests/download_tests.cpp index 0cf12bae..51e38bd7 100644 --- a/sdk-cpp/tests/download_tests.cpp +++ b/sdk-cpp/tests/download_tests.cpp @@ -11,6 +11,7 @@ #include "do_download.h" #include "do_download_status.h" #include "do_exceptions.h" +#include "do_test_helpers.h" #include "test_data.h" #include "test_helpers.h" @@ -162,6 +163,11 @@ TEST_F(DownloadTests, SimpleDownloadTest_With404UrlAndMalformedPath) ASSERT_EQ(e.error_code(), DO_ERROR_FROM_XPLAT_SYSERR(ENOENT)); ASSERT_FALSE(boost::filesystem::exists(g_tmpFileName)); } + catch (const std::exception& se) + { + std::cout << "Unexpected exception: " << se.what() << std::endl; + ASSERT_TRUE(false); + } } TEST_F(DownloadTests, Download1PausedDownload2SameDestTest) @@ -432,7 +438,7 @@ TEST_F(DownloadTests, MultipleConcurrentDownloadTest) TEST_F(DownloadTests, MultipleConcurrentDownloadTest_WithCancels) { std::atomic_bool cancelToken { false }; - ASSERT_FALSE(boost::filesystem::exists(g_tmpFileName)); + std::thread downloadThread([&]() { try @@ -453,6 +459,7 @@ TEST_F(DownloadTests, MultipleConcurrentDownloadTest_WithCancels) } catch (const msdo::exception& e) { + ASSERT_EQ(e.error_code(), static_cast(std::errc::operation_canceled)); } }); std::thread downloadThread3([&]() @@ -467,9 +474,9 @@ TEST_F(DownloadTests, MultipleConcurrentDownloadTest_WithCancels) } }); + std::this_thread::sleep_for(2s); cancelToken = true; - std::this_thread::sleep_for(3s); downloadThread.join(); downloadThread2.join(); downloadThread3.join(); @@ -481,10 +488,11 @@ TEST_F(DownloadTests, MultipleConcurrentDownloadTest_WithCancels) TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunning) { -// Enable this after we can start the service either from sdk or tests. -// Right now all further tests will fail because docs daemon will be stopped. -#if 0 - TestHelpers::ShutdownProcess(g_docsProcName); + TestHelpers::StopService("deliveryoptimization-agent.service"); + auto startService = dotest::util::scope_exit([]() + { + TestHelpers::StartService("deliveryoptimization-agent.service"); + }); TestHelpers::DeleteRestPortFiles(); // can be removed if docs deletes file on shutdown ASSERT_FALSE(boost::filesystem::exists(g_tmpFileName)); @@ -498,9 +506,33 @@ TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunning) ASSERT_EQ(ex.error_code(), static_cast(msdo::errc::no_service)); } ASSERT_FALSE(boost::filesystem::exists(g_tmpFileName)); -#endif } +TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunningPortFilePresent) +{ + // TODO(shishirb) Service name should come from cmake + TestHelpers::StopService("deliveryoptimization-agent.service"); + auto startService = dotest::util::scope_exit([]() + { + TestHelpers::StartService("deliveryoptimization-agent.service"); + }); + TestHelpers::DeleteRestPortFiles(); + TestHelpers::CreateRestPortFiles(1); + + ASSERT_FALSE(boost::filesystem::exists(g_tmpFileName)); + try + { + msdo::download::download_url_to_path(g_smallFileUrl, g_tmpFileName); + ASSERT_TRUE(!"Expected operation to throw exception"); + } + catch (const msdo::exception& ex) + { + ASSERT_EQ(ex.error_code(), static_cast(msdo::errc::no_service)); + } + ASSERT_FALSE(boost::filesystem::exists(g_tmpFileName)); +} + + TEST_F(DownloadTests, MultipleRestPortFileExists_Download) { // Enable after we have the ability to start the daemon after creating rest port files. diff --git a/sdk-cpp/tests/port_discovery_tests.cpp b/sdk-cpp/tests/port_discovery_tests.cpp index 5306a69e..7cd93226 100644 --- a/sdk-cpp/tests/port_discovery_tests.cpp +++ b/sdk-cpp/tests/port_discovery_tests.cpp @@ -55,4 +55,4 @@ TEST_F(PortDiscoveryTests, DiscoverPortTest) { std::string url = msdod::CPortFinder::GetDOPort(false); ASSERT_EQ(url, samplePortNumber); -} \ No newline at end of file +} diff --git a/sdk-cpp/tests/test_helpers.cpp b/sdk-cpp/tests/test_helpers.cpp index 05f47f22..05d4a5f8 100644 --- a/sdk-cpp/tests/test_helpers.cpp +++ b/sdk-cpp/tests/test_helpers.cpp @@ -50,6 +50,16 @@ void TestHelpers::RestartService(const std::string& name) dtu::ExecuteSystemCommand(restartCmd.data()); } +void TestHelpers::StartService(const std::string& name) +{ + dtu::ExecuteSystemCommand(dtu::FormatString("systemctl start %s", name.c_str()).data()); +} + +void TestHelpers::StopService(const std::string& name) +{ + dtu::ExecuteSystemCommand(dtu::FormatString("systemctl stop %s", name.c_str()).data()); +} + int TestHelpers::_KillProcess(int pid, int signal) { try diff --git a/sdk-cpp/tests/test_helpers.h b/sdk-cpp/tests/test_helpers.h index d4e94646..5aa63de3 100644 --- a/sdk-cpp/tests/test_helpers.h +++ b/sdk-cpp/tests/test_helpers.h @@ -8,6 +8,8 @@ class TestHelpers static bool IsActiveProcess(std::string name); static int ShutdownProcess(std::string name); static void RestartService(const std::string& name); + static void StartService(const std::string& name); + static void StopService(const std::string& name); static void CreateRestPortFiles(int numFiles); static void DeleteRestPortFiles(); static void CleanTestDir();