syncing 1.7.61 v2 auth changes
Some checks failed
CI / run_server_binary (push) Has been cancelled

This commit is contained in:
Ayush Saini 2026-02-23 19:28:45 +05:30
parent f0fc46a51d
commit 9e0176e424
19 changed files with 374 additions and 72 deletions

View file

@ -24,7 +24,6 @@ from _babase import (
add_clean_frame_callback, add_clean_frame_callback,
allows_ticket_sales, allows_ticket_sales,
android_get_external_files_dir, android_get_external_files_dir,
app_instance_uuid,
appname, appname,
appnameupper, appnameupper,
apptime, apptime,
@ -234,7 +233,6 @@ __all__ = [
'AppIntentExec', 'AppIntentExec',
'AppMode', 'AppMode',
'AppState', 'AppState',
'app_instance_uuid',
'applog', 'applog',
'appname', 'appname',
'appnameupper', 'appnameupper',

View file

@ -4,9 +4,11 @@
from __future__ import annotations from __future__ import annotations
import time
import hashlib import hashlib
import logging import logging
from functools import partial from functools import partial
from dataclasses import dataclass
from typing import TYPE_CHECKING, assert_never from typing import TYPE_CHECKING, assert_never
from efro.error import CommunicationError from efro.error import CommunicationError
@ -19,6 +21,8 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
import bacommon.cloud
from babase._login import LoginAdapter, LoginInfo from babase._login import LoginAdapter, LoginInfo
@ -62,6 +66,9 @@ class AccountV2Subsystem:
Callable[[AccountV2Handle | None], None] Callable[[AccountV2Handle | None], None]
] = CallbackSet() ] = CallbackSet()
# Request state per global-app-instance-id
self._auth_requests: dict[str, _AuthRequest] = {}
adapter: LoginAdapter adapter: LoginAdapter
if _babase.using_google_play_game_services(): if _babase.using_google_play_game_services():
adapter = LoginAdapterGPGS() adapter = LoginAdapterGPGS()
@ -104,6 +111,9 @@ class AccountV2Subsystem:
""" """
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
# Blow away any outstanding auth-requests.
self._auth_requests = {}
# Inform the base layer of new names/etc. # Inform the base layer of new names/etc.
if account is not None: if account is not None:
_babase.set_account_sign_in_state(True, account.tag) _babase.set_account_sign_in_state(True, account.tag)
@ -201,6 +211,78 @@ class AccountV2Subsystem:
self._initial_sign_in_completed = True self._initial_sign_in_completed = True
_babase.app.on_initial_sign_in_complete() _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 @staticmethod
def _hashstr(val: str) -> str: def _hashstr(val: str) -> str:
md5 = hashlib.md5() md5 = hashlib.md5()
@ -501,3 +583,10 @@ class AccountV2Handle:
This allows cloud messages to be sent on our behalf. This allows cloud messages to be sent on our behalf.
""" """
@dataclass
class _AuthRequest:
expire_time: float
error: str | None
token: str | None

View file

@ -242,7 +242,6 @@ class App:
loop. Hopefully this situation will be improved in the future loop. Hopefully this situation will be improved in the future
with a unified event loop. with a unified event loop.
""" """
assert _babase.in_logic_thread()
assert self._asyncio_loop is not None assert self._asyncio_loop is not None
return self._asyncio_loop return self._asyncio_loop

View file

@ -11,7 +11,7 @@ import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
_g_pending_apply = False # pylint: disable=invalid-name _g_pending_apply = False
class AppConfig(dict): class AppConfig(dict):

View file

