mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-29 04:01:45 +01:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f6847d6d7 |
No files matched your search
@@ -19,6 +19,13 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
|
|||||||
Status ParseRobloxLaunchUri(std::string_view uri,
|
Status ParseRobloxLaunchUri(std::string_view uri,
|
||||||
RobloxExperienceLaunchRequest* request);
|
RobloxExperienceLaunchRequest* request);
|
||||||
|
|
||||||
|
// Converts a WebView launchGame command
|
||||||
|
// ({"functionName":"launchGame","params":{"request":{...}}}) into the same
|
||||||
|
// contract. Returns kUnsupported for other commands and kInvalidArgument for
|
||||||
|
// a malformed launchGame request.
|
||||||
|
Status ParseRobloxWebViewLaunchGame(std::string_view command,
|
||||||
|
RobloxExperienceLaunchRequest* request);
|
||||||
|
|
||||||
} // namespace runtime
|
} // namespace runtime
|
||||||
} // namespace mocktail
|
} // namespace mocktail
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "jnivm/jnivm.h"
|
#include "jnivm/jnivm.h"
|
||||||
#include "runtime/external_launch_broker.h"
|
#include "runtime/external_launch_broker.h"
|
||||||
|
#include "runtime/roblox_launch_uri.h"
|
||||||
#include "runtime/webview_helper_launcher.h"
|
#include "runtime/webview_helper_launcher.h"
|
||||||
#include "window/window.h"
|
#include "window/window.h"
|
||||||
|
|
||||||
@@ -951,9 +952,23 @@ Status RobloxExperienceComposition::RouteCurrentWebSurfaceEvent(
|
|||||||
route = web_surface_route_;
|
route = web_surface_route_;
|
||||||
web_view_bridge = web_view_bridge_.get();
|
web_view_bridge = web_view_bridge_.get();
|
||||||
}
|
}
|
||||||
return event.type == WebViewHelperEventType::kRobloxCookie
|
if (event.type == WebViewHelperEventType::kRobloxCookie) {
|
||||||
? AcceptWebViewRobloxCookie(event.payload)
|
return AcceptWebViewRobloxCookie(event.payload);
|
||||||
: RouteWebSurfaceEvent(route, event, web_view_bridge);
|
}
|
||||||
|
if (event.type == WebViewHelperEventType::kExecuteRoblox) {
|
||||||
|
// Roblox defers a signalled launchGame until the Lua app next navigates.
|
||||||
|
// A parsed launchGame is launched here and never forwarded, so a deferred
|
||||||
|
// copy cannot start a second join later.
|
||||||
|
RobloxExperienceLaunchRequest request;
|
||||||
|
if (ParseRobloxWebViewLaunchGame(event.payload, &request).ok() &&
|
||||||
|
Dispatch(request).ok()) {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[experience] WebView launchGame queued place_id=%lld\n",
|
||||||
|
static_cast<long long>(request.place_id));
|
||||||
|
return CloseWebSurface();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RouteWebSurfaceEvent(route, event, web_view_bridge);
|
||||||
}
|
}
|
||||||
|
|
||||||
Status RobloxExperienceComposition::DrainPlatformEvents() {
|
Status RobloxExperienceComposition::DrainPlatformEvents() {
|
||||||
|
|||||||
@@ -640,5 +640,64 @@ Status ParseRobloxLaunchUri(std::string_view uri,
|
|||||||
return MakeRequest(fields, request);
|
return MakeRequest(fields, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Status ParseRobloxWebViewLaunchGame(std::string_view command,
|
||||||
|
RobloxExperienceLaunchRequest* request) {
|
||||||
|
if (request == nullptr) {
|
||||||
|
return Invalid("Roblox launchGame output is null");
|
||||||
|
}
|
||||||
|
*request = {};
|
||||||
|
const nlohmann::json document =
|
||||||
|
command.size() <= kMaximumRobloxLaunchUriBytes
|
||||||
|
? nlohmann::json::parse(command, nullptr, false)
|
||||||
|
: nlohmann::json();
|
||||||
|
const auto function_name = document.is_object()
|
||||||
|
? document.find("functionName")
|
||||||
|
: document.end();
|
||||||
|
if (!document.is_object() || function_name == document.end() ||
|
||||||
|
*function_name != "launchGame") {
|
||||||
|
return Status::Error(StatusCode::kUnsupported,
|
||||||
|
"WebView command is not launchGame");
|
||||||
|
}
|
||||||
|
const auto params = document.find("params");
|
||||||
|
if (params == document.end() || !params->is_object()) {
|
||||||
|
return Invalid("Roblox launchGame has no params");
|
||||||
|
}
|
||||||
|
const auto launch = params->find("request");
|
||||||
|
if (launch == params->end() || !launch->is_object()) {
|
||||||
|
return Invalid("Roblox launchGame has no request");
|
||||||
|
}
|
||||||
|
if (launch->size() > kMaximumParameterCount) {
|
||||||
|
return Invalid("Roblox launchGame request has too many fields");
|
||||||
|
}
|
||||||
|
LaunchFields fields;
|
||||||
|
for (const auto& [key, value] : launch->items()) {
|
||||||
|
std::string text;
|
||||||
|
if (value.is_string()) {
|
||||||
|
text = value.get<std::string>();
|
||||||
|
} else if (value.is_number_unsigned()) {
|
||||||
|
text = std::to_string(value.get<uint64_t>());
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (text.size() > kMaximumParameterValueBytes ||
|
||||||
|
ContainsControlBytes(text)) {
|
||||||
|
return Invalid("Roblox launchGame field is invalid");
|
||||||
|
}
|
||||||
|
Status status = AssignField(key == "requestType" ? "request" : key,
|
||||||
|
std::move(text), false, &fields);
|
||||||
|
if (!status.ok()) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fields.request_type.empty()) {
|
||||||
|
return Invalid("Roblox launchGame request type is missing");
|
||||||
|
}
|
||||||
|
Status status = MakeRequest(fields, request);
|
||||||
|
if (!status.ok()) {
|
||||||
|
*request = {};
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace runtime
|
} // namespace runtime
|
||||||
} // namespace mocktail
|
} // namespace mocktail
|
||||||
@@ -322,6 +322,77 @@ TEST(RobloxLaunchUriTest, ClearsOutputAndRedactsTicketOnFailure) {
|
|||||||
EXPECT_FALSE(ParseRobloxLaunchUri(kModernPublicLaunchUri, nullptr).ok());
|
EXPECT_FALSE(ParseRobloxLaunchUri(kModernPublicLaunchUri, nullptr).ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RobloxWebViewLaunchGameTest, ParsesPrivateServerPlayButton) {
|
||||||
|
constexpr char kCommand[] =
|
||||||
|
R"({"moduleID":"Game","functionName":"launchGame",)"
|
||||||
|
R"("callbackID":"b8cf5fbd-f6a8-cfba-2cae-84bce46f46b2",)"
|
||||||
|
R"("params":{"request":{"requestType":"RequestPrivateGame",)"
|
||||||
|
R"("browserTrackerId":"1790271319250009",)"
|
||||||
|
R"("accessCode":"664b524f-19cc-4e28-a947-e8f5286f4179",)"
|
||||||
|
R"("placeId":"920587237","joinAttemptId":"attempt-1",)"
|
||||||
|
R"("joinAttemptOrigin":"privateServerListJoin",)"
|
||||||
|
R"("isPlayTogetherGame":false}}})";
|
||||||
|
RobloxExperienceLaunchRequest request;
|
||||||
|
|
||||||
|
const Status status = ParseRobloxWebViewLaunchGame(kCommand, &request);
|
||||||
|
|
||||||
|
ASSERT_TRUE(status.ok()) << status.message();
|
||||||
|
EXPECT_EQ(request.place_id, 920587237);
|
||||||
|
EXPECT_EQ(request.access_code, "664b524f-19cc-4e28-a947-e8f5286f4179");
|
||||||
|
EXPECT_EQ(request.join_attempt_id, "attempt-1");
|
||||||
|
EXPECT_EQ(request.join_attempt_origin, "privateServerListJoin");
|
||||||
|
EXPECT_EQ(request.canonical_json.find("browserTrackerId"), std::string::npos);
|
||||||
|
ExpectCanonicalRequestMatches(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RobloxWebViewLaunchGameTest, ParsesServerJobWithNumericPlaceId) {
|
||||||
|
constexpr char kCommand[] =
|
||||||
|
R"({"moduleID":"Game","functionName":"launchGame",)"
|
||||||
|
R"("params":{"request":{"requestType":"RequestGameJob",)"
|
||||||
|
R"("placeId":1962086868,"gameId":"97ea-49e4"}}})";
|
||||||
|
RobloxExperienceLaunchRequest request;
|
||||||
|
|
||||||
|
const Status status = ParseRobloxWebViewLaunchGame(kCommand, &request);
|
||||||
|
|
||||||
|
ASSERT_TRUE(status.ok()) << status.message();
|
||||||
|
EXPECT_EQ(request.place_id, 1962086868);
|
||||||
|
EXPECT_EQ(request.game_instance_id, "97ea-49e4");
|
||||||
|
ExpectCanonicalRequestMatches(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RobloxWebViewLaunchGameTest, ReportsOtherCommandsAsUnsupported) {
|
||||||
|
RobloxExperienceLaunchRequest request;
|
||||||
|
request.place_id = 123;
|
||||||
|
request.canonical_json = "stale";
|
||||||
|
|
||||||
|
const Status status = ParseRobloxWebViewLaunchGame(
|
||||||
|
R"({"moduleID":"Game","functionName":"getSystemInfo","params":{}})",
|
||||||
|
&request);
|
||||||
|
|
||||||
|
EXPECT_EQ(status.code(), StatusCode::kUnsupported);
|
||||||
|
EXPECT_EQ(request.place_id, 0);
|
||||||
|
EXPECT_TRUE(request.canonical_json.empty());
|
||||||
|
EXPECT_EQ(ParseRobloxWebViewLaunchGame("not json", &request).code(),
|
||||||
|
StatusCode::kUnsupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RobloxWebViewLaunchGameTest, RejectsMalformedLaunchGame) {
|
||||||
|
const char* const kCommands[] = {
|
||||||
|
R"({"functionName":"launchGame"})",
|
||||||
|
R"({"functionName":"launchGame","params":{"request":"x"}})",
|
||||||
|
R"({"functionName":"launchGame","params":{"request":{)"
|
||||||
|
R"("requestType":"RequestPrivateGame","placeId":"920587237"}}})",
|
||||||
|
R"({"functionName":"launchGame","params":{"request":{)"
|
||||||
|
R"("requestType":"RequestGame","placeId":"-1"}}})",
|
||||||
|
};
|
||||||
|
for (const char* command : kCommands) {
|
||||||
|
RobloxExperienceLaunchRequest request;
|
||||||
|
const Status status = ParseRobloxWebViewLaunchGame(command, &request);
|
||||||
|
EXPECT_EQ(status.code(), StatusCode::kInvalidArgument) << command;
|
||||||
|
EXPECT_EQ(request.place_id, 0) << command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace runtime
|
} // namespace runtime
|
||||||
} // namespace mocktail
|
} // namespace mocktail
|
||||||
Reference in new issue
Block a user