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.client.features import FeatureBase
logger = logging.getLogger("SteamClient")
class SteamClient(EventEmitter, FeatureBase):
class SteamClient(CMClient, FeatureBase):
"""
Implementation of Steam client based on ``gevent``
@ -27,33 +25,29 @@ class SteamClient(EventEmitter, FeatureBase):
current_jobid = 0
credential_location = None #: location for sentry
username = None #: username when logged on
_logger = logger
def __init__(self):
self.cm = CMClient()
CMClient.__init__(self)
self._LOG = logging.getLogger("SteamClient")
# register listners
self.cm.on(None, self._handle_cm_events)
self.cm.on("disconnected", self._handle_disconnect)
self.cm.on("reconnect", self._handle_disconnect)
self.on(None, self._handle_jobs)
self.on("disconnected", self._handle_disconnect)
self.on("reconnect", self._handle_disconnect)
self.on(EMsg.ClientLogOnResponse, self._handle_logon)
self.on(EMsg.ClientUpdateMachineAuth, self._handle_update_machine_auth)
#: indicates logged on status. Listen to ``logged_on`` when change to ``True``
self.logged_on = False
super(SteamClient, self).__init__()
FeatureBase.__init__(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',
)
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):
"""
Sets folder location for sentry files
@ -62,37 +56,14 @@ class SteamClient(EventEmitter, FeatureBase):
"""
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):
"""
Close connection
"""
self.logged_on = False
self.cm.disconnect()
def _handle_cm_events(self, event, *args):
self.emit(event, *args)
CMClient.disconnect(self)
def _handle_jobs(self, event, *args):
if isinstance(event, EMsg):
message = args[0]
@ -166,8 +137,7 @@ class SteamClient(EventEmitter, FeatureBase):
"""
if not self.connected:
raise RuntimeError("Cannot send message while not connected")
self.cm.send_message(message)
CMClient.send(self, message)
def send_job(self, message):
"""
@ -278,7 +248,7 @@ class SteamClient(EventEmitter, FeatureBase):
with open(filepath, 'rb') as f:
return f.read()
except IOError as e:
logger.error("get_sentry: %s" % str(e))
self._LOG.error("get_sentry: %s" % str(e))
return None
@ -298,19 +268,19 @@ class SteamClient(EventEmitter, FeatureBase):
f.write(sentry_bytes)
return True
except IOError as e:
logger.error("store_sentry: %s" % str(e))
self._LOG.error("store_sentry: %s" % str(e))
return False
def _pre_login(self):
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")
if not self.connected:
self.connect()
if not self.cm.channel_secured:
if not self.channel_secured:
self.wait_event("channel_secured")
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.
See :meth:`set_credential_location`
"""
logger.debug("Attempting login")
self._LOG.debug("Attempting login")
self._pre_login()
@ -380,7 +350,7 @@ class SteamClient(EventEmitter, FeatureBase):
"""
Login as anonymous user
"""
logger.debug("Attempting Anonymous login")
self._LOG.debug("Attempting Anonymous 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
logger = logging.getLogger("CMClient")
class CMClient(EventEmitter):
"""
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
their :class:`steam.enums.emsg.EMsg` as event identifier
@ -42,8 +40,8 @@ class CMClient(EventEmitter):
connected = False #: :class:`True` if connected to CM
channel_secured = False #: :class:`True` once secure channel handshake is complete
key = None #: channel encryption key
hmac_secret = None #: HMAC secret
channel_key = None #: channel encryption key
channel_hmac = None #: HMAC secret
steam_id = SteamID() #: :class:`steam.steamid.SteamID` of the current user
session_id = None #: session id when logged in
@ -52,8 +50,10 @@ class CMClient(EventEmitter):
_recv_loop = None
_heartbeat_loop = None
_reconnect_backoff_c = 0
_LOG = None
def __init__(self, protocol=0):
self._LOG = logging.getLogger("CMClient")
self.servers = CMServerList()
if protocol == CMClient.TCP:
@ -61,27 +61,27 @@ class CMClient(EventEmitter):
else:
raise ValueError("Only TCP is supported")
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.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),
def emit(self, event, *args):
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)
def connect(self):
"""Initiate connection to CM. Blocks until we connect."""
if self.connected:
logger.debug("Connect called, but we are connected?")
self._LOG.debug("Connect called, but we are connected?")
return
if self._connecting:
logger.debug("Connect called, but we are already connecting.")
self._LOG.debug("Connect called, but we are already connecting.")
return
self._connecting = True
logger.debug("Connect initiated.")
self._LOG.debug("Connect initiated.")
for server_addr in self.servers:
start = time()
@ -91,7 +91,7 @@ class CMClient(EventEmitter):
diff = time() - start
logger.debug("Failed to connect. Retrying...")
self._LOG.debug("Failed to connect. Retrying...")
if diff < 5:
gevent.sleep(5 - diff)
@ -153,7 +153,7 @@ class CMClient(EventEmitter):
for name in ['connected',
'channel_secured',
'key',
'hmac_secret',
'channel_hmac',
'steam_id',
'session_id',
'webapi_authenticate_user_nonce',
@ -162,9 +162,9 @@ class CMClient(EventEmitter):
]:
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
:type message: :class:`steam.core.msg.Msg`, :class:`steam.core.msg.MsgProto`
@ -178,17 +178,17 @@ class CMClient(EventEmitter):
message.sessionID = self.session_id
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:
logger.debug("Outgoing: %s", repr(message))
self._LOG.debug("Outgoing: %s", repr(message))
data = message.serialize()
if self.key:
if self.hmac_secret:
data = crypto.symmetric_encrypt_HMAC(data, self.key, self.hmac_secret)
if self.channel_key:
if self.channel_hmac:
data = crypto.symmetric_encrypt_HMAC(data, self.channel_key, self.channel_hmac)
else:
data = crypto.symmetric_encrypt(data, self.key)
data = crypto.symmetric_encrypt(data, self.channel_key)
self.connection.put_message(data)
@ -197,16 +197,16 @@ class CMClient(EventEmitter):
if not self.connected:
break
if self.key:
if self.hmac_secret:
if self.channel_key:
if self.channel_hmac:
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:
logger.exception(e)
self._LOG.exception(e)
gevent.spawn(self.disconnect)
return
else:
message = crypto.symmetric_decrypt(message, self.key)
message = crypto.symmetric_decrypt(message, self.channel_key)
gevent.spawn(self._parse_message, message)
gevent.idle()
@ -233,21 +233,21 @@ class CMClient(EventEmitter):
else:
msg = Msg(emsg, message, extended=True)
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),
is_proto(emsg_id)
)
logger.exception(e)
self._LOG.exception(e)
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:
logger.debug("Incoming: %s", repr(msg))
self._LOG.debug("Incoming: %s", repr(msg))
self.emit(emsg, msg)
def _handle_encrypt_request(self, msg):
logger.debug("Securing channel")
def __handle_encrypt_request(self, msg):
self._LOG.debug("Securing channel")
try:
if msg.body.protocolVersion != 1:
@ -255,7 +255,7 @@ class CMClient(EventEmitter):
if msg.body.universe != EUniverse.Public:
raise RuntimeError("Unsupported universe")
except RuntimeError as e:
logger.exception(e)
self._LOG.exception(e)
gevent.spawn(self.disconnect)
return
@ -265,7 +265,7 @@ class CMClient(EventEmitter):
key, resp.body.key = crypto.generate_session_key(challenge)
resp.body.crc = binascii.crc32(resp.body.key) & 0xffffffff
self.send_message(resp)
self.send(resp)
resp = self.wait_event(EMsg.ChannelEncryptResult, timeout=5)
@ -277,32 +277,32 @@ class CMClient(EventEmitter):
msg, = resp
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)
return
self.key = key
self.channel_key = key
if challenge:
logger.debug("Channel secured")
self.hmac_secret = key[:16]
self._LOG.debug("Channel secured")
self.channel_hmac = key[:16]
else:
logger.debug("Channel secured (legacy mode)")
self._LOG.debug("Channel secured (legacy mode)")
self.channel_secured = True
self.emit('channel_secured')
def _handle_multi(self, msg):
logger.debug("Unpacking CMsgMulti")
def __handle_multi(self, msg):
self._LOG.debug("Unpacking CMsgMulti")
if msg.body.size_unzipped:
logger.debug("Unzipping body")
self._LOG.debug("Unzipping body")
with GzipFile(fileobj=BytesIO(msg.body.message_body)) as f:
data = f.read()
if len(data) != msg.body.size_unzipped:
logger.fatal("Unzipped size mismatch")
self._LOG.fatal("Unzipped size mismatch")
gevent.spawn(self.disconnect)
return
else:
@ -313,14 +313,14 @@ class CMClient(EventEmitter):
self._parse_message(data[4:4+size])
data = data[4+size:]
def _heartbeat(self, interval):
def __heartbeat(self, interval):
message = MsgProto(EMsg.ClientHeartBeat)
while True:
gevent.sleep(interval)
self.send_message(message)
self.send(message)
def _handle_logon(self, msg):
def __handle_logon(self, msg):
result = msg.body.eresult
if result in (EResult.TryAnotherCM,
@ -331,7 +331,7 @@ class CMClient(EventEmitter):
elif result == EResult.OK:
self._reconnect_backoff_c = 0
logger.debug("Logon completed")
self._LOG.debug("Logon completed")
self.steam_id = SteamID(msg.header.steamid)
self.session_id = msg.header.client_sessionid
@ -341,16 +341,16 @@ class CMClient(EventEmitter):
if self._heartbeat_loop:
self._heartbeat_loop.kill()
logger.debug("Heartbeat started.")
self._LOG.debug("Heartbeat started.")
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:
self.emit("error", EResult(result))
self.disconnect()
def _handle_cm_list(self, msg):
logger.debug("Updating CM list")
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)

Loading…
Cancel
Save