# 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()