Files
ReSkate/Server/server_release.cpp
AyeZeeBB 101e1af6e4 Official, staff and friends in the server browser; server tokens; Linux self-update
Server browser
- Official servers (the backend's "official_servers") sort first and are
  drawn in ReSkate blue
- Servers Steam friends are skating in sort next, drawn in green with the
  friends' names; searching a friend's name finds their server
- A player in a session they joined can join another server straight from
  the list; hosts still end their session first, and a Steam friend's join
  offer never replaces a session
- Joining a dedicated server from the list records it as the player's
  public session, so friends see it and the row shows HERE
- Blocked servers (the backend's "blocked_servers") are hidden and cannot
  be joined, by code either
- When the backend sets "server_tokens_required", dedicated servers that
  signed in without a login token are hidden, except on the player's own
  network
- A search that gets no internet list from Steam keeps the last list for
  two minutes, and waits six seconds before treating it as empty
Roles
- New Staff tag from the backend's "staff" list: green name, badge and
  item animation, ranked after Dev
- Staff can pick a rainbow for their items on the Special page; it is sent
  as a solid colour older builds can show, so the protocol is unchanged
Dedicated server
- steam_token: sign in with a Steam game server login token and keep the
  same Steam ID every start; empty signs in anonymously as before
- A listed server without a token says so at startup, and when the token
  rule starts or stops hiding it
- Linux servers update themselves from the release's "server_linux"
  tarball, with the machine's curl and tar, pinned by SHA-256, and carry
  on in the same process
Launcher
- Only create the MiniDumpWriteDump API-set shim under Wine/Proton
2026-10-07 00:19:56 -05:00

98 lines
5.0 KiB
C++

#include "server_release.h"
#include "Engine/Core/Json/json.h"
#include <algorithm>
#include <cctype>
#include <stdexcept>
namespace dingosdk::server {
namespace {
bool sha256_text(std::string_view text) noexcept {
return text.size() == 64 &&
std::all_of(text.begin(), text.end(), [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); });
}
} // namespace
bool https_url(std::string_view url) noexcept {
constexpr std::string_view scheme = "https://";
if (!url.starts_with(scheme) || url.size() == scheme.size() || url.size() > 2048) return false;
return std::all_of(url.begin(), url.end(), [](unsigned char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
std::string_view("-._~:/?#[]@!$&'()*+,;=%").find(static_cast<char>(c)) != std::string_view::npos;
});
}
std::map<std::string, std::string, std::less<>> release_assets(std::string_view release_json) {
const auto release = Json::parse(release_json);
if (!release.is_object() || !release.contains("assets") || !release.at("assets").is_array())
throw std::runtime_error("GitHub's answer is not a release");
std::map<std::string, std::string, std::less<>> assets;
for (const auto &asset : release.at("assets"))
if (asset.is_object()) assets[asset.value("name", "")] = asset.value("browser_download_url", "");
return assets;
}
std::optional<LinuxRelease> parse_linux_release(std::string_view launcher_json,
const std::map<std::string, std::string, std::less<>> &assets) {
const auto root = Json::parse(launcher_json);
if (!root.is_object()) throw std::runtime_error("launcher.json is not a JSON object");
if (!root.contains("server_linux")) return std::nullopt;
const auto &entry = root.at("server_linux");
if (!entry.is_object()) throw std::runtime_error("launcher.json \"server_linux\" is not an object");
LinuxRelease release;
release.version = entry.value("version", "");
release.url = entry.value("url", "");
release.sha256 = entry.value("sha256", "");
release.exe_sha256 = entry.value("exe_sha256", "");
if (entry.contains("size") && entry.at("size").is_number_unsigned()) release.size = entry.at("size").get<std::uint64_t>();
constexpr std::string_view asset = "asset:";
if (release.url.starts_with(asset)) {
const auto found = assets.find(std::string_view(release.url).substr(asset.size()));
if (found == assets.end()) throw std::runtime_error("the release is missing asset " + release.url.substr(asset.size()));
release.url = found->second;
}
if (!https_url(release.url)) throw std::runtime_error("launcher.json \"server_linux.url\" is not an HTTPS link");
if (!sha256_text(release.sha256) || !sha256_text(release.exe_sha256))
throw std::runtime_error("launcher.json \"server_linux\" hashes must be 64 lowercase hex digits");
if (!release.size || release.size > max_linux_release_bytes)
throw std::runtime_error("launcher.json \"server_linux.size\" is out of range");
if (release.version.empty() || release.version.size() > 32 ||
!std::all_of(release.version.begin(), release.version.end(),
[](unsigned char c) { return std::isalnum(c) || c == '.' || c == '-' || c == '_'; }))
throw std::runtime_error("launcher.json \"server_linux.version\" is not a version");
return release;
}
std::string checked_archive_listing(std::string_view listing) {
std::string top;
std::size_t entries{};
for (std::size_t start = 0; start < listing.size();) {
auto end = listing.find('\n', start);
if (end == std::string_view::npos) end = listing.size();
auto line = listing.substr(start, end - start);
start = end + 1;
if (!line.empty() && line.back() == '\r') line.remove_suffix(1);
if (line.empty()) continue;
if (line.front() == '/' || line.find('\\') != std::string_view::npos || line.size() > 512)
throw std::runtime_error("the update has a file outside its folder");
// "./name/..." is how some tars write it.
if (line.starts_with("./")) line.remove_prefix(2);
std::string_view first;
for (std::size_t at = 0; at <= line.size();) {
auto slash = line.find('/', at);
if (slash == std::string_view::npos) slash = line.size();
const auto part = line.substr(at, slash - at);
if (part == ".." || part == "." || (part.empty() && slash != line.size()))
throw std::runtime_error("the update has a file outside its folder");
if (at == 0) first = part;
at = slash + 1;
}
if (first.empty()) throw std::runtime_error("the update has a file outside its folder");
if (top.empty()) top = first;
else if (top != first) throw std::runtime_error("the update is not one folder");
++entries;
}
if (top.empty() || entries < 2) throw std::runtime_error("the update is empty");
return top;
}
} // namespace dingosdk::server