mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-23 06:00:55 +02:00
1528 lines
62 KiB
C++
1528 lines
62 KiB
C++
// Copyright 2026 Mocktail Project Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "runtime/auth_runtime_composition.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <gtest/gtest.h>
|
|
#include <sys/file.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <atomic>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
#include "jnivm/jnivm.h"
|
|
#include "legacy/legacy_runtime.h"
|
|
#include "mocktail/sha256.h"
|
|
#include "runtime/environment.h"
|
|
#include "runtime/runtime_paths.h"
|
|
#include "services/auth_service.h"
|
|
#include "services/http_client.h"
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
constexpr char kCredential[] = "_|test-auth-runtime-credential";
|
|
|
|
static_assert(!std::is_copy_constructible_v<SecureRobloxCredential>);
|
|
static_assert(!std::is_move_constructible_v<ScopedRobloxCredentialBinding>,
|
|
"credential provider context must keep a stable address");
|
|
static_assert(!std::is_move_assignable_v<ScopedRobloxCredentialBinding>,
|
|
"credential provider binding must not move implicitly");
|
|
static_assert(!std::is_copy_assignable_v<SecureRobloxCredential>);
|
|
static_assert(std::is_nothrow_move_constructible_v<SecureRobloxCredential>);
|
|
|
|
class MapEnvironment final : public Environment {
|
|
public:
|
|
void Set(std::string name, std::string value) {
|
|
values_[std::move(name)] = std::move(value);
|
|
}
|
|
|
|
std::optional<std::string> Get(std::string_view name) const override {
|
|
const auto found = values_.find(std::string(name));
|
|
if (found == values_.end()) {
|
|
return std::nullopt;
|
|
}
|
|
return found->second;
|
|
}
|
|
|
|
private:
|
|
std::map<std::string, std::string> values_;
|
|
};
|
|
|
|
class FakeHttpClient final : public services::HttpClient {
|
|
public:
|
|
services::HttpResponse Get(const services::HttpRequest& request) override {
|
|
++request_count;
|
|
last_request = request;
|
|
if (before_response) {
|
|
before_response();
|
|
}
|
|
return response;
|
|
}
|
|
|
|
services::HttpResponse response;
|
|
services::HttpRequest last_request;
|
|
std::function<void()> before_response;
|
|
int request_count = 0;
|
|
};
|
|
|
|
class TempDirectory final {
|
|
public:
|
|
TempDirectory() {
|
|
static std::atomic<unsigned int> serial{0};
|
|
path_ = std::filesystem::temp_directory_path() /
|
|
("mocktail-auth-composition-" + std::to_string(getpid()) + "-" +
|
|
std::to_string(serial.fetch_add(1)));
|
|
std::error_code error;
|
|
std::filesystem::create_directories(path_, error);
|
|
}
|
|
|
|
~TempDirectory() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(path_, error);
|
|
}
|
|
|
|
const std::filesystem::path& path() const { return path_; }
|
|
|
|
private:
|
|
std::filesystem::path path_;
|
|
};
|
|
|
|
bool WriteFile(const std::filesystem::path& path, const std::string& contents) {
|
|
std::error_code error;
|
|
std::filesystem::create_directories(path.parent_path(), error);
|
|
if (error) {
|
|
return false;
|
|
}
|
|
if (chmod(path.parent_path().c_str(), S_IRWXU) != 0) {
|
|
return false;
|
|
}
|
|
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
|
|
stream << contents;
|
|
stream.close();
|
|
return stream.good() && chmod(path.c_str(), S_IRUSR | S_IWUSR) == 0;
|
|
}
|
|
|
|
std::string ReadFile(const std::filesystem::path& path) {
|
|
std::ifstream input(path, std::ios::binary);
|
|
return {std::istreambuf_iterator<char>(input),
|
|
std::istreambuf_iterator<char>()};
|
|
}
|
|
|
|
RuntimePaths PathsFor(const MapEnvironment& environment,
|
|
const TempDirectory& directory) {
|
|
return RuntimePaths::FromEnvironment(environment, directory.path());
|
|
}
|
|
|
|
std::filesystem::path SoberRejectionPathFor(const RuntimePaths& paths) {
|
|
return paths.default_sober_rejection_file();
|
|
}
|
|
|
|
std::string SoberRejectionMarkerFor(std::string_view credential) {
|
|
constexpr char kDomain[] = "mocktail-default-sober-rejection-v1";
|
|
std::string fingerprint_input(kDomain, sizeof(kDomain));
|
|
fingerprint_input.append(credential);
|
|
return "v1:" + foundation::ComputeSha256Hex(fingerprint_input) + "\n";
|
|
}
|
|
|
|
std::string ReadJavaString(JNIEnv* env, jstring value) {
|
|
if (env == nullptr || value == nullptr) {
|
|
return {};
|
|
}
|
|
const char* chars = env->GetStringUTFChars(value, nullptr);
|
|
if (chars == nullptr) {
|
|
return {};
|
|
}
|
|
const std::string result(chars);
|
|
env->ReleaseStringUTFChars(value, chars);
|
|
return result;
|
|
}
|
|
|
|
struct NativeCookieCallbackProbe {
|
|
int calls = 0;
|
|
std::string cookie;
|
|
std::string url;
|
|
};
|
|
|
|
void IgnoreDataModelNotification(void*, JNIEnv*, jstring, jstring) {}
|
|
|
|
void CaptureNativeCookie(void* context, JNIEnv* env, jstring cookie,
|
|
jstring url) {
|
|
auto* probe = static_cast<NativeCookieCallbackProbe*>(context);
|
|
++probe->calls;
|
|
probe->cookie = ReadJavaString(env, cookie);
|
|
probe->url = ReadJavaString(env, url);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
ResolvesSavedCookieAndInjectsOnlyTypedIdentity) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ROBLOX_COOKIES",
|
|
std::string(".ROBLOSECURITY=") + kCredential);
|
|
FakeHttpClient http;
|
|
http.response = {
|
|
true,
|
|
200,
|
|
R"({"id":321,"name":"RuntimeUser","displayName":"Runtime Display"})",
|
|
{}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
|
|
ASSERT_TRUE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kAuthenticated);
|
|
EXPECT_EQ(composition.http_status, 200);
|
|
EXPECT_TRUE(composition.error.empty());
|
|
ASSERT_EQ(http.request_count, 1);
|
|
EXPECT_FALSE(http.last_request.follow_redirects);
|
|
ASSERT_EQ(http.last_request.headers.size(), 2U);
|
|
EXPECT_EQ(http.last_request.headers[1],
|
|
std::string("Cookie: .ROBLOSECURITY=") + kCredential);
|
|
EXPECT_EQ(composition.credential.view(),
|
|
std::string(".ROBLOSECURITY=") + kCredential);
|
|
|
|
const jnivm::RobloxAuthIdentity identity =
|
|
composition.jni_vm->GetRobloxAuthIdentitySnapshot();
|
|
EXPECT_EQ(identity.user_id, 321);
|
|
EXPECT_EQ(identity.username, "RuntimeUser");
|
|
EXPECT_EQ(identity.display_name, "Runtime Display");
|
|
EXPECT_EQ(composition.account_identity.user_id, identity.user_id);
|
|
EXPECT_EQ(composition.account_identity.username, identity.username);
|
|
EXPECT_EQ(identity.username.find(kCredential), std::string::npos);
|
|
EXPECT_EQ(identity.display_name.find(kCredential), std::string::npos);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, MissingCookieRequiresExplicitGuestPolicy) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
FakeHttpClient http;
|
|
services::AuthService auth_service(http);
|
|
|
|
AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.error, "missing .ROBLOSECURITY cookie");
|
|
EXPECT_EQ(http.request_count, 0);
|
|
EXPECT_TRUE(composition.credential.empty());
|
|
EXPECT_EQ(composition.account_identity.user_id, -1);
|
|
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
ASSERT_TRUE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(http.request_count, 0);
|
|
const jnivm::RobloxAuthIdentity identity =
|
|
composition.jni_vm->GetRobloxAuthIdentitySnapshot();
|
|
EXPECT_EQ(identity.user_id, -1);
|
|
EXPECT_TRUE(identity.username.empty());
|
|
EXPECT_TRUE(identity.display_name.empty());
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
GuestJavaCookiePersistsPrivatelyAndBecomesCurrentCredential) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
FakeHttpClient http;
|
|
services::AuthService auth_service(http);
|
|
AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
ASSERT_TRUE(composition);
|
|
ASSERT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
|
|
const std::string saved = ".ROBLOSECURITY=_|embedded-login-cookie";
|
|
{
|
|
const ScopedRobloxCredentialBinding binding(composition.jni_vm.get(),
|
|
composition.credential);
|
|
ASSERT_TRUE(binding.bound());
|
|
JNIEnv* env = composition.jni_vm->GetJNIEnv();
|
|
jclass clazz =
|
|
env->FindClass("com/roblox/universalapp/cookie/JNICookieManager");
|
|
ASSERT_NE(clazz, nullptr);
|
|
jobject manager = env->AllocObject(clazz);
|
|
jmethodID setter = env->GetMethodID(
|
|
clazz, "setCookie", "(Ljava/lang/String;Ljava/lang/String;)V");
|
|
ASSERT_NE(setter, nullptr);
|
|
env->CallVoidMethod(manager, setter,
|
|
env->NewStringUTF("https://www.roblox.com/"),
|
|
env->NewStringUTF(saved.c_str()));
|
|
|
|
std::string provider_value = "must be cleared";
|
|
EXPECT_TRUE(composition.jni_vm->CopyRobloxCredentialFromProvider(
|
|
&provider_value));
|
|
EXPECT_EQ(provider_value, saved);
|
|
jmethodID getter = env->GetStaticMethodID(
|
|
clazz, "getCookie", "(Ljava/lang/String;)Ljava/lang/String;");
|
|
auto current = static_cast<jstring>(env->CallStaticObjectMethod(
|
|
clazz, getter, env->NewStringUTF("roblox.com")));
|
|
EXPECT_EQ(ReadJavaString(env, current), saved);
|
|
EXPECT_EQ(
|
|
composition.jni_vm->GetRobloxAuthIdentitySnapshot().user_id, -1);
|
|
}
|
|
|
|
std::ifstream input(paths.cookie_file(), std::ios::binary);
|
|
ASSERT_TRUE(input.good());
|
|
const std::string persisted((std::istreambuf_iterator<char>(input)),
|
|
std::istreambuf_iterator<char>());
|
|
EXPECT_EQ(persisted, saved + "\n");
|
|
|
|
struct stat auth_status = {};
|
|
struct stat cookie_status = {};
|
|
ASSERT_EQ(lstat(paths.auth_root().c_str(), &auth_status), 0);
|
|
ASSERT_EQ(lstat(paths.cookie_file().c_str(), &cookie_status), 0);
|
|
EXPECT_TRUE(S_ISDIR(auth_status.st_mode));
|
|
EXPECT_EQ(auth_status.st_mode & (S_IRWXG | S_IRWXO), 0U);
|
|
EXPECT_TRUE(S_ISREG(cookie_status.st_mode));
|
|
EXPECT_EQ(cookie_status.st_mode & (S_IRWXG | S_IRWXO), 0U);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
NativeSignInPromotesValidatedIdentityWithoutRestart) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
auto http = std::make_shared<FakeHttpClient>();
|
|
http->response = {
|
|
true,
|
|
200,
|
|
R"({"id":987,"name":"LiveUser","displayName":"Live User"})",
|
|
{}};
|
|
services::AuthService auth_service(*http);
|
|
AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service, http);
|
|
ASSERT_TRUE(composition);
|
|
ASSERT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
ASSERT_EQ(http->request_count, 0);
|
|
|
|
const std::string saved = ".ROBLOSECURITY=_|live-native-login-cookie";
|
|
{
|
|
const ScopedRobloxCredentialBinding binding(composition.jni_vm.get(),
|
|
composition.credential);
|
|
ASSERT_TRUE(binding.bound());
|
|
ASSERT_TRUE(composition.jni_vm->DispatchRobloxCredential(saved.data(),
|
|
saved.size()));
|
|
std::string current;
|
|
ASSERT_TRUE(
|
|
composition.jni_vm->CopyRobloxCredentialFromProvider(¤t));
|
|
EXPECT_EQ(current, saved);
|
|
}
|
|
|
|
ASSERT_EQ(http->request_count, 1);
|
|
ASSERT_GE(http->last_request.headers.size(), 2U);
|
|
EXPECT_EQ(http->last_request.headers[1], "Cookie: " + saved);
|
|
const jnivm::RobloxAuthIdentity identity =
|
|
composition.jni_vm->GetRobloxAuthIdentitySnapshot();
|
|
EXPECT_EQ(identity.user_id, 987);
|
|
EXPECT_EQ(identity.username, "LiveUser");
|
|
EXPECT_EQ(identity.display_name, "Live User");
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), saved + "\n");
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
NativeCookieCallbackAuthenticatesFromManagedFileAfterRestart) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
FakeHttpClient initial_http;
|
|
services::AuthService initial_auth_service(initial_http);
|
|
AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, initial_auth_service);
|
|
ASSERT_TRUE(composition);
|
|
ASSERT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
|
|
const std::string saved = ".ROBLOSECURITY=_|native-login-cookie";
|
|
auto callback_probe = std::make_shared<NativeCookieCallbackProbe>();
|
|
jnivm::RobloxDataModelNotificationCallbacks callbacks;
|
|
callbacks.on_notification = &IgnoreDataModelNotification;
|
|
callbacks.on_set_cookie = &CaptureNativeCookie;
|
|
composition.jni_vm->SetRobloxDataModelNotificationCallbacks(callback_probe,
|
|
callbacks);
|
|
{
|
|
const ScopedRobloxCredentialBinding binding(composition.jni_vm.get(),
|
|
composition.credential);
|
|
ASSERT_TRUE(binding.bound());
|
|
JNIEnv* env = composition.jni_vm->GetJNIEnv();
|
|
jclass handler_class = env->FindClass(
|
|
"com/roblox/universalapp/cookie/"
|
|
"JNICookieProtocol$OnSetCookieHandler");
|
|
jobject handler = env->AllocObject(handler_class);
|
|
jmethodID on_set_cookie = env->GetMethodID(
|
|
handler_class, "onSetCookie",
|
|
"([Ljava/lang/String;Ljava/lang/String;)V");
|
|
jclass string_class = env->FindClass("java/lang/String");
|
|
jobjectArray cookies = env->NewObjectArray(1, string_class, nullptr);
|
|
env->SetObjectArrayElement(
|
|
cookies, 0,
|
|
env->NewStringUTF(
|
|
".ROBLOSECURITY=_|native-login-cookie; Domain=.roblox.com; "
|
|
"Path=/; Secure; HttpOnly"));
|
|
env->CallVoidMethod(handler, on_set_cookie, cookies,
|
|
env->NewStringUTF("https://www.roblox.com/"));
|
|
}
|
|
composition.jni_vm->ClearRobloxDataModelNotificationCallbacks();
|
|
|
|
EXPECT_EQ(callback_probe->calls, 1);
|
|
EXPECT_EQ(callback_probe->cookie, saved);
|
|
EXPECT_EQ(callback_probe->url, "https://www.roblox.com/");
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), saved + "\n");
|
|
|
|
MapEnvironment restart_environment;
|
|
restart_environment.Set("HOME", directory.path().string());
|
|
restart_environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
FakeHttpClient restart_http;
|
|
restart_http.response = {
|
|
true,
|
|
200,
|
|
R"({"id":777,"name":"RestartUser","displayName":"Restart User"})",
|
|
{}};
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted = ComposeAuthRuntime(
|
|
restart_environment, PathsFor(restart_environment, directory),
|
|
restart_auth_service);
|
|
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kAuthenticated);
|
|
ASSERT_EQ(restart_http.request_count, 1);
|
|
EXPECT_EQ(restart_http.last_request.headers[1], "Cookie: " + saved);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
RejectedEnvironmentCredentialNeverCreatesProductionVm) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ROBLOX_COOKIES", kCredential);
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string managed_cookie =
|
|
"RBXEventTrackerV2=browserid=123456&browserid_insert_timestamp=1\n";
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(), managed_cookie));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(composition.error, "authentication rejected");
|
|
EXPECT_EQ(composition.error.find(kCredential), std::string::npos);
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), managed_cookie);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401ClearsManagedCredentialAndStartsNativeSignInGuest) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string remaining_cookies =
|
|
"RBXEventTrackerV2=browserid=123456&browserid_insert_timestamp=1\n"
|
|
"OtherCookie=keep\n";
|
|
const std::string rejected_cookie =
|
|
std::string(".ROBLOSECURITY=") + kCredential;
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(),
|
|
rejected_cookie + "\n" + remaining_cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_TRUE(composition.error.empty());
|
|
EXPECT_TRUE(composition.credential.empty());
|
|
EXPECT_EQ(composition.account_identity.user_id, -1);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(
|
|
ReadFile(paths.cookie_file()),
|
|
std::string(rejected_cookie.size(), ' ') + "\n" + remaining_cookies);
|
|
struct stat metadata = {};
|
|
ASSERT_EQ(lstat(paths.cookie_file().c_str(), &metadata), 0);
|
|
EXPECT_TRUE(S_ISREG(metadata.st_mode));
|
|
EXPECT_EQ(metadata.st_mode & (S_IRWXG | S_IRWXO), 0U);
|
|
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401ClearsManagedAndSuppressesMatchingDefaultSoberCredential) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string rejected_cookie =
|
|
std::string(".ROBLOSECURITY=") + kCredential;
|
|
const std::string managed_other = "ManagedCookie=keep\n";
|
|
const std::string sober_other = "SoberCookie=keep\n";
|
|
ASSERT_TRUE(
|
|
WriteFile(paths.cookie_file(), rejected_cookie + "\n" + managed_other));
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(),
|
|
rejected_cookie + "\n" + sober_other));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
const std::string redacted_prefix(rejected_cookie.size(), ' ');
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()),
|
|
redacted_prefix + "\n" + managed_other);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()),
|
|
rejected_cookie + "\n" + sober_other);
|
|
const std::filesystem::path rejection_path = SoberRejectionPathFor(paths);
|
|
const std::string marker = ReadFile(rejection_path);
|
|
EXPECT_EQ(marker.size(), 68U);
|
|
EXPECT_EQ(marker.rfind("v1:", 0), 0U);
|
|
EXPECT_EQ(marker.find(kCredential), std::string::npos);
|
|
struct stat marker_metadata = {};
|
|
ASSERT_EQ(lstat(rejection_path.c_str(), &marker_metadata), 0);
|
|
EXPECT_TRUE(S_ISREG(marker_metadata.st_mode));
|
|
EXPECT_EQ(marker_metadata.st_mode & (S_IRWXG | S_IRWXO), 0U);
|
|
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401SuppressesDefaultSoberCredentialWithoutMutatingItsFile) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string remaining_cookies =
|
|
"RBXEventTrackerV2=browserid=654321&browserid_insert_timestamp=1\n"
|
|
"OtherCookie=keep\n";
|
|
const std::string rejected_cookie =
|
|
std::string(".ROBLOSECURITY=") + kCredential;
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(),
|
|
rejected_cookie + "\n" + remaining_cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()),
|
|
rejected_cookie + "\n" + remaining_cookies);
|
|
EXPECT_FALSE(std::filesystem::exists(paths.cookie_file()));
|
|
const std::filesystem::path rejection_path = SoberRejectionPathFor(paths);
|
|
EXPECT_EQ(ReadFile(rejection_path).find(kCredential), std::string::npos);
|
|
|
|
const std::string changed_tracker_cookies =
|
|
rejected_cookie +
|
|
"\nRBXEventTrackerV2=browserid=999999&browserid_insert_timestamp=2\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), changed_tracker_cookies));
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), changed_tracker_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
RelativeNestedAuthRootPersistsDefaultSoberRejection) {
|
|
TempDirectory directory;
|
|
std::error_code error;
|
|
const std::filesystem::path current_directory =
|
|
std::filesystem::current_path(error);
|
|
ASSERT_FALSE(error);
|
|
const std::filesystem::path relative_auth_root = std::filesystem::relative(
|
|
directory.path() / "relative/auth/root", current_directory, error);
|
|
ASSERT_FALSE(error);
|
|
ASSERT_FALSE(relative_auth_root.empty());
|
|
ASSERT_FALSE(relative_auth_root.is_absolute());
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_AUTH_ROOT", relative_auth_root.string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(ReadFile(paths.default_sober_rejection_file()),
|
|
SoberRejectionMarkerFor(kCredential));
|
|
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
ChangedDefaultSoberCredentialIsCheckedAndReplacesOldMarker) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string first_cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\nOther=keep\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), first_cookies));
|
|
FakeHttpClient first_http;
|
|
first_http.response = {true, 401, {}, {}};
|
|
services::AuthService first_auth_service(first_http);
|
|
ASSERT_TRUE(ComposeAuthRuntime(environment, paths, first_auth_service));
|
|
|
|
const std::filesystem::path rejection_path = SoberRejectionPathFor(paths);
|
|
const std::string first_marker = ReadFile(rejection_path);
|
|
struct stat first_metadata = {};
|
|
ASSERT_EQ(lstat(rejection_path.c_str(), &first_metadata), 0);
|
|
|
|
const std::string changed_value = "_|changed-default-sober-credential";
|
|
const std::string changed_cookies =
|
|
".ROBLOSECURITY=" + changed_value + "\nOther=still-keep\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), changed_cookies));
|
|
FakeHttpClient changed_http;
|
|
changed_http.response = {true, 401, {}, {}};
|
|
services::AuthService changed_auth_service(changed_http);
|
|
|
|
const AuthRuntimeComposition changed =
|
|
ComposeAuthRuntime(environment, paths, changed_auth_service);
|
|
|
|
ASSERT_TRUE(changed) << changed.error;
|
|
EXPECT_EQ(changed.status, AuthRuntimeStatus::kGuest);
|
|
ASSERT_EQ(changed_http.request_count, 1);
|
|
ASSERT_EQ(changed_http.last_request.headers.size(), 2U);
|
|
EXPECT_EQ(changed_http.last_request.headers[1],
|
|
"Cookie: .ROBLOSECURITY=" + changed_value);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), changed_cookies);
|
|
const std::string changed_marker = ReadFile(rejection_path);
|
|
EXPECT_NE(changed_marker, first_marker);
|
|
EXPECT_EQ(changed_marker.find(changed_value), std::string::npos);
|
|
struct stat changed_metadata = {};
|
|
ASSERT_EQ(lstat(rejection_path.c_str(), &changed_metadata), 0);
|
|
EXPECT_NE(changed_metadata.st_ino, first_metadata.st_ino);
|
|
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
DefaultSoberRefreshDuringHttp401RecoveryIsNeverModified) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(),
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n"));
|
|
const std::string refreshed_value = "_|refreshed-during-http";
|
|
const std::string refreshed_cookies =
|
|
".ROBLOSECURITY=" + refreshed_value + "\nOther=keep\n";
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
http.before_response = [&]() {
|
|
EXPECT_TRUE(WriteFile(paths.sober_cookie_file(), refreshed_cookies));
|
|
};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), refreshed_cookies);
|
|
|
|
FakeHttpClient restart_http;
|
|
restart_http.response = {
|
|
true,
|
|
200,
|
|
R"({"id":654,"name":"Refreshed","displayName":"Refreshed User"})",
|
|
{}};
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kAuthenticated);
|
|
EXPECT_EQ(restart_http.request_count, 1);
|
|
ASSERT_EQ(restart_http.last_request.headers.size(), 2U);
|
|
EXPECT_EQ(restart_http.last_request.headers[1],
|
|
"Cookie: .ROBLOSECURITY=" + refreshed_value);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), refreshed_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
MalformedDefaultSoberRejectionMarkerFailsClosedBeforeHttp) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string sober_cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), sober_cookies));
|
|
ASSERT_TRUE(WriteFile(SoberRejectionPathFor(paths), "not-a-valid-marker\n"));
|
|
ASSERT_EQ(chmod(paths.auth_root().c_str(), S_IRWXU), 0);
|
|
FakeHttpClient http;
|
|
http.response = {true, 200, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error, "saved Sober rejection marker is invalid");
|
|
EXPECT_EQ(http.request_count, 0);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), sober_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
UnsafeAuthRootWithoutMarkerFailsClosedBeforeHttp) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string sober_cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), sober_cookies));
|
|
std::error_code error;
|
|
std::filesystem::create_directories(paths.auth_root(), error);
|
|
ASSERT_FALSE(error);
|
|
ASSERT_EQ(chmod(paths.auth_root().c_str(),
|
|
S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH),
|
|
0);
|
|
FakeHttpClient http;
|
|
http.response = {true, 200, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error,
|
|
"saved Sober rejection marker storage is unsafe");
|
|
EXPECT_EQ(http.request_count, 0);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), sober_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
SymlinkDefaultSoberRejectionMarkerFailsClosedWithoutTouchingTarget) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string sober_cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), sober_cookies));
|
|
const std::filesystem::path protected_target =
|
|
directory.path() / "protected-marker-target";
|
|
const std::string protected_contents = "must remain untouched\n";
|
|
ASSERT_TRUE(WriteFile(protected_target, protected_contents));
|
|
std::error_code error;
|
|
std::filesystem::create_directories(paths.auth_root(), error);
|
|
ASSERT_FALSE(error);
|
|
ASSERT_EQ(chmod(paths.auth_root().c_str(), S_IRWXU), 0);
|
|
std::filesystem::create_symlink(protected_target,
|
|
SoberRejectionPathFor(paths), error);
|
|
ASSERT_FALSE(error);
|
|
FakeHttpClient http;
|
|
http.response = {true, 200, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(http.request_count, 0);
|
|
EXPECT_EQ(ReadFile(protected_target), protected_contents);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), sober_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
FifoDefaultSoberRejectionMarkerFailsClosedWithoutBlocking) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string sober_cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), sober_cookies));
|
|
std::error_code error;
|
|
std::filesystem::create_directories(paths.auth_root(), error);
|
|
ASSERT_FALSE(error);
|
|
ASSERT_EQ(chmod(paths.auth_root().c_str(), S_IRWXU), 0);
|
|
ASSERT_EQ(mkfifo(SoberRejectionPathFor(paths).c_str(), S_IRUSR | S_IWUSR), 0);
|
|
FakeHttpClient http;
|
|
http.response = {true, 200, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(http.request_count, 0);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), sober_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
RejectionWriterLockHardlinkCannotChangeDefaultSoberFile) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string sober_cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), sober_cookies));
|
|
ASSERT_EQ(chmod(paths.sober_cookie_file().c_str(), S_IRWXU), 0);
|
|
std::error_code error;
|
|
std::filesystem::create_directories(paths.auth_root(), error);
|
|
ASSERT_FALSE(error);
|
|
ASSERT_EQ(chmod(paths.auth_root().c_str(), S_IRWXU), 0);
|
|
const std::filesystem::path lock_path =
|
|
paths.auth_root() / ".default-sober-rejection.mocktail-writer.lock";
|
|
std::filesystem::create_hard_link(paths.sober_cookie_file(), lock_path,
|
|
error);
|
|
ASSERT_FALSE(error);
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(composition.error,
|
|
"rejected Sober credential could not be retired safely");
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), sober_cookies);
|
|
struct stat sober_metadata = {};
|
|
ASSERT_EQ(lstat(paths.sober_cookie_file().c_str(), &sober_metadata), 0);
|
|
EXPECT_EQ(sober_metadata.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO), S_IRWXU);
|
|
EXPECT_FALSE(std::filesystem::exists(paths.default_sober_rejection_file()));
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401ClearsPrivateReadOnlyManagedCredential) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string remaining_cookies = "OtherCookie=keep\n";
|
|
const std::string rejected_cookie =
|
|
std::string(".ROBLOSECURITY=") + kCredential;
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(),
|
|
rejected_cookie + "\n" + remaining_cookies));
|
|
ASSERT_EQ(chmod(paths.cookie_file().c_str(), S_IRUSR), 0);
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(
|
|
ReadFile(paths.cookie_file()),
|
|
std::string(rejected_cookie.size(), ' ') + "\n" + remaining_cookies);
|
|
struct stat metadata = {};
|
|
ASSERT_EQ(lstat(paths.cookie_file().c_str(), &metadata), 0);
|
|
EXPECT_EQ(metadata.st_mode & (S_IRWXG | S_IRWXO), 0U);
|
|
EXPECT_NE(metadata.st_mode & S_IWUSR, 0U);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401RedactsRawAndDuplicateSegmentsWithMixedDelimiters) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string raw_cookie = kCredential;
|
|
const std::string duplicate = ".ROBLOSECURITY=duplicate-value";
|
|
const std::string cookies =
|
|
raw_cookie + "\r\nOtherCookie=keep; " + duplicate + "\tTailCookie=keep\n";
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kGuest);
|
|
const std::string expected =
|
|
std::string(raw_cookie.size(), ' ') + "\r\nOtherCookie=keep;" +
|
|
std::string(duplicate.size() + 1, ' ') + "\tTailCookie=keep\n";
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), expected);
|
|
EXPECT_EQ(expected.size(), cookies.size());
|
|
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
ASSERT_TRUE(restarted) << restarted.error;
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kGuest);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, Http403DoesNotClearOrStartGuest) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies = std::string(".ROBLOSECURITY=") + kCredential +
|
|
"\nRBXEventTrackerV2=browserid=123456\n";
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 403, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 403);
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
DefaultSoberHttp403DoesNotCreateRejectionMarker) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\nOther=keep\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 403, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 403);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), cookies);
|
|
EXPECT_FALSE(std::filesystem::exists(SoberRejectionPathFor(paths)));
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
DefaultSoberTransportFailureDoesNotCreateRejectionMarker) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {false, 0, {}, "request contained sensitive details"};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error, "authentication service unavailable");
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), cookies);
|
|
EXPECT_FALSE(std::filesystem::exists(SoberRejectionPathFor(paths)));
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
DefaultSoberHttp401PersistsRejectionWhenGuestIsDisabled) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), cookies);
|
|
EXPECT_EQ(ReadFile(SoberRejectionPathFor(paths)),
|
|
SoberRejectionMarkerFor(kCredential));
|
|
|
|
FakeHttpClient restart_http;
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted =
|
|
ComposeAuthRuntime(environment, paths, restart_auth_service);
|
|
EXPECT_FALSE(restarted);
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(restart_http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401ClearsManagedCredentialButDoesNotStartGuestWhenDisabled) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string remaining_cookies = "RBXEventTrackerV2=browserid=123456\n";
|
|
const std::string rejected_cookie =
|
|
std::string(".ROBLOSECURITY=") + kCredential;
|
|
const std::string cookies = rejected_cookie + "\n" + remaining_cookies;
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(), cookies));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(
|
|
ReadFile(paths.cookie_file()),
|
|
std::string(rejected_cookie.size(), ' ') + "\n" + remaining_cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401DoesNotClearExplicitCredentialOverride) {
|
|
TempDirectory directory;
|
|
const std::filesystem::path explicit_cookie =
|
|
directory.path() / "explicit-cookie";
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(explicit_cookie, cookies));
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_COOKIE_FILE", explicit_cookie.string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(ReadFile(explicit_cookie), cookies);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401DoesNotClearExplicitSoberCredentialOverride) {
|
|
TempDirectory directory;
|
|
const std::filesystem::path explicit_sober_cookie =
|
|
directory.path() / "explicit-sober-cookie";
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(explicit_sober_cookie, cookies));
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_SOBER_COOKIE_FILE", explicit_sober_cookie.string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string marker = SoberRejectionMarkerFor(kCredential);
|
|
ASSERT_TRUE(WriteFile(SoberRejectionPathFor(paths), marker));
|
|
ASSERT_EQ(chmod(paths.auth_root().c_str(), S_IRWXU), 0);
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kInvalidCredentials);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(ReadFile(explicit_sober_cookie), cookies);
|
|
EXPECT_EQ(ReadFile(SoberRejectionPathFor(paths)), marker);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
Http401FailsClosedWhileCookieWriterLockIsHeld) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n";
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(), cookies));
|
|
const std::filesystem::path lock_path =
|
|
paths.cookie_file().parent_path() /
|
|
("." + paths.cookie_file().filename().string() + ".mocktail-writer.lock");
|
|
ASSERT_TRUE(WriteFile(lock_path, ""));
|
|
const int lock_descriptor =
|
|
open(lock_path.c_str(), O_RDWR | O_CLOEXEC | O_NOFOLLOW);
|
|
ASSERT_GE(lock_descriptor, 0);
|
|
ASSERT_EQ(flock(lock_descriptor, LOCK_EX | LOCK_NB), 0);
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), cookies);
|
|
EXPECT_EQ(flock(lock_descriptor, LOCK_UN), 0);
|
|
EXPECT_EQ(close(lock_descriptor), 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
CredentialRefreshDuringHttp401RecoveryIsPreserved) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(),
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n"));
|
|
struct stat original_metadata = {};
|
|
ASSERT_EQ(lstat(paths.cookie_file().c_str(), &original_metadata), 0);
|
|
const std::string refreshed_contents =
|
|
".ROBLOSECURITY=_|newly-refreshed-credential\n"
|
|
"RBXEventTrackerV2=browserid=987654\n";
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
http.before_response = [&]() {
|
|
EXPECT_TRUE(WriteFile(paths.cookie_file(), refreshed_contents));
|
|
};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(composition.error,
|
|
"Roblox cookie changed while authentication was checked");
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), refreshed_contents);
|
|
struct stat refreshed_metadata = {};
|
|
ASSERT_EQ(lstat(paths.cookie_file().c_str(), &refreshed_metadata), 0);
|
|
EXPECT_EQ(refreshed_metadata.st_dev, original_metadata.st_dev);
|
|
EXPECT_EQ(refreshed_metadata.st_ino, original_metadata.st_ino);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
CookieReplacementDuringHttp401RecoveryFailsClosed) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(),
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n"));
|
|
const std::filesystem::path protected_target =
|
|
directory.path() / "protected-cookie";
|
|
const std::string protected_contents =
|
|
".ROBLOSECURITY=_|newly-refreshed-credential\n";
|
|
ASSERT_TRUE(WriteFile(protected_target, protected_contents));
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
http.before_response = [&]() {
|
|
std::error_code error;
|
|
std::filesystem::remove(paths.cookie_file(), error);
|
|
EXPECT_FALSE(error);
|
|
std::filesystem::create_symlink(protected_target, paths.cookie_file(),
|
|
error);
|
|
EXPECT_FALSE(error);
|
|
};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(composition.error,
|
|
"rejected Roblox cookie could not be cleared safely");
|
|
EXPECT_TRUE(std::filesystem::is_symlink(paths.cookie_file()));
|
|
EXPECT_EQ(ReadFile(protected_target), protected_contents);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
ManagedHardlinkToDefaultSoberCookieFailsClosedWithoutRedaction) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
const std::string cookies =
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\nOther=keep\n";
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(), cookies));
|
|
ASSERT_EQ(chmod(paths.sober_cookie_file().c_str(), S_IRUSR), 0);
|
|
std::error_code error;
|
|
std::filesystem::create_directories(paths.auth_root(), error);
|
|
ASSERT_FALSE(error);
|
|
std::filesystem::create_hard_link(paths.sober_cookie_file(),
|
|
paths.cookie_file(), error);
|
|
ASSERT_FALSE(error);
|
|
FakeHttpClient http;
|
|
http.response = {true, 401, {}, {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.http_status, 401);
|
|
EXPECT_EQ(composition.error,
|
|
"managed Roblox cookie aliases the protected Sober file");
|
|
EXPECT_EQ(ReadFile(paths.cookie_file()), cookies);
|
|
EXPECT_EQ(ReadFile(paths.sober_cookie_file()), cookies);
|
|
struct stat sober_metadata = {};
|
|
ASSERT_EQ(lstat(paths.sober_cookie_file().c_str(), &sober_metadata), 0);
|
|
EXPECT_EQ(sober_metadata.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO), S_IRUSR);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, UnavailableAuthFailsClosedAndRedactsErrors) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_ROBLOX_COOKIES", kCredential);
|
|
FakeHttpClient http;
|
|
http.response = {false, 0, {}, std::string("request exposed ") + kCredential};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error, "authentication service unavailable");
|
|
EXPECT_EQ(composition.error.find(kCredential), std::string::npos);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, ExplicitCookieFileIsAuthoritative) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_CONFIG_ROOT",
|
|
(directory.path() / "config").string());
|
|
environment.Set("MOCKTAIL_COOKIE_FILE",
|
|
(directory.path() / "missing-cookie").string());
|
|
environment.Set("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
ASSERT_TRUE(WriteFile(paths.cookie_file(),
|
|
std::string(".ROBLOSECURITY=") + kCredential));
|
|
FakeHttpClient http;
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error, "configured Roblox cookie file is unavailable");
|
|
EXPECT_EQ(http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, RejectsCookieFileWithSharedPermissions) {
|
|
TempDirectory directory;
|
|
const std::filesystem::path cookie_file = directory.path() / "cookie";
|
|
ASSERT_TRUE(
|
|
WriteFile(cookie_file, std::string(".ROBLOSECURITY=") + kCredential));
|
|
ASSERT_EQ(chmod(cookie_file.c_str(), S_IRUSR | S_IWUSR | S_IRGRP), 0);
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_COOKIE_FILE", cookie_file.string());
|
|
FakeHttpClient http;
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error,
|
|
"Roblox cookie file permissions are not private");
|
|
EXPECT_EQ(http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, RejectsSymlinkCookieSource) {
|
|
TempDirectory directory;
|
|
const std::filesystem::path target = directory.path() / "cookie-target";
|
|
const std::filesystem::path symlink = directory.path() / "cookie-link";
|
|
ASSERT_TRUE(WriteFile(target, std::string(".ROBLOSECURITY=") + kCredential));
|
|
std::error_code error;
|
|
std::filesystem::create_symlink(target, symlink, error);
|
|
ASSERT_FALSE(error);
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_COOKIE_FILE", symlink.string());
|
|
FakeHttpClient http;
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
|
|
EXPECT_FALSE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kUnavailable);
|
|
EXPECT_EQ(composition.error, "configured Roblox cookie file is unavailable");
|
|
EXPECT_EQ(http.request_count, 0);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest, FallsBackToConfiguredSoberCookieFile) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_CONFIG_ROOT",
|
|
(directory.path() / "config").string());
|
|
environment.Set("MOCKTAIL_SOBER_COOKIE_FILE",
|
|
(directory.path() / "sober-cookies").string());
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(),
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n"));
|
|
FakeHttpClient http;
|
|
http.response = {
|
|
true, 200, R"({"id":654,"name":"SoberUser","displayName":"Sober"})", {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition);
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kAuthenticated);
|
|
EXPECT_EQ(composition.jni_vm->GetRobloxAuthIdentitySnapshot().user_id, 654);
|
|
EXPECT_EQ(http.request_count, 1);
|
|
|
|
std::ifstream migrated(paths.cookie_file(), std::ios::binary);
|
|
ASSERT_TRUE(migrated.good());
|
|
const std::string migrated_value(
|
|
(std::istreambuf_iterator<char>(migrated)),
|
|
std::istreambuf_iterator<char>());
|
|
EXPECT_EQ(migrated_value,
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n");
|
|
struct stat migrated_status = {};
|
|
ASSERT_EQ(lstat(paths.cookie_file().c_str(), &migrated_status), 0);
|
|
EXPECT_EQ(migrated_status.st_mode & (S_IRWXG | S_IRWXO), 0U);
|
|
|
|
MapEnvironment restart_environment;
|
|
restart_environment.Set("HOME", directory.path().string());
|
|
restart_environment.Set("MOCKTAIL_USE_SOBER_COOKIES", "0");
|
|
FakeHttpClient restart_http;
|
|
restart_http.response = {
|
|
true, 200, R"({"id":654,"name":"SoberUser","displayName":"Sober"})", {}};
|
|
services::AuthService restart_auth_service(restart_http);
|
|
const AuthRuntimeComposition restarted = ComposeAuthRuntime(
|
|
restart_environment, PathsFor(restart_environment, directory),
|
|
restart_auth_service);
|
|
ASSERT_TRUE(restarted);
|
|
EXPECT_EQ(restarted.status, AuthRuntimeStatus::kAuthenticated);
|
|
ASSERT_EQ(restart_http.request_count, 1);
|
|
EXPECT_EQ(restart_http.last_request.headers[1],
|
|
std::string("Cookie: .ROBLOSECURITY=") + kCredential);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
TrackerOnlyLocalCookieFallsBackToSoberCredential) {
|
|
TempDirectory directory;
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_CONFIG_ROOT",
|
|
(directory.path() / "config").string());
|
|
environment.Set("MOCKTAIL_SOBER_COOKIE_FILE",
|
|
(directory.path() / "sober-cookies").string());
|
|
const RuntimePaths paths = PathsFor(environment, directory);
|
|
ASSERT_TRUE(WriteFile(
|
|
paths.cookie_file(),
|
|
"RBXEventTrackerV2=browserid=123456&browserid_insert_timestamp=1\n"));
|
|
ASSERT_TRUE(WriteFile(paths.sober_cookie_file(),
|
|
std::string(".ROBLOSECURITY=") + kCredential + "\n"));
|
|
FakeHttpClient http;
|
|
http.response = {
|
|
true, 200, R"({"id":654,"name":"SoberUser","displayName":"Sober"})", {}};
|
|
services::AuthService auth_service(http);
|
|
|
|
const AuthRuntimeComposition composition =
|
|
ComposeAuthRuntime(environment, paths, auth_service);
|
|
|
|
ASSERT_TRUE(composition) << composition.error;
|
|
EXPECT_EQ(composition.status, AuthRuntimeStatus::kAuthenticated);
|
|
ASSERT_EQ(http.request_count, 1);
|
|
ASSERT_GE(http.last_request.headers.size(), 2U);
|
|
EXPECT_EQ(http.last_request.headers[1],
|
|
std::string("Cookie: .ROBLOSECURITY=") + kCredential);
|
|
}
|
|
|
|
TEST(AuthRuntimeCompositionTest,
|
|
FileReplacementCannotChangeCredentialOrIdentityDependency) {
|
|
constexpr char kCredentialA[] = "_|validated-credential-a";
|
|
constexpr char kCredentialB[] = "_|replacement-credential-b";
|
|
TempDirectory directory;
|
|
const std::filesystem::path cookie_file = directory.path() / "cookie";
|
|
ASSERT_TRUE(
|
|
WriteFile(cookie_file, std::string(".ROBLOSECURITY=") + kCredentialA));
|
|
MapEnvironment environment;
|
|
environment.Set("HOME", directory.path().string());
|
|
environment.Set("MOCKTAIL_COOKIE_FILE", cookie_file.string());
|
|
FakeHttpClient http;
|
|
http.response = {
|
|
true,
|
|
200,
|
|
R"({"id":777,"name":"ConsistentUser","displayName":"Consistent"})",
|
|
{}};
|
|
services::AuthService auth_service(http);
|
|
AuthRuntimeComposition composition = ComposeAuthRuntime(
|
|
environment, PathsFor(environment, directory), auth_service);
|
|
ASSERT_TRUE(composition);
|
|
|
|
ASSERT_TRUE(
|
|
WriteFile(cookie_file, std::string(".ROBLOSECURITY=") + kCredentialB));
|
|
mocktail::legacy::RuntimeDependencies dependencies(std::move(composition));
|
|
|
|
const std::string expected_header =
|
|
std::string(".ROBLOSECURITY=") + kCredentialA;
|
|
ASSERT_EQ(http.request_count, 1);
|
|
EXPECT_EQ(http.last_request.headers[1], "Cookie: " + expected_header);
|
|
EXPECT_EQ(dependencies.roblox_credential().view(), expected_header);
|
|
EXPECT_EQ(dependencies.account_identity().user_id, 777);
|
|
EXPECT_EQ(dependencies.account_identity().username, "ConsistentUser");
|
|
ASSERT_NE(dependencies.jni_vm(), nullptr);
|
|
const jnivm::RobloxAuthIdentity vm_identity =
|
|
dependencies.jni_vm()->GetRobloxAuthIdentitySnapshot();
|
|
EXPECT_EQ(vm_identity.user_id, dependencies.account_identity().user_id);
|
|
EXPECT_EQ(vm_identity.username, dependencies.account_identity().username);
|
|
|
|
{
|
|
const ScopedRobloxCredentialBinding credential_binding(
|
|
dependencies.jni_vm().get(), dependencies.roblox_credential());
|
|
ASSERT_TRUE(credential_binding.bound());
|
|
JNIEnv* env = dependencies.jni_vm()->GetJNIEnv();
|
|
jclass cookie_manager =
|
|
env->FindClass("com/roblox/universalapp/cookie/JNICookieManager");
|
|
jmethodID get_cookie = env->GetStaticMethodID(
|
|
cookie_manager, "getCookie", "(Ljava/lang/String;)Ljava/lang/String;");
|
|
auto java_cookie = static_cast<jstring>(env->CallStaticObjectMethod(
|
|
cookie_manager, get_cookie, env->NewStringUTF("roblox.com")));
|
|
EXPECT_EQ(ReadJavaString(env, java_cookie), expected_header);
|
|
}
|
|
EXPECT_FALSE(
|
|
dependencies.jni_vm()->CopyRobloxCredentialFromProvider(nullptr));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace runtime
|
|
} // namespace mocktail
|