def pollserver(self, host="localhost", port=None): if port is None: port = self.srv_data.server_port server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # server_sock = socket.socket() server_sock.settimeout(5) server_sock.connect((host, port)) packet = Packet(server_sock, self) packet.send(0x00, "varint|string|ushort|varint", (5, host, port, 1)) packet.send(0x00, "", ()) packet.flush() self.srv_data.protocolVersion = -1 while True: pkid, original = packet.grabpacket() if pkid == 0x00: data = json.loads(packet.read("string:response")["response"]) self.srv_data.protocolVersion = data["version"]["protocol"] self.srv_data.version = data["version"]["name"] if "modinfo" in data and data["modinfo"]["type"] == "FML": self.forge = True self.mod_info["modinfo"] = data["modinfo"] break server_sock.close()
def pollserver(self, host="localhost", port=None): if port is None: port = self.srv_data.server_port server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # server_sock = socket.socket() server_sock.settimeout(5) server_sock.connect((host, port)) packet = Packet(server_sock, self) packet.send(0x00, "varint|string|ushort|varint", (5, host, port, 1)) packet.send(0x00, "", ()) packet.flush() self.srv_data.protocolVersion = -1 while True: pkid, original = packet.grabpacket() if pkid == 0x00: data = json.loads(packet.read("string:response")["response"]) self.srv_data.protocolVersion = data["version"][ "protocol"] self.srv_data.version = data["version"]["name"] if "modinfo" in data and data["modinfo"]["type"] == "FML": self.forge = True self.mod_info["modinfo"] = data["modinfo"] break server_sock.close()
def pollserver(self, host="localhost", port=None): """ Pings server for server json response information. :param host: ip of server :param port: server port. :returns: a list - [protocol, string version name, Forge?(Bool), modinfo (if Forge) ] """ if port is None: port = self.srv_data.server_port server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # server_sock = socket.socket() server_sock.connect((host, port)) packet = Packet(server_sock, self) packet.sendpkt( # 340 is protocol and 1 means "Next State = status" 0x00, [VARINT, STRING, USHORT, VARINT], (340, host, port, STATUS)) # Disconnect packet.sendpkt(0x00, [ NULL, ], [ "", ]) packet.flush() self.srv_data.protocolVersion = -1 container = [] while True: pkid, packet_tuple = packet.grabpacket() if pkid == 0x00: data = json.loads(packet.readpkt([ STRING, ])[0]) container.append(data["version"]["protocol"]) container.append(data["version"]["name"]) if "modinfo" in data and data["modinfo"]["type"] == "FML": container.append(True) container.append(data["modinfo"]) else: container.append(False) break server_sock.close() return container
def pollserver(self, host="localhost", port=None): if port is None: port = self.srv_data.server_port server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # server_sock = socket.socket() server_sock.connect((host, port)) packet = Packet(server_sock, self) packet.sendpkt( # 340 is protocol and 1 means "Next State = status" 0x00, [VARINT, STRING, USHORT, VARINT], (340, host, port, STATUS)) # Disconnect packet.sendpkt(0x00, [ NULL, ], [ "", ]) packet.flush() self.srv_data.protocolVersion = -1 while True: pkid, packet_tuple = packet.grabpacket() if pkid == 0x00: data = json.loads(packet.readpkt([ STRING, ])[0]) self.srv_data.protocolVersion = data["version"]["protocol"] self.srv_data.version = data["version"]["name"] if "modinfo" in data and data["modinfo"]["type"] == "FML": self.forge = True self.mod_info["modinfo"] = data["modinfo"] break server_sock.close()
def pollserver(self, host="localhost", port=None): """ Pings server for server json response information. :param host: ip of server :param port: server port. :returns: a list - [protocol, string version name, Forge?(Bool), modinfo (if Forge) ] """ if port is None: port = self.javaserver.server_port server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # server_sock = socket.socket() server_sock.connect((host, port)) packet = Packet(server_sock, self) packet.sendpkt( # 340 is protocol and 1 means "Next State = status" 0x00, [VARINT, STRING, USHORT, VARINT], (340, host, port, STATUS)) # Disconnect packet.sendpkt(0x00, [NULL, ], ["", ]) packet.flush() self.javaserver.protocolVersion = -1 container = [] while True: pkid, packet_tuple = packet.grabpacket() if pkid == 0x00: data = json.loads(packet.readpkt([STRING, ])[0]) container.append(data["version"]["protocol"]) container.append(data["version"]["name"]) if "modinfo" in data and data["modinfo"]["type"] == "FML": container.append(True) container.append(data["modinfo"]) else: container.append(False) break server_sock.close() return container
def connect(self): """ This simply establishes the tcp socket connection and starts the flush loop, NOTHING MORE. """ self.state = LOGIN # Connect to a local server address if self.ip is None: self.server_socket.connect( ("localhost", self.proxy.srv_data.server_port)) # Connect to some specific server address else: self.server_socket.connect((self.ip, self.port)) # start packet handler self.packet = Packet(self.server_socket, self) self.packet.version = self.client.clientversion # define parsers self.parse_cb = ParseCB(self, self.packet) self._define_parsers() t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start()
def connect(self): """ This simply establishes the tcp socket connection and starts the flush loop, NOTHING MORE. """ self.state = LOGIN # Connect to a local server address if self.ip is None: self.server_socket.connect(( "localhost", self.proxy.srv_data.server_port)) # Connect to some specific server address else: self.server_socket.connect((self.ip, self.port)) # start packet handler self.packet = Packet(self.server_socket, self) self.packet.version = self.client.clientversion # define parsers self.parse_cb = ParseCB(self, self.packet) self._define_parsers() t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start()
def __init__(self, proxy, clientsock, client_addr, banned=False): """ Handle the client connection. This class Client is a "fake" server, accepting connections from clients. It receives "SERVER BOUND" packets from client, parses them, and forards them on to the server. It "sends" to the client (self.send() or self.sendpkt()) Client receives the parent proxy as it's argument. No longer receives the proxy's wrapper instance! All data is passed via servervitals from proxy's srv_data. """ # basic __init__ items from passed arguments self.client_socket = clientsock self.client_address = client_addr self.proxy = proxy self.publicKey = self.proxy.publicKey self.privateKey = self.proxy.privateKey self.servervitals = self.proxy.srv_data self.log = self.proxy.log self.ipbanned = banned # constants from config: self.spigot_mode = self.proxy.config["spigot-mode"] self.hidden_ops = self.proxy.config["hidden-ops"] self.silent_bans = self.proxy.config["silent-ipban"] # client setup and operating paramenters self.username = "******" self.packet = Packet(self.client_socket, self) self.verifyToken = encryption.generate_challenge_token() self.serverID = encryption.generate_server_id().encode('utf-8') self.MOTD = {} # client will reset this later, if need be.. self.clientversion = self.servervitals.protocolVersion # default server port (to this wrapper's server) self.serverport = self.servervitals.server_port self.onlinemode = self.proxy.config["online-mode"] # packet stuff self.pktSB = mcpackets_sb.Packets(self.clientversion) self.pktCB = mcpackets_cb.Packets(self.clientversion) self.parse_sb = ParseSB(self, self.packet) # dictionary of parser packet constants and associated parsing methods self.parsers = {} self._getclientpacketset() self.buildmode = False # keep alive data self.time_server_pinged = 0 self.time_client_responded = 0 self.keepalive_val = 0 # client and server status self.abort = False # Proxy ServerConnection() (not the javaserver) self.server_connection = None self.state = HANDSHAKE # UUIDs - all should use MCUUID unless otherwise specified # -------------------------------------------------------- # Server UUID - which is the local offline UUID. self.serveruuid = None # -------------------------------------------------------- # The client UUID authenticated by connection to session server. self.uuid = None # -------------------------------------------------------- # the formal, unique, mojang UUID as looked up on mojang servers. # This ID will be the same no matter what mode wrapper is in # or whether it is a lobby, etc. This will be the formal uuid # to use for all wrapper internal functions for referencing a # unique player. # TODO - Unused except by plugin channel. # not to be confused with the fact that API player has a property # with the same name. self.mojanguuid = None # information gathered during login or socket connection processes # TODO in the future, we could use plugin channels to # communicate these to subworld wrappers From socket data self.address = None # this will store the client IP for use by player.py self.ip = self.client_address[0] # From client handshake. For vanilla clients, it is what # the user entered to connect to your wrapper. self.serveraddressplayerused = None self.serverportplayerused = None # player api Items # EID collected by serverconnection (changes on each server) self.server_eid = 0 self.gamemode = 0 self.dimension = 0 self.position = (0, 0, 0) # X, Y, Z self.head = (0, 0) # Yaw, Pitch self.inventory = {} self.slot = 0 self.riding = None # last placement (for use in cases of bucket use) self.lastplacecoords = (0, 0, 0) self.properties = {} self.clientSettings = False self.clientSettingsSent = False self.skinBlob = {} self.windowCounter = 2 self.currentwindowid = -1 self.noninventoryslotcount = 0 self.lastitem = None # wrapper's own channel on each player client self.shared = { "username": "", "uuid": "", "ip": "", "received": False, "sent": False }
class Client(object): def __init__(self, proxy, clientsock, client_addr, banned=False): """ Handle the client connection. This class Client is a "fake" server, accepting connections from clients. It receives "SERVER BOUND" packets from client, parses them, and forards them on to the server. It "sends" to the client (self.send() or self.sendpkt()) Client receives the parent proxy as it's argument. No longer receives the proxy's wrapper instance! All data is passed via servervitals from proxy's srv_data. """ # basic __init__ items from passed arguments self.client_socket = clientsock self.client_address = client_addr self.proxy = proxy self.publicKey = self.proxy.publicKey self.privateKey = self.proxy.privateKey self.servervitals = self.proxy.srv_data self.log = self.proxy.log self.ipbanned = banned # constants from config: self.spigot_mode = self.proxy.config["spigot-mode"] self.hidden_ops = self.proxy.config["hidden-ops"] self.silent_bans = self.proxy.config["silent-ipban"] # client setup and operating paramenters self.username = "******" self.packet = Packet(self.client_socket, self) self.verifyToken = encryption.generate_challenge_token() self.serverID = encryption.generate_server_id().encode('utf-8') self.MOTD = {} # client will reset this later, if need be.. self.clientversion = self.servervitals.protocolVersion # default server port (to this wrapper's server) self.serverport = self.servervitals.server_port self.onlinemode = self.proxy.config["online-mode"] # packet stuff self.pktSB = mcpackets_sb.Packets(self.clientversion) self.pktCB = mcpackets_cb.Packets(self.clientversion) self.parse_sb = ParseSB(self, self.packet) # dictionary of parser packet constants and associated parsing methods self.parsers = {} self._getclientpacketset() self.buildmode = False # keep alive data self.time_server_pinged = 0 self.time_client_responded = 0 self.keepalive_val = 0 # client and server status self.abort = False # Proxy ServerConnection() (not the javaserver) self.server_connection = None self.state = HANDSHAKE # UUIDs - all should use MCUUID unless otherwise specified # -------------------------------------------------------- # Server UUID - which is the local offline UUID. self.serveruuid = None # -------------------------------------------------------- # The client UUID authenticated by connection to session server. self.uuid = None # -------------------------------------------------------- # the formal, unique, mojang UUID as looked up on mojang servers. # This ID will be the same no matter what mode wrapper is in # or whether it is a lobby, etc. This will be the formal uuid # to use for all wrapper internal functions for referencing a # unique player. # TODO - Unused except by plugin channel. # not to be confused with the fact that API player has a property # with the same name. self.mojanguuid = None # information gathered during login or socket connection processes # TODO in the future, we could use plugin channels to # communicate these to subworld wrappers From socket data self.address = None # this will store the client IP for use by player.py self.ip = self.client_address[0] # From client handshake. For vanilla clients, it is what # the user entered to connect to your wrapper. self.serveraddressplayerused = None self.serverportplayerused = None # player api Items # EID collected by serverconnection (changes on each server) self.server_eid = 0 self.gamemode = 0 self.dimension = 0 self.position = (0, 0, 0) # X, Y, Z self.head = (0, 0) # Yaw, Pitch self.inventory = {} self.slot = 0 self.riding = None # last placement (for use in cases of bucket use) self.lastplacecoords = (0, 0, 0) self.properties = {} self.clientSettings = False self.clientSettingsSent = False self.skinBlob = {} self.windowCounter = 2 self.currentwindowid = -1 self.noninventoryslotcount = 0 self.lastitem = None # wrapper's own channel on each player client self.shared = { "username": "", "uuid": "", "ip": "", "received": False, "sent": False } def handle(self): t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start() while not self.abort: try: pkid, original = self.packet.grabpacket() except EOFError: # This is not really an error.. It means the client # is not sending packet stream anymore if self.username != "PING REQUEST": self.log.debug("%s Client Packet stream ended [EOF]", self.username) self.abort = True break except socket_error: # occurs anytime a socket is closed. if self.username != "PING REQUEST": self.log.debug("%s Client Proxy Failed to grab packet", self.username) self.abort = True break except Exception as e: # anything that gets here is a bona-fide error # we need to become aware of self.log.error("%s Client Exception: Failed to grab packet " "\n%s", self.username, e) self.abort = True break # self.parse(pkid) # send packet if server available and parsing passed. # already tested - Python will not attempt eval of # self.server_connection.state if self.server_connection is False if self.parse(pkid) and self.server_connection and \ self.server_connection.state in (PLAY, LOBBY): # sending to the server only happens in # PLAY/LOBBY (not IDLE, HANDSHAKE, or LOGIN) # wrapper handles LOGIN/HANDSHAKE with servers (via # self.parse(pkid), which DOES happen in all modes) self.server_connection.packet.send_raw(original) # sometimes (like during a ping request), a client may never enter PLAY # mode and will never be assigned a server connection... if self.server_connection: self.close_server_instance("Client Handle Ended") # upon self.abort def flush_loop(self): while not self.abort: try: self.packet.flush() except socket_error: self.log.debug("%s client socket closed (socket_error).", self.username) break time.sleep(0.01) if self.username != "PING REQUEST": self.log.debug("%s clientconnection flush_loop thread ended", self.username) self.proxy.removestaleclients() # from this instance from proxy.srv_data.clients def change_servers(self, ip=None, port=None): # close current connection and start new one was_lobby = False if self.state == LOBBY: was_lobby = True # get out of PLAY "NOW" to prevent second disconnect that kills client self.state = IDLE # keep server from sending disconnects self.server_connection.state = IDLE self.close_server_instance("Lobbification") # lobby_return = True time.sleep(1) # setup for connect self.clientSettingsSent = False # connect to server self.state = PLAY self.connect_to_server(ip, port) # if the client was in LOBBY state (connected to remote server) if was_lobby: self.log.info("%s's client Returned from remote server:" " (UUID: %s | IP: %s | SecureConnection: %s)", self.username, self.uuid.string, self.ip, self.onlinemode) self._add_client() # TODO whatever respawn stuff works # send these right quick to client self._send_client_settings() self.packet.sendpkt( self.pktCB.CHANGE_GAME_STATE, [UBYTE, FLOAT], (1, 0)) self.packet.sendpkt( self.pktCB.RESPAWN, [INT, UBYTE, UBYTE, STRING], [-1, 3, 0, 'default']) if self.version < PROTOCOL_1_8START: self.server_connection.packet.sendpkt( self.pktSB.CLIENT_STATUS, [BYTE, ], (0, )) else: self.packet.sendpkt( 0x2c, [VARINT, INT, STRING, ], (self.server_eid, "DURNIT")) # self.packet.sendpkt(0x3e, # [FLOAT, VARINT, FLOAT], # (-1, 0, 0.0)) self.server_connection.packet.sendpkt( self.pktSB.CLIENT_STATUS, [VARINT, ], (0, )) self.server_connection.packet.sendpkt( self.pktSB.PLAYER, [BOOL, ], (True,)) self.state = LOBBY def logon_client_into_proxy(self): """ When the client first logs in to the wrapper proxy """ # check for uuid ban if self.proxy.isuuidbanned(self.uuid.__str__()): banreason = self.proxy.getuuidbanreason( self.uuid.__str__()) self.log.info("Banned player %s tried to" " connect:\n %s" % (self.username, banreason)) self.state = HANDSHAKE self.disconnect("Banned: %s" % banreason) return # Run the pre-login event if not self.proxy.eventhandler.callevent( "player.preLogin", { "playername": self.username, "player": self.username, # not a real player object! "online_uuid": self.uuid.string, "offline_uuid": self.serveruuid.string, "ip": self.ip, "secure_connection": self.onlinemode }): """ eventdoc <group> Proxy <group> <description> Called before client logs on. <description> <abortable> Yes, return False to disconnect the client. <abortable> <comments> - If aborted, the client is disconnnected with message "Login denied by a Plugin." - Event occurs after proxy ban code runs right after a successful handshake with Proxy. <comments> <payload> "playername": self.username, "player": username (name only - player object does not yet exist) "online_uuid": online UUID, "offline_uuid": UUID on local server (offline), "ip": the user/client IP on the internet. "secure_connection": Proxy's online mode <payload> """ self.state = HANDSHAKE self.disconnect("Login denied by a Plugin.") return self.log.info("%s's Proxy Client LOGON occurred: (UUID: %s" " | IP: %s | SecureConnection: %s)", self.username, self.uuid.string, self.ip, self.onlinemode) self._inittheplayer() # set up inventory and stuff self._add_client() # start keep alives # send login success to client self.packet.sendpkt( self.pktCB.LOGIN_SUCCESS, [STRING, STRING], (self.uuid.string, self.username)) self.time_client_responded = time.time() t_keepalives = threading.Thread( target=self._keep_alive_tracker, args=()) t_keepalives.daemon = True t_keepalives.start() def connect_to_server(self, ip=None, port=None): """ Connects the client to a server. Creates a new server instance and tries to connect to it. Leave ip and port blank to connect to the local wrapped javaserver instance. it is the responsibility of the calling method to shutdown any existing server connection first. It is also the caller's responsibility to track LOBBY modes and handle respawns, rain, etc. """ self.server_connection = ServerConnection(self, ip, port) # connect the socket and start its flush_loop try: self.server_connection.connect() except Exception as e: self.disconnect("Proxy client could not connect to the server" " (%s)" % e) return # start server handle() to read the packets t = threading.Thread(target=self.server_connection.handle, args=()) t.daemon = True t.start() # switch server_connection to LOGIN to log in to (offline) server. self.server_connection.state = LOGIN # now we send it a handshake to request the server go to login mode server_addr = "localhost" if self.spigot_mode: server_addr = "localhost\x00%s\x00%s" % \ (self.client_address[0], self.uuid.hex) if self.proxy.forge: server_addr = "localhost\x00FML\x00" self.server_connection.packet.sendpkt( self.server_connection.pktSB.HANDSHAKE, [VARINT, STRING, USHORT, VARINT], (self.clientversion, server_addr, self.serverport, LOGIN)) # send the login request (server is offline, so it will # accept immediately by sending login_success) self.server_connection.packet.sendpkt( self.server_connection.pktSB.LOGIN_START, [STRING], [self.username]) # LOBBY code and such to go elsewhere def close_server_instance(self, term_message): """ Close the server connection gracefully if possible. """ if self.server_connection: self.server_connection.close_server(term_message) def disconnect(self, message): """ disconnects the client (runs close_server(), which will also shut off the serverconnection.py) Not used to disconnect from a server! This disconnects the client. """ jsonmessage = message # server packets are read as json if type(message) is dict: if "text" in message: jsonmessage = {"text": message} if "color" in message: jsonmessage["color"] = message["color"] if "bold" in message: jsonmessage["bold"] = message["bold"] message = jsonmessage["text"] jsonmessage = json.dumps(jsonmessage) else: jsonmessage = message # server packets are read as json if self.state in (PLAY, LOBBY): self.packet.sendpkt( self.pktCB.DISCONNECT, [JSON], [jsonmessage]) self.log.debug("Sent PLAY state DISCONNECT packet to %s", self.username) else: self.packet.sendpkt( self.pktCB.LOGIN_DISCONNECT, [JSON], [message]) if self.username != "PING REQUEST": self.log.debug( "State was 'other': sent LOGIN_DISCONNECT to %s", self.username) time.sleep(1) self.state = HANDSHAKE self.close_server_instance("run Disconnect() client. Aborting client thread") self.abort = True # internal init and properties # ----------------------------- @property def version(self): return self.clientversion def _inittheplayer(self): # so few items and so infrequently run that fussing with # xrange/range PY2 difference is not needed. # there are 46 items 0-45 in 1.9 (shield) versus # 45 (0-44) in 1.8 and below. for i in self.proxy.inv_slots: self.inventory[i] = None self.time_server_pinged = time.time() self.time_client_responded = time.time() def _getclientpacketset(self): # Determine packet types - in this context, pktSB/pktCB is # what is being received/sent from/to the client. # That is why we refresh to the clientversion. self.pktSB = mcpackets_sb.Packets(self.clientversion) self.pktCB = mcpackets_cb.Packets(self.clientversion) self._define_parsers() # api related # ----------------------------- def getplayerobject(self): if self.username in self.servervitals.players: return self.servervitals.players[self.username] self.log.error("In playerlist:\n%s\nI could not locate player: %s\n" "This resulted in setting the player object to FALSE!", self.servervitals.players, self.username) return False def editsign(self, position, line1, line2, line3, line4, pre18=False): if pre18: x = position[0] y = position[1] z = position[2] self.server_connection.packet.sendpkt( self.pktSB.PLAYER_UPDATE_SIGN, [INT, SHORT, INT, STRING, STRING, STRING, STRING], (x, y, z, line1, line2, line3, line4)) else: self.server_connection.packet.sendpkt( self.pktSB.PLAYER_UPDATE_SIGN, [POSITION, STRING, STRING, STRING, STRING], (position, line1, line2, line3, line4)) def chat_to_server(self, message, position=0): """ used to resend modified chat packets. Also to mimic player in API player for say() and execute() methods """ if self.version < PROTOCOL_1_11: if len(message) > 100: self.log.error( "chat to server exceeded 100 characters " "(%s probably got kicked)", self.username) if len(message) > 256: self.log.error( "chat to server exceeded 256 characters " "(%s probably got kicked)", self.username) self.server_connection.packet.sendpkt( self.pktSB.CHAT_MESSAGE[PKT], self.pktSB.CHAT_MESSAGE[PARSER], (message, position)) def chat_to_client(self, message, position=0): """ used by player API to player.message(). sendpacket for chat knows how to process either a chat dictionary or a string message! don't try sending a json.dumps string... it will simply be sent as a chat string inside a chat.message translate item... """ self.packet.sendpkt(self.pktCB.CHAT_MESSAGE[PKT], self.pktCB.CHAT_MESSAGE[PARSER], (message, position)) # internal client login methods # ----------------------------- def _keep_alive_tracker(self): """ Send keep alives to client and send client settings to server. """ while not self.abort: time.sleep(.1) if self.state in (PLAY, LOBBY): # client expects < 20sec # sending more frequently (5 seconds) seems to help with # some slower connections. if time.time() - self.time_server_pinged > 5: # create the keep alive value # MC 1.12 .2 uses a time() value. # Old way takes almost full second to generate: if self.version < PROTOCOL_1_12_2: self.keepalive_val = random.randrange(0, 99999) else: self.keepalive_val = int((time.time() * 100) % 10000000) # challenge the client with it self.packet.sendpkt( self.pktCB.KEEP_ALIVE[PKT], self.pktCB.KEEP_ALIVE[PARSER], [self.keepalive_val]) self.time_server_pinged = time.time() # check for active client keep alive status: # server can allow up to 30 seconds for response if time.time() - self.time_client_responded > 25: # \ # and not self.abort: self.disconnect("Client closed due to lack of" " keepalive response") self.log.debug("Closed %s's client thread due to " "lack of keepalive response", self.username) return self.log.debug("%s Client keepalive tracker aborted", self.username) def _login_authenticate_client(self, server_id): if self.onlinemode: r = requests.get("https://sessionserver.mojang.com" "/session/minecraft/hasJoined?username=%s" "&serverId=%s" % (self.username, server_id)) if r.status_code == 200: requestdata = r.json() self.uuid = MCUUID(requestdata["id"]) # TODO if requestdata["name"] != self.username: self.disconnect("Client's username did not" " match Mojang's record") self.log.info("Client's username did not" " match Mojang's record %s != %s", requestdata["name"], self.username) return False for prop in requestdata["properties"]: if prop["name"] == "textures": self.skinBlob = prop["value"] self.proxy.skins[ self.uuid.string] = self.skinBlob self.properties = requestdata["properties"] else: self.disconnect("Proxy Client Session Error" " (HTTP Status Code %d)" % r.status_code) return False currentname = self.proxy.uuids.getusernamebyuuid( self.uuid.string) if currentname: if currentname != self.username: self.log.info("%s's client performed LOGON in with" " new name, falling back to %s", self.username, currentname) self.username = currentname self.serveruuid = self.proxy.uuids.getuuidfromname(self.username) # Wrapper offline and not authenticating # maybe it is the destination of a hub? or you use another # way to authenticate (passwords?) else: # I'll take your word for it, bub... You are: self.serveruuid = self.proxy.uuids.getuuidfromname(self.username) self.uuid = self.serveruuid self.log.debug("Client logon with wrapper offline-" " 'self.uuid = OfflinePlayer:<name>'") # no idea what is special about version 26 if self.clientversion > 26: self.packet.setcompression(256) def _add_client(self): # Put XXXplayer_object_andXXX client into server data. (player login # will be called later by mcserver.py) if self not in self.proxy.srv_data.clients: self.proxy.srv_data.clients.append(self) def _send_client_settings(self): if self.clientSettings and not self.clientSettingsSent: self.server_connection.packet.sendpkt( self.pktSB.CLIENT_SETTINGS, [RAW, ], (self.clientSettings,)) self.clientSettingsSent = True def _send_forge_client_handshakereset(self): """ Sends a forge plugin channel packet to causes the client to recomplete its entire handshake from the start. from 'http://wiki.vg/Minecraft_Forge_Handshake': The normal forge server does not ever use this packet, but it is used when connecting through a BungeeCord instance, specifically when transitioning from a vanilla server to a modded one or from a modded server to another modded server. """ channel = "FML|HS" if self.clientversion < PROTOCOL_1_8START: self.packet.sendpkt( self.pktCB.PLUGIN_MESSAGE, [STRING, SHORT, BYTE], [channel, 1, 254]) else: self.packet.sendpkt( self.pktCB.PLUGIN_MESSAGE, [STRING, BYTE], [channel, 254]) def _transmit_downstream(self): """ transmit wrapper channel status info to the server's direction to help sync hub/lobby wrappers """ channel = "WRAPPER.PY|PING" state = self.state if self.server_connection: if self.version < PROTOCOL_1_8START: self.server_connection.packet.sendpkt( self.pktSB.PLUGIN_MESSAGE, [STRING, SHORT, BYTE], [channel, 1, state]) else: self.server_connection.packet.sendpkt( self.pktSB.PLUGIN_MESSAGE, [STRING, BOOL], [channel, state]) def _whitelist_processing(self): pass # This needs re-worked. should likely be in main wrapper of server # instance, not at each client connection # Rename UUIDs accordingly # if self.config["Proxy"]["convert-player-files"]: # if self.config["Proxy"]["online-mode"]: # # Check player files, and rename them accordingly # # to offline-mode UUID # worldname = self.servervitals.worldname # if not os.path.exists("%s/playerdata/%s.dat" % ( # worldname, self.serveruuid.string)): # if os.path.exists("%s/playerdata/%s.dat" % ( # worldname, self.uuid.string)): # self.log.info("Migrating %s's playerdata" # " file to proxy mode", self.username) # shutil.move("%s/playerdata/%s.dat" % # (worldname, self.uuid.string), # "%s/playerdata/%s.dat" % ( # worldname, self.serveruuid.string)) # with open("%s/.wrapper-proxy-playerdata-migrate" % # worldname, "a") as f: # f.write("%s %s\n" % (self.uuid.string, # self.serveruuid.string)) # # Change whitelist entries to offline mode versions # if os.path.exists("whitelist.json"): # with open("whitelist.json", "r") as f: # jsonwhitelistdata = json.loads(f.read()) # if jsonwhitelistdata: # for player in jsonwhitelistdata: # if not player["uuid"] == self.serveruuid.string\ # and player["uuid"] == self.uuid.string: # self.log.info("Migrating %s's whitelist entry" # " to proxy mode", self.username) # jsonwhitelistdata.append( # {"uuid": self.serveruuid.string, # "name": self.username}) # with open("whitelist.json", "w") as f: # f.write(json.dumps(jsonwhitelistdata)) # ##self.XXXservervitalsXXX.console( # "##whitelist reload") # => self.proxy.eventhandler.callevent("proxy.console", {"command": "whitelist reload"}) """ eventdoc <description> internalfunction <description> """ # with open("%s/.wrapper-proxy-whitelist-" # "migrate" % worldname, "a") as f: # f.write("%s %s\n" % ( # self.uuid.string, # self.serveruuid.string)) # PARSERS SECTION # ----------------------------- def _parse_keep_alive(self): data = self.packet.readpkt(self.pktSB.KEEP_ALIVE[PARSER]) if data[0] == self.keepalive_val: self.time_client_responded = time.time() return False # plugin channel handler # ----------------------- def _parse_plugin_message(self): channel = self.packet.readpkt([STRING, ])[0] if channel not in self.proxy.registered_channels: # we are not actually registering our channels with the MC server. return True if channel == "WRAPPER.PY|PING": self.proxy.pinged = True return False if channel == "WRAPPER.PY|": if self.clientversion < PROTOCOL_1_8START: datarest = self.packet.readpkt([SHORT, REST])[1] else: datarest = self.packet.readpkt([REST, ])[0] print("\nDATA REST = %s\n" % datarest) response = json.loads(datarest.decode('utf-8'), encoding='utf-8') self._plugin_response(response) return True return True def _plugin_response(self, response): if "ip" in response: self.shared = { "username": response["username"], "uuid": response["uuid"], "ip": response["ip"], "received": True, } self.ip = response["ip"] self.mojanguuid = response["uuid"] # Login parsers # ----------------------- def _parse_handshaking(self): # self.log.debug("HANDSHAKE") # "version|address|port|state" data = self.packet.readpkt([VARINT, STRING, USHORT, VARINT]) self.clientversion = data[0] self._getclientpacketset() self.serveraddressplayerused = data[1] self.serverportplayerused = data[2] requestedstate = data[3] if requestedstate == STATUS: self.state = STATUS # wrapper will handle responses, so do not pass this to the server. return False if requestedstate == LOGIN: # TODO - coming soon: allow client connections # despite lack of server connection if self.servervitals.protocolVersion == -1: # ... returns -1 to signal no server self.disconnect("The server is not started.") return False if not self.servervitals.state == 2: self.disconnect("Server has not finished booting. Please try" " connecting again in a few seconds") return False if PROTOCOL_1_9START < self.clientversion < PROTOCOL_1_9REL1: self.disconnect("You're running an unsupported snapshot" " (protocol: %s)!" % self.clientversion) return False if self.servervitals.protocolVersion == self.clientversion: # login start... self.state = LOGIN # packet passes to server, which will also switch to Login return True else: self.disconnect("You're not running the same Minecraft" " version as the server!") return False self.disconnect("Invalid HANDSHAKE: 'requested state:" " %d'" % requestedstate) return False def _parse_status_ping(self): # self.log.debug("SB -> STATUS PING") data = self.packet.readpkt([LONG]) self.packet.sendpkt(self.pktCB.PING_PONG, [LONG], [data[0]]) # self.log.debug("CB (W)-> STATUS PING") self.state = HANDSHAKE return False def _parse_status_request(self): # self.log.debug("SB -> STATUS REQUEST") sample = [] for player in self.servervitals.players: playerobj = self.servervitals.players[player] if playerobj.username not in self.hidden_ops: sample.append({"name": playerobj.username, "id": str(playerobj.mojangUuid)}) if len(sample) > 5: break reported_version = self.servervitals.protocolVersion reported_name = self.servervitals.version motdtext = self.servervitals.motd if self.clientversion >= PROTOCOL_1_8START: motdtext = json.loads(processcolorcodes(motdtext.replace( "\\", ""))) self.MOTD = { "description": motdtext, "players": { "max": int(self.servervitals.maxPlayers), "online": len(self.servervitals.players), "sample": sample }, "version": { "name": reported_name, "protocol": reported_version } } # add Favicon, if it exists if self.servervitals.serverIcon: self.MOTD["favicon"] = self.servervitals.serverIcon # add Forge information, if applicable. if self.proxy.forge: self.MOTD["modinfo"] = self.proxy.mod_info["modinfo"] self.packet.sendpkt( self.pktCB.PING_JSON_RESPONSE, [STRING], [json.dumps(self.MOTD)]) # self.log.debug("CB (W)-> JSON RESPONSE") # after this, proxy waits for the expected PING to # go back to Handshake mode return False def _parse_login_start(self): # self.log.debug("SB -> LOGIN START") data = self.packet.readpkt([STRING, NULL]) # "username" self.username = data[0] # just to be clear, this refers to wrapper's mode, not the server. if self.onlinemode: # Wrapper sends client a login encryption request # 1.7.x versions if self.servervitals.protocolVersion < 6: # send to client 1.7 self.packet.sendpkt( self.pktCB.LOGIN_ENCR_REQUEST, [STRING, BYTEARRAY_SHORT, BYTEARRAY_SHORT], (self.serverID, self.publicKey, self.verifyToken)) else: # send to client 1.8 + self.packet.sendpkt( self.pktCB.LOGIN_ENCR_REQUEST, [STRING, BYTEARRAY, BYTEARRAY], (self.serverID, self.publicKey, self.verifyToken)) # self.log.debug("CB (W)-> LOGIN ENCR REQUEST") # Server UUID is always offline (at the present time) # MCUUID object self.serveruuid = self.proxy.uuids.getuuidfromname(self.username) else: # Wrapper offline and not authenticating # maybe it is the destination of a hub? or you use another # way to authenticate (password plugin?) # Server UUID is always offline (at the present time) self.uuid = self.proxy.uuids.getuuidfromname(self.username) # Since wrapper is offline, we are using offline for self.uuid also self.serveruuid = self.uuid # MCUUID object # log the client on self.state = PLAY self.logon_client_into_proxy() # connect to server self.connect_to_server() return False def _parse_login_encr_response(self): # the client is RESPONDING to our request for # encryption (if we sent one above) # read response Tokens - "shared_secret|verify_token" # self.log.debug("SB -> LOGIN ENCR RESPONSE") if self.servervitals.protocolVersion < 6: data = self.packet.readpkt([BYTEARRAY_SHORT, BYTEARRAY_SHORT]) else: data = self.packet.readpkt([BYTEARRAY, BYTEARRAY]) sharedsecret = encryption.decrypt_shared_secret( data[0], self.privateKey) verifytoken = encryption.decrypt_shared_secret( data[1], self.privateKey) h = hashlib.sha1() # self.serverID already encoded h.update(self.serverID) h.update(sharedsecret) h.update(self.publicKey) serverid = self.packet.hexdigest(h) # feed info to packet.py for parsing self.packet.sendCipher = encryption.AES128CFB8(sharedsecret) self.packet.recvCipher = encryption.AES128CFB8(sharedsecret) # verify correct response if not verifytoken == self.verifyToken: self.disconnect("Verify tokens are not the same") return False # determine if IP is silent banned: if self.ipbanned: self.log.info("Player %s tried to connect from banned ip:" " %s", self.username, self.ip) self.state = HANDSHAKE if self.silent_bans: self.disconnect("unknown host") else: # self disconnect does not "return" anything. self.disconnect("Your address is IP-banned from this server!.") return False # begin Client logon process # Wrapper in online mode, taking care of authentication if self._login_authenticate_client(serverid) is False: return False # client failed to authenticate # TODO Whitelist processing Here (or should it be at javaserver start?) # log the client on self.state = PLAY self.logon_client_into_proxy() # connect to server self.connect_to_server() return False # Lobby parsers # ----------------------- def _parse_lobby_chat_message(self): data = self.packet.readpkt([STRING]) if data is None: return True # Get the packet chat message contents chatmsg = data[0] if chatmsg in ("/lobby", "/hub"): # stop any raining # close current connection and start new one # noinspection PyBroadException self.change_servers() return False # we are just sniffing this packet for lobby return # commands, so send it on to the destination. return True def parse(self, pkid): if pkid in self.parsers[self.state]: return self.parsers[self.state][pkid]() else: return True def _define_parsers(self): # the packets we parse and the methods that parse them. self.parsers = { HANDSHAKE: { self.pktSB.HANDSHAKE: self._parse_handshaking, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, STATUS: { self.pktSB.STATUS_PING: self._parse_status_ping, self.pktSB.REQUEST: self._parse_status_request, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, LOGIN: { self.pktSB.LOGIN_START: self._parse_login_start, self.pktSB.LOGIN_ENCR_RESPONSE: self._parse_login_encr_response, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, PLAY: { self.pktSB.CHAT_MESSAGE[PKT]: self.parse_sb.parse_play_chat_message, self.pktSB.CLICK_WINDOW: self.parse_sb.parse_play_click_window, self.pktSB.CLIENT_SETTINGS: self.parse_sb.parse_play_client_settings, self.pktSB.HELD_ITEM_CHANGE: self.parse_sb.parse_play_held_item_change, self.pktSB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktSB.PLAYER_BLOCK_PLACEMENT: self.parse_sb.parse_play_player_block_placement, self.pktSB.PLAYER_DIGGING: self.parse_sb.parse_play_player_digging, self.pktSB.PLAYER_LOOK: self.parse_sb.parse_play_player_look, self.pktSB.PLAYER_POSITION: self.parse_sb.parse_play_player_position, self.pktSB.PLAYER_POSLOOK[PKT]: self.parse_sb.parse_play_player_poslook, self.pktSB.PLAYER_UPDATE_SIGN: self.parse_sb.parse_play_player_update_sign, self.pktSB.SPECTATE: self.parse_sb.parse_play_spectate, self.pktSB.USE_ITEM: self.parse_sb.parse_play_use_item, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, LOBBY: { self.pktSB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktSB.CHAT_MESSAGE[PKT]: self._parse_lobby_chat_message, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, IDLE: { self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, } }
class ServerConnection(object): def __init__(self, client, ip=None, port=None): """ This class ServerConnection is a "fake" client connecting to the server. It receives "CLIENT BOUND" packets from server, parses them, and sends them on to the client. ServerConnection receives the parent client as it's argument. It receives the proxy instance from the Client. Therefore, a server instance does not really validly exist unless it has a valid parent client. Client, by contrast, can exist and run in the absence of a server. """ # TODO server needs to be a true child of clientconnection process. # It should not close its own instance, etc # basic __init__ items from passed arguments self.client = client self.proxy = client.proxy self.log = client.log self.ip = ip self.port = port # server setup and operating paramenters self.abort = False self.state = HANDSHAKE self.packet = None self.parse_cb = None # dictionary of parser packet constants and associated parsing methods self.parsers = {} self.entity_controls = self.proxy.ent_config["enable-entity-controls"] self.version = -1 # self parsers get updated here self._refresh_server_version() # temporary assignment. The actual socket is assigned later. self.server_socket = socket.socket() self.infos_debug = "(player=%s, IP=%s, Port=%s)" % ( self.client.username, self.ip, self.port) def _refresh_server_version(self): """Get serverversion for mcpackets use""" self.version = self.proxy.srv_data.protocolVersion self.pktSB = mcpackets_sb.Packets(self.version) self.pktCB = mcpackets_cb.Packets(self.version) self.parse_cb = ParseCB(self, self.packet) self._define_parsers() def send(self, packetid, xpr, payload): """ Not supported. A wrapper of packet.send(), which is further a wrapper for packet.sendpkt(); both wrappers exist for older code compatability purposes only for 0.7.x version plugins that might use it.""" self.log.debug("deprecated server.send() called. Use " "server.packet.sendpkt() for best performance.") self.packet.send(packetid, xpr, payload) pass def connect(self): """ This simply establishes the tcp socket connection and starts the flush loop, NOTHING MORE. """ self.state = LOGIN # Connect to a local server address if self.ip is None: self.server_socket.connect( ("localhost", self.proxy.srv_data.server_port)) # Connect to some specific server address else: self.server_socket.connect((self.ip, self.port)) # start packet handler self.packet = Packet(self.server_socket, self) self.packet.version = self.client.clientversion # define parsers self.parse_cb = ParseCB(self, self.packet) self._define_parsers() t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start() def close_server(self, reason="Disconnected"): """ Client is responsible for closing the server connection and handling lobby states. """ self.log.debug("%s called serverconnection.close_server(): %s", self.client.username, reason) # end 'handle' and 'flush_loop' cleanly self.abort = True time.sleep(0.1) # noinspection PyBroadException try: self.server_socket.shutdown(2) self.log.debug("Sucessfully closed server socket for" " %s", self.client.username) except: self.log.debug("Server socket for %s already " "closed", self.infos_debug) pass # allow packet instance to be Garbage Collected self.packet = None def flush_loop(self): while not self.abort: try: self.packet.flush() except socket.error: self.log.debug("Socket_error- server socket was closed" " %s", self.infos_debug) break time.sleep(0.01) self.log.debug("%s serverconnection flush_loop thread ended.", self.client.username) def handle(self): while not self.abort: # get packet try: pkid, original = self.packet.grabpacket() # possible connection losses: except EOFError: # This is not a true error, but means the connection closed. return self.close_server("handle EOF") except socket.error: return self.close_server("handle socket.error") except Exception as e: return self.close_server( "handle Exception: %s TRACEBACK: \n%s" % (e, traceback)) # parse it if self.parse(pkid) and self.client.state == PLAY: try: self.client.packet.send_raw(original) except Exception as e: return self.close_server( "handle could not send packet '%s'. " "Exception: %s TRACEBACK: \n%s" % (pkid, e, traceback)) def _parse_keep_alive(self): data = self.packet.readpkt(self.pktSB.KEEP_ALIVE[PARSER]) self.packet.sendpkt(self.pktSB.KEEP_ALIVE[PKT], self.pktSB.KEEP_ALIVE[PARSER], data) return False def _transmit_upstream(self): # TODO this probably needs to be removed. Wrapper's proxy is fragile/slow enought ATM """ transmit wrapper channel status info to the server's direction to help sync hub/lobby wrappers """ channel = "WRAPPER|SYNC" # received SYNC from the client (this is a child wrapper) received = self.proxy.shared["received"] # if true, this is a multiworld (child wrapper instance) sent = self.proxy.shared["sent"] state = self.state if self.version < PROTOCOL_1_8START: self.packet.sendpkt(self.pktCB.PLUGIN_MESSAGE, [STRING, SHORT, BOOL, BOOL, BYTE], [channel, 3, received, sent, state]) else: self.packet.sendpkt(self.pktCB.PLUGIN_MESSAGE, [STRING, BOOL, BOOL, BYTE], [channel, received, sent, state]) # PARSERS SECTION # ----------------------------- # Login parsers # ----------------------- def _parse_login_disconnect(self): message = self.packet.readpkt([STRING]) self.log.info("Disconnected from server: %s", message) self.close_server(message) return False def _parse_login_encr_request(self): self.close_server("Server is in online mode. Please turn it off " "in server.properties and allow Proxy to " "handle the authetication.") return False # Login Success - UUID & Username are sent in this packet as strings def _parse_login_success(self): self.state = PLAY # todo - we may not need to assign this to a variable. # (we supplied uuid/name anyway!) # noinspection PyUnusedLocal data = self.packet.readpkt([STRING, STRING]) return False def _parse_login_set_compression(self): data = self.packet.readpkt([VARINT]) # ("varint:threshold") if data[0] != -1: self.packet.compression = True self.packet.compressThreshold = data[0] else: self.packet.compression = False self.packet.compressThreshold = -1 time.sleep(10) return # False def parse(self, pkid): if pkid in self.parsers[self.state]: return self.parsers[self.state][pkid]() return True def _define_parsers(self): # the packets we parse and the methods that parse them. self.parsers = { HANDSHAKE: {}, # maps identically to OFFLINE ( '0' ) LOGIN: { self.pktCB.LOGIN_DISCONNECT: self._parse_login_disconnect, self.pktCB.LOGIN_ENCR_REQUEST: self._parse_login_encr_request, self.pktCB.LOGIN_SUCCESS: self._parse_login_success, self.pktCB.LOGIN_SET_COMPRESSION: self._parse_login_set_compression }, PLAY: { self.pktCB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktCB.CHAT_MESSAGE[PKT]: self.parse_cb.parse_play_chat_message, self.pktCB.JOIN_GAME[PKT]: self.parse_cb.parse_play_join_game, self.pktCB.TIME_UPDATE: self.parse_cb.parse_play_time_update, self.pktCB.SPAWN_POSITION: self.parse_cb.parse_play_spawn_position, self.pktCB.RESPAWN: self.parse_cb.parse_play_respawn, self.pktCB.PLAYER_POSLOOK[PKT]: self.parse_cb.parse_play_player_poslook, self.pktCB.USE_BED: self.parse_cb.parse_play_use_bed, self.pktCB.SPAWN_PLAYER: self.parse_cb.parse_play_spawn_player, self.pktCB.CHANGE_GAME_STATE: self.parse_cb.parse_play_change_game_state, self.pktCB.OPEN_WINDOW[PKT]: self.parse_cb.parse_play_open_window, self.pktCB.SET_SLOT[PKT]: self.parse_cb.parse_play_set_slot, self.pktCB.PLAYER_LIST_ITEM: self.parse_cb.parse_play_player_list_item, self.pktCB.DISCONNECT: self.parse_cb.parse_play_disconnect, } } if self.entity_controls: self.parsers[PLAY][ self.pktCB. SPAWN_OBJECT] = self.parse_cb.parse_play_spawn_object self.parsers[PLAY][ self.pktCB.SPAWN_MOB] = self.parse_cb.parse_play_spawn_mob self.parsers[PLAY][ self.pktCB. ENTITY_RELATIVE_MOVE] = self.parse_cb.parse_play_entity_relative_move self.parsers[PLAY][ self.pktCB. ENTITY_TELEPORT] = self.parse_cb.parse_play_entity_teleport self.parsers[PLAY][ self.pktCB. ATTACH_ENTITY] = self.parse_cb.parse_play_attach_entity self.parsers[PLAY][ self.pktCB. DESTROY_ENTITIES] = self.parse_cb.parse_play_destroy_entities
class ServerConnection(object): def __init__(self, client, ip=None, port=None): """ This class ServerConnection is a "fake" client connecting to the server. It receives "CLIENT BOUND" packets from server, parses them, and sends them on to the client. ServerConnection receives the parent client as it's argument. It receives the proxy instance from the Client. Therefore, a server instance does not really validly exist unless it has a valid parent client. Client, by contrast, can exist and run in the absence of a server. """ # TODO server needs to be a true child of clientconnection process. # It should not close its own instance, etc # basic __init__ items from passed arguments self.client = client self.proxy = client.proxy self.log = client.log self.ip = ip self.port = port # server setup and operating paramenters self.abort = False self.state = HANDSHAKE self.packet = None self.parse_cb = None # dictionary of parser packet constants and associated parsing methods self.parsers = {} self.entity_controls = self.proxy.ent_config["enable-entity-controls"] self.version = -1 # self parsers get updated here self._refresh_server_version() # temporary assignment. The actual socket is assigned later. self.server_socket = socket.socket() self.infos_debug = "(player=%s, IP=%s, Port=%s)" % ( self.client.username, self.ip, self.port) def _refresh_server_version(self): """Get serverversion for mcpackets use""" self.version = self.proxy.srv_data.protocolVersion self.pktSB = mcpackets_sb.Packets(self.version) self.pktCB = mcpackets_cb.Packets(self.version) self.parse_cb = ParseCB(self, self.packet) self._define_parsers() def send(self, packetid, xpr, payload): """ Not supported. A wrapper of packet.send(), which is further a wrapper for packet.sendpkt(); both wrappers exist for older code compatability purposes only for 0.7.x version plugins that might use it.""" self.log.debug("deprecated server.send() called. Use " "server.packet.sendpkt() for best performance.") self.packet.send(packetid, xpr, payload) pass def connect(self): """ This simply establishes the tcp socket connection and starts the flush loop, NOTHING MORE. """ self.state = LOGIN # Connect to a local server address if self.ip is None: self.server_socket.connect(( "localhost", self.proxy.srv_data.server_port)) # Connect to some specific server address else: self.server_socket.connect((self.ip, self.port)) # start packet handler self.packet = Packet(self.server_socket, self) self.packet.version = self.client.clientversion # define parsers self.parse_cb = ParseCB(self, self.packet) self._define_parsers() t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start() def close_server(self, reason="Disconnected"): """ Client is responsible for closing the server connection and handling lobby states. """ self.log.debug("%s called serverconnection.close_server(): %s", self.client.username, reason) # end 'handle' and 'flush_loop' cleanly self.abort = True time.sleep(0.1) # noinspection PyBroadException try: self.server_socket.shutdown(2) self.log.debug("Sucessfully closed server socket for" " %s", self.client.username) except: self.log.debug("Server socket for %s already " "closed", self.infos_debug) pass # allow packet instance to be Garbage Collected self.packet = None def flush_loop(self): while not self.abort: try: self.packet.flush() except socket.error: self.log.debug("Socket_error- server socket was closed" " %s", self.infos_debug) break time.sleep(0.01) self.log.debug("%s serverconnection flush_loop thread ended.", self.client.username) def handle(self): while not self.abort: # get packet try: pkid, original = self.packet.grabpacket() # possible connection losses: except EOFError: # This is not a true error, but means the connection closed. return self.close_server("handle EOF") except socket.error: return self.close_server("handle socket.error") except Exception as e: return self.close_server( "handle Exception: %s TRACEBACK: \n%s" % (e, traceback)) # parse it if self.parse(pkid) and self.client.state == PLAY: try: self.client.packet.send_raw(original) except Exception as e: return self.close_server( "handle could not send packet '%s'. " "Exception: %s TRACEBACK: \n%s" % ( pkid, e, traceback) ) def _parse_keep_alive(self): data = self.packet.readpkt( self.pktSB.KEEP_ALIVE[PARSER]) self.packet.sendpkt( self.pktSB.KEEP_ALIVE[PKT], self.pktSB.KEEP_ALIVE[PARSER], data) return False def _transmit_upstream(self): # TODO this probably needs to be removed. Wrapper's proxy is fragile/slow enought ATM """ transmit wrapper channel status info to the server's direction to help sync hub/lobby wrappers """ channel = "WRAPPER|SYNC" # received SYNC from the client (this is a child wrapper) received = self.proxy.shared["received"] # if true, this is a multiworld (child wrapper instance) sent = self.proxy.shared["sent"] state = self.state if self.version < PROTOCOL_1_8START: self.packet.sendpkt( self.pktCB.PLUGIN_MESSAGE, [STRING, SHORT, BOOL, BOOL, BYTE], [channel, 3, received, sent, state]) else: self.packet.sendpkt( self.pktCB.PLUGIN_MESSAGE, [STRING, BOOL, BOOL, BYTE], [channel, received, sent, state]) # PARSERS SECTION # ----------------------------- # Login parsers # ----------------------- def _parse_login_disconnect(self): message = self.packet.readpkt([STRING]) self.log.info("Disconnected from server: %s", message) self.close_server(message) return False def _parse_login_encr_request(self): self.close_server("Server is in online mode. Please turn it off " "in server.properties and allow Proxy to " "handle the authetication.") return False # Login Success - UUID & Username are sent in this packet as strings def _parse_login_success(self): self.state = PLAY # todo - we may not need to assign this to a variable. # (we supplied uuid/name anyway!) # noinspection PyUnusedLocal data = self.packet.readpkt([STRING, STRING]) return False def _parse_login_set_compression(self): data = self.packet.readpkt([VARINT]) # ("varint:threshold") if data[0] != -1: self.packet.compression = True self.packet.compressThreshold = data[0] else: self.packet.compression = False self.packet.compressThreshold = -1 time.sleep(10) return # False def parse(self, pkid): if pkid in self.parsers[self.state]: return self.parsers[self.state][pkid]() return True def _define_parsers(self): # the packets we parse and the methods that parse them. self.parsers = { HANDSHAKE: {}, # maps identically to OFFLINE ( '0' ) LOGIN: { self.pktCB.LOGIN_DISCONNECT: self._parse_login_disconnect, self.pktCB.LOGIN_ENCR_REQUEST: self._parse_login_encr_request, self.pktCB.LOGIN_SUCCESS: self._parse_login_success, self.pktCB.LOGIN_SET_COMPRESSION: self._parse_login_set_compression }, PLAY: { self.pktCB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktCB.CHAT_MESSAGE[PKT]: self.parse_cb.parse_play_chat_message, self.pktCB.JOIN_GAME[PKT]: self.parse_cb.parse_play_join_game, self.pktCB.TIME_UPDATE: self.parse_cb.parse_play_time_update, self.pktCB.SPAWN_POSITION: self.parse_cb.parse_play_spawn_position, self.pktCB.RESPAWN: self.parse_cb.parse_play_respawn, self.pktCB.PLAYER_POSLOOK[PKT]: self.parse_cb.parse_play_player_poslook, self.pktCB.USE_BED: self.parse_cb.parse_play_use_bed, self.pktCB.SPAWN_PLAYER: self.parse_cb.parse_play_spawn_player, self.pktCB.CHANGE_GAME_STATE: self.parse_cb.parse_play_change_game_state, self.pktCB.OPEN_WINDOW[PKT]: self.parse_cb.parse_play_open_window, self.pktCB.SET_SLOT[PKT]: self.parse_cb.parse_play_set_slot, self.pktCB.PLAYER_LIST_ITEM: self.parse_cb.parse_play_player_list_item, self.pktCB.DISCONNECT: self.parse_cb.parse_play_disconnect, } } if self.entity_controls: self.parsers[PLAY][ self.pktCB.SPAWN_OBJECT] = self.parse_cb.parse_play_spawn_object self.parsers[PLAY][ self.pktCB.SPAWN_MOB] = self.parse_cb.parse_play_spawn_mob self.parsers[PLAY][ self.pktCB.ENTITY_RELATIVE_MOVE] = self.parse_cb.parse_play_entity_relative_move self.parsers[PLAY][ self.pktCB.ENTITY_TELEPORT] = self.parse_cb.parse_play_entity_teleport self.parsers[PLAY][ self.pktCB.ATTACH_ENTITY] = self.parse_cb.parse_play_attach_entity self.parsers[PLAY][ self.pktCB.DESTROY_ENTITIES] = self.parse_cb.parse_play_destroy_entities
class ServerConnection(object): def __init__(self, client, ip=None, port=None): """ This class ServerConnection is a "fake" client connecting to the server. It receives "CLIENT BOUND" packets from server, parses them, and sends them on to the client. ServerConnection receives the parent client as it's argument. It receives the proxy instance from the Client. Therefore, a server instance does not really validly exist unless it has a valid parent client. Client, by contrast, can exist and run in the absence of a server. """ # basic __init__ items from passed arguments self.client = client self.username = self.client.username self.proxy = client.proxy self.log = client.log self.ip = ip self.port = port self.srv_data = self.proxy.srv_data # server setup and operating paramenters self.abort = False self.flush_rate = self.client.flush_rate self.state = HANDSHAKE self.packet = None self.parse_cb = None # dictionary of parser packet constants and associated parsing methods self.parsers = {} self.entity_controls = self.proxy.ent_config["enable-entity-controls"] self.version = -1 # self parsers get updated here self._refresh_server_version() # temporary assignment. The actual socket is assigned later. self.server_socket = socket.socket() self.infos_debug = "(player=%s, IP=%s, Port=%s)" % (self.username, self.ip, self.port) def _refresh_server_version(self): """Get serverversion for mcpackets use""" self.version = self.proxy.srv_data.protocolVersion self.pktSB = mcpackets_sb.Packets(self.version) self.pktCB = mcpackets_cb.Packets(self.version) self.parse_cb = ParseCB(self, self.packet) self._define_parsers() # ---------------------------------------------- # Client calls connect(), then handle() # ---------------------------------------------- def connect(self): """ This simply establishes the tcp socket connection and starts the flush loop, NOTHING MORE. """ self.state = LOGIN # Connect to a local server address if self.ip is None: self.server_socket.connect( ("localhost", self.proxy.srv_data.server_port)) # Connect to some specific server address else: self.server_socket.connect((self.ip, self.port)) # start packet handler self.packet = Packet(self.server_socket, self) self.packet.version = self.client.clientversion # define parsers self.parse_cb = ParseCB(self, self.packet) self._define_parsers() t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start() def flush_loop(self): rate = self.flush_rate while not self.abort: time.sleep(rate) try: self.packet.flush() except AttributeError: self.log.debug("%s server packet instance gone.", self.username) except socket.error: self.log.debug("Socket_error- server socket was closed" " %s", self.infos_debug) break self.log.debug("%s serverconnection flush_loop thread ended.", self.username) def handle(self): while not (self.abort or self.client.abort): # get packet try: pkid, orig_packet = self.packet.grabpacket() # noqa # possible connection losses: except EOFError: # This is not a true error, but means the connection closed. return self.close_server("handle EOF") except socket.error: return self.close_server("handle socket.error") except Exception as e: return self.close_server( "handle Exception: %s TRACEBACK: \n%s" % (e, traceback.format_exc())) # parse it # send packet if parsing passed and client in play mode. # all packets are parsed, but only play mode ones are transmitted. if self.parse(pkid) and self.client.state == PLAY: try: # self.parse will reject (False) any packet proxy modifies. self.client.packet.send_raw_untouched(orig_packet) except Exception as e: return self.close_server( "handle() could not send packet '%s'. " "Exception: %s TRACEBACK: \n%s" % (pkid, e, traceback.format_exc())) return self.close_server("handle() received abort signal.") def close_server(self, reason="Disconnected"): """ Client is responsible for closing the server connection and handling lobby states. """ # if close_server already ran, server_socket will be None. if not self.server_socket: return False self.log.info("%s's proxy server connection closed: %s", self.username, reason) # end 'handle' and 'flush_loop' cleanly self.abort = True # time.sleep(0.1) # noinspection PyBroadException try: self.server_socket.shutdown(2) self.log.debug("Sucessfully closed server socket for" " %s", self.username) # allow old packet and socket to be Garbage Collected condition = True except: condition = False # allow old packet and socket to be Garbage Collected self.packet = None self.server_socket = None return condition # PARSERS SECTION # ----------------------------- def _parse_keep_alive(self): data = self.packet.readpkt(self.pktSB.KEEP_ALIVE[PARSER]) # data is a list of one item and will be sent back that way self.packet.sendpkt(self.pktSB.KEEP_ALIVE[PKT], self.pktSB.KEEP_ALIVE[PARSER], data) return False # Plugin channel senders # ----------------------- # SB RESP def plugin_response(self): channel = "WRAPPER.PY|RESP" self.client.info["server-is-wrapper"] = True data = json.dumps(self.client.info) # only our wrappers communicate with this, so, format is not critical. self.packet.sendpkt(self.pktSB.PLUGIN_MESSAGE[PKT], [STRING, STRING], (channel, data)) # SB PING def plugin_ping(self): """ this is initiated by server/parse_cb.py parse_play_join_game """ channel = "WRAPPER.PY|PING" data = int(time.time()) self.packet.sendpkt(self.pktSB.PLUGIN_MESSAGE[PKT], [STRING, INT], (channel, data)) def _parse_plugin_message(self): """server-bound""" channel = self.packet.readpkt([ STRING, ])[0] if channel not in self.proxy.registered_channels: # we are not actually registering our channels with the MC server # and there will be no parsing of other channels. return True # SB PING if channel == "WRAPPER.PY|PONG": # then we now know this wrapper is a child wrapper since # minecraft clients will not ping us self.client.info["client-is-wrapper"] = True self.plugin_response() # do not pass Wrapper.py registered plugin messages return False # Plugin channel parsers # ----------------------- # Login parsers # ----------------------- def _parse_login_disconnect(self): message = self.packet.readpkt([STRING])[0] self.log.info("Disconnected from server: %s", message) self.client.notify_disconnect(message) self.close_server(message) return False def _parse_login_encr_request(self): self.close_server("Server is in online mode. Please turn it off " "in server.properties and allow Proxy to " "handle the authentication.") return False # Login Success - UUID & Username are sent in this packet as strings def _parse_login_success(self): self.state = PLAY # todo - we may not need to assign this to a variable. # (we supplied uuid/name anyway!) # noinspection PyUnusedLocal data = self.packet.readpkt([STRING, STRING]) self.client.local_uuid = MCUUID(data[0]) return False def _parse_login_set_compression(self): data = self.packet.readpkt([VARINT])[0] # ("varint:threshold") if data == -1: self.packet.compression = False else: self.packet.compression = True self.packet.compressThreshold = data # no point - client connection already has the client waiting in # compression enabled mode return False def parse(self, pkid): if pkid in self.parsers[self.state]: return self.parsers[self.state][pkid]() return True def _define_parsers(self): # the packets we parse and the methods that parse them. self.parsers = { HANDSHAKE: {}, # maps identically to OFFLINE ( '0' ) LOGIN: { self.pktCB.LOGIN_DISCONNECT[PKT]: self._parse_login_disconnect, self.pktCB.LOGIN_ENCR_REQUEST[PKT]: self._parse_login_encr_request, self.pktCB.LOGIN_SUCCESS[PKT]: self._parse_login_success, self.pktCB.LOGIN_SET_COMPRESSION[PKT]: self._parse_login_set_compression, self.pktCB.PLUGIN_MESSAGE[PKT]: self._parse_plugin_message }, PLAY: { # required base items self.pktCB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktCB.CHAT_MESSAGE[PKT]: self.parse_cb.play_chat_message, self.pktCB.PLUGIN_MESSAGE[PKT]: self._parse_plugin_message, self.pktCB.DISCONNECT[PKT]: self.parse_cb.play_disconnect, # required for proper player list display self.pktCB.PLAYER_LIST_ITEM[PKT]: self.parse_cb.play_player_list_item, self.pktCB.SPAWN_PLAYER[PKT]: self.parse_cb.play_spawn_player, # features self.pktCB.USE_BED[PKT]: self.parse_cb.play_use_bed, self.pktCB.TIME_UPDATE[PKT]: self.parse_cb.play_time_update, self.pktCB.TAB_COMPLETE[PKT]: self.parse_cb.play_tab_complete, self.pktCB.SPAWN_POSITION[PKT]: self.parse_cb.play_spawn_position, # Monitor player states self.pktCB.CHANGE_GAME_STATE[PKT]: self.parse_cb.play_change_game_state, self.pktCB.PLAYER_POSLOOK[PKT]: self.parse_cb.play_player_poslook, self.pktCB.JOIN_GAME[PKT]: self.parse_cb.play_join_game, # hub information self.pktCB.RESPAWN[PKT]: self.parse_cb.play_respawn, self.pktCB.UPDATE_HEALTH[PKT]: self.parse_cb.update_health, self.pktCB.CHUNK_DATA[PKT]: self.parse_cb.play_chunk_data, # inventory management self.pktCB.OPEN_WINDOW[PKT]: self.parse_cb.play_open_window, self.pktCB.WINDOW_ITEMS[PKT]: self.parse_cb.play_window_items, self.pktCB.HELD_ITEM_CHANGE[PKT]: self.parse_cb.play_held_item_change, self.pktCB.SET_SLOT[PKT]: self.parse_cb.play_set_slot } } if self.entity_controls: self.parsers[PLAY][ self.pktCB.SPAWN_OBJECT[PKT]] = self.parse_cb.play_spawn_object self.parsers[PLAY][ self.pktCB.SPAWN_MOB[PKT]] = self.parse_cb.play_spawn_mob self.parsers[PLAY][self.pktCB.ENTITY_RELATIVE_MOVE[ PKT]] = self.parse_cb.play_entity_relative_move # noqa self.parsers[PLAY][self.pktCB.ENTITY_TELEPORT[ PKT]] = self.parse_cb.play_entity_teleport # noqa self.parsers[PLAY][self.pktCB.ATTACH_ENTITY[ PKT]] = self.parse_cb.play_attach_entity # noqa self.parsers[PLAY][self.pktCB.DESTROY_ENTITIES[ PKT]] = self.parse_cb.play_destroy_entities # noqa
def __init__(self, proxy, clientsock, client_addr, banned=False): """ Handle the client connection. This class Client is a "fake" server, accepting connections from clients. It receives "SERVER BOUND" packets from client, parses them, and forards them on to the server. It "sends" to the client (self.send() or self.sendpkt()) Client receives the parent proxy as it's argument. No longer receives the proxy's wrapper instance! All data is passed via servervitals from proxy's srv_data. """ # basic __init__ items from passed arguments self.client_socket = clientsock self.client_address = client_addr self.proxy = proxy self.publicKey = self.proxy.publicKey self.privateKey = self.proxy.privateKey self.servervitals = self.proxy.srv_data self.log = self.proxy.log self.ipbanned = banned # constants from config: self.spigot_mode = self.proxy.config["spigot-mode"] self.hidden_ops = self.proxy.config["hidden-ops"] self.silent_bans = self.proxy.config["silent-ipban"] # client setup and operating paramenters self.username = "******" self.packet = Packet(self.client_socket, self) self.verifyToken = encryption.generate_challenge_token() self.serverID = encryption.generate_server_id().encode('utf-8') self.MOTD = {} # client will reset this later, if need be.. self.clientversion = self.servervitals.protocolVersion # default server port (to this wrapper's server) self.serverport = self.servervitals.server_port self.onlinemode = self.proxy.config["online-mode"] # packet stuff self.pktSB = mcpackets_sb.Packets(self.clientversion) self.pktCB = mcpackets_cb.Packets(self.clientversion) self.parse_sb = ParseSB(self, self.packet) # dictionary of parser packet constants and associated parsing methods self.parsers = {} self._getclientpacketset() self.buildmode = False # keep alive data self.time_server_pinged = 0 self.time_client_responded = 0 self.keepalive_val = 0 # client and server status self.abort = False # Proxy ServerConnection() (not the javaserver) self.server_connection = None self.state = HANDSHAKE # UUIDs - all should use MCUUID unless otherwise specified # -------------------------------------------------------- # Server UUID - which is the local offline UUID. self.serveruuid = None # -------------------------------------------------------- # The client UUID authenticated by connection to session server. self.uuid = None # -------------------------------------------------------- # the formal, unique, mojang UUID as looked up on mojang servers. # This ID will be the same no matter what mode wrapper is in # or whether it is a lobby, etc. This will be the formal uuid # to use for all wrapper internal functions for referencing a # unique player. # TODO - Unused except by plugin channel. # not to be confused with the fact that API player has a property # with the same name. self.mojanguuid = None # information gathered during login or socket connection processes # TODO in the future, we could use plugin channels to # communicate these to subworld wrappers From socket data self.address = None # this will store the client IP for use by player.py self.ip = self.client_address[0] # From client handshake. For vanilla clients, it is what # the user entered to connect to your wrapper. self.serveraddressplayerused = None self.serverportplayerused = None # player api Items # EID collected by serverconnection (changes on each server) self.server_eid = 0 self.gamemode = 0 self.dimension = 0 self.position = (0, 0, 0) # X, Y, Z self.head = (0, 0) # Yaw, Pitch self.inventory = {} self.slot = 0 self.riding = None # last placement (for use in cases of bucket use) self.lastplacecoords = (0, 0, 0) self.properties = {} self.clientSettings = False self.clientSettingsSent = False self.skinBlob = {} self.windowCounter = 2 self.currentwindowid = -1 self.noninventoryslotcount = 0 self.lastitem = None # wrapper's own channel on each player client self.shared = { "username": "", "uuid": "", "ip": "", "received": False, "sent": False }
class Client(object): def __init__(self, proxy, clientsock, client_addr, banned=False): """ Handle the client connection. This class Client is a "fake" server, accepting connections from clients. It receives "SERVER BOUND" packets from client, parses them, and forards them on to the server. It "sends" to the client (self.send() or self.sendpkt()) Client receives the parent proxy as it's argument. No longer receives the proxy's wrapper instance! All data is passed via servervitals from proxy's srv_data. """ # basic __init__ items from passed arguments self.client_socket = clientsock self.client_address = client_addr self.proxy = proxy self.publicKey = self.proxy.publicKey self.privateKey = self.proxy.privateKey self.servervitals = self.proxy.srv_data self.log = self.proxy.log self.ipbanned = banned # constants from config: self.spigot_mode = self.proxy.config["spigot-mode"] self.hidden_ops = self.proxy.config["hidden-ops"] self.silent_bans = self.proxy.config["silent-ipban"] # client setup and operating paramenters self.username = "******" self.packet = Packet(self.client_socket, self) self.verifyToken = encryption.generate_challenge_token() self.serverID = encryption.generate_server_id().encode('utf-8') self.MOTD = {} # client will reset this later, if need be.. self.clientversion = self.servervitals.protocolVersion # default server port (to this wrapper's server) self.serverport = self.servervitals.server_port self.onlinemode = self.proxy.config["online-mode"] # packet stuff self.pktSB = mcpackets_sb.Packets(self.clientversion) self.pktCB = mcpackets_cb.Packets(self.clientversion) self.parse_sb = ParseSB(self, self.packet) # dictionary of parser packet constants and associated parsing methods self.parsers = {} self._getclientpacketset() self.buildmode = False # keep alive data self.time_server_pinged = 0 self.time_client_responded = 0 self.keepalive_val = 0 # client and server status self.abort = False # Proxy ServerConnection() (not the javaserver) self.server_connection = None self.state = HANDSHAKE # UUIDs - all should use MCUUID unless otherwise specified # -------------------------------------------------------- # Server UUID - which is the local offline UUID. self.serveruuid = None # -------------------------------------------------------- # The client UUID authenticated by connection to session server. self.uuid = None # -------------------------------------------------------- # the formal, unique, mojang UUID as looked up on mojang servers. # This ID will be the same no matter what mode wrapper is in # or whether it is a lobby, etc. This will be the formal uuid # to use for all wrapper internal functions for referencing a # unique player. # TODO - Unused except by plugin channel. # not to be confused with the fact that API player has a property # with the same name. self.mojanguuid = None # information gathered during login or socket connection processes # TODO in the future, we could use plugin channels to # communicate these to subworld wrappers From socket data self.address = None # this will store the client IP for use by player.py self.ip = self.client_address[0] # From client handshake. For vanilla clients, it is what # the user entered to connect to your wrapper. self.serveraddressplayerused = None self.serverportplayerused = None # player api Items # EID collected by serverconnection (changes on each server) self.server_eid = 0 self.gamemode = 0 self.dimension = 0 self.position = (0, 0, 0) # X, Y, Z self.head = (0, 0) # Yaw, Pitch self.inventory = {} self.slot = 0 self.riding = None # last placement (for use in cases of bucket use) self.lastplacecoords = (0, 0, 0) self.properties = {} self.clientSettings = False self.clientSettingsSent = False self.skinBlob = {} self.windowCounter = 2 self.currentwindowid = -1 self.noninventoryslotcount = 0 self.lastitem = None # wrapper's own channel on each player client self.shared = { "username": "", "uuid": "", "ip": "", "received": False, "sent": False } def handle(self): t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start() while not self.abort: try: pkid, original = self.packet.grabpacket() except EOFError: # This is not really an error.. It means the client # is not sending packet stream anymore if self.username != "PING REQUEST": self.log.debug("%s Client Packet stream ended [EOF]", self.username) self.abort = True break except socket_error: # occurs anytime a socket is closed. if self.username != "PING REQUEST": self.log.debug("%s Client Proxy Failed to grab packet", self.username) self.abort = True break except Exception as e: # anything that gets here is a bona-fide error # we need to become aware of self.log.error( "%s Client Exception: Failed to grab packet " "\n%s", self.username, e) self.abort = True break # self.parse(pkid) # send packet if server available and parsing passed. # already tested - Python will not attempt eval of # self.server_connection.state if self.server_connection is False if self.parse(pkid) and self.server_connection and \ self.server_connection.state in (PLAY, LOBBY): # sending to the server only happens in # PLAY/LOBBY (not IDLE, HANDSHAKE, or LOGIN) # wrapper handles LOGIN/HANDSHAKE with servers (via # self.parse(pkid), which DOES happen in all modes) self.server_connection.packet.send_raw(original) # sometimes (like during a ping request), a client may never enter PLAY # mode and will never be assigned a server connection... if self.server_connection: self.close_server_instance( "Client Handle Ended") # upon self.abort def flush_loop(self): while not self.abort: try: self.packet.flush() except socket_error: self.log.debug("%s client socket closed (socket_error).", self.username) break time.sleep(0.01) if self.username != "PING REQUEST": self.log.debug("%s clientconnection flush_loop thread ended", self.username) self.proxy.removestaleclients( ) # from this instance from proxy.srv_data.clients def change_servers(self, ip=None, port=None): # close current connection and start new one was_lobby = False if self.state == LOBBY: was_lobby = True # get out of PLAY "NOW" to prevent second disconnect that kills client self.state = IDLE # keep server from sending disconnects self.server_connection.state = IDLE self.close_server_instance("Lobbification") # lobby_return = True time.sleep(1) # setup for connect self.clientSettingsSent = False # connect to server self.state = PLAY self.connect_to_server(ip, port) # if the client was in LOBBY state (connected to remote server) if was_lobby: self.log.info( "%s's client Returned from remote server:" " (UUID: %s | IP: %s | SecureConnection: %s)", self.username, self.uuid.string, self.ip, self.onlinemode) self._add_client() # TODO whatever respawn stuff works # send these right quick to client self._send_client_settings() self.packet.sendpkt(self.pktCB.CHANGE_GAME_STATE, [UBYTE, FLOAT], (1, 0)) self.packet.sendpkt(self.pktCB.RESPAWN, [INT, UBYTE, UBYTE, STRING], [-1, 3, 0, 'default']) if self.version < PROTOCOL_1_8START: self.server_connection.packet.sendpkt(self.pktSB.CLIENT_STATUS, [ BYTE, ], (0, )) else: self.packet.sendpkt(0x2c, [ VARINT, INT, STRING, ], (self.server_eid, "DURNIT")) # self.packet.sendpkt(0x3e, # [FLOAT, VARINT, FLOAT], # (-1, 0, 0.0)) self.server_connection.packet.sendpkt(self.pktSB.CLIENT_STATUS, [ VARINT, ], (0, )) self.server_connection.packet.sendpkt(self.pktSB.PLAYER, [ BOOL, ], (True, )) self.state = LOBBY def logon_client_into_proxy(self): """ When the client first logs in to the wrapper proxy """ # check for uuid ban if self.proxy.isuuidbanned(self.uuid.__str__()): banreason = self.proxy.getuuidbanreason(self.uuid.__str__()) self.log.info("Banned player %s tried to" " connect:\n %s" % (self.username, banreason)) self.state = HANDSHAKE self.disconnect("Banned: %s" % banreason) return # Run the pre-login event if not self.proxy.eventhandler.callevent( "player.preLogin", { "playername": self.username, "player": self.username, # not a real player object! "online_uuid": self.uuid.string, "offline_uuid": self.serveruuid.string, "ip": self.ip, "secure_connection": self.onlinemode }): """ eventdoc <group> Proxy <group> <description> Called before client logs on. <description> <abortable> Yes, return False to disconnect the client. <abortable> <comments> - If aborted, the client is disconnnected with message "Login denied by a Plugin." - Event occurs after proxy ban code runs right after a successful handshake with Proxy. <comments> <payload> "playername": self.username, "player": username (name only - player object does not yet exist) "online_uuid": online UUID, "offline_uuid": UUID on local server (offline), "ip": the user/client IP on the internet. "secure_connection": Proxy's online mode <payload> """ self.state = HANDSHAKE self.disconnect("Login denied by a Plugin.") return self.log.info( "%s's Proxy Client LOGON occurred: (UUID: %s" " | IP: %s | SecureConnection: %s)", self.username, self.uuid.string, self.ip, self.onlinemode) self._inittheplayer() # set up inventory and stuff self._add_client() # start keep alives # send login success to client self.packet.sendpkt(self.pktCB.LOGIN_SUCCESS, [STRING, STRING], (self.uuid.string, self.username)) self.time_client_responded = time.time() t_keepalives = threading.Thread(target=self._keep_alive_tracker, args=()) t_keepalives.daemon = True t_keepalives.start() def connect_to_server(self, ip=None, port=None): """ Connects the client to a server. Creates a new server instance and tries to connect to it. Leave ip and port blank to connect to the local wrapped javaserver instance. it is the responsibility of the calling method to shutdown any existing server connection first. It is also the caller's responsibility to track LOBBY modes and handle respawns, rain, etc. """ self.server_connection = ServerConnection(self, ip, port) # connect the socket and start its flush_loop try: self.server_connection.connect() except Exception as e: self.disconnect("Proxy client could not connect to the server" " (%s)" % e) return # start server handle() to read the packets t = threading.Thread(target=self.server_connection.handle, args=()) t.daemon = True t.start() # switch server_connection to LOGIN to log in to (offline) server. self.server_connection.state = LOGIN # now we send it a handshake to request the server go to login mode server_addr = "localhost" if self.spigot_mode: server_addr = "localhost\x00%s\x00%s" % \ (self.client_address[0], self.uuid.hex) if self.proxy.forge: server_addr = "localhost\x00FML\x00" self.server_connection.packet.sendpkt( self.server_connection.pktSB.HANDSHAKE, [VARINT, STRING, USHORT, VARINT], (self.clientversion, server_addr, self.serverport, LOGIN)) # send the login request (server is offline, so it will # accept immediately by sending login_success) self.server_connection.packet.sendpkt( self.server_connection.pktSB.LOGIN_START, [STRING], [self.username]) # LOBBY code and such to go elsewhere def close_server_instance(self, term_message): """ Close the server connection gracefully if possible. """ if self.server_connection: self.server_connection.close_server(term_message) def disconnect(self, message): """ disconnects the client (runs close_server(), which will also shut off the serverconnection.py) Not used to disconnect from a server! This disconnects the client. """ jsonmessage = message # server packets are read as json if type(message) is dict: if "text" in message: jsonmessage = {"text": message} if "color" in message: jsonmessage["color"] = message["color"] if "bold" in message: jsonmessage["bold"] = message["bold"] message = jsonmessage["text"] jsonmessage = json.dumps(jsonmessage) else: jsonmessage = message # server packets are read as json if self.state in (PLAY, LOBBY): self.packet.sendpkt(self.pktCB.DISCONNECT, [JSON], [jsonmessage]) self.log.debug("Sent PLAY state DISCONNECT packet to %s", self.username) else: self.packet.sendpkt(self.pktCB.LOGIN_DISCONNECT, [JSON], [message]) if self.username != "PING REQUEST": self.log.debug( "State was 'other': sent LOGIN_DISCONNECT to %s", self.username) time.sleep(1) self.state = HANDSHAKE self.close_server_instance( "run Disconnect() client. Aborting client thread") self.abort = True # internal init and properties # ----------------------------- @property def version(self): return self.clientversion def _inittheplayer(self): # so few items and so infrequently run that fussing with # xrange/range PY2 difference is not needed. # there are 46 items 0-45 in 1.9 (shield) versus # 45 (0-44) in 1.8 and below. for i in self.proxy.inv_slots: self.inventory[i] = None self.time_server_pinged = time.time() self.time_client_responded = time.time() def _getclientpacketset(self): # Determine packet types - in this context, pktSB/pktCB is # what is being received/sent from/to the client. # That is why we refresh to the clientversion. self.pktSB = mcpackets_sb.Packets(self.clientversion) self.pktCB = mcpackets_cb.Packets(self.clientversion) self._define_parsers() # api related # ----------------------------- def getplayerobject(self): if self.username in self.servervitals.players: return self.servervitals.players[self.username] self.log.error( "In playerlist:\n%s\nI could not locate player: %s\n" "This resulted in setting the player object to FALSE!", self.servervitals.players, self.username) return False def editsign(self, position, line1, line2, line3, line4, pre18=False): if pre18: x = position[0] y = position[1] z = position[2] self.server_connection.packet.sendpkt( self.pktSB.PLAYER_UPDATE_SIGN, [INT, SHORT, INT, STRING, STRING, STRING, STRING], (x, y, z, line1, line2, line3, line4)) else: self.server_connection.packet.sendpkt( self.pktSB.PLAYER_UPDATE_SIGN, [POSITION, STRING, STRING, STRING, STRING], (position, line1, line2, line3, line4)) def chat_to_server(self, message, position=0): """ used to resend modified chat packets. Also to mimic player in API player for say() and execute() methods """ if self.version < PROTOCOL_1_11: if len(message) > 100: self.log.error( "chat to server exceeded 100 characters " "(%s probably got kicked)", self.username) if len(message) > 256: self.log.error( "chat to server exceeded 256 characters " "(%s probably got kicked)", self.username) self.server_connection.packet.sendpkt(self.pktSB.CHAT_MESSAGE[PKT], self.pktSB.CHAT_MESSAGE[PARSER], (message, position)) def chat_to_client(self, message, position=0): """ used by player API to player.message(). sendpacket for chat knows how to process either a chat dictionary or a string message! don't try sending a json.dumps string... it will simply be sent as a chat string inside a chat.message translate item... """ self.packet.sendpkt(self.pktCB.CHAT_MESSAGE[PKT], self.pktCB.CHAT_MESSAGE[PARSER], (message, position)) # internal client login methods # ----------------------------- def _keep_alive_tracker(self): """ Send keep alives to client and send client settings to server. """ while not self.abort: time.sleep(.1) if self.state in (PLAY, LOBBY): # client expects < 20sec # sending more frequently (5 seconds) seems to help with # some slower connections. if time.time() - self.time_server_pinged > 5: # create the keep alive value # MC 1.12 .2 uses a time() value. # Old way takes almost full second to generate: if self.version < PROTOCOL_1_12_2: self.keepalive_val = random.randrange(0, 99999) else: self.keepalive_val = int( (time.time() * 100) % 10000000) # challenge the client with it self.packet.sendpkt(self.pktCB.KEEP_ALIVE[PKT], self.pktCB.KEEP_ALIVE[PARSER], [self.keepalive_val]) self.time_server_pinged = time.time() # check for active client keep alive status: # server can allow up to 30 seconds for response if time.time() - self.time_client_responded > 25: # \ # and not self.abort: self.disconnect("Client closed due to lack of" " keepalive response") self.log.debug( "Closed %s's client thread due to " "lack of keepalive response", self.username) return self.log.debug("%s Client keepalive tracker aborted", self.username) def _login_authenticate_client(self, server_id): if self.onlinemode: r = requests.get("https://sessionserver.mojang.com" "/session/minecraft/hasJoined?username=%s" "&serverId=%s" % (self.username, server_id)) if r.status_code == 200: requestdata = r.json() self.uuid = MCUUID(requestdata["id"]) # TODO if requestdata["name"] != self.username: self.disconnect("Client's username did not" " match Mojang's record") self.log.info( "Client's username did not" " match Mojang's record %s != %s", requestdata["name"], self.username) return False for prop in requestdata["properties"]: if prop["name"] == "textures": self.skinBlob = prop["value"] self.proxy.skins[self.uuid.string] = self.skinBlob self.properties = requestdata["properties"] else: self.disconnect("Proxy Client Session Error" " (HTTP Status Code %d)" % r.status_code) return False currentname = self.proxy.uuids.getusernamebyuuid(self.uuid.string) if currentname: if currentname != self.username: self.log.info( "%s's client performed LOGON in with" " new name, falling back to %s", self.username, currentname) self.username = currentname self.serveruuid = self.proxy.uuids.getuuidfromname(self.username) # Wrapper offline and not authenticating # maybe it is the destination of a hub? or you use another # way to authenticate (passwords?) else: # I'll take your word for it, bub... You are: self.serveruuid = self.proxy.uuids.getuuidfromname(self.username) self.uuid = self.serveruuid self.log.debug("Client logon with wrapper offline-" " 'self.uuid = OfflinePlayer:<name>'") # no idea what is special about version 26 if self.clientversion > 26: self.packet.setcompression(256) def _add_client(self): # Put XXXplayer_object_andXXX client into server data. (player login # will be called later by mcserver.py) if self not in self.proxy.srv_data.clients: self.proxy.srv_data.clients.append(self) def _send_client_settings(self): if self.clientSettings and not self.clientSettingsSent: self.server_connection.packet.sendpkt(self.pktSB.CLIENT_SETTINGS, [ RAW, ], (self.clientSettings, )) self.clientSettingsSent = True def _send_forge_client_handshakereset(self): """ Sends a forge plugin channel packet to causes the client to recomplete its entire handshake from the start. from 'http://wiki.vg/Minecraft_Forge_Handshake': The normal forge server does not ever use this packet, but it is used when connecting through a BungeeCord instance, specifically when transitioning from a vanilla server to a modded one or from a modded server to another modded server. """ channel = "FML|HS" if self.clientversion < PROTOCOL_1_8START: self.packet.sendpkt(self.pktCB.PLUGIN_MESSAGE, [STRING, SHORT, BYTE], [channel, 1, 254]) else: self.packet.sendpkt(self.pktCB.PLUGIN_MESSAGE, [STRING, BYTE], [channel, 254]) def _transmit_downstream(self): """ transmit wrapper channel status info to the server's direction to help sync hub/lobby wrappers """ channel = "WRAPPER.PY|PING" state = self.state if self.server_connection: if self.version < PROTOCOL_1_8START: self.server_connection.packet.sendpkt( self.pktSB.PLUGIN_MESSAGE, [STRING, SHORT, BYTE], [channel, 1, state]) else: self.server_connection.packet.sendpkt( self.pktSB.PLUGIN_MESSAGE, [STRING, BOOL], [channel, state]) def _whitelist_processing(self): pass # This needs re-worked. should likely be in main wrapper of server # instance, not at each client connection # Rename UUIDs accordingly # if self.config["Proxy"]["convert-player-files"]: # if self.config["Proxy"]["online-mode"]: # # Check player files, and rename them accordingly # # to offline-mode UUID # worldname = self.servervitals.worldname # if not os.path.exists("%s/playerdata/%s.dat" % ( # worldname, self.serveruuid.string)): # if os.path.exists("%s/playerdata/%s.dat" % ( # worldname, self.uuid.string)): # self.log.info("Migrating %s's playerdata" # " file to proxy mode", self.username) # shutil.move("%s/playerdata/%s.dat" % # (worldname, self.uuid.string), # "%s/playerdata/%s.dat" % ( # worldname, self.serveruuid.string)) # with open("%s/.wrapper-proxy-playerdata-migrate" % # worldname, "a") as f: # f.write("%s %s\n" % (self.uuid.string, # self.serveruuid.string)) # # Change whitelist entries to offline mode versions # if os.path.exists("whitelist.json"): # with open("whitelist.json", "r") as f: # jsonwhitelistdata = json.loads(f.read()) # if jsonwhitelistdata: # for player in jsonwhitelistdata: # if not player["uuid"] == self.serveruuid.string\ # and player["uuid"] == self.uuid.string: # self.log.info("Migrating %s's whitelist entry" # " to proxy mode", self.username) # jsonwhitelistdata.append( # {"uuid": self.serveruuid.string, # "name": self.username}) # with open("whitelist.json", "w") as f: # f.write(json.dumps(jsonwhitelistdata)) # ##self.XXXservervitalsXXX.console( # "##whitelist reload") # => self.proxy.eventhandler.callevent("proxy.console", {"command": "whitelist reload"}) """ eventdoc <description> internalfunction <description> """ # with open("%s/.wrapper-proxy-whitelist-" # "migrate" % worldname, "a") as f: # f.write("%s %s\n" % ( # self.uuid.string, # self.serveruuid.string)) # PARSERS SECTION # ----------------------------- def _parse_keep_alive(self): data = self.packet.readpkt(self.pktSB.KEEP_ALIVE[PARSER]) if data[0] == self.keepalive_val: self.time_client_responded = time.time() return False # plugin channel handler # ----------------------- def _parse_plugin_message(self): channel = self.packet.readpkt([ STRING, ])[0] if channel not in self.proxy.registered_channels: # we are not actually registering our channels with the MC server. return True if channel == "WRAPPER.PY|PING": self.proxy.pinged = True return False if channel == "WRAPPER.PY|": if self.clientversion < PROTOCOL_1_8START: datarest = self.packet.readpkt([SHORT, REST])[1] else: datarest = self.packet.readpkt([ REST, ])[0] print("\nDATA REST = %s\n" % datarest) response = json.loads(datarest.decode('utf-8'), encoding='utf-8') self._plugin_response(response) return True return True def _plugin_response(self, response): if "ip" in response: self.shared = { "username": response["username"], "uuid": response["uuid"], "ip": response["ip"], "received": True, } self.ip = response["ip"] self.mojanguuid = response["uuid"] # Login parsers # ----------------------- def _parse_handshaking(self): # self.log.debug("HANDSHAKE") # "version|address|port|state" data = self.packet.readpkt([VARINT, STRING, USHORT, VARINT]) self.clientversion = data[0] self._getclientpacketset() self.serveraddressplayerused = data[1] self.serverportplayerused = data[2] requestedstate = data[3] if requestedstate == STATUS: self.state = STATUS # wrapper will handle responses, so do not pass this to the server. return False if requestedstate == LOGIN: # TODO - coming soon: allow client connections # despite lack of server connection if self.servervitals.protocolVersion == -1: # ... returns -1 to signal no server self.disconnect("The server is not started.") return False if not self.servervitals.state == 2: self.disconnect("Server has not finished booting. Please try" " connecting again in a few seconds") return False if PROTOCOL_1_9START < self.clientversion < PROTOCOL_1_9REL1: self.disconnect("You're running an unsupported snapshot" " (protocol: %s)!" % self.clientversion) return False if self.servervitals.protocolVersion == self.clientversion: # login start... self.state = LOGIN # packet passes to server, which will also switch to Login return True else: self.disconnect("You're not running the same Minecraft" " version as the server!") return False self.disconnect("Invalid HANDSHAKE: 'requested state:" " %d'" % requestedstate) return False def _parse_status_ping(self): # self.log.debug("SB -> STATUS PING") data = self.packet.readpkt([LONG]) self.packet.sendpkt(self.pktCB.PING_PONG, [LONG], [data[0]]) # self.log.debug("CB (W)-> STATUS PING") self.state = HANDSHAKE return False def _parse_status_request(self): # self.log.debug("SB -> STATUS REQUEST") sample = [] for player in self.servervitals.players: playerobj = self.servervitals.players[player] if playerobj.username not in self.hidden_ops: sample.append({ "name": playerobj.username, "id": str(playerobj.mojangUuid) }) if len(sample) > 5: break reported_version = self.servervitals.protocolVersion reported_name = self.servervitals.version motdtext = self.servervitals.motd if self.clientversion >= PROTOCOL_1_8START: motdtext = json.loads(processcolorcodes(motdtext.replace("\\", ""))) self.MOTD = { "description": motdtext, "players": { "max": int(self.servervitals.maxPlayers), "online": len(self.servervitals.players), "sample": sample }, "version": { "name": reported_name, "protocol": reported_version } } # add Favicon, if it exists if self.servervitals.serverIcon: self.MOTD["favicon"] = self.servervitals.serverIcon # add Forge information, if applicable. if self.proxy.forge: self.MOTD["modinfo"] = self.proxy.mod_info["modinfo"] self.packet.sendpkt(self.pktCB.PING_JSON_RESPONSE, [STRING], [json.dumps(self.MOTD)]) # self.log.debug("CB (W)-> JSON RESPONSE") # after this, proxy waits for the expected PING to # go back to Handshake mode return False def _parse_login_start(self): # self.log.debug("SB -> LOGIN START") data = self.packet.readpkt([STRING, NULL]) # "username" self.username = data[0] # just to be clear, this refers to wrapper's mode, not the server. if self.onlinemode: # Wrapper sends client a login encryption request # 1.7.x versions if self.servervitals.protocolVersion < 6: # send to client 1.7 self.packet.sendpkt( self.pktCB.LOGIN_ENCR_REQUEST, [STRING, BYTEARRAY_SHORT, BYTEARRAY_SHORT], (self.serverID, self.publicKey, self.verifyToken)) else: # send to client 1.8 + self.packet.sendpkt( self.pktCB.LOGIN_ENCR_REQUEST, [STRING, BYTEARRAY, BYTEARRAY], (self.serverID, self.publicKey, self.verifyToken)) # self.log.debug("CB (W)-> LOGIN ENCR REQUEST") # Server UUID is always offline (at the present time) # MCUUID object self.serveruuid = self.proxy.uuids.getuuidfromname(self.username) else: # Wrapper offline and not authenticating # maybe it is the destination of a hub? or you use another # way to authenticate (password plugin?) # Server UUID is always offline (at the present time) self.uuid = self.proxy.uuids.getuuidfromname(self.username) # Since wrapper is offline, we are using offline for self.uuid also self.serveruuid = self.uuid # MCUUID object # log the client on self.state = PLAY self.logon_client_into_proxy() # connect to server self.connect_to_server() return False def _parse_login_encr_response(self): # the client is RESPONDING to our request for # encryption (if we sent one above) # read response Tokens - "shared_secret|verify_token" # self.log.debug("SB -> LOGIN ENCR RESPONSE") if self.servervitals.protocolVersion < 6: data = self.packet.readpkt([BYTEARRAY_SHORT, BYTEARRAY_SHORT]) else: data = self.packet.readpkt([BYTEARRAY, BYTEARRAY]) sharedsecret = encryption.decrypt_shared_secret( data[0], self.privateKey) verifytoken = encryption.decrypt_shared_secret(data[1], self.privateKey) h = hashlib.sha1() # self.serverID already encoded h.update(self.serverID) h.update(sharedsecret) h.update(self.publicKey) serverid = self.packet.hexdigest(h) # feed info to packet.py for parsing self.packet.sendCipher = encryption.AES128CFB8(sharedsecret) self.packet.recvCipher = encryption.AES128CFB8(sharedsecret) # verify correct response if not verifytoken == self.verifyToken: self.disconnect("Verify tokens are not the same") return False # determine if IP is silent banned: if self.ipbanned: self.log.info("Player %s tried to connect from banned ip:" " %s", self.username, self.ip) self.state = HANDSHAKE if self.silent_bans: self.disconnect("unknown host") else: # self disconnect does not "return" anything. self.disconnect("Your address is IP-banned from this server!.") return False # begin Client logon process # Wrapper in online mode, taking care of authentication if self._login_authenticate_client(serverid) is False: return False # client failed to authenticate # TODO Whitelist processing Here (or should it be at javaserver start?) # log the client on self.state = PLAY self.logon_client_into_proxy() # connect to server self.connect_to_server() return False # Lobby parsers # ----------------------- def _parse_lobby_chat_message(self): data = self.packet.readpkt([STRING]) if data is None: return True # Get the packet chat message contents chatmsg = data[0] if chatmsg in ("/lobby", "/hub"): # stop any raining # close current connection and start new one # noinspection PyBroadException self.change_servers() return False # we are just sniffing this packet for lobby return # commands, so send it on to the destination. return True def parse(self, pkid): if pkid in self.parsers[self.state]: return self.parsers[self.state][pkid]() else: return True def _define_parsers(self): # the packets we parse and the methods that parse them. self.parsers = { HANDSHAKE: { self.pktSB.HANDSHAKE: self._parse_handshaking, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, STATUS: { self.pktSB.STATUS_PING: self._parse_status_ping, self.pktSB.REQUEST: self._parse_status_request, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, LOGIN: { self.pktSB.LOGIN_START: self._parse_login_start, self.pktSB.LOGIN_ENCR_RESPONSE: self._parse_login_encr_response, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, PLAY: { self.pktSB.CHAT_MESSAGE[PKT]: self.parse_sb.parse_play_chat_message, self.pktSB.CLICK_WINDOW: self.parse_sb.parse_play_click_window, self.pktSB.CLIENT_SETTINGS: self.parse_sb.parse_play_client_settings, self.pktSB.HELD_ITEM_CHANGE: self.parse_sb.parse_play_held_item_change, self.pktSB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktSB.PLAYER_BLOCK_PLACEMENT: self.parse_sb.parse_play_player_block_placement, self.pktSB.PLAYER_DIGGING: self.parse_sb.parse_play_player_digging, self.pktSB.PLAYER_LOOK: self.parse_sb.parse_play_player_look, self.pktSB.PLAYER_POSITION: self.parse_sb.parse_play_player_position, self.pktSB.PLAYER_POSLOOK[PKT]: self.parse_sb.parse_play_player_poslook, self.pktSB.PLAYER_UPDATE_SIGN: self.parse_sb.parse_play_player_update_sign, self.pktSB.SPECTATE: self.parse_sb.parse_play_spectate, self.pktSB.USE_ITEM: self.parse_sb.parse_play_use_item, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, LOBBY: { self.pktSB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktSB.CHAT_MESSAGE[PKT]: self._parse_lobby_chat_message, self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, }, IDLE: { self.pktSB.PLUGIN_MESSAGE: self._parse_plugin_message, } }
class ServerConnection(object): def __init__(self, client, ip=None, port=None): """ This class ServerConnection is a "fake" client connecting to the server. It receives "CLIENT BOUND" packets from server, parses them, and sends them on to the client. ServerConnection receives the parent client as it's argument. It receives the proxy instance from the Client. Therefore, a server instance does not really validly exist unless it has a valid parent client. Client, by contrast, can exist and run in the absence of a server. """ # basic __init__ items from passed arguments self.client = client self.username = self.client.username self.proxy = client.proxy self.wrapper = self.proxy.wrapper self.javaserver = self.wrapper.javaserver self.log = client.log self.ip = ip self.port = port # server setup and operating paramenters self.abort = False self.flush_rate = self.client.flush_rate self.state = HANDSHAKE self.packet = None self.parse_cb = None # dictionary of parser packet constants and associated parsing methods self.parsers = {} self.entity_controls = self.proxy.ent_config["enable-entity-controls"] self.version = -1 # self parsers get updated here self._refresh_server_version() # temporary assignment. The actual socket is assigned later. self.server_socket = socket.socket() self.infos_debug = "(player=%s, IP=%s, Port=%s)" % ( self.username, self.ip, self.port) def _refresh_server_version(self): """Get serverversion for mcpackets use""" self.version = self.proxy.javaserver.protocolVersion self.pktSB = mcpackets_sb.Packets(self.version) self.pktCB = mcpackets_cb.Packets(self.version) self.parse_cb = ParseCB(self, self.packet) self._define_parsers() # ---------------------------------------------- # Client calls connect(), then handle() # ---------------------------------------------- def connect(self): """ This simply establishes the tcp socket connection and starts the flush loop, NOTHING MORE. """ self.state = LOGIN # Connect to a local server address if self.ip is None: self.server_socket.connect(( "localhost", self.proxy.javaserver.server_port)) # Connect to some specific server address else: self.server_socket.connect((self.ip, self.port)) # start packet handler self.packet = Packet(self.server_socket, self) self.packet.version = self.client.clientversion # define parsers self.parse_cb = ParseCB(self, self.packet) self._define_parsers() t = threading.Thread(target=self.flush_loop, args=()) t.daemon = True t.start() def flush_loop(self): rate = self.flush_rate while not self.abort: time.sleep(rate) try: self.packet.flush() except AttributeError: self.log.debug( "%s server packet instance gone.", self.username ) except socket.error: self.log.debug("Socket_error- server socket was closed" " %s", self.infos_debug) break self.log.debug("%s serverconnection flush_loop thread ended.", self.username) def handle(self): while not (self.abort or self.client.abort): # get packet try: pkid, orig_packet = self.packet.grabpacket() # noqa # possible connection losses: except EOFError: # This is not a true error, but means the connection closed. return self.close_server("handle EOF") except socket.error: return self.close_server("handle socket.error") except Exception as e: return self.close_server( "handle Exception: %s TRACEBACK: \n%s" % ( e, traceback.format_exc()) ) # parse it # send packet if parsing passed and client in play mode. # all packets are parsed, but only play mode ones are transmitted. if self.parse(pkid) and self.client.state == PLAY: try: # self.parse will reject (False) any packet proxy modifies. self.client.packet.send_raw_untouched(orig_packet) except Exception as e: return self.close_server( "handle() could not send packet '%s'. " "Exception: %s TRACEBACK: \n%s" % ( pkid, e, traceback.format_exc()) ) return self.close_server("handle() received abort signal.") def close_server(self, reason="Disconnected"): """ Client is responsible for closing the server connection and handling lobby states. """ # if close_server already ran, server_socket will be None. if not self.server_socket: return False self.log.info("%s's proxy server connection closed: %s", self.username, reason) # end 'handle' and 'flush_loop' cleanly self.abort = True # time.sleep(0.1) # noinspection PyBroadException try: self.server_socket.shutdown(2) self.server_socket.close() self.log.debug("Sucessfully closed server socket for" " %s", self.username) # allow old packet and socket to be Garbage Collected condition = True except: condition = False # allow old packet and socket to be Garbage Collected self.packet = None self.server_socket = None return condition # PARSERS SECTION # ----------------------------- def _parse_keep_alive(self): data = self.packet.readpkt( self.pktSB.KEEP_ALIVE[PARSER]) # data is a list of one item and will be sent back that way self.packet.sendpkt( self.pktSB.KEEP_ALIVE[PKT], self.pktSB.KEEP_ALIVE[PARSER], data) return False # Plugin channel senders # ----------------------- # SB RESP def plugin_response(self): channel = "WRAPPER.PY|RESP" self.client.info["server-is-wrapper"] = True data = json.dumps(self.client.info) # only our wrappers communicate with this, so, format is not critical. self.packet.sendpkt(self.pktSB.PLUGIN_MESSAGE[PKT], [STRING, STRING], (channel, data)) # SB PING def plugin_ping(self): """ this is initiated by server/parse_cb.py parse_play_join_game """ channel = "WRAPPER.PY|PING" data = int(time.time()) self.packet.sendpkt(self.pktSB.PLUGIN_MESSAGE[PKT], [STRING, INT], (channel, data)) def _parse_plugin_message(self): """server-bound""" channel = self.packet.readpkt([STRING, ])[0] if channel not in self.proxy.registered_channels: # we are not actually registering our channels with the MC server # and there will be no parsing of other channels. return True # SB PING if channel == "WRAPPER.PY|PONG": # then we now know this wrapper is a child wrapper since # minecraft clients will not ping us self.client.info["client-is-wrapper"] = True self.plugin_response() # do not pass Wrapper.py registered plugin messages return False # Plugin channel parsers # ----------------------- # Login parsers # ----------------------- def _parse_login_disconnect(self): message = self.packet.readpkt([STRING])[0] self.log.info("Disconnected from server: %s", message) self.client.notify_disconnect(message) self.close_server(message) return False def _parse_login_encr_request(self): self.close_server("Server is in online mode. Please turn it off " "in server.properties and allow Proxy to " "handle the authentication.") return False # Login Success - UUID & Username are sent in this packet as strings # no point in parsing because we already know the UUID and Username def _parse_login_success(self): self.state = PLAY return False def _parse_login_set_compression(self): data = self.packet.readpkt([VARINT])[0] # ("varint:threshold") if data == -1: self.packet.compression = False else: self.packet.compression = True self.packet.compressThreshold = data # no point - client connection already has the client waiting in # compression enabled mode return False def parse(self, pkid): if pkid in self.parsers[self.state]: return self.parsers[self.state][pkid]() return True def _define_parsers(self): # the packets we parse and the methods that parse them. self.parsers = { HANDSHAKE: {}, # maps identically to OFFLINE ( '0' ) LOGIN: { self.pktCB.LOGIN_DISCONNECT[PKT]: self._parse_login_disconnect, self.pktCB.LOGIN_ENCR_REQUEST[PKT]: self._parse_login_encr_request, self.pktCB.LOGIN_SUCCESS[PKT]: self._parse_login_success, self.pktCB.LOGIN_SET_COMPRESSION[PKT]: self._parse_login_set_compression, self.pktCB.PLUGIN_MESSAGE[PKT]: self._parse_plugin_message }, PLAY: { # required base items self.pktCB.KEEP_ALIVE[PKT]: self._parse_keep_alive, self.pktCB.CHAT_MESSAGE[PKT]: self.parse_cb.play_chat_message, self.pktCB.PLUGIN_MESSAGE[PKT]: self._parse_plugin_message, self.pktCB.DISCONNECT[PKT]: self.parse_cb.play_disconnect, # required for proper player list display self.pktCB.PLAYER_LIST_ITEM[PKT]: self.parse_cb.play_player_list_item, self.pktCB.SPAWN_PLAYER[PKT]: self.parse_cb.play_spawn_player, # features self.pktCB.USE_BED[PKT]: self.parse_cb.play_use_bed, self.pktCB.TIME_UPDATE[PKT]: self.parse_cb.play_time_update, self.pktCB.TAB_COMPLETE[PKT]: self.parse_cb.play_tab_complete, self.pktCB.SPAWN_POSITION[PKT]: self.parse_cb.play_spawn_position, # Monitor player states self.pktCB.CHANGE_GAME_STATE[PKT]: self.parse_cb.play_change_game_state, self.pktCB.PLAYER_POSLOOK[PKT]: self.parse_cb.play_player_poslook, self.pktCB.JOIN_GAME[PKT]: self.parse_cb.play_join_game, # hub information self.pktCB.RESPAWN[PKT]: self.parse_cb.play_respawn, self.pktCB.UPDATE_HEALTH[PKT]: self.parse_cb.update_health, self.pktCB.CHUNK_DATA[PKT]: self.parse_cb.play_chunk_data, # inventory management self.pktCB.OPEN_WINDOW[PKT]: self.parse_cb.play_open_window, self.pktCB.WINDOW_ITEMS[PKT]: self.parse_cb.play_window_items, self.pktCB.HELD_ITEM_CHANGE[PKT]: self.parse_cb.play_held_item_change, self.pktCB.SET_SLOT[PKT]: self.parse_cb.play_set_slot } } if self.entity_controls: self.parsers[PLAY][ self.pktCB.SPAWN_OBJECT[PKT]] = self.parse_cb.play_spawn_object self.parsers[PLAY][ self.pktCB.SPAWN_MOB[PKT]] = self.parse_cb.play_spawn_mob self.parsers[PLAY][ self.pktCB.ENTITY_RELATIVE_MOVE[PKT]] = self.parse_cb.play_entity_relative_move # noqa self.parsers[PLAY][ self.pktCB.ENTITY_TELEPORT[PKT]] = self.parse_cb.play_entity_teleport # noqa self.parsers[PLAY][ self.pktCB.ATTACH_ENTITY[PKT]] = self.parse_cb.play_attach_entity # noqa self.parsers[PLAY][ self.pktCB.DESTROY_ENTITIES[PKT]] = self.parse_cb.play_destroy_entities # noqa