#include "CommandLineParser.hpp" #include #include "ss.hpp" #include "config.hpp" #include #include #include #include #include "unistd.h" #include "UpdaterSecurity.hpp" #include "Updater.hpp" #include #include "SysExec.hpp" #include "TemporaryFile.hpp" #include "json_variant.hpp" #include "GithubResponseHeaders.hpp" using namespace std; using namespace pipedal; namespace fs = std::filesystem; std::string getVersionString() { std::string displayVersion = PROJECT_DISPLAY_VERSION; auto npos = displayVersion.find_last_of('-'); if (npos == std::string::npos) { throw std::runtime_error(SS("PROJECT_DISPLAY_VERSION does not contain a '-' character." << displayVersion)); } std::string releaseKind = displayVersion.substr(npos + 1); return SS(PROJECT_VER << "-" << releaseKind); } std::string psystem(const std::string &command) { std::string cmdRedirected = command + " 2>&1"; std::array buffer; std::string result; std::unique_ptr pipe(popen(cmdRedirected.c_str(), "r"), pclose); if (!pipe) { throw std::runtime_error("popen() failed!"); } while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { result += buffer.data(); } return result; } using namespace pipedal; void SignPackage() { auto versionString = getVersionString(); cout << "Processing release " << versionString << endl; // build/pipedal_1.2.49_arm64.deb fs::path packagePath = SS("build/pipedal_" << PROJECT_VER << "_arm64.deb"); packagePath = fs::absolute(packagePath); if (!fs::exists(packagePath)) { throw std::runtime_error(SS("File does not exist: " << packagePath)); } // sign the package. // gpg --armor --output "$filename".asc -b "$filename" cout << "--------------------------------------------------------------" << endl; cout << "Signing the package" << endl; cout << "--------------------------------------------------------------" << endl; std::string signCmd = SS("/usr/bin/gpg --pinentry-mode loopback --yes --default-key " << UPDATE_GPG_FINGERPRINT2 << " --armor --output " << packagePath << ".asc" << " --detach-sign " << packagePath.c_str()); int result = system(signCmd.c_str()); if (result != EXIT_SUCCESS) { throw std::runtime_error("Failed to sign package."); } cout << "--------------------------------------------------------------" << endl; cout << "Building a test keyring." << endl; cout << "--------------------------------------------------------------" << endl; fs::path keyringPath = fs::absolute("build/gpg"); fs::path keyPath = fs::absolute("config/updatekey2.asc"); fs::remove_all(keyringPath); fs::create_directories(keyringPath); std::ostringstream importCommand; importCommand << "/usr/bin/gpg --homedir " << keyringPath.c_str() << " --import " << keyPath.c_str(); int rc = system(importCommand.str().c_str()); if (rc != EXIT_SUCCESS) { throw std::runtime_error("Failed to create test keyring."); } cout << "--------------------------------------------------------------" << endl; cout << "Verifying signature" << endl; cout << "--------------------------------------------------------------" << endl; // verify the signature. // warnings are unavoidable as there's no way to create a root certificate without // an elaborate manual procedure. cout << "Warnings are expected, since there's no convenient way to install a " << endl; cout << "trusted certificate authority in the Updater keychain." << endl; std::string verifyCommand = SS("/usr/bin/gpg --verify --no-default-keyring " << "--homedir " << keyringPath.c_str() << " --armor " << packagePath << ".asc " << packagePath.c_str()); auto output = psystem(verifyCommand.c_str()); cout << output << endl; auto npos = output.find("Good signature from \"" UPDATE_GPG_ADDRESS2 "\""); if (npos == std::string::npos) { throw std::runtime_error("Signature validation failed."); } npos == output.find("using RSA key " UPDATE_GPG_FINGERPRINT2); if (npos == std::string::npos) { throw std::runtime_error("Signature validation failed."); } cout << "Signature check succeeded." << endl; cout << "--------------------------------------------------------------" << endl; cout << "Validating signing procedure" << endl; cout << "--------------------------------------------------------------" << endl; // Actually a test to make sure that signature checks fail on a modified binary. // Calling the test "Testing signature failure" is a more disconcerting than it needs to be. // Just one last sanity check before letting this procedure loose in the world. // create a bad test file. fs::path badPackage = "build/gpgtest/bad_package.deb"; fs::create_directories("build/gpgtest"); fs::remove(badPackage); fs::copy_file(packagePath, badPackage); { std::ofstream f; f.open(badPackage, ios_base::out | ios_base::ate); if (!f.is_open()) { throw std::runtime_error(SS("Can't open " << badPackage)); } f << "Bad stuff."; } // verify the signature. { std::string verifyCommand = SS("/usr/bin/gpg --verify --no-default-keyring " << "--homedir " << keyringPath.c_str() << " --armor " << packagePath << ".asc " << badPackage.c_str()); auto output = psystem(verifyCommand.c_str()); auto npos = output.find("BAD signature from "); if (npos == std::string::npos) { throw std::runtime_error("Signature validation succeeded when it should not have."); } } cout << "Test succeeded." << endl; } void GenerateUpdateJson() { cout << "--------------------------------------------------------------" << endl; cout << "Validating signing procedure" << endl; cout << "--------------------------------------------------------------" << endl; // Actually a test to make sure that signature checks fail on a modified binary. // Calling the test "Testing signature failure" is a more disconcerting than it needs to be. // Just one last sanity check in the std::filesystem::path workingPath = fs::absolute("build/updater"); Updater::ptr updater = Updater::Create(workingPath); // do NOT start it. We'll do the github request on the current thread. std::string packageName = SS("build/pipedal_" << PROJECT_VER << "_arm64.deb"); // pipedal_1.2.3_arm64.deb UpdateStatus updateStatus = updater->GetReleaseGeneratorStatus(); updateStatus.AddRelease(packageName); } class DownloadCountAsset { public: DownloadCountAsset(const json_variant &v) { auto o = v.as_object(); this->name = o->at("name").as_string(); this->browser_download_url = o->at("browser_download_url").as_string(); this->updated_at = o->at("updated_at").as_string(); this->downloads = (uint64_t)(o->at("download_count").as_number()); } std::string name; std::string browser_download_url; std::string updated_at; uint64_t downloads; }; class DownloadCountRelease { public: DownloadCountRelease(const json_variant &v) { auto o = v.as_object(); this->name = o->at("name").as_string(); this->draft = o->at("draft").as_bool(); this->prerelease = o->at("prerelease").as_bool(); auto assets = o->at("assets").as_array(); for (size_t i = 0; i < assets->size(); ++i) { auto &el = assets->at(i); this->assets.push_back(DownloadCountAsset(el)); } this->published_at = o->at("published_at").as_string(); } std::string name; bool draft = false, prerelease = false; std::vector assets; std::string published_at; }; static std::string timePointToISO8601(const std::chrono::system_clock::time_point &tp) { auto tt = std::chrono::system_clock::to_time_t(tp); std::tm tm = *std::gmtime(&tt); std::stringstream ss; ss << std::put_time(&tm,"%Y-%m-%dT%H:%M:%S") << "Z"; return ss.str(); } void GetDownloadCounts(bool gplotDownloads) { // error handling and temporary file use is different enough that it justifies // not including this code in production code. // const std::string responseOption = "-w \"%{response_code}\""; #ifdef WIN32 responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. #endif TemporaryFile headerFile{"/tmp"}; std::string args = SS("-s -L " << GITHUB_RELEASES_URL << " -D " << headerFile.str()); auto result = sysExecForOutput("curl", args); if (result.exitCode != EXIT_SUCCESS) { throw std::runtime_error("Github APIs not available."); } else { // hard throttling for github. GithubResponseHeaders githubHeaders{headerFile.Path()}; if (githubHeaders.code_ != 200) { if ( githubHeaders.ratelimit_limit_ != 0 && githubHeaders.ratelimit_limit_ == githubHeaders.ratelimit_used_) { std::time_t time = std::chrono::system_clock::to_time_t(githubHeaders.ratelimit_reset_); std::string strTime = std::ctime(&time); throw std::runtime_error(SS("Github API rate limit exceeded. Try again at " << strTime)); } } if (result.output.length() == 0) { throw std::runtime_error("No internet access."); } std::stringstream ss(result.output); json_reader reader(ss); json_variant vResult(reader); if (vResult.is_object()) { auto o = vResult.as_object(); std::string message = "Unknown error."; if (o->at("message").is_string()) { message = o->at("message").as_string(); } throw std::runtime_error(SS("Github Service error: " << message)); } else if (!vResult.is_array()) { throw std::runtime_error("Invalid file format error."); } else { json_variant::array_ptr vArray = vResult.as_array(); std::vector releases; for (size_t i = 0; i < vArray->size(); ++i) { auto &el = vArray->at(i); DownloadCountRelease release{el}; if (!release.draft) { releases.push_back(std::move(release)); } } if (gplotDownloads) { std::sort( releases.begin(), releases.end(), [](const DownloadCountRelease &left, const DownloadCountRelease &right) { return left.published_at < right.published_at; // date ascending }); for (size_t i = 0; i < releases.size()-1; ++i) { releases[i].published_at = releases[i+1].published_at; } if (releases.size() >= 1) { releases[releases.size()-1].published_at = timePointToISO8601(std::chrono::system_clock::now()); } uint64_t cumulativeCount = 0; for (const auto &release : releases) { for (const auto &asset : release.assets) { if (asset.name.ends_with(".deb")) { cumulativeCount += asset.downloads; } } cout << release.published_at << " " << cumulativeCount << endl; } } else { std::sort( releases.begin(), releases.end(), [](const DownloadCountRelease &left, const DownloadCountRelease &right) { return left.published_at > right.published_at; // latest date first. }); for (const auto &release : releases) { cout << release.name << endl; for (const auto &asset : release.assets) { cout << " " << asset.name << ": " << asset.downloads << endl; } } } } } } int main(int argc, char **argv) { CommandLineParser commandLine; bool getDownloadCounts = false; bool gPlotDownloads = false; try { commandLine.AddOption("--downloads", &getDownloadCounts); commandLine.AddOption("--gplot-downloads", &gPlotDownloads); commandLine.Parse(argc, (const char **)argv); if (commandLine.Arguments().size() != 0) { throw std::runtime_error("Invalid arguments."); } if (getDownloadCounts || gPlotDownloads) { GetDownloadCounts(gPlotDownloads); } else { SignPackage(); } } catch (const std::exception &e) { cout << "ERROR: " << e.what() << endl; return EXIT_FAILURE; } }