Browse Source

turn CMClient into a mixin

pull/34/head
Rossen Georgiev 9 years ago
parent
commit
a98ec478a3
  1. 66
      steam/client/__init__.py
  2. 106
      steam/core/cm.py

66
steam/client/__init__.py

@ -14,10 +14,8 @@ from steam.core.cm import CMClient
from steam import SteamID from steam import SteamID
from steam.client.features import FeatureBase from steam.client.features import FeatureBase
logger = logging.getLogger("SteamClient")
class SteamClient(CMClient, FeatureBase):
class SteamClient(EventEmitter, FeatureBase):
""" """
Implementation of Steam client based on ``gevent`` Implementation of Steam client based on ``gevent``
@ -27,33 +25,29 @@ class SteamClient(EventEmitter, FeatureBase):
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
_logger = logger
def __init__(self): def __init__(self):
self.cm = CMClient() CMClient.__init__(self)
self._LOG = logging.getLogger("SteamClient")
# register listners # register listners
self.cm.on(None, self._handle_cm_events) self.on(None, self._handle_jobs)
self.cm.on("disconnected", self._handle_disconnect) self.on("disconnected", self._handle_disconnect)
self.cm.on("reconnect", self._handle_disconnect) self.on("reconnect", self._handle_disconnect)
self.on(EMsg.ClientLogOnResponse, self._handle_logon) self.on(EMsg.ClientLogOnResponse, self._handle_logon)
self.on(EMsg.ClientUpdateMachineAuth, self._handle_update_machine_auth) self.on(EMsg.ClientUpdateMachineAuth, self._handle_update_machine_auth)
#: indicates logged on status. Listen to ``logged_on`` when change to ``True`` #: indicates logged on status. Listen to ``logged_on`` when change to ``True``
self.logged_on = False self.logged_on = False
super(SteamClient, self).__init__() FeatureBase.__init__(self)
def __repr__(self): def __repr__(self):
return "<%s() %s>" % (self.__class__.__name__, return "<%s(%s) %s>" % (self.__class__.__name__,
repr(self.current_server_addr),
'online' if self.connected else 'offline', 'online' if self.connected else 'offline',
) )
def emit(self, event, *args):
if event is not None:
logger.debug("Emit event: %s" % repr(event))
super(SteamClient, self).emit(event, *args)
def set_credential_location(self, path): def set_credential_location(self, path):
""" """
Sets folder location for sentry files Sets folder location for sentry files
@ -62,37 +56,14 @@ class SteamClient(EventEmitter, FeatureBase):
""" """
self.credential_location = path self.credential_location = path
@property
def steam_id(self):
"""
``steam.steamid.SteamID`` of the current logged on user.
Points to invalid user, if not logged on.
"""
return self.cm.steam_id
@property
def connected(self):
"""
Monitor ``connected`` and ``disconnected`` events for when this changes.
"""
return self.cm.connected
def connect(self):
"""
Initiate connection
"""
self.cm.connect()
def disconnect(self): def disconnect(self):
""" """
Close connection Close connection
""" """
self.logged_on = False self.logged_on = False
self.cm.disconnect() CMClient.disconnect(self)
def _handle_cm_events(self, event, *args):
self.emit(event, *args)
def _handle_jobs(self, event, *args):
if isinstance(event, EMsg): if isinstance(event, EMsg):
message = args[0] message = args[0]
@ -166,8 +137,7 @@ class SteamClient(EventEmitter, FeatureBase):
""" """
if not self.connected: if not self.connected:
raise RuntimeError("Cannot send message while not connected") raise RuntimeError("Cannot send message while not connected")
CMClient.send(self, message)
self.cm.send_message(message)
def send_job(self, message): def send_job(self, message):
""" """
@ -278,7 +248,7 @@ class SteamClient(EventEmitter, FeatureBase):
with open(filepath, 'rb') as f: with open(filepath, 'rb') as f:
return f.read() return f.read()
except IOError as e: except IOError as e:
logger.error("get_sentry: %s" % str(e)) self._LOG.error("get_sentry: %s" % str(e))
return None return None
@ -298,19 +268,19 @@ class SteamClient(EventEmitter, FeatureBase):
f.write(sentry_bytes) f.write(sentry_bytes)
return True return True
except IOError as e: except IOError as e:
logger.error("store_sentry: %s" % str(e)) self._LOG.error("store_sentry: %s" % str(e))
return False return False
def _pre_login(self): def _pre_login(self):
if self.logged_on: if self.logged_on:
logger.debug("Trying to login while logged on???") self._LOG.debug("Trying to login while logged on???")
raise RuntimeError("Already logged on") raise RuntimeError("Already logged on")
if not self.connected: if not self.connected:
self.connect() self.connect()
if not self.cm.channel_secured: if not self.channel_secured:
self.wait_event("channel_secured") self.wait_event("channel_secured")
def login(self, username, password, auth_code=None, two_factor_code=None): def login(self, username, password, auth_code=None, two_factor_code=None):
@ -346,7 +316,7 @@ class SteamClient(EventEmitter, FeatureBase):
Codes are required every time a user logins if sentry is not setup. Codes are required every time a user logins if sentry is not setup.
See :meth:`set_credential_location` See :meth:`set_credential_location`
""" """
logger.debug("Attempting login") self._LOG.debug("Attempting login")
self._pre_login() self._pre_login()
@ -380,7 +350,7 @@ class SteamClient(EventEmitter, FeatureBase):
""" """
Login as anonymous user Login as anonymous user
""" """
logger.debug("Attempting Anonymous login") self._LOG.debug("Attempting Anonymous login")
self._pre_login() self._pre_login()

