mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 23:17:06 +02:00
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
316 lines
14 KiB
C++
316 lines
14 KiB
C++
#include "server_update.h"
|
|
#include "Engine/Core/Platform/launcher_support.h"
|
|
#ifdef _WIN32
|
|
#include "Launcher/updater.h"
|
|
#include <Windows.h>
|
|
#else
|
|
#include "server_release.h"
|
|
#include "launcher_update_config.h"
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <vector>
|
|
#endif
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
|
|
namespace dingosdk::server {
|
|
#ifdef _WIN32
|
|
namespace {
|
|
std::mutex release_mutex;
|
|
std::optional<launcher_update::Config> release; // from the last check
|
|
|
|
std::filesystem::path self() {
|
|
std::wstring path(32768, L'\0');
|
|
const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
|
|
if (!length || length >= path.size()) throw std::runtime_error("Cannot find ReSkateServer.exe");
|
|
path.resize(length);
|
|
return path;
|
|
}
|
|
} // namespace
|
|
|
|
bool updates_enabled() noexcept { return launcher_update::binary_updates_enabled(); }
|
|
|
|
UpdateCheck check_for_update() {
|
|
UpdateCheck check;
|
|
try {
|
|
auto config = launcher_update::fetch_config();
|
|
if (!config) {
|
|
check.problem = "GitHub could not be reached";
|
|
return check;
|
|
}
|
|
if (config->server.url.empty()) {
|
|
check.problem = "the latest release has no server";
|
|
return check;
|
|
}
|
|
check.version = config->server.version;
|
|
check.available = launcher::sha256_file(self()) != config->server_exe_sha256;
|
|
std::lock_guard lock(release_mutex);
|
|
release = std::move(config);
|
|
} catch (const std::exception &e) {
|
|
check.problem = e.what();
|
|
}
|
|
return check;
|
|
}
|
|
|
|
void install_update(const std::filesystem::path &folder) {
|
|
launcher_update::RemoteFile file;
|
|
std::string exe_sha256;
|
|
{
|
|
std::lock_guard lock(release_mutex);
|
|
if (!release || release->server.url.empty()) throw std::runtime_error("no update has been found");
|
|
file = release->server;
|
|
exe_sha256 = release->server_exe_sha256;
|
|
}
|
|
const auto archive = launcher_update::download_verified(folder / L"ReSkateServer-update.zip", file);
|
|
struct Remove {
|
|
std::filesystem::path path;
|
|
~Remove() { DeleteFileW(path.c_str()); }
|
|
} remove{archive};
|
|
// Otherwise the new copy would find itself out of date and update forever.
|
|
if (launcher_update::archive_entry_sha256(archive, "ReSkateServer.exe") != exe_sha256)
|
|
throw std::runtime_error("the release's ReSkateServer.exe doesn't match launcher.json");
|
|
launcher_update::install_archive(archive, folder);
|
|
}
|
|
|
|
bool relaunch() {
|
|
STARTUPINFOW startup{};
|
|
startup.cb = sizeof(startup);
|
|
PROCESS_INFORMATION process{};
|
|
std::wstring command = GetCommandLineW();
|
|
const auto started = CreateProcessW(self().c_str(), command.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr,
|
|
&startup, &process);
|
|
if (!started) return false;
|
|
CloseHandle(process.hThread);
|
|
CloseHandle(process.hProcess);
|
|
return true;
|
|
}
|
|
|
|
void remove_previous_update(const std::filesystem::path &folder) noexcept {
|
|
launcher_update::remove_replaced_files(folder);
|
|
DeleteFileW((folder / L"ReSkateServer-update.zip.new").c_str());
|
|
}
|
|
#else // _WIN32
|
|
|
|
// Linux: the same releases, read with the tools every server machine has. curl does the HTTPS
|
|
// (as for the global bans) and tar unpacks; both are run directly, never through a shell, and
|
|
// nothing is installed that launcher.json does not pin by SHA-256.
|
|
namespace {
|
|
std::mutex release_mutex;
|
|
std::optional<LinuxRelease> release; // from the last check
|
|
|
|
// Read once, before an update can replace the file: /proc/self/exe then names a deleted one.
|
|
const std::filesystem::path &self() {
|
|
static const std::filesystem::path path = std::filesystem::read_symlink("/proc/self/exe");
|
|
return path;
|
|
}
|
|
|
|
// Runs a program found on PATH with these arguments and returns what it printed, at most
|
|
// `limit` bytes. Throws with the program's own complaint when it fails or cannot be started.
|
|
std::string run_tool(const std::vector<std::string> &arguments, std::size_t limit) {
|
|
int out[2], err[2];
|
|
if (pipe(out) != 0) throw std::runtime_error(arguments.front() + " could not be started");
|
|
if (pipe(err) != 0) {
|
|
close(out[0]), close(out[1]);
|
|
throw std::runtime_error(arguments.front() + " could not be started");
|
|
}
|
|
const auto child = fork();
|
|
if (child < 0) {
|
|
for (const int fd : {out[0], out[1], err[0], err[1]}) close(fd);
|
|
throw std::runtime_error(arguments.front() + " could not be started");
|
|
}
|
|
if (child == 0) {
|
|
std::vector<char *> argv;
|
|
for (const auto &argument : arguments) argv.push_back(const_cast<char *>(argument.c_str()));
|
|
argv.push_back(nullptr);
|
|
dup2(out[1], STDOUT_FILENO);
|
|
dup2(err[1], STDERR_FILENO);
|
|
if (const int null = open("/dev/null", O_RDONLY); null >= 0) dup2(null, STDIN_FILENO);
|
|
for (int fd = 3; fd < 1024; ++fd) close(fd);
|
|
execvp(argv.front(), argv.data());
|
|
_exit(127);
|
|
}
|
|
close(out[1]), close(err[1]);
|
|
std::string output, complaint;
|
|
bool too_much{};
|
|
pollfd fds[2]{{out[0], POLLIN, 0}, {err[0], POLLIN, 0}};
|
|
for (int open_ends = 2; open_ends;) {
|
|
if (poll(fds, 2, -1) < 0) {
|
|
if (errno == EINTR) continue;
|
|
break;
|
|
}
|
|
for (auto &fd : fds) {
|
|
if (fd.fd < 0 || !(fd.revents & (POLLIN | POLLHUP | POLLERR))) continue;
|
|
char buffer[65536];
|
|
const auto count = read(fd.fd, buffer, sizeof(buffer));
|
|
if (count > 0) {
|
|
auto &text = fd.fd == out[0] ? output : complaint;
|
|
const auto room = (fd.fd == out[0] ? limit : std::size_t{4096}) - std::min(text.size(), fd.fd == out[0] ? limit : std::size_t{4096});
|
|
text.append(buffer, std::min<std::size_t>(room, static_cast<std::size_t>(count)));
|
|
if (fd.fd == out[0] && static_cast<std::size_t>(count) > room) too_much = true;
|
|
} else if (count == 0 || errno != EINTR) {
|
|
close(fd.fd);
|
|
fd.fd = -1;
|
|
--open_ends;
|
|
}
|
|
}
|
|
}
|
|
for (auto &fd : fds)
|
|
if (fd.fd >= 0) close(fd.fd);
|
|
int status{};
|
|
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
|
|
if (WIFEXITED(status) && WEXITSTATUS(status) == 127 && complaint.empty())
|
|
throw std::runtime_error(arguments.front() + " is not installed");
|
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
|
while (!complaint.empty() && (complaint.back() == '\n' || complaint.back() == '\r')) complaint.pop_back();
|
|
std::replace(complaint.begin(), complaint.end(), '\n', ' ');
|
|
throw std::runtime_error(complaint.empty() ? arguments.front() + " failed" : complaint);
|
|
}
|
|
if (too_much) throw std::runtime_error(arguments.front() + " answered with more than expected");
|
|
return output;
|
|
}
|
|
|
|
std::vector<std::string> curl(const std::string &url, std::uint64_t limit, int seconds) {
|
|
if (!https_url(url)) throw std::runtime_error("a download link is not HTTPS");
|
|
return {"curl", "--silent", "--show-error", "--fail", "--location", "--proto", "=https", "--proto-redir", "=https",
|
|
"--max-time", std::to_string(seconds), "--max-filesize", std::to_string(limit), "--user-agent", "ReSkateServer/1",
|
|
"--url", url};
|
|
}
|
|
std::string https_text(const std::string &url, std::size_t limit) { return run_tool(curl(url, limit, 20), limit); }
|
|
|
|
struct RemovePath {
|
|
std::filesystem::path path;
|
|
~RemovePath() {
|
|
std::error_code ignored;
|
|
std::filesystem::remove_all(path, ignored);
|
|
}
|
|
};
|
|
const char *const archive_name = "ReSkateServer-update.tar.gz";
|
|
const char *const stage_name = ".reskate-update";
|
|
} // namespace
|
|
|
|
bool updates_enabled() noexcept { return launcher_update::launcher_binary_updates; }
|
|
|
|
UpdateCheck check_for_update() {
|
|
UpdateCheck check;
|
|
try {
|
|
std::optional<LinuxRelease> found;
|
|
if (const char *local = std::getenv("RESKATE_LAUNCHER_CONFIG_FILE"); local && *local) {
|
|
// Test an unpublished config before attaching it to a release, as the launcher can.
|
|
// Its links must be whole HTTPS ones: there is no release to find an "asset:" in.
|
|
std::ifstream input(local, std::ios::binary);
|
|
const std::string text{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
|
if (!input && !input.eof()) throw std::runtime_error(std::string("cannot read ") + local);
|
|
found = parse_linux_release(text, {});
|
|
} else {
|
|
std::string repo;
|
|
for (const wchar_t c : std::wstring_view(launcher_update::launcher_release_repo)) repo += static_cast<char>(c);
|
|
// Only this lookup uses GitHub's rate-limited API; the files come from the release's download links.
|
|
const auto assets = release_assets(https_text("https://api.github.com/repos/" + repo + "/releases/latest", 4 * 1024 * 1024));
|
|
const auto manifest = assets.find("launcher.json");
|
|
if (manifest == assets.end()) {
|
|
check.problem = "the latest release has no launcher.json";
|
|
return check;
|
|
}
|
|
found = parse_linux_release(https_text(manifest->second, 256 * 1024), assets);
|
|
}
|
|
if (!found) {
|
|
check.problem = "the latest release has no Linux server";
|
|
return check;
|
|
}
|
|
check.version = found->version;
|
|
check.available = launcher::sha256_file(self()) != found->exe_sha256;
|
|
std::lock_guard lock(release_mutex);
|
|
release = std::move(found);
|
|
} catch (const std::exception &e) {
|
|
check.problem = e.what();
|
|
}
|
|
return check;
|
|
}
|
|
|
|
void install_archive(const std::filesystem::path &archive, const std::filesystem::path &folder, std::string_view exe_sha256) {
|
|
namespace fs = std::filesystem;
|
|
// Looked at before it is unpacked: one folder, nothing that reaches outside it.
|
|
checked_archive_listing(run_tool({"tar", "-tzf", archive.string()}, 1024 * 1024));
|
|
const auto stage = folder / stage_name;
|
|
RemovePath remove_stage{stage};
|
|
fs::remove_all(stage);
|
|
fs::create_directory(stage);
|
|
run_tool({"tar", "-xzf", archive.string(), "-C", stage.string(), "--strip-components=1", "--no-same-owner"}, 64 * 1024);
|
|
// Plain files and folders only: a link could point anywhere.
|
|
std::vector<fs::path> files, folders;
|
|
for (auto entry = fs::recursive_directory_iterator(stage); entry != fs::recursive_directory_iterator(); ++entry) {
|
|
const auto status = entry->symlink_status();
|
|
if (fs::is_directory(status)) folders.push_back(fs::relative(entry->path(), stage));
|
|
else if (fs::is_regular_file(status)) files.push_back(fs::relative(entry->path(), stage));
|
|
else throw std::runtime_error("the update has something that is not a file or a folder");
|
|
}
|
|
const auto exe = self().filename();
|
|
// Otherwise the new copy would find itself out of date and update forever.
|
|
if (std::find(files.begin(), files.end(), exe) == files.end() || launcher::sha256_file(stage / exe) != exe_sha256)
|
|
throw std::runtime_error("the release's ReSkateServer doesn't match launcher.json");
|
|
// Nothing in the folder has changed until here. Each file then takes its place in one step
|
|
// (a rename within the folder), which Linux allows for a program and libraries in use; the
|
|
// server itself goes last, so a failure part-way leaves a server that updates again.
|
|
for (const auto &relative : folders) fs::create_directories(folder / relative);
|
|
std::stable_partition(files.begin(), files.end(), [&](const fs::path &file) { return file != exe; });
|
|
for (const auto &relative : files) fs::rename(stage / relative, folder / relative);
|
|
}
|
|
|
|
void install_update(const std::filesystem::path &folder) {
|
|
LinuxRelease pinned;
|
|
{
|
|
std::lock_guard lock(release_mutex);
|
|
if (!release) throw std::runtime_error("no update has been found");
|
|
pinned = *release;
|
|
}
|
|
const auto archive = folder / archive_name;
|
|
RemovePath remove_archive{archive};
|
|
auto download = curl(pinned.url, pinned.size, 600);
|
|
download.insert(download.end(), {"--output", archive.string()});
|
|
run_tool(download, 64 * 1024);
|
|
if (std::filesystem::file_size(archive) != pinned.size || launcher::sha256_file(archive) != pinned.sha256)
|
|
throw std::runtime_error("the downloaded update doesn't match launcher.json");
|
|
install_archive(archive, folder, pinned.exe_sha256);
|
|
}
|
|
|
|
// The updated server takes this one's place: the same process, so whatever started it (a
|
|
// terminal, systemd) keeps the server it started.
|
|
bool relaunch() {
|
|
std::vector<std::string> arguments;
|
|
{
|
|
std::ifstream in("/proc/self/cmdline", std::ios::binary);
|
|
for (std::string argument; std::getline(in, argument, '\0');) arguments.push_back(argument);
|
|
}
|
|
if (arguments.empty()) arguments.push_back(self().string());
|
|
std::vector<char *> argv;
|
|
for (auto &argument : arguments) argv.push_back(argument.data());
|
|
argv.push_back(nullptr);
|
|
// Nothing this server had open (Steam's sockets, the log) carries over.
|
|
std::error_code ignored;
|
|
std::vector<int> open_files;
|
|
for (const auto &entry : std::filesystem::directory_iterator("/proc/self/fd", ignored)) {
|
|
const auto fd = std::atoi(entry.path().filename().c_str());
|
|
if (fd > STDERR_FILENO) open_files.push_back(fd);
|
|
}
|
|
for (const int fd : open_files)
|
|
if (const int flags = fcntl(fd, F_GETFD); flags >= 0) fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
|
|
execv(self().c_str(), argv.data());
|
|
return false;
|
|
}
|
|
|
|
void remove_previous_update(const std::filesystem::path &folder) noexcept {
|
|
std::error_code ignored;
|
|
std::filesystem::remove_all(folder / stage_name, ignored);
|
|
std::filesystem::remove(folder / archive_name, ignored);
|
|
}
|
|
|
|
#endif // _WIN32
|
|
} // namespace dingosdk::server
|