4 changed files with 700 additions and 2 deletions
@ -0,0 +1,277 @@ |
|||
import struct |
|||
import binascii |
|||
import logging |
|||
import zipfile |
|||
|
|||
try: |
|||
from cStringIO import StringIO |
|||
except ImportError: |
|||
from StringIO import StringIO |
|||
|
|||
import gevent |
|||
from gevent import event |
|||
from gevent import queue |
|||
from Crypto.Random import random |
|||
|
|||
from steam.steamid import SteamID |
|||
from steam.enums import EMsg, EResult, EUniverse |
|||
from steam.client import crypto |
|||
from steam.client.connection import TCPConnection |
|||
from steam.client.msg import is_proto, clear_proto_bit |
|||
from steam.client.msg import Msg, MsgProto |
|||
|
|||
server_list = [ |
|||
('162.254.196.41', '27020'), ('162.254.196.40', '27021'), |
|||
('162.254.196.43', '27019'), ('162.254.196.40', '27018'), |
|||
('162.254.196.43', '27020'), ('162.254.196.41', '27019'), |
|||
('162.254.196.41', '27018'), ('162.254.196.42', '27020'), |
|||
('162.254.196.41', '27017'), ('162.254.196.41', '27021'), |
|||
('146.66.152.10', '27017'), ('146.66.152.10', '27018'), |
|||
('146.66.152.11', '27019'), ('146.66.152.11', '27020'), |
|||
('146.66.152.10', '27019'), ('162.254.197.42', '27018'), |
|||
('162.254.197.41', '27019'), ('162.254.197.41', '27017'), |
|||
('208.78.164.14', '27017'), ('208.78.164.14', '27019'), |
|||
('208.78.164.9', '27019'), ('208.78.164.14', '27018'), |
|||
('208.78.164.9', '27018'), ('208.78.164.13', '27017'), |
|||
] |
|||
|
|||
logger = logging.getLogger("CMClient") |
|||
|
|||
|
|||
class CMClient: |
|||
TCP = 0 |
|||
UDP = 1 |
|||
|
|||
def __init__(self, protocol=0): |
|||
self.reconnect = False |
|||
|
|||
self._init_attributes() |
|||
|
|||
self.registered_callbacks = {} |
|||
|
|||
if protocol == CMClient.TCP: |
|||
self.connection = TCPConnection() |
|||
# elif protocol == CMClient.UDP: |
|||
# self.connection = UDPConnection() |
|||
else: |
|||
raise ValueError("Only TCP is supported") |
|||
|
|||
self.event_connected = event.Event() |
|||
self.event_ready = event.Event() |
|||
self.event_disconnected = event.Event() |
|||
|
|||
self.register_callback(EMsg.ChannelEncryptRequest, self._handle_encrypt_request), |
|||
self.register_callback(EMsg.Multi, self._handle_multi), |
|||
self.register_callback(EMsg.ClientLogOnResponse, self._handle_logon), |
|||
|
|||
def connect(self, reconnect=None): |
|||
if reconnect is not None: |
|||
self.reconnect = reconnect |
|||
|
|||
logger.debug("Connect (reconnect=%s)" % self.reconnect) |
|||
|
|||
while True: |
|||
with gevent.Timeout(15, False): |
|||
server_addr = random.choice(server_list) |
|||
self.connection.connect(server_addr) |
|||
|
|||
if not self.connection.event_connected.is_set(): |
|||
if self.reconnect: |
|||
logger.debug("Failed to connect. Retrying...") |
|||
continue |
|||
|
|||
logger.debug("Failed to connect") |
|||
return False |
|||
break |
|||
|
|||
logger.debug("Event: Connected") |
|||
self.event_connected.set() |
|||
self._recv_loop = gevent.spawn(self._recv_messages) |
|||
return True |
|||
|
|||
def disconnect(self): |
|||
self.connection.disconnect() |
|||
|
|||
self._recv_loop.kill(block=False) |
|||
self._heartbeat_loop.kill() |
|||
|
|||
self._init_attributes() |
|||
|
|||
self.event_connected.clear() |
|||
self.event_ready.clear() |
|||
self.event_disconnected.set() |
|||
|
|||
logger.debug("Event: Disconnected") |
|||
|
|||
def _init_attributes(self): |
|||
self.key = None |
|||
|
|||
self.steam_id = None |
|||
self.session_id = None |
|||
|
|||
self.cell_id = None |
|||
self.webapi_nonce = None |
|||
|
|||
self._recv_loop = None |
|||
self._heartbeat_loop = None |
|||
|
|||
def send_message(self, message): |
|||
if not isinstance(message, (Msg, MsgProto)): |
|||
raise ValueError("Expected Msg or MsgProto, got %s" % message) |
|||
|
|||
data = message.serialize() |
|||
|
|||
if self.key: |
|||
data = crypto.encrypt(data, self.key) |
|||
|
|||
logger.debug("Outgoing: %s", repr(message.msg)) |
|||
self.connection.put_message(data) |
|||
|
|||
def _recv_messages(self): |
|||
while True: |
|||
try: |
|||
message = self.connection.get_message(timeout=1) |
|||
except queue.Empty: |
|||
if not self.connection.event_connected.is_set(): |
|||
self.disconnect() |
|||
if self.reconnect: |
|||
gevent.spawn(self.connect) |
|||
return |
|||
continue |
|||
|
|||
if self.key: |
|||
message = crypto.decrypt(message, self.key) |
|||
|
|||
self._parse_message(message) |
|||
|
|||
def _parse_message(self, message): |
|||
emsg_id, = struct.unpack_from("<I", message) |
|||
emsg = EMsg(clear_proto_bit(emsg_id)) |
|||
|
|||
logger.debug("Incoming: %s", repr(emsg)) |
|||
|
|||
if emsg in (EMsg.ChannelEncryptRequest, |
|||
EMsg.ChannelEncryptResponse, |
|||
EMsg.ChannelEncryptResult, |
|||
): |
|||
|
|||
msg = Msg(emsg, message) |
|||
else: |
|||
try: |
|||
if is_proto(emsg_id): |
|||
msg = MsgProto(emsg, message) |
|||
print str(msg) |
|||
else: |
|||
msg = Msg(emsg, message, extended=True) |
|||
except: |
|||
logger.fatal("Failed to deserialize message: %s %s", |
|||
str(emsg), |
|||
is_proto(emsg_id) |
|||
) |
|||
raise |
|||
|
|||
self.dispatch_message(emsg, msg) |
|||
|
|||
def dispatch_message(self, emsg, msg): |
|||
if emsg in self.registered_callbacks: |
|||
for callback in list(self.registered_callbacks[emsg]): |
|||
if isinstance(callback, event.AsyncResult): |
|||
self.unregister_callback(emsg, callback) |
|||
callback.set((emsg, msg)) |
|||
else: |
|||
gevent.spawn(callback, emsg, msg) |
|||
|
|||
def register_callback(self, emsg, callback): |
|||
if emsg not in self.registered_callbacks: |
|||
self.registered_callbacks[emsg] = [callback] |
|||
else: |
|||
callbacks = self.registered_callbacks[emsg] |
|||
|
|||
if callback not in callbacks: |
|||
callbacks.append(callback) |
|||
|
|||
def unregister_callback(self, emsg, callback): |
|||
if (emsg not in self.registered_callbacks |
|||
or callback not in self.registered_callbacks[emsg]): |
|||
return ValueError("Callback is not registered") |
|||
|
|||
callbacks = self.registered_callbacks[emsg] |
|||
|
|||
if len(callbacks) == 1: |
|||
self.registered_callbacks.pop(emsg) |
|||
else: |
|||
callbacks.pop(callbacks.index(callback)) |
|||
|
|||
def wait_for_message(self, emsg, block=True, timeout=None): |
|||
result = event.AsyncResult() |
|||
self.register_callback(emsg, result) |
|||
return result.get(block, timeout)[1] |
|||
|
|||
def _handle_encrypt_request(self, emsg, msg): |
|||
logger.debug("Securing channel") |
|||
|
|||
if msg.body.protocolVersion != 1: |
|||
raise RuntimeError("Unsupported protocol version") |
|||
if msg.body.universe != EUniverse.Public: |
|||
raise RuntimeError("Unsupported universe") |
|||
|
|||
resp = Msg(EMsg.ChannelEncryptResponse) |
|||
|
|||
key, resp.body.key = crypto.generate_session_key() |
|||
resp.body.crc = binascii.crc32(resp.body.key) & 0xffffffff |
|||
|
|||
self.send_message(resp) |
|||
|
|||
msg = self.wait_for_message(EMsg.ChannelEncryptResult) |
|||
|
|||
if msg.body.result != EResult.OK: |
|||
logger.debug("Failed to secure channel: %s" % msg.body.result) |
|||
self.disconnect() |
|||
return |
|||
|
|||
logger.debug("Channel secured") |
|||
|
|||
self.key = key |
|||
self.event_ready.set() |
|||
|
|||
logger.debug("Event: Ready") |
|||
|
|||
def _handle_multi(self, emsg, msg): |
|||
logger.debug("Unzipping CMsgMulti") |
|||
|
|||
data = zipfile.ZipFile(StringIO(msg.body.message_body)).read('z') |
|||
|
|||
if len(data) != msg.body.size_unzipped: |
|||
logger.fatal("Unzipped size mismatch") |
|||
self.disconnect() |
|||
return |
|||
|
|||
while len(data) > 0: |
|||
size, = struct.unpack_from("<I", data) |
|||
self._parse_message(data[4:4+size]) |
|||
data = data[4+size:] |
|||
|
|||
def _heartbeat(self, interval): |
|||
message = MsgProto(EMsg.ClientHeartBeat) |
|||
|
|||
while True: |
|||
gevent.sleep(interval) |
|||
self.send_message(message) |
|||
|
|||
def _handle_logon(self, emsg, msg): |
|||
if msg.body.eresult == EResult.OK: |
|||
logger.debug("Logon completed") |
|||
|
|||
self.steam_id = SteamID(msg.header.steamid) |
|||
self.session_id = msg.header.client_sessionid |
|||
|
|||
self.cell_id = msg.body.cell_id |
|||
self.webapi_nonce = msg.body.webapi_authenticate_user_nonce |
|||
|
|||
if self._heartbeat_loop: |
|||
self._heartbeat_loop.kill() |
|||
|
|||
logger.debug("Heartbeat started.") |
|||
|
|||
interval = msg.body.out_of_game_heartbeat_seconds |
|||
self._heartbeat_loop = gevent.spawn(self._heartbeat, interval) |
@ -0,0 +1,147 @@ |
|||
import struct |
|||
import logging |
|||
|
|||
import gevent |
|||
from gevent import socket |
|||
from gevent import queue |
|||
from gevent import event |
|||
from gevent.select import select as gselect |
|||
|
|||
logger = logging.getLogger("Connection") |
|||
|
|||
|
|||
class Connection: |
|||
MAGIC = 'VT01' |
|||
FMT = '<I4s' |
|||
FMT_SIZE = struct.calcsize(FMT) |
|||
|
|||
def __init__(self): |
|||
self.socket = None |
|||
self.connected = False |
|||
self.server_addr = None |
|||
|
|||
self._reader = None |
|||
self._writer = None |
|||
self._readbuf = '' |
|||
self.send_queue = queue.Queue() |
|||
self.recv_queue = queue.Queue() |
|||
|
|||
self.event_connected = event.Event() |
|||
|
|||
def connect(self, server_addr): |
|||
self._new_socket() |
|||
|
|||
logger.debug("Attempting connection to %s", str(server_addr)) |
|||
self._connect(server_addr) |
|||
|
|||
self.server_addr = server_addr |
|||
|
|||
self._reader = gevent.spawn(self._reader_loop) |
|||
self._writer = gevent.spawn(self._writer_loop) |
|||
|
|||
logger.debug("Connected.") |
|||
self.event_connected.set() |
|||
|
|||
def disconnect(self): |
|||
if not self.event_connected.is_set(): |
|||
return |
|||
|
|||
self.server_addr = None |
|||
|
|||
if self._reader: |
|||
self._reader.kill(block=False) |
|||
self._reader = None |
|||
if self._writer: |
|||
self._writer.kill(block=False) |
|||
self._writer = None |
|||
|
|||
self._readbuf = '' |
|||
self.send_queue.queue.clear() |
|||
self.recv_queue.queue.clear() |
|||
|
|||
self.socket.close() |
|||
|
|||
logger.debug("Disconnected.") |
|||
self.event_connected.clear() |
|||
|
|||
def get_message(self, block=True, timeout=None): |
|||
return self.recv_queue.get(block, timeout) |
|||
|
|||
def put_message(self, message): |
|||
self.send_queue.put(message) |
|||
|
|||
def _writer_loop(self): |
|||
while True: |
|||
message = self.send_queue.get() |
|||
packet = struct.pack(Connection.FMT, len(message), Connection.MAGIC) + message |
|||
try: |
|||
self._write_data(packet) |
|||
except: |
|||
logger.debug("Connection error (writer).") |
|||
self.disconnect() |
|||
return |
|||
|
|||
def _reader_loop(self): |
|||
while True: |
|||
rlist, _, _ = gselect([self.socket], [], []) |
|||
|
|||
if self.socket in rlist: |
|||
data = self._read_data() |
|||
|
|||
if not data: |
|||
logger.debug("Connection error (reader).") |
|||
self.disconnect() |
|||
return |
|||
|
|||
self._readbuf += data |
|||
self._read_packets() |
|||
|
|||
def _read_packets(self): |
|||
header_size = Connection.FMT_SIZE |
|||
buf = self._readbuf |
|||
|
|||
while len(buf) > header_size: |
|||
message_length, magic = struct.unpack_from(Connection.FMT, buf) |
|||
|
|||
if magic != Connection.MAGIC: |
|||
raise RuntimeError("invalid magic, got %s" % repr(magic)) |
|||
|
|||
packet_length = header_size + message_length |
|||
|
|||
if len(buf) < packet_length: |
|||
return |
|||
|
|||
message = buf[header_size:packet_length] |
|||
buf = buf[packet_length:] |
|||
|
|||
self.recv_queue.put(message) |
|||
|
|||
self._readbuf = buf |
|||
|
|||
|
|||
class TCPConnection(Connection): |
|||
def _new_socket(self): |
|||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|||
|
|||
def _connect(self, server_addr): |
|||
self.socket.connect(server_addr) |
|||
|
|||
def _read_data(self): |
|||
return self.socket.recv(2048) |
|||
|
|||
def _write_data(self, data): |
|||
self.socket.sendall(data) |
|||
|
|||
|
|||
class UDPConnection(Connection): |
|||
def _new_socket(self): |
|||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
|||
|
|||
def _connect(self, server_addr): |
|||
pass |
|||
|
|||
def _read_data(self): |
|||
pass |
|||
|
|||
def _write_data(self, data): |
|||
pass |
@ -0,0 +1,265 @@ |
|||
import struct |
|||
import fnmatch |
|||
from steam.enums import EMsg, EUniverse, EResult |
|||
from steam.protobufs import steammessages_base_pb2 |
|||
from steam.protobufs import steammessages_clientserver_pb2 |
|||
from steam.protobufs import steammessages_clientserver_2_pb2 |
|||
|
|||
|
|||
class MsgHdr: |
|||
_size = struct.calcsize("<Iqq") |
|||
|
|||
def __init__(self, data=None): |
|||
if data: |
|||
self.load(data) |
|||
else: |
|||
self.msg = EMsg.Invalid |
|||
self.targetJobID = -1 |
|||
self.sourceJobID = -1 |
|||
|
|||
def serialize(self): |
|||
return struct.pack("<Iqq", |
|||
self.msg, |
|||
self.targetJobID, |
|||
self.sourceJobID, |
|||
) |
|||
|
|||
def load(self, data): |
|||
(msg, |
|||
self.targetJobID, |
|||
self.sourceJobID, |
|||
) = struct.unpack_from("<Iqq", data) |
|||
|
|||
self.msg = EMsg(msg) |
|||
|
|||
|
|||
class ExtendedMsgHdr: |
|||
_size = struct.calcsize("<IBHqqBqi") |
|||
|
|||
def __init__(self, data=None): |
|||
if data: |
|||
self.load(data) |
|||
else: |
|||
self.msg = EMsg.Invalid |
|||
self.headerSize = 36 |
|||
self.headerVersion = 2 |
|||
self.targetJobID = -1 |
|||
self.sourceJobID = -1 |
|||
self.headerCanary = 239 |
|||
self.steamID = -1 |
|||
self.sessionID = -1 |
|||
|
|||
def serialize(self): |
|||
return struct.pack("<IBHqqBqi", |
|||
self.msg, |
|||
self.headerSize, |
|||
self.headerVersion, |
|||
self.targetJobID, |
|||
self.sourceJobID, |
|||
self.headerCanary, |
|||
self.steamID, |
|||
self.sessionID, |
|||
) |
|||
|
|||
def load(self, data): |
|||
(msg, |
|||
self.headerSize, |
|||
self.headerVersion, |
|||
self.targetJobID, |
|||
self.sourceJobID, |
|||
self.headerCanary, |
|||
self.steamID, |
|||
self.sessionID, |
|||
) = struct.unpack_from("<IBHqqBqi", data) |
|||
|
|||
self.msg = EMsg(msg) |
|||
|
|||
if self.headerSize != 36 or self.headerVersion != 2: |
|||
raise RuntimeError("Failed to parse header") |
|||
|
|||
|
|||
protobuf_mask = 0x80000000 |
|||
|
|||
|
|||
def is_proto(emsg): |
|||
return (int(emsg) & protobuf_mask) > 0 |
|||
|
|||
|
|||
def set_proto_bit(emsg): |
|||
return int(emsg) | protobuf_mask |
|||
|
|||
|
|||
def clear_proto_bit(emsg): |
|||
return int(emsg) & ~protobuf_mask |
|||
|
|||
|
|||
class MsgHdrProtoBuf: |
|||
_size = struct.calcsize("<II") |
|||
|
|||
def __init__(self, data=None): |
|||
self.proto = steammessages_base_pb2.CMsgProtoBufHeader() |
|||
|
|||
if data: |
|||
self.load(data) |
|||
else: |
|||
self.msg = EMsg.Invalid |
|||
|
|||
def serialize(self): |
|||
proto_data = self.proto.SerializeToString() |
|||
return struct.pack("<II", set_proto_bit(self.msg), len(proto_data)) + proto_data |
|||
|
|||
def load(self, data): |
|||
msg, proto_length = struct.unpack_from("<II", data) |
|||
|
|||
self.msg = EMsg(clear_proto_bit(msg)) |
|||
size = MsgHdrProtoBuf._size |
|||
self._fullsize = size + proto_length |
|||
self.proto.ParseFromString(data[size:self._fullsize]) |
|||
|
|||
|
|||
class Msg: |
|||
def __init__(self, msg, data=None, extended=False): |
|||
self.extended = extended |
|||
self.header = ExtendedMsgHdr(data) if extended else MsgHdr(data) |
|||
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) |
|||
else: |
|||
self.body = None |
|||
|
|||
def serialize(self): |
|||
return self.header.serialize() + self.body.serialize() |
|||
|
|||
def __str__(self): |
|||
return '' |
|||
|
|||
|
|||
cmsg_lookup = None |
|||
cmsg_lookup2 = None |
|||
|
|||
|
|||
def get_cmsg(emsg): |
|||
global cmsg_lookup, cmsg_lookup2 |
|||
|
|||
if emsg == EMsg.Multi: |
|||
return steammessages_base_pb2.CMsgMulti |
|||
|
|||
emsg = "cmsg" + str(emsg).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) |
|||
if name: |
|||
return getattr(steammessages_clientserver_pb2, name) |
|||
|
|||
if not cmsg_lookup2: |
|||
cmsg_list = steammessages_clientserver_2_pb2.__dict__ |
|||
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) |
|||
if name: |
|||
return getattr(steammessages_clientserver_2_pb2, name) |
|||
|
|||
return None |
|||
|
|||
|
|||
class MsgProto: |
|||
def __init__(self, msg, data=None): |
|||
self._header = MsgHdrProtoBuf(data) |
|||
self._header.msg = msg |
|||
|
|||
self.msg = msg |
|||
self.header = self._header.proto |
|||
self.body = get_cmsg(msg)() |
|||
|
|||
if data: |
|||
data = data[self._header._fullsize:] |
|||
self.body.ParseFromString(data) |
|||
|
|||
def serialize(self): |
|||
return self._header.serialize() + self.body.SerializeToString() |
|||
|
|||
def __str__(self): |
|||
return '\n'.join(['MsgProto', |
|||
'-' * 20, |
|||
str(self.header), |
|||
'-' * 20, |
|||
str(self.body), |
|||
]) |
|||
|
|||
|
|||
class ChannelEncryptRequest: |
|||
def __init__(self, data=None): |
|||
if data: |
|||
self.load(data) |
|||
else: |
|||
self.protocolVersion = 1 |
|||
self.universe = EUniverse.Invalid |
|||
|
|||
def serialize(self): |
|||
return struct.pack("<II", self.protocolVersion, self.universe) |
|||
|
|||
def load(self, data): |
|||
(self.protocolVersion, |
|||
universe, |
|||
) = struct.unpack_from("<II", data) |
|||
|
|||
self.universe = EUniverse(universe) |
|||
|
|||
|
|||
class ChannelEncryptResponse: |
|||
def __init__(self, data=None): |
|||
if data: |
|||
self.load(data) |
|||
else: |
|||
self.protocolVersion = 1 |
|||
self.keySize = 128 |
|||
self.key = '' |
|||
self.crc = 0 |
|||
|
|||
def serialize(self): |
|||
return struct.pack("<II128sII", |
|||
self.protocolVersion, |
|||
self.keySize, |
|||
self.key, |
|||
self.crc, |
|||
0 |
|||
) |
|||
|
|||
def load(self, data): |
|||
(self.protocolVersion, |
|||
self.keySize, |
|||
self.key, |
|||
self.crc, |
|||
_, |
|||
) = struct.unpack_from("<II128sII", data) |
|||
|
|||
|
|||
class ChannelEncryptResult: |
|||
def __init__(self, data=None): |
|||
if data: |
|||
self.load(data) |
|||
else: |
|||
self.result |
|||
|
|||
def serialize(self): |
|||
return struct.pack("<I", self.result) |
|||
|
|||
def load(self, data): |
|||
(result,) = struct.unpack_from("<I", data) |
|||
self.result = EResult(result) |
Loading…
Reference in new issue