106
steam/core/cm.py

@ -21,12 +21,10 @@ from eventemitter import EventEmitter
from steam.util import ip_from_int, is_proto, clear_proto_bit from steam.util import ip_from_int, is_proto, clear_proto_bit
logger = logging.getLogger("CMClient")
class CMClient(EventEmitter): class CMClient(EventEmitter):
""" """
CMClient provides a secure message channel to Steam CM servers CMClient provides a secure message channel to Steam CM servers
Can be used as mixing or on it's own.
Incoming messages are parsed and emitted as events using Incoming messages are parsed and emitted as events using
their :class:`steam.enums.emsg.EMsg` as event identifier their :class:`steam.enums.emsg.EMsg` as event identifier
@ -42,8 +40,8 @@ class CMClient(EventEmitter):
connected = False #: :class:`True` if connected to CM connected = False #: :class:`True` if connected to CM
channel_secured = False #: :class:`True` once secure channel handshake is complete channel_secured = False #: :class:`True` once secure channel handshake is complete
key = None #: channel encryption key channel_key = None #: channel encryption key
hmac_secret = None #: HMAC secret channel_hmac = None #: HMAC secret
steam_id = SteamID() #: :class:`steam.steamid.SteamID` of the current user steam_id = SteamID() #: :class:`steam.steamid.SteamID` of the current user
session_id = None #: session id when logged in session_id = None #: session id when logged in
@ -52,8 +50,10 @@ class CMClient(EventEmitter):
_recv_loop = None _recv_loop = None
_heartbeat_loop = None _heartbeat_loop = None
_reconnect_backoff_c = 0 _reconnect_backoff_c = 0
_LOG = None
def __init__(self, protocol=0): def __init__(self, protocol=0):
self._LOG = logging.getLogger("CMClient")
self.servers = CMServerList() self.servers = CMServerList()
if protocol == CMClient.TCP: if protocol == CMClient.TCP:
@ -61,27 +61,27 @@ class CMClient(EventEmitter):
else: else:
raise ValueError("Only TCP is supported") raise ValueError("Only TCP is supported")
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:
logger.debug("Emit event: %s" % repr(event)) self._LOG.debug("Emit event: %s" % repr(event))
super(CMClient, self).emit(event, *args) super(CMClient, self).emit(event, *args)
def connect(self): def connect(self):
"""Initiate connection to CM. Blocks until we connect.""" """Initiate connection to CM. Blocks until we connect."""
if self.connected: if self.connected:
logger.debug("Connect called, but we are connected?") self._LOG.debug("Connect called, but we are connected?")
return return
if self._connecting: if self._connecting:
logger.debug("Connect called, but we are already connecting.") self._LOG.debug("Connect called, but we are already connecting.")
return return
self._connecting = True self._connecting = True
logger.debug("Connect initiated.") self._LOG.debug("Connect initiated.")
for server_addr in self.servers: for server_addr in self.servers:
start = time() start = time()
@ -91,7 +91,7 @@ class CMClient(EventEmitter):
diff = time() - start diff = time() - start
logger.debug("Failed to connect. Retrying...") self._LOG.debug("Failed to connect. Retrying...")
if diff < 5: if diff < 5:
gevent.sleep(5 - diff) gevent.sleep(5 - diff)
@ -153,7 +153,7 @@ class CMClient(EventEmitter):
for name in ['connected', for name in ['connected',
'channel_secured', 'channel_secured',
'key', 'key',
'hmac_secret', 'channel_hmac',
'steam_id', 'steam_id',
'session_id', 'session_id',
'webapi_authenticate_user_nonce', 'webapi_authenticate_user_nonce',
@ -162,9 +162,9 @@ class CMClient(EventEmitter):
]: ]:
self.__dict__.pop(name, None) self.__dict__.pop(name, None)
def send_message(self, message): def send(self, message):
""" """
Sends a message Send a message
:param message: a message instance :param message: a message instance
:type message: :class:`steam.core.msg.Msg`, :class:`steam.core.msg.MsgProto` :type message: :class:`steam.core.msg.Msg`, :class:`steam.core.msg.MsgProto`
@ -178,17 +178,17 @@ class CMClient(EventEmitter):
message.sessionID = self.session_id message.sessionID = self.session_id
if self.verbose_debug: if self.verbose_debug:
logger.debug("Outgoing: %s\n%s" % (repr(message), str(message))) self._LOG.debug("Outgoing: %s\n%s" % (repr(message), str(message)))
else: else:
logger.debug("Outgoing: %s", repr(message)) self._LOG.debug("Outgoing: %s", repr(message))
data = message.serialize() data = message.serialize()
if self.key: if self.channel_key:
if self.hmac_secret: if self.channel_hmac:
data = crypto.symmetric_encrypt_HMAC(data, self.key, self.hmac_secret) data = crypto.symmetric_encrypt_HMAC(data, self.channel_key, self.channel_hmac)
else: else:
data = crypto.symmetric_encrypt(data, self.key) data = crypto.symmetric_encrypt(data, self.channel_key)
self.connection.put_message(data) self.connection.put_message(data)
@ -197,16 +197,16 @@ class CMClient(EventEmitter):
if not self.connected: if not self.connected:
break break
if self.key: if self.channel_key:
if self.hmac_secret: if self.channel_hmac:
try: try:
message = crypto.symmetric_decrypt_HMAC(message, self.key, self.hmac_secret) message = crypto.symmetric_decrypt_HMAC(message, self.channel_key, self.channel_hmac)
except RuntimeError as e: except RuntimeError as e:
logger.exception(e) self._LOG.exception(e)
gevent.spawn(self.disconnect) gevent.spawn(self.disconnect)
return return
else: else:
message = crypto.symmetric_decrypt(message, self.key) message = crypto.symmetric_decrypt(message, self.channel_key)
gevent.spawn(self._parse_message, message) gevent.spawn(self._parse_message, message)
gevent.idle() gevent.idle()
@ -233,21 +233,21 @@ class CMClient(EventEmitter):
else: else:
msg = Msg(emsg, message, extended=True) msg = Msg(emsg, message, extended=True)
except Exception as e: except Exception as e:
logger.fatal("Failed to deserialize message: %s (is_proto: %s)", self._LOG.fatal("Failed to deserialize message: %s (is_proto: %s)",
str(emsg), str(emsg),
is_proto(emsg_id) is_proto(emsg_id)
) )
logger.exception(e) self._LOG.exception(e)
if self.verbose_debug: if self.verbose_debug:
logger.debug("Incoming: %s\n%s" % (repr(msg), str(msg))) self._LOG.debug("Incoming: %s\n%s" % (repr(msg), str(msg)))
else: else:
logger.debug("Incoming: %s", repr(msg)) self._LOG.debug("Incoming: %s", repr(msg))
self.emit(emsg, msg) self.emit(emsg, msg)
def _handle_encrypt_request(self, msg): def __handle_encrypt_request(self, msg):
logger.debug("Securing channel") self._LOG.debug("Securing channel")
try: try:
if msg.body.protocolVersion != 1: if msg.body.protocolVersion != 1:
@ -255,7 +255,7 @@ class CMClient(EventEmitter):
if msg.body.universe != EUniverse.Public: if msg.body.universe != EUniverse.Public:
raise RuntimeError("Unsupported universe") raise RuntimeError("Unsupported universe")
except RuntimeError as e: except RuntimeError as e:
logger.exception(e) self._LOG.exception(e)
gevent.spawn(self.disconnect) gevent.spawn(self.disconnect)
return return
@ -265,7 +265,7 @@ class CMClient(EventEmitter):
key, resp.body.key = crypto.generate_session_key(challenge) key, resp.body.key = crypto.generate_session_key(challenge)
resp.body.crc = binascii.crc32(resp.body.key) & 0xffffffff resp.body.crc = binascii.crc32(resp.body.key) & 0xffffffff
self.send_message(resp) self.send(resp)
resp = self.wait_event(EMsg.ChannelEncryptResult, timeout=5) resp = self.wait_event(EMsg.ChannelEncryptResult, timeout=5)
@ -277,32 +277,32 @@ class CMClient(EventEmitter):
msg, = resp msg, = resp
if msg.body.eresult != EResult.OK: if msg.body.eresult != EResult.OK:
logger.debug("Failed to secure channel: %s" % msg.body.eresult) self._LOG.debug("Failed to secure channel: %s" % msg.body.eresult)
gevent.spawn(self.disconnect) gevent.spawn(self.disconnect)
return return
self.key = key self.channel_key = key
if challenge: if challenge:
logger.debug("Channel secured") self._LOG.debug("Channel secured")
self.hmac_secret = key[:16] self.channel_hmac = key[:16]
else: else:
logger.debug("Channel secured (legacy mode)") self._LOG.debug("Channel secured (legacy mode)")
self.channel_secured = True self.channel_secured = True
self.emit('channel_secured') self.emit('channel_secured')
def _handle_multi(self, msg): def __handle_multi(self, msg):
logger.debug("Unpacking CMsgMulti") self._LOG.debug("Unpacking CMsgMulti")
if msg.body.size_unzipped: if msg.body.size_unzipped:
logger.debug("Unzipping body") self._LOG.debug("Unzipping body")
with GzipFile(fileobj=BytesIO(msg.body.message_body)) as f: with GzipFile(fileobj=BytesIO(msg.body.message_body)) as f:
data = f.read() data = f.read()
if len(data) != msg.body.size_unzipped: if len(data) != msg.body.size_unzipped:
logger.fatal("Unzipped size mismatch") self._LOG.fatal("Unzipped size mismatch")
gevent.spawn(self.disconnect) gevent.spawn(self.disconnect)
return return
else: else:
@ -313,14 +313,14 @@ class CMClient(EventEmitter):
self._parse_message(data[4:4+size]) self._parse_message(data[4:4+size])
data = data[4+size:] data = data[4+size:]
def _heartbeat(self, interval): def __heartbeat(self, interval):
message = MsgProto(EMsg.ClientHeartBeat) message = MsgProto(EMsg.ClientHeartBeat)
while True: while True:
gevent.sleep(interval) gevent.sleep(interval)
self.send_message(message) self.send(message)
def _handle_logon(self, msg): def __handle_logon(self, msg):
result = msg.body.eresult result = msg.body.eresult
if result in (EResult.TryAnotherCM, if result in (EResult.TryAnotherCM,
@ -331,7 +331,7 @@ class CMClient(EventEmitter):
elif result == EResult.OK: elif result == EResult.OK:
self._reconnect_backoff_c = 0 self._reconnect_backoff_c = 0
logger.debug("Logon completed") self._LOG.debug("Logon completed")
self.steam_id = SteamID(msg.header.steamid) self.steam_id = SteamID(msg.header.steamid)
self.session_id = msg.header.client_sessionid self.session_id = msg.header.client_sessionid
@ -341,16 +341,16 @@ class CMClient(EventEmitter):
if self._heartbeat_loop: if self._heartbeat_loop:
self._heartbeat_loop.kill() self._heartbeat_loop.kill()
logger.debug("Heartbeat started.") self._LOG.debug("Heartbeat started.")
interval = msg.body.out_of_game_heartbeat_seconds interval = msg.body.out_of_game_heartbeat_seconds
self._heartbeat_loop = gevent.spawn(self._heartbeat, interval) self._heartbeat_loop = gevent.spawn(self.__heartbeat, interval)
else: else:
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):
logger.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.servers.merge_list(new_servers)

Loading…
Cancel
Save