vh-bombsquad-modded-server-.../dist/ba_data/python/ba/_netutils.py
2024-06-06 19:50:58 +05:30

186 lines
6.8 KiB
Python

# Released under the MIT License. See LICENSE for details.
#
"""Networking related functionality."""
from __future__ import annotations
import copy
import threading
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Any, Dict, Union, Callable, Optional
import socket
import ba
ServerCallbackType = Callable[[Union[None, Dict[str, Any]]], None]
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 ServerResponseType(Enum):
"""How to interpret responses from the server."""
JSON = 0
class ServerCallThread(threading.Thread):
"""Thread to communicate with the master server."""
def __init__(self, request: str, request_type: str,
data: Optional[Dict[str, Any]],
callback: Optional[ServerCallbackType],
response_type: ServerResponseType):
super().__init__()
self._request = request
self._request_type = request_type
if not isinstance(response_type, ServerResponseType):
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: Optional[ServerCallbackType] = 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: Union[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=too-many-branches
import urllib.request
import urllib.error
import json
import http.client
from ba import _general
try:
self._data = _general.utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread')
parse = urllib.parse
if self._request_type == 'get':
response = urllib.request.urlopen(
urllib.request.Request(
(_ba.get_master_server_address() + '/' +
self._request + '?' + parse.urlencode(self._data)),
None, {'User-Agent': _ba.app.user_agent_string}))
elif self._request_type == 'post':
response = urllib.request.urlopen(
urllib.request.Request(
_ba.get_master_server_address() + '/' + self._request,
parse.urlencode(self._data).encode(),
{'User-Agent': _ba.app.user_agent_string}))
else:
raise TypeError('Invalid request_type: ' + self._request_type)
# If html request failed.
if response.getcode() != 200:
response_data = None
elif self._response_type == ServerResponseType.JSON:
raw_data = response.read()
# Empty string here means something failed server side.
if raw_data == b'':
response_data = None
else:
# Json.loads requires str in python < 3.6.
raw_data_s = raw_data.decode()
response_data = json.loads(raw_data_s)
else:
raise TypeError(f'invalid responsetype: {self._response_type}')
except Exception as exc:
import errno
do_print = False
response_data = None
# Ignore common network errors; note unexpected ones.
if isinstance(
exc,
(urllib.error.URLError, ConnectionError,
http.client.IncompleteRead, http.client.BadStatusLine)):
pass
elif isinstance(exc, OSError):
if exc.errno == 10051: # Windows unreachable network error.
pass
elif exc.errno in [
errno.ETIMEDOUT, errno.EHOSTUNREACH, errno.ENETUNREACH
]:
pass
else:
do_print = True
elif (self._response_type == ServerResponseType.JSON
and isinstance(exc, json.decoder.JSONDecodeError)):
pass
else:
do_print = True
if do_print:
# Any other error here is unexpected,
# so let's make a note of it,
print(f'Error in ServerCallThread'
f' (response-type={self._response_type},'
f' response-data={response_data}):')
import traceback
traceback.print_exc()
if self._callback is not None:
_ba.pushcall(_general.Call(self._run_callback, response_data),
from_other_thread=True)
def master_server_get(
request: str,
data: Dict[str, Any],
callback: Optional[ServerCallbackType] = None,
response_type: ServerResponseType = ServerResponseType.JSON) -> None:
"""Make a call to the master server via a http GET."""
ServerCallThread(request, 'get', data, callback, response_type).start()
def master_server_post(
request: str,
data: Dict[str, Any],
callback: Optional[ServerCallbackType] = None,
response_type: ServerResponseType = ServerResponseType.JSON) -> None:
"""Make a call to the master server via a http POST."""
ServerCallThread(request, 'post', data, callback, response_type).start()