@ -23,8 +23,8 @@ if TYPE_CHECKING:
import babase import babase
# Our timer and event loop for the ballistica logic thread. # Our timer and event loop for the ballistica logic thread.
_asyncio_timer: babase.AppTimer | None = None _g_asyncio_timer: babase.AppTimer | None = None
_asyncio_event_loop: asyncio.AbstractEventLoop | None = None _g_asyncio_event_loop: asyncio.AbstractEventLoop | None = None
DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1' DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1'
@ -46,12 +46,12 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
except RuntimeError: except RuntimeError:
pass pass
global _asyncio_event_loop global _g_asyncio_event_loop
_asyncio_event_loop = asyncio.new_event_loop() _g_asyncio_event_loop = asyncio.new_event_loop()
_asyncio_event_loop.set_default_executor(babase.app.threadpool) _g_asyncio_event_loop.set_default_executor(babase.app.threadpool)
# Try to avoid reference loops from exceptions. # 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 # Ideally we should integrate asyncio into our C++ Thread class's
# low level event loop so that asyncio timers/sockets/etc. could # 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/ # See https://stackoverflow.com/questions/29782377/
# is-it-possible-to-run-only-a-single-step-of-the-asyncio-event-loop # is-it-possible-to-run-only-a-single-step-of-the-asyncio-event-loop
def run_cycle() -> None: def run_cycle() -> None:
assert _asyncio_event_loop is not None assert _g_asyncio_event_loop is not None
_asyncio_event_loop.call_soon(_asyncio_event_loop.stop) _g_asyncio_event_loop.call_soon(_g_asyncio_event_loop.stop)
starttime = time.monotonic() if DEBUG_TIMING else 0 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 endtime = time.monotonic() if DEBUG_TIMING else 0
# Let's aim to have nothing take longer than 1/120 of a second. # Let's aim to have nothing take longer than 1/120 of a second.
@ -79,21 +79,21 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
warn_time, warn_time,
) )
global _asyncio_timer global _g_asyncio_timer
_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) _g_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True)
if bool(False): if bool(False):
async def aio_test() -> None: async def aio_test() -> None:
print('TEST AIO TASK STARTING') print('TEST AIO TASK STARTING')
assert _asyncio_event_loop is not None assert _g_asyncio_event_loop is not None
assert asyncio.get_running_loop() is _asyncio_event_loop assert asyncio.get_running_loop() is _g_asyncio_event_loop
await asyncio.sleep(2.0) await asyncio.sleep(2.0)
print('TEST AIO TASK ENDING') 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( def _exception_handler(

View file

@ -462,3 +462,36 @@ def copy_dev_console_history() -> None:
_babase.clipboard_set_text('\n'.join(lines)) _babase.clipboard_set_text('\n'.join(lines))
_babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0)) _babase.screenmessage(Lstr(resource='copyConfirmText'), color=(0, 1, 0))
_babase.getsimplesound('gunCocking').play() _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,
)

View file

