diff --git a/steam/__init__.py b/steam/__init__.py index 52e4122..3b6d36f 100644 --- a/steam/__init__.py +++ b/steam/__init__.py @@ -3,3 +3,11 @@ __author__ = "Rossen Georgiev" from steam.steamid import SteamID from steam.webapi import WebAPI + + +# proxy object +# avoids importing steam.enums.emsg unless it's needed +class SteamClient(object): + def __new__(cls, *args, **kwargs): + from steam.client import SteamClient as SC + return SC(*args, **kwargs) diff --git a/steam/client.py b/steam/client.py new file mode 100644 index 0000000..fec5220 --- /dev/null +++ b/steam/client.py @@ -0,0 +1,117 @@ +import logging +import gevent +from steam.util.events import EventEmitter +from steam.enums.emsg import EMsg +from steam.enums import EResult +from steam.core.msg import MsgProto +from steam.core.cm import CMClient +from steam import SteamID + +logger = logging.getLogger("SteamClient") + +class SteamClient(EventEmitter): + def __init__(self): + self.cm = CMClient() + + # re-emit all events from CMClient + self.cm.on(None, self.emit) + + # register listners + self.on(EMsg.ClientLogOnResponse, self._handle_logon) + self.on("disconnected", self._handle_disconnect) + + self.logged_on = False + + def emit(self, event, *args): + if event is not None: + logger.debug("Emit event: %s" % repr(event)) + super(SteamClient, self).emit(event, *args) + + @property + def steam_id(self): + return self.cm.steam_id + @property + + def connected(self): + return self.cm.connected + + def connect(self): + self.cm.connect() + + def disconnect(self): + self.cm.disconnect() + + def _handle_disconnect(self): + self.logged_on = False + + def _handle_logon(self, msg): + result = EResult(msg.body.eresult) + + if result == EResult.OK: + self.emit("logged_on") + self.logged_on = True + return + + # CM kills the connection on error anyway + self.disconnect() + + if result in (EResult.AccountLogonDenied, + EResult.AccountLoginDeniedNeedTwoFactor, + EResult.TwoFactorCodeMismatch, + ): + self.emit("need_code", result) + else: + self.emit("error", result) + + def send(self, message): + self.cm.send_message(message) + + def anonymous_login(self): + logger.debug("Attempting Anonymous login") + + if self.logged_on: + logger.debug("Aready logged on") + return + if not self.connected: + self.connect() + + + self.wait_event("channel_secured") + + message = MsgProto(EMsg.ClientLogon) + message.header.steamid = SteamID(type='AnonUser', universe='Public') + message.body.protocol_version = 65575 + self.send(message) + + def login(self, username, password, auth_code=None, two_factor_code=None, remember=False): + logger.debug("Attempting login") + + if self.logged_on: + logger.debug("Aready logged on") + return + if not self.connected: + self.connect() + + self.wait_event("channel_secured") + + message = MsgProto(EMsg.ClientLogon) + message.header.steamid = SteamID(type='Individual', universe='Public') + message.body.protocol_version = 65575 + message.body.client_package_version = 1771 + message.body.client_os_type = 13 + message.body.client_language = "english" + + message.body.account_name = username + message.body.password = password + + if auth_code: + message.body.auth_code = auth_code + if two_factor_code: + message.body.two_factor_code = two_factor_code + + self.send(message) + + def logout(self): + if self.logged_on: + self.send(MsgProto(EMsg.ClientLogOff)) + self.logged_on = False diff --git a/steam/core/cm.py b/steam/core/cm.py index 5050383..808b833 100644 --- a/steam/core/cm.py +++ b/steam/core/cm.py @@ -61,7 +61,8 @@ class CMClient(EventEmitter): self.on(EMsg.ClientLogOnResponse, self._handle_logon), def emit(self, event, *args): - logger.debug("Emit event: %s" % str(event)) + if event is not None: + logger.debug("Emit event: %s" % repr(event)) super(CMClient, self).emit(event, *args) def connect(self): @@ -78,21 +79,26 @@ class CMClient(EventEmitter): break + self.connected = True self.emit("connected") self._recv_loop = gevent.spawn(self._recv_messages) - def disconnect(self): - self.connection.disconnect() + def disconnect(self, reconnect=False): + if reconnect: + gevent.spawn(self.connect) - self._recv_loop.kill(block=False) + self.connection.disconnect() if self._heartbeat_loop: self._heartbeat_loop.kill() + self._recv_loop.kill() self._init_attributes() self.emit('disconnected') def _init_attributes(self): + self.connected = False + self.key = None self.steam_id = None @@ -128,8 +134,7 @@ class CMClient(EventEmitter): message = self.connection.get_message(timeout=1) except queue.Empty: if not self.connection.event_connected.is_set(): - self.disconnect() - gevent.spawn(self.connect) + gevent.spawn(self.disconnect) return continue @@ -154,12 +159,12 @@ class CMClient(EventEmitter): msg = MsgProto(emsg, message) else: msg = Msg(emsg, message, extended=True) - except: - logger.fatal("Failed to deserialize message: %s %s", + except Exception as e: + logger.fatal("Failed to deserialize message: %s (is_proto: %s)", str(emsg), is_proto(emsg_id) ) - raise + logger.exception(e) if self.verbose_debug: logger.debug("Incoming: %s\n%s" % (repr(msg), str(msg))) @@ -185,15 +190,15 @@ class CMClient(EventEmitter): msg = self.wait_event(EMsg.ChannelEncryptResult) - if msg.body.result != EResult.OK: - logger.debug("Failed to secure channel: %s" % msg.body.result) + if msg.body.eresult != EResult.OK: + logger.debug("Failed to secure channel: %s" % msg.body.eresult) self.disconnect() return logger.debug("Channel secured") self.key = key - self.emit('ready') + self.emit('channel_secured') def _handle_multi(self, msg): logger.debug("Unpacking CMsgMulti") @@ -224,23 +229,23 @@ class CMClient(EventEmitter): def _handle_logon(self, msg): result = msg.body.eresult - if result != EResult.OK: - self.disconnect() - - if result in (EResult.TryAnotherCM, EResult.ServiceUnavailable): - gevent.spawn(self.connect) + if result in (EResult.TryAnotherCM, + EResult.ServiceUnavailable + ): + self.disconnect(True) return - logger.debug("Logon completed") + elif result == EResult.OK: + logger.debug("Logon completed") - self.steam_id = SteamID(msg.header.steamid) - self.session_id = msg.header.client_sessionid + self.steam_id = SteamID(msg.header.steamid) + self.session_id = msg.header.client_sessionid - if self._heartbeat_loop: - self._heartbeat_loop.kill() + if self._heartbeat_loop: + self._heartbeat_loop.kill() - logger.debug("Heartbeat started.") + logger.debug("Heartbeat started.") - interval = msg.body.out_of_game_heartbeat_seconds - self._heartbeat_loop = gevent.spawn(self._heartbeat, interval) + interval = msg.body.out_of_game_heartbeat_seconds + self._heartbeat_loop = gevent.spawn(self._heartbeat, interval) diff --git a/steam/core/msg.py b/steam/core/msg.py index 2b41bdd..7c0076c 100644 --- a/steam/core/msg.py +++ b/steam/core/msg.py @@ -139,20 +139,20 @@ class Msg(object): def __init__(self, msg, data=None, extended=False): self.extended = extended self.header = ExtendedMsgHdr(data) if extended else MsgHdr(data) + self.header.msg = msg self.msg = msg if data: data = data[self.header._size:] if msg == EMsg.ChannelEncryptRequest: - self.header.msg = EMsg.ChannelEncryptRequest self.body = ChannelEncryptRequest(data) elif msg == EMsg.ChannelEncryptResponse: - self.header.msg = EMsg.ChannelEncryptResponse self.body = ChannelEncryptResponse(data) elif msg == EMsg.ChannelEncryptResult: - self.header.msg = EMsg.ChannelEncryptResult self.body = ChannelEncryptResult(data) + elif msg == EMsg.ClientLogOnResponse: + self.body = ClientLogOnResponse(data) else: self.body = None @@ -214,15 +214,17 @@ def get_cmsg(emsg): if emsg == EMsg.Multi: return steammessages_base_pb2.CMsgMulti - - emsg = "cmsg" + str(emsg).split('.', 1)[1].lower() + elif emsg in (EMsg.ClientToGC, EMsg.ClientFromGC): + return steammessages_clientserver_2_pb2.CMsgGCClient + else: + cmsg_name = "cmsg" + str(emsg).split('.', 1)[1].lower() if not cmsg_lookup: cmsg_list = steammessages_clientserver_pb2.__dict__ cmsg_list = fnmatch.filter(cmsg_list, 'CMsg*') cmsg_lookup = dict(zip(map(lambda x: x.lower(), cmsg_list), cmsg_list)) - name = cmsg_lookup.get(emsg, None) + name = cmsg_lookup.get(cmsg_name, None) if name: return getattr(steammessages_clientserver_pb2, name) @@ -231,7 +233,7 @@ def get_cmsg(emsg): cmsg_list = fnmatch.filter(cmsg_list, 'CMsg*') cmsg_lookup2 = dict(zip(map(lambda x: x.lower(), cmsg_list), cmsg_list)) - name = cmsg_lookup2.get(emsg, None) + name = cmsg_lookup2.get(cmsg_name, None) if name: return getattr(steammessages_clientserver_2_pb2, name) @@ -356,14 +358,31 @@ class ChannelEncryptResult: if data: self.load(data) else: - self.result + self.eresult = EResult.Invalid + + def serialize(self): + return struct.pack("