From a6864ff7254c6ddd9907be89d8f4c36fb542f71b Mon Sep 17 00:00:00 2001 From: Diavolo Date: Fri, 16 Jun 2023 21:56:18 +0200 Subject: [PATCH] feature: /serverlist TCP endpoint --- .github/workflows/discord-notify.yml | 17 -- .github/workflows/draft-new-release.yml | 48 ------ .github/workflows/release.yml | 83 --------- src/Components/Modules/Download.cpp | 213 ++++++++++++++---------- src/Components/Modules/Download.hpp | 46 ++--- src/Components/Modules/Node.cpp | 6 +- src/Components/Modules/ServerList.cpp | 2 +- 7 files changed, 153 insertions(+), 262 deletions(-) delete mode 100644 .github/workflows/discord-notify.yml delete mode 100644 .github/workflows/draft-new-release.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/discord-notify.yml b/.github/workflows/discord-notify.yml deleted file mode 100644 index d80352fb..00000000 --- a/.github/workflows/discord-notify.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Notify Discord - -on: - push: - branches: - - "*" - issues: - -jobs: - notify: - runs-on: ubuntu-latest - if: github.repository_owner == 'XLabsProject' - steps: - - name: Send notification to Discord - uses: Ilshidur/action-discord@master - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_CI_BOT_WEBHOOK }} diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml deleted file mode 100644 index f3c13068..00000000 --- a/.github/workflows/draft-new-release.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: "Draft new release" - -on: - workflow_dispatch: - inputs: - version: - description: "The version you want to release." - required: true - -jobs: - draft-new-release: - name: "Draft a new release" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.5.2 - - - name: Normalize version - id: normalize_version - run: | - version="${{ github.event.inputs.version }}" - version="r${version#v}" - echo "::set-output name=version::$version" - - # Set up committer info and GPG key - - name: Import GPG key - id: import_gpg - uses: XLabsProject/ghaction-import-gpg@25d9d6ab99eb355c169c33c2306a72df85d9f516 - with: - git-commit-gpgsign: true - git-committer-email: "${{ secrets.XLABS_CI_EMAIL }}" - git-committer-name: "${{ secrets.XLABS_CI_NAME }}" - # git-push-gpgsign: true - git-tag-gpgsign: true - git-user-signingkey: true - gpg-private-key: ${{ secrets.XLABS_CI_GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.XLABS_CI_GPG_PASSWORD }} - - - name: Create Pull Request - uses: repo-sync/pull-request@v2 - with: - github_token: ${{ secrets.XLABS_CI_GITHUB_TOKEN }} - source_branch: "develop" - destination_branch: "master" - pr_allow_empty: true - pr_body: | - This Pull Request is for the release of IW4x ${{ steps.normalize_version.outputs.version }} and was [automatically created by a workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) triggered by @${{ github.actor }}. - pr_title: Release ${{ steps.normalize_version.outputs.version }} - pr_label: release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index fb5e9814..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Release - -on: - pull_request: - branches: - - "master" - types: [closed] -jobs: - merge: - runs-on: ubuntu-latest - name: Merge Release - steps: - - name: Check out files - if: github.event.pull_request.merged - uses: actions/checkout@v3.5.2 - with: - submodules: false - lfs: false - - # Set up committer info and GPG key - - name: Import GPG key - if: github.event.pull_request.merged - id: import_gpg - uses: XLabsProject/ghaction-import-gpg@25d9d6ab99eb355c169c33c2306a72df85d9f516 - with: - git-commit-gpgsign: true - git-committer-email: "${{ secrets.XLABS_CI_EMAIL }}" - git-committer-name: "${{ secrets.XLABS_CI_NAME }}" - git-push-gpgsign: false - git-tag-gpgsign: true - git-user-signingkey: true - gpg-private-key: ${{ secrets.XLABS_CI_GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.XLABS_CI_GPG_PASSWORD }} - - - name: Extract version from pull request - if: github.event.pull_request.merged - id: extract_version - run: | - title="${{ github.event.pull_request.title }}" - version="${title#Release }" - echo "::set-output name=version::$version" - - - name: Create annotated tag - if: github.event.pull_request.merged - run: | - git tag -a -m "${{ github.event.pull_request.title }}" \ - "${{ steps.extract_version.outputs.version }}" \ - "${{ github.event.pull_request.merge_commit_sha }}" - git push origin --tags - - - name: Create Pull Request - if: github.event.pull_request.merged - uses: repo-sync/pull-request@v2 - with: - github_token: ${{ secrets.XLABS_CI_GITHUB_TOKEN }} - source_branch: "master" - destination_branch: "develop" - pr_allow_empty: true - pr_body: | - This Pull Request merges the release of IW4x ${{ steps.extract_version.outputs.version }} and was [automatically created by a workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) triggered by @${{ github.actor }}. - pr_title: Merge release ${{ steps.extract_version.outputs.version }} - pr_label: release - - - notify: - name: Notify Discord - runs-on: ubuntu-latest - if: | - github.repository_owner == 'XLabsProject' && ( - ( - github.event.pull_request.merged - ) || ( - github.event.push.ref == 'refs/heads/master' || - github.event.push.ref == 'refs/heads/develop' - ) - ) - steps: - - name: Post CI status notification to Discord - uses: sarisia/actions-status-discord@v1.7.1 - if: always() - with: - webhook: ${{ secrets.DISCORD_CI_BOT_WEBHOOK }} - title: "Build" diff --git a/src/Components/Modules/Download.cpp b/src/Components/Modules/Download.cpp index eee8c80d..282f6e86 100644 --- a/src/Components/Modules/Download.cpp +++ b/src/Components/Modules/Download.cpp @@ -5,6 +5,7 @@ #include "Download.hpp" #include "Events.hpp" #include "MapRotation.hpp" +#include "Node.hpp" #include "Party.hpp" #include "ServerInfo.hpp" @@ -40,7 +41,7 @@ namespace Components void Download::InitiateClientDownload(const std::string& mod, bool needPassword, bool map) { - if (CLDownload.running) return; + if (CLDownload.running_) return; Scheduler::Once([] { @@ -61,26 +62,26 @@ namespace Components return; } - CLDownload.hashedPassword = Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(password), ""); + CLDownload.hashedPassword_ = Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(password), ""); } - CLDownload.running = true; - CLDownload.isMap = map; - CLDownload.mod = mod; - CLDownload.terminateThread = false; - CLDownload.totalBytes = 0; - CLDownload.lastTimeStamp = 0; - CLDownload.downBytes = 0; - CLDownload.timeStampBytes = 0; - CLDownload.isPrivate = needPassword; - CLDownload.target = Party::Target(); - CLDownload.thread = std::thread(ModDownloader, &CLDownload); + CLDownload.running_ = true; + CLDownload.isMap_ = map; + CLDownload.mod_ = mod; + CLDownload.terminateThread_ = false; + CLDownload.totalBytes_ = 0; + CLDownload.lastTimeStamp_ = 0; + CLDownload.downBytes_ = 0; + CLDownload.timeStampBytes_ = 0; + CLDownload.isPrivate_ = needPassword; + CLDownload.target_ = Party::Target(); + CLDownload.thread_ = std::thread(ModDownloader, &CLDownload); } bool Download::ParseModList(ClientDownload* download, const std::string& list) { if (!download) return false; - download->files.clear(); + download->files_.clear(); nlohmann::json listData; try @@ -98,7 +99,7 @@ namespace Components return false; } - download->totalBytes = 0; + download->totalBytes_ = 0; const nlohmann::json::array_t listDataArray = listData; for (auto& file : listDataArray) @@ -118,8 +119,8 @@ namespace Components if (!fileEntry.name.empty()) { - download->files.push_back(fileEntry); - download->totalBytes += fileEntry.size; + download->files_.push_back(fileEntry); + download->totalBytes_ += fileEntry.size; } } catch (const nlohmann::json::exception& ex) @@ -134,12 +135,12 @@ namespace Components bool Download::DownloadFile(ClientDownload* download, unsigned int index) { - if (!download || download->files.size() <= index) return false; + if (!download || download->files_.size() <= index) return false; - auto file = download->files[index]; + auto file = download->files_[index]; - auto path = download->mod + "/" + file.name; - if (download->isMap) + auto path = download->mod_ + "/" + file.name; + if (download->isMap_) { path = "usermaps/" + path; } @@ -149,16 +150,16 @@ namespace Components auto data = Utils::IO::ReadFile(path); if (data.size() == file.size && Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(data), "") == file.hash) { - download->totalBytes += file.size; + download->totalBytes_ += file.size; return true; } } - auto host = "http://" + download->target.getString(); + auto host = "http://" + download->target_.getString(); auto fastHost = SV_wwwBaseUrl.get(); if (Utils::String::StartsWith(fastHost, "https://")) { - download->thread.detach(); + download->thread_.detach(); download->clear(); Scheduler::Once([] @@ -197,8 +198,8 @@ namespace Components } else { - url = host + "/file/" + (download->isMap ? "map/" : "") + file.name - + (download->isPrivate ? ("?password=" + download->hashedPassword) : ""); + url = host + "/file/" + (download->isMap_ ? "map/" : "") + file.name + + (download->isPrivate_ ? ("?password=" + download->hashedPassword_) : ""); } Logger::Print("Downloading from url {}\n", url); @@ -212,14 +213,14 @@ namespace Components Utils::String::Replace(url, " ", "%20"); - download->valid = true; + download->valid_ = true; fDownload.downloading = true; Utils::WebIO webIO; webIO.setProgressCallback([&fDownload, &webIO](std::size_t bytes, std::size_t) { - if(!fDownload.downloading || fDownload.download->terminateThread) + if(!fDownload.downloading || fDownload.download->terminateThread_) { webIO.cancelDownload(); return; @@ -234,14 +235,14 @@ namespace Components fDownload.downloading = false; - download->valid = false; + download->valid_ = false; if (fDownload.buffer.size() != file.size || Utils::Cryptography::SHA256::Compute(fDownload.buffer, true) != file.hash) { return false; } - if (download->isMap) Utils::IO::CreateDir("usermaps/" + download->mod); + if (download->isMap_) Utils::IO::CreateDir("usermaps/" + download->mod_); Utils::IO::WriteFile(path, fDownload.buffer); return true; @@ -251,16 +252,16 @@ namespace Components { if (!download) download = &CLDownload; - const auto host = "http://" + download->target.getString(); + const auto host = "http://" + download->target_.getString(); - const auto listUrl = host + (download->isMap ? "/map" : "/list") + (download->isPrivate ? ("?password=" + download->hashedPassword) : ""); + const auto listUrl = host + (download->isMap_ ? "/map" : "/list") + (download->isPrivate_ ? ("?password=" + download->hashedPassword_) : ""); const auto list = Utils::WebIO("IW4x", listUrl).setTimeout(5000)->get(); if (list.empty()) { - if (download->terminateThread) return; + if (download->terminateThread_) return; - download->thread.detach(); + download->thread_.detach(); download->clear(); Scheduler::Once([] @@ -272,13 +273,13 @@ namespace Components return; } - if (download->terminateThread) return; + if (download->terminateThread_) return; if (!ParseModList(download, list)) { - if (download->terminateThread) return; + if (download->terminateThread_) return; - download->thread.detach(); + download->thread_.detach(); download->clear(); Scheduler::Once([] @@ -290,21 +291,21 @@ namespace Components return; } - if (download->terminateThread) return; + if (download->terminateThread_) return; static std::string mod; - mod = download->mod; + mod = download->mod_; - for (std::size_t i = 0; i < download->files.size(); ++i) + for (std::size_t i = 0; i < download->files_.size(); ++i) { - if (download->terminateThread) return; + if (download->terminateThread_) return; if (!DownloadFile(download, i)) { - if (download->terminateThread) return; + if (download->terminateThread_) return; - mod = std::format("Failed to download file: {}!", download->files[i].name); - download->thread.detach(); + mod = std::format("Failed to download file: {}!", download->files_[i].name); + download->thread_.detach(); download->clear(); Scheduler::Once([] @@ -320,12 +321,12 @@ namespace Components } } - if (download->terminateThread) return; + if (download->terminateThread_) return; - download->thread.detach(); + download->thread_.detach(); download->clear(); - if (download->isMap) + if (download->isMap_) { Scheduler::Once([] { @@ -357,22 +358,22 @@ namespace Components void Download::DownloadProgress(FileDownload* fDownload, std::size_t bytes) { fDownload->receivedBytes += bytes; - fDownload->download->downBytes += bytes; - fDownload->download->timeStampBytes += bytes; + fDownload->download->downBytes_ += bytes; + fDownload->download->timeStampBytes_ += bytes; static volatile bool framePushed = false; if (!framePushed) { double progress = 0; - if (fDownload->download->totalBytes) + if (fDownload->download->totalBytes_) { - progress = (100.0 / fDownload->download->totalBytes) * fDownload->download->downBytes; + progress = (100.0 / fDownload->download->totalBytes_) * fDownload->download->downBytes_; } static std::uint32_t dlIndex, dlSize, dlProgress; dlIndex = fDownload->index + 1; - dlSize = fDownload->download->files.size(); + dlSize = fDownload->download->files_.size(); dlProgress = static_cast(progress); framePushed = true; @@ -383,18 +384,18 @@ namespace Components }, Scheduler::Pipeline::MAIN); } - auto delta = Game::Sys_Milliseconds() - fDownload->download->lastTimeStamp; + auto delta = Game::Sys_Milliseconds() - fDownload->download->lastTimeStamp_; if (delta > 300) { - const auto doFormat = fDownload->download->lastTimeStamp != 0; - fDownload->download->lastTimeStamp = Game::Sys_Milliseconds(); + const auto doFormat = fDownload->download->lastTimeStamp_ != 0; + fDownload->download->lastTimeStamp_ = Game::Sys_Milliseconds(); - const auto dataLeft = fDownload->download->totalBytes - fDownload->download->downBytes; + const auto dataLeft = fDownload->download->totalBytes_ - fDownload->download->downBytes_; int timeLeft = 0; - if (fDownload->download->timeStampBytes) + if (fDownload->download->timeStampBytes_) { - const double timeLeftD = ((1.0 * dataLeft) / fDownload->download->timeStampBytes) * delta; + const double timeLeftD = ((1.0 * dataLeft) / fDownload->download->timeStampBytes_) * delta; timeLeft = static_cast(timeLeftD); } @@ -404,7 +405,7 @@ namespace Components static int dlDelta, dlTimeLeft; dlTimeLeft = timeLeft; dlDelta = delta; - dlTsBytes = fDownload->download->timeStampBytes; + dlTsBytes = fDownload->download->timeStampBytes_; Scheduler::Once([] { @@ -413,7 +414,7 @@ namespace Components }, Scheduler::Pipeline::MAIN); } - fDownload->download->timeStampBytes = 0; + fDownload->download->timeStampBytes_ = 0; } } @@ -434,7 +435,7 @@ namespace Components MongooseLogBuffer.push_back(c); } - static std::string InfoHandler() + static std::optional InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) { const auto status = ServerInfo::GetInfo(); const auto host = ServerInfo::GetHostInfo(); @@ -480,10 +481,12 @@ namespace Components } info["players"] = players; - return nlohmann::json(info).dump(); + std::string out = nlohmann::json(info).dump(); + + return { out }; } - static std::string ListHandler() + static std::optional ListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) { static nlohmann::json jsonList; static std::filesystem::path fsGamePre; @@ -526,10 +529,12 @@ namespace Components jsonList = fileList; } - return jsonList.dump(); + std::string out = jsonList.dump(); + + return { out }; } - static std::string MapHandler() + static std::optional MapHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) { static std::string mapNamePre; static nlohmann::json jsonList; @@ -570,10 +575,12 @@ namespace Components jsonList = fileList; } - return jsonList.dump(); + std::string out = jsonList.dump(); + + return { out }; } - static void FileHandler(mg_connection* c, const mg_http_message* hm) + static std::optional FileHandler(mg_connection* c, const mg_http_message* hm) { std::string url(hm->uri.ptr, hm->uri.len); @@ -602,7 +609,7 @@ namespace Components if ((!Maps::GetUserMap()->isValid() && !Party::IsInUserMapLobby()) || !isValidFile) { mg_http_reply(c, 403, "Content-Type: text/html\r\n", "%s", "403 - Forbidden"); - return; + return {}; } url = std::format("usermaps\\{}\\{}", mapName, url); @@ -612,7 +619,7 @@ namespace Components if ((!url.ends_with(".iwd") && url != "mod.ff") || url.find("_svr_") != std::string::npos) { mg_http_reply(c, 403, "Content-Type: text/html\r\n", "%s", "403 - Forbidden"); - return; + return {}; } } @@ -633,10 +640,44 @@ namespace Components mg_printf(c, "%s", "\r\n"); mg_send(c, file.data(), file.size()); } + + return {}; + } + + static std::optional ServerListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm) + { + std::vector servers; + + const auto nodes = Node::GetNodes(); + for (const auto& node : nodes) + { + const auto address = node.address.getString(); + servers.emplace_back(address); + } + + nlohmann::json jsonList = servers; + std::string out = jsonList.dump(); + + return { out }; } static void EventHandler(mg_connection* c, const int ev, void* ev_data, [[maybe_unused]] void* fn_data) { + using callback = std::function(mg_connection*, const mg_http_message*)>; + + static const auto handlers = []() -> std::unordered_map + { + std::unordered_map f; + + f["/file"] = FileHandler; + f["/info"] = InfoHandler; + f["/list"] = ListHandler; + f["/map"] = MapHandler; + f["/serverlist"] = ServerListHandler; + + return f; + }(); + if (ev != MG_EV_HTTP_MSG) { return; @@ -645,26 +686,24 @@ namespace Components auto* hm = static_cast(ev_data); const std::string url(hm->uri.ptr, hm->uri.len); - if (url.starts_with("/info")) + auto handled = false; + for (auto i = handlers.begin(); i != handlers.end();) { - const auto reply = InfoHandler(); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.data()); + if (url.starts_with(i->first)) + { + if (const auto reply = i->second(c, hm)) + { + mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.value().data()); + } + + handled = true; + break; + } + + ++i; } - else if (url.starts_with("/list")) - { - const auto reply = ListHandler(); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.data()); - } - else if (url.starts_with("/map")) - { - const auto reply = MapHandler(); - mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.data()); - } - else if (url.starts_with("/file")) - { - FileHandler(c, hm); - } - else + + if (!handled) { mg_http_serve_opts opts = { .root_dir = "iw4x/html" }; // Serve local dir mg_http_serve_dir(c, hm, &opts); diff --git a/src/Components/Modules/Download.hpp b/src/Components/Modules/Download.hpp index fe8ab847..84ee7584 100644 --- a/src/Components/Modules/Download.hpp +++ b/src/Components/Modules/Download.hpp @@ -24,24 +24,24 @@ namespace Components class ClientDownload { public: - ClientDownload(bool _isMap = false) : running(false), valid(false), terminateThread(false), isMap(_isMap), totalBytes(0), downBytes(0), lastTimeStamp(0), timeStampBytes(0) {} + ClientDownload(bool isMap = false) : running_(false), valid_(false), terminateThread_(false), isMap_(isMap), totalBytes_(0), downBytes_(0), lastTimeStamp_(0), timeStampBytes_(0) {} ~ClientDownload() { this->clear(); } - bool running; - bool valid; - bool terminateThread; - bool isMap; - bool isPrivate; - Network::Address target; - std::string hashedPassword; - std::string mod; - std::thread thread; + bool running_; + bool valid_; + bool terminateThread_; + bool isMap_; + bool isPrivate_; + Network::Address target_; + std::string hashedPassword_; + std::string mod_; + std::thread thread_; - std::size_t totalBytes; - std::size_t downBytes; + std::size_t totalBytes_; + std::size_t downBytes_; - int lastTimeStamp; - std::size_t timeStampBytes; + int lastTimeStamp_; + std::size_t timeStampBytes_; class File { @@ -51,24 +51,24 @@ namespace Components std::size_t size; }; - std::vector files; + std::vector files_; void clear() { - this->terminateThread = true; + this->terminateThread_ = true; - if (this->thread.joinable()) + if (this->thread_.joinable()) { - this->thread.join(); + this->thread_.join(); } - this->running = false; - this->mod.clear(); - this->files.clear(); + this->running_ = false; + this->mod_.clear(); + this->files_.clear(); - if (this->valid) + if (this->valid_) { - this->valid = false; + this->valid_ = false; } } }; diff --git a/src/Components/Modules/Node.cpp b/src/Components/Modules/Node.cpp index 3fa2217a..b922e249 100644 --- a/src/Components/Modules/Node.cpp +++ b/src/Components/Modules/Node.cpp @@ -353,18 +353,18 @@ namespace Components Scheduler::OnGameInitialized(loadNodes, Scheduler::Pipeline::MAIN); - Command::Add("listnodes", [](const Command::Params*) + Command::Add("listNodes", [](const Command::Params*) { Logger::Print("Nodes: {}\n", Node::Nodes.size()); std::lock_guard _(Node::Mutex); - for (auto& node : Node::Nodes) + for (const auto& node : Node::Nodes) { Logger::Print("{}\t({})\n", node.address.getString(), node.isValid() ? "Valid" : "Invalid"); } }); - Command::Add("addnode", [](const Command::Params* params) + Command::Add("addNode", [](const Command::Params* params) { if (params->size() < 2) return; auto address = Network::Address{ params->get(1) }; diff --git a/src/Components/Modules/ServerList.cpp b/src/Components/Modules/ServerList.cpp index 06668ad3..1f5f58db 100644 --- a/src/Components/Modules/ServerList.cpp +++ b/src/Components/Modules/ServerList.cpp @@ -29,7 +29,7 @@ namespace Components Dvar::Var ServerList::NETServerQueryLimit; Dvar::Var ServerList::NETServerFrames; - bool ServerList::UseMasterServer = true; + bool ServerList::UseMasterServer = false; std::vector* ServerList::GetList() {