@ -6,10 +6,12 @@
from __future__ import annotations from __future__ import annotations
import time
import random import random
import logging import logging
import weakref 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 from efro.dataclassio import dataclass_from_dict
import babase import babase
@ -26,7 +28,8 @@ from baclassic._store import StoreSubsystem
from baclassic import _input from baclassic import _input
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any, Sequence import datetime
from typing import Callable, Any, Sequence, Awaitable
import bacommon.classic import bacommon.classic
import bacommon.clienteffect as clfx import bacommon.clienteffect as clfx
@ -39,16 +42,72 @@ if TYPE_CHECKING:
class ClassicAppSubsystem(babase.AppSubsystem): 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 The single shared instance of this app can be accessed at
babase.app.classic. Note that it is possible for babase.app.classic to babase.app.classic. Note that it is possible for babase.app.classic
be None if the classic package is not present, and code should handle to be None if the classic package is not present, and futureproof
that case gracefully. code should handle that case gracefully.
""" """
# pylint: disable=too-many-public-methods # 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 from baclassic._music import MusicPlayMode
def __init__(self) -> None: def __init__(self) -> None:
@ -95,6 +154,21 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# Server Mode. # Server Mode.
self.server: ServerController | None = None 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_have_new = False
self.log_upload_timer_started = False self.log_upload_timer_started = False
self.printed_live_object_warning = False self.printed_live_object_warning = False
@ -129,8 +203,48 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self.pro_sale_start_time: int | None = None self.pro_sale_start_time: int | None = None
self.pro_sale_start_val: 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: 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 there's no main window up, just call immediately.
if not babase.app.ui_v1.has_main_window(): if not babase.app.ui_v1.has_main_window():
@ -174,7 +288,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
return self._env['platform'] return self._env['platform']
def scene_v1_protocol_version(self) -> int: def scene_v1_protocol_version(self) -> int:
"""(internal)""" """:meta private:"""
return bascenev1.protocol_version() return bascenev1.protocol_version()
@property @property
@ -464,7 +578,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
) )
def game_begin_analytics(self) -> None: def game_begin_analytics(self) -> None:
"""(internal)""" """:meta private:"""
from baclassic import _analytics from baclassic import _analytics
_analytics.game_begin_analytics() _analytics.game_begin_analytics()
@ -656,11 +770,11 @@ class ClassicAppSubsystem(babase.AppSubsystem):
return bascenev1.get_player_profile_colors(profilename, profiles) return bascenev1.get_player_profile_colors(profilename, profiles)
def get_foreground_host_session(self) -> bascenev1.Session | None: def get_foreground_host_session(self) -> bascenev1.Session | None:
"""(internal)""" """:meta private:"""
return bascenev1.get_foreground_host_session() return bascenev1.get_foreground_host_session()
def get_foreground_host_activity(self) -> bascenev1.Activity | None: def get_foreground_host_activity(self) -> bascenev1.Activity | None:
"""(internal)""" """:meta private:"""
return bascenev1.get_foreground_host_activity() return bascenev1.get_foreground_host_activity()
def value_test( def value_test(
@ -669,26 +783,26 @@ class ClassicAppSubsystem(babase.AppSubsystem):
change: float | None = None, change: float | None = None,
absolute: float | None = None, absolute: float | None = None,
) -> float: ) -> float:
"""(internal)""" """:meta private:"""
return _baclassic.value_test(arg, change, absolute) return _baclassic.value_test(arg, change, absolute)
def set_master_server_source(self, source: int) -> None: def set_master_server_source(self, source: int) -> None:
"""(internal)""" """:meta private:"""
bascenev1.set_master_server_source(source) bascenev1.set_master_server_source(source)
def get_game_port(self) -> int: def get_game_port(self) -> int:
"""(internal)""" """:meta private:"""
return bascenev1.get_game_port() return bascenev1.get_game_port()
def v2_upgrade_window(self, login_name: str, code: str) -> None: def v2_upgrade_window(self, login_name: str, code: str) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.v2upgrade import V2UpgradeWindow from bauiv1lib.v2upgrade import V2UpgradeWindow
V2UpgradeWindow(login_name, code) V2UpgradeWindow(login_name, code)
def server_dialog(self, delay: float, data: dict[str, Any]) -> None: def server_dialog(self, delay: float, data: dict[str, Any]) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.serverdialog import ( from bauiv1lib.serverdialog import (
ServerDialogData, ServerDialogData,
ServerDialogWindow, ServerDialogWindow,
@ -709,13 +823,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
) )
def show_url_window(self, address: str) -> None: def show_url_window(self, address: str) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.url import ShowURLWindow from bauiv1lib.url import ShowURLWindow
ShowURLWindow(address) ShowURLWindow(address)
def quit_window(self, quit_type: babase.QuitType) -> None: def quit_window(self, quit_type: babase.QuitType) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.confirm import QuitWindow from bauiv1lib.confirm import QuitWindow
QuitWindow(quit_type) QuitWindow(quit_type)
@ -731,7 +845,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
offset: tuple[float, float] = (0.0, 0.0), offset: tuple[float, float] = (0.0, 0.0),
on_close_call: Callable[[], Any] | None = None, on_close_call: Callable[[], Any] | None = None,
) -> None: ) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.tournamententry import TournamentEntryWindow from bauiv1lib.tournamententry import TournamentEntryWindow
TournamentEntryWindow( TournamentEntryWindow(
@ -745,7 +859,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
) )
def get_main_menu_session(self) -> type[bascenev1.Session]: def get_main_menu_session(self) -> type[bascenev1.Session]:
"""(internal)""" """:meta private:"""
from bascenev1lib.mainmenu import MainMenuSession from bascenev1lib.mainmenu import MainMenuSession
return MainMenuSession return MainMenuSession
@ -796,7 +910,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
logging.exception('Error preloading map preview media.') logging.exception('Error preloading map preview media.')
def party_icon_activate(self, origin: Sequence[float]) -> None: def party_icon_activate(self, origin: Sequence[float]) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.party import PartyWindow from bauiv1lib.party import PartyWindow
from babase import app from babase import app
@ -817,7 +931,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self.party_window = weakref.ref(PartyWindow(origin=origin)) self.party_window = weakref.ref(PartyWindow(origin=origin))
def request_main_ui(self) -> None: def request_main_ui(self) -> None:
"""(internal)""" """:meta private:"""
from bauiv1lib.ingamemenu import InGameMenuWindow from bauiv1lib.ingamemenu import InGameMenuWindow
assert babase.app is not None assert babase.app is not None

