vh-bombsquad-modded-server-.../dist/ba_data/python/ba/_net.py
2024-02-26 00:17:10 +05:30

237 lines
7.9 KiB
Python

# Released under the MIT License. See LICENSE for details.
#
"""Networking related functionality."""
from __future__ import annotations
import ssl
import copy
import threading
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Any, Callable
import socket
MasterServerCallback = Callable[[None | dict[str, Any]], None]
# Timeout for standard functions talking to the master-server/etc.
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
class NetworkSubsystem:
"""Network related app subsystem."""
def __init__(self) -> None:
# Anyone accessing/modifying zone_pings should hold this lock,
# as it is updated by a background thread.
self.zone_pings_lock = threading.Lock()
# Zone IDs mapped to average pings. This will remain empty
# until enough pings have been run to be reasonably certain
# that a nearby server has been pinged.
self.zone_pings: dict[str, float] = {}
self._sslcontext: ssl.SSLContext | None = None
# For debugging.
self.v1_test_log: str = ''
self.v1_ctest_results: dict[int, str] = {}
self.server_time_offset_hours: float | None = None
@property
def sslcontext(self) -> ssl.SSLContext:
"""Create/return our shared SSLContext.
This can be reused for all standard urllib requests/etc.
"""
# Note: I've run into older Android devices taking upwards of 1 second
# to put together a default SSLContext, so recycling one can definitely
# be a worthwhile optimization. This was suggested to me in this
# thread by one of Python's SSL maintainers:
# https://github.com/python/cpython/issues/94637
if self._sslcontext is None:
self._sslcontext = ssl.create_default_context()
return self._sslcontext
def get_ip_address_type(addr: str) -> socket.AddressFamily:
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
import socket
socket_type = None
# First try it as an ipv4 address.
try:
socket.inet_pton(socket.AF_INET, addr)
socket_type = socket.AF_INET
except OSError:
pass
# Hmm apparently not ipv4; try ipv6.
if socket_type is None:
try:
socket.inet_pton(socket.AF_INET6, addr)
socket_type = socket.AF_INET6
except OSError:
pass
if socket_type is None:
raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
return socket_type
class MasterServerResponseType(Enum):
"""How to interpret responses from the master-server."""
JSON = 0
class MasterServerCallThread(threading.Thread):
"""Thread to communicate with the master-server."""
def __init__(
self,
request: str,
request_type: str,
data: dict[str, Any] | None,
callback: MasterServerCallback | None,
response_type: MasterServerResponseType,
):
super().__init__()
self._request = request
self._request_type = request_type
if not isinstance(response_type, MasterServerResponseType):
raise TypeError(f'Invalid response type: {response_type}')
self._response_type = response_type
self._data = {} if data is None else copy.deepcopy(data)
self._callback: MasterServerCallback | None = callback
self._context = _ba.Context('current')
# Save and restore the context we were created from.
activity = _ba.getactivity(doraise=False)
self._activity = weakref.ref(activity) if activity is not None else None
def _run_callback(self, arg: None | dict[str, Any]) -> None:
# If we were created in an activity context and that activity has
# since died, do nothing.
# FIXME: Should we just be using a ContextCall instead of doing
# this check manually?
if self._activity is not None:
activity = self._activity()
if activity is None or activity.expired:
return
# Technically we could do the same check for session contexts,
# but not gonna worry about it for now.
assert self._context is not None
assert self._callback is not None
with self._context:
self._callback(arg)
def run(self) -> None:
# pylint: disable=consider-using-with
import urllib.request
import urllib.parse
import urllib.error
import json
from efro.error import is_urllib_communication_error
from ba._general import Call, utf8_all
from ba._internal import get_master_server_address
response_data: Any = None
url: str | None = None
try:
self._data = utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread')
if self._request_type == 'get':
url = (
get_master_server_address()
+ '/'
+ self._request
+ '?'
+ urllib.parse.urlencode(self._data)
)
response = urllib.request.urlopen(
urllib.request.Request(
url, None, {'User-Agent': _ba.app.user_agent_string}
),
context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
elif self._request_type == 'post':
url = get_master_server_address() + '/' + self._request
response = urllib.request.urlopen(
urllib.request.Request(
url,
urllib.parse.urlencode(self._data).encode(),
{'User-Agent': _ba.app.user_agent_string},
),
context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
else:
raise TypeError('Invalid request_type: ' + self._request_type)
# If html request failed.
if response.getcode() != 200:
response_data = None
elif self._response_type == MasterServerResponseType.JSON:
raw_data = response.read()
# Empty string here means something failed server side.
if raw_data == b'':
response_data = None
else:
response_data = json.loads(raw_data)
else:
raise TypeError(f'invalid responsetype: {self._response_type}')
except Exception as exc:
# Ignore common network errors; note unexpected ones.
if not is_urllib_communication_error(exc, url=url):
print(
f'Error in MasterServerCallThread'
f' (url={url},'
f' response-type={self._response_type},'
f' response-data={response_data}):'
)
import traceback
traceback.print_exc()
response_data = None
if self._callback is not None:
_ba.pushcall(
Call(self._run_callback, response_data), from_other_thread=True
)
def master_server_get(
request: str,
data: dict[str, Any],
callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None:
"""Make a call to the master server via a http GET."""
MasterServerCallThread(
request, 'get', data, callback, response_type
).start()
def master_server_post(
request: str,
data: dict[str, Any],
callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None:
"""Make a call to the master server via a http POST."""
MasterServerCallThread(
request, 'post', data, callback, response_type
).start()