def __init__(self, detected_ip: IPv4Address, ports): super().__init__() self.logger = logging.getLogger(__name__) self.firewall = FirewallClient(ports) self.login_server = None self.server_id = None self.match_id = None self.detected_ip = detected_ip self.address_pair = None self.port = None self.pingport = None self.description = None self.motd = None self.password_hash = None self.region = None self.game_setting_mode = None self.joinable = False self.players = TracingDict(refsonly=True) self.player_kicking = None self.player_being_kicked = None self.match_end_time_rel_or_abs = None self.match_time_counting = False self.be_score = 0 self.ds_score = 0 self.map_id = 0 self.start_time = None self.votable_maps = [] self.map_votes = {} self.next_map_idx = None if self.detected_ip.is_global: req = urllib.request.Request( 'https://tools.keycdn.com/geo.json?host=%s' % self.detected_ip, data=None, headers={ 'User-Agent': 'keycdn-tools:https://github.com/Griffon26/taserver/blob/master/README.md' }) response = urllib.request.urlopen(req, cafile=certifi.where()) result = response.read() json_result = json.loads(result) continent_code_to_region = { 'NA': REGION_NORTH_AMERICA, 'EU': REGION_EUROPE, 'OC': REGION_OCEANIA_AUSTRALIA } try: self.region = continent_code_to_region[ json_result['data']['geo']['continent_code']] except KeyError: self.region = REGION_EUROPE else: self.region = REGION_EUROPE
def __init__(self, server_queue, client_queues, server_stats_queue, ports, accounts): self.logger = logging.getLogger(__name__) self.server_queue = server_queue self.client_queues = client_queues self.server_stats_queue = server_stats_queue self.game_servers = TracingDict() self.players = TracingDict() self.social_network = SocialNetwork() self.firewall = FirewallClient(ports) self.accounts = accounts self.message_handlers = { Auth2LoginAuthCodeRequestMessage: self.handle_authcode_request_message, Auth2LoginChatMessage: self.handle_auth_channel_chat_message, Auth2LoginRegisterAsBotMessage: self.handle_register_as_bot_message, Auth2LoginSetEmailMessage: self.handle_set_email_message, ExecuteCallbackMessage: self.handle_execute_callback_message, HttpRequestMessage: self.handle_http_request_message, PeerConnectedMessage: self.handle_client_connected_message, PeerDisconnectedMessage: self.handle_client_disconnected_message, LoginProtocolMessage: self.handle_client_message, Launcher2LoginProtocolVersionMessage: self.handle_launcher_protocol_version_message, Launcher2LoginAddressInfoMessage: self.handle_address_info_message, Launcher2LoginServerInfoMessage: self.handle_server_info_message, Launcher2LoginMapInfoMessage: self.handle_map_info_message, Launcher2LoginTeamInfoMessage: self.handle_team_info_message, Launcher2LoginScoreInfoMessage: self.handle_score_info_message, Launcher2LoginMatchTimeMessage: self.handle_match_time_message, Launcher2LoginServerReadyMessage: self.handle_server_ready_message, Launcher2LoginMatchEndMessage: self.handle_match_end_message, Launcher2LoginWaitingForMap: self.handle_waiting_for_map_message, } self.pending_callbacks = PendingCallbacks(server_queue) self.last_player_update_time = datetime.datetime.utcnow() self.address_pair, errormsg = IPAddressPair.detect() if not self.address_pair.external_ip: self.logger.warning('Unable to detect public IP address: %s\n' 'This will cause problems if the login server ' 'and any players are on the same LAN, but the ' 'game server is not.' % errormsg) else: self.logger.info('detected external IP: %s' % self.address_pair.external_ip) self.pending_callbacks.add(self, 0, self.remove_old_authcodes)
def __init__(self, detected_ip: IPv4Address, ports): super().__init__() self.logger = logging.getLogger(__name__) self.firewall = FirewallClient(ports) self.login_server = None self.server_id = None self.match_id = None self.detected_ip = detected_ip self.address_pair = None self.port = None self.pingport = None self.description = None self.motd = None self.password_hash = None self.region = None self.game_setting_mode = 'ootb' self.joinable = False self.players = TracingDict(refsonly=True) self.player_being_kicked = None self.match_end_time_rel_or_abs = None self.match_time_counting = False self.be_score = 0 self.ds_score = 0 self.map_id = 0 self.start_time = None if self.detected_ip.is_global: response = urllib.request.urlopen( 'http://tools.keycdn.com/geo.json?host=%s' % self.detected_ip) result = response.read() json_result = json.loads(result) continent_code_to_region = { 'NA': REGION_NORTH_AMERICA, 'EU': REGION_EUROPE, 'OC': REGION_OCEANIA_AUSTRALIA } try: self.region = continent_code_to_region[ json_result['data']['geo']['continent_code']] except KeyError: self.region = REGION_EUROPE else: self.region = REGION_EUROPE
class GameServer(Peer): def __init__(self, detected_ip: IPv4Address, ports): super().__init__() self.logger = logging.getLogger(__name__) self.firewall = FirewallClient(ports) self.login_server = None self.server_id = None self.match_id = None self.detected_ip = detected_ip self.address_pair = None self.port = None self.pingport = None self.description = None self.motd = None self.password_hash = None self.region = None self.game_setting_mode = None self.joinable = False self.players = TracingDict(refsonly=True) self.player_kicking = None self.player_being_kicked = None self.match_end_time_rel_or_abs = None self.match_time_counting = False self.be_score = 0 self.ds_score = 0 self.map_id = 0 self.start_time = None self.votable_maps = [] self.map_votes = {} self.next_map_idx = None if self.detected_ip.is_global: req = urllib.request.Request( 'https://tools.keycdn.com/geo.json?host=%s' % self.detected_ip, data=None, headers={ 'User-Agent': 'keycdn-tools:https://github.com/Griffon26/taserver/blob/master/README.md' }) response = urllib.request.urlopen(req, cafile=certifi.where()) result = response.read() json_result = json.loads(result) continent_code_to_region = { 'NA': REGION_NORTH_AMERICA, 'EU': REGION_EUROPE, 'OC': REGION_OCEANIA_AUSTRALIA } try: self.region = continent_code_to_region[ json_result['data']['geo']['continent_code']] except KeyError: self.region = REGION_EUROPE else: self.region = REGION_EUROPE def __repr__(self): return 'server %d (%s %s:%s/%s)' % ( self.server_id, self.game_setting_mode, self.detected_ip, self.port, self.pingport) def disconnect(self, exception=None): for player in list(self.players.values()): player.set_state(AuthenticatedState) super().disconnect(exception) def set_address_info(self, address_pair): self.address_pair = address_pair self.send_pings() def set_info(self, description: str, motd: str, game_setting_mode: str, password_hash: bytes): self.description = description self.motd = motd self.game_setting_mode = game_setting_mode self.password_hash = password_hash def set_match_time(self, seconds_remaining, counting): self.match_time_counting = counting if counting: self.match_end_time_rel_or_abs = int(time.time() + seconds_remaining) else: self.match_end_time_rel_or_abs = seconds_remaining def set_ready(self, port, pingport): if port is not None: self.be_score = 0 self.ds_score = 0 self.port = port self.pingport = pingport self.joinable = True self.start_time = datetime.datetime.utcnow() for unique_id, player in self.players.items(): b4msg = a00b4().set_server(self).set_player(unique_id) b4msg.findbytype(m042a).set(3) b4msg.content.append(m02ff()) player.send(b4msg) self.send(Login2LauncherNextMapMessage()) else: self.joinable = False def get_time_remaining(self): if self.match_end_time_rel_or_abs is not None: if self.match_time_counting: time_remaining = int(self.match_end_time_rel_or_abs - time.time()) else: time_remaining = self.match_end_time_rel_or_abs else: time_remaining = 0 if time_remaining < 0: time_remaining = 0 return time_remaining def add_player(self, player): assert player.unique_id not in self.players self.players[player.unique_id] = player player.vote = None player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherAddPlayer( player.unique_id, str(player_ip) if player_ip is not None else '', player.player_settings.progression.rank_xp, player.player_settings.progression.is_eligible_for_first_win()) self.send(msg) def remove_player(self, player): assert player.unique_id in self.players del self.players[player.unique_id] player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherRemovePlayer( player.unique_id, str(player_ip) if player_ip is not None else '') self.send(msg) def _send_public_message_from_server(self, text): for player in self.players.values(): msg = a0070().set([ m009e().set(MESSAGE_PUBLIC), m02e6().set(text), m034a().set(player.display_name), m0574(), m02fe().set('taserver'), m06de().set('bot') ]) player.send(msg) def send_all_players(self, data): for player in self.players.values(): player.send(data) def send_all_players_on_team(self, data, team): for player in self.players.values(): if player.team == team: player.send(data) def set_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherSetPlayerLoadoutsMessage( player.unique_id, player.get_current_loadouts().get_data()) self.send(msg) def remove_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherRemovePlayerLoadoutsMessage(player.unique_id) self.send(msg) def start_votekick(self, kicker, kickee): if kickee.unique_id in self.players and self.player_being_kicked is None: # Start a new vote reply = a018c() reply.content = [ m02c4().set(self.match_id), m034a().set(kickee.display_name), m0348().set(kickee.unique_id), m02fc().set(STDMSG_VOTE_BY_X_KICK_PLAYER_X_YES_NO), m0442().set_success(True), m0704().set(kicker.unique_id), m0705().set(kicker.display_name) ] self.send_all_players(reply) for player in self.players.values(): player.vote = None self.player_kicking = kicker self.player_being_kicked = kickee self.logger.info( '%s: votekick started by %d:"%s" against %d:"%s"' % (self, kicker.unique_id, kicker.display_name, kickee.unique_id, kickee.display_name)) self.login_server.pending_callbacks.add(self, 35, self.end_votekick) def end_votekick(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) self.logger.info( '%s: votekick started by %d:"%s" against %d:"%s" %s at timeout with %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % (self, self.player_kicking.unique_id, self.player_kicking.display_name, self.player_being_kicked.unique_id, self.player_being_kicked.display_name, 'passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def check_votes(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) # If enough people vote yes or the vote is unanimous, end the vote immediately. # Otherwise wait for the timeout. if (yes_votes >= 8 and vote_passed) or total_votes == eligible_voters: self.logger.info( '%s: votekick started by %d:"%s" against %d:"%s" %s immediately %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % (self, self.player_kicking.unique_id, self.player_kicking.display_name, self.player_being_kicked.unique_id, self.player_being_kicked.display_name, 'passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def _tally_votes(self): if self.player_being_kicked.player_settings.progression.rank_xp > LEVEL_15_XP: required_majority = 0.66 else: required_majority = 0.5 eligible_voters_votes = { p.address_pair.get_address_seen_from( self.login_server.address_pair): p.vote for p in self.players.values() } votes = [v for v in eligible_voters_votes.values() if v is not None] yes_votes = [v for v in votes if v] vote_passed = len(votes) >= 4 and (len(yes_votes) / len(votes)) > required_majority return len(eligible_voters_votes), len(votes), len( yes_votes), vote_passed def _do_kick(self, votekick_passed): player_to_kick = self.player_being_kicked reply = a018c() reply.content = [ m0348().set(player_to_kick.unique_id), m034a().set(player_to_kick.display_name) ] if votekick_passed: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_HAS_BEEN_KICKED), m0442().set_success(True) ]) else: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_WAS_NOT_VOTED_OUT), m0442().set_success(False) ]) self.send_all_players(reply) if votekick_passed: # TODO: figure out if a real votekick also causes an # inconsistency between the menu you see and the one # you're really in for msg in [a00b0(), a0035().setmainmenu(), a006f()]: player_to_kick.send(msg) player_to_kick.set_state(UnauthenticatedState) ip_to_ban_on_login_server = player_to_kick.address_pair.get_address_seen_from( self.login_server.address_pair) self.firewall.modify_firewall('blacklist', 'add', player_to_kick.unique_id, ip_to_ban_on_login_server) def remove_blacklist_rule(): self.firewall.modify_firewall('blacklist', 'remove', player_to_kick.unique_id, ip_to_ban_on_login_server) self.login_server.pending_callbacks.add(self.login_server, 8 * 3600, remove_blacklist_rule) self.player_kicking = None self.player_being_kicked = None def send_pings(self): player_pings = {} for unique_id, player in self.players.items(): player_pings[unique_id] = player.pings[ self.region] if self.region in player.pings else 999 self.send(Login2LauncherPings(player_pings)) self.login_server.pending_callbacks.add(self, PING_UPDATE_TIME, self.send_pings) def initialize_map_vote(self, next_map_idx, votable_maps): self.votable_maps = votable_maps self.map_votes = {} self.next_map_idx = next_map_idx if self.votable_maps: self.logger.info(f'{self}: initiating map vote') self._send_public_message_from_server( 'Please vote for the next map by typing its number in public chat. Only votes from verified players will count.' ) for idx, map in enumerate(votable_maps): suffix = '<-- next in rotation' if idx == next_map_idx else '' self._send_public_message_from_server( f'{idx}. {map} {suffix}') def inspect_message_for_map_vote(self, player, text): if not player.verified: return try: idx = int(text) except ValueError: return if 0 <= idx < len(self.votable_maps): self.map_votes[player.unique_id] = idx def process_map_votes(self): max_votes = 0 map_with_max_votes = self.next_map_idx if self.votable_maps: votes_per_map = Counter(self.map_votes.values()).most_common() max_voted = [ map_id for map_id, nr_of_votes in votes_per_map if nr_of_votes == votes_per_map[0][1] ] if max_voted: map_with_max_votes = random.choice(max_voted) self.logger.info( f'{self}: map with most votes was {map_with_max_votes}') self._send_public_message_from_server( f'Map vote ended. Next map will be {map_with_max_votes}.') self.send(Login2LauncherMapVoteResult(map_with_max_votes))
class LoginServer: def __init__(self, server_queue, client_queues, server_stats_queue, ports, accounts): self.logger = logging.getLogger(__name__) self.server_queue = server_queue self.client_queues = client_queues self.server_stats_queue = server_stats_queue self.game_servers = TracingDict() self.players = TracingDict() self.social_network = SocialNetwork() self.firewall = FirewallClient(ports) self.accounts = accounts self.message_handlers = { Auth2LoginAuthCodeRequestMessage: self.handle_authcode_request_message, Auth2LoginChatMessage: self.handle_auth_channel_chat_message, Auth2LoginRegisterAsBotMessage: self.handle_register_as_bot_message, Auth2LoginSetEmailMessage: self.handle_set_email_message, ExecuteCallbackMessage: self.handle_execute_callback_message, HttpRequestMessage: self.handle_http_request_message, PeerConnectedMessage: self.handle_client_connected_message, PeerDisconnectedMessage: self.handle_client_disconnected_message, LoginProtocolMessage: self.handle_client_message, Launcher2LoginProtocolVersionMessage: self.handle_launcher_protocol_version_message, Launcher2LoginAddressInfoMessage: self.handle_address_info_message, Launcher2LoginServerInfoMessage: self.handle_server_info_message, Launcher2LoginMapInfoMessage: self.handle_map_info_message, Launcher2LoginTeamInfoMessage: self.handle_team_info_message, Launcher2LoginScoreInfoMessage: self.handle_score_info_message, Launcher2LoginMatchTimeMessage: self.handle_match_time_message, Launcher2LoginServerReadyMessage: self.handle_server_ready_message, Launcher2LoginMatchEndMessage: self.handle_match_end_message, Launcher2LoginWaitingForMap: self.handle_waiting_for_map_message, } self.pending_callbacks = PendingCallbacks(server_queue) self.last_player_update_time = datetime.datetime.utcnow() self.address_pair, errormsg = IPAddressPair.detect() if not self.address_pair.external_ip: self.logger.warning('Unable to detect public IP address: %s\n' 'This will cause problems if the login server ' 'and any players are on the same LAN, but the ' 'game server is not.' % errormsg) else: self.logger.info('detected external IP: %s' % self.address_pair.external_ip) self.pending_callbacks.add(self, 0, self.remove_old_authcodes) def remove_old_authcodes(self): if self.accounts.remove_old_authcodes(): self.accounts.save() self.pending_callbacks.add(self, UNUSED_AUTHCODE_CHECK_TIME, self.remove_old_authcodes) def run(self): gevent.getcurrent().name = 'loginserver' self.logger.info('login server started') self.firewall.reset_firewall('blacklist') while True: for message in self.server_queue: handler = self.message_handlers[type(message)] try: handler(message) except Exception as e: if hasattr(message, 'peer'): self.logger.error( 'an exception occurred while handling a message; passing it on to the peer...' ) message.peer.disconnect(e) else: raise def all_game_servers(self): return self.game_servers def find_server_by_id(self, server_id): for game_server in self.all_game_servers().values(): if game_server.server_id == server_id: return game_server raise ProtocolViolationError( 'No server found with specified server ID') def find_server_by_match_id(self, match_id): for game_server in self.all_game_servers().values(): if game_server.match_id == match_id: return game_server raise ProtocolViolationError('No server found with specified match ID') def find_player_by(self, **kwargs): matching_players = self.find_players_by(**kwargs) if len(matching_players) > 1: raise ValueError("More than one player matched query") return matching_players[0] if matching_players else None def find_players_by(self, **kwargs): matching_players = self.players.values() for key, val in kwargs.items(): matching_players = [ player for player in matching_players if getattr(player, key) == val ] return matching_players def find_player_by_display_name(self, display_name): matching_players = [ p for p in self.players.values() if p.display_name is not None and p.display_name.lower() == display_name.lower() ] if matching_players: return matching_players[0] else: return None def change_player_unique_id(self, old_id, new_id): if new_id in self.players: raise AlreadyLoggedInError() assert old_id in self.players assert new_id not in self.players player = self.players.pop(old_id) player.unique_id = new_id self.players[new_id] = player def validate_username(self, username): if len(username) < Player.min_name_length: return 'User name is too short, min length is %d characters.' % Player.min_name_length if len(username) > Player.max_name_length: return 'User name is too long, max length is %d characters.' % Player.max_name_length try: ascii_bytes = username.encode('ascii') except UnicodeError: return 'User name contains invalid (i.e. non-ascii) characters' if not utils.is_valid_ascii_for_name(ascii_bytes): return 'User name contains invalid characters' if username.lower() == 'taserverbot': return 'User name is reserved' return None def send_server_stats(self): stats = [{ 'locked': gs.password_hash is not None, 'mode': gs.game_setting_mode, 'description': gs.description, 'nplayers': len(gs.players) } for gs in self.game_servers.values() if gs.joinable] self.server_stats_queue.put(stats) def email_address_to_hash(self, email_address): email_hash = hashlib.sha256(email_address.encode('utf-8')).hexdigest() return email_hash def handle_authcode_request_message(self, msg): authcode_requester = msg.peer validation_failure = self.validate_username(msg.login_name) if validation_failure: self.logger.warning( "authcode requested for invalid user name '%s': %s. Refused." % (msg.login_name, validation_failure)) authcode_requester.send('Error: %s' % validation_failure) else: availablechars = ''.join(c for c in (string.ascii_letters + string.digits) if c not in 'O0Il') authcode = ''.join( [random.choice(availablechars) for i in range(8)]) email_hash = self.email_address_to_hash(msg.email_address) if msg.login_name not in self.accounts or self.accounts[ msg.login_name].email_hash == email_hash: self.logger.info('authcode requested for %s, returned %s' % (msg.login_name, authcode)) self.accounts.update_account(msg.login_name, email_hash, authcode) self.accounts.save() authcode_requester.send( Login2AuthAuthCodeResultMessage(msg.source, msg.login_name, msg.email_address, authcode, None)) else: authcode_requester.send( Login2AuthAuthCodeResultMessage( msg.source, msg.login_name, msg.email_address, None, 'The specified email address does not match the one stored for the account' )) def handle_auth_channel_chat_message(self, msg): player = self.find_player_by(login_name=msg.login_name) msg = a0070().set([ m009e().set(MESSAGE_PRIVATE), m02e6().set(msg.text), m034a().set(player.display_name), m0574(), m02fe().set('taserverbot'), m06de().set('') ]) player.send(msg) def handle_register_as_bot_message(self, msg): bot = msg.peer.authbot self.players[utils.AUTHBOT_ID] = bot bot.friends.connect_to_social_network(self.social_network) bot.friends.notify_online() def handle_set_email_message(self, msg): self.logger.info(f'new email set for {msg.login_name}') email_hash = self.email_address_to_hash(msg.email_address) self.accounts.update_email_hash(msg.login_name, email_hash) self.accounts.save() def handle_execute_callback_message(self, msg): callback_id = msg.callback_id self.pending_callbacks.execute(callback_id) def handle_client_connected_message(self, msg): if isinstance(msg.peer, Player): unique_id = utils.first_unused_number_above( self.players.keys(), utils.MIN_UNVERIFIED_ID, utils.MAX_UNVERIFIED_ID) player = msg.peer player.friends.connect_to_social_network(self.social_network) player.unique_id = unique_id player.login_server = self player.complement_address_pair(self.address_pair) player.set_state(UnauthenticatedState) self.players[unique_id] = player elif isinstance(msg.peer, GameServer): server_id = utils.first_unused_number_above( self.all_game_servers().keys(), 1) game_server = msg.peer game_server.server_id = server_id game_server.match_id = server_id + 10000000 game_server.game_setting_mode = None game_server.login_server = self self.game_servers[server_id] = game_server self.logger.info(f'{game_server}: added') elif isinstance(msg.peer, AuthCodeRequester): pass else: assert False, "Invalid connection message received" def handle_client_disconnected_message(self, msg): if isinstance(msg.peer, Player): player = msg.peer player.disconnect() self.pending_callbacks.remove_receiver(player) player.set_state(OfflineState) del (self.players[player.unique_id]) elif isinstance(msg.peer, GameServer): game_server = msg.peer self.logger.info(f'{game_server}: removed') game_server.disconnect() self.pending_callbacks.remove_receiver(game_server) del (self.game_servers[game_server.server_id]) elif isinstance(msg.peer, AuthCodeRequester): if utils.AUTHBOT_ID in self.players and self.players[ utils.AUTHBOT_ID] == msg.peer.authbot: msg.peer.authbot.friends.notify_offline() msg.peer.disconnect() else: assert False, "Invalid disconnection message received" def handle_client_message(self, msg): current_player = msg.peer current_player.last_received_seq = msg.clientseq for request in msg.requests: if not current_player.handle_request(request): self.logger.info('%s sent: %04X' % (current_player, request.ident)) # This output is mostly for debugging of the incorrect number of players/servers online current_time = datetime.datetime.utcnow() if int((current_time - self.last_player_update_time).total_seconds()) > 15 * 60: self.logger.info( 'currently online players:\n%s' % '\n'.join([f' {p}' for p in self.players.values()])) self.logger.info( 'currently online servers:\n%s' % '\n'.join([f' {s}' for s in self.game_servers.values()])) self.last_player_update_time = current_time def handle_http_request_message(self, msg): if msg.env['PATH_INFO'] == '/status': if "REMOTE_ADDR" in msg.env: self.logger.info('Served status request via HTTP to peer "' + msg.env["REMOTE_ADDR"] + '"') else: self.logger.info( 'Served status request via HTTP to Unknown peer') msg.peer.send_response( json.dumps( { 'online_players': len(self.players), 'online_servers': len(self.game_servers) }, sort_keys=True, indent=4)) elif msg.env['PATH_INFO'] == '/detailed_status': if "REMOTE_ADDR" in msg.env: self.logger.info( 'Served detailed status request via HTTP to peer "' + msg.env["REMOTE_ADDR"] + '"') else: self.logger.info( 'Served detailed status request via HTTP to Unknown peer') online_game_servers_list = [{ 'locked': gs.password_hash is not None, 'mode': gs.game_setting_mode, 'name': gs.description, 'map': self.convert_map_id_to_map_name_and_game_type(gs.map_id)[0], 'type': self.convert_map_id_to_map_name_and_game_type(gs.map_id)[1], 'players': [p.display_name for p in gs.players.values()] } for gs in self.game_servers.values()] msg.peer.send_response( json.dumps( { 'online_players_list': [p.display_name for p in self.players.values()], 'online_servers_list': online_game_servers_list }, sort_keys=True, indent=4)) elif msg.env['PATH_INFO'] == '/player': if "REMOTE_ADDR" in msg.env: self.logger.info( 'Served player stats request via HTTP to peer "' + msg.env["REMOTE_ADDR"] + '"') else: self.logger.info( 'Served player stats request via HTTP to Unknown peer') if "QUERY_STRING" in msg.env: filtered_player_name = ''.join( filter(str.isalnum, msg.env['QUERY_STRING'])) player_data = None if filtered_player_name in self.accounts: player_data = self.get_player_settings_data( filtered_player_name) if player_data: filtered_player_data = { "player_found": True, "clan_tag": player_data["clan_tag"], "player_name": filtered_player_name, "rank_xp": player_data["progression"]["rank_xp"] } msg.peer.send_response( json.dumps(filtered_player_data, sort_keys=True, indent=4)) else: msg.peer.send_response( json.dumps({'player_found': False}, sort_keys=True, indent=4)) else: msg.peer.send_response(None) else: msg.peer.send_response(None) def get_player_settings_data(self, player_name): try: with open('data/players/' + player_name + '_settings.json', "r") as f: file_contents = json.load(f) return file_contents except FileNotFoundError: return None def convert_map_id_to_map_name_and_game_type(self, map_id): map_names_and_types = { "1447": ["Katabatic", "CTF"], "1456": ["Arx Novena", "CTF"], "1457": ["Drydock", "CTF"], "1458": ["Outskirts", "Rabbit"], "1461": ["Quicksand", "Rabbit"], "1462": ["Crossfire", "CTF"], "1464": ["Crossfire", "Rabbit"], "1473": ["Bella Omega", "CTF"], "1480": ["Drydock Night", "TDM"], "1482": ["Crossfire", "TDM"], "1484": ["Quicksand", "TDM"], "1485": ["Nightabatic", "TDM"], "1487": ["Inferno", "TDM"], "1488": ["Sulfur Cove", "TDM"], "1490": ["Outskirts", "TDM"], "1491": ["Inferno", "Rabbit"], "1493": ["Temple Ruins", "CTF"], "1494": ["Nightabatic", "Rabbit"], "1495": ["Air Arena", "Arena"], "1496": ["Sulfur Cove", "Rabbit"], "1497": ["Walled In", "Arena"], "1498": ["Lava Arena", "Arena"], "1512": ["Tartarus", "CTF"], "1514": ["Canyon Crusade Revival", "CTF"], "1516": ["Raindance", "CTF"], "1521": ["Katabatic", "CaH"], "1522": ["Stonehenge", "CTF"], "1523": ["Sunstar", "CTF"], "1525": ["Drydock Night", "CaH"], "1526": ["Outskirts 3P", "CaH"], "1528": ["Raindance", "CaH"], "1533": ["Hinterlands", "Arena"], "1534": ["Permafrost", "CTF"], "1535": ["Sulfur Cove", "CaH"], "1536": ["Miasma", "TDM"], "1537": ["Tartarus", "CaH"], "1538": ["Dangerous Crossing", "CTF"], "1539": ["Katabatic", "Blitz"], "1540": ["Arx Novena", "Blitz"], "1541": ["Drydock", "Blitz"], "1542": ["Crossfire", "Blitz"], "1543": ["Blueshift", "CTF"], "1544": ["Whiteout", "Arena"], "1545": ["Fraytown", "Arena"], "1546": ["Undercroft", "Arena"], "1548": ["Canyon Crusade Revival", "CaH"], "1549": ["Canyon Crusade Revival", "Blitz"], "1550": ["Bella Omega", "Blitz"], "1551": ["Bella Omega NS", "CTF"], "1552": ["Blueshift", "Blitz"], "1553": ["Terminus", "CTF"], "1554": ["Icecoaster", "CTF"], "1555": ["Perdition", "CTF"], "1557": ["Perdition", "TDM"], "1558": ["Icecoaster", "Blitz"], "1559": ["Terminus", "Blitz"], "1560": ["Hellfire", "CTF"], "1561": ["Hellfire", "Blitz"] } return map_names_and_types.get(str(map_id), ["Unknown", "Unknown"]) def handle_launcher_protocol_version_message(self, msg): launcher_version = StrictVersion(msg.version) my_version = launcher2loginserver_protocol_version if my_version.version[0] != launcher_version.version[0]: game_server = msg.peer self.logger.warning( f"{game_server} uses launcher protocol {launcher_version} which is " f"not compatible with this login server's protocol version {my_version}. " "Disconnecting game server...") msg.peer.send(Login2LauncherProtocolVersionMessage( str(my_version))) msg.peer.disconnect() def handle_address_info_message(self, msg): game_server = msg.peer external_ip = IPv4Address(msg.external_ip) if msg.external_ip else None internal_ip = IPv4Address(msg.internal_ip) if msg.internal_ip else None game_server.set_address_info(IPAddressPair(external_ip, internal_ip)) self.logger.info(f'{game_server}: address info received') def handle_server_info_message(self, msg): game_server = msg.peer password_hash = bytes( msg.password_hash) if msg.password_hash is not None else None game_server.set_info(msg.description, msg.motd, msg.game_setting_mode, password_hash) self.logger.info(f'{game_server}: server info received') def handle_map_info_message(self, msg): game_server = msg.peer game_server.map_id = msg.map_id def handle_team_info_message(self, msg): game_server = msg.peer for player_id, team_id in msg.player_to_team_id.items(): player_id = int(player_id) if player_id in self.players and self.players[ player_id].game_server is game_server: self.players[player_id].team = team_id else: self.logger.warning( 'received an invalid message from %s about ' 'player %d while that player is not on that server' % (game_server, player_id)) def handle_score_info_message(self, msg): game_server = msg.peer game_server.be_score = msg.be_score game_server.ds_score = msg.ds_score def handle_match_time_message(self, msg): game_server = msg.peer self.logger.info( f'{game_server}: received match time: {msg.seconds_remaining} seconds remaining (counting = {msg.counting})' ) game_server.set_match_time(msg.seconds_remaining, msg.counting) def handle_server_ready_message(self, msg): game_server = msg.peer game_server.set_ready(msg.port, msg.pingport) status = 'ready' if msg.port else 'not ready' self.logger.info(f'{game_server}: reports {status}') def handle_match_end_message(self, msg): game_server = msg.peer server_uptime = int((datetime.datetime.utcnow() - game_server.start_time).total_seconds()) for player in game_server.players.values(): if str(player.unique_id) in msg.players_time_played: time_played = msg.players_time_played[str( player.unique_id)]['time'] was_win = msg.players_time_played[str(player.unique_id)]['win'] # Cap playtime by the time the server has been active time_played = min(time_played, server_uptime) # Calculate and save the player's earned XP from this map player.player_settings.progression.earn_xp( time_played, was_win) # Update the XP in the UI player.send(a006d().set([ m04cb(), m05dc().set(player.player_settings.progression.rank_xp), m03ce().set(0x434D0000), m00fe().set([]), m0632(), m0296(), ])) self.logger.info(f'{game_server}: match ended') game_server.initialize_map_vote(msg.next_map_idx, msg.votable_maps) def handle_waiting_for_map_message(self, msg): game_server = msg.peer self.logger.info(f'{game_server}: is waiting to receive the next map') game_server.process_map_votes()
class LoginServer: def __init__(self, server_queue, client_queues, server_stats_queue, ports, accounts): self.logger = logging.getLogger(__name__) self.server_queue = server_queue self.client_queues = client_queues self.server_stats_queue = server_stats_queue self.game_servers = TracingDict() self.players = TracingDict() self.social_network = SocialNetwork() self.firewall = FirewallClient(ports) self.accounts = accounts self.message_handlers = { AuthCodeRequestMessage: self.handle_authcode_request_message, ExecuteCallbackMessage: self.handle_execute_callback_message, HttpRequestMessage: self.handle_http_request_message, PeerConnectedMessage: self.handle_client_connected_message, PeerDisconnectedMessage: self.handle_client_disconnected_message, LoginProtocolMessage: self.handle_client_message, Launcher2LoginProtocolVersionMessage: self.handle_launcher_protocol_version_message, Launcher2LoginAddressInfoMessage: self.handle_address_info_message, Launcher2LoginServerInfoMessage: self.handle_server_info_message, Launcher2LoginMapInfoMessage: self.handle_map_info_message, Launcher2LoginTeamInfoMessage: self.handle_team_info_message, Launcher2LoginScoreInfoMessage: self.handle_score_info_message, Launcher2LoginMatchTimeMessage: self.handle_match_time_message, Launcher2LoginServerReadyMessage: self.handle_server_ready_message, Launcher2LoginMatchEndMessage: self.handle_match_end_message, } self.pending_callbacks = PendingCallbacks(server_queue) self.address_pair, errormsg = IPAddressPair.detect() if not self.address_pair.external_ip: self.logger.warning('Unable to detect public IP address: %s\n' 'This will cause problems if the login server ' 'and any players are on the same LAN, but the ' 'game server is not.' % errormsg) else: self.logger.info('server: detected external IP: %s' % self.address_pair.external_ip) def run(self): gevent.getcurrent().name = 'loginserver' self.logger.info('server: login server started') self.firewall.reset_firewall('blacklist') while True: for message in self.server_queue: handler = self.message_handlers[type(message)] try: handler(message) except Exception as e: if hasattr(message, 'peer'): self.logger.error( 'server: an exception occurred while handling a message; passing it on to the peer...' ) message.peer.disconnect(e) else: raise def all_game_servers(self): return self.game_servers def find_server_by_id(self, server_id): for game_server in self.all_game_servers().values(): if game_server.server_id == server_id: return game_server raise ProtocolViolationError( 'No server found with specified server ID') def find_server_by_match_id(self, match_id): for game_server in self.all_game_servers().values(): if game_server.match_id == match_id: return game_server raise ProtocolViolationError('No server found with specified match ID') def find_player_by(self, **kwargs): matching_players = self.find_players_by(**kwargs) if len(matching_players) > 1: raise ValueError("More than one player matched query") return matching_players[0] if matching_players else None def find_players_by(self, **kwargs): matching_players = self.players.values() for key, val in kwargs.items(): matching_players = [ player for player in matching_players if getattr(player, key) == val ] return matching_players def find_player_by_display_name(self, display_name): matching_players = [ p for p in self.players.values() if p.display_name is not None and p.display_name.lower() == display_name.lower() ] if matching_players: return matching_players[0] else: return None def change_player_unique_id(self, old_id, new_id): if new_id in self.players: raise AlreadyLoggedInError() assert old_id in self.players assert new_id not in self.players player = self.players.pop(old_id) player.unique_id = new_id self.players[new_id] = player def validate_username(self, username): if len(username) < Player.min_name_length: return 'User name is too short, min length is %d characters.' % Player.min_name_length if len(username) > Player.max_name_length: return 'User name is too long, max length is %d characters.' % Player.max_name_length try: ascii_bytes = username.encode('ascii') except UnicodeError: return 'User name contains invalid (i.e. non-ascii) characters' if not utils.is_valid_ascii_for_name(ascii_bytes): return 'User name contains invalid characters' return None def send_server_stats(self): stats = [{ 'locked': gs.password_hash is not None, 'mode': gs.game_setting_mode, 'description': gs.description, 'nplayers': len(gs.players) } for gs in self.game_servers.values() if gs.joinable] self.server_stats_queue.put(stats) def handle_authcode_request_message(self, msg): authcode_requester = msg.peer validation_failure = self.validate_username(msg.login_name) if validation_failure: self.logger.warning( "server: authcode requested for invalid user name '%s': %s. Refused." % (msg.login_name, validation_failure)) authcode_requester.send('Error: %s' % validation_failure) else: availablechars = ''.join(c for c in (string.ascii_letters + string.digits) if c not in 'O0Il') authcode = ''.join( [random.choice(availablechars) for i in range(8)]) self.logger.info('server: authcode requested for %s, returned %s' % (msg.login_name, authcode)) self.accounts.add_account(msg.login_name, authcode) self.accounts.save() authcode_requester.send(authcode) def handle_execute_callback_message(self, msg): callback_id = msg.callback_id self.pending_callbacks.execute(callback_id) def handle_client_connected_message(self, msg): if isinstance(msg.peer, Player): unique_id = utils.first_unused_number_above( self.players.keys(), 10000000) player = msg.peer player.friends.connect_to_social_network(self.social_network) player.unique_id = unique_id player.login_server = self player.complement_address_pair(self.address_pair) player.set_state(UnauthenticatedState) self.players[unique_id] = player elif isinstance(msg.peer, GameServer): server_id = utils.first_unused_number_above( self.all_game_servers().keys(), 1) game_server = msg.peer game_server.server_id = server_id game_server.match_id = server_id + 10000000 game_server.game_setting_mode = None game_server.login_server = self self.game_servers[server_id] = game_server self.logger.info('server: added game server %s (%s)' % (server_id, game_server.detected_ip)) elif isinstance(msg.peer, AuthCodeRequester): pass else: assert False, "Invalid connection message received" def handle_client_disconnected_message(self, msg): if isinstance(msg.peer, Player): player = msg.peer player.disconnect() self.pending_callbacks.remove_receiver(player) player.set_state(OfflineState) del (self.players[player.unique_id]) elif isinstance(msg.peer, GameServer): game_server = msg.peer self.logger.info('server: removed game server %s (%s:%s)' % (game_server.server_id, game_server.detected_ip, game_server.port)) game_server.disconnect() self.pending_callbacks.remove_receiver(game_server) del (self.game_servers[game_server.server_id]) elif isinstance(msg.peer, AuthCodeRequester): msg.peer.disconnect() else: assert False, "Invalid disconnection message received" def handle_client_message(self, msg): current_player = msg.peer current_player.last_received_seq = msg.clientseq requests = '\n'.join([' %04X' % req.ident for req in msg.requests]) self.logger.info('server: %s sent: %s' % (current_player, requests)) for request in msg.requests: current_player.handle_request(request) def handle_http_request_message(self, msg): if msg.env['PATH_INFO'] == '/status': msg.peer.send_response( json.dumps( { 'online_players': len(self.players), 'online_servers': len(self.game_servers) }, sort_keys=True, indent=4)) else: msg.peer.send_response(None) def handle_launcher_protocol_version_message(self, msg): launcher_version = StrictVersion(msg.version) my_version = launcher2loginserver_protocol_version if my_version.version[0] != launcher_version.version[0]: game_server = msg.peer self.logger.warning( "server: game server %s (%s) uses launcher protocol %s which is " "not compatible with this login server's protocol version %s. " "Disconnecting game server..." % (game_server.server_id, game_server.detected_ip, launcher_version, my_version)) msg.peer.send(Login2LauncherProtocolVersionMessage( str(my_version))) msg.peer.disconnect() def handle_address_info_message(self, msg): game_server = msg.peer external_ip = IPv4Address(msg.external_ip) if msg.external_ip else None internal_ip = IPv4Address(msg.internal_ip) if msg.internal_ip else None game_server.set_address_info(IPAddressPair(external_ip, internal_ip)) self.logger.info('server: address info received for server %s (%s)' % (game_server.server_id, game_server.detected_ip)) def handle_server_info_message(self, msg): game_server = msg.peer password_hash = bytes( msg.password_hash) if msg.password_hash is not None else None game_server.set_info(msg.description, msg.motd, msg.game_setting_mode, password_hash) self.logger.info('server: server info received for %s server %s (%s)' % (game_server.game_setting_mode, game_server.server_id, game_server.detected_ip)) def handle_map_info_message(self, msg): game_server = msg.peer game_server.map_id = msg.map_id def handle_team_info_message(self, msg): game_server = msg.peer for player_id, team_id in msg.player_to_team_id.items(): player_id = int(player_id) if player_id in self.players and self.players[ player_id].game_server is game_server: self.players[player_id].team = team_id else: self.logger.warning( 'server: received an invalid message from server %s about ' 'player %d while that player is not on that server' % (game_server.server_id, player_id)) def handle_score_info_message(self, msg): game_server = msg.peer game_server.be_score = msg.be_score game_server.ds_score = msg.ds_score def handle_match_time_message(self, msg): game_server = msg.peer self.logger.info( 'server: received match time for server %s: %s seconds remaining (counting = %s)' % (game_server.server_id, msg.seconds_remaining, msg.counting)) game_server.set_match_time(msg.seconds_remaining, msg.counting) def handle_server_ready_message(self, msg): game_server = msg.peer game_server.set_ready(msg.port, msg.pingport) status = 'ready' if msg.port else 'not ready' self.logger.info('server: server %s (%s:%s/%s) reports %s' % (game_server.server_id, game_server.detected_ip, game_server.port, game_server.pingport, status)) def handle_match_end_message(self, msg): game_server = msg.peer server_uptime = int((datetime.datetime.utcnow() - game_server.start_time).total_seconds()) for player in game_server.players.values(): if str(player.unique_id) in msg.players_time_played: time_played = msg.players_time_played[str( player.unique_id)]['time'] was_win = msg.players_time_played[str(player.unique_id)]['win'] # Cap playtime by the time the server has been active time_played = min(time_played, server_uptime) # Calculate and save the player's earned XP from this map player.player_settings.progression.earn_xp( time_played, was_win) # Update the XP in the UI player.send(a006d().set([ m04cb(), m05dc().set(player.player_settings.progression.rank_xp), m03ce().set(0x434D0000), m00fe().set([]), m0632(), m0296(), ])) self.logger.info('server: match ended on server %s.' % game_server.server_id)
def __init__(self, game_server_config, ports, incoming_queue, server_handler_queue): gevent.getcurrent().name = 'launcher' self.pending_callbacks = PendingCallbacks(incoming_queue) self.logger = logging.getLogger(__name__) self.ports = ports self.firewall = FirewallClient(ports) self.game_server_config = game_server_config self.incoming_queue = incoming_queue self.server_handler_queue = server_handler_queue self.players = TracingDict() self.game_controller = None self.login_server = None self.active_server = GameServerProcess('gameserver1', self.ports, server_handler_queue) self.pending_server = GameServerProcess('gameserver2', self.ports, server_handler_queue) self.min_next_switch_time = None try: with open(map_rotation_state_path, 'rt') as f: self.controller_context = json.load(f) except IOError: self.controller_context = {} self.last_waiting_for_map_message = None self.last_server_info_message = None self.last_map_info_message = None self.last_team_info_message = None self.last_score_info_message = None self.last_match_time_message = None self.last_server_ready_message = None self.last_match_end_message = None self.address_pair, errormsg = IPAddressPair.detect() if not self.address_pair.external_ip: self.logger.warning('Unable to detect public IP address: %s\n' 'This will cause problems if the login server ' 'or any of your players are not on your LAN.' % errormsg) else: self.logger.info('launcher: detected external IP: %s' % self.address_pair.external_ip) if not self.address_pair.internal_ip: self.logger.warning( 'You appear to be running the game server on a machine ' 'directly connected to the internet. This is will cause ' 'problems if the login server or any of your players ' 'are on your LAN.') else: self.logger.info('launcher: detected internal IP: %s' % self.address_pair.internal_ip) self.message_handlers = { PeerConnectedMessage: self.handle_peer_connected, PeerDisconnectedMessage: self.handle_peer_disconnected, Login2LauncherProtocolVersionMessage: self.handle_login_server_protocol_version_message, Login2LauncherNextMapMessage: self.handle_next_map_message, Login2LauncherSetPlayerLoadoutsMessage: self.handle_set_player_loadouts_message, Login2LauncherRemovePlayerLoadoutsMessage: self.handle_remove_player_loadouts_message, Login2LauncherAddPlayer: self.handle_add_player_message, Login2LauncherRemovePlayer: self.handle_remove_player_message, Login2LauncherPings: self.handle_pings_message, Login2LauncherMapVoteResult: self.handle_map_vote_result, Game2LauncherProtocolVersionMessage: self.handle_game_controller_protocol_version_message, Game2LauncherServerInfoMessage: self.handle_server_info_message, Game2LauncherMapInfoMessage: self.handle_map_info_message, Game2LauncherTeamInfoMessage: self.handle_team_info_message, Game2LauncherScoreInfoMessage: self.handle_score_info_message, Game2LauncherMatchTimeMessage: self.handle_match_time_message, Game2LauncherMatchEndMessage: self.handle_match_end_message, Game2LauncherLoadoutRequest: self.handle_loadout_request_message, GameServerTerminatedMessage: self.handle_game_server_terminated_message, ExecuteCallbackMessage: self.handle_execute_callback_message }
class Launcher: def __init__(self, game_server_config, ports, incoming_queue, server_handler_queue): gevent.getcurrent().name = 'launcher' self.pending_callbacks = PendingCallbacks(incoming_queue) self.logger = logging.getLogger(__name__) self.ports = ports self.firewall = FirewallClient(ports) self.game_server_config = game_server_config self.incoming_queue = incoming_queue self.server_handler_queue = server_handler_queue self.players = TracingDict() self.game_controller = None self.login_server = None self.active_server = GameServerProcess('gameserver1', self.ports, server_handler_queue) self.pending_server = GameServerProcess('gameserver2', self.ports, server_handler_queue) self.min_next_switch_time = None try: with open(map_rotation_state_path, 'rt') as f: self.controller_context = json.load(f) except IOError: self.controller_context = {} self.last_waiting_for_map_message = None self.last_server_info_message = None self.last_map_info_message = None self.last_team_info_message = None self.last_score_info_message = None self.last_match_time_message = None self.last_server_ready_message = None self.last_match_end_message = None self.address_pair, errormsg = IPAddressPair.detect() if not self.address_pair.external_ip: self.logger.warning('Unable to detect public IP address: %s\n' 'This will cause problems if the login server ' 'or any of your players are not on your LAN.' % errormsg) else: self.logger.info('launcher: detected external IP: %s' % self.address_pair.external_ip) if not self.address_pair.internal_ip: self.logger.warning( 'You appear to be running the game server on a machine ' 'directly connected to the internet. This is will cause ' 'problems if the login server or any of your players ' 'are on your LAN.') else: self.logger.info('launcher: detected internal IP: %s' % self.address_pair.internal_ip) self.message_handlers = { PeerConnectedMessage: self.handle_peer_connected, PeerDisconnectedMessage: self.handle_peer_disconnected, Login2LauncherProtocolVersionMessage: self.handle_login_server_protocol_version_message, Login2LauncherNextMapMessage: self.handle_next_map_message, Login2LauncherSetPlayerLoadoutsMessage: self.handle_set_player_loadouts_message, Login2LauncherRemovePlayerLoadoutsMessage: self.handle_remove_player_loadouts_message, Login2LauncherAddPlayer: self.handle_add_player_message, Login2LauncherRemovePlayer: self.handle_remove_player_message, Login2LauncherPings: self.handle_pings_message, Login2LauncherMapVoteResult: self.handle_map_vote_result, Game2LauncherProtocolVersionMessage: self.handle_game_controller_protocol_version_message, Game2LauncherServerInfoMessage: self.handle_server_info_message, Game2LauncherMapInfoMessage: self.handle_map_info_message, Game2LauncherTeamInfoMessage: self.handle_team_info_message, Game2LauncherScoreInfoMessage: self.handle_score_info_message, Game2LauncherMatchTimeMessage: self.handle_match_time_message, Game2LauncherMatchEndMessage: self.handle_match_end_message, Game2LauncherLoadoutRequest: self.handle_loadout_request_message, GameServerTerminatedMessage: self.handle_game_server_terminated_message, ExecuteCallbackMessage: self.handle_execute_callback_message } def run(self): self.firewall.reset_firewall('whitelist') self.pending_server.start() while True: for message in self.incoming_queue: handler = self.message_handlers[type(message)] handler(message) def get_other_server(self, server): for other_server in ['gameserver1', 'gameserver2']: if other_server != server: return other_server assert False def handle_peer_connected(self, msg): if isinstance(msg.peer, GameController): pass elif isinstance(msg.peer, LoginServer): if self.login_server is not None: raise RuntimeError( 'There should only be a connection to one login server at a time' ) self.login_server = msg.peer msg = Launcher2LoginProtocolVersionMessage( str(versions.launcher2loginserver_protocol_version)) self.login_server.send(msg) msg = Launcher2LoginAddressInfoMessage( str(self.address_pair.external_ip) if self.address_pair.external_ip else '', str(self.address_pair.internal_ip) if self.address_pair.internal_ip else '') self.login_server.send(msg) # Send the latest relevant information that was received # while the login server was not connected if self.last_waiting_for_map_message: self.login_server.send(self.last_waiting_for_map_message) self.last_waiting_for_map_message = None if self.last_server_info_message: self.login_server.send(self.last_server_info_message) self.last_server_info_message = None if self.last_map_info_message: self.login_server.send(self.last_map_info_message) self.last_map_info_message = None if self.last_team_info_message: self.login_server.send(self.last_team_info_message) self.last_team_info_message = None if self.last_score_info_message: self.login_server.send(self.last_score_info_message) self.last_score_info_message = None if self.last_match_time_message: self.login_server.send(self.last_match_time_message) self.last_match_time_message = None if self.last_server_ready_message: self.login_server.send(self.last_server_ready_message) self.last_server_ready_message = None if self.last_match_end_message: self.login_server.send(self.last_match_end_message) self.last_match_end_message = None else: assert False, "Invalid connection message received" def hash_server_password(self, password: str) -> List[int]: hash_constants = [0x55, 0x93, 0x55, 0x58, 0xBA, 0x6f, 0xe9, 0xf9] interspersed_constants = [ 0x7a, 0x1e, 0x9f, 0x47, 0xf9, 0x17, 0xb0, 0x03 ] result = [] for idx, c in enumerate(password.encode('latin1')): pattern_idx = idx % 8 result.extend([(c ^ hash_constants[pattern_idx]), interspersed_constants[pattern_idx]]) return result def handle_peer_disconnected(self, msg): if isinstance(msg.peer, GameController): msg.peer.disconnect() elif isinstance(msg.peer, LoginServer): if self.login_server is None: raise RuntimeError( 'How can a login server disconnect if it\'s not there?') self.login_server.disconnect() self.login_server = None else: assert False, "Invalid disconnection message received" def handle_execute_callback_message(self, msg): callback_id = msg.callback_id self.pending_callbacks.execute(callback_id) def handle_login_server_protocol_version_message(self, msg): # The only time we get a message with the login server's protocol version # is when the version that we sent is incompatible with it. raise IncompatibleVersionError( 'The protocol version that this game server launcher supports (%s) is ' 'incompatible with the version supported by the login server at %s:%d (%s)' % (versions.launcher2loginserver_protocol_version, self.login_server.ip, self.login_server.port, StrictVersion(msg.version))) def freeze_active_server_if_empty(self): if len( self.players ) == 0 and self.active_server.ready and not self.active_server.frozen: self.active_server.freeze() def handle_next_map_message(self, msg): self.logger.info( f'launcher: switching to {self.pending_server.name} on port {self.pending_server.port}' ) if self.active_server.running: self.logger.info(f'launcher: stopping {self.active_server.name}') self.active_server.stop() self.active_server, self.pending_server = self.pending_server, self.active_server self.pending_callbacks.add(self, 5, self.freeze_active_server_if_empty) def handle_set_player_loadouts_message(self, msg): self.logger.info('launcher: loadouts changed for player %d' % msg.unique_id) self.players[msg.unique_id] = msg.loadouts def handle_remove_player_loadouts_message(self, msg): self.logger.info('launcher: loadouts removed for player %d' % msg.unique_id) self.players[msg.unique_id] = None def handle_add_player_message(self, msg): if msg.ip: self.logger.info( 'launcher: login server added player %d with ip %s' % (msg.unique_id, msg.ip)) self.firewall.modify_firewall('whitelist', 'add', msg.unique_id, msg.ip) else: self.logger.info('launcher: login server added local player %d' % msg.unique_id) if len(self.players) == 0 and self.active_server.frozen: self.active_server.unfreeze() self.players[msg.unique_id] = None # If the active server is not ready then we are between match end and the switch to the pending server. # It's ok to just drop this message in that case, because when the players are redirected to the pending # server another add_player message will come. if self.active_server.ready: self.game_controller.send( Launcher2GamePlayerInfo(msg.unique_id, msg.rank_xp, msg.eligible_for_first_win)) def handle_remove_player_message(self, msg): if msg.ip: self.logger.info( 'launcher: login server removed player %d with ip %s' % (msg.unique_id, msg.ip)) self.firewall.modify_firewall('whitelist', 'remove', msg.unique_id, msg.ip) else: self.logger.info('launcher: login server removed local player %d' % msg.unique_id) del (self.players[msg.unique_id]) self.freeze_active_server_if_empty() def handle_pings_message(self, msg): if self.game_controller: self.game_controller.send(Launcher2GamePings(msg.player_pings)) def handle_game_controller_protocol_version_message(self, msg): controller_version = StrictVersion(msg.version) my_version = versions.launcher2controller_protocol_version self.logger.info( 'launcher: received protocol version %s from game controller' % controller_version) if controller_version.version[0] != my_version.version[0]: raise IncompatibleVersionError( 'The protocol version of the game controller DLL (%s) is incompatible ' 'with the version supported by this game server launcher (%s)' % (controller_version, my_version)) self.game_controller = msg.peer msg = Launcher2LoginWaitingForMap() if self.login_server: self.login_server.send(msg) else: self.last_waiting_for_map_message = msg def handle_map_vote_result(self, msg): self.logger.info( f'launcher: received map vote result from login server: map = {msg.map_id}' ) if msg.map_id is not None: self.controller_context['next_map_index'] = msg.map_id msg = Launcher2GameInit(self.controller_context) self.game_controller.send(msg) def handle_server_info_message(self, msg): self.logger.info('launcher: received server info from game controller') msg = Launcher2LoginServerInfoMessage(msg.description, msg.motd, msg.game_setting_mode, msg.password_hash) if self.login_server: self.login_server.send(msg) else: self.last_server_info_message = msg def handle_map_info_message(self, msg): self.logger.info('launcher: received map info from game controller') msg = Launcher2LoginMapInfoMessage(msg.map_id) if self.login_server: self.login_server.send(msg) else: self.last_map_info_message = msg def handle_team_info_message(self, msg): self.logger.info('launcher: received team info from game controller') for player_id, team_id in msg.player_to_team_id.items(): if int(player_id) not in self.players: return msg = Launcher2LoginTeamInfoMessage(msg.player_to_team_id) if self.login_server: self.login_server.send(msg) else: self.last_team_info_message = msg def handle_score_info_message(self, msg): self.logger.info('launcher: received score info from game controller') msg = Launcher2LoginScoreInfoMessage(msg.be_score, msg.ds_score) if self.login_server: self.login_server.send(msg) else: self.last_score_info_message = msg def set_server_ready(self): self.pending_server.set_ready(True) self.logger.info( f'launcher: reporting {self.pending_server.name} as ready') msg = Launcher2LoginServerReadyMessage(self.pending_server.port, self.ports['launcherping']) if self.login_server: self.login_server.send(msg) else: self.last_server_ready_message = msg def handle_match_time_message(self, msg): self.logger.info('launcher: received match time from game controller') msg = Launcher2LoginMatchTimeMessage(msg.seconds_remaining, msg.counting) if self.login_server: self.login_server.send(msg) else: self.last_match_time_message = msg if self.pending_server.running and not self.pending_server.ready: if self.min_next_switch_time: time_left = (self.min_next_switch_time - datetime.datetime.utcnow()).total_seconds() else: time_left = 0 if time_left > 0: self.pending_callbacks.add(self, time_left, self.set_server_ready) else: self.set_server_ready() def handle_match_end_message(self, msg): self.logger.info( 'launcher: received match end from game controller (controller context = %s)' % msg.controller_context) self.game_controller = None self.active_server.set_ready(False) self.controller_context = msg.controller_context with open(map_rotation_state_path, 'wt') as f: json.dump(self.controller_context, f) if 'next_map_index' in self.controller_context: next_map_idx = self.controller_context['next_map_index'] else: next_map_idx = 0 msg_to_login = Launcher2LoginMatchEndMessage(next_map_idx, msg.votable_maps, msg.players_time_played) if self.login_server: self.login_server.send(msg_to_login) else: self.last_match_end_message = msg_to_login self.min_next_switch_time = datetime.datetime.utcnow( ) + datetime.timedelta(seconds=msg.next_map_wait_time) self.pending_server.start() def handle_loadout_request_message(self, msg): self.logger.info( 'launcher: received loadout request from game controller') # Class and loadout keys are strings because they came in as json. # There's not much point in converting all keys in the loadouts # dictionary from strings back to ints if we are just going to # send it out as json again later. player_key = msg.player_unique_id class_key = str(msg.class_id) loadout_key = str(msg.loadout_number) if msg.player_unique_id in self.players: try: loadout = self.players[player_key][class_key][loadout_key] except KeyError: # TODO: This is a temporary solution to a bug in tamods-server that causes an incorrect class to be sent ('1686') # We should figure out what's going on and then remove this code again. self.logger.warning( 'launcher: Incorrect params for loadout of player %d [class = %s, loadout = %s]. Sending empty loadout.' % (msg.player_unique_id, class_key, loadout_key)) loadout = {} else: self.logger.warning( 'launcher: Unable to find player %d\'s loadouts. Sending empty loadout.' % msg.player_unique_id) loadout = {} msg = Launcher2GameLoadoutMessage(msg.player_unique_id, msg.class_id, loadout) self.game_controller.send(msg) def handle_game_server_terminated_message(self, msg): terminated_server = self.active_server if self.active_server.name == msg.server else self.pending_server was_already_stopping = terminated_server.stopping terminated_server.terminated() if was_already_stopping: self.logger.info( f'launcher: {terminated_server.name} process terminated.') else: if self.pending_server.running: self.logger.info( f'launcher: {terminated_server.name} process terminated unexpectedly; ' f'{self.pending_server.name} already starting.') else: self.logger.info( f'launcher: {terminated_server.name} process terminated unexpectedly; ' f'starting {self.pending_server.name} to take over.') self.pending_server.start() msg = Launcher2LoginServerReadyMessage(None, None) if self.login_server: self.login_server.send(msg) else: self.last_server_ready_message = msg
class GameServer(Peer): def __init__(self, detected_ip: IPv4Address, ports): super().__init__() self.logger = logging.getLogger(__name__) self.firewall = FirewallClient(ports) self.login_server = None self.server_id = None self.match_id = None self.detected_ip = detected_ip self.address_pair = None self.port = None self.pingport = None self.description = None self.motd = None self.password_hash = None self.region = None self.game_setting_mode = 'ootb' self.joinable = False self.players = TracingDict(refsonly=True) self.player_being_kicked = None self.match_end_time_rel_or_abs = None self.match_time_counting = False self.be_score = 0 self.ds_score = 0 self.map_id = 0 self.start_time = None if self.detected_ip.is_global: response = urllib.request.urlopen( 'http://tools.keycdn.com/geo.json?host=%s' % self.detected_ip) result = response.read() json_result = json.loads(result) continent_code_to_region = { 'NA': REGION_NORTH_AMERICA, 'EU': REGION_EUROPE, 'OC': REGION_OCEANIA_AUSTRALIA } try: self.region = continent_code_to_region[ json_result['data']['geo']['continent_code']] except KeyError: self.region = REGION_EUROPE else: self.region = REGION_EUROPE def __str__(self): return 'GameServer(%d)' % self.server_id def disconnect(self, exception=None): for player in list(self.players.values()): player.set_state(AuthenticatedState) super().disconnect(exception) def set_address_info(self, address_pair): self.address_pair = address_pair self.send_pings() def set_info(self, description: str, motd: str, game_setting_mode: str, password_hash: bytes): self.description = description self.motd = motd self.game_setting_mode = game_setting_mode self.password_hash = password_hash def set_match_time(self, seconds_remaining, counting): self.match_time_counting = counting if counting: self.match_end_time_rel_or_abs = int(time.time() + seconds_remaining) else: self.match_end_time_rel_or_abs = seconds_remaining def set_ready(self, port, pingport): if port is not None: self.be_score = 0 self.ds_score = 0 self.port = port self.pingport = pingport self.joinable = True self.start_time = datetime.datetime.utcnow() for unique_id, player in self.players.items(): b4msg = a00b4().set_server(self).set_player(unique_id) b4msg.findbytype(m042a).set(3) b4msg.content.append(m02ff()) player.send(b4msg) self.send(Login2LauncherNextMapMessage()) else: self.joinable = False def get_time_remaining(self): if self.match_end_time_rel_or_abs is not None: if self.match_time_counting: time_remaining = int(self.match_end_time_rel_or_abs - time.time()) else: time_remaining = self.match_end_time_rel_or_abs else: time_remaining = 0 if time_remaining < 0: time_remaining = 0 return time_remaining def add_player(self, player): assert player.unique_id not in self.players self.players[player.unique_id] = player player.vote = None player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherAddPlayer( player.unique_id, str(player_ip) if player_ip is not None else '', player.player_settings.progression.rank_xp, player.player_settings.progression.is_eligible_for_first_win()) self.send(msg) def remove_player(self, player): assert player.unique_id in self.players del self.players[player.unique_id] player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherRemovePlayer( player.unique_id, str(player_ip) if player_ip is not None else '') self.send(msg) def send_all_players(self, data): for player in self.players.values(): player.send(data) def send_all_players_on_team(self, data, team): for player in self.players.values(): if player.team == team: player.send(data) def set_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherSetPlayerLoadoutsMessage( player.unique_id, player.get_current_loadouts().get_data()) self.send(msg) def remove_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherRemovePlayerLoadoutsMessage(player.unique_id) self.send(msg) def start_votekick(self, kicker, kickee): if kickee.unique_id in self.players and self.player_being_kicked is None: # Start a new vote reply = a018c() reply.content = [ m02c4().set(self.match_id), m034a().set(kickee.display_name), m0348().set(kickee.unique_id), m02fc().set(STDMSG_VOTE_BY_X_KICK_PLAYER_X_YES_NO), m0442().set_success(True), m0704().set(kicker.unique_id), m0705().set(kicker.display_name) ] self.send_all_players(reply) for player in self.players.values(): player.vote = None self.player_being_kicked = kickee self.logger.info( 'server: votekick started by %d:"%s" against %d:"%s"' % (kicker.unique_id, kicker.display_name, kickee.unique_id, kickee.display_name)) self.login_server.pending_callbacks.add(self, 35, self.end_votekick) def end_votekick(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) self.logger.info( 'server: votekick %s at timeout with %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % ('passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def check_votes(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) # If enough people vote yes or the vote is unanimous, end the vote immediately. # Otherwise wait for the timeout. if (yes_votes >= 8 and vote_passed) or total_votes == eligible_voters: self.logger.info( 'server: votekick %s immediately %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % ('passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def _tally_votes(self): eligible_voters_votes = { p.address_pair.get_address_seen_from( self.login_server.address_pair): p.vote for p in self.players.values() } votes = [v for v in eligible_voters_votes.values() if v is not None] yes_votes = [v for v in votes if v] vote_passed = len(votes) >= 4 and len(yes_votes) / len(votes) >= 0.5 return len(eligible_voters_votes), len(votes), len( yes_votes), vote_passed def _do_kick(self, votekick_passed): player_to_kick = self.player_being_kicked reply = a018c() reply.content = [ m0348().set(player_to_kick.unique_id), m034a().set(player_to_kick.display_name) ] if votekick_passed: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_HAS_BEEN_KICKED), m0442().set_success(True) ]) else: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_WAS_NOT_VOTED_OUT), m0442().set_success(False) ]) self.send_all_players(reply) if votekick_passed: # TODO: figure out if a real votekick also causes an # inconsistency between the menu you see and the one # you're really in for msg in [a00b0(), a0035().setmainmenu(), a006f()]: player_to_kick.send(msg) player_to_kick.set_state(UnauthenticatedState) ip_to_ban_on_login_server = player_to_kick.address_pair.get_address_seen_from( self.login_server.address_pair) self.firewall.modify_firewall('blacklist', 'add', player_to_kick.unique_id, ip_to_ban_on_login_server) def remove_blacklist_rule(): self.firewall.modify_firewall('blacklist', 'remove', player_to_kick.unique_id, ip_to_ban_on_login_server) self.login_server.pending_callbacks.add(self.login_server, 8 * 3600, remove_blacklist_rule) self.player_being_kicked = None def send_pings(self): player_pings = {} for unique_id, player in self.players.items(): player_pings[unique_id] = player.pings[ self.region] if self.region in player.pings else 999 self.send(Login2LauncherPings(player_pings)) self.login_server.pending_callbacks.add(self, PING_UPDATE_TIME, self.send_pings)