View file

@ -108,6 +108,12 @@ class ServerController:
self._playlist_fetch_got_response = False self._playlist_fetch_got_response = False
self._playlist_fetch_code = -1 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 # Now sit around doing any pre-launch prep such as waiting for
# account sign-in or fetching playlists; this will kick off the # account sign-in or fetching playlists; this will kick off the
# session once done. # session once done.
@ -428,8 +434,6 @@ class ServerController:
classic.teams_series_length = self._config.teams_series_length classic.teams_series_length = self._config.teams_series_length
classic.ffa_series_length = self._config.ffa_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( bascenev1.set_enable_default_kick_voting(
self._config.enable_default_kick_voting self._config.enable_default_kick_voting
) )
@ -452,7 +456,6 @@ class ServerController:
bascenev1.set_player_rejoin_cooldown( bascenev1.set_player_rejoin_cooldown(
self._config.player_rejoin_cooldown self._config.player_rejoin_cooldown
) )
bascenev1.set_max_players_override( bascenev1.set_max_players_override(
self._config.session_max_players_override self._config.session_max_players_override
) )
@ -471,6 +474,6 @@ class ServerController:
bascenev1.new_host_session(sessiontype) bascenev1.new_host_session(sessiontype)
# Run an access check if we're trying to make a public party. # Run an access check if we're trying to make a public party.
if not self._ran_access_check : if self._config.party_is_public and not self._ran_access_check:
self._run_access_check() self._run_access_check()
self._ran_access_check = True self._ran_access_check = True

View file

@ -462,3 +462,25 @@ class AnalyticsEventMessage(Message):
"""Have a nice analytics event!""" """Have a nice analytics event!"""
event: Annotated[AnalyticsEvent, IOAttrs('e')] 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')]

View file

@ -30,13 +30,19 @@ class ServerConfig:
# If True, all connecting clients will be authenticated through the # If True, all connecting clients will be authenticated through the
# master server to screen for fake account info. Generally this # master server to screen for fake account info. Generally this
# should always be enabled unless you are hosting on a LAN with no # 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 authenticate_clients: bool = True
# IDs of server admins. Server admins are not kickable through the # IDs of server admins. Server admins are not kickable through the
# default kick vote system and they are able to kick players without # default kick vote system and they are able to kick players without
# a vote. To get your account id, enter 'getaccountid' in # a vote. If protocol_version is set to 36 or newer this will use V2
# settings->advanced->enter-code. # 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) admins: list[str] = field(default_factory=list)
# Whether the default kick-voting system is enabled. # 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 # Protocol version we host with. Currently the default is 33 which
# still allows older 1.4 game clients to connect. Explicitly setting # still allows older 1.4 game clients to connect. Explicitly setting
# to 35 no longer allows those clients but adds/fixes a few things # 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 protocol_version: int | None = None
# (internal) stress-testing mode. # (internal) stress-testing mode.

View file

@ -57,7 +57,7 @@ logger = logging.getLogger('ba.env')
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 22712 TARGET_BALLISTICA_BUILD = 22714
TARGET_BALLISTICA_VERSION = '1.7.61' TARGET_BALLISTICA_VERSION = '1.7.61'

View file

