diff --git a/.efrocachemap b/.efrocachemap index 314fa3e51..34b523aa0 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -432,21 +432,21 @@ "build/assets/ba_data/audio/zoeOw.ogg": "b2d705c31c9dcc1efdc71394764c3beb", "build/assets/ba_data/audio/zoePickup01.ogg": "e9366dc2d2b8ab8b0c4e2c14c02d0789", "build/assets/ba_data/audio/zoeScream01.ogg": "903e0e45ee9b3373e9d9ce20c814374e", - "build/assets/ba_data/data/langdata.json": "877df556e23febbdc8e266f69dac7bfc", + "build/assets/ba_data/data/langdata.json": "050a030236ce4f860dada6a5ef795045", "build/assets/ba_data/data/languages/arabic.json": "5edf715823cf15c046a7dc00c20f09f2", - "build/assets/ba_data/data/languages/belarussian.json": "355911950be67d7d0e59feef1decc082", + "build/assets/ba_data/data/languages/belarussian.json": "f9e8a5183ab0e5931bee5c2c315c7325", "build/assets/ba_data/data/languages/chinesesimplified.json": "b6448580e4126a86c9236216836a8dbd", "build/assets/ba_data/data/languages/chinesetraditional.json": "e51ac225c281671cff4edc5f8dbf3f9c", "build/assets/ba_data/data/languages/croatian.json": "57c5aeb4fa901c6cb3dde121f3992254", - "build/assets/ba_data/data/languages/czech.json": "6355807bc9ead3b5a996e64ade968158", + "build/assets/ba_data/data/languages/czech.json": "c5901cc18a57f1b8a201a8370ef4f41a", "build/assets/ba_data/data/languages/danish.json": "8576479c883d6928615e1d92e2858d21", "build/assets/ba_data/data/languages/dutch.json": "c82d6006c77f67f093175a8e359dde99", - "build/assets/ba_data/data/languages/english.json": "bba0b9421321dcff6c26dc801d978576", + "build/assets/ba_data/data/languages/english.json": "e8f40a64100b34eaaf926fc2fc711c22", "build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880", "build/assets/ba_data/data/languages/filipino.json": "edb202712c6ad5e420dafc0d8c1f9790", "build/assets/ba_data/data/languages/french.json": "1774066ac0f9b3c576b53d16260782c4", "build/assets/ba_data/data/languages/german.json": "9f5348438c63e13f5a861f2bf75c02e5", - "build/assets/ba_data/data/languages/gibberish.json": "6ec3e70b70eda9a59e85fdf82be334b2", + "build/assets/ba_data/data/languages/gibberish.json": "2ecdeaec89279745fa0a36861d09b990", "build/assets/ba_data/data/languages/greek.json": "b000dc81cf72771eb59abf55dfa47eed", "build/assets/ba_data/data/languages/hindi.json": "6b9ffe2e34fdab1a0aff316fc3ed92d1", "build/assets/ba_data/data/languages/hungarian.json": "4caa75658d8447fd8932f988db6c14ac", @@ -462,18 +462,18 @@ "build/assets/ba_data/data/languages/portuguesebrazil.json": "e4cdf6a43c10b42805381d9461b18379", "build/assets/ba_data/data/languages/portugueseportugal.json": "78ad0798a00c020798777f0de95a7701", "build/assets/ba_data/data/languages/romanian.json": "b4f537c9fff77d8a99fad683ac85150d", - "build/assets/ba_data/data/languages/russian.json": "a285c3dc002d7788eb6ccfdb269be52a", + "build/assets/ba_data/data/languages/russian.json": "ce16fe61e1f6ff793a51bce8015f6f58", "build/assets/ba_data/data/languages/serbian.json": "623fa4129a1154c2f32ed7867e56ff6a", "build/assets/ba_data/data/languages/slovak.json": "c9db851b588dbc682739f91093b75f27", - "build/assets/ba_data/data/languages/spanishlatinamerica.json": "8d31346a5385ba03e8372b6be0269a13", + "build/assets/ba_data/data/languages/spanishlatinamerica.json": "80d943b6c95579deb4468c549142c0bc", "build/assets/ba_data/data/languages/spanishspain.json": "e7bb815a3ed410e61196ab0c4621adeb", "build/assets/ba_data/data/languages/swedish.json": "51c5dec33c557404dfc265e583bff0d4", "build/assets/ba_data/data/languages/tamil.json": "3c21428c423ecf3524ff8efa18964d92", "build/assets/ba_data/data/languages/thai.json": "afc4cdc1b30f511ea5e27727d16f960a", - "build/assets/ba_data/data/languages/turkish.json": "79ef054b6328ae04da93216ebc4dccf2", + "build/assets/ba_data/data/languages/turkish.json": "d2f5260b651eb4d079d1a90f5d092f2c", "build/assets/ba_data/data/languages/ukrainian.json": "1b204d74e82f9d1fff515f7f5da34510", "build/assets/ba_data/data/languages/venetian.json": "4c3a567a8da87defb9d7acc74e1ecc0e", - "build/assets/ba_data/data/languages/vietnamese.json": "5dc3e11314bb63374ce2d76ca6f80fa2", + "build/assets/ba_data/data/languages/vietnamese.json": "0509767bb1b0524b98433c08df86d873", "build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054", "build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422", "build/assets/ba_data/data/maps/courtyard.json": "4b836554c8949bcd2ae382f5e3c1a9cc", @@ -4311,53 +4311,53 @@ "build/assets/windows/x64/pythonw.exe": "007fb5e669a3b6b3e8facf0728b87521", "build/assets/windows/x64/pythonw_d.exe": "46925c0fa3ca3fd29a33e54ee31f6cd5", "build/assets/windows/x64/vc_redist.x64.exe": "a8cf8406eae3c38b6bc3285685266b47", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "e74d982a0ec86796330ff99fc9eced46", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "ac11397e76adfa2c08be161daeeea325", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "8f98723c53496321a11f458f32f4ef3e", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "c0857cd649a4ffb98aab814c8b419425", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "fb027505d1bff36ddcb8e12eb3f777df", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "ddf7ada2884309d55433375a55b126c8", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "bef5bd3ec2d35814bf7ba3808888e8c9", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "2f6986d8602b4a97cee363b2e4a547fc", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "877bedb8fb4237d9a5740c42fc5cecd7", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "d33efecb41adf8a140835116cca16eab", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "722c32ffeab47c8767ce3b403848cb84", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "fffee5b0a739b5710c0812f538a94b7e", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "32937182f64c75358392653efdf91470", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "5b0d14eba0fbe0a7d7977352fb6f5114", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "d418faa1654c81bf2305a64d442a7872", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "2a2e70044d6b1aaa0a455c6346834d26", - "build/prefab/full/windows_x86_64_gui/debug/BallisticaKit.exe": "de4baeec3ca6b271918e5655b3a20075", - "build/prefab/full/windows_x86_64_gui/release/BallisticaKit.exe": "65fe24ec6c123ad8fbc639ea22a2857f", - "build/prefab/full/windows_x86_64_server/debug/dist/BallisticaKitHeadless.exe": "9b53b88eeeb14e359ad6eb47cbd49e82", - "build/prefab/full/windows_x86_64_server/release/dist/BallisticaKitHeadless.exe": "9df6c4db6d40c7605edb80b33c0b2b67", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "e28b9037a8b947a41de21f585912dbc8", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "108ab0fd6efe8eb647dd58ef1ccdd180", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "e28b9037a8b947a41de21f585912dbc8", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "108ab0fd6efe8eb647dd58ef1ccdd180", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "78e3270bbbbb42f47e76d88936472417", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "e05aa47c84077743c08fa73e70350155", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "78e3270bbbbb42f47e76d88936472417", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "e05aa47c84077743c08fa73e70350155", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "c9246391c4c753130ada114a12b1b852", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "9c098739549bdccfb059666e59b1ab64", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "c9246391c4c753130ada114a12b1b852", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "9c098739549bdccfb059666e59b1ab64", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "e48590b1175334ca2a6ac29105dc6d39", - "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "90411e32b6511614d7bf776f5c22e9e5", - "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "b6bcf331d03bea7d1f01a983aabbfe4f", - "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "90411e32b6511614d7bf776f5c22e9e5", - "build/prefab/lib/windows/Debug_x64/BallisticaKitGenericPlus.lib": "e365ef1f77d51a9760277bf7b2070c03", - "build/prefab/lib/windows/Debug_x64/BallisticaKitGenericPlus.pdb": "e5f1b5ce4db2f6f070705e81957981eb", - "build/prefab/lib/windows/Debug_x64/BallisticaKitHeadlessPlus.lib": "9dcdb478056a80e68eb907bc53a9b4d9", - "build/prefab/lib/windows/Debug_x64/BallisticaKitHeadlessPlus.pdb": "26475177750fb5943e69695e94f44f40", - "build/prefab/lib/windows/Release_x64/BallisticaKitGenericPlus.lib": "f38ae0d974c0d6bb68f61371103923d1", - "build/prefab/lib/windows/Release_x64/BallisticaKitGenericPlus.pdb": "6140cbb77c272d080203a7767dbbd5ad", - "build/prefab/lib/windows/Release_x64/BallisticaKitHeadlessPlus.lib": "c2770332c9d9d756fef076408a682bb6", - "build/prefab/lib/windows/Release_x64/BallisticaKitHeadlessPlus.pdb": "3a6bc4661709ff04483d441882d47ca3", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "d7147cb7713784e2ae61a5cbec7e45d8", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "00d6bff54bbee244eee63d364f4d17ff", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "7a44563fb64574216b77f0d81713d7d4", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "f3fe26d6daec54382e47d7a17cad92c8", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "ebca3f17b72d279aeba4086baa438e10", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "9a8f79586cb0ac0fe198e16c5750ae9c", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "d89a3c2339266de6df7b10daadc3c899", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "c87bf8cd93dd9b56094211987aef84e1", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "b7da415e47a72f79292944cbd4380f24", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "7def069c7c89b688e5482120d391a75c", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "e61bbd2c9e49f60afdd828480d90e47d", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "08cabf710bf80dd21eef5b0af72c70d7", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "7b8ee7df1b71e9c4238b4e90089352df", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "16f455689441513f04c3fe54249ef942", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "1e5fc5b33c2b8b008df471e21e8b661d", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "bde20632396ac6badcc72a6e8ba36e48", + "build/prefab/full/windows_x86_64_gui/debug/BallisticaKit.exe": "47ecc444f5b06bda5b163f72de59998c", + "build/prefab/full/windows_x86_64_gui/release/BallisticaKit.exe": "e7cd2626e7044480177eb83e1488d93b", + "build/prefab/full/windows_x86_64_server/debug/dist/BallisticaKitHeadless.exe": "c836355cf3f358efb66c6d789f6b6910", + "build/prefab/full/windows_x86_64_server/release/dist/BallisticaKitHeadless.exe": "8b39341609010b444d7ce1dccf2e69d0", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "5fa3cf6c07c69d931d18ef9ca2c688fb", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "dd01d447e04aa062e309e23fa38ac37e", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "5fa3cf6c07c69d931d18ef9ca2c688fb", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "dd01d447e04aa062e309e23fa38ac37e", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "db273f31be73e12464ec8cd8ab9e0564", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "81e4665a586d3e49f181e74ddc10f9f0", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "db273f31be73e12464ec8cd8ab9e0564", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "81e4665a586d3e49f181e74ddc10f9f0", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "3d9feb1853b66fd1d383b1a300ff0086", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "97891a1e652d88ee23d100cf7391d4c8", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "3d9feb1853b66fd1d383b1a300ff0086", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "97891a1e652d88ee23d100cf7391d4c8", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "307d25c0ad84e068dcb661a28346615a", + "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "d66227793ffea89c39f916ac3c258b97", + "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "f7db6a44c8f820012c2be90d2494139b", + "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "d66227793ffea89c39f916ac3c258b97", + "build/prefab/lib/windows/Debug_x64/BallisticaKitGenericPlus.lib": "8ceb45cd2d55454a0cdffb5047fc5203", + "build/prefab/lib/windows/Debug_x64/BallisticaKitGenericPlus.pdb": "85047eb7a35cb695410af030de0e53ba", + "build/prefab/lib/windows/Debug_x64/BallisticaKitHeadlessPlus.lib": "0a4cd99750f15aa54e0a760943e3eff6", + "build/prefab/lib/windows/Debug_x64/BallisticaKitHeadlessPlus.pdb": "c78a027732f324e3c483dc46bc8d7939", + "build/prefab/lib/windows/Release_x64/BallisticaKitGenericPlus.lib": "9016f9fa676880ea0869a455b8a64967", + "build/prefab/lib/windows/Release_x64/BallisticaKitGenericPlus.pdb": "58a867f65d24f34788b14b696d8741b5", + "build/prefab/lib/windows/Release_x64/BallisticaKitHeadlessPlus.lib": "c6842dc02d847a32af37e5308c4c6588", + "build/prefab/lib/windows/Release_x64/BallisticaKitHeadlessPlus.pdb": "e25c053b4f4cf46de3bd14e03e942849", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "b7b673ec92ffabd4813d222dfeae9bc8", - "src/ballistica/base/mgen/pyembed/binding_base.inc": "943cdbda1dcf399783675b115c22dae5", + "src/ballistica/base/mgen/pyembed/binding_base.inc": "9d4fec7f10a3214e7aaef5b2c40a3ae1", "src/ballistica/base/mgen/pyembed/binding_base_app.inc": "626a9e15808353edc4cf29d0c4c3fcc7", "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "926ea4fc0c64ba8db4fc7f9df5fb818a", "src/ballistica/core/mgen/pyembed/binding_core.inc": "522036812168bc95c62dd4bafa2f7248", diff --git a/.gitignore b/.gitignore index 2c0aadae6..d279bae71 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ local.properties .clang-format .style.yapf .irony +.claude/worktrees/ PUBSYNC_IN_PROGRESS _fulltest_buildfile_* ballistica_files/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9067dbf2c..a68185dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ -### 1.7.61 (build 22712, api 9, 2026-02-12) +### 1.7.61 (build 22714, api 9, 2026-02-21) +- Added scenev1 protocol 36, which enables V2 auth for servers. This allows + servers to receive authenticated V2 account info for all players before they + are allowed to join and fixes the spoofing vulnerabilities that V1 auth had. + V2 account ids look like 'a-XXX' whereas old V1 looked like 'pb-XXXX'. The + default protocol is still 33, but if you are running a server it is highly + recommended to set your protocol to 36 in your server config to enable this. +- Clients will now wait for responses for up to 10 seconds when connecting to a + server and 30 seconds if contact is lost once connected. Hopefully this + reduces disconnects due to momentary network issues. Holler if this feels like + too long. Old values were 5 and 10 seconds respectively. +- Improved efficiency of various low level logging calls in the C++ layer + (thanks std::string_view!). +- Fixed an issue where clicks could sometimes be lost in the nearby-parties + browser. ### 1.7.60 (build 22709, api 9, 2026-02-11) - Fixed a longstanding issue causing impact, roll, and skid sounds to not diff --git a/config/requirements.txt b/config/requirements.txt index 1150350ca..c16f12916 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -1,13 +1,13 @@ cpplint==2.0.2 cryptography==46.0.5 dmgbuild==1.6.7 -filelock==3.20.3 +filelock==3.24.3 furo==2025.12.19 libcst==1.8.6 mypy==1.19.1 pbxproj==4.3.3 pur==7.3.3 -pylint==4.0.3 +pylint==4.0.5 pylsp-mypy==0.7.1 pytest==9.0.2 python-daemon==3.1.2 @@ -22,4 +22,4 @@ types-filelock==3.2.7 types-requests==2.32.4.20260107 typing_extensions==4.15.0 urllib3==2.6.3 -zuban==0.5.1 +zuban==0.6.0 diff --git a/config/toolconfigsrc/pylintrc b/config/toolconfigsrc/pylintrc index 1ab005198..b20e9b683 100644 --- a/config/toolconfigsrc/pylintrc +++ b/config/toolconfigsrc/pylintrc @@ -67,9 +67,12 @@ disable=broad-except, enable=useless-suppression [BASIC] + +# Allow module level names like _g_foo or _FOO. +const-rgx=^(__.*__|_?[A-Z][A-Z0-9_]*|_g_[a-z][a-z0-9_]*)$ + # Allowing a handful of short names commonly understood to be iterators, # math concepts, or short-but-complete words. - good-names=i, j, k, diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index 7253a157c..43a4f5579 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -24,7 +24,6 @@ add_clean_frame_callback, allows_ticket_sales, android_get_external_files_dir, - app_instance_uuid, appname, appnameupper, apptime, @@ -234,7 +233,6 @@ 'AppIntentExec', 'AppMode', 'AppState', - 'app_instance_uuid', 'applog', 'appname', 'appnameupper', diff --git a/src/assets/ba_data/python/babase/_accountv2.py b/src/assets/ba_data/python/babase/_accountv2.py index 0a24d40c0..657432a80 100644 --- a/src/assets/ba_data/python/babase/_accountv2.py +++ b/src/assets/ba_data/python/babase/_accountv2.py @@ -4,9 +4,11 @@ from __future__ import annotations +import time import hashlib import logging from functools import partial +from dataclasses import dataclass from typing import TYPE_CHECKING, assert_never from efro.error import CommunicationError @@ -19,6 +21,8 @@ if TYPE_CHECKING: from typing import Any, Callable + import bacommon.cloud + from babase._login import LoginAdapter, LoginInfo @@ -62,6 +66,9 @@ def __init__(self) -> None: Callable[[AccountV2Handle | None], None] ] = CallbackSet() + # Request state per global-app-instance-id + self._auth_requests: dict[str, _AuthRequest] = {} + adapter: LoginAdapter if _babase.using_google_play_game_services(): adapter = LoginAdapterGPGS() @@ -104,6 +111,9 @@ def on_primary_account_changed( """ assert _babase.in_logic_thread() + # Blow away any outstanding auth-requests. + self._auth_requests = {} + # Inform the base layer of new names/etc. if account is not None: _babase.set_account_sign_in_state(True, account.tag) @@ -201,6 +211,78 @@ def on_no_initial_primary_account(self) -> None: self._initial_sign_in_completed = True _babase.app.on_initial_sign_in_complete() + def auth_request( + self, global_app_instance_id: str + ) -> None | tuple[bool, str]: + """Start/process an auth request.""" + import bacommon.cloud + + assert _babase.in_logic_thread() + plus = _babase.app.plus + assert plus is not None + + now = time.monotonic() + + # If there are any expired ones, do a prune pass. + if any(r.expire_time <= now for r in self._auth_requests.values()): + self._auth_requests = { + rid: r + for rid, r in self._auth_requests.items() + if r.expire_time > now + } + + auth_request = self._auth_requests.get(global_app_instance_id) + + # If we find no attempt in progress, kick one off. + if ( + auth_request is None + and plus.cloud.connected + and self.primary is not None + ): + # print('SENDING AUTH REQUEST') + auth_request = self._auth_requests[global_app_instance_id] = ( + _AuthRequest(expire_time=now + 10.0, error=None, token=None) + ) + with self.primary: + plus.cloud.send_message_cb( + bacommon.cloud.AuthRequestMessage(global_app_instance_id), + on_response=partial( + self._on_auth_request_response, auth_request + ), + ) + + # If we found results, return them. + if auth_request is None: + return None + if auth_request.error is not None: + assert auth_request.token is None + return (False, auth_request.error) + if auth_request.token is not None: + assert auth_request.error is None + return (True, auth_request.token) + # No error or token; its still in flight. + return None + + def _on_auth_request_response( + self, + auth_request: _AuthRequest, + response: bacommon.cloud.AuthRequestResponse | Exception, + ) -> None: + assert _babase.in_logic_thread() + + assert auth_request.error is None + assert auth_request.token is None + + if isinstance(response, Exception): + auth_request.error = 'An error has occurred.' + else: + # print('SETTING AUTH RESPONSE') + auth_request.error = response.error + auth_request.token = response.token + # Make sure this sticks around for long enough to complete + # the connection. + auth_request.expire_time = time.monotonic() + 10.0 + @staticmethod def _hashstr(val: str) -> str: md5 = hashlib.md5() @@ -501,3 +583,10 @@ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: This allows cloud messages to be sent on our behalf. """ + + +@dataclass +class _AuthRequest: + expire_time: float + error: str | None + token: str | None diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py index 8ac017773..3807b8849 100644 --- a/src/assets/ba_data/python/babase/_app.py +++ b/src/assets/ba_data/python/babase/_app.py @@ -242,7 +242,6 @@ def asyncio_loop(self) -> asyncio.AbstractEventLoop: loop. Hopefully this situation will be improved in the future with a unified event loop. """ - assert _babase.in_logic_thread() assert self._asyncio_loop is not None return self._asyncio_loop diff --git a/src/assets/ba_data/python/babase/_appconfig.py b/src/assets/ba_data/python/babase/_appconfig.py index 56f271a37..89716b4cd 100644 --- a/src/assets/ba_data/python/babase/_appconfig.py +++ b/src/assets/ba_data/python/babase/_appconfig.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from typing import Any -_g_pending_apply = False # pylint: disable=invalid-name +_g_pending_apply = False class AppConfig(dict): diff --git a/src/assets/ba_data/python/babase/_asyncio.py b/src/assets/ba_data/python/babase/_asyncio.py index 3d4901286..89f9ae7ba 100644 --- a/src/assets/ba_data/python/babase/_asyncio.py +++ b/src/assets/ba_data/python/babase/_asyncio.py @@ -23,8 +23,8 @@ import babase # Our timer and event loop for the ballistica logic thread. -_asyncio_timer: babase.AppTimer | None = None -_asyncio_event_loop: asyncio.AbstractEventLoop | None = None +_g_asyncio_timer: babase.AppTimer | None = None +_g_asyncio_event_loop: asyncio.AbstractEventLoop | None = None DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1' @@ -46,12 +46,12 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: except RuntimeError: pass - global _asyncio_event_loop - _asyncio_event_loop = asyncio.new_event_loop() - _asyncio_event_loop.set_default_executor(babase.app.threadpool) + global _g_asyncio_event_loop + _g_asyncio_event_loop = asyncio.new_event_loop() + _g_asyncio_event_loop.set_default_executor(babase.app.threadpool) # Try to avoid reference loops from exceptions. - _asyncio_event_loop.set_exception_handler(_exception_handler) + _g_asyncio_event_loop.set_exception_handler(_exception_handler) # Ideally we should integrate asyncio into our C++ Thread class's # low level event loop so that asyncio timers/sockets/etc. could @@ -62,10 +62,10 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: # See https://stackoverflow.com/questions/29782377/ # is-it-possible-to-run-only-a-single-step-of-the-asyncio-event-loop def run_cycle() -> None: - assert _asyncio_event_loop is not None - _asyncio_event_loop.call_soon(_asyncio_event_loop.stop) + assert _g_asyncio_event_loop is not None + _g_asyncio_event_loop.call_soon(_g_asyncio_event_loop.stop) starttime = time.monotonic() if DEBUG_TIMING else 0 - _asyncio_event_loop.run_forever() + _g_asyncio_event_loop.run_forever() endtime = time.monotonic() if DEBUG_TIMING else 0 # Let's aim to have nothing take longer than 1/120 of a second. @@ -79,21 +79,21 @@ def run_cycle() -> None: warn_time, ) - global _asyncio_timer - _asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) + global _g_asyncio_timer + _g_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) if bool(False): async def aio_test() -> None: print('TEST AIO TASK STARTING') - assert _asyncio_event_loop is not None - assert asyncio.get_running_loop() is _asyncio_event_loop + assert _g_asyncio_event_loop is not None + assert asyncio.get_running_loop() is _g_asyncio_event_loop await asyncio.sleep(2.0) print('TEST AIO TASK ENDING') - _testtask = _asyncio_event_loop.create_task(aio_test()) + _testtask = _g_asyncio_event_loop.create_task(aio_test()) - return _asyncio_event_loop + return _g_asyncio_event_loop def _exception_handler( diff --git a/src/assets/ba_data/python/babase/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py index 5021d570f..e5df00ddb 100644 --- a/src/assets/ba_data/python/babase/_hooks.py +++ b/src/assets/ba_data/python/babase/_hooks.py @@ -462,3 +462,36 @@ def copy_dev_console_history() -> None: _babase.clipboard_set_text('\n'.join(lines)) _babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0)) _babase.getsimplesound('gunCocking').play() + + +def v2_auth_request(global_app_instance_id: str) -> None | tuple[bool, str]: + """Kick off or process v2 auth requests. + + Return None if no results or (success, error/token) + """ + assert _babase.app.plus is not None + out: None | tuple[bool, str] = _babase.app.plus.accounts.auth_request( + global_app_instance_id + ) + return out + + +def v2_auth_data(token: str) -> None | tuple[str, str, dict]: + """Look up autheneticated v2 account data via a token.""" + assert _babase.in_logic_thread() + + classic = _babase.app.classic + if classic is None: + return None + + now = time.monotonic() + authdata = classic.v2_auth_datas.get(token) + if authdata is None or authdata.expire_time <= now: + return None + + # Success! + return ( + authdata.account_id, + authdata.account_tag, + authdata.player_profiles, + ) diff --git a/src/assets/ba_data/python/baclassic/_appsubsystem.py b/src/assets/ba_data/python/baclassic/_appsubsystem.py index 65381ddc2..9f7ac8392 100644 --- a/src/assets/ba_data/python/baclassic/_appsubsystem.py +++ b/src/assets/ba_data/python/baclassic/_appsubsystem.py @@ -6,10 +6,12 @@ from __future__ import annotations +import time import random import logging import weakref -from typing import TYPE_CHECKING, override, assert_never +from dataclasses import dataclass +from typing import TYPE_CHECKING, override, assert_never, final from efro.dataclassio import dataclass_from_dict import babase @@ -26,7 +28,8 @@ from baclassic import _input if TYPE_CHECKING: - from typing import Callable, Any, Sequence + import datetime + from typing import Callable, Any, Sequence, Awaitable import bacommon.classic import bacommon.clienteffect as clfx @@ -39,16 +42,72 @@ class ClassicAppSubsystem(babase.AppSubsystem): - """Subsystem for classic functionality in the app. + """Subsystem for classic bombsquad functionality in the app. The single shared instance of this app can be accessed at - babase.app.classic. Note that it is possible for babase.app.classic to - be None if the classic package is not present, and code should handle - that case gracefully. + babase.app.classic. Note that it is possible for babase.app.classic + to be None if the classic package is not present, and futureproof + code should handle that case gracefully. """ # pylint: disable=too-many-public-methods + @dataclass + class V2AuthRequest: + """What is passed in to V2 auth handler.""" + + #: V2 account id of the connecting account (a-XXX). + account_id: str + + #: Globally unique tag of the connecting account. + account_tag: str + + #: When the connecting account was created. + account_create_time: datetime.datetime + + #: Total number of days the connecting account has been active. + account_total_active_days: int + + #: An abstract value generated from the connecting client's + #: app-instance-id. Can be used to identify repeat connection + #: attempts/etc. Note that this value changes each time the + #: client re-launches the app. + app_instance_signature: str + + #: An abstract value generated from the connecting client's ip + #: address. Can be used to identify repeat connection + #: attempts/etc. + address_signature: str + + #: An abstract value generated from the connecting client's + #: device. Can be used to identify repeat connection + #: attempts/etc. This value may change over time for a given + #: device but should be mostly constant. + device_signature: str + + @dataclass + class V2AuthResponse: + """What a V2 auth handler returns.""" + + #: Whether to allow this client to enter the server. + allow: bool + + #: A message to be shown to the client if allow is False. A + #: defualt rejection message will be shown if none is present + #: here. This will be translated using the 'serverResponses' + #: translation category so it can be good to use one of the + #: entries there if your server has multilingual users. + error_message: str | None = None + + @dataclass + class V2AuthData: + """Authenticated data we store for accepted clients.""" + + account_id: str + account_tag: str + player_profiles: dict + expire_time: float + from baclassic._music import MusicPlayMode def __init__(self) -> None: @@ -95,6 +154,21 @@ def __init__(self) -> None: # Server Mode. self.server: ServerController | None = None + #: V2 authentication handler. + #: + #: To customize who is allowed in your server, assign your own + #: custom handler function to this attribute. This will be called + #: for all connecting clients before they are allowed in the + #: game. Note that protocol must be set to 36 or newer and + #: authenticate_clients must be enabled. For servers you can set + #: those values in the server config. + self.v2_auth_handler: Callable[ + [ClassicAppSubsystem.V2AuthRequest], + Awaitable[ClassicAppSubsystem.V2AuthResponse], + ] = self.default_v2_auth_handler + self.v2_auth_datas: dict[str, ClassicAppSubsystem.V2AuthData] = {} + + # Logging/debugging. self.log_have_new = False self.log_upload_timer_started = False self.printed_live_object_warning = False @@ -129,8 +203,48 @@ def __init__(self) -> None: self.pro_sale_start_time: int | None = None self.pro_sale_start_val: int | None = None + async def default_v2_auth_handler( + self, request: V2AuthRequest + ) -> V2AuthResponse: + """Default auth handler function. Just allows everyone.""" + + # A custom handler would look at request here to determine + # whether to let this client in. + del request # Unused. + + return self.V2AuthResponse(allow=True) + + @final + async def run_v2_auth_handler( + self, request: V2AuthRequest, player_profiles: Any, token: str + ) -> V2AuthResponse: + """:meta private:""" + assert babase.in_logic_thread() + result = await self.v2_auth_handler(request) + + # If it was accepted, keep their auth data around just long enough + # for them to connect. + if result.allow: + now = time.monotonic() + self.v2_auth_datas[token] = self.V2AuthData( + account_id=request.account_id, + account_tag=request.account_tag, + player_profiles=player_profiles, + expire_time=now + 30.0, + ) + + # Lazily prune expired auth-data. + if any(a.expire_time <= now for a in self.v2_auth_datas.values()): + self.v2_auth_datas = { + k: v + for k, v in self.v2_auth_datas.items() + if v.expire_time > now + } + + return result + def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: - """(internal)""" + """:meta private:""" # If there's no main window up, just call immediately. if not babase.app.ui_v1.has_main_window(): @@ -174,7 +288,7 @@ def platform(self) -> str: return self._env['platform'] def scene_v1_protocol_version(self) -> int: - """(internal)""" + """:meta private:""" return bascenev1.protocol_version() @property @@ -464,7 +578,7 @@ def getmaps(self, playtype: str) -> list[str]: ) def game_begin_analytics(self) -> None: - """(internal)""" + """:meta private:""" from baclassic import _analytics _analytics.game_begin_analytics() @@ -656,11 +770,11 @@ def get_player_profile_colors( return bascenev1.get_player_profile_colors(profilename, profiles) def get_foreground_host_session(self) -> bascenev1.Session | None: - """(internal)""" + """:meta private:""" return bascenev1.get_foreground_host_session() def get_foreground_host_activity(self) -> bascenev1.Activity | None: - """(internal)""" + """:meta private:""" return bascenev1.get_foreground_host_activity() def value_test( @@ -669,26 +783,26 @@ def value_test( change: float | None = None, absolute: float | None = None, ) -> float: - """(internal)""" + """:meta private:""" return _baclassic.value_test(arg, change, absolute) def set_master_server_source(self, source: int) -> None: - """(internal)""" + """:meta private:""" bascenev1.set_master_server_source(source) def get_game_port(self) -> int: - """(internal)""" + """:meta private:""" return bascenev1.get_game_port() def v2_upgrade_window(self, login_name: str, code: str) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.v2upgrade import V2UpgradeWindow V2UpgradeWindow(login_name, code) def server_dialog(self, delay: float, data: dict[str, Any]) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.serverdialog import ( ServerDialogData, ServerDialogWindow, @@ -709,13 +823,13 @@ def server_dialog(self, delay: float, data: dict[str, Any]) -> None: ) def show_url_window(self, address: str) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.url import ShowURLWindow ShowURLWindow(address) def quit_window(self, quit_type: babase.QuitType) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.confirm import QuitWindow QuitWindow(quit_type) @@ -731,7 +845,7 @@ def tournament_entry_window( offset: tuple[float, float] = (0.0, 0.0), on_close_call: Callable[[], Any] | None = None, ) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.tournamententry import TournamentEntryWindow TournamentEntryWindow( @@ -745,7 +859,7 @@ def tournament_entry_window( ) def get_main_menu_session(self) -> type[bascenev1.Session]: - """(internal)""" + """:meta private:""" from bascenev1lib.mainmenu import MainMenuSession return MainMenuSession @@ -796,7 +910,7 @@ def preload_map_preview_media(self) -> None: logging.exception('Error preloading map preview media.') def party_icon_activate(self, origin: Sequence[float]) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.party import PartyWindow from babase import app @@ -817,7 +931,7 @@ def party_icon_activate(self, origin: Sequence[float]) -> None: self.party_window = weakref.ref(PartyWindow(origin=origin)) def request_main_ui(self) -> None: - """(internal)""" + """:meta private:""" from bauiv1lib.ingamemenu import InGameMenuWindow assert babase.app is not None diff --git a/src/assets/ba_data/python/baclassic/_servermode.py b/src/assets/ba_data/python/baclassic/_servermode.py index f8e87a99f..2b0c78c9b 100644 --- a/src/assets/ba_data/python/baclassic/_servermode.py +++ b/src/assets/ba_data/python/baclassic/_servermode.py @@ -108,6 +108,12 @@ def __init__(self, config: ServerConfig) -> None: self._playlist_fetch_got_response = False self._playlist_fetch_code = -1 + # We do most configuration *after* we've fetched playlists or + # whatever other prep stuff we need to do, but this we should + # set immediately; otherwise unauthenticated clients can sneak + # into auth-enabled servers while they're bootstrapping. + bascenev1.set_authenticate_clients(self._config.authenticate_clients) + # Now sit around doing any pre-launch prep such as waiting for # account sign-in or fetching playlists; this will kick off the # session once done. @@ -424,8 +430,6 @@ def _launch_server_session(self) -> None: classic.teams_series_length = self._config.teams_series_length classic.ffa_series_length = self._config.ffa_series_length - bascenev1.set_authenticate_clients(self._config.authenticate_clients) - bascenev1.set_enable_default_kick_voting( self._config.enable_default_kick_voting ) @@ -448,7 +452,6 @@ def _launch_server_session(self) -> None: bascenev1.set_player_rejoin_cooldown( self._config.player_rejoin_cooldown ) - bascenev1.set_max_players_override( self._config.session_max_players_override ) @@ -467,6 +470,6 @@ def _launch_server_session(self) -> None: bascenev1.new_host_session(sessiontype) # Run an access check if we're trying to make a public party. - if not self._ran_access_check and self._config.party_is_public: + if self._config.party_is_public and not self._ran_access_check: self._run_access_check() self._ran_access_check = True diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 45e0309cf..a9995ce46 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -57,7 +57,7 @@ # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 22712 +TARGET_BALLISTICA_BUILD = 22714 TARGET_BALLISTICA_VERSION = '1.7.61' diff --git a/src/assets/ba_data/python/baplus/_cloud.py b/src/assets/ba_data/python/baplus/_cloud.py index 83d527ac0..bdf2e0af5 100644 --- a/src/assets/ba_data/python/baplus/_cloud.py +++ b/src/assets/ba_data/python/baplus/_cloud.py @@ -332,6 +332,16 @@ def send_message_cb( ], ) -> None: ... + @overload + def send_message_cb( + self, + msg: bacommon.cloud.AuthRequestMessage, + on_response: Callable[ + [bacommon.cloud.AuthRequestResponse | Exception], + None, + ], + ) -> None: ... + def send_message_cb( self, msg: Message, diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py index aead594ed..0fa9ad875 100644 --- a/src/assets/ba_data/python/bascenev1/__init__.py +++ b/src/assets/ba_data/python/bascenev1/__init__.py @@ -86,6 +86,7 @@ emitfx, end_host_scanning, get_chat_messages, + get_client_ping, get_connection_to_host_info, get_connection_to_host_info_2, get_foreground_host_activity, @@ -329,6 +330,7 @@ 'GameResults', 'GameTip', 'get_chat_messages', + 'get_client_ping', 'get_connection_to_host_info', 'get_connection_to_host_info_2', 'get_default_free_for_all_playlist', diff --git a/src/assets/ba_data/python/bascenev1/_lobby.py b/src/assets/ba_data/python/bascenev1/_lobby.py index df73f3d0b..69814a54c 100644 --- a/src/assets/ba_data/python/bascenev1/_lobby.py +++ b/src/assets/ba_data/python/bascenev1/_lobby.py @@ -339,10 +339,10 @@ def _select_initial_profile(self) -> int: if inputdevice.is_controller_app and '_random' in profilenames: return profilenames.index('_random') - # If its a client connection, for now just force - # the account profile if possible.. (need to provide a - # way for clients to specify/remember their default - # profile on remote servers that do not already know them). + # If its a client connection, for now just force the account + # profile if possible. (need to provide a way for clients to + # specify/remember their default profile on remote servers that + # do not already know them). if inputdevice.is_remote_client and '__account__' in profilenames: return profilenames.index('__account__') diff --git a/src/assets/ba_data/python/bascenev1/_session.py b/src/assets/ba_data/python/bascenev1/_session.py index b93bb501f..b02183972 100644 --- a/src/assets/ba_data/python/bascenev1/_session.py +++ b/src/assets/ba_data/python/bascenev1/_session.py @@ -25,7 +25,7 @@ _g_player_rejoin_cooldown: float = 0.0 # overrides the session's decision of max_players -_max_players_override: int | None = None +_g_max_players_override: int | None = None def set_player_rejoin_cooldown(cooldown: float) -> None: @@ -36,8 +36,8 @@ def set_player_rejoin_cooldown(cooldown: float) -> None: def set_max_players_override(max_players: int | None) -> None: """Set the override for how many players can join a session""" - global _max_players_override # pylint: disable=global-statement - _max_players_override = max_players + global _g_max_players_override # pylint: disable=global-statement + _g_max_players_override = max_players class Session: @@ -176,8 +176,8 @@ def __init__( self.min_players = min_players self.max_players = ( max_players - if _max_players_override is None - else _max_players_override + if _g_max_players_override is None + else _g_max_players_override ) self.submit_score = submit_score diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py index e3887c9a8..8a06a83d3 100644 --- a/src/assets/ba_data/python/bauiv1/__init__.py +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -14,8 +14,6 @@ # pylint: disable=redefined-builtin -import logging - from babase import ( accountlog, AccountV2Handle, @@ -319,7 +317,7 @@ if __debug__: for _mdl in 'babase', '_babase': if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): - logging.warning( + balog.warning( '%s was imported before %s finished importing;' ' should not happen.', __name__, diff --git a/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py b/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py index 36d964f0f..621bda8bb 100644 --- a/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py +++ b/src/assets/ba_data/python/bauiv1lib/gather/nearbytab.py @@ -45,14 +45,17 @@ def __init__( bui.widget(edit=self._columnwidget, up_widget=tab_button) self._width = width self._last_selected_host: dict[str, Any] | None = None + self._last_scan: list[dict[str, str]] | None = None self._update_timer = bui.AppTimer( - 1.0, bui.WeakCallStrict(self.update), repeat=True + 1.0, bui.WeakCallStrict(self._update), repeat=True ) - # Go ahead and run a few *almost* immediately so we don't - # have to wait a second. - self.update() - bui.apptimer(0.25, bui.WeakCallStrict(self.update)) + # Run two cycles pretty immediately - this should send out a + # "who's there" and update the list with any immediate-ish + # results so we may not have to wait a second to see things + # appear. + self._update() + bui.apptimer(0.25, bui.WeakCallStrict(self._update)) def __del__(self) -> None: bs.end_host_scanning() @@ -74,23 +77,33 @@ def _on_activate(self, host: dict[str, Any]) -> None: bs.connect_to_party(host['address']) - def update(self) -> None: + def _update(self) -> None: """(internal)""" # In case our UI was killed from under us. if not self._columnwidget: - print( - f'ERROR: NetScanner running without UI at time {bui.apptime()}.' + bui.uilog.error( + 'nearbytab NetScanner running without UI at time %s.', + bui.apptime(), ) return + hosts = bs.host_scan_cycle() + + # If nothing has changed since our last run, do nothing. If we + # do redundant rebuilds then we are likely to lose some clicks + # due to rebuilding after a click starts but before it ends. + if hosts == self._last_scan: + return + + self._last_scan = hosts + t_scale = 1.6 for child in self._columnwidget.get_children(): child.delete() # Grab this now this since adding widgets will change it. last_selected_host = self._last_selected_host - hosts = bs.host_scan_cycle() for i, host in enumerate(hosts): txt3 = bui.textwidget( parent=self._columnwidget, diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc index 9503c1903..c23390d5b 100644 --- a/src/ballistica/base/base.cc +++ b/src/ballistica/base/base.cc @@ -4,6 +4,7 @@ #include #include +#include #include #include "ballistica/base/app_adapter/app_adapter.h" @@ -37,6 +38,7 @@ #include "ballistica/core/platform/core_platform.h" #include "ballistica/core/python/core_python.h" #include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/macros.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/math/vector4f.h" #include "ballistica/shared/python/python_command.h" @@ -586,38 +588,40 @@ void BaseFeatureSet::set_classic(ClassicSoftInterface* classic) { classic_soft_ = classic; } -auto BaseFeatureSet::GetAppInstanceUUID() -> const std::string& { - static std::string app_instance_uuid; - static bool have_app_instance_uuid = false; +auto BaseFeatureSet::LocalAppInstanceUUID() -> const std::string& { + if (!have_local_app_instance_uuid_) { + assert(g_base); + Python::ScopedInterpreterLock gil; + auto uuid{g_core->python->objs() + .Get(core::CorePython::ObjID::kUUIDStrCall) + .Call()}; + BA_PRECONDITION_FATAL(uuid.exists()); + local_app_instance_uuid_ = "lai-" + uuid.ValueAsString(); + assert(local_app_instance_uuid_.size() < 100); + have_local_app_instance_uuid_ = true; + } + return local_app_instance_uuid_; +} - if (!have_app_instance_uuid) { - if (g_base) { - Python::ScopedInterpreterLock gil; - auto uuid = g_core->python->objs() - .Get(core::CorePython::ObjID::kUUIDStrCall) - .Call(); - if (uuid.exists()) { - app_instance_uuid = uuid.ValueAsString(); - have_app_instance_uuid = true; - } - } - if (!have_app_instance_uuid) { - // As an emergency fallback simply use a single random number. We - // should probably simply disallow this before Python is up. - g_core->logging->Log(LogName::kBa, LogLevel::kWarning, - "GetSessionUUID() using rand fallback."); - srand(static_cast( - core::CorePlatform::TimeMonotonicMillisecs())); // NOLINT - app_instance_uuid = - std::to_string(static_cast(rand())); // NOLINT - have_app_instance_uuid = true; - } - if (app_instance_uuid.size() >= 100) { - g_core->logging->Log(LogName::kBa, LogLevel::kWarning, - "session id longer than it should be."); - } +auto BaseFeatureSet::GlobalAppInstanceUUID() -> std::optional { + std::scoped_lock lock(global_app_instance_uuid_lock_); + + // If we have a value but it is expired, clear it. + if (global_app_instance_uuid_.has_value() + && core::CorePlatform::TimeSinceEpochSeconds() + > global_app_instance_uuid_expire_time_) { + global_app_instance_uuid_.reset(); + global_app_instance_uuid_expire_time_ = 0.0; } - return app_instance_uuid; + + return global_app_instance_uuid_; +} + +void BaseFeatureSet::SetGlobalAppInstanceUUID(std::string value, + seconds_t expire_time) { + std::scoped_lock lock(global_app_instance_uuid_lock_); + global_app_instance_uuid_ = std::move(value); + global_app_instance_uuid_expire_time_ = expire_time; } void BaseFeatureSet::PlusDirectSendV1CloudLogs(const std::string& prefix, @@ -763,8 +767,8 @@ void BaseFeatureSet::DoV1CloudLog(const std::string& msg) { Plus()->DirectSendV1CloudLogs(logprefix, logsuffix, false, nullptr); } -void BaseFeatureSet::PushDevConsolePrintCall(const std::string& msg, - float scale, Vector4f color) { +void BaseFeatureSet::PushDevConsolePrintCall(std::string_view msg, float scale, + Vector4f color) { ui->PushDevConsolePrintCall(msg, scale, color); } diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h index b0a32bce1..4a4301edd 100644 --- a/src/ballistica/base/base.h +++ b/src/ballistica/base/base.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "ballistica/base/discord/discord.h" #include "ballistica/core/support/base_soft.h" @@ -720,15 +721,27 @@ class BaseFeatureSet : public FeatureSetNativeComponent, void set_classic(ClassicSoftInterface* classic); - /// Return a string that should be universally unique to this particular - /// running instance of the app. - auto GetAppInstanceUUID() -> const std::string&; + /// Generate/return a locally-generated string that should be universally + /// unique to this particular running instance of the app. It is + /// considered public and may be passed around as part of game joins/etc. + /// Be aware that it may be spoofed; use the global version if you need + /// more security. + auto LocalAppInstanceUUID() -> const std::string&; + + /// A universally-unique string generated by the cloud representing this + /// particular running instance of the app. Note that this value may be + /// delayed in appearing and may change if the app loses connectivity for + /// a while and then regains it. + auto GlobalAppInstanceUUID() -> std::optional; + + void SetGlobalAppInstanceUUID(std::string value, seconds_t expire_time); /// Does it appear that we are a blessed build with no known /// user-modifications? - /// Note that some corner cases (such as being called too early in the launch - /// process) may result in false negatives (saying we're *not* unmodified when - /// in reality we are unmodified). + /// + /// Note that some corner cases (such as being called too early in the + /// launch process) may result in false negatives (saying we're *not* + /// unmodified when in reality we are unmodified). auto IsUnmodifiedBlessedBuild() -> bool override; /// Return true if both babase and _babase modules have completed their @@ -768,7 +781,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent, -> PyObject* override; auto FeatureSetFromData(PyObject* obj) -> FeatureSetNativeComponent* override; void DoV1CloudLog(const std::string& msg) override; - void PushDevConsolePrintCall(const std::string& msg, float scale, + void PushDevConsolePrintCall(std::string_view msg, float scale, Vector4f color) override; auto GetPyExceptionType(PyExcType exctype) -> PyObject* override; auto PrintPythonStackTrace() -> bool override; @@ -885,11 +898,15 @@ class BaseFeatureSet : public FeatureSetNativeComponent, AppMode* app_mode_; PlusSoftInterface* plus_soft_{}; ClassicSoftInterface* classic_soft_{}; + std::string local_app_instance_uuid_; std::mutex shutdown_suppress_lock_; + std::mutex global_app_instance_uuid_lock_; /// Main thread informs logic thread when this changes, but then logic /// reads original value here set by main. need to be sure they never read /// stale values. std::atomic_bool app_active_{true}; + std::optional global_app_instance_uuid_; + seconds_t global_app_instance_uuid_expire_time_{}; int shutdown_suppress_count_{}; bool have_clipboard_is_supported_{}; bool clipboard_is_supported_{}; @@ -907,6 +924,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent, bool basn_log_behavior_{}; bool server_wrapper_managed_{}; bool config_and_state_writes_suppressed_{}; + bool have_local_app_instance_uuid_{}; }; } // namespace ballistica::base diff --git a/src/ballistica/base/python/base_python.h b/src/ballistica/base/python/base_python.h index 5621f3a9d..4213fcd8c 100644 --- a/src/ballistica/base/python/base_python.h +++ b/src/ballistica/base/python/base_python.h @@ -125,6 +125,8 @@ class BasePython { kAppPlatform, kAppVariantType, kAppVariant, + kV2AuthRequestCall, + kV2AuthDataCall, kLast // Sentinel; must be at end. }; diff --git a/src/ballistica/base/python/methods/python_methods_base_1.cc b/src/ballistica/base/python/methods/python_methods_base_1.cc index b00051723..0a0c9d865 100644 --- a/src/ballistica/base/python/methods/python_methods_base_1.cc +++ b/src/ballistica/base/python/methods/python_methods_base_1.cc @@ -468,7 +468,7 @@ static auto PyAppInstanceUUID(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist))) { return nullptr; } - return PyUnicode_FromString(g_base->GetAppInstanceUUID().c_str()); + return PyUnicode_FromString(g_base->LocalAppInstanceUUID().c_str()); BA_PYTHON_CATCH; } @@ -1948,7 +1948,6 @@ auto PythonMethodsBase1::GetMethods() -> std::vector { PyMacMusicAppPlayPlaylistDef, PyMacMusicAppGetPlaylistsDef, PyIsOSPlayingMusicDef, - // PyLifecycleLogDef, PyExecArgDef, PyOnAppRunningDef, PyOnInitialAppModeSetDef, diff --git a/src/ballistica/base/support/app_config.cc b/src/ballistica/base/support/app_config.cc index 1f0ef850c..36a40121a 100644 --- a/src/ballistica/base/support/app_config.cc +++ b/src/ballistica/base/support/app_config.cc @@ -2,6 +2,7 @@ #include "ballistica/base/support/app_config.h" +#include #include #include @@ -9,6 +10,7 @@ #include "ballistica/core/core.h" #include "ballistica/core/platform/core_platform.h" #include "ballistica/shared/ballistica.h" +#include "ballistica/shared/buildconfig/buildconfig_common.h" namespace ballistica::base { @@ -206,8 +208,16 @@ void AppConfig::SetupEntries_() { int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort); int_entries_[IntID::kMaxFPS] = IntEntry("Max FPS", 60); - int_entries_[IntID::kSceneV1HostProtocol] = - IntEntry("SceneV1 Host Protocol", 33); + + // TEMP - forcing protocol 36 while I test v2 auth. + if (g_buildconfig.headless_build() && explicit_bool(false)) { + int_entries_[IntID::kSceneV1HostProtocol] = + IntEntry("SceneV1 Host Protocol", 36); + printf("TEMP DOING PROTOCOL 36 DEFAULT!!!\n"); + } else { + int_entries_[IntID::kSceneV1HostProtocol] = + IntEntry("SceneV1 Host Protocol", 33); + } bool_entries_[BoolID::kTouchControlsSwipeHidden] = BoolEntry("Touch Controls Swipe Hidden", false); diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc index f4a9b8f87..975b6752d 100644 --- a/src/ballistica/base/ui/dev_console.cc +++ b/src/ballistica/base/ui/dev_console.cc @@ -1345,7 +1345,7 @@ void DevConsole::SubmitPythonCommand_(const std::string& command) { if (cmd.CanEval()) { auto obj = cmd.Eval(true, nullptr, nullptr); if (obj.exists() && obj.get() != Py_None) { - Print(obj.Repr(), 1.0f, kVector4f1); + Print(obj.Repr().c_str(), 1.0f, kVector4f1); } } else { // Not eval-able; just exec it. @@ -1410,9 +1410,11 @@ void DevConsole::CycleState(bool backwards) { transition_start_ = g_base->logic->display_time(); } -void DevConsole::Print(const std::string& s_in, float scale, Vector4f color) { +void DevConsole::Print(const char* s_in, float scale, Vector4f color) { assert(g_base->InLogicThread()); - std::string s = Utils::GetValidUTF8(s_in.c_str(), "cspr"); + + std::string s = Utils::GetValidUTF8(s_in, "cspr"); + std::vector broken_up; g_base->text_graphics->BreakUpString( s.c_str(), kDevConsoleStringBreakUpSize / scale, &broken_up); diff --git a/src/ballistica/base/ui/dev_console.h b/src/ballistica/base/ui/dev_console.h index ce9fca178..aa6c63a76 100644 --- a/src/ballistica/base/ui/dev_console.h +++ b/src/ballistica/base/ui/dev_console.h @@ -38,7 +38,7 @@ class DevConsole { auto PasteFromClipboard() -> bool; /// Print text to the console. - void Print(const std::string& s_in, float scale, Vector4f color); + void Print(const char* s_in, float scale, Vector4f color); void Draw(FrameDef* frame_def); void ApplyAppConfig(); diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc index a6cafd059..6486ac32b 100644 --- a/src/ballistica/base/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -722,7 +722,7 @@ void UI::SetUIDelegate(base::UIDelegateInterface* delegate) { } } -void UI::PushDevConsolePrintCall(const std::string& msg, float scale, +void UI::PushDevConsolePrintCall(std::string_view msg, float scale, Vector4f color) { // Completely ignore this stuff in headless mode. if (g_core->HeadlessMode()) { @@ -730,10 +730,13 @@ void UI::PushDevConsolePrintCall(const std::string& msg, float scale, } // If our event loop AND console are up and running, ship it off to be // printed. Otherwise store it for the console to grab when it's ready. + // + // IMPORTANT: We're holding a string_view here so we need to copy it into + // a string to keep it valid when its called later. if (auto* event_loop = g_base->logic->event_loop()) { if (dev_console_ != nullptr) { - event_loop->PushCall([this, msg, scale, color] { - dev_console_->Print(msg, scale, color); + event_loop->PushCall([this, msg_s = std::string(msg), scale, color] { + dev_console_->Print(msg_s.c_str(), scale, color); }); return; } @@ -758,7 +761,7 @@ void UI::OnAssetsAvailable() { // Print any messages that have built up. if (!dev_console_startup_messages_.empty()) { for (auto&& entry : dev_console_startup_messages_) { - dev_console_->Print(std::get<0>(entry), std::get<1>(entry), + dev_console_->Print(std::get<0>(entry).c_str(), std::get<1>(entry), std::get<2>(entry)); } dev_console_startup_messages_.clear(); diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h index 548f28eca..c8af2f984 100644 --- a/src/ballistica/base/ui/ui.h +++ b/src/ballistica/base/ui/ui.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "ballistica/base/graphics/support/frame_def.h" @@ -138,7 +139,7 @@ class UI { auto* dev_console() const { return dev_console_; } - void PushDevConsolePrintCall(const std::string& msg, float scale, + void PushDevConsolePrintCall(std::string_view msg, float scale, Vector4f color); auto* delegate() const { return delegate_; } diff --git a/src/ballistica/classic/support/classic_app_mode.cc b/src/ballistica/classic/support/classic_app_mode.cc index 40a1efc7a..2fd26fef8 100644 --- a/src/ballistica/classic/support/classic_app_mode.cc +++ b/src/ballistica/classic/support/classic_app_mode.cc @@ -90,7 +90,7 @@ bool ClassicAppMode::IsInMainMenu() const { return (hostsession && hostsession->is_main_menu()); } -static ClassicAppMode* g_scene_v1_app_mode{}; +static ClassicAppMode* g_classic_app_mode{}; void ClassicAppMode::OnActivate() { assert(g_base->InLogicThread()); @@ -363,7 +363,7 @@ void ClassicAppMode::HostScanCycle() { std::scoped_lock lock(scan_results_mutex_); // Ignore if it looks like its us. - if (id != g_base->GetAppInstanceUUID()) { + if (id != g_base->LocalAppInstanceUUID()) { std::string key = id; auto i = scan_results_.find(key); @@ -443,8 +443,8 @@ auto ClassicAppMode::GetActive() -> ClassicAppMode* { // keep in mind that app-mode may change under them. // Otherwise return our singleton only if it is current. - if (g_base->app_mode() == g_scene_v1_app_mode) { - return g_scene_v1_app_mode; + if (g_base->app_mode() == g_classic_app_mode) { + return g_classic_app_mode; } return nullptr; } @@ -491,10 +491,10 @@ auto ClassicAppMode::GetActiveOrFatal() -> ClassicAppMode* { auto ClassicAppMode::GetSingleton() -> ClassicAppMode* { assert(g_base->InLogicThread()); - if (g_scene_v1_app_mode == nullptr) { - g_scene_v1_app_mode = new ClassicAppMode(); + if (g_classic_app_mode == nullptr) { + g_classic_app_mode = new ClassicAppMode(); } - return g_scene_v1_app_mode; + return g_classic_app_mode; } ClassicAppMode::ClassicAppMode() @@ -1086,14 +1086,15 @@ auto ClassicAppMode::GetDisplayPing() -> std::optional { void ClassicAppMode::CleanUpBeforeConnectingToHost() { // We can't have connected clients and a host-connection at the same time. - // Make a minimal attempt to disconnect any client connections we have, but - // get them off the list immediately. + // Make a minimal attempt to disconnect any client connections we have, + // but get them off the list immediately. + // // FIXME: Should we have a 'purgatory' for dying client connections?.. // (they may not get the single 'go away' packet we send here) connections_->ForceDisconnectClients(); - // Also make sure our public party state is off; this will inform the server - // that it should not be handing out our address to anyone. + // Also make sure our public party state is off; this will inform the + // server that it should not be handing out our address to anyone. SetPublicPartyEnabled(false); } @@ -1550,7 +1551,7 @@ void ClassicAppMode::HandleGameQuery(const char* buffer, size_t size, // version, our unique-app-instance-id, and our player_spec. char msg[400]; - std::string usid = g_base->GetAppInstanceUUID(); + std::string usid = g_base->LocalAppInstanceUUID(); std::string player_spec_string; // If we're signed in, send our account spec. Otherwise just send a diff --git a/src/ballistica/classic/support/classic_app_mode.h b/src/ballistica/classic/support/classic_app_mode.h index eafb58a2a..dd09ffb4a 100644 --- a/src/ballistica/classic/support/classic_app_mode.h +++ b/src/ballistica/classic/support/classic_app_mode.h @@ -158,6 +158,14 @@ class ClassicAppMode : public base::AppMode { void set_require_client_authentication(bool enable) { require_client_authentication_ = enable; } + // void set_client_authentication_version(int version) { + // assert(version == 1 || version == 2); + // client_authentication_version_ = version; + // } + auto client_authentication_version() const { + assert(host_protocol_version_ != -1); + return host_protocol_version_ >= 36 ? 2 : 1; + } auto IsPlayerBanned(const scene_v1::PlayerSpec& spec) -> bool; void BanPlayer(const scene_v1::PlayerSpec& spec, millisecs_t duration); void OnAppStart() override; diff --git a/src/ballistica/core/logging/logging.cc b/src/ballistica/core/logging/logging.cc index 49baca3ac..56ffef2e7 100644 --- a/src/ballistica/core/logging/logging.cc +++ b/src/ballistica/core/logging/logging.cc @@ -14,8 +14,8 @@ namespace ballistica::core { int g_early_v1_cloud_log_writes{10}; -void Logging::EmitLog(const std::string& name, LogLevel level, double timestamp, - const std::string& msg) { +void Logging::EmitLog(std::string_view name, LogLevel level, double timestamp, + std::string_view msg) { assert(g_base_soft); // Print to the dev console. if (name == "stdout" || name == "stderr") { @@ -48,7 +48,8 @@ void Logging::EmitLog(const std::string& name, LogLevel level, double timestamp, } char prestr[256]; - snprintf(prestr, sizeof(prestr), "%.3f %s", rel_time, name.c_str()); + snprintf(prestr, sizeof(prestr), "%.3f %.*s", rel_time, + static_cast(name.size()), name.data()); g_base_soft->PushDevConsolePrintCall("", 0.3f, kVector4f1); g_base_soft->PushDevConsolePrintCall( prestr, 0.75f, @@ -95,7 +96,7 @@ void Logging::V1CloudLog(const std::string& msg) { } } -void Logging::Log_(LogName name, LogLevel level, const std::string& msg) { +void Logging::Log_(LogName name, LogLevel level, const char* msg) { assert(g_core); // Wrappers calling us should be checking this. assert(LogLevelEnabled(name, level)); diff --git a/src/ballistica/core/logging/logging.h b/src/ballistica/core/logging/logging.h index 9b4698125..31c004991 100644 --- a/src/ballistica/core/logging/logging.h +++ b/src/ballistica/core/logging/logging.h @@ -22,7 +22,7 @@ class Logging { // Checking log-level here is more efficient than letting it happen in // Python land. if (LogLevelEnabled(name, level)) { - Log_(name, level, msg); + Log_(name, level, msg.c_str()); } } @@ -50,10 +50,10 @@ class Logging { void Log(LogName name, LogLevel level, C getmsgcall) { // Make sure provided lambdas return std::string; otherwise it would be // an easy mistake to return a char* to invalid function-local memory. - static_assert(std::is_same::value, - "Lambda must return std::string"); + // static_assert(std::is_same::value, + // "Lambda must return std::string"); if (LogLevelEnabled(name, level)) { - Log_(name, level, getmsgcall()); + Log_(name, level, getmsgcall().c_str()); } } @@ -80,8 +80,8 @@ class Logging { /// Send a log message to the in-app console, platform-specific logs, etc. /// This generally should not be called directly but instead wired up to /// log messages coming through the Python logging system. - void EmitLog(const std::string& name, LogLevel level, double timestamp, - const std::string& msg); + void EmitLog(std::string_view name, LogLevel level, double timestamp, + std::string_view msg); /// Write a message to the v1 cloud log. This is considered legacy and /// will be phased out eventually. @@ -106,7 +106,7 @@ class Logging { /// ever made will be routed through the app, visible in in-app consoles, /// etc. Note that direct Python logging calls or prints occurring before /// babase is imported may not be visible in the app for that same reason. - void Log_(LogName name, LogLevel level, const std::string& msg); + void Log_(LogName name, LogLevel level, const char* msg); LogLevel log_levels_[static_cast(LogName::kLast)]{}; bool did_put_v1_cloud_log_{}; diff --git a/src/ballistica/core/platform/apple/core_platform_apple.cc b/src/ballistica/core/platform/apple/core_platform_apple.cc index 1f4c1644d..cd0c0f94e 100644 --- a/src/ballistica/core/platform/apple/core_platform_apple.cc +++ b/src/ballistica/core/platform/apple/core_platform_apple.cc @@ -200,8 +200,8 @@ auto CorePlatformApple::IsRunningOnDesktop() -> bool { #endif } -void CorePlatformApple::EmitPlatformLog(const std::string& name, LogLevel level, - const std::string& msg) { +void CorePlatformApple::EmitPlatformLog(std::string_view name, LogLevel level, + std::string_view msg) { #if BA_XCODE_BUILD && !BA_HEADLESS_BUILD // HMM: do we want to use proper logging APIs here or simple printing? diff --git a/src/ballistica/core/platform/apple/core_platform_apple.h b/src/ballistica/core/platform/apple/core_platform_apple.h index 3e09105c4..57268cbd8 100644 --- a/src/ballistica/core/platform/apple/core_platform_apple.h +++ b/src/ballistica/core/platform/apple/core_platform_apple.h @@ -25,8 +25,8 @@ class CorePlatformApple : public CorePlatform { auto DoHasTouchScreen() -> bool override; auto GetDefaultUIScale() -> UIScale override; auto IsRunningOnDesktop() -> bool override; - void EmitPlatformLog(const std::string& name, LogLevel level, - const std::string& msg) override; + void EmitPlatformLog(std::string_view name, LogLevel level, + std::string_view msg) override; void GetTextBoundsAndWidth(const std::string& text, Rect* r, float* width) override; void FreeTextTexture(void* tex) override; diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc index 9eddf6e24..68df2e740 100644 --- a/src/ballistica/core/platform/core_platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -549,8 +549,8 @@ auto CorePlatform::GetDefaultUIScale() -> UIScale { return UIScale::kLarge; } -void CorePlatform::EmitPlatformLog(const std::string& name, LogLevel level, - const std::string& msg) { +void CorePlatform::EmitPlatformLog(std::string_view name, LogLevel level, + std::string_view msg) { // Do nothing by default. } diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h index 7bef468bc..93d2d518e 100644 --- a/src/ballistica/core/platform/core_platform.h +++ b/src/ballistica/core/platform/core_platform.h @@ -84,8 +84,8 @@ class CorePlatform { /// those to log messages is handled at a higher level. Implementations should /// not use any Python functionality, as this may be called before Python is /// spun up or after it is finalized. - virtual void EmitPlatformLog(const std::string& name, LogLevel level, - const std::string& msg); + virtual void EmitPlatformLog(std::string_view name, LogLevel level, + std::string_view msg); #pragma mark ENVIRONMENT ------------------------------------------------------- diff --git a/src/ballistica/core/platform/windows/core_platform_windows.cc b/src/ballistica/core/platform/windows/core_platform_windows.cc index c127b377b..e95e8a5c5 100644 --- a/src/ballistica/core/platform/windows/core_platform_windows.cc +++ b/src/ballistica/core/platform/windows/core_platform_windows.cc @@ -48,11 +48,13 @@ #include "ballistica/core/core.h" #include "ballistica/core/logging/logging.h" +#include "ballistica/core/logging/logging_macros.h" #include "ballistica/shared/foundation/event_loop.h" #include "ballistica/shared/generic/native_stack_trace.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/networking/networking_sys.h" +// Make sure we're using unicode versions of things. #if !defined(UNICODE) || !defined(_UNICODE) #error Unicode not defined. #endif @@ -183,25 +185,73 @@ auto CorePlatformWindows::GetNativeStackTrace() -> NativeStackTrace* { return new WinStackTrace(this); } -// Convert a wide Unicode string to an UTF8 string. -auto CorePlatformWindows::UTF8Encode(const std::wstring& wstr) -> std::string { - if (wstr.empty()) return std::string(); - int size_needed = WideCharToMultiByte( - CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), NULL, 0, NULL, NULL); - std::string str(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), - &str[0], size_needed, NULL, NULL); - return str; +auto CorePlatformWindows::UTF8Encode(std::wstring_view wstr) -> std::string { + if (wstr.empty()) { + return std::string(); + } + + if (wstr.size() > static_cast(std::numeric_limits::max())) { + BA_LOG_ONCE(LogName::kBa, LogLevel::kCritical, + "UTF8Encode input too large."); + return std::string(); + } + + const auto* wdata = wstr.data(); + const int wlen = static_cast(wstr.size()); + + const int size_needed = WideCharToMultiByte(CP_UTF8, 0, wdata, wlen, nullptr, + 0, nullptr, nullptr); + if (size_needed <= 0) { + BA_LOG_ONCE(LogName::kBa, LogLevel::kCritical, + "UTF8Encode unexpected size_needed <= 0."); + return std::string(); // or throw/log + } + + std::string out(static_cast(size_needed), '\0'); + const int written = WideCharToMultiByte(CP_UTF8, 0, wdata, wlen, out.data(), + size_needed, nullptr, nullptr); + if (written != size_needed) { + BA_LOG_ONCE(LogName::kBa, LogLevel::kCritical, + "UTF8Encode incomplete conversion."); + return std::string(); // or handle mismatch + } + + return out; } // Convert an UTF8 string to a wide Unicode String. -auto CorePlatformWindows::UTF8Decode(const std::string& str) -> std::wstring { - if (str.empty()) return std::wstring(); - int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], - static_cast(str.size()), NULL, 0); - std::wstring wstr(size_needed, 0); - MultiByteToWideChar(CP_UTF8, 0, &str[0], static_cast(str.size()), - &wstr[0], size_needed); +auto CorePlatformWindows::UTF8Decode(std::string_view str) -> std::wstring { + if (str.empty()) { + return std::wstring(); + } + + // MultiByteToWideChar takes an int length; guard against overflow. + if (str.size() > static_cast(std::numeric_limits::max())) { + BA_LOG_ONCE(LogName::kBa, LogLevel::kCritical, + "UTF8Decode input too large."); + return std::wstring(); + } + + const auto* bytes = str.data(); + const int byte_count = static_cast(str.size()); + + const int size_needed = + MultiByteToWideChar(CP_UTF8, 0, bytes, byte_count, nullptr, 0); + if (size_needed <= 0) { + BA_LOG_ONCE(LogName::kBa, LogLevel::kCritical, + "UTF8Decode unexpected size_needed <= 0."); + return std::wstring(); + } + + std::wstring wstr(static_cast(size_needed), L'\0'); + const int converted = MultiByteToWideChar(CP_UTF8, 0, bytes, byte_count, + wstr.data(), size_needed); + if (converted != size_needed) { + BA_LOG_ONCE(LogName::kBa, LogLevel::kCritical, + "UTF8Decode incomplete conversion."); + return std::wstring(); + } + return wstr; } @@ -369,7 +419,7 @@ auto CorePlatformWindows::FOpen(const char* path, const char* mode) -> FILE* { void CorePlatformWindows::DoMakeDir(const std::string& dir, bool quiet) { std::wstring stemp = UTF8Decode(dir); - int result = CreateDirectory(stemp.c_str(), 0); + int result = CreateDirectoryW(stemp.c_str(), 0); if (result == 0) { DWORD err = GetLastError(); if (err != ERROR_ALREADY_EXISTS) { @@ -811,7 +861,7 @@ std::string CorePlatformWindows::DoGetDeviceName() { std::string device_name; wchar_t computer_name[256]; DWORD computer_name_size = 256; - int result = GetComputerName(computer_name, &computer_name_size); + int result = GetComputerNameW(computer_name, &computer_name_size); if (result != 0) { device_name = UTF8Encode(computer_name); if (device_name.size() != 0) { @@ -843,16 +893,21 @@ std::string CorePlatformWindows::DoGetDeviceDescription() { bool CorePlatformWindows::DoHasTouchScreen() { return false; } -void CorePlatformWindows::EmitPlatformLog(const std::string& name, - LogLevel level, - const std::string& msg) { - // Spit this out as a debug-string for when running from msvc. - OutputDebugString(UTF8Decode(msg).c_str()); +void CorePlatformWindows::EmitPlatformLog(std::string_view name, LogLevel level, + std::string_view msg) { + // Spit this out as a debug-string only if we're being debugged. + // + // Note that this only checks for the presence of a user-mode debugger; if + // we ever need to see this stuff in remote debugging or system-monitoring + // situations we'll need to check for that or force this to always run. + if (IsDebuggerPresent()) { + OutputDebugStringW(UTF8Decode(msg).c_str()); + } } auto CorePlatformWindows::DoGetDataDirectoryMonolithicDefault() -> std::string { wchar_t sz_file_name[MAX_PATH + 1]; - GetModuleFileName(nullptr, sz_file_name, MAX_PATH + 1); + GetModuleFileNameW(nullptr, sz_file_name, MAX_PATH + 1); wchar_t* last_slash = nullptr; for (wchar_t* s = sz_file_name; *s != 0; ++s) { if (*s == '\\') { diff --git a/src/ballistica/core/platform/windows/core_platform_windows.h b/src/ballistica/core/platform/windows/core_platform_windows.h index 27e7f3dd2..b8fbc4308 100644 --- a/src/ballistica/core/platform/windows/core_platform_windows.h +++ b/src/ballistica/core/platform/windows/core_platform_windows.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "ballistica/core/platform/core_platform.h" @@ -20,8 +21,8 @@ class CorePlatformWindows : public CorePlatform { public: CorePlatformWindows(); - static auto UTF8Encode(const std::wstring& wstr) -> std::string; - static auto UTF8Decode(const std::string& str) -> std::wstring; + static auto UTF8Encode(std::wstring_view str) -> std::string; + static auto UTF8Decode(std::string_view str) -> std::wstring; auto GetNativeStackTrace() -> NativeStackTrace* override; auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; } @@ -43,8 +44,8 @@ class CorePlatformWindows : public CorePlatform { auto DoGetDeviceName() -> std::string override; auto DoGetDeviceDescription() -> std::string override; auto DoHasTouchScreen() -> bool override; - void EmitPlatformLog(const std::string& name, LogLevel level, - const std::string& msg) override; + void EmitPlatformLog(std::string_view name, LogLevel level, + std::string_view msg) override; void SetEnv(const std::string& name, const std::string& value) override; auto GetEnv(const std::string& name) -> std::optional override; auto GetIsStdinATerminal() -> bool override; diff --git a/src/ballistica/core/python/core_python.cc b/src/ballistica/core/python/core_python.cc index 94ec9eb09..aa0ba69dd 100644 --- a/src/ballistica/core/python/core_python.cc +++ b/src/ballistica/core/python/core_python.cc @@ -313,7 +313,7 @@ void CorePython::EnablePythonLoggingCalls() { python_logging_calls_enabled_ = true; for (auto&& entry : early_logs_) { LoggingCall(std::get<0>(entry), std::get<1>(entry), - "[HELD] " + std::get<2>(entry)); + ("[HELD] " + std::get<2>(entry)).c_str()); } early_logs_.clear(); } @@ -535,7 +535,7 @@ void CorePython::MonolithicModeBaEnvConfigure() { } void CorePython::LoggingCall(LogName logname, LogLevel loglevel, - const std::string& msg) { + const char* msg) { // If we're not yet sending logs to Python, store this one away until we // are. if (!python_logging_calls_enabled_) { @@ -557,7 +557,7 @@ void CorePython::LoggingCall(LogName logname, LogLevel loglevel, g_core->platform->EmitPlatformLog("root", LogLevel::kError, errmsg); g_core->platform->EmitPlatformLog("root", loglevel, msg); } - fprintf(stderr, "%s\n%s\n", errmsg, msg.c_str()); + fprintf(stderr, "%s\n%s\n", errmsg, msg); } return; } @@ -649,18 +649,17 @@ void CorePython::LoggingCall(LogName logname, LogLevel loglevel, fprintf(stderr, "Unexpected LogLevel %d\n", static_cast(loglevel)); break; } - PythonRef args( - Py_BuildValue("(Os)", objs().Get(loglevelobjid).get(), msg.c_str()), - PythonRef::kSteal); + PythonRef args(Py_BuildValue("(Os)", objs().Get(loglevelobjid).get(), msg), + PythonRef::kSteal); objs().Get(logcallobj).Call(args); } auto CorePython::WasModularMainCalled() -> bool { assert(!g_buildconfig.monolithic_build()); - // This gets called in modular builds before anything is inited, so we need - // to avoid using anything from g_core or whatnot here; only raw Python - // stuff. + // This gets called in modular builds before anything is inited, so we + // need to avoid using anything from g_core or whatnot here; only raw + // Python stuff. PyObject* baenv = PyImport_ImportModule("baenv"); if (!baenv) { @@ -697,9 +696,9 @@ auto CorePython::WasModularMainCalled() -> bool { auto CorePython::FetchPythonArgs(std::vector* buffer) -> std::vector { - // This gets called in modular builds before anything is inited, so we need - // to avoid using anything from g_core or whatnot here; only raw Python - // stuff. + // This gets called in modular builds before anything is inited, so we + // need to avoid using anything from g_core or whatnot here; only raw + // Python stuff. assert(buffer && buffer->empty()); PyObject* sys = PyImport_ImportModule("sys"); diff --git a/src/ballistica/core/python/core_python.h b/src/ballistica/core/python/core_python.h index 82d1d4bda..d85f39477 100644 --- a/src/ballistica/core/python/core_python.h +++ b/src/ballistica/core/python/core_python.h @@ -85,7 +85,7 @@ class CorePython { /// Can be called from any thread at any time. If called before Python /// logging is available, logs locally using Logging::EmitPlatformLog() /// (with an added warning). - void LoggingCall(LogName logname, LogLevel loglevel, const std::string& msg); + void LoggingCall(LogName logname, LogLevel loglevel, const char* msg); void ImportPythonObjs(); void VerifyPythonEnvironment(); void SoftImportBase(); diff --git a/src/ballistica/core/support/base_soft.h b/src/ballistica/core/support/base_soft.h index 97983c33a..efca72c40 100644 --- a/src/ballistica/core/support/base_soft.h +++ b/src/ballistica/core/support/base_soft.h @@ -4,6 +4,7 @@ #define BALLISTICA_CORE_SUPPORT_BASE_SOFT_H_ #include +#include #include "ballistica/shared/foundation/feature_set_native_component.h" #include "ballistica/shared/math/vector3f.h" @@ -36,7 +37,7 @@ class BaseSoftInterface { virtual auto FeatureSetFromData(PyObject* obj) -> FeatureSetNativeComponent* = 0; virtual void DoV1CloudLog(const std::string& msg) = 0; - virtual void PushDevConsolePrintCall(const std::string& msg, float scale, + virtual void PushDevConsolePrintCall(std::string_view msg, float scale, Vector4f color) = 0; virtual auto GetPyExceptionType(PyExcType exctype) -> PyObject* = 0; virtual auto PrintPythonStackTrace() -> bool = 0; diff --git a/src/ballistica/scene_v1/connection/connection_set.cc b/src/ballistica/scene_v1/connection/connection_set.cc index ca2573221..02e6f99d6 100644 --- a/src/ballistica/scene_v1/connection/connection_set.cc +++ b/src/ballistica/scene_v1/connection/connection_set.cc @@ -616,15 +616,15 @@ void ConnectionSet::HandleIncomingUDPPacket(const std::vector& data_in, } case BA_PACKET_CLIENT_REQUEST: { if (data_size > 4) { - // Bytes 2 and 3 are their protocol ID, byte 4 is request ID, the rest - // is session-id. + // Bytes 2 and 3 are their protocol ID, byte 4 is request ID, the + // rest is their app-instance-uuid. uint16_t protocol_id; memcpy(&protocol_id, data + 1, 2); uint8_t request_id = data[3]; - // They also send us their session-ID which should - // be completely unique to them; we can use this to lump client - // requests together and such. + // They also send us their app-instance-uuid which should be + // completely unique to them (though spoofable since it is public). + // We can use this to lump client requests together and such. std::vector client_instance_buffer(data_size - 4 + 1); memcpy(&(client_instance_buffer[0]), data + 4, data_size - 4); client_instance_buffer[data_size - 4] = 0; // terminate string @@ -632,11 +632,12 @@ void ConnectionSet::HandleIncomingUDPPacket(const std::vector& data_in, if (static_cast(connections_to_clients_.size() + 1) >= appmode->public_party_max_size()) { - // If we've reached our party size limit (including ourself in that - // count), reject. + // If we've reached our party size limit (including ourself in + // that count), reject. - // Newer version have a specific party-full message; send that first - // but also follow up with a generic deny message for older clients. + // Newer version have a specific party-full message; send that + // first but also follow up with a generic deny message for older + // clients. g_base->network_writer->PushSendToCall( {BA_PACKET_CLIENT_DENY_PARTY_FULL, request_id}, addr); @@ -692,9 +693,8 @@ void ConnectionSet::HandleIncomingUDPPacket(const std::vector& data_in, connections_to_clients_[client_id] = connection_to_client; } - // If we got to this point, regardless of whether - // we already had a connection or not, tell them - // they're accepted. + // If we got to this point, regardless of whether we already had a + // connection or not, tell them they're accepted. std::vector msg_out(3); msg_out[0] = BA_PACKET_CLIENT_ACCEPT; assert(connection_to_client->id() < 256); diff --git a/src/ballistica/scene_v1/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc index 8afbfe4d5..255bbd7c5 100644 --- a/src/ballistica/scene_v1/connection/connection_to_client.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -36,14 +37,14 @@ ConnectionToClient::ConnectionToClient(int id) : id_(id), protocol_version_{ classic::ClassicAppMode::GetSingleton()->host_protocol_version()} { - // We calc this once just in case it changes on our end - // (the client uses it for their verification hash so we need to - // ensure it stays consistent). + // We calc this once just in case it changes on our end (the client uses + // it for their verification hash so we need to ensure it stays + // consistent). our_handshake_player_spec_str_ = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); - // On newer protocols we include an extra salt value - // to ensure the hash the client generates can't be recycled. + // On newer protocols we include an extra salt value to ensure the hash + // the client generates can't be recycled. if (explicit_bool(protocol_version() >= 33)) { our_handshake_salt_ = std::to_string(rand()); // NOLINT } @@ -63,10 +64,9 @@ void ConnectionToClient::SetController(ClientControllerInterface* c) { // If we've got a new one, connect it. if (c) { controller_ = c; - // We automatically push a session reset command before turning - // a client connection over to a new controller. - // The previous client may not have cleaned up after itself - // in cases such as truncated replays, etc. + // We automatically push a session reset command before turning a client + // connection over to a new controller. The previous client may not have + // cleaned up after itself in cases such as truncated replays, etc. SendReliableMessage(std::vector(1, BA_MESSAGE_SESSION_RESET)); controller_->OnClientConnected(this); } @@ -76,14 +76,14 @@ ConnectionToClient::~ConnectionToClient() { // If we've got a controller, disconnect from it. SetController(nullptr); - // If we had made any input-devices, they're just pointers that - // we have to pass along to g_input to delete for us. + // If we had made any input-devices, they're just pointers that we have to + // pass along to g_input to delete for us. for (auto&& i : client_input_devices_) { g_base->input->RemoveInputDevice(i.second, false); } - // If they had been announced as connected, announce their departure. - // It's also expected our app mode may no longer be active here; that's ok. + // If they had been announced as connected, announce their departure. It's + // also expected our app mode may no longer be active here; that's ok. auto* appmode = classic::ClassicAppMode::GetActive(); if (appmode && can_communicate() && appmode->ShouldAnnouncePartyJoinsAndLeaves()) { @@ -101,12 +101,28 @@ void ConnectionToClient::Update() { millisecs_t real_time = g_core->AppTimeMillisecs(); - // If we're waiting for handshake response still, keep sending out handshake - // attempts. + // If we're doing v2 auth but haven't gotten our global-app-instance-uuid + // yet, hold off on handshake sends. + auto* appmode = classic::ClassicAppMode::GetActive(); + if (!appmode) { + return; + } + auto doing_v2_auth{appmode->require_client_authentication() + && appmode->client_authentication_version() == 2}; + + if (doing_v2_auth && !g_base->GlobalAppInstanceUUID().has_value()) { + BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, + "V2 client auth enabled but still waiting on " + "global-app-instance-uuid..."); + return; + } + + // If we're waiting for handshake response still, keep sending out + // handshake attempts. if (!can_communicate() && real_time - last_hand_shake_send_time_ > 1000) { // In newer protocols we embed a json dict as the second part of the - // handshake packet; this way we can evolve the protocol more - // easily in the future. + // handshake packet; this way we can evolve the protocol more easily in + // the future. if (explicit_bool(protocol_version() >= 33)) { // Construct a json dict with our player-spec-string as one element. JsonDict dict; @@ -115,6 +131,13 @@ void ConnectionToClient::Update() { // We also add our random salt for hashing. dict.AddString("l", our_handshake_salt_); + // If we're doing V2 auth, bundle our global-app-instance-uuid so they + // can ask the cloud to send us their credentials. + auto app_uuid{g_base->GlobalAppInstanceUUID()}; + if (doing_v2_auth && app_uuid.has_value()) { + dict.AddString("v2a", *app_uuid); + } + std::string out = dict.PrintUnformatted(); std::vector data(3 + out.size()); data[0] = BA_SCENEPACKET_HANDSHAKE; @@ -123,9 +146,10 @@ void ConnectionToClient::Update() { memcpy(data.data() + 3, out.c_str(), out.size()); SendGamePacket(data); } else { - // (KILL THIS WHEN kProtocolVersionClientMin >= 33) - // on older protocols, we simply embedded our spec-string as the second - // part of the handshake packet + // (KILL THIS WHEN kProtocolVersionClientMin >= 33). + // + // On older protocols, we simply embedded our spec-string as the + // second part of the handshake packet. std::vector data(3 + our_handshake_player_spec_str_.size()); data[0] = BA_SCENEPACKET_HANDSHAKE; uint16_t val = protocol_version(); @@ -160,6 +184,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { switch (data[0]) { case BA_SCENEPACKET_HANDSHAKE_RESPONSE: { + std::optional v2_auth_token; // We sent the client a handshake and they're responding. if (data.size() < 3) { BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, @@ -167,16 +192,25 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { return; } - // In newer builds we expect to be sent a json dict here; - // pull client's spec from that. + // In newer builds we expect to be sent a json dict here; pull + // client's spec from that. if (protocol_version() >= 33) { std::vector string_buffer(data.size() - 3 + 1); memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3); string_buffer[string_buffer.size() - 1] = 0; if (cJSON* handshake = cJSON_Parse(string_buffer.data())) { if (cJSON_IsObject(handshake)) { + // Grab V2 auth token if present. + if (cJSON* v2at = cJSON_GetObjectItem(handshake, "v2at")) { + if (cJSON_IsString(v2at)) { + v2_auth_token = v2at->valuestring; + } + } if (cJSON* pspec = cJSON_GetObjectItem(handshake, "s")) { if (cJSON_IsString(pspec)) { + // Set peer-spec to what they pass us (note this is + // untrusted). With v2 auth we'll override this further down + // when we look at their token. set_peer_spec(PlayerSpec(pspec->valuestring)); } else { BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, @@ -202,34 +236,106 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { } } else { // (KILL THIS WHEN kProtocolVersionClientMin >= 33) - // older versions only contained the client spec - // pull client's spec from the handshake packet.. + // + // Older versions only contained the client spec; pull client's spec + // from the handshake packet. std::vector string_buffer(data.size() - 3 + 1); memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3); string_buffer[string_buffer.size() - 1] = 0; set_peer_spec(PlayerSpec(&(string_buffer[0]))); } + // If we require v2 auth, look up their info using the token they + // passed. + auto doing_v2_auth{appmode->require_client_authentication() + && appmode->client_authentication_version() == 2}; + if (doing_v2_auth) { + if (!v2_auth_token.has_value()) { + // Because v2 auth requires protocol 36, no client should get to + // this point without knowing they need to provide us a token. + // Make noise if that does happen. + g_core->logging->Log( + LogName::kBaNetworking, LogLevel::kWarning, + "Rejecting a join attempt that lacks a V2 auth token; " + "this should not happen."); + Error(""); + return; + } else { + // printf("GOT TOKEN FROM JOINER: %s\n", v2_auth_token->c_str()); + + auto args = + PythonRef::Stolen(Py_BuildValue("(s)", v2_auth_token->c_str())); + auto result = g_base->python->objs() + .Get(base::BasePython::ObjID::kV2AuthDataCall) + .Call(args); + if (!result.exists()) { + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kError, + "Error looking up v2 auth data for joiner."); + Error(""); + return; + } + if (result.ValueIsNone()) { + // No auth found for this client (curious if this will happen + // enough to make warning overkill; can downgrade to debug if + // so). + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kWarning, + "Rejecting invalid v2 auth token."); + Error(""); + return; + } + auto valid_format{false}; + PythonRef account_id_ref; + PythonRef account_tag_ref; + PythonRef profiles_ref; + if (result.ValueIsSequence()) { + auto vals{result.ValueAsSequence()}; + if (vals.size() == 3 && vals[0].ValueIsString() + && vals[1].ValueIsString() && PyDict_Check(*vals[2])) { + valid_format = true; + account_id_ref = vals[0]; + account_tag_ref = vals[1]; + profiles_ref = vals[2]; + } + } + if (!valid_format) { + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kError, + "Invalid type returned from v2_auth_data."); + Error(""); + return; + } + + peer_public_account_id_ = account_id_ref.ValueAsString(); + + // Create a v2 account id spec for them. + // + // Verified account tag should always be simple ascii with no + // quotes or crazy stuff so we can just stuff it directly into a + // json str. + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "{\"n\":\"%s\",\"a\":\"V2\",\"sn\":\"\"}", + account_tag_ref.ValueAsString().c_str()); + set_peer_spec(PlayerSpec(buffer)); + + // printf("TODO: SET PEER PROFILES TO %s.\n", + // profiles_ref.Str().c_str()); + player_profiles_ = profiles_ref; + } + } // If they sent us a garbage player-spec, kick them right out. if (!peer_spec().valid()) { - g_core->logging->Log(LogName::kBaNetworking, LogLevel::kDebug, [] { - return std::string( - "Rejecting client for submitting invalid player-spec."); - }); + g_core->logging->Log( + LogName::kBaNetworking, LogLevel::kDebug, + "Rejecting client for submitting invalid player-spec."); Error(""); return; } - // FIXME: We should maybe set some sort of 'pending' peer-spec - // and fetch their actual info from the master-server. - // (or at least make that an option for internet servers) - - // Compare this against our blocked specs.. if there's a match, reject + // Compare this against our blocked specs. If there's a match, reject // them. if (appmode->IsPlayerBanned(peer_spec())) { - g_core->logging->Log(LogName::kBaNetworking, LogLevel::kDebug, [] { - return std::string("Rejecting join attempt by banned player."); - }); + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kDebug, + "Rejecting join attempt by banned player."); Error(""); return; } @@ -239,15 +345,15 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { memcpy(&val, data.data() + 1, sizeof(val)); if (val != protocol_version()) { // Depending on the connection type we may print the connection - // failure or not. (If we invited them it'd be good to know about the - // failure). + // failure or not. (If we invited them it'd be good to know about + // the failure). std::string s; if (ShouldPrintIncompatibleClientErrors()) { // If they get here, announce on the host that the client is // incompatible. UDP connections will get rejected during the - // connection attempt so this will only apply to things like Google - // Play invites where we probably want to be more verbose as - // to why the game just died. + // connection attempt so this will only apply to things like + // Google Play invites where we probably want to be more verbose + // as to why the game just died. s = g_base->assets->GetResourceString( "incompatibleVersionPlayerText"); Utils::StringReplaceOne(&s, "${NAME}", @@ -284,11 +390,12 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { g_core->AppTimeMillisecs()); // Added midway through protocol 29: + // // We now send a json dict of info about ourself first thing. This // gives us a nice open-ended way to expand functionality/etc. going - // forward. The other end will expect that this is the first reliable - // message they get; if something else shows up first they'll assume - // we're an old build and not sending this. + // forward. The other end will expect that this is the first + // reliable message they get; if something else shows up first + // they'll assume we're an old build and not sending this. { cJSON* info_dict = cJSON_CreateObject(); cJSON_AddItemToObject(info_dict, "b", @@ -323,8 +430,8 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { } } - // Update the game party roster and send it to all clients (including - // this new one). + // Update the game party roster and send it to all clients + // (including this new one). appmode->UpdateGameRoster(); // Lastly, we hand this connection over to whoever is currently @@ -344,6 +451,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { break; } } + void ConnectionToClient::Error(const std::string& msg) { // Take no further action at this time aside from printing it. // If we receive any more messages from the client we'll respond @@ -353,8 +461,8 @@ void ConnectionToClient::Error(const std::string& msg) { void ConnectionToClient::SendScreenMessage(const std::string& s, float r, float g, float b) { - // Older clients don't support the screen-message message, so in that case - // we just send it as a chat-message from . + // Older clients don't support the screen-message message, so in that + // case we just send it as a chat-message from . if (build_number() < 14248) { std::string value = g_base->assets->CompileResourceString(s); std::string our_spec_string = @@ -393,8 +501,8 @@ void ConnectionToClient::HandleMessagePacket( return; } - // If the first message we get is not client-info, it means we're talking to - // an older client that won't be sending us info. + // If the first message we get is not client-info, it means we're + // talking to an older client that won't be sending us info. if (!got_client_info_ && buffer[0] != BA_MESSAGE_CLIENT_INFO) { build_number_ = 0; got_client_info_ = true; @@ -443,8 +551,8 @@ void ConnectionToClient::HandleMessagePacket( Error(""); } - // Grab their token (we use this to ask the server for their v1 - // account info). + // Grab their token (we use this to ask the server for their + // v1 account info). cJSON* t = cJSON_GetObjectItem(info, "tk"); if (cJSON_IsString(t)) { token_ = t->valuestring; @@ -454,15 +562,21 @@ void ConnectionToClient::HandleMessagePacket( Error(""); } - // Newer clients also pass a peer-hash, which we can include with - // the token to allow the v1 server to better verify the client's - // identity. + // Newer clients also pass a peer-hash, which we can include + // with the token to allow the v1 server to better verify the + // client's identity. cJSON* ph = cJSON_GetObjectItem(info, "ph"); if (cJSON_IsString(ph)) { peer_hash_ = ph->valuestring; } - if (!token_.empty()) { - // Kick off a query to the master-server for this client's info. + auto doing_v2_auth{appmode->require_client_authentication() + && appmode->client_authentication_version() + == 2}; + + if (!token_.empty() && !doing_v2_auth) { + // If we're NOT doing v2 auth, kick off a query to the + // master-server for this client's info. + // // FIXME: we need to add retries for this in case of failure. g_base->Plus()->ClientInfoQuery( token_, our_handshake_player_spec_str_ + our_handshake_salt_, @@ -489,22 +603,45 @@ void ConnectionToClient::HandleMessagePacket( BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, "Ignoring invalid client-player-profiles-json msg."); } else { - // Only accept peer info if we've not gotten official info from - // the master server (and if we're allowing it in general). - if (!appmode->require_client_authentication() - && !got_info_from_master_server_) { - // Create a string from bytes 1+ of msg. - std::vector b2(buffer.size()); // Preallocate full space. - std::copy(buffer.begin() + 1, buffer.end(), b2.begin()); - b2.back() = 0; // Null terminate. - - PythonRef args(Py_BuildValue("(s)", b2.data()), PythonRef::kSteal); - PythonRef results = g_core->python->objs() - .Get(core::CorePython::ObjID::kJsonLoadsCall) - .Call(args); - if (results.exists()) { - player_profiles_ = results; + switch (appmode->client_authentication_version()) { + case 1: { + // Ok; doing old-school V1 auth. + // + // Only accept peer profiles if we're allowing that and have not + // gotten official ones through v1 client auth. + if (!appmode->require_client_authentication() + && !got_v1_auth_from_master_server_) { + // Create a string from bytes 1+ of msg. + std::vector b2(buffer.size()); // Preallocate full space. + std::copy(buffer.begin() + 1, buffer.end(), b2.begin()); + b2.back() = 0; // Null terminate. + + PythonRef args(Py_BuildValue("(s)", b2.data()), + PythonRef::kSteal); + PythonRef results = + g_core->python->objs() + .Get(core::CorePython::ObjID::kJsonLoadsCall) + .Call(args); + if (results.exists()) { + player_profiles_ = results; + } + } + break; } + case 2: { + // In client-auth version 2, profiles are sent to us by the + // cloud *before* the connection is allowed, so fully ignore + // anything that comes through here. But also clients should + // know not to bother sending us profiles so this should never + // happen. + BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, + "Got client-profiles message while v2-auth is enabled; " + "this should not happen."); + break; + } + default: + FatalError("Unexpected client-auth version."); + break; } } break; @@ -517,10 +654,10 @@ void ConnectionToClient::HandleMessagePacket( // ('u' prefixes before unicode and this and that) // Just gonna hope everyone is updated to a recent-ish version so // we don't get these. - // This might be a good argument to separate out the protocol versions - // we support for game streams vs client-connections. We could disallow - // connections to/from these older peers while still allowing old replays - // to play back. + // This might be a good argument to separate out the protocol + // versions we support for game streams vs client-connections. We + // could disallow connections to/from these older peers while still + // allowing old replays to play back. BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, "Received old pre-json player profiles msg; ignoring."); break; @@ -536,19 +673,20 @@ void ConnectionToClient::HandleMessagePacket( // If they exceed a certain amount in the last several seconds, // Institute a chat block. last_chat_times_.push_back(now); - uint32_t timeSample = 5000; - if (now >= timeSample) { + uint32_t time_sample = 5000; + if (now >= time_sample) { while (!last_chat_times_.empty() - && last_chat_times_[0] < now - timeSample) { + && last_chat_times_[0] < now - time_sample) { last_chat_times_.erase(last_chat_times_.begin()); } } - // If we require client-info and don't have it from this guy yet, + // If we require v1 client-info and don't have it from this guy yet, // ignore their chat messages (prevent bots from jumping in and // spamming before we can verify their identities) if (appmode->require_client_authentication() - && !got_info_from_master_server_) { + && appmode->client_authentication_version() < 2 + && !got_v1_auth_from_master_server_) { BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, "Ignoring chat message from peer with no client info."); SendScreenMessage(R"({"r":"loadingTryAgainText"})", 1, 0, 0); @@ -565,8 +703,8 @@ void ConnectionToClient::HandleMessagePacket( } else { // Send this along to all clients. - // *however* we want to ignore the player-spec that was included in - // the chat message and replace it with our own for this + // *however* we want to ignore the player-spec that was included + // in the chat message and replace it with our own for this // client-connection. if (buffer.size() > 3) { int spec_len = buffer[1]; @@ -592,9 +730,10 @@ void ConnectionToClient::HandleMessagePacket( } else if (kick_vote_in_progress && (!strcmp(b2.data(), "1") || !strcmp(b2.data(), "2"))) { - // Special case - if there's a kick vote going on, take '1' or - // '2' to be votes. - // TODO(ericf): Disable this based on build-numbers once we've + // Special case - if there's a kick vote going on, take + // '1' or '2' to be votes. + // TODO(ericf): Disable this based on build-numbers once + // we've // got GUI voting working. if (!kick_voted_) { kick_voted_ = true; @@ -603,8 +742,8 @@ void ConnectionToClient::HandleMessagePacket( SendScreenMessage(R"({"r":"votedAlreadyText"})", 1, 0, 0); } } else { - // Pass the message through any custom filtering we've got. - // If the filter tells us to ignore it, we're done. + // Pass the message through any custom filtering we've + // got. If the filter tells us to ignore it, we're done. std::string message = b2.data(); bool allow_message = g_scene_v1->python->FilterChatMessage(&message, id()); @@ -687,9 +826,9 @@ void ConnectionToClient::HandleMessagePacket( host_session->RemovePlayer(player); } } else { - BA_LOG_ONCE( - LogName::kBaNetworking, LogLevel::kWarning, - "Unable to get ClientInputDevice for remove-remote-player msg."); + BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, + "Unable to get ClientInputDevice for " + "remove-remote-player msg."); } } break; @@ -709,10 +848,10 @@ void ConnectionToClient::HandleMessagePacket( // It should have one of our special client delegates attached. auto* cid_d = dynamic_cast(&cid->delegate()); if (!cid_d) { - BA_LOG_ONCE( - LogName::kBaNetworking, LogLevel::kWarning, - "Can't get client-input-device-delegate in request-remote-player " - "msg."); + BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, + "Can't get client-input-device-delegate in " + "request-remote-player " + "msg."); break; } if (auto* hs = @@ -720,7 +859,8 @@ void ConnectionToClient::HandleMessagePacket( if (!cid->AttachedToPlayer()) { bool still_waiting_for_auth = (appmode->require_client_authentication() - && !got_info_from_master_server_); + && appmode->client_authentication_version() < 2 + && !got_v1_auth_from_master_server_); // If we're not allowing peer client-info and have yet to get // master-server info for this client, delay their join (we'll @@ -734,11 +874,11 @@ void ConnectionToClient::HandleMessagePacket( } else { // Either timed out or have info; let the request go through. if (still_waiting_for_auth) { - BA_LOG_ONCE( - LogName::kBaNetworking, LogLevel::kWarning, - "Allowing player-request without client\'s master-server " - "info (build " - + std::to_string(build_number_) + ")"); + BA_LOG_ONCE(LogName::kBaNetworking, LogLevel::kWarning, + "Allowing player-request without client\'s " + "master-server " + "info (build " + + std::to_string(build_number_) + ")"); } hs->RequestPlayer(cid_d); } @@ -751,9 +891,10 @@ void ConnectionToClient::HandleMessagePacket( break; } default: { - // Hackers have attempted to mess with servers by sending huge amounts of - // data through chat messages/etc. Let's watch out for mutli-part messages - // growing too large and kick/ban the client if they do. + // Hackers have attempted to mess with servers by sending huge + // amounts of data through chat messages/etc. Let's watch out for + // mutli-part messages growing too large and kick/ban the client if + // they do. if (buffer[0] == BA_MESSAGE_MULTIPART) { if (multipart_buffer_size() > 50000) { // Its not actually unknown but shhh don't tell the hackers... @@ -776,8 +917,8 @@ void ConnectionToClient::HandleMessagePacket( auto ConnectionToClient::GetCombinedSpec() -> PlayerSpec { auto* appmode = classic::ClassicAppMode::GetActiveOrFatal(); - // Look for players coming from this client-connection. - // If we find any, make a spec out of their name(s). + // Look for players coming from this client-connection. If we find any, + // make a spec out of their name(s). if (auto* hs = dynamic_cast(appmode->GetForegroundSession())) { std::string p_name_combined; for (auto&& p : hs->players()) { @@ -815,7 +956,8 @@ auto ConnectionToClient::GetClientInputDevice(int remote_id) -> ClientInputDevice* { auto i = client_input_devices_.find(remote_id); if (i == client_input_devices_.end()) { - // InputDevices get allocated as deferred and passed to g_input to store. + // InputDevices get allocated as deferred and passed to g_input to + // store. auto cid = Object::NewDeferred(remote_id, this); client_input_devices_[remote_id] = cid; g_base->input->AddInputDevice(cid, false); @@ -828,9 +970,15 @@ auto ConnectionToClient::GetAsUDP() -> ConnectionToClientUDP* { return nullptr; } +// Old V1 authentication stuff: void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); + // Sanity check; should never come through here if we're doing v2 auth. + auto doing_v2_auth{appmode->require_client_authentication() + && appmode->client_authentication_version() == 2}; + assert(!doing_v2_auth); + PyObject* profiles_obj = PyDict_GetItemString(info_obj, "p"); if (profiles_obj != nullptr) { player_profiles_.Acquire(profiles_obj); @@ -863,7 +1011,7 @@ void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { Error(""); } } - got_info_from_master_server_ = true; + got_v1_auth_from_master_server_ = true; } auto ConnectionToClient::IsAdmin() const -> bool { diff --git a/src/ballistica/scene_v1/connection/connection_to_client.h b/src/ballistica/scene_v1/connection/connection_to_client.h index 0174b3206..5821a157b 100644 --- a/src/ballistica/scene_v1/connection/connection_to_client.h +++ b/src/ballistica/scene_v1/connection/connection_to_client.h @@ -82,7 +82,7 @@ class ConnectionToClient : public Connection { std::string token_; std::string peer_hash_; PythonRef player_profiles_; - bool got_info_from_master_server_{}; + bool got_v1_auth_from_master_server_{}; std::vector last_chat_times_; millisecs_t next_kick_vote_allow_time_{}; millisecs_t chat_block_time_{}; diff --git a/src/ballistica/scene_v1/connection/connection_to_client_udp.cc b/src/ballistica/scene_v1/connection/connection_to_client_udp.cc index 896eacb55..1275960a6 100644 --- a/src/ballistica/scene_v1/connection/connection_to_client_udp.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client_udp.cc @@ -55,11 +55,13 @@ void ConnectionToClientUDP::Update() { auto current_time_millisecs = static_cast(g_base->logic->display_time() * 1000.0); - // if its been long enough since we've heard anything from the host, error. + // If its been long enough since we've heard anything from the host, + // error. if (current_time_millisecs - last_client_response_time_millisecs_ - > (can_communicate() ? 10000u : 5000u)) { - // die immediately in this case; no use trying to wait for a - // disconnect-ack since we've already given up hope of hearing from them.. + > (can_communicate() ? 30000u : 10000u)) { + // Die immediately in this case; no use trying to wait for a + // disconnect-ack since we've already given up hope of hearing from + // them. Die(); return; } diff --git a/src/ballistica/scene_v1/connection/connection_to_host.cc b/src/ballistica/scene_v1/connection/connection_to_host.cc index e11293ab4..c4a4cf9cb 100644 --- a/src/ballistica/scene_v1/connection/connection_to_host.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host.cc @@ -30,6 +30,52 @@ namespace ballistica::scene_v1 { // How long to go between sending out null packets for pings. const int kPingSendInterval = 2000; +static auto MakeServerResponseJson_(const std::string& passed_str) + -> std::string { + // Root object. + cJSON* root = cJSON_CreateObject(); + if (!root) { + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kError, + "MakeServerResponseJson_: cJSON_CreateObject failed."); + return ""; + } + + // Create array for "t". + cJSON* t_array = cJSON_CreateArray(); + if (!t_array) { + cJSON_Delete(root); + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kError, + "MakeServerResponseJson_: cJSON_CreateArray failed."); + return ""; + } + + // Add array to root under key "t". + cJSON_AddItemToObject(root, "t", t_array); + + // Add elements to array. + cJSON_AddItemToArray(t_array, cJSON_CreateString("serverResponses")); + cJSON_AddItemToArray(t_array, cJSON_CreateString(passed_str.c_str())); + + // Serialize to compact JSON. + char* json_cstr = cJSON_PrintUnformatted(root); + if (!json_cstr) { + cJSON_Delete(root); + g_core->logging->Log( + LogName::kBaNetworking, LogLevel::kError, + "MakeServerResponseJson_: cJSON_PrintUnformatted failed."); + return ""; + } + + // Copy into std::string. + std::string result(json_cstr); + + // Free cJSON allocations. + cJSON_free(json_cstr); + cJSON_Delete(root); + + return result; +} + ConnectionToHost::ConnectionToHost() : protocol_version_{ classic::ClassicAppMode::GetSingleton()->host_protocol_version()} {} @@ -101,10 +147,10 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); // We expect a > 3 byte handshake packet with protocol version as the - // second and third bytes and name/info beyond that. - // (player-spec for protocol <= 32 and info json dict for 33+). + // second and third bytes and name/info beyond that. (player-spec for + // protocol <= 32 and info json dict for 33+). - // If we don't support their protocol, let them know.. + // If we don't support their protocol, let them know. bool compatible = false; uint16_t their_protocol_version; memcpy(&their_protocol_version, data.data() + 1, @@ -113,13 +159,89 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { && their_protocol_version <= kProtocolVersionMax) { compatible = true; - // If we are compatible, set our protocol version to match - // what they're dealing. + // If we are compatible, set our protocol version to match what + // they're dealing. protocol_version_ = their_protocol_version; } - // Ok now we know if we can talk to them. Respond so they know - // whether they can talk to us. + // See if the server uses v2 auth. + if (!got_v2_auth_usage_) { + // If server requires v2 auth, it will have a 'v2a' value in its + // handshake which is its global-app-instance-uuid. We'll ask the + // cloud to send our account info to that app-instance and give us a + // token we can use to identify ourself as that account to them. + if (their_protocol_version >= 33) { + std::vector string_buffer(data.size() - 3 + 1); + memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3); + string_buffer[string_buffer.size() - 1] = 0; + if (cJSON* handshake = cJSON_Parse(string_buffer.data())) { + if (cJSON_IsObject(handshake)) { + cJSON* v2a = cJSON_GetObjectItem(handshake, "v2a"); + if (cJSON_IsString(v2a)) { + v2_auth_global_app_instance_id_ = v2a->valuestring; + } + } + cJSON_Delete(handshake); + } + } + got_v2_auth_usage_ = true; + } + + std::optional v2_auth_token; + + // If the server does use v2 auth, process v2-auth requests as needed + // and hold off on handshake-responses until something goes through. + assert(got_v2_auth_usage_); + if (v2_auth_global_app_instance_id_.has_value()) { + auto args = PythonRef::Stolen( + Py_BuildValue("(s)", v2_auth_global_app_instance_id_->c_str())); + auto result = g_base->python->objs() + .Get(base::BasePython::ObjID::kV2AuthRequestCall) + .Call(args); + if (!result.exists()) { + g_core->logging->Log(LogName::kBaNetworking, LogLevel::kError, + "Error running v2_auth_request."); + } else { + if (result.ValueIsNone()) { + // Still waiting... + } else { + auto valid_format{false}; + if (result.ValueIsSequence()) { + auto vals{result.ValueAsSequence()}; + if (vals.size() == 2 && PyBool_Check(*vals[0]) + && vals[1].ValueIsString()) { + // Success!!! + auto success{vals[0].ValueAsBool()}; + auto sval{vals[1].ValueAsString()}; + valid_format = true; + + if (!success) { + // If auth rejected us, show auth error message and fail. + Error(MakeServerResponseJson_(sval)); + return; + } else { + // Auth accepted us! Pass along this token in our + // handshake-response. + v2_auth_token = sval; + } + } + } + + if (!valid_format) { + g_core->logging->Log( + LogName::kBaNetworking, LogLevel::kError, + "Invalid type returned from v2_auth_request."); + } + } + } + // If we're still waiting on a token, go no further. + if (!v2_auth_token.has_value()) { + return; + } + } + + // Ok now we know if we can talk to them. Respond so they know whether + // they can talk to us. // (packet-type, our protocol-version, our spec/info) // For server-protocol < 32 we provide our player-spec. @@ -129,10 +251,15 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { JsonDict dict; dict.AddString("s", PlayerSpec::GetAccountPlayerSpec().GetSpecString()); - // Also add our public device id. Servers can - // use this to combat spammers. + // Also add our public device id. Servers can use this to combat + // spammers. dict.AddString("d", g_base->platform->GetPublicDeviceUUID()); + // Add v2 auth token. + if (v2_auth_token.has_value()) { + dict.AddString("v2at", *v2_auth_token); + } + std::string out = dict.PrintUnformatted(); std::vector data2(3 + out.size()); @@ -164,15 +291,15 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { return; } - // If we're freshly establishing that we're able to talk to them - // in a language they understand, go ahead and kick some stuff off. + // If we're freshly establishing that we're able to talk to them in a + // language they understand, go ahead and kick some stuff off. if (!can_communicate()) { if (their_protocol_version >= 33) { - // In newer protocols, handshake contains a json dict - // so we can evolve it going forward. std::vector string_buffer(data.size() - 3 + 1); memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3); string_buffer[string_buffer.size() - 1] = 0; + // In newer protocols, handshake contains a json dict so we can + // evolve it going forward. if (cJSON* handshake = cJSON_Parse(string_buffer.data())) { if (cJSON_IsObject(handshake)) { // We hash this to prove that we're us; keep it around. @@ -191,8 +318,9 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { } } else { // (KILL THIS WHEN kProtocolVersionClientMin >= 33) - // In older protocols, handshake simply contained a - // player-spec for the host. + // + // In older protocols, handshake simply contained a player-spec + // for the host. // Pull host's PlayerSpec from the handshake packet. std::vector string_buffer(data.size() - 3 + 1); @@ -210,8 +338,9 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { appmode->LaunchClientSession(); // NOTE: - // we don't actually print a 'connected' message until after - // we get our first message (it may influence the message we print and + // + // we don't actually print a 'connected' message until after we get + // our first message (it may influence the message we print and // there's also a chance we could still get booted after sending our // info message) @@ -223,8 +352,8 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { client_session_ = cs; cs->SetConnectionToHost(this); - // The very first thing we send is our client-info - // which is a json dict with arbitrary data. + // The very first thing we send is our client-info which is a json + // dict with arbitrary data. { JsonDict dict; dict.AddNumber("b", kEngineBuildNumber); @@ -232,7 +361,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { g_base->Plus()->V1SetClientInfo(&dict); // Pass the hash we generated from their handshake; they can use - // this to make sure we're who we say we are. + // this for v1 client auth. dict.AddString("ph", peer_hash_); std::string info = dict.PrintUnformatted(); std::vector msg(info.size() + 1); @@ -242,11 +371,16 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { } // Send them our player-profiles so we can use them on their end. - // (the host generally will pull these from the master server - // to prevent cheating, but in some cases these are used) - - // On newer hosts we send these as json. - if (protocol_version_ >= 32) { + // (the host generally will pull these from the master server to + // prevent cheating, but in some cases these are used). + + if (v2_auth_global_app_instance_id_.has_value()) { + // Host has enabled v2-auth. Don't bother sending our profiles + // directly as they will be ignored anyway (host gets profiles + // from cloud in this case). + } else if (protocol_version_ >= 32) { + // On newer hosts we send profiles as json. + // // (This is a borrowed ref) PyObject* profiles = g_base->python->GetRawConfigValue("Player Profiles"); @@ -384,7 +518,7 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { } case BA_MESSAGE_JMESSAGE: { - // High level json messages (nice and easy to expand on but not + // High level json screen-messages (nice and easy to expand on but not // especially efficient). if (buffer.size() >= 3 && buffer[buffer.size() - 1] == 0) { if (cJSON* msg = diff --git a/src/ballistica/scene_v1/connection/connection_to_host.h b/src/ballistica/scene_v1/connection/connection_to_host.h index 4d8934438..c8eb996b4 100644 --- a/src/ballistica/scene_v1/connection/connection_to_host.h +++ b/src/ballistica/scene_v1/connection/connection_to_host.h @@ -3,6 +3,7 @@ #ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_H_ #define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_H_ +#include #include #include @@ -33,15 +34,17 @@ class ConnectionToHost : public Connection { std::string party_name_; std::string peer_hash_input_; std::string peer_hash_; + std::optional v2_auth_global_app_instance_id_; + // The client-session that we're driving + Object::WeakRef client_session_; + int protocol_version_{-1}; + int build_number_{}; + millisecs_t last_ping_send_time_{}; // Can remove once back-compat protocol is > 29 bool ignore_old_attach_remote_player_packets_{}; bool printed_connect_message_{}; bool got_host_info_{}; - int protocol_version_{-1}; - int build_number_{}; - millisecs_t last_ping_send_time_{}; - // the client-session that we're driving - Object::WeakRef client_session_; + bool got_v2_auth_usage_{}; }; } // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc index a9e457039..d4abe95db 100644 --- a/src/ballistica/scene_v1/connection/connection_to_host_udp.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc @@ -79,9 +79,9 @@ void ConnectionToHostUDP::Update() { last_client_id_request_time_ = current_time_millisecs; // Client request packet: contains our protocol version (2 bytes), our - // request id (1 byte), and our session-identifier (remainder of the + // request id (1 byte), and our app-instance-uuid (remainder of the // message). - const std::string& uuid{g_base->GetAppInstanceUUID()}; + const std::string& uuid{g_base->LocalAppInstanceUUID()}; std::vector msg(4 + uuid.size()); msg[0] = BA_PACKET_CLIENT_REQUEST; auto p_version = static_cast(protocol_version()); @@ -94,7 +94,7 @@ void ConnectionToHostUDP::Update() { // If its been long enough since we've heard anything from the host, error. if (current_time_millisecs - last_host_response_time_millisecs_ - > (can_communicate() ? 10000u : 5000u)) { + > (can_communicate() ? 30000u : 10000u)) { // If the connection never got established, announce it failed. if (!can_communicate()) { g_base->ScreenMessage( diff --git a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc index e0c5381ee..6c2d00951 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc @@ -16,6 +16,7 @@ #include "ballistica/scene_v1/connection/connection_to_client.h" #include "ballistica/scene_v1/connection/connection_to_host_udp.h" #include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/shared/foundation/macros.h" #include "ballistica/shared/math/vector3f.h" #include "ballistica/shared/networking/sockaddr.h" #include "ballistica/shared/python/python.h" @@ -295,9 +296,10 @@ static auto PySetAuthenticateClients(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; int enable; + int version; static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "p", const_cast(kwlist), &enable, &version)) { return nullptr; } auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); @@ -676,6 +678,51 @@ static PyMethodDef PyGetClientPublicDeviceUUIDDef = { "periodically with updates to the game or operating system.", }; +// ----------------------- get_client_ping ----------------------------- + +static PyObject* PyGetClientPing(PyObject* self, PyObject* args, + PyObject* keywds) { + BA_PYTHON_TRY; + + int client_id; + static const char* kwlist[] = {"client_id", nullptr}; + + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &client_id)) { + return nullptr; + } + + auto* appmode = classic::ClassicAppMode::GetActiveOrThrow(); + + auto&& connection_iter{ + appmode->connections()->connections_to_clients().find(client_id)}; + + if (connection_iter + == appmode->connections()->connections_to_clients().end()) { + return PyFloat_FromDouble(-1.0f); + } + + assert(connection_iter->second.exists()); + + ConnectionToClient* connection = connection_iter->second.get(); + + float ping = connection->current_ping(); + + return PyFloat_FromDouble(ping); + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetClientPingDef = { + "get_client_ping", // name + (PyCFunction)PyGetClientPing, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_client_ping(client_id: int) -> float\n" + "\n" + "Return the current ping (RTT in ms) for a connected client.\n" + "Returns -1.0 if client_id is invalid.\n"}; + // ----------------------------- get_game_port --------------------------------- static auto PyGetGamePort(PyObject* self, PyObject* args) -> PyObject* { @@ -752,7 +799,7 @@ static PyMethodDef PyHostScanCycleDef = { (PyCFunction)PyHostScanCycle, // method METH_VARARGS | METH_KEYWORDS, // flags - "host_scan_cycle() -> list\n" + "host_scan_cycle() -> list[dict[str, str]]\n" "\n" "(internal)\n" "\n" @@ -900,6 +947,7 @@ auto PythonMethodsNetworking::GetMethods() -> std::vector { PyDisconnectFromHostDef, PyDisconnectClientDef, PyGetClientPublicDeviceUUIDDef, + PyGetClientPingDef, PyGetConnectionToHostInfoDef, PyGetConnectionToHostInfo2Def, PyClientInfoQueryResponseDef, diff --git a/src/ballistica/scene_v1/scene_v1.h b/src/ballistica/scene_v1/scene_v1.h index a2f6840fe..5d5d8646d 100644 --- a/src/ballistica/scene_v1/scene_v1.h +++ b/src/ballistica/scene_v1/scene_v1.h @@ -42,7 +42,7 @@ const int kProtocolVersionHostMin = 33; const int kProtocolVersionClientMin = 24; // Newest protocol version we can act as a client OR host for. -const int kProtocolVersionMax = 35; +const int kProtocolVersionMax = 36; // The protocol version we actually host is now read as a setting; see // kSceneV1HostProtocol in ballistica/base/support/app_config.h. @@ -74,6 +74,12 @@ const int kProtocolVersionMax = 35; // 34: New image_node enums, data assets. // // 35: Camera shake in netplay. how did I apparently miss this for 10 years!?! +// +// 36: Enables V2 auth for servers when authenticate-clients is enabled. +// This gives servers verified v2 account info for all joiners and +// allows screening them before they are even allowed in the game, +// unlike V1 auth. It is also free from V1 auth's spoofing +// vulnerabilities. // Sim step size in milliseconds. const int kGameStepMilliseconds = 8; diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index 8a48e8f89..30055d061 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -44,7 +44,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 22712; +const int kEngineBuildNumber = 22714; const char* kEngineVersion = "1.7.61"; const int kEngineApiVersion = 9; diff --git a/src/ballistica/shared/generic/utils.cc b/src/ballistica/shared/generic/utils.cc index 8aa71bfa4..6ecc19704 100644 --- a/src/ballistica/shared/generic/utils.cc +++ b/src/ballistica/shared/generic/utils.cc @@ -194,28 +194,27 @@ static auto utf8_check_is_valid(const std::string& string) -> bool { return true; } -// added by ericf from http://stackoverflow.com/questions/17316506/ +// Added by ericf from: http://stackoverflow.com/questions/17316506/ // strip-invalid-utf8-from-string-in-c-c -// static std::string correct_non_utf_8(std::string *str) { auto Utils::GetValidUTF8(const char* str, const char* loc) -> std::string { int i, f_size = static_cast(strlen(str)); unsigned char c, c2 = 0, c3, c4; std::string to; to.reserve(static_cast(f_size)); - // ok, it seems we're somehow letting some funky utf8 through that's - // causing crashes.. for now lets try this all-or-nothing func and return - // ascii only if it fails + // Ok, it seems we're somehow letting some funky utf8 through that's + // causing crashes. For now lets try this all-or-nothing func and return + // ascii only if it fails. if (!utf8_check_is_valid(str)) { - // now strip out anything but normal ascii... + // Now strip out anything but normal ascii. for (i = 0; i < f_size; i++) { c = (unsigned char)(str)[i]; - if (c < 127) { // normal ASCII + if (c < 127) { // Normal ASCII. to.append(1, static_cast(c)); } } - // phone home a few times for bad strings + // Phone home a few times for bad strings. static int logged_count = 0; if (logged_count < 10) { std::string log_str; diff --git a/src/ballistica/shared/python/python.cc b/src/ballistica/shared/python/python.cc index 4cac5e016..9bfb1c8c1 100644 --- a/src/ballistica/shared/python/python.cc +++ b/src/ballistica/shared/python/python.cc @@ -117,6 +117,17 @@ auto Python::IsString(PyObject* o) -> bool { return PyUnicode_Check(o); } +auto Python::IsSequence(PyObject* o) -> bool { + assert(HaveGIL()); + + // We now gracefully handle null values. + if (o == nullptr) { + return false; + } + + return PySequence_Check(o); +} + auto Python::GetString(PyObject* o) -> std::string { assert(HaveGIL()); diff --git a/src/ballistica/shared/python/python.h b/src/ballistica/shared/python/python.h index 9ebd03edd..89378f2e9 100644 --- a/src/ballistica/shared/python/python.h +++ b/src/ballistica/shared/python/python.h @@ -141,6 +141,9 @@ class Python { return static_cast(GetDouble(o)); } + /// Is the provided PyObject a sequence? + static auto IsSequence(PyObject* o) -> bool; + /// Return float values any Python sequence of numeric objects. static auto GetFloats(PyObject* o) -> std::vector; diff --git a/src/ballistica/shared/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc index 9259f7e76..ab3e0920a 100644 --- a/src/ballistica/shared/python/python_ref.cc +++ b/src/ballistica/shared/python/python_ref.cc @@ -171,6 +171,12 @@ auto PythonRef::ValueIsString() const -> bool { return Python::IsString(obj_); } +auto PythonRef::ValueIsSequence() const -> bool { + assert(Python::HaveGIL()); + ThrowIfUnset(); + return Python::IsSequence(obj_); +} + auto PythonRef::ValueAsLString() const -> std::string { assert(Python::HaveGIL()); ThrowIfUnset(); @@ -192,6 +198,26 @@ auto PythonRef::ValueAsStringSequence() const -> std::vector { return Python::GetStrings(obj_); } +auto PythonRef::ValueAsSequence() const -> std::vector { + assert(Python::HaveGIL()); + ThrowIfUnset(); + + if (!PySequence_Check(obj_)) { + throw Exception("Expected a sequence object; got type " + + Python::ObjTypeToString(obj_) + ".", + PyExcType::kType); + } + + Py_ssize_t size = PySequence_Size(obj_); + + std::vector result; + result.reserve(static_cast(size)); + for (Py_ssize_t i = 0; i < size; ++i) { + result.push_back(PythonRef::Stolen(PySequence_GetItem(obj_, i))); + } + return result; +} + auto PythonRef::ValueAsOptionalInt() const -> std::optional { assert(Python::HaveGIL()); ThrowIfUnset(); @@ -226,6 +252,12 @@ auto PythonRef::ValueAsOptionalStringSequence() const return Python::GetStrings(obj_); } +auto PythonRef::ValueAsBool() const -> bool { + assert(Python::HaveGIL()); + ThrowIfUnset(); + return Python::GetBool(obj_); +} + auto PythonRef::ValueAsInt() const -> int64_t { assert(Python::HaveGIL()); ThrowIfUnset(); diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h index 6c1357ead..c15c9b490 100644 --- a/src/ballistica/shared/python/python_ref.h +++ b/src/ballistica/shared/python/python_ref.h @@ -182,6 +182,10 @@ class PythonRef { auto ValueAsOptionalStringSequence() const -> std::optional>; + auto ValueIsSequence() const -> bool; + auto ValueAsSequence() const -> std::vector; + + auto ValueAsBool() const -> bool; auto ValueAsInt() const -> int64_t; auto ValueAsDouble() const -> double; auto ValueAsOptionalInt() const -> std::optional; diff --git a/src/meta/babasemeta/pyembed/binding_base.py b/src/meta/babasemeta/pyembed/binding_base.py index c809d80d1..6d019b25d 100644 --- a/src/meta/babasemeta/pyembed/binding_base.py +++ b/src/meta/babasemeta/pyembed/binding_base.py @@ -80,4 +80,6 @@ AppArchitecture, # kAppArchitectureType AppPlatform, # kAppPlatformType AppVariant, # kAppVariantType + _hooks.v2_auth_request, # kV2AuthRequestCall + _hooks.v2_auth_data, # kV2AuthDataCall ] diff --git a/tools/bacommon/cloud.py b/tools/bacommon/cloud.py index 1895a6683..c12690b38 100644 --- a/tools/bacommon/cloud.py +++ b/tools/bacommon/cloud.py @@ -462,3 +462,25 @@ class AnalyticsEventMessage(Message): """Have a nice analytics event!""" event: Annotated[AnalyticsEvent, IOAttrs('e')] + + +@ioprepped +@dataclass +class AuthRequestMessage(Message): + """Request access to a server for a current account.""" + + global_app_instance_uuid: Annotated[str, IOAttrs('a')] + + @override + @classmethod + def get_response_types(cls) -> list[type[Response] | None]: + return [AuthRequestResponse] + + +@ioprepped +@dataclass +class AuthRequestResponse(Response): + """Here's that access ya asked for boss.""" + + error: Annotated[str | None, IOAttrs('e')] + token: Annotated[str | None, IOAttrs('t')] diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index 1b464dca0..e053098e6 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -30,13 +30,19 @@ class ServerConfig: # If True, all connecting clients will be authenticated through the # master server to screen for fake account info. Generally this # should always be enabled unless you are hosting on a LAN with no - # internet connection. + # internet connection. Note that if you set protocol_version to 36 + # or newer, client authentication uses V2 account info. This is + # highly recommended as it does not have spoofing vulnerabilities + # like the earlier V1 authentication. authenticate_clients: bool = True # IDs of server admins. Server admins are not kickable through the # default kick vote system and they are able to kick players without - # a vote. To get your account id, enter 'getaccountid' in - # settings->advanced->enter-code. + # a vote. If protocol_version is set to 36 or newer this will use V2 + # account ids (a-XXX); otherwise it will use V1 ids (pb-XXX). To get + # your V2 account id, poke the 'manage account' button in the + # account window in-game. To get your V1 account id, enter + # 'getaccountid' in Settings->Advanced->Send Info admins: list[str] = field(default_factory=list) # Whether the default kick-voting system is enabled. @@ -176,7 +182,10 @@ class ServerConfig: # Protocol version we host with. Currently the default is 33 which # still allows older 1.4 game clients to connect. Explicitly setting # to 35 no longer allows those clients but adds/fixes a few things - # such as making camera shake properly work in net games. + # such as making camera shake properly work in net games. Protocol + # 36 enables V2 account ids (a-XXX) for client authentication, which + # does not suffer from spoofing vulnerabilities that V1 account ids + # (pb-XXX) did. protocol_version: int | None = None # (internal) stress-testing mode. diff --git a/tools/batools/build.py b/tools/batools/build.py index bf31188b6..26733d84a 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -527,7 +527,7 @@ def _get_server_config_template_toml(projroot: str) -> str: cfg.clean_exit_minutes = 60 cfg.unclean_exit_minutes = 90 cfg.idle_exit_minutes = 20 - cfg.admins = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE'] + cfg.admins = ['a-YOUR-ID-HERE', 'a-ANOTHER-ID-HERE'] cfg.protocol_version = 35 cfg.session_max_players_override = 8 cfg.playlist_inline = [] diff --git a/tools/batools/docs.py b/tools/batools/docs.py index e64ea9639..e1f68dd73 100755 --- a/tools/batools/docs.py +++ b/tools/batools/docs.py @@ -29,7 +29,7 @@ class AttributeInfo: docs: str | None = None -_g_genned_pdoc_with_dummy_modules = False # pylint: disable=invalid-name +_g_genned_pdoc_with_dummy_modules = False def parse_docs_attrs(attrs: list[AttributeInfo], docs: str) -> str: diff --git a/tools/batools/dummymodule.py b/tools/batools/dummymodule.py index 6b22f0e41..ce6313e86 100755 --- a/tools/batools/dummymodule.py +++ b/tools/batools/dummymodule.py @@ -306,6 +306,8 @@ def _writefuncs( returnstr = 'return (0, 0)' elif returns == 'list[dict[str, Any]]': returnstr = "return [{'foo': 'bar'}]" + elif returns == 'list[dict[str, str]]': + returnstr = "return [{'foo': 'bar'}]" elif returns in { 'session.Session', 'team.Team', @@ -346,7 +348,8 @@ def _writefuncs( returnstr = 'return ' + returns + '()' else: raise RuntimeError( - f'Unknown returns value: {returns} for {funcname}' + f'Unknown returns value: {returns} for {funcname}.' + f' You may need to add this case to dummymodule.py.' ) returnstr = ( f'# This is a dummy stub;' diff --git a/tools/efro/terminal.py b/tools/efro/terminal.py index 796d693cc..4ddbcf153 100644 --- a/tools/efro/terminal.py +++ b/tools/efro/terminal.py @@ -315,7 +315,7 @@ class ClrNever(ClrBase): _envval = os.environ.get('EFRO_TERMCOLORS') -color_enabled: bool = ( +color_enabled: bool = ( # pylint: disable=invalid-name True if _envval == '1' else False if _envval == '0' else _default_color_enabled() diff --git a/tools/efro/util.py b/tools/efro/util.py index 74a7d2ff3..9ded139d4 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -1089,3 +1089,17 @@ def strip_exception_tracebacks(exc: BaseException) -> None: cause = getattr(e, '__cause__', None) if cause is not None: stack.append(cause) + + +def secure_id() -> str: + """Generate a 20 char cryptographically secure string. + + Basically what firestore does for its random document ids. + If its good enough for firestore its good enough for us. + """ + import secrets + import string + + alphabet = string.ascii_letters + string.digits # 62 chars + + return ''.join(secrets.choice(alphabet) for _ in range(20)) diff --git a/tools/efrotools/efrocache.py b/tools/efrotools/efrocache.py index 6f92aacea..ca2a0f082 100644 --- a/tools/efrotools/efrocache.py +++ b/tools/efrotools/efrocache.py @@ -51,8 +51,8 @@ class CacheMetadata: executable: Annotated[bool, IOAttrs('e')] -g_cache_prefix_noexec: bytes | None = None -g_cache_prefix_exec: bytes | None = None +_g_cache_prefix_noexec: bytes | None = None +_g_cache_prefix_exec: bytes | None = None def get_local_cache_dir() -> str: @@ -664,8 +664,8 @@ def _cache_prefix_for_file(fname: str) -> bytes: # pylint: disable=global-statement from efrotools.util import is_wsl_windows_build_path - global g_cache_prefix_exec - global g_cache_prefix_noexec + global _g_cache_prefix_exec + global _g_cache_prefix_noexec # We'll be calling this a lot when checking existing files, so we # want it to be efficient. Let's cache the two options there are at @@ -689,21 +689,21 @@ def _cache_prefix_for_file(fname: str) -> bytes: executable = False if executable: - if g_cache_prefix_exec is None: + if _g_cache_prefix_exec is None: metadata = dataclass_to_json( CacheMetadata(executable=True) ).encode() assert len(metadata) < 256 - g_cache_prefix_exec = ( + _g_cache_prefix_exec = ( CACHE_HEADER + len(metadata).to_bytes() + metadata ) - return g_cache_prefix_exec + return _g_cache_prefix_exec # Ok; non-executable it is. metadata = dataclass_to_json(CacheMetadata(executable=False)).encode() assert len(metadata) < 256 - g_cache_prefix_noexec = CACHE_HEADER + len(metadata).to_bytes() + metadata - return g_cache_prefix_noexec + _g_cache_prefix_noexec = CACHE_HEADER + len(metadata).to_bytes() + metadata + return _g_cache_prefix_noexec def _check_warm_start_entry(entry: tuple[str, str]) -> None: diff --git a/tools/efrotools/pcommands.py b/tools/efrotools/pcommands.py index e7da574c0..d6c5dbfd1 100644 --- a/tools/efrotools/pcommands.py +++ b/tools/efrotools/pcommands.py @@ -89,7 +89,7 @@ def requirements_upgrade() -> None: # Fails to build on bastaging (submitted fix). ('pyicu==2.16.1', 'pyicu==2.15.2'), ('google-auth-oauthlib==1.2.3', 'google-auth-oauthlib==1.2.2'), - ('pylint==4.0.4', 'pylint==4.0.3'), + # ('pylint==4.0.4', 'pylint==4.0.3'), ('Sphinx==9.1.0', 'Sphinx==8.2.3'), ('gunicorn==24.0.0', 'gunicorn==23.0.0'), ] diff --git a/tools/efrotools/pyver.py b/tools/efrotools/pyver.py index b25c2291a..a83d41977 100644 --- a/tools/efrotools/pyver.py +++ b/tools/efrotools/pyver.py @@ -13,10 +13,8 @@ PYVER = '3.13' PYVERNODOT = PYVER.replace('.', '') -# pylint: disable=invalid-name -_checked_valid_sys_executable = False -_valid_sys_executable: str | None = None -# pylint: enable=invalid-name +_g_checked_valid_sys_executable = False +_g_valid_sys_executable: str | None = None def get_project_python_executable(projroot: Path | str) -> str: