Repository navigation
Scripting: cover client-reported Epic ID whitelists #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,9 +8,14 @@ | |
|
|
||
| #pragma once | ||
|
|
||
| #include "connection_admission_ut.h" | ||
|
|
||
| #include "core_modules.h" | ||
| #include "integrations/server/connection_gate.h" | ||
| #include "integrations/server/scripting/module.h" | ||
| #include "networking/replication/network_entity.h" | ||
| #include "networking/replication/replication_manager.h" | ||
| #include "scripting/builtins/player.h" | ||
| #include "scripting/node_engine.h" | ||
| #include "scripting/resource/resource_manager.h" | ||
|
|
||
|
|
@@ -26,10 +31,32 @@ | |
|
|
||
| // The admission gate against a real Node engine and a real resource: what a playerConnecting | ||
| // handler does -- return, reject, throw, await, hang -- and what the gate decides for it. The | ||
| // network side is left out (no NetworkServer, so update() sends nothing); the decisions are what | ||
| // the instance acts on, and those are what is asserted. | ||
| // decisions are what the instance acts on. A loopback admission also checks the joined player's | ||
| // script-visible identity. | ||
| class ConnectionGateTestRig { | ||
| public: | ||
| // Register an admitted peer's avatar for the same owner-guid lookup used by server scripts. | ||
| struct JoinedPlayer { | ||
| Framework::Networking::NetworkServer &server; | ||
| Framework::Networking::Replication::NetworkEntity avatar; | ||
|
|
||
| JoinedPlayer(Framework::Networking::NetworkServer &peer, MafiaNet::RakNetGUID guid): server(peer) { | ||
| avatar.SetNetworkID(1); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| avatar.ownerGUID = MafiaNet::ToPeerGuid(guid); | ||
| server.GetReplicationManager()->Reference(&avatar); | ||
| server.GetReplicationManager()->SetViewer(avatar.ownerGUID, &avatar); | ||
| Framework::CoreModules::SetNetworkPeer(&server); | ||
| Framework::CoreModules::SetReplication(server.GetReplicationManager()); | ||
| } | ||
|
|
||
| ~JoinedPlayer() { | ||
| Framework::CoreModules::SetReplication(nullptr); | ||
| Framework::CoreModules::SetNetworkPeer(nullptr); | ||
| server.GetReplicationManager()->ClearViewer(avatar.ownerGUID); | ||
| server.GetReplicationManager()->Dereference(&avatar); | ||
| } | ||
| }; | ||
|
|
||
| static std::string ResourcesPath() { | ||
| #ifdef _WIN32 | ||
| const char *temp = std::getenv("TEMP"); | ||
|
|
@@ -64,6 +91,12 @@ class ConnectionGateTestRig { | |
| globalThis.__probe = connection; | ||
| globalThis.__pendingDuring = connection.isPending() ? 1 : 0; | ||
| return; | ||
| case "epic-whitelist": | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| globalThis.__epicId = connection.epicId; | ||
| if (connection.epicId !== "0123456789abcdef0123456789abcdef") { | ||
| connection.reject("Your Epic account is not on the whitelist."); | ||
| } | ||
| return; | ||
| } | ||
| }); | ||
| )"; | ||
|
|
@@ -186,6 +219,75 @@ MODULE(connection_gate, { | |
| STREQUALS(Rig::EvalString(module.GetEngine(), "globalThis.__seen").c_str(), "admit|one-time-ticket|10.0.0.2|76561198000000000"); | ||
| }); | ||
|
|
||
| IT("admits an Epic whitelist entry using the client-reported ID without a ticket", { | ||
| auto identity = Rig::Identity("epic-whitelist"); | ||
| identity.steamId.clear(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block (clear |
||
| identity.ticket.clear(); | ||
| identity.epicId = "0123456789abcdef0123456789abcdef"; | ||
| EQUALS(gate.Begin(Rig::Guid(12), identity, "10.0.0.12"), true); | ||
| const auto decisions = Rig::Settle(module, gate); | ||
| EQUALS(decisions.size(), 1u); | ||
| EQUALS(decisions[0].admitted, true); | ||
| STREQUALS(Rig::EvalString(module.GetEngine(), "globalThis.__epicId").c_str(), identity.epicId.c_str()); | ||
| }); | ||
|
|
||
| IT("exposes the admitted peer's Epic ID through script-visible Player.epicId after joining", { | ||
| ConnectionAdmissionRig network; | ||
| EQUALS(network.StartServer(true), true); | ||
| const char *epicId = "0123456789abcdef0123456789abcdef"; | ||
| EQUALS(network.Connect(ConnectionAdmissionRig::IdentityPayload("epic-whitelist", "", epicId)), true); | ||
| EQUALS(network.PumpUntil([&] { return !network.seen.requests.empty(); }), true); | ||
| EQUALS(network.seen.requests[0].has_value(), true); | ||
|
|
||
| const auto guid = network.seen.requestGuids[0]; | ||
| const auto &identity = *network.seen.requests[0]; | ||
| // The instance retains the received identity before running playerConnecting. | ||
| network.server->SetPeerIdentity(guid, identity); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This copies |
||
| EQUALS(gate.Begin(guid, identity, network.server->GetAddress(guid)), true); | ||
| const auto decisions = Rig::Settle(module, gate); | ||
| EQUALS(decisions.size(), 1u); | ||
| EQUALS(decisions[0].guid.g, guid.g); | ||
| EQUALS(decisions[0].admitted, true); | ||
| network.server->AcceptSession(decisions[0].guid); | ||
| EQUALS(network.PumpUntil([&] { return network.seen.clientConnected && network.server->GetPlayerCount() == 1u; }), true); | ||
| EQUALS(network.seen.clientDisconnected, false); | ||
|
|
||
| Rig::JoinedPlayer player(*network.server, guid); | ||
| auto *engine = module.GetEngine(); | ||
| v8::Isolate *isolate = engine->GetResourceRuntime("gate")->GetIsolate(); | ||
| { | ||
| v8::Locker locker(isolate); | ||
| v8::Isolate::Scope isolateScope(isolate); | ||
| v8::HandleScope handleScope(isolate); | ||
| v8::Local<v8::Context> context = engine->GetContext(); | ||
| v8::Context::Scope contextScope(context); | ||
| Framework::Scripting::Builtins::Player::Register(isolate, context->Global()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-registering the |
||
| } | ||
| STREQUALS(Rig::EvalString(engine, "new Player(1).epicId").c_str(), epicId); | ||
| }); | ||
|
|
||
| IT("lets an Epic whitelist refuse a connection without an Epic ID", { | ||
| EQUALS(gate.Begin(Rig::Guid(13), Rig::Identity("epic-whitelist"), "10.0.0.13"), true); | ||
| const auto decisions = Rig::Settle(module, gate); | ||
| EQUALS(decisions.size(), 1u); | ||
| EQUALS(decisions[0].admitted, false); | ||
| STREQUALS(decisions[0].reason.c_str(), "Your Epic account is not on the whitelist."); | ||
| STREQUALS(Rig::EvalString(module.GetEngine(), "globalThis.__epicId").c_str(), ""); | ||
| }); | ||
|
|
||
| IT("lets an Epic whitelist refuse a nonempty ID that is not listed", { | ||
| auto identity = Rig::Identity("epic-whitelist"); | ||
| identity.steamId.clear(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate of the setup at L224, see above. |
||
| identity.ticket.clear(); | ||
| identity.epicId = "fedcba9876543210fedcba9876543210"; | ||
| EQUALS(gate.Begin(Rig::Guid(14), identity, "10.0.0.14"), true); | ||
| const auto decisions = Rig::Settle(module, gate); | ||
| EQUALS(decisions.size(), 1u); | ||
| EQUALS(decisions[0].admitted, false); | ||
| STREQUALS(decisions[0].reason.c_str(), "Your Epic account is not on the whitelist."); | ||
| STREQUALS(Rig::EvalString(module.GetEngine(), "globalThis.__epicId").c_str(), identity.epicId.c_str()); | ||
| }); | ||
|
|
||
| IT("refuses with the script's reason", { | ||
| EQUALS(gate.Begin(Rig::Guid(3), Rig::Identity("refuse"), "10.0.0.3"), true); | ||
| const auto decisions = Rig::Settle(module, gate); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,7 @@ waits up to two minutes for an answer; it is always the server that decides. | |
| |---|---| | ||
| | `nickname` | the name the player asked to join under | | ||
| | `steamId`, `discordId`, `hardwareId` | as reported by the client; empty when absent | | ||
| | `epicId` | Epic account ID reported by the client; empty when absent | | ||
| | `ticket` | the `ticket` of the client's launch link, untouched | | ||
| | `ip` | remote address, no port | | ||
| | `reject(reason?)` | refuse; reason up to 512 bytes | | ||
|
|
@@ -116,6 +117,27 @@ itself; `steamId` is read from an environment variable the launcher sets and is | |
| not checked against Steam. A whitelist that must hold against a modified client | ||
| should check the `ticket` with whatever issued it. | ||
|
|
||
| The Epic launcher sets `MafiaHubEpicId` from its signed-in account, and the client | ||
| includes that ID in the existing connection handshake. The server applies a hex | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "applies a hex format check": the check accepts any length from 1 to 32 and either case without normalising. Either enforce exactly 32 and lowercase on the server, or say here that the ID may be uppercase or mixed case and that comparisons must be case-insensitive. |
||
| format check and exposes it as `PendingConnection.epicId` before admission and | ||
| `Player.epicId` after joining. It is an Epic account ID, not an EOS Product User | ||
| ID. No Epic proof or server-side account verification is performed. | ||
|
|
||
| For temporary, non-security filtering only, a handler can compare the | ||
| client-reported Epic ID with a list. This does not authenticate the player: a | ||
| modified client can submit a listed ID. For an authorization whitelist, validate | ||
| `connection.ticket` with its issuer before admitting the player. | ||
|
|
||
| The example below performs only the client-reported ID filter: | ||
|
|
||
| ```js | ||
| Events.on("playerConnecting", async (connection) => { | ||
| if (!connection.epicId || !await whitelist.containsEpicAccount(connection.epicId)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This snippet will be copied into real auth whitelists regardless of the caveat above. Consider making the primary example the |
||
| connection.reject("Your Epic account is not on the whitelist."); | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
|
Kheartz marked this conversation as resolved.
|
||
| --- | ||
|
|
||
| ## 5. Passing a ticket | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This couples the gate tests to the admission test module only for the loopback
Player.epicIdtest. If that test goes, so should this include and the replication/player includes below.