diff --git a/.github/actions/build-native-binary/action.yaml b/.github/actions/build-native-binary/action.yaml index 903e17e..fcc069d 100644 --- a/.github/actions/build-native-binary/action.yaml +++ b/.github/actions/build-native-binary/action.yaml @@ -102,11 +102,13 @@ runs: - name: Generate SSL key and certificate run: | + sudo mkdir -p /usr/share/ca-certificates/extra sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/private/selfupdateagent.key \ - -out /etc/ssl/certs/selfupdateagent.crt \ - -config utest/sua-certificate.config - sudo tee -a /etc/ssl/certs/ca-certificates.crt < /etc/ssl/certs/selfupdateagent.crt > /dev/null + -out /usr/share/ca-certificates/extra/selfupdateagent.crt \ + -subj '/CN=localhost' -extensions EXT -config utest/sua-certificate.config + echo -e "\nextra/selfupdateagent.crt" | sudo tee -a /etc/ca-certificates.conf > /dev/null + sudo update-ca-certificates shell: bash - name: Install and configure apache2 @@ -145,13 +147,15 @@ runs: dockerRunArgs: | --volume "${PWD}:/sua" --volume "/data/selfupdates:/data/selfupdates" - --volume "/etc/ssl/certs:/etc/ssl/certs" + --volume "/usr/share/ca-certificates/extra:/usr/share/ca-certificates/extra" --net=host shell: /bin/sh install: | apt-get -y update - apt-get -y install mosquitto-clients + apt-get -y install mosquitto-clients ca-certificates run: | + echo "\nextra/selfupdateagent.crt" | tee -a /etc/ca-certificates.conf > /dev/null + update-ca-certificates cd /sua/dist_arm64/utest LD_LIBRARY_PATH=../lib ./TestSelfUpdateAgent > ../../unit_tests_report_arm64.txt diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7152212..3a6fc1c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -80,7 +80,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - arch: [ arm64, amd64 ] + arch: [ amd64, arm64 ] steps: - uses: actions/checkout@v3 with: diff --git a/.ort.yml b/.ort.yml index 4861c99..ae2a714 100644 --- a/.ort.yml +++ b/.ort.yml @@ -1,5 +1,8 @@ excludes: paths: + - pattern: "project.spdx.yml" + reason: "OTHER" + comment: "Configuration for Open Source Scan" - pattern: "3rdparty/openssl/tlsfuzzer/**" reason: "TEST_OF" comment: "Test suite for SSL & TLS, not included in production release." @@ -36,6 +39,9 @@ excludes: - pattern: "3rdparty/curl/docs/**" reason: "DOCUMENTATION_OF" comment: "Code examples, not included in production release." + - pattern: "3rdparty/curl/tests/**" + reason: "TEST_OF" + comment: "curl tests, not included in production release." - pattern: "3rdparty/glib/gio/tests/**" reason: "TEST_OF" comment: "Tests of glib, not included in production release." diff --git a/3rdparty/curl b/3rdparty/curl index a8e0288..db12037 160000 --- a/3rdparty/curl +++ b/3rdparty/curl @@ -1 +1 @@ -Subproject commit a8e02881ec9417706610443bcfee6e1104bb44c6 +Subproject commit db1203781cd703ab7975bb37b1673214946dd3c9 diff --git a/src/Download/Downloader.cpp b/src/Download/Downloader.cpp index 119088c..09b0fb5 100644 --- a/src/Download/Downloader.cpp +++ b/src/Download/Downloader.cpp @@ -24,6 +24,8 @@ #include #include +#include + #include #include #include @@ -82,18 +84,49 @@ namespace { return written; } - sua::TechCode download(const std::string & caPath, const std::string & caFile, const char* url) + class CurlGlobalGuard { + public: + CURLcode init() { + return curl_global_init(CURL_GLOBAL_ALL); + } + + ~CurlGlobalGuard() { + curl_global_cleanup(); + } + }; + + class CurlEasyHandleGuard { + public: + CURL * init() { + _handle = curl_easy_init(); + return _handle; + } + + ~CurlEasyHandleGuard() { + if(_handle) { + curl_easy_cleanup(_handle); + } + } + + private: + CURL * _handle = nullptr; + }; + + sua::DownloadResult download(const std::string & caPath, const std::string & caFile, const char* url) { - CURLcode gres = curl_global_init(CURL_GLOBAL_ALL); - if(gres != 0) { - sua::Logger::critical("curl_global_init failed with code = {}", gres); - return sua::TechCode::DownloadFailed; + CurlGlobalGuard global_guard; + auto init_status = global_guard.init(); + if(init_status != 0) { + sua::Logger::critical("curl_global_init failed with code = {}", init_status); + return std::make_tuple(sua::TechCode::DownloadFailed, "libcurl init failed"); } - CURL* easy_handle = curl_easy_init(); - if(!easy_handle) { + CurlEasyHandleGuard easy_guard; + auto h = easy_guard.init(); + + if(!h) { sua::Logger::critical("curl_easy_init failed"); - return sua::TechCode::DownloadFailed; + return std::make_tuple(sua::TechCode::DownloadFailed, "libcurl init failed"); } DIR* dir = opendir(FILE_DIR); @@ -103,7 +136,7 @@ namespace { const int dir_err = mkdir(FILE_DIR, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); if(dir_err) { sua::Logger::critical("Issue with creating a dir {}, error: {}", FILE_DIR, dir_err); - return sua::TechCode::DownloadFailed; + return std::make_tuple(sua::TechCode::DownloadFailed, "temporary file folder could not be created"); } } @@ -111,41 +144,42 @@ namespace { FILE* fp = fopen(FILE_PATH, "wb"); if(!fp) { sua::Logger::critical("Failed to open '{}' for writing", FILE_PATH); - return sua::TechCode::DownloadFailed; + return std::make_tuple(sua::TechCode::DownloadFailed, "temporary file could not be opened for writing"); } - curl_easy_setopt(easy_handle, CURLOPT_URL, url); - curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &response_code); + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &response_code); if(!caFile.empty()) { - curl_easy_setopt(easy_handle, CURLOPT_CAINFO, caFile.c_str()); + curl_easy_setopt(h, CURLOPT_CAINFO, caFile.c_str()); } else { - curl_easy_setopt(easy_handle, CURLOPT_CAPATH, caPath.c_str()); + curl_easy_setopt(h, CURLOPT_CAPATH, caPath.c_str()); } - curl_easy_setopt(easy_handle, CURLOPT_SSL_VERIFYPEER, 1); - curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(easy_handle, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(easy_handle, CURLOPT_PROGRESSDATA, &data); - curl_easy_setopt(easy_handle, CURLOPT_PROGRESSFUNCTION, progress_callback); - curl_easy_setopt(easy_handle, CURLOPT_NOPROGRESS, 0); - CURLcode res = curl_easy_perform(easy_handle); + curl_easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(h, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(h, CURLOPT_WRITEDATA, fp); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_PROGRESSDATA, &data); + curl_easy_setopt(h, CURLOPT_PROGRESSFUNCTION, progress_callback); + curl_easy_setopt(h, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(h, CURLOPT_PROTOCOLS_STR, "https"); + CURLcode res = curl_easy_perform(h); sua::Logger::debug("curl_easy_perform ended with code = '{}'", res); long http_code = 0; - curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_cleanup(easy_handle); - curl_global_cleanup(); + curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &http_code); fclose(fp); progressNotificationLimiter = 0; sua::Logger::debug("CURLINFO_RESPONSE_CODE = {}", http_code); if(http_code != 200) { - sua::Logger::error(curl_easy_strerror(res)); - return sua::TechCode::DownloadFailed; + auto e = curl_easy_strerror(res); + sua::Logger::error(e); + return std::make_tuple(sua::TechCode::DownloadFailed, e); } - return sua::TechCode::OK; + return std::make_tuple(sua::TechCode::OK, ""); } } // namespace @@ -162,7 +196,7 @@ namespace sua { strncpy(FILE_PATH, filepath.c_str(), FILENAME_MAX - 1); } - TechCode Downloader::start(const std::string & input) + DownloadResult Downloader::start(const std::string & input) { return download(_context.caDirectory, _context.caFilepath, input.c_str()); } diff --git a/src/Download/Downloader.h b/src/Download/Downloader.h index a140994..1664715 100644 --- a/src/Download/Downloader.h +++ b/src/Download/Downloader.h @@ -28,7 +28,7 @@ namespace sua { static const std::string EVENT_DOWNLOADING; - TechCode start(const std::string & input) override; + DownloadResult start(const std::string & input) override; private: class Context & _context; diff --git a/src/Download/IDownloader.h b/src/Download/IDownloader.h index 957cdbd..f98d316 100644 --- a/src/Download/IDownloader.h +++ b/src/Download/IDownloader.h @@ -20,14 +20,17 @@ #include "TechCodes.h" #include +#include namespace sua { + using DownloadResult = std::tuple; + class IDownloader { public: virtual ~IDownloader() = default; - virtual TechCode start(const std::string & input) = 0; + virtual DownloadResult start(const std::string & input) = 0; }; } // namespace sua diff --git a/src/FSM/FSM.cpp b/src/FSM/FSM.cpp index 4b79094..27309c7 100644 --- a/src/FSM/FSM.cpp +++ b/src/FSM/FSM.cpp @@ -41,6 +41,8 @@ namespace sua { std::string FSM::activeState() const { + assert(_currentState != nullptr); + return _currentState->name(); } diff --git a/src/FSM/States/Downloading.cpp b/src/FSM/States/Downloading.cpp index 69f3676..eb2d305 100644 --- a/src/FSM/States/Downloading.cpp +++ b/src/FSM/States/Downloading.cpp @@ -57,7 +57,7 @@ namespace sua { Logger::info("Downloading bundle: '{}'", ctx.desiredState.bundleDownloadUrl); const auto result = ctx.downloaderAgent->start(ctx.desiredState.bundleDownloadUrl); - if(result == TechCode::OK) { + if(std::get<0>(result) == TechCode::OK) { Logger::info("Download progress: 100%"); send(ctx, IMqttProcessor::TOPIC_FEEDBACK, MqttMessage::Downloaded); @@ -67,9 +67,9 @@ namespace sua { return FotaEvent::DownloadSucceeded; } else { Logger::error("Download failed."); - send(ctx, IMqttProcessor::TOPIC_FEEDBACK, MqttMessage::DownloadFailed); ctx.desiredState.actionStatus = "DOWNLOAD_FAILURE"; - ctx.desiredState.actionMessage = "Download failed."; + ctx.desiredState.actionMessage = "Download failed: " + std::get<1>(result); + send(ctx, IMqttProcessor::TOPIC_FEEDBACK, MqttMessage::DownloadFailed); return FotaEvent::DownloadFailed; } } else { diff --git a/src/Mqtt/MqttMessagingProtocolJSON.cpp b/src/Mqtt/MqttMessagingProtocolJSON.cpp index 8476802..24723c4 100644 --- a/src/Mqtt/MqttMessagingProtocolJSON.cpp +++ b/src/Mqtt/MqttMessagingProtocolJSON.cpp @@ -179,7 +179,7 @@ namespace sua { case MqttMessage::DownloadFailed: return writeFeedbackWithPayload(ctx.desiredState, "DOWNLOAD_FAILURE", "Download failed.", - "DOWNLOAD_FAILURE", "Download failed.", + ctx.desiredState.actionStatus, ctx.desiredState.actionMessage, message, ctx.desiredState.downloadProgressPercentage); case MqttMessage::VersionChecking: return writeFeedbackWithPayload(ctx.desiredState, diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 6c033fa..423d2a8 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable(TestSelfUpdateAgent TestDispatcher.cpp + TestDownloader.cpp TestFSM.cpp TestLogger.cpp TestMqttMessagingProtocolJSON.cpp diff --git a/utest/MockDownloader.h b/utest/MockDownloader.h index 1ffdde5..7539759 100644 --- a/utest/MockDownloader.h +++ b/utest/MockDownloader.h @@ -23,7 +23,7 @@ class MockDownloader : public sua::IDownloader { public: - MOCK_METHOD(sua::TechCode, start, (const std::string & input), (override)); + MOCK_METHOD(sua::DownloadResult, start, (const std::string & input), (override)); }; #endif diff --git a/utest/TestDownloader.cpp b/utest/TestDownloader.cpp new file mode 100644 index 0000000..789ff8b --- /dev/null +++ b/utest/TestDownloader.cpp @@ -0,0 +1,35 @@ +#include "gtest/gtest.h" + +#include "Context.h" +#include "Download/Downloader.h" + +#include + +namespace { + + class TestDownloaderP + : public ::testing::TestWithParam + { }; + + TEST_P(TestDownloaderP, downloadViaUnsupportedProtocolFails) + { + sua::Context ctx; + sua::Downloader d(ctx); + + auto url = fmt::format("{}://127.0.0.1/bundle", GetParam()); + auto res = d.start(url); + + EXPECT_EQ(std::get<0>(res), sua::TechCode::DownloadFailed); + } + + INSTANTIATE_TEST_CASE_P( + TestDownloaderViaUnsupportedProtocols, + TestDownloaderP, + ::testing::Values( + "ftp", "http", "sftp", "smb", "smbs", "ldap", "ldaps" + ) + ); + +} + + diff --git a/utest/TestMqttMessagingProtocolJSON.cpp b/utest/TestMqttMessagingProtocolJSON.cpp index 1e43ff5..03885fd 100644 --- a/utest/TestMqttMessagingProtocolJSON.cpp +++ b/utest/TestMqttMessagingProtocolJSON.cpp @@ -596,6 +596,9 @@ namespace { { d.downloadProgressPercentage = 66; + ctx.desiredState.actionStatus = "DOWNLOAD_FAILURE"; + ctx.desiredState.actionMessage = "Download failed: test"; + const std::string result = ProtocolJSON().createMessage(ctx, sua::MqttMessage::DownloadFailed); // clang-format off @@ -614,7 +617,7 @@ namespace { }, "status": "DOWNLOAD_FAILURE", "progress": 66, - "message": "Download failed." + "message": "Download failed: test" } ] } diff --git a/utest/TestSelfUpdateScenarios.cpp b/utest/TestSelfUpdateScenarios.cpp index 7694a8f..b7c8184 100644 --- a/utest/TestSelfUpdateScenarios.cpp +++ b/utest/TestSelfUpdateScenarios.cpp @@ -102,7 +102,7 @@ namespace { std::this_thread::sleep_for(1s); } - void triggerIdentify(const std::string & bundle, const std::string & version) { + void triggerIdentify(const std::string & /*bundle*/, const std::string & version) { // clang-format off const std::string json = sua::jsonTemplate(R"( { @@ -119,7 +119,7 @@ namespace { "config": [ { "key": "image", - "value": "https://127.0.0.1/bundle" + "value": "{}://localhost/bundle" } ] } @@ -131,7 +131,7 @@ namespace { )"); // clang-format on - execute(sua::IMqttProcessor::TOPIC_IDENTIFY, fmt::format(json, version, bundle)); + execute(sua::IMqttProcessor::TOPIC_IDENTIFY, fmt::format(json, version, downloadProtocol)); } void trigger(const std::string & action) { @@ -193,6 +193,10 @@ namespace { ctx().bundleChecker = std::make_shared(); } + void TearDown() override { + mqttProcessor->stop(); + } + sua::SelfUpdateAgent sua; std::shared_ptr fsm; @@ -202,6 +206,8 @@ namespace { std::string testBrokerHost = "localhost"; int testBrokerPort = 1883; + std::string downloadProtocol = "https"; + std::string bundleUnderTest; std::vector visitedStates; @@ -285,7 +291,7 @@ namespace { sua.init(); start(); - EXPECT_CALL(*downloader, start(_)).WillOnce(Return(sua::TechCode::DownloadFailed)); + EXPECT_CALL(*downloader, start(_)).WillOnce(Return(std::make_tuple(sua::TechCode::DownloadFailed, ""))); triggerIdentify(BUNDLE_11, "1.1"); trigger(COMMAND_DOWNLOAD); @@ -409,4 +415,21 @@ namespace { EXPECT_EQ(sentMessages, expectedMessages); } + TEST_F(TestSelfUpdateScenarios, downloadViaHttpFails_endsInFailedState) + { + expectedStates = {"Uninitialized", "Connected", "Downloading", "Failed"}; + expectedMessages = {M::SystemVersion, M::Identifying, M::Identified, M::DownloadFailed}; + + downloadProtocol = "http"; + + sua.init(); + start(); + + triggerIdentify(BUNDLE_11, "1.2"); + trigger(COMMAND_DOWNLOAD); + + EXPECT_EQ(visitedStates, expectedStates); + EXPECT_EQ(sentMessages, expectedMessages); + } + } diff --git a/utest/sua-apache2.conf b/utest/sua-apache2.conf index cdb00db..24e4f96 100644 --- a/utest/sua-apache2.conf +++ b/utest/sua-apache2.conf @@ -1,7 +1,7 @@ - - ServerName 127.0.0.1 + + ServerName localhost DocumentRoot /var/www/html SSLEngine on - SSLCertificateFile /etc/ssl/certs/selfupdateagent.crt + SSLCertificateFile /usr/share/ca-certificates/extra/selfupdateagent.crt SSLCertificateKeyFile /etc/ssl/private/selfupdateagent.key diff --git a/utest/sua-certificate.config b/utest/sua-certificate.config index a1f698f..d85b50c 100644 --- a/utest/sua-certificate.config +++ b/utest/sua-certificate.config @@ -1,22 +1,10 @@ -[req] -default_bits = 2048 -distinguished_name = req_distinguished_name -req_extensions = req_ext -x509_extensions = v3_req -prompt = no - -[req_distinguished_name] -countryName = XX -stateOrProvinceName = NA -localityName = NA -organizationName = Self-signed certificate -commonName = 127.0.0.1 +[dn] +CN=localhost -[req_ext] -subjectAltName = @alt_names - -[v3_req] -subjectAltName = @alt_names +[req] +distinguished_name = dn -[alt_names] -IP.1 = 127.0.0.1 +[EXT] +subjectAltName=DNS:localhost +keyUsage=digitalSignature +extendedKeyUsage=serverAuth