Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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))
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
    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
        }
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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)