@ -332,6 +332,16 @@ class CloudSubsystem(babase.AppSubsystem):
], ],
) -> None: ... ) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.cloud.AuthRequestMessage,
on_response: Callable[
[bacommon.cloud.AuthRequestResponse | Exception],
None,
],
) -> None: ...
def send_message_cb( def send_message_cb(
self, self,
msg: Message, msg: Message,

View file

@ -339,10 +339,10 @@ class Chooser:
if inputdevice.is_controller_app and '_random' in profilenames: if inputdevice.is_controller_app and '_random' in profilenames:
return profilenames.index('_random') return profilenames.index('_random')
# If its a client connection, for now just force # If its a client connection, for now just force the account
# the account profile if possible.. (need to provide a # profile if possible. (need to provide a way for clients to
# way for clients to specify/remember their default # specify/remember their default profile on remote servers that
# profile on remote servers that do not already know them). # do not already know them).
if inputdevice.is_remote_client and '__account__' in profilenames: if inputdevice.is_remote_client and '__account__' in profilenames:
return profilenames.index('__account__') return profilenames.index('__account__')

View file

@ -25,7 +25,7 @@ if TYPE_CHECKING:
_g_player_rejoin_cooldown: float = 0.0 _g_player_rejoin_cooldown: float = 0.0
# overrides the session's decision of max_players # 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: 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: def set_max_players_override(max_players: int | None) -> None:
"""Set the override for how many players can join a session""" """Set the override for how many players can join a session"""
global _max_players_override # pylint: disable=global-statement global _g_max_players_override # pylint: disable=global-statement
_max_players_override = max_players _g_max_players_override = max_players
class Session: class Session:
@ -176,8 +176,8 @@ class Session:
self.min_players = min_players self.min_players = min_players
self.max_players = ( self.max_players = (
max_players max_players
if _max_players_override is None if _g_max_players_override is None
else _max_players_override else _g_max_players_override
) )
self.submit_score = submit_score self.submit_score = submit_score

View file

@ -14,8 +14,6 @@ from __future__ import annotations
# pylint: disable=redefined-builtin # pylint: disable=redefined-builtin
import logging
from babase import ( from babase import (
accountlog, accountlog,
AccountV2Handle, AccountV2Handle,
@ -319,7 +317,7 @@ __all__ = [
if __debug__: if __debug__:
for _mdl in 'babase', '_babase': for _mdl in 'babase', '_babase':
if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
logging.warning( balog.warning(
'%s was imported before %s finished importing;' '%s was imported before %s finished importing;'
' should not happen.', ' should not happen.',
__name__, __name__,

View file

@ -45,14 +45,17 @@ class NetScanner:
bui.widget(edit=self._columnwidget, up_widget=tab_button) bui.widget(edit=self._columnwidget, up_widget=tab_button)
self._width = width self._width = width
self._last_selected_host: dict[str, Any] | None = None self._last_selected_host: dict[str, Any] | None = None
self._last_scan: list[dict[str, str]] | None = None
self._update_timer = bui.AppTimer( 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 # Run two cycles pretty immediately - this should send out a
# have to wait a second. # "who's there" and update the list with any immediate-ish
self.update() # results so we may not have to wait a second to see things
bui.apptimer(0.25, bui.WeakCallStrict(self.update)) # appear.
self._update()
bui.apptimer(0.25, bui.WeakCallStrict(self._update))
def __del__(self) -> None: def __del__(self) -> None:
bs.end_host_scanning() bs.end_host_scanning()
@ -74,23 +77,33 @@ class NetScanner:
bs.connect_to_party(host['address']) bs.connect_to_party(host['address'])
def update(self) -> None: def _update(self) -> None:
"""(internal)""" """(internal)"""
# In case our UI was killed from under us. # In case our UI was killed from under us.
if not self._columnwidget: if not self._columnwidget:
print( bui.uilog.error(
f'ERROR: NetScanner running without UI at time {bui.apptime()}.' 'nearbytab NetScanner running without UI at time %s.',
bui.apptime(),
) )
return 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 t_scale = 1.6
for child in self._columnwidget.get_children(): for child in self._columnwidget.get_children():
child.delete() child.delete()
# Grab this now this since adding widgets will change it. # Grab this now this since adding widgets will change it.
last_selected_host = self._last_selected_host last_selected_host = self._last_selected_host
hosts = bs.host_scan_cycle()
for i, host in enumerate(hosts): for i, host in enumerate(hosts):
txt3 = bui.textwidget( txt3 = bui.textwidget(
parent=self._columnwidget, parent=self._columnwidget,

View file

@ -315,7 +315,7 @@ class ClrNever(ClrBase):
_envval = os.environ.get('EFRO_TERMCOLORS') _envval = os.environ.get('EFRO_TERMCOLORS')
color_enabled: bool = ( color_enabled: bool = ( # pylint: disable=invalid-name
True True
if _envval == '1' if _envval == '1'
else False if _envval == '0' else _default_color_enabled() else False if _envval == '0' else _default_color_enabled()

View file

@ -1089,3 +1089,17 @@ def strip_exception_tracebacks(exc: BaseException) -> None:
cause = getattr(e, '__cause__', None) cause = getattr(e, '__cause__', None)
if cause is not None: if cause is not None:
stack.append(cause) 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))

Binary file not shown.