Browse Source

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.
pull/41/head
Rossen Georgiev 9 years ago
parent
commit
5f11d6d7cc
  1. 60
      steam/client/__init__.py
  2. 38
      steam/core/cm.py

60
steam/client/__init__.py

@ -25,6 +25,9 @@ Events
""" """
import os import os
import json
from time import time
from io import open
import logging import logging
import gevent import gevent
import gevent.monkey import gevent.monkey
@ -39,14 +42,16 @@ from steam.core.msg import MsgProto
from steam.core.cm import CMClient from steam.core.cm import CMClient
from steam import SteamID from steam import SteamID
from steam.client.builtins import BuiltinBase from steam.client.builtins import BuiltinBase
from steam.util import ip_from_int
class SteamClient(CMClient, BuiltinBase): class SteamClient(CMClient, BuiltinBase):
_cm_servers_timestamp = None # used to decide when to update CM list on disk
_reconnect_backoff_c = 0 _reconnect_backoff_c = 0
current_jobid = 0 current_jobid = 0
credential_location = None #: location for sentry credential_location = None #: location for sentry
username = None #: username when logged on username = None #: username when logged on
login_key = None #: can be used for subsequent logins (no 2FA code will be required) login_key = None #: can be used for subsequent logins (no 2FA code will be required)
def __init__(self): def __init__(self):
CMClient.__init__(self) CMClient.__init__(self)
@ -78,13 +83,56 @@ class SteamClient(CMClient, BuiltinBase):
""" """
self.credential_location = path 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): def disconnect(self, *args, **kwargs):
""" """Close connection, see :method:`.CMClient.disconnect`"""
Close connection
"""
self.logged_on = False self.logged_on = False
CMClient.disconnect(self, *args, **kwargs) 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): def _handle_jobs(self, event, *args):
if isinstance(event, EMsg): if isinstance(event, EMsg):
message = args[0] message = args[0]

38
steam/core/cm.py

@ -34,7 +34,7 @@ class CMClient(EventEmitter):
UDP = 1 #: UDP protocol enum UDP = 1 #: UDP protocol enum
verbose_debug = False #: print message connects in debug 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 current_server_addr = None #: (ip, port) tuple
_seen_logon = False _seen_logon = False
_connecting = False _connecting = False
@ -54,7 +54,7 @@ class CMClient(EventEmitter):
def __init__(self, protocol=0): def __init__(self, protocol=0):
self._LOG = logging.getLogger("CMClient") self._LOG = logging.getLogger("CMClient")
self.servers = CMServerList() self.cm_servers = CMServerList()
if protocol == CMClient.TCP: if protocol == CMClient.TCP:
self.connection = TCPConnection() self.connection = TCPConnection()
@ -64,7 +64,7 @@ class CMClient(EventEmitter):
self.on(EMsg.ChannelEncryptRequest, self.__handle_encrypt_request), self.on(EMsg.ChannelEncryptRequest, self.__handle_encrypt_request),
self.on(EMsg.Multi, self.__handle_multi), self.on(EMsg.Multi, self.__handle_multi),
self.on(EMsg.ClientLogOnResponse, self._handle_logon), 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): def emit(self, event, *args):
if event is not None: if event is not None:
@ -96,7 +96,7 @@ class CMClient(EventEmitter):
self._LOG.debug("Connect initiated.") 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: if retry and i > retry:
return False return False
@ -278,7 +278,7 @@ class CMClient(EventEmitter):
result = self.wait_event(EMsg.ChannelEncryptResult, timeout=5) result = self.wait_event(EMsg.ChannelEncryptResult, timeout=5)
if result is None: 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) gevent.spawn(self.disconnect)
return return
@ -337,7 +337,7 @@ class CMClient(EventEmitter):
if result in (EResult.TryAnotherCM, if result in (EResult.TryAnotherCM,
EResult.ServiceUnavailable EResult.ServiceUnavailable
): ):
self.servers.mark_bad(self.current_server_addr) self.cm_servers.mark_bad(self.current_server_addr)
self.disconnect() self.disconnect()
elif result == EResult.OK: elif result == EResult.OK:
self._seen_logon = True self._seen_logon = True
@ -360,11 +360,11 @@ class CMClient(EventEmitter):
self.emit("error", EResult(result)) self.emit("error", EResult(result))
self.disconnect() self.disconnect()
def __handle_cm_list(self, msg): def _handle_cm_list(self, msg):
self._LOG.debug("Updating CM list") self._LOG.debug("Updating CM list")
new_servers = zip(map(ip_from_int, msg.body.cm_addresses), msg.body.cm_ports) 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): class CMServerList(object):
@ -397,12 +397,19 @@ class CMServerList(object):
self.bootstrap_from_builtin_list() 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): def bootstrap_from_builtin_list(self):
""" """
Resets the server list to the built in one. Resets the server list to the built in one.
This method is called during initialization. This method is called during initialization.
""" """
self.list.clear() self._log.debug("Bootstraping from builtin list")
self.clear()
# build-in list # build-in list
self.merge_list([('162.254.195.44', 27019), ('146.66.152.11', 27017), self.merge_list([('162.254.195.44', 27019), ('146.66.152.11', 27017),
@ -431,8 +438,9 @@ class CMServerList(object):
:return: booststrap success :return: booststrap success
:rtype: :class:`bool` :rtype: :class:`bool`
""" """
from steam import _webapi self._log.debug("Attempting bootstrap via WebAPI")
from steam import _webapi
try: try:
resp = _webapi.get('ISteamDirectory', 'GetCMList', 1, params={'cellid': cellid}) resp = _webapi.get('ISteamDirectory', 'GetCMList', 1, params={'cellid': cellid})
except Exception as exp: except Exception as exp:
@ -452,12 +460,11 @@ class CMServerList(object):
ip, port = serveraddr.split(':') ip, port = serveraddr.split(':')
return str(ip), int(port) return str(ip), int(port)
self.list.clear() self.clear()
self.merge_list(map(str_to_tuple, serverlist)) self.merge_list(map(str_to_tuple, serverlist))
return True return True
def __iter__(self): def __iter__(self):
def genfunc(): def genfunc():
while True: while True:
@ -502,13 +509,14 @@ class CMServerList(object):
def merge_list(self, new_list): def merge_list(self, new_list):
"""Add new CM servers to the 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` :type new_list: :class:`list`
""" """
total = len(self.list) total = len(self.list)
for ip, port in new_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)) self._log.debug("Added %d new CM addresses." % (len(self.list) - total))

Loading…
Cancel
Save