From 5f11d6d7cc15ba0eb810150bd5cd074fbab8840f Mon Sep 17 00:00:00 2001 From: Rossen Georgiev Date: Mon, 6 Jun 2016 07:29:57 +0100 Subject: [PATCH] persist CM server list in credential location fix #31 When credential location is set, SteamClient will save CM server list and use it to bootstrap when necessary. --- steam/client/__init__.py | 60 ++++++++++++++++++++++++++++++++++++---- steam/core/cm.py | 38 +++++++++++++++---------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/steam/client/__init__.py b/steam/client/__init__.py index a465cb7..c208512 100644 --- a/steam/client/__init__.py +++ b/steam/client/__init__.py @@ -25,6 +25,9 @@ Events """ import os +import json +from time import time +from io import open import logging import gevent import gevent.monkey @@ -39,14 +42,16 @@ from steam.core.msg import MsgProto from steam.core.cm import CMClient from steam import SteamID from steam.client.builtins import BuiltinBase +from steam.util import ip_from_int class SteamClient(CMClient, BuiltinBase): + _cm_servers_timestamp = None # used to decide when to update CM list on disk _reconnect_backoff_c = 0 current_jobid = 0 - credential_location = None #: location for sentry - username = None #: username when logged on - login_key = None #: can be used for subsequent logins (no 2FA code will be required) + credential_location = None #: location for sentry + username = None #: username when logged on + login_key = None #: can be used for subsequent logins (no 2FA code will be required) def __init__(self): CMClient.__init__(self) @@ -78,13 +83,56 @@ class SteamClient(CMClient, BuiltinBase): """ self.credential_location = path + def connect(self, *args, **kwargs): + """Attempt to establish connection, see :method:`.CMClient.connect`""" + self._bootstrap_cm_list_from_file() + CMClient.connect(self, *args, **kwargs) + def disconnect(self, *args, **kwargs): - """ - Close connection - """ + """Close connection, see :method:`.CMClient.disconnect`""" self.logged_on = False CMClient.disconnect(self, *args, **kwargs) + def _bootstrap_cm_list_from_file(self): + if not self.credential_location or self._cm_servers_timestamp is not None: return + + filepath = os.path.join(self.credential_location, 'cm_servers.json') + if not os.path.isfile(filepath): return + + self._LOG.debug("Reading CM servers from %s" % repr(filepath)) + try: + with open(filepath, 'r') as f: + data = json.load(f) + except IOError as e: + self._LOG.error("load %s: %s" % (repr(filepath), str(e))) + return + + self.cm_servers.clear() + self.cm_servers.merge_list(data['servers']) + self._cm_servers_timestamp = int(data['timestamp']) + + def _handle_cm_list(self, msg): + if self._cm_servers_timestamp is None: + self.cm_servers.clear() + self._cm_servers_timestamp = int(time()) + + CMClient._handle_cm_list(self, msg) # just merges the list + + if self.credential_location: + filepath = os.path.join(self.credential_location, 'cm_servers.json') + + if not os.path.exists(filepath) or time() - (3600*24) > self._cm_servers_timestamp: + data = { + 'timestamp': self._cm_servers_timestamp, + 'servers': list(zip(map(ip_from_int, msg.body.cm_addresses), msg.body.cm_ports)), + } + try: + with open(filepath, 'wb') as f: + f.write(json.dumps(data, indent=True).encode('ascii')) + self._LOG.debug("Saved CM servers to %s" % repr(filepath)) + except IOError as e: + self._LOG.error("saving %s: %s" % (filepath, str(e))) + def _handle_jobs(self, event, *args): if isinstance(event, EMsg): message = args[0] diff --git a/steam/core/cm.py b/steam/core/cm.py index 5d11df8..24df23c 100644 --- a/steam/core/cm.py +++ b/steam/core/cm.py @@ -34,7 +34,7 @@ class CMClient(EventEmitter): UDP = 1 #: UDP protocol enum verbose_debug = False #: print message connects in debug - servers = None #: a instance of :class:`steam.core.cm.CMServerList` + cm_servers = None #: a instance of :class:`steam.core.cm.CMServerList` current_server_addr = None #: (ip, port) tuple _seen_logon = False _connecting = False @@ -54,7 +54,7 @@ class CMClient(EventEmitter): def __init__(self, protocol=0): self._LOG = logging.getLogger("CMClient") - self.servers = CMServerList() + self.cm_servers = CMServerList() if protocol == CMClient.TCP: self.connection = TCPConnection() @@ -64,7 +64,7 @@ class CMClient(EventEmitter): self.on(EMsg.ChannelEncryptRequest, self.__handle_encrypt_request), self.on(EMsg.Multi, self.__handle_multi), self.on(EMsg.ClientLogOnResponse, self._handle_logon), - self.on(EMsg.ClientCMList, self.__handle_cm_list), + self.on(EMsg.ClientCMList, self._handle_cm_list), def emit(self, event, *args): if event is not None: @@ -96,7 +96,7 @@ class CMClient(EventEmitter): self._LOG.debug("Connect initiated.") - for i, server_addr in enumerate(self.servers): + for i, server_addr in enumerate(self.cm_servers): if retry and i > retry: return False @@ -278,7 +278,7 @@ class CMClient(EventEmitter): result = self.wait_event(EMsg.ChannelEncryptResult, timeout=5) if result is None: - self.servers.mark_bad(self.current_server_addr) + self.cm_servers.mark_bad(self.current_server_addr) gevent.spawn(self.disconnect) return @@ -337,7 +337,7 @@ class CMClient(EventEmitter): if result in (EResult.TryAnotherCM, EResult.ServiceUnavailable ): - self.servers.mark_bad(self.current_server_addr) + self.cm_servers.mark_bad(self.current_server_addr) self.disconnect() elif result == EResult.OK: self._seen_logon = True @@ -360,11 +360,11 @@ class CMClient(EventEmitter): self.emit("error", EResult(result)) self.disconnect() - def __handle_cm_list(self, msg): + def _handle_cm_list(self, msg): self._LOG.debug("Updating CM list") new_servers = zip(map(ip_from_int, msg.body.cm_addresses), msg.body.cm_ports) - self.servers.merge_list(new_servers) + self.cm_servers.merge_list(new_servers) class CMServerList(object): @@ -397,12 +397,19 @@ class CMServerList(object): self.bootstrap_from_builtin_list() + def clear(self): + """Clears the server list""" + if len(self.list): + self._log.debug("List cleared.") + self.list.clear() + def bootstrap_from_builtin_list(self): """ Resets the server list to the built in one. This method is called during initialization. """ - self.list.clear() + self._log.debug("Bootstraping from builtin list") + self.clear() # build-in list self.merge_list([('162.254.195.44', 27019), ('146.66.152.11', 27017), @@ -431,8 +438,9 @@ class CMServerList(object): :return: booststrap success :rtype: :class:`bool` """ - from steam import _webapi + self._log.debug("Attempting bootstrap via WebAPI") + from steam import _webapi try: resp = _webapi.get('ISteamDirectory', 'GetCMList', 1, params={'cellid': cellid}) except Exception as exp: @@ -452,12 +460,11 @@ class CMServerList(object): ip, port = serveraddr.split(':') return str(ip), int(port) - self.list.clear() + self.clear() self.merge_list(map(str_to_tuple, serverlist)) return True - def __iter__(self): def genfunc(): while True: @@ -502,13 +509,14 @@ class CMServerList(object): def merge_list(self, new_list): """Add new CM servers to the list - :param new_list: a list of (ip, port) tuples + :param new_list: a list of ``(ip, port)`` tuples :type new_list: :class:`list` """ total = len(self.list) for ip, port in new_list: - self.mark_good((ip, port)) + if (ip, port) not in self.list: + self.mark_good((ip, port)) - if total: + if len(self.list) > total: self._log.debug("Added %d new CM addresses." % (len(self.list) - total))