import socket import threading import struct import urllib2 import google.protobuf import playerio_pb2 from StringIO import StringIO def hexdump(bytes): bytes = bytes[0:100] print "plain: " + repr(bytes) print "hex:", for b in bytes: print "%x" % ord(b), class PlayerIOError(Exception): def __init__(self, message, code=None): self.message = message self.code = code def __str__(self): return self.message + " (" + str(self.code) + ")" class HTTPChannel(object): def __init__(self): self.endpoint = "http://api.playerio.com/api" self.token = "" def request(self, method, input_msg, output_msg, error_msg, on_success, on_error): data = input_msg.SerializeToString() headers = {} if self.token != "": headers["playertoken"] = self.token req = urllib2.Request(self.endpoint + "/" + str(method), data, headers) res = urllib2.build_opener().open(req) #req = urllib2.urlopen(self.endpoint + "/" + method, data) has_token = ord(res.read(1)) if has_token != 0: data = res.read(2) length = struct.unpack(">H", data)[0] data = res.read(length) if "Unexpected error occured - Player.IO" in data: raise PlayerIOError("HTTP interface returned error") self.token = data.decode("utf8") status = ord(res.read(1)) if status != 0: if status == 1: output_msg.ParseFromString(res.read()) on_success(output_msg) else: error_msg.ParseFromString(res.read()) on_error(error_msg) class Message(object): def __init__(self, type, *values): self.type = type self.content = [] if len(values): self.add(*values) def add(self, *values): for value in values: self._add_single(value) def _add_single(self, value): # TODO: type checks self.content.append(value) def __len__(self): return len(self.content) def __getitem__(self, index): return self.content[index] def __str__(self): r = "message " + self.type + "{" for val in self.content: r += "\n " if type(val) == str or type(val) == unicode: r += "string \"%s\"" % val[0:100] elif type(val) == int: r += "int %s" % val elif type(val) == bool: r += "bool %s" % val elif type(val) == float: r += "double %s" % val else: r += "unknown \"%s\"" % val r += "\n}" return r class BinarySerializer(object): ShortUnsignedIntPattern = 0x80 IntPattern = 0x04 ShortStringPattern = 0xC0 StringPattern = 0x0C BooleanTruePattern = 0x01 BooleanFalsePattern = 0x00 ByteArrayPattern = 0x10 DoublePattern = 0x03 # expecting: StateInit = 0 # type StateHeader = 1 # header content StateData = 2 # data def __init__(self): self.state = self.StateInit self.valtype = None self.partlen = None self.valleft = None self.msg = None self.buf = None self._clear_buf() self.on_message = None def _clear_buf(self): if self.buf is None or self.buf.tell() > 0: self.buf = StringIO() def add_bytes(self, bytes): for c in bytes: self.add_byte(ord(c)) def add_byte(self, byte): # byte = int if self.state == self.StateInit: if byte & self.ShortStringPattern == self.ShortStringPattern: self.valtype = self.ShortStringPattern self.partlen = byte & ~self.ShortStringPattern if self.partlen > 0: self.state = self.StateData else: self._got_value("") elif byte & self.ShortUnsignedIntPattern == self.ShortUnsignedIntPattern: self._got_value(byte & ~self.ShortUnsignedIntPattern) elif byte & self.StringPattern == self.StringPattern: self.valtype = self.StringPattern self.partlen = (byte & ~self.StringPattern) + 1 self.state = self.StateHeader #print "string header len: " + str(self.partlen) elif byte & self.IntPattern == self.IntPattern: self.valtype = self.IntPattern self.partlen = (byte & ~self.IntPattern) + 1 self.state = self.StateHeader elif byte == self.BooleanTruePattern: self._got_value(True) elif byte == self.BooleanFalsePattern: self._got_value(False) elif byte == self.DoublePattern: self.valtype = self.DoublePattern self.partlen = 8 self.state = self.StateData else: raise PlayerIOError("Unknown valtype: %x" % byte) elif self.state == self.StateHeader: self.buf.write(chr(byte)) if self.buf.tell() == self.partlen: if self.valtype == self.StringPattern: self.state = self.StateData self.partlen = self._decode_varint(self.buf.getvalue()) self._clear_buf() elif self.valtype == self.IntPattern: self._got_value(self._decode_varint(self.buf.getvalue())) #print "string len: " + str(self.partlen) elif self.state == self.StateData: self.buf.write(chr(byte)) if self.buf.tell() == self.partlen: if self.valtype == self.ShortStringPattern or self.valtype == self.StringPattern: #print "string: " + self.buf.getvalue() self._got_value(self.buf.getvalue().decode("utf8")) elif self.valtype == self.DoublePattern: self._got_value(struct.unpack(">d", self.buf.getvalue())[0]) def _got_value(self, value): #print "value: " + str(value) self.state = self.StateInit self._clear_buf() msgdone = False if self.valleft is None: self.valleft = value elif self.msg is None: self.msg = Message(value) if self.valleft == 0: msgdone = True else: self.valleft -= 1 if self.valleft == 0: msgdone = True self.msg._add_single(value) if msgdone: if self.on_message is not None: self.on_message(self.msg) self.valleft = None self.msg = None def _encode_varint(self, val): r = "" val = int(val) while True: r += chr(val & 0xFF) val >>= 8 if val == 0: break return r def _decode_varint(self, val): print "decode varint" hexdump(val) r = 0 for c in val: r <<= 8 r |= ord(c) & 0xFF print "result", r return r def _get_header(self, type, val): val = self._encode_varint(val) return chr(type | (len(val) - 1)) + val def serialize(self, value): # TODO: type checks return self._serialize_value(value) def _serialize_value(self, value): r = "" if type(value) == str or type(value) == unicode: bytes = value.encode("utf8") if len(bytes) < 64: r = chr(self.ShortStringPattern | len(bytes)) else: r = self._get_header(self.StringPattern, len(bytes)) r += bytes elif type(value) == bool: if value: r = chr(self.BooleanTruePattern) else: r = chr(self.BooleanFalsePattern) elif type(value) == int: if value >= 0 and value < 64: r = chr(self.ShortUnsignedIntPattern | value) else: r = self._get_header(self.IntPattern, value) else: raise Exception("Value type error") return r def serialize_message(self, msg): buf = StringIO() buf.write(self._serialize_value(len(msg))) buf.write(self._serialize_value(msg.type)) for value in msg: buf.write(self.serialize(value)) return buf.getvalue() def _to_key_value_array(dict): for key in dict: yield playerio_pb2.KeyValuePair(key, dict[key]) class Connection(object): def __init__(self, client, roomid, joinkey, endpoints, joindata, on_success, on_error, devserver): self.client = client self.roomid = roomid self.joinkey = joinkey self.devserver = devserver self.on_error = on_error self.connected = False self.message_handlers = {} self.disconnect_handler = None endpoint = endpoints[0] self.serializer = BinarySerializer() self.serializer.on_message = self.handle_message if devserver is None: print endpoint self.sock = socket.create_connection((endpoint.address, endpoint.port)) else: info = devserver.split(":") self.sock = socket.create_connection((info[0], int(info[1]))) self.sock.settimeout(5) self._send("\0") msg = Message("join", self.joinkey) for key in joindata: msg.add(str(key), str(joindata[key])) self.send_message(msg) self.connected = True self.thread = threading.Thread(target=self.read_thread, name="read_thread") self.thread.start() on_success(self) def add_message_handler(self, type, handler): self.message_handlers[type] = handler def set_disconnect_handler(self, handler): self.disconnect_handler = handler def disconnect(self): self.connected = False self.sock.close() self.thread.join() self.thread = None def _send(self, data): #print ">>>" #hexdump(data) self.sock.send(data) def _recv(self, num): data = self.sock.recv(num) #print "<<<" #hexdump(data) return data def read_thread(self): try: while self.connected: try: data = self._recv(4096) self.serializer.add_bytes(data) except socket.timeout: pass print "read_thread exiting cleanly" except Exception, e: raise self.connected = False def handle_disconnect(self): if self.client.trace: print "Connection: DISCONNECTED" if self.disconnect_handler: self.disconnect_handler() def handle_message(self, msg): if self.client.trace: print "Connection: RECV: " + str(msg) if msg.type in self.message_handlers: self.message_handlers[msg.type](msg) def send(self, type, *values): self.send_message(Message(type, *values)) def send_message(self, msg): if self.client.trace: print "Connection: SEND: " + str(msg) self._send(self.serializer.serialize_message(msg)) class RoomInfo(object): def __init__(self, msg): self.id = msg.id self.server_type = msg.serverType self.online_users = msg.onlineUsers self.room_data = None def __str__(self): r = "room {" r += " id: \"" + self.id + "\"" r += " server_type: \"" + self.server_type + "\"" r += " online_users: " + str(self.online_users) r += " }" return r class Multiplayer(object): def __init__(self, channel, client): self.channel = channel self.client = client def handle_error(self, msg, on_error): err = PlayerIOError(msg.message, msg.errorCode) if on_error is None: raise err else: on_error(err) def create_join_room(self, roomid, servertype, visible, roomdata, joindata, on_success, on_error=None): input = playerio_pb2.CreateJoinRoomArgs() input.roomId = roomid input.serverType = servertype input.visible = visible for kv in _to_key_value_array(roomdata): input.roomData.append(kv) for kv in _to_key_value_array(joindata): input.joinData.append(kv) input.isDevRoom = False output = playerio_pb2.CreateJoinRoomOutput() error = playerio_pb2.CreateJoinRoomError() self.channel.request(27, input, output, error, lambda msg: self.create_join_room_success(msg, joindata, on_success, on_error), lambda msg: self.handle_error(msg, on_error)) def create_join_room_success(self, msg, joindata, on_success, on_error): Connection(self.client, msg.roomId, msg.joinKey, msg.endpoints, joindata, on_success, on_error, None) def list_rooms(self, servertype, searchcriteria, resultlimit, resultoffset, on_success, on_error=None): input = playerio_pb2.ListRoomsArgs() input.serverType = servertype for kv in _to_key_value_array(searchcriteria): input.searchCriteria.append(kv) input.resultLimit = resultlimit input.resultOffset = resultoffset input.onlyDevRooms = False output = playerio_pb2.ListRoomsOutput() error = playerio_pb2.ListRoomsError() self.channel.request(30, input, output, error, lambda msg: self.list_rooms_success(msg, on_success), lambda msg: self.handle_error(msg, on_error)) def list_rooms_success(self, msg, on_success): rooms = [] for room in msg.rooms: rooms.append(RoomInfo(room)) on_success(rooms) class Client(object): def __init__(self, channel): self.trace = False self.multiplayer = Multiplayer(channel, self) class _Connector(object): def connect(self, gameid, connectionid, userid, auth, on_success, on_error=None): self.on_success = on_success self.on_error = on_error self.channel = HTTPChannel() input = playerio_pb2.ConnectArgs() input.gameId = gameid input.connectionId = connectionid input.userId = userid input.auth = auth output = playerio_pb2.ConnectOutput() error = playerio_pb2.ConnectError() self.channel.request(10, input, output, error, self.handle_success, self.handle_error) def handle_success(self, msg): self.channel.token = msg.token self.on_success(Client(self.channel)) def handle_error(self, msg): err = PlayerIOError(msg.message, msg.errorCode) if self.on_error is None: raise err else: self.on_error(err) def connect(gameid, connectionid, userid, auth, on_success, on_error=None): return _Connector().connect(gameid, connectionid, userid, auth, on_success, on_error)