mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-23 11:01:04 +02:00
1 parent
ab4944a907
commit
becaee71c0
20 files changed
+605
-19
No files matched your search
+1
-13
@@ -57,19 +57,7 @@ Makefile.cmake
|
||||
/cmake-build-release/
|
||||
|
||||
# Roblox payload
|
||||
/rbx_bin/libroblox.so
|
||||
/rbx_bin/libroblox.so.*
|
||||
/rbx_bin/client_settings_android.json
|
||||
/rbx_bin/assets/
|
||||
/rbx_bin/android/
|
||||
/rbx_bin/sober_apk/
|
||||
/rbx_bin/versions/
|
||||
/rbx_bin/.payload-update.*/
|
||||
/rbx_bin/*.apk
|
||||
/rbx_bin/*.so
|
||||
/rbx_bin/*.so.*
|
||||
|
||||
!/rbx_bin/.gitkeep
|
||||
/rbx_bin/
|
||||
|
||||
# Runtime state
|
||||
/runtime/
|
||||
|
||||
@@ -268,6 +268,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
src/runtime/frame_rate_policy.cc
|
||||
src/runtime/fleasion.cc
|
||||
src/runtime/game_mode.cc
|
||||
src/runtime/game_session_coordinator.cc
|
||||
src/runtime/graphics_launch_policy.cc
|
||||
@@ -329,6 +330,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
endif()
|
||||
target_link_libraries(mocktail_runtime PUBLIC Threads::Threads)
|
||||
target_link_libraries(mocktail_runtime PRIVATE
|
||||
OpenSSL::Crypto
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
PkgConfig::GIO
|
||||
|
||||
@@ -75,6 +75,16 @@ audio:
|
||||
input_device: default
|
||||
|
||||
integrations:
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
enabled: false
|
||||
# String: match Fleasion's routing mode: env or hosts.
|
||||
proxy_mode: env
|
||||
# Integer: Fleasion's local HTTP proxy port in env mode.
|
||||
proxy_port: 58443
|
||||
# Optional absolute public CA path. Default: $XDG_CONFIG_HOME/Fleasion/proxy_ca/ca.crt
|
||||
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
|
||||
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_FLEASION_H_
|
||||
#define MOCKTAIL_RUNTIME_FLEASION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "runtime/runtime_config.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
struct FleasionPreparation {
|
||||
std::filesystem::path certificate;
|
||||
std::filesystem::path base_bundle;
|
||||
std::filesystem::path bundle;
|
||||
std::string error;
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Generates a private, replaceable trust bundle outside the managed payload.
|
||||
// Reads certificates only; never reads Fleasion's private key or changes it.
|
||||
FleasionPreparation PrepareFleasion(const RuntimeConfig& config,
|
||||
const Environment& environment,
|
||||
const RuntimePaths& paths);
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif
|
||||
@@ -119,6 +119,13 @@ class RuntimeConfig {
|
||||
}
|
||||
bool ca_bundle_valid() const { return ca_bundle_valid_; }
|
||||
bool use_system_proxy() const { return use_system_proxy_; }
|
||||
bool fleasion_enabled() const { return fleasion_enabled_; }
|
||||
bool fleasion_valid() const { return fleasion_valid_; }
|
||||
const std::string& fleasion_proxy_mode() const { return fleasion_proxy_mode_; }
|
||||
int fleasion_proxy_port() const { return fleasion_proxy_port_; }
|
||||
const std::optional<std::filesystem::path>& fleasion_ca_certificate() const {
|
||||
return fleasion_ca_certificate_;
|
||||
}
|
||||
const DiscordRpcConfig& discord_rpc() const { return discord_rpc_; }
|
||||
bool discord_rpc_valid() const { return discord_rpc_valid_; }
|
||||
|
||||
@@ -152,6 +159,11 @@ class RuntimeConfig {
|
||||
std::string audio_input_device_ = "default";
|
||||
bool audio_input_device_valid_ = true;
|
||||
bool use_system_proxy_ = false;
|
||||
bool fleasion_enabled_ = false;
|
||||
bool fleasion_valid_ = true;
|
||||
std::string fleasion_proxy_mode_ = "env";
|
||||
int fleasion_proxy_port_ = 58443;
|
||||
std::optional<std::filesystem::path> fleasion_ca_certificate_;
|
||||
std::optional<NetworkProxyConfig> network_proxy_;
|
||||
std::optional<std::filesystem::path> ca_bundle_;
|
||||
bool ca_bundle_valid_ = true;
|
||||
|
||||
Whitespace-only changes.
@@ -8,16 +8,19 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits.h>
|
||||
#include <unordered_map>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace libc_shim {
|
||||
|
||||
namespace {
|
||||
|
||||
std::unordered_map<std::string, std::string> g_path_mappings;
|
||||
std::vector<std::string> g_ca_bundle_aliases;
|
||||
|
||||
bool g_installed = false;
|
||||
std::atomic<GuestAllocator> g_guest_allocator{nullptr};
|
||||
@@ -27,9 +30,15 @@ constexpr std::array<const char*, 2> kDefaultCaBundles = {
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
};
|
||||
constexpr std::array<const char*, 2> kAndroidCaBundlePaths = {
|
||||
constexpr std::array<const char*, 8> kAndroidCaBundlePaths = {
|
||||
"/data/user/0/com.roblox.client/files/exe/cacert.pem",
|
||||
"/data/data/com.roblox.client/files/exe/cacert.pem",
|
||||
"/data/user/0/com.roblox.client/files/exe/ssl/cacert.pem",
|
||||
"/data/data/com.roblox.client/files/exe/ssl/cacert.pem",
|
||||
"ssl/cacert.pem",
|
||||
"content/ssl/cacert.pem",
|
||||
"rbx_bin/assets/ssl/cacert.pem",
|
||||
"rbx_bin/assets/content/ssl/cacert.pem",
|
||||
};
|
||||
|
||||
bool IsEnabled(const char* name) {
|
||||
@@ -192,17 +201,22 @@ HostCaBundleResolution ResolveHostCaBundle() {
|
||||
}
|
||||
|
||||
HostCaBundleResolution ConfigureHostCaBundlePathMappings() {
|
||||
for (const char* android_path : kAndroidCaBundlePaths) {
|
||||
g_path_mappings.erase(android_path);
|
||||
for (const auto& alias : g_ca_bundle_aliases) g_path_mappings.erase(alias);
|
||||
g_ca_bundle_aliases.assign(kAndroidCaBundlePaths.begin(), kAndroidCaBundlePaths.end());
|
||||
if (const char* content = GetEnvNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
||||
auto root = std::filesystem::path(content).lexically_normal();
|
||||
if (root.filename().empty()) root = root.parent_path();
|
||||
g_ca_bundle_aliases.push_back((root / "ssl/cacert.pem").string());
|
||||
if (root.filename() == "content")
|
||||
g_ca_bundle_aliases.push_back((root.parent_path() / "ssl/cacert.pem").string());
|
||||
}
|
||||
|
||||
HostCaBundleResolution resolution = ResolveHostCaBundle();
|
||||
if (!resolution.ok()) {
|
||||
return resolution;
|
||||
}
|
||||
for (const char* android_path : kAndroidCaBundlePaths) {
|
||||
RegisterPathMapping(android_path, resolution.host_path);
|
||||
}
|
||||
for (const auto& alias : g_ca_bundle_aliases)
|
||||
RegisterPathMapping(alias, resolution.host_path);
|
||||
return resolution;
|
||||
}
|
||||
|
||||
@@ -308,6 +322,7 @@ void RegisterPathMapping(const std::string& android_prefix,
|
||||
|
||||
void ClearPathMappings() {
|
||||
g_path_mappings.clear();
|
||||
g_ca_bundle_aliases.clear();
|
||||
}
|
||||
|
||||
} // namespace libc_shim
|
||||
|
||||
+33
@@ -29,6 +29,7 @@
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/external_launch_broker.h"
|
||||
#include "runtime/failure_dialog.h"
|
||||
#include "runtime/fleasion.h"
|
||||
#include "runtime/game_mode.h"
|
||||
#include "runtime/graphics_launch_policy.h"
|
||||
#include "runtime/memory_limit.h"
|
||||
@@ -469,6 +470,38 @@ int main(int argc, char* argv[]) {
|
||||
mocktail::runtime::LogProcessDiagnostics(
|
||||
mocktail::runtime::ProcessDiagnosticStage::kStartup);
|
||||
}
|
||||
if (runtime_config.config.fleasion_enabled()) {
|
||||
const auto fleasion = mocktail::runtime::PrepareFleasion(
|
||||
runtime_config.config, environment, paths);
|
||||
if (!fleasion ||
|
||||
setenv("MOCKTAIL_CA_BUNDLE", fleasion.bundle.c_str(), 1) != 0 ||
|
||||
setenv("MOCKTAIL_FLEASION_BASE_CA_BUNDLE", fleasion.base_bundle.c_str(), 1) != 0 ||
|
||||
setenv("MOCKTAIL_FLEASION_GENERATED_BUNDLE", fleasion.bundle.c_str(), 1) != 0 ||
|
||||
setenv("MOCKTAIL_FLEASION_CA_CERTIFICATE", fleasion.certificate.c_str(), 1) != 0) {
|
||||
std::cerr << "[FATAL] Cannot prepare Fleasion: "
|
||||
<< (!fleasion ? fleasion.error : "cannot export certificate paths") << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (const auto& proxy = runtime_config.config.network_proxy(); proxy &&
|
||||
(setenv("MOCKTAIL_HTTP_PROXY_HOST", proxy->host.c_str(), 1) != 0 ||
|
||||
setenv("MOCKTAIL_HTTP_PROXY_PORT", std::to_string(proxy->port).c_str(), 1) != 0 ||
|
||||
setenv("MOCKTAIL_HTTP_PROXY_SCHEME", proxy->scheme.c_str(), 1) != 0)) {
|
||||
std::cerr << "[FATAL] Cannot export Fleasion proxy\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
runtime_config = mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
|
||||
if (!runtime_config) {
|
||||
std::cerr << "[FATAL] Cannot apply Fleasion: " << runtime_config.error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << " [fleasion] mode=" << runtime_config.config.fleasion_proxy_mode()
|
||||
<< " certificate=" << fleasion.certificate
|
||||
<< " trust_bundle=" << fleasion.bundle << '\n';
|
||||
if (runtime_config.config.network_proxy()) {
|
||||
std::cout << " [fleasion] proxy=" << mocktail::runtime::BuildNetworkProxyUrl(
|
||||
*runtime_config.config.network_proxy()) << "; start Fleasion before Roblox\n";
|
||||
}
|
||||
}
|
||||
if (config_bootstrap.created()) {
|
||||
std::cout << " [runtime] created first-run configuration: "
|
||||
<< paths.config_file() << '\n';
|
||||
|
||||
@@ -422,8 +422,12 @@ std::optional<Json> GetRobloxJson(std::string url) {
|
||||
if (proxy.has_value()) {
|
||||
proxy_url = BuildNetworkProxyUrl(*proxy);
|
||||
curl_easy_setopt(curl, CURLOPT_PROXY, proxy_url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROXY, "");
|
||||
}
|
||||
}
|
||||
if (const char* ca_bundle = std::getenv("MOCKTAIL_CA_BUNDLE")) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
|
||||
}
|
||||
const CURLcode status = curl_easy_perform(curl);
|
||||
long http_status = 0;
|
||||
if (status == CURLE_OK) {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "runtime/fleasion.h"
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
namespace {
|
||||
|
||||
using Bio = std::unique_ptr<BIO, decltype(&BIO_free)>;
|
||||
using Certificate = std::unique_ptr<X509, decltype(&X509_free)>;
|
||||
|
||||
bool ReadFile(const std::filesystem::path& path, std::string* contents) {
|
||||
std::error_code error;
|
||||
if (!path.is_absolute() || !std::filesystem::is_regular_file(path, error))
|
||||
return false;
|
||||
const auto size = std::filesystem::file_size(path, error);
|
||||
if (error || size == 0 || size > 4 * 1024 * 1024) return false;
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
contents->resize(static_cast<std::size_t>(size));
|
||||
return static_cast<bool>(input.read(contents->data(), contents->size()));
|
||||
}
|
||||
|
||||
std::filesystem::path DefaultCertificate(const Environment& environment,
|
||||
const RuntimePaths& paths) {
|
||||
std::filesystem::path root = environment.GetOr("XDG_CONFIG_HOME", "");
|
||||
if (!root.is_absolute()) root = paths.home() / ".config";
|
||||
return root / "Fleasion/proxy_ca/ca.crt";
|
||||
}
|
||||
|
||||
bool WriteBundle(const std::filesystem::path& destination,
|
||||
const char* contents, std::size_t size) {
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(destination.parent_path(), error);
|
||||
if (error || std::filesystem::is_symlink(destination.parent_path(), error))
|
||||
return false;
|
||||
std::string temporary = destination.string() + ".XXXXXX";
|
||||
const int descriptor = mkstemp(temporary.data());
|
||||
if (descriptor < 0) return false;
|
||||
std::size_t offset = 0;
|
||||
while (offset < size) {
|
||||
const ssize_t count = write(descriptor, contents + offset, size - offset);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
if (count <= 0) break;
|
||||
offset += static_cast<std::size_t>(count);
|
||||
}
|
||||
bool ok = offset == size && fsync(descriptor) == 0;
|
||||
if (close(descriptor) != 0) ok = false;
|
||||
if (ok) ok = rename(temporary.c_str(), destination.c_str()) == 0;
|
||||
if (!ok) unlink(temporary.c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FleasionPreparation PrepareFleasion(const RuntimeConfig& config,
|
||||
const Environment& environment,
|
||||
const RuntimePaths& paths) {
|
||||
FleasionPreparation result;
|
||||
if (!config.fleasion_enabled()) return result;
|
||||
if (!config.fleasion_valid()) {
|
||||
result.error = "invalid Fleasion configuration";
|
||||
return result;
|
||||
}
|
||||
result.certificate = config.fleasion_ca_certificate().value_or(
|
||||
DefaultCertificate(environment, paths));
|
||||
result.bundle = paths.cache_root() / "fleasion/cacert.pem";
|
||||
result.base_bundle = config.ca_bundle().value_or(std::filesystem::path{});
|
||||
// Re-exec and updater canaries inherit the generated bundle. Rebuild from
|
||||
// the original roots so a rotated Fleasion CA does not accumulate forever.
|
||||
if (result.base_bundle == result.bundle ||
|
||||
result.base_bundle == environment.GetOr("MOCKTAIL_FLEASION_GENERATED_BUNDLE", "")) {
|
||||
result.base_bundle = environment.GetOr("MOCKTAIL_FLEASION_BASE_CA_BUNDLE", "");
|
||||
}
|
||||
if (result.base_bundle.empty()) {
|
||||
for (const char* candidate : {"/etc/ssl/cert.pem", "/etc/ssl/certs/ca-certificates.crt"}) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::is_regular_file(candidate, error)) {
|
||||
result.base_bundle = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::error_code equivalent_error;
|
||||
const bool certificate_is_output = std::filesystem::equivalent(
|
||||
result.certificate, result.bundle, equivalent_error);
|
||||
equivalent_error.clear();
|
||||
const bool base_is_output = std::filesystem::equivalent(
|
||||
result.base_bundle, result.bundle, equivalent_error);
|
||||
if (result.certificate == result.bundle || result.base_bundle == result.bundle ||
|
||||
certificate_is_output || base_is_output) {
|
||||
result.error = "Fleasion source certificates must be outside its generated bundle";
|
||||
return result;
|
||||
}
|
||||
std::string ca_pem;
|
||||
if (!ReadFile(result.certificate, &ca_pem)) {
|
||||
result.error = "cannot read Fleasion CA certificate at " + result.certificate.string() +
|
||||
"; start Fleasion first or set integrations.fleasion.ca_certificate";
|
||||
return result;
|
||||
}
|
||||
Bio ca_input(BIO_new_mem_buf(ca_pem.data(), static_cast<int>(ca_pem.size())), BIO_free);
|
||||
Certificate ca(ca_input ? PEM_read_bio_X509(ca_input.get(), nullptr, nullptr, nullptr)
|
||||
: nullptr, X509_free);
|
||||
if (!ca || X509_check_ca(ca.get()) <= 0 ||
|
||||
X509_cmp_current_time(X509_get0_notBefore(ca.get())) >= 0 ||
|
||||
X509_cmp_current_time(X509_get0_notAfter(ca.get())) <= 0) {
|
||||
ERR_clear_error();
|
||||
result.error = "Fleasion CA must be a currently valid PEM CA certificate";
|
||||
return result;
|
||||
}
|
||||
std::string roots;
|
||||
if (!ReadFile(result.base_bundle, &roots)) {
|
||||
result.error = "cannot read base CA bundle for Fleasion: " + result.base_bundle.string();
|
||||
return result;
|
||||
}
|
||||
Bio input(BIO_new_mem_buf(roots.data(), static_cast<int>(roots.size())), BIO_free);
|
||||
Bio output(BIO_new(BIO_s_mem()), BIO_free);
|
||||
if (!input || !output) {
|
||||
result.error = "cannot allocate Fleasion certificate bundle";
|
||||
return result;
|
||||
}
|
||||
int count = 0;
|
||||
for (;;) {
|
||||
Certificate root(PEM_read_bio_X509(input.get(), nullptr, nullptr, nullptr), X509_free);
|
||||
if (!root) break;
|
||||
++count;
|
||||
if (X509_cmp(root.get(), ca.get()) != 0 &&
|
||||
PEM_write_bio_X509(output.get(), root.get()) != 1) {
|
||||
result.error = "cannot encode base CA certificate";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
ERR_clear_error();
|
||||
if (count == 0 || PEM_write_bio_X509(output.get(), ca.get()) != 1) {
|
||||
result.error = "base bundle must contain PEM certificates";
|
||||
return result;
|
||||
}
|
||||
char* bytes = nullptr;
|
||||
const long size = BIO_get_mem_data(output.get(), &bytes);
|
||||
if (size <= 0 || !WriteBundle(result.bundle, bytes, static_cast<std::size_t>(size)))
|
||||
result.error = "cannot write generated Fleasion CA bundle: " + result.bundle.string();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
@@ -287,6 +287,35 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
|
||||
config.use_system_proxy_ =
|
||||
LegacyEnabled(environment, "MOCKTAIL_USE_SYSTEM_PROXY");
|
||||
config.network_proxy_ = ReadNetworkProxy(environment);
|
||||
config.fleasion_enabled_ = ReadBoolean(
|
||||
environment, "MOCKTAIL_FLEASION_ENABLED", false, &config.fleasion_valid_);
|
||||
config.fleasion_proxy_mode_ = environment.GetOr("MOCKTAIL_FLEASION_PROXY_MODE", "env");
|
||||
const auto fleasion_proxy = ParseNetworkProxyConfig(
|
||||
"127.0.0.1", environment.GetOr("MOCKTAIL_FLEASION_PROXY_PORT", "58443"));
|
||||
if (!fleasion_proxy || (config.fleasion_proxy_mode_ != "env" &&
|
||||
config.fleasion_proxy_mode_ != "hosts")) {
|
||||
config.fleasion_valid_ = false;
|
||||
} else {
|
||||
config.fleasion_proxy_port_ = fleasion_proxy->port;
|
||||
}
|
||||
if (const auto certificate = environment.Get("MOCKTAIL_FLEASION_CA_CERTIFICATE")) {
|
||||
config.fleasion_ca_certificate_ = *certificate;
|
||||
if (certificate->empty() || !config.fleasion_ca_certificate_->is_absolute())
|
||||
config.fleasion_valid_ = false;
|
||||
}
|
||||
if (config.fleasion_enabled_) {
|
||||
if (config.use_system_proxy_) config.fleasion_valid_ = false;
|
||||
if (config.fleasion_proxy_mode_ == "env" && fleasion_proxy) {
|
||||
if (config.network_proxy_ &&
|
||||
(config.network_proxy_->host != fleasion_proxy->host ||
|
||||
config.network_proxy_->port != fleasion_proxy->port ||
|
||||
config.network_proxy_->scheme != "http"))
|
||||
config.fleasion_valid_ = false;
|
||||
config.network_proxy_ = fleasion_proxy;
|
||||
} else if (config.network_proxy_) {
|
||||
config.fleasion_valid_ = false;
|
||||
}
|
||||
}
|
||||
if (const std::optional<std::string> ca_bundle =
|
||||
environment.Get("MOCKTAIL_CA_BUNDLE");
|
||||
ca_bundle.has_value()) {
|
||||
|
||||
@@ -93,6 +93,16 @@ audio:
|
||||
input_device: default
|
||||
|
||||
integrations:
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
enabled: false
|
||||
# String: match Fleasion's routing mode: env or hosts.
|
||||
proxy_mode: env
|
||||
# Integer: Fleasion's local HTTP proxy port in env mode.
|
||||
proxy_port: 58443
|
||||
# Optional absolute public CA path. Default: $XDG_CONFIG_HOME/Fleasion/proxy_ca/ca.crt
|
||||
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
|
||||
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
|
||||
@@ -188,6 +188,10 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
"network.proxy_host",
|
||||
"network.proxy_port",
|
||||
"network.ca_bundle",
|
||||
"integrations.fleasion.enabled",
|
||||
"integrations.fleasion.proxy_mode",
|
||||
"integrations.fleasion.proxy_port",
|
||||
"integrations.fleasion.ca_certificate",
|
||||
"integrations.discord_rpc.enabled",
|
||||
"integrations.discord_rpc.show_place_name",
|
||||
"integrations.discord_rpc.show_elapsed_time",
|
||||
@@ -483,6 +487,7 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
std::pair<std::string_view, std::string_view>(
|
||||
"integrations.discord_rpc.enabled",
|
||||
"MOCKTAIL_DISCORD_RPC_ENABLED"),
|
||||
{"integrations.fleasion.enabled", "MOCKTAIL_FLEASION_ENABLED"},
|
||||
{"integrations.discord_rpc.show_place_name",
|
||||
"MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME"},
|
||||
{"integrations.discord_rpc.show_elapsed_time",
|
||||
@@ -503,6 +508,15 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
}
|
||||
(*environment)[std::string(variable)] = parsed ? "1" : "0";
|
||||
}
|
||||
for (const auto& [key, variable] : {
|
||||
std::pair<std::string_view, std::string_view>(
|
||||
"integrations.fleasion.proxy_mode", "MOCKTAIL_FLEASION_PROXY_MODE"),
|
||||
{"integrations.fleasion.proxy_port", "MOCKTAIL_FLEASION_PROXY_PORT"},
|
||||
{"integrations.fleasion.ca_certificate", "MOCKTAIL_FLEASION_CA_CERTIFICATE"},
|
||||
}) {
|
||||
if (const auto configured = value(key))
|
||||
(*environment)[std::string(variable)] = *configured;
|
||||
}
|
||||
if (const auto application_id =
|
||||
value("integrations.discord_rpc.application_id");
|
||||
application_id.has_value()) {
|
||||
@@ -751,6 +765,10 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
|
||||
result.error = "GameMode policy is invalid";
|
||||
} else if (!result.config.performance().physics_worker_mode_valid) {
|
||||
result.error = "physics worker policy is invalid";
|
||||
} else if (!result.config.fleasion_valid()) {
|
||||
result.error = "Fleasion configuration is invalid: use proxy_mode env or hosts, "
|
||||
"a port from 1 to 65535, an absolute CA certificate path, and "
|
||||
"no conflicting system/fixed proxy";
|
||||
} else if (!result.config.ca_bundle_valid()) {
|
||||
result.error = "CA bundle path is invalid";
|
||||
} else if (!result.config.discord_rpc_valid()) {
|
||||
@@ -761,6 +779,10 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
|
||||
|
||||
bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
std::string* error) {
|
||||
if (!config.fleasion_valid()) {
|
||||
if (error != nullptr) *error = "cannot export invalid Fleasion configuration";
|
||||
return false;
|
||||
}
|
||||
if (!config.device_profile_valid()) {
|
||||
if (error != nullptr) {
|
||||
*error = "cannot export an invalid device profile";
|
||||
@@ -910,6 +932,20 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
if (!base_exported) {
|
||||
return false;
|
||||
}
|
||||
if (!SetEnvironmentValue("MOCKTAIL_FLEASION_ENABLED",
|
||||
config.fleasion_enabled() ? "1" : "0", error) ||
|
||||
!SetEnvironmentValue("MOCKTAIL_FLEASION_PROXY_MODE",
|
||||
config.fleasion_proxy_mode(), error) ||
|
||||
!SetEnvironmentValue("MOCKTAIL_FLEASION_PROXY_PORT",
|
||||
std::to_string(config.fleasion_proxy_port()), error))
|
||||
return false;
|
||||
if (config.fleasion_ca_certificate()) {
|
||||
if (!SetEnvironmentValue("MOCKTAIL_FLEASION_CA_CERTIFICATE",
|
||||
config.fleasion_ca_certificate()->string(), error))
|
||||
return false;
|
||||
} else if (!UnsetEnvironmentValue("MOCKTAIL_FLEASION_CA_CERTIFICATE", error)) {
|
||||
return false;
|
||||
}
|
||||
if (config.ca_bundle().has_value()) {
|
||||
if (!SetEnvironmentValue("MOCKTAIL_CA_BUNDLE",
|
||||
config.ca_bundle()->string(), error)) {
|
||||
|
||||
@@ -118,6 +118,9 @@ HttpResponse PerformRequest(const HttpRequest& request,
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &writer);
|
||||
curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, WriteHeader);
|
||||
curl_easy_setopt(handle, CURLOPT_HEADERDATA, &response.headers);
|
||||
if (const char* ca_bundle = std::getenv("MOCKTAIL_CA_BUNDLE")) {
|
||||
curl_easy_setopt(handle, CURLOPT_CAINFO, ca_bundle);
|
||||
}
|
||||
std::string proxy_url;
|
||||
const char* proxy_host = std::getenv("MOCKTAIL_HTTP_PROXY_HOST");
|
||||
const char* proxy_port = std::getenv("MOCKTAIL_HTTP_PROXY_PORT");
|
||||
@@ -130,6 +133,9 @@ HttpResponse PerformRequest(const HttpRequest& request,
|
||||
if (proxy.has_value()) {
|
||||
proxy_url = runtime::BuildNetworkProxyUrl(*proxy);
|
||||
curl_easy_setopt(handle, CURLOPT_PROXY, proxy_url.c_str());
|
||||
// A configured application proxy must not be bypassed by an inherited
|
||||
// NO_PROXY entry, including '*' from a desktop launcher.
|
||||
curl_easy_setopt(handle, CURLOPT_NOPROXY, "");
|
||||
}
|
||||
}
|
||||
if (headers != nullptr) {
|
||||
|
||||
@@ -16,6 +16,18 @@ target_link_libraries(runtime_config_test PRIVATE
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_executable(fleasion_probe fleasion_probe.cc)
|
||||
target_link_libraries(fleasion_probe PRIVATE Mocktail::Services libc_shim)
|
||||
mocktail_apply_compile_options(fleasion_probe)
|
||||
find_program(MOCKTAIL_TEST_PYTHON python3)
|
||||
find_program(MOCKTAIL_TEST_OPENSSL openssl)
|
||||
if(MOCKTAIL_TEST_PYTHON AND MOCKTAIL_TEST_OPENSSL)
|
||||
add_test(NAME FleasionIntegrationTest.ProxyAndTrust
|
||||
COMMAND ${MOCKTAIL_TEST_PYTHON} ${CMAKE_CURRENT_SOURCE_DIR}/fleasion_integration_test.py
|
||||
$<TARGET_FILE:fleasion_probe> ${MOCKTAIL_TEST_OPENSSL})
|
||||
set_tests_properties(FleasionIntegrationTest.ProxyAndTrust PROPERTIES TIMEOUT 45)
|
||||
endif()
|
||||
|
||||
add_executable(runtime_config_file_test runtime_config_file_test.cc)
|
||||
target_link_libraries(runtime_config_file_test PRIVATE
|
||||
Mocktail::Runtime
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Exercise real CA composition, native path mapping, and HTTPS CONNECT routing."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
|
||||
probe, openssl = sys.argv[1:]
|
||||
|
||||
|
||||
def command(*args):
|
||||
subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="mocktail-fleasion-test-") as directory:
|
||||
root = Path(directory)
|
||||
for name in ("public", "fleasion", "rotated"):
|
||||
command(openssl, "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2",
|
||||
"-subj", f"/CN={name}", "-addext", "basicConstraints=critical,CA:TRUE",
|
||||
"-keyout", str(root / f"{name}.key"), "-out", str(root / f"{name}.crt"))
|
||||
command(openssl, "req", "-newkey", "rsa:2048", "-nodes", "-subj",
|
||||
"/CN=assetdelivery.roblox.com", "-keyout", str(root / "leaf.key"),
|
||||
"-out", str(root / "leaf.csr"))
|
||||
(root / "leaf.ext").write_text("subjectAltName=DNS:assetdelivery.roblox.com\nbasicConstraints=CA:FALSE\n")
|
||||
command(openssl, "x509", "-req", "-in", str(root / "leaf.csr"), "-CA",
|
||||
str(root / "fleasion.crt"), "-CAkey", str(root / "fleasion.key"),
|
||||
"-CAcreateserial", "-days", "1", "-extfile", str(root / "leaf.ext"),
|
||||
"-out", str(root / "leaf.crt"))
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(root / "leaf.crt", root / "leaf.key")
|
||||
intercepted = []
|
||||
failures = []
|
||||
listener = socket.socket()
|
||||
listener.bind(("127.0.0.1", 0))
|
||||
listener.listen()
|
||||
listener.settimeout(5)
|
||||
port = listener.getsockname()[1]
|
||||
|
||||
def serve():
|
||||
try:
|
||||
# Correct CA succeeds; the unrelated CA must fail TLS verification.
|
||||
for attempt in range(2):
|
||||
connection, _ = listener.accept()
|
||||
with connection:
|
||||
connection.settimeout(5)
|
||||
connect = b""
|
||||
while not connect.endswith(b"\r\n\r\n"):
|
||||
data = connection.recv(4096)
|
||||
if not data:
|
||||
raise AssertionError("proxy connection closed before CONNECT")
|
||||
connect += data
|
||||
assert connect.startswith(b"CONNECT assetdelivery.roblox.com:443 "), connect
|
||||
connection.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
try:
|
||||
with context.wrap_socket(connection, server_side=True) as tls:
|
||||
request = tls.recv(8192)
|
||||
assert request.startswith(b"GET /replacement "), request
|
||||
intercepted.append(request)
|
||||
body = b"fleasion-replaced-asset"
|
||||
tls.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: " +
|
||||
str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body)
|
||||
except (ssl.SSLError, ConnectionError):
|
||||
if attempt == 0:
|
||||
raise
|
||||
# The wrong CA may close with either a TLS alert or
|
||||
# a TCP reset before the server finishes its handshake.
|
||||
except BaseException as error:
|
||||
failures.append(error)
|
||||
|
||||
thread = threading.Thread(target=serve, daemon=True)
|
||||
thread.start()
|
||||
env = {key: value for key, value in os.environ.items()
|
||||
if not key.startswith(("MOCKTAIL_", "ROBLOX_"))}
|
||||
for key in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
|
||||
env.pop(key, None)
|
||||
env.update(HOME=str(root), XDG_CONFIG_HOME=str(root / "config"),
|
||||
MOCKTAIL_CACHE_ROOT=str(root / "cache"), NO_PROXY="*", no_proxy="*")
|
||||
certificate = root / "config/Fleasion/proxy_ca/ca.crt"
|
||||
certificate.parent.mkdir(parents=True)
|
||||
original = (root / "fleasion.crt").read_bytes()
|
||||
certificate.write_bytes(original)
|
||||
config = root / "config.yaml"
|
||||
|
||||
def configure(extra="", enabled="true", mode="env"):
|
||||
config.write_text(f"version: 1\nnetwork:\n ca_bundle: {root / 'public.crt'}\n"
|
||||
f"integrations:\n fleasion:\n enabled: {enabled}\n"
|
||||
f" proxy_mode: {mode}\n proxy_port: {port}\n{extra}")
|
||||
|
||||
def run(url="prepare", success=True, environment=None):
|
||||
result = subprocess.run([probe, str(config), url], env=environment or env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
assert (result.returncode == 0) == success, result.stdout + result.stderr
|
||||
return result
|
||||
|
||||
configure()
|
||||
response = run("https://assetdelivery.roblox.com/replacement")
|
||||
assert "fleasion-replaced-asset" in response.stdout
|
||||
assert certificate.read_bytes() == original
|
||||
generated = root / "cache/fleasion/cacert.pem"
|
||||
assert generated.stat().st_mode & 0o777 == 0o600
|
||||
command(openssl, "verify", "-CAfile", str(generated), str(root / "public.crt"))
|
||||
first_bundle = generated.read_bytes()
|
||||
run() # Repeated startup must not duplicate trust roots.
|
||||
assert generated.read_bytes() == first_bundle
|
||||
certificate.write_bytes((root / "rotated.crt").read_bytes())
|
||||
response = run("https://assetdelivery.roblox.com/replacement", success=False)
|
||||
assert response.returncode == 10, response.stderr
|
||||
thread.join(timeout=7)
|
||||
listener.close()
|
||||
assert not thread.is_alive() and not failures, failures
|
||||
assert len(intercepted) == 1
|
||||
assert original not in generated.read_bytes()
|
||||
command(openssl, "verify", "-CAfile", str(generated), str(root / "rotated.crt"))
|
||||
# Re-exec must compose from the original public roots, not old proxy CAs.
|
||||
inherited = dict(env, MOCKTAIL_CA_BUNDLE=str(generated),
|
||||
MOCKTAIL_FLEASION_GENERATED_BUNDLE=str(generated),
|
||||
MOCKTAIL_FLEASION_BASE_CA_BUNDLE=str(root / "public.crt"))
|
||||
certificate.write_bytes(original)
|
||||
run(environment=inherited)
|
||||
assert generated.read_bytes() == first_bundle
|
||||
for bad in (b"not a certificate", (root / "leaf.crt").read_bytes()):
|
||||
certificate.write_bytes(bad)
|
||||
run(success=False)
|
||||
assert generated.read_bytes() == first_bundle
|
||||
certificate.unlink()
|
||||
run(success=False)
|
||||
configure(enabled="false")
|
||||
run() # Disabled integration does not require Fleasion.
|
||||
certificate.write_bytes(original)
|
||||
configure(mode="hosts")
|
||||
run()
|
||||
configure(mode="invalid")
|
||||
run(success=False)
|
||||
configure(extra=" ca_certificate: relative.crt\n")
|
||||
run(success=False)
|
||||
print("Fleasion integration: proxy replacement, TLS rejection, CA rotation, "
|
||||
"native aliases, and configuration checks passed")
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "runtime/fleasion.h"
|
||||
#include "runtime/runtime_config_file.h"
|
||||
#include "services/http_client.h"
|
||||
#include "libc_shim/libc_shim.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3) return 2;
|
||||
const mocktail::runtime::ProcessEnvironment environment;
|
||||
const auto paths = mocktail::runtime::RuntimePaths::FromEnvironment(environment);
|
||||
const auto loaded = mocktail::runtime::LoadRuntimeConfig(environment, argv[1]);
|
||||
if (!loaded) {
|
||||
std::cerr << loaded.error << '\n';
|
||||
return 3;
|
||||
}
|
||||
const auto prepared = mocktail::runtime::PrepareFleasion(loaded.config, environment, paths);
|
||||
if (!prepared) {
|
||||
std::cerr << prepared.error << '\n';
|
||||
return 4;
|
||||
}
|
||||
std::string error;
|
||||
if (!mocktail::runtime::ExportRuntimeConfigEnvironment(loaded.config, &error)) return 5;
|
||||
if (!prepared.bundle.empty()) {
|
||||
if (setenv("MOCKTAIL_CA_BUNDLE", prepared.bundle.c_str(), 1) != 0) return 6;
|
||||
const auto mapped = libc_shim::ConfigureHostCaBundlePathMappings();
|
||||
if (!mapped.ok()) return 7;
|
||||
FILE* certificate = mocktail_fopen("ssl/cacert.pem", "r");
|
||||
if (certificate == nullptr) return 8;
|
||||
const int first = fgetc(certificate);
|
||||
fclose(certificate);
|
||||
if (first != '-') return 9;
|
||||
}
|
||||
if (std::string(argv[2]) == "prepare") {
|
||||
std::cout << prepared.bundle.string() << '\n';
|
||||
return 0;
|
||||
}
|
||||
mocktail::services::CurlHttpClient client;
|
||||
mocktail::services::HttpRequest request;
|
||||
request.url = argv[2];
|
||||
request.timeout_ms = 3000;
|
||||
const auto response = client.Get(request);
|
||||
if (!response.transport_ok) {
|
||||
std::cerr << response.error << '\n';
|
||||
return 10;
|
||||
}
|
||||
std::cout << response.status_code << '\n' << response.body;
|
||||
return response.status_code == 200 ? 0 : 11;
|
||||
}
|
||||
@@ -141,6 +141,7 @@ TEST_F(LibcShimTest, ValidCaBundleOverrideMapsAndroidAliases) {
|
||||
static_cast<ssize_t>(sizeof(kTestCertificate) - 1));
|
||||
ASSERT_EQ(close(fd), 0);
|
||||
ASSERT_EQ(setenv("MOCKTAIL_CA_BUNDLE", temp_template, 1), 0);
|
||||
ASSERT_EQ(setenv("MOCKTAIL_ASSET_PATH", "/payload/assets/content/", 1), 0);
|
||||
|
||||
RegisterPathMapping("/data/user/0/com.roblox.client", "/host/sober");
|
||||
const HostCaBundleResolution resolution =
|
||||
@@ -157,6 +158,14 @@ TEST_F(LibcShimTest, ValidCaBundleOverrideMapsAndroidAliases) {
|
||||
temp_template);
|
||||
EXPECT_EQ(TranslatePath("/data/user/0/com.roblox.client/files/other"),
|
||||
"/host/sober/files/other");
|
||||
for (const char* alias : {
|
||||
"ssl/cacert.pem", "content/ssl/cacert.pem",
|
||||
"rbx_bin/assets/ssl/cacert.pem", "rbx_bin/assets/content/ssl/cacert.pem",
|
||||
"/payload/assets/ssl/cacert.pem", "/payload/assets/content/ssl/cacert.pem",
|
||||
"/data/user/0/com.roblox.client/files/exe/ssl/cacert.pem",
|
||||
"/data/data/com.roblox.client/files/exe/ssl/cacert.pem"}) {
|
||||
EXPECT_EQ(TranslatePath(alias), temp_template);
|
||||
}
|
||||
FILE* mapped_file = mocktail_fopen(
|
||||
"/data/user/0/com.roblox.client/files/exe/cacert.pem", "r");
|
||||
ASSERT_NE(mapped_file, nullptr);
|
||||
|
||||
@@ -1040,6 +1040,54 @@ TEST(RuntimeConfigFileTest, RejectsMalformedYaml) {
|
||||
EXPECT_NE(loaded.error.find("invalid YAML"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigFileTest, FleasionRoutesThroughConfiguredLoopbackPort) {
|
||||
TemporaryDirectory temporary;
|
||||
const auto file = temporary.Write(
|
||||
"integrations:\n fleasion:\n enabled: true\n proxy_mode: env\n"
|
||||
" proxy_port: 59443\n ca_certificate: /tmp/fleasion-ca.crt\n");
|
||||
auto loaded = LoadRuntimeConfig(MapEnvironment(), file);
|
||||
ASSERT_TRUE(loaded) << loaded.error;
|
||||
EXPECT_TRUE(loaded.config.fleasion_enabled());
|
||||
ASSERT_TRUE(loaded.config.network_proxy());
|
||||
EXPECT_EQ(loaded.config.network_proxy()->host, "127.0.0.1");
|
||||
EXPECT_EQ(loaded.config.network_proxy()->port, 59443);
|
||||
EXPECT_EQ(*loaded.config.fleasion_ca_certificate(), "/tmp/fleasion-ca.crt");
|
||||
loaded = LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_FLEASION_ENABLED", "0"}}), file);
|
||||
ASSERT_TRUE(loaded) << loaded.error;
|
||||
EXPECT_FALSE(loaded.config.fleasion_enabled());
|
||||
EXPECT_FALSE(loaded.config.network_proxy());
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigFileTest, FleasionRejectsInvalidConfigurationAndProxyConflicts) {
|
||||
TemporaryDirectory temporary;
|
||||
for (const char* setting : {"proxy_port: 0", "proxy_port: 65536", "proxy_port: 123x",
|
||||
"proxy_mode: invalid", "ca_certificate: relative.pem"}) {
|
||||
const auto file = temporary.Write(std::string(
|
||||
"integrations:\n fleasion:\n enabled: true\n ") + setting + "\n");
|
||||
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment(), file)) << setting;
|
||||
}
|
||||
const auto file = temporary.Write("integrations:\n fleasion:\n enabled: true\n");
|
||||
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_USE_SYSTEM_PROXY", "1"}}), file));
|
||||
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_HTTP_PROXY_HOST", "other"},
|
||||
{"MOCKTAIL_HTTP_PROXY_PORT", "3128"}}), file));
|
||||
}
|
||||
|
||||
TEST(RuntimeConfigFileTest, FleasionEnvironmentRoundTripsAndHostsModeDoesNotSetProxy) {
|
||||
TemporaryDirectory temporary;
|
||||
const auto file = temporary.Write("integrations:\n fleasion:\n enabled: true\n"
|
||||
" proxy_mode: hosts\n");
|
||||
const auto loaded = LoadRuntimeConfig(MapEnvironment(), file);
|
||||
ASSERT_TRUE(loaded) << loaded.error;
|
||||
EXPECT_FALSE(loaded.config.network_proxy());
|
||||
std::string error;
|
||||
ASSERT_TRUE(ExportRuntimeConfigEnvironment(loaded.config, &error)) << error;
|
||||
const auto resolved = RuntimeConfig::FromEnvironment(ProcessEnvironment());
|
||||
EXPECT_TRUE(resolved.fleasion_enabled());
|
||||
EXPECT_TRUE(resolved.fleasion_valid());
|
||||
EXPECT_EQ(resolved.fleasion_proxy_mode(), "hosts");
|
||||
ASSERT_TRUE(ExportRuntimeConfigEnvironment(RuntimeConfig::FromEnvironment(MapEnvironment()), &error));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -71,6 +71,8 @@ TEST(RuntimeConfigTest, UsesSupportedDefaults) {
|
||||
EXPECT_TRUE(config.audio_input_device_valid());
|
||||
EXPECT_TRUE(config.microphone_enabled());
|
||||
EXPECT_FALSE(config.use_system_proxy());
|
||||
EXPECT_FALSE(config.fleasion_enabled());
|
||||
EXPECT_TRUE(config.fleasion_valid());
|
||||
EXPECT_FALSE(config.network_proxy().has_value());
|
||||
EXPECT_FALSE(config.discord_rpc().enabled);
|
||||
EXPECT_TRUE(config.discord_rpc().show_place_name);
|
||||
|
||||
Reference in new issue
Block a user