async def test_cancel_twice(ladder_service: LadderService, player_factory): p1 = player_factory('Dostya', player_id=1, ladder_rating=(1500, 500), ladder_games=0) p2 = player_factory('Brackman', player_id=2, ladder_rating=(2000, 500), ladder_games=0) search = Search([p1]) search2 = Search([p2]) await ladder_service.start_search(p1, search, 'ladder1v1') await ladder_service.start_search(p2, search2, 'ladder1v1') searches = ladder_service._cancel_existing_searches(p1) assert search.is_cancelled assert searches == [search] assert not search2.is_cancelled searches = ladder_service._cancel_existing_searches(p1) assert searches == [] searches = ladder_service._cancel_existing_searches(p2) assert search2.is_cancelled assert searches == [search2]
def test_make_teams_single_2v2_large_pool(player_factory): """ When we have a large number of players all with similar ratings, we want teams to be formed by putting players with the same rating on the same team. """ # Large enough so the test is unlikely to pass by chance num = 40 searches = [ Search([player_factory(random.uniform(950, 1050), 10, name=f"p{i}")]) for i in range(num) ] searches += [ Search([player_factory(random.uniform(450, 550), 10, name=f"p{i}")]) for i in range(num) ] matched, non_matched = algorithm.make_teams_from_single(searches, size=2) assert matched != [] assert non_matched == [] for search in matched: p1, p2 = search.players p1_mean, _ = p1.ratings[RatingType.LADDER_1V1] p2_mean, _ = p2.ratings[RatingType.LADDER_1V1] # assert math.fabs(p1_mean - p2_mean) <= 100
async def test_queue_mid_cancel(matchmaker_queue, matchmaker_players_all_match): # Turn list of players into map from ids to players. _, p1, p2, p3, _ = matchmaker_players_all_match (s1, s2, s3) = (Search([p1]), Search([p2]), Search([p3])) asyncio.create_task(matchmaker_queue.search(s1)) asyncio.create_task(matchmaker_queue.search(s2)) s1.cancel() async def find_matches(): await asyncio.sleep(0.01) await matchmaker_queue.find_matches() try: await asyncio.gather( asyncio.wait_for(matchmaker_queue.search(s3), 0.1), asyncio.create_task(find_matches())) except CancelledError: pass assert not s1.is_matched assert s2.is_matched assert s3.is_matched assert len(matchmaker_queue._queue) == 0 matchmaker_queue.on_match_found.assert_called_once_with( s2, s3, matchmaker_queue)
async def test_cancel_twice(ladder_service: LadderService): p1 = mock.create_autospec(Player('Dostya', id=1)) p1.ladder_rating = (1500, 500) p1.numGames = 0 p2 = mock.create_autospec(Player('Brackman', id=1)) p2.ladder_rating = (2000, 50) p2.numGames = 0 search = Search([p1]) search2 = Search([p2]) ladder_service.start_search(p1, search, 'ladder1v1') ladder_service.start_search(p2, search2, 'ladder1v1') searches = ladder_service._cancel_existing_searches(p1) assert search.is_cancelled assert searches == [search] assert not search2.is_cancelled searches = ladder_service._cancel_existing_searches(p1) assert searches == [] searches = ladder_service._cancel_existing_searches(p2) assert search2.is_cancelled assert searches == [search2]
def test_make_teams_single_2v2_small_pool(player_factory): """ When we have a small number of players, we want teams to be formed by distributing players of equal skill to different teams so that we can maximize the chances of getting a match. """ # Try a bunch of times so it is unlikely to pass by chance for _ in range(20): searches = [ Search([player_factory(random.gauss(1000, 5), 10, name=f"p{i}")]) for i in range(2) ] searches += [ Search([player_factory(random.gauss(500, 5), 10, name=f"r{i}")]) for i in range(2) ] matched, non_matched = algorithm.make_teams_from_single(searches, size=2) assert matched != [] assert non_matched == [] for search in matched: p1, p2 = search.players # Order doesn't matter if p1.ratings[RatingType.LADDER_1V1][0] > 900: assert p2.ratings[RatingType.LADDER_1V1][0] < 600 else: assert p1.ratings[RatingType.LADDER_1V1][0] < 600 assert p2.ratings[RatingType.LADDER_1V1][0] > 900
async def test_queue_mid_cancel(mocker, player_service, matchmaker_queue, matchmaker_players_all_match): # Turn list of players into map from ids to players. player_service.players = dict( map(lambda x: (x.id, x), list(matchmaker_players_all_match))) p0, p1, p2, p3, _ = matchmaker_players_all_match (s1, s2, s3) = (Search([p1]), Search([p2]), Search([p3])) asyncio.ensure_future(matchmaker_queue.search(s1)) asyncio.ensure_future(matchmaker_queue.search(s2)) s1.cancel() async def find_matches(): await asyncio.sleep(0.01) matchmaker_queue.find_matches() try: await asyncio.gather( asyncio.wait_for(matchmaker_queue.search(s3), 0.1), asyncio.ensure_future(find_matches())) except CancelledError: pass assert not s1.is_matched assert s2.is_matched assert s3.is_matched assert len(matchmaker_queue) == 0
def test_search_await(mocker, loop, matchmaker_players): p1, p2, _, _, _ = matchmaker_players s1, s2 = Search(p1), Search(p2) assert not s1.matches_with(s2) await_coro = asyncio.async(s1.await_match()) s1.match(s2) yield from asyncio.wait_for(await_coro, 1) assert await_coro.done()
def test_unmatched_newbies_do_not_forcefully_match_teams(p): newbie = Search([p(1500, 500, ladder_games=0)]) team = Search([p(1500, 100), p(1500, 100)]) searches = [newbie, team] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 0
def test_unmatched_newbies_do_notforcefully_match_top_players(p): newbie = Search([p(1500, 500, ladder_games=0)]) top_player = Search([p(2500, 10, ladder_games=100)]) searches = [newbie, top_player] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 0
def test_unmatched_newbies_forcefully_match_pros(p): newbie = Search([p(1500, 500, ladder_games=0)]) pro = Search([p(1400, 10, ladder_games=100)]) searches = [newbie, pro] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 2
def test_combined_search_attributes(matchmaker_players): p1, p2, p3, _, _, _ = matchmaker_players search = CombinedSearch(Search([p1, p2]), Search([p3])) assert search.players == [p1, p2, p3] assert search.raw_ratings == [ p1.ratings[RatingType.LADDER_1V1], p2.ratings[RatingType.LADDER_1V1], p3.ratings[RatingType.LADDER_1V1] ]
def test_matchmaker_random_only(player_factory): newbie1 = Search([player_factory(1550, 500, ladder_games=1)]) newbie2 = Search([player_factory(200, 400, ladder_games=9)]) searches = (newbie1, newbie2) match_pairs = algorithm.make_matches(searches) match_sets = [set(pair) for pair in match_pairs] assert {newbie1, newbie2} in match_sets
def test_remove_isolated(player_factory): s1 = Search([player_factory(1500, 64, ladder_games=20)]) s2 = Search([player_factory(1500, 63, ladder_games=20)]) s3 = Search([player_factory(1600, 75, ladder_games=50)]) ranks = add_graph_edge_weights({s1: [s3], s2: [], s3: [s1]}) algorithm._MatchingGraph.remove_isolated(ranks) assert ranks == add_graph_edge_weights({s1: [s3], s3: [s1]})
def test_unmatched_newbies_do_not_forcefully_match_top_players(player_factory): newbie = Search([player_factory(1500, 500, ladder_games=0)]) top_player = Search([player_factory(2500, 10, ladder_games=100)]) top_player.register_failed_matching_attempt() searches = [newbie, top_player] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 0
def test_match_graph_will_not_include_matches_below_threshold_quality( player_factory, build_func): s1 = Search([player_factory(1500, 500)]) s2 = Search([player_factory(2000, 300)]) searches = [s1, s2] ranks = build_func(searches) assert ranks == {s1: [], s2: []}
def test_remove_isolated_2(player_factory): s1 = Search([player_factory(1500, 64, ladder_games=20)]) s2 = Search([player_factory(1500, 63, ladder_games=20)]) s3 = Search([player_factory(1600, 75, ladder_games=50)]) ranks = {s1: [], s2: [], s3: []} algorithm._MatchingGraph.remove_isolated(ranks) assert ranks == {}
def test_newbies_are_forcefully_matched_with_newbies(p): newbie1 = Search([p(0, 500, ladder_games=9)]) newbie2 = Search([p(1500, 500, ladder_games=9)]) pro = Search([p(1500, 10, ladder_games=100)]) searches = [newbie1, pro, newbie2] matches = algorithm.RandomlyMatchNewbies().find(searches) assert matches[newbie1] == newbie2 assert matches[newbie2] == newbie1
def unmatched_newbie_teams_do_not_forcefully_match_pros(p): newbie_team = Search( [p(1500, 500, ladder_games=0), p(1500, 500, ladder_games=0)]) pro = Search([p(1800, 10, ladder_games=100)]) searches = [newbie_team, pro] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 0
def test_odd_number_of_unmatched_newbies(p): newbie1 = Search([p(-250, 500, ladder_games=9)]) newbie2 = Search([p(750, 500, ladder_games=9)]) newbie3 = Search([p(1500, 500, ladder_games=9)]) pro = Search([p(1500, 10, ladder_games=100)]) searches = [newbie1, pro, newbie2, newbie3] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 4
def test_make_matches_will_not_match_low_quality_games(p): s1 = Search([p(100, 64, name='p1')]) s2 = Search([p(2000, 64, name='p2')]) searches = [s1, s2] matches = algorithm.make_matches(searches) assert (s1, s2) not in matches assert (s2, s1) not in matches
def test_make_matches_will_not_match_low_quality_games(player_factory): s1 = Search([player_factory(100, 64, name="p1")]) s2 = Search([player_factory(2000, 64, name="p2")]) searches = [s1, s2] matches = algorithm.make_matches(searches) assert (s1, s2) not in matches assert (s2, s1) not in matches
def command_game_matchmaking(self, message): mod = message.get('mod', 'ladder1v1') port = message.get('gameport', None) state = message['state'] if not self.able_to_launch_game: raise ClientError( "You are already in a game or are otherwise having connection problems. Please report this issue using HELP -> Tech support." ) if state == "stop": if self.search: self._logger.info("%s stopped searching for ladder: %s", self.player, self.search) self.search.cancel() return if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address( *message['relay_address']) if port: self.player.game_port = port with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute( "SELECT id FROM matchmaker_ban WHERE `userid` = %s", (self.player.id)) if cursor.rowcount > 0: self.sendJSON( dict( command="notice", style="error", text= "You are banned from the matchmaker. Contact an admin to have the reason." )) return if mod == "ladder1v1": if state == "start": if self.search: self.search.cancel() assert self.player is not None self.search = Search(self.player) self.player.faction = message['faction'] self.game_service.ladder_service.inform_player(self.player) self._logger.info("%s is searching for ladder: %s", self.player, self.search) asyncio.ensure_future( self.player_service.ladder_queue.search( self.player, search=self.search))
def unmatched_newbie_teams_do_not_forcefully_match_pros(player_factory): newbie_team = Search([ player_factory(1500, 500, ladder_games=0), player_factory(1500, 500, ladder_games=0) ]) pro = Search([player_factory(1800, 10, ladder_games=100)]) pro.register_failed_matching_attempt() searches = [newbie_team, pro] matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 0
def test_unmatched_newbies_forcefully_match_pros(player_factory): newbie = Search([player_factory(1500, 500, ladder_games=0)]) pro = Search([player_factory(1400, 10, ladder_games=100)]) searches = [newbie, pro] matches = algorithm.RandomlyMatchNewbies().find(searches) # No match if the pro is on their first attempt assert len(matches) == 0 pro.register_failed_matching_attempt() matches = algorithm.RandomlyMatchNewbies().find(searches) assert len(matches) == 2
def test_stable_marriage_unmatch(p): s1 = Search([p(503, 64, name='p1')]) s2 = Search([p(504, 64, name='p2')]) s3 = Search([p(504, 64, name='p3')]) s4 = Search([p(505, 64, name='p4')]) searches = [s1, s2, s3, s4] ranks = algorithm._MatchingGraph.build_full(searches) matches = algorithm.StableMarriage().find(ranks) assert matches[s1] == s4 # quality: 0.96622 assert matches[s2] == s3 # quality: 0.96623
def test_build_matching_graph_different_ranks(player_factory, build_func): s1 = Search([player_factory(1500, 64, ladder_games=20)]) s2 = Search([player_factory(200, 63, ladder_games=20)]) searches = [s1, s2] ranks = build_func(searches) empty_graph = add_graph_edge_weights({ s1: [], s2: [], }) assert ranks == empty_graph
def test_stable_marriage_unmatch(player_factory): s1 = Search([player_factory(503, 64, name="p1")]) s2 = Search([player_factory(504, 64, name="p2")]) s3 = Search([player_factory(504, 64, name="p3")]) s4 = Search([player_factory(505, 64, name="p4")]) searches = [s1, s2, s3, s4] ranks = algorithm._MatchingGraph.build_full(searches) matches = algorithm.StableMarriage().find(ranks) assert matches[s1] == s4 # quality: 0.96622 assert matches[s2] == s3 # quality: 0.96623
async def test_queue_cancel(matchmaker_queue, matchmaker_players): # Turn list of players into map from ids to players. s1, s2 = Search([matchmaker_players[1]]), Search([matchmaker_players[2]]) matchmaker_queue.push(s1) s1.cancel() try: await asyncio.wait_for(matchmaker_queue.search(s2), 0.01) except (TimeoutError, CancelledError): pass assert not s1.is_matched assert not s2.is_matched
def test_rank_all(): s1 = Search([p(1500, 500, num_games=0)]) s2 = Search([p(1500, 400, num_games=20)]) s3 = Search([p(2000, 300, num_games=50)]) searches = [s1, s2, s3] ranks = algorithm._rank_all(searches) assert ranks == { s1: [s3, s2], s2: [s1, s3], s3: [s1, s2] }
def test_queue_cancel(mocker, player_service, matchmaker_queue, matchmaker_players): # Turn list of players into map from ids to players. player_service.players = dict(map(lambda x: (x.id, x), list(matchmaker_players))) s1, s2 = Search(matchmaker_players[1]), Search(matchmaker_players[2]) matchmaker_queue.push(s1) s1.cancel() try: yield from asyncio.wait_for(matchmaker_queue.search(s2.player, search=s2), 0.01) except (TimeoutError, CancelledError): pass assert not s1.is_matched assert not s2.is_matched
def test_stable_marriage_matches_new_players_with_new_and_old_with_old_if_different_mean( player_factory): new1 = Search([player_factory(1500, 500, name="new1", ladder_games=1)]) new2 = Search([player_factory(1400, 500, name="new2", ladder_games=2)]) old1 = Search([player_factory(2300, 75, name="old1", ladder_games=100)]) old2 = Search([player_factory(2350, 75, name="old2", ladder_games=200)]) searches = [new1, new2, old1, old2] ranks = algorithm._MatchingGraph.build_full(searches) matches = algorithm.StableMarriage().find(ranks) assert matches[new1] == new2 assert matches[old1] == old2
def test_newbie_and_top_rated_team_not_matched_randomly(player_factory): newbie_and_top_rated = Search([ player_factory(0, 500, ladder_games=9), player_factory(2500, 10, ladder_games=1000) ]) newbie = Search([ player_factory(1500, 500, ladder_games=9), player_factory(1500, 500, ladder_games=9) ]) searches = [newbie_and_top_rated, newbie] matches = algorithm.RandomlyMatchNewbies().find(searches) assert not matches
async def test_queue_mid_cancel(mocker, player_service, matchmaker_queue, matchmaker_players_all_match): # Turn list of players into map from ids to players. player_service.players = dict(map(lambda x: (x.id, x), list(matchmaker_players_all_match))) matchmaker_queue.game_service.ladder_service.start_game = CoroMock() s1, s2, s3 = Search(matchmaker_players_all_match[1]), Search(matchmaker_players_all_match[2]), Search(matchmaker_players_all_match[3]) matchmaker_queue.push(s1) matchmaker_queue.push(s2) s1.cancel() try: await asyncio.wait_for(matchmaker_queue.search(s3.player, search=s3), 0.1) except CancelledError: pass assert not s1.is_matched assert s2.is_matched assert s3.is_matched
def command_game_matchmaking(self, message): mod = message.get('mod', 'ladder1v1') port = message.get('gameport', None) state = message['state'] if not self.able_to_launch_game: raise ClientError("You are already in a game or are otherwise having connection problems. Please report this issue using HELP -> Tech support.") if state == "stop": if self.search: self._logger.info("%s stopped searching for ladder: %s", self.player, self.search) self.search.cancel() return if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) if port: self.player.game_port = port with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("SELECT id FROM matchmaker_ban WHERE `userid` = %s", (self.player.id)) if cursor.rowcount > 0: self.sendJSON(dict(command="notice", style="error", text="You are banned from the matchmaker. Contact an admin to have the reason.")) return if mod == "ladder1v1": if state == "start": if self.search: self.search.cancel() assert self.player is not None self.search = Search(self.player) self.player.faction = message['faction'] self.game_service.ladder_service.inform_player(self.player) self._logger.info("%s is searching for ladder: %s", self.player, self.search) asyncio.ensure_future(self.player_service.ladder_queue.search(self.player, search=self.search))
class LobbyConnection: @timed() def __init__(self, loop, context=None, games: GameService=None, players: PlayerService=None, db=None): super(LobbyConnection, self).__init__() self.loop = loop self.db = db self.game_service = games self.player_service = players # type: PlayerService self.context = context self.ladderPotentialPlayers = [] self.warned = False self._authenticated = False self.player = None # type: Player self.game_connection = None # type: GameConnection self.connectivity = None # type: Connectivity self.leagueAvatar = None self.peer_address = None # type: Optional[Address] self.session = int(random.randrange(0, 4294967295)) self.protocol = None self._logger.debug("LobbyConnection initialized") self.search = None self.user_agent = None @property def authenticated(self): return self._authenticated @asyncio.coroutine def on_connection_made(self, protocol: QDataStreamProtocol, peername: Address): self.protocol = protocol self.peer_address = peername server.stats.incr("server.connections") def abort(self, logspam=""): if self.player: self._logger.warning("Client %s dropped. %s" % (self.player.login, logspam)) else: self._logger.warning("Aborting %s. %s" % (self.peer_address.host, logspam)) if self.game_connection: self.game_connection.abort() self._authenticated = False self.protocol.writer.close() def ensure_authenticated(self, cmd): if not self._authenticated: if cmd not in ['hello', 'ask_session', 'create_account', 'ping', 'pong']: self.abort("Message invalid for unauthenticated connection: %s" % cmd) return False return True async def on_message_received(self, message): """ Dispatches incoming messages """ try: cmd = message['command'] if not self.ensure_authenticated(cmd): return target = message.get('target') if target == 'game': if not self.game_connection: return await self.game_connection.handle_action(cmd, message.get('args', [])) return elif target == 'connectivity': if not self.connectivity: return await self.connectivity.on_message_received(message) return handler = getattr(self, 'command_{}'.format(cmd)) if asyncio.iscoroutinefunction(handler): await handler(message) else: handler(message) except AuthenticationError as ex: self.protocol.send_message( {'command': 'authentication_failed', 'text': ex.message} ) except ClientError as ex: self.protocol.send_message( {'command': 'notice', 'style': 'error', 'text': ex.message} ) if not ex.recoverable: self.abort(ex.message) except (KeyError, ValueError) as ex: self._logger.exception(ex) self.abort("Garbage command: {}".format(message)) except Exception as ex: self.protocol.send_message({'command': 'invalid'}) self._logger.exception(ex) self.abort("Error processing command") def command_ping(self, msg): self.protocol.send_raw(self.protocol.pack_message('PONG')) def command_pong(self, msg): pass @staticmethod def generate_expiring_request(lifetime, plaintext): """ Generate the parameters needed for an expiring email request with the given payload. Payload should be comma-delimited, and the consumer should expect to find and verify a timestamp and nonce appended to the given plaintext. """ # Add nonce rng = Random.new() nonce = ''.join(choice(string.ascii_uppercase + string.digits) for _ in range(256)) expiry = str(time.time() + lifetime) plaintext = (plaintext + "," + expiry + "," + nonce).encode('utf-8') # Pad the plaintext to the next full block with commas, because I can't be arsed to # write an actually clever parser. bs = Blowfish.block_size paddinglen = bs - (len(plaintext) % bs) plaintext += b',' * paddinglen # Generate random IV of size one block. iv = rng.read(bs) cipher = Blowfish.new(VERIFICATION_SECRET_KEY, Blowfish.MODE_CBC, iv) ciphertext = cipher.encrypt(plaintext) # Generate the verification hash. verification = hashlib.sha256() verification.update(plaintext + VERIFICATION_HASH_SECRET.encode('utf-8')) verify_hex = verification.hexdigest() return base64.urlsafe_b64encode(iv), base64.urlsafe_b64encode(ciphertext), verify_hex @asyncio.coroutine def command_create_account(self, message): raise ClientError("FAF no longer supports direct registration. Please use the website to register.", recoverable=True) @timed() def send_tutorial_section(self): reply = [] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() # Can probably replace two queries with one here if we're smart enough. yield from cursor.execute("SELECT `section`,`description` FROM `tutorial_sections`") for i in range(0, cursor.rowcount): section, description = yield from cursor.fetchone() reply.append( {"command": "tutorials_info", "section": section, "description": description}) yield from cursor.execute("SELECT tutorial_sections.`section`, `name`, `url`, `tutorials`.`description`, `map` FROM `tutorials` LEFT JOIN tutorial_sections ON tutorial_sections.id = tutorials.section ORDER BY `tutorials`.`section`, name") for i in range(0, cursor.rowcount): section, tutorial_name, url, description, map_name = yield from cursor.fetchone() reply.append({"command": "tutorials_info", "tutorial": tutorial_name, "url": url, "tutorial_section": section, "description": description, "mapname": map_name}) self.protocol.send_messages(reply) async def send_coop_maps(self): async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute("SELECT name, description, filename, type, id FROM `coop_map`") maps = [] for i in range(0, cursor.rowcount): name, description, filename, type, id = await cursor.fetchone() jsonToSend = {"command": "coop_info", "name": name, "description": description, "filename": filename, "featured_mod": "coop"} if type == 0: jsonToSend["type"] = "FA Campaign" elif type == 1: jsonToSend["type"] = "Aeon Vanilla Campaign" elif type == 2: jsonToSend["type"] = "Cybran Vanilla Campaign" elif type == 3: jsonToSend["type"] = "UEF Vanilla Campaign" elif type == 4: jsonToSend["type"] = "Custom Missions" else: # Don't sent corrupt data to the client... self._logger.error("Unknown coop type!") return jsonToSend["uid"] = id maps.append(jsonToSend) self.protocol.send_messages(maps) @timed def send_mod_list(self): self.protocol.send_messages(self.game_service.all_game_modes()) @timed() def send_game_list(self): self.sendJSON({ 'command': 'game_info', 'games': [game.to_dict() for game in self.game_service.open_games] }) @asyncio.coroutine def command_social_remove(self, message): if "friend" in message: target_id = message['friend'] elif "foe" in message: target_id = message['foe'] else: self.abort("No-op social_remove.") return with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("DELETE FROM friends_and_foes WHERE user_id = %s AND subject_id = %s", (self.player.id, target_id)) @timed() @asyncio.coroutine def command_social_add(self, message): if "friend" in message: status = "FRIEND" target_id = message['friend'] elif "foe" in message: status = "FOE" target_id = message['foe'] else: return with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("INSERT INTO friends_and_foes(user_id, subject_id, `status`) VALUES(%s, %s, %s)", (self.player.id, target_id, status)) def kick(self, message=None): self.sendJSON(dict(command="notice", style="kick")) if message: self.sendJSON(dict(command="notice", style="info", text=message)) self.abort() def send_updated_achievements(self, updated_achievements): self.sendJSON(dict(command="updated_achievements", updated_achievements=updated_achievements)) @asyncio.coroutine def command_admin(self, message): action = message['action'] if self.player.admin: if action == "closeFA": player = self.player_service[message['user_id']] if player: self._logger.warn('Administrative action: %s closed game for %s', self.player, player) player.lobby_connection.sendJSON(dict(command="notice", style="kill")) player.lobby_connection.sendJSON(dict(command="notice", style="info", text=("Your game was closed by an administrator ({admin_name}). " "Please refer to our rules for the lobby/game here {rule_link}." .format(admin_name=self.player.login, rule_link=config.RULE_LINK)))) elif action == "closelobby": player = self.player_service[message['user_id']] ban_fail = None if player: if 'ban' in message: reason = message['ban'].get('reason', 'Unspecified') duration = int(message['ban'].get('duration', 1)) period = message['ban'].get('period', 'DAY') self._logger.warn('Administrative action: %s closed client for %s with %s ban (Reason: %s)', self.player, player, duration, reason) with (yield from db.db_pool) as conn: try: cursor = yield from conn.cursor() yield from cursor.execute("SELECT reason from lobby_ban WHERE idUser=%s AND expires_at > NOW()", (message['user_id'])) if cursor.rowcount > 0: ban_fail = yield from cursor.fetchone() else: # XXX Interpolating the period into this is terrible and insecure - but the data comes from trusted users (admins) only yield from cursor.execute("INSERT INTO ban (player_id, author_id, reason, expires_at, level) VALUES (%s, %s, %s, DATE_ADD(NOW(), INTERVAL %s {}), 'GLOBAL')".format(period), (player.id, self.player.id, reason, duration)) except pymysql.MySQLError as e: raise ClientError('Your ban attempt upset the database: {}'.format(e)) else: self._logger.warn('Administrative action: %s closed client for %s', self.player, player) player.lobby_connection.kick( message=("You were kicked from FAF by an administrator ({admin_name}). " "Please refer to our rules for the lobby/game here {rule_link}." .format(admin_name=self.player.login, rule_link=config.RULE_LINK))) if ban_fail: raise ClientError("Kicked the player, but he was already banned!") elif action == "requestavatars": with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("SELECT url, tooltip FROM `avatars_list`") avatars = yield from cursor.fetchall() data = {"command": "admin", "avatarlist": []} for url, tooltip in avatars: data['avatarlist'].append({"url": url, "tooltip": tooltip}) self.sendJSON(data) elif action == "remove_avatar": idavatar = message["idavatar"] iduser = message["iduser"] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("DELETE FROM `avatars` " "WHERE `idUser` = %s " "AND `idAvatar` = %s", (iduser, idavatar)) elif action == "add_avatar": who = message['user'] avatar = message['avatar'] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() if avatar is None: yield from cursor.execute( "DELETE FROM `avatars` " "WHERE `idUser` = " "(SELECT `id` FROM `login` WHERE `login`.`login` = %s)", (who, )) else: yield from cursor.execute( "INSERT INTO `avatars`(`idUser`, `idAvatar`) " "VALUES ((SELECT id FROM login WHERE login.login = %s)," "(SELECT id FROM avatars_list WHERE avatars_list.url = %s)) " "ON DUPLICATE KEY UPDATE `idAvatar` = (SELECT id FROM avatars_list WHERE avatars_list.url = %s)", (who, avatar, avatar)) elif action == "broadcast": for player in self.player_service: try: if player.lobby_connection: player.lobby_connection.send_warning(message.get('message')) except Exception as ex: self._logger.debug("Could not send broadcast message to %s: %s".format(player, ex)) elif self.player.mod: if action == "join_channel": user_ids = message['user_ids'] channel = message['channel'] for user_id in user_ids: player = self.player_service[message[user_id]] if player: player.lobby_connection.sendJSON(dict(command="social", autojoin=[channel])) async def check_user_login(self, cursor, login, password): # TODO: Hash passwords server-side so the hashing actually *does* something. await cursor.execute("SELECT login.id as id," "login.login as username," "login.password as password," "login.steamid as steamid," "login.create_time as create_time," "lobby_ban.reason as reason," "lobby_ban.expires_at as expires_at " "FROM login " "LEFT JOIN lobby_ban ON login.id = lobby_ban.idUser " "WHERE LOWER(login)=%s " "ORDER BY expires_at DESC", (login.lower(), )) if cursor.rowcount < 1: raise AuthenticationError("Login not found or password incorrect. They are case sensitive.") player_id, real_username, dbPassword, steamid, create_time, ban_reason, ban_expiry = await cursor.fetchone() if dbPassword != password: raise AuthenticationError("Login not found or password incorrect. They are case sensitive.") now = datetime.datetime.now() if ban_reason is not None and now < ban_expiry: self._logger.debug('Rejected login from banned user: %s, %s, %s', player_id, login, self.session) raise ClientError("You are banned from FAF.\n Reason :\n {}".format(ban_reason), recoverable=False) self._logger.debug("Login from: %s, %s, %s", player_id, login, self.session) return player_id, real_username, steamid def check_version(self, message): versionDB, updateFile = self.player_service.client_version_info update_msg = dict(command="update", update=updateFile, new_version=versionDB) self.user_agent = message.get('user_agent') version = message.get('version') server.stats.gauge('user.agents.None', -1, delta=True) server.stats.gauge('user.agents.{}'.format(self.user_agent), 1, delta=True) if not version or not self.user_agent: update_msg['command'] = 'welcome' # For compatibility with 0.10.x updating mechanism self.sendJSON(update_msg) return False # Check their client is reporting the right version number. if 'downlords-faf-client' not in self.user_agent: try: if "-" in version: version = version.split('-')[0] if "+" in version: version = version.split('+')[0] if semver.compare(versionDB, version) > 0: self.sendJSON(update_msg) return False except ValueError: self.sendJSON(update_msg) return False return True async def check_policy_conformity(self, player_id, uid_hash, session): url = FAF_POLICY_SERVER_BASE_URL + '/verify' payload = dict(player_id=player_id, uid_hash=uid_hash, session=session) headers = { 'content-type': "application/json", 'cache-control': "no-cache" } response = requests.post(url, json=payload, headers=headers).json() if response.get('result', '') == 'vm': self._logger.debug("Using VM: %d: %s", player_id, uid_hash) self.sendJSON(dict(command="notice", style="error", text="You need to link your account to Steam in order to use FAF in a Virtual Machine. " "You can contact an admin on the forums.")) if response.get('result', '') == 'already_associated': self._logger.warning("UID hit: %d: %s", player_id, uid_hash) self.send_warning("Your computer is already associated with another FAF account.<br><br>In order to " "log in with a new account, you have to link it to Steam: <a href='" + config.WWW_URL + "/account/link'>" + config.WWW_URL + "/account/link</a>.<br>If you need an exception, please contact an " "admin on the forums", fatal=True) return False if response.get('result', '') == 'fraudulent': self._logger.info("Banning player %s for fraudulent looking login.", player_id) self.send_warning("Fraudulent login attempt detected. As a precautionary measure, your account has been " "banned permanently. Please contact a moderator if you feel this is a false positive.", fatal=True) with await db.db_pool as conn: try: cursor = await conn.cursor() await cursor.execute("INSERT INTO ban (player_id, author_id, reason, level) VALUES (%s, %s, %s, 'GLOBAL')", (player_id, player_id, "Auto-banned because of fraudulent login attempt")) except pymysql.MySQLError as e: raise ClientError('Banning failed: {}'.format(e)) return False return response.get('result', '') == 'honest' async def command_hello(self, message): login = message['login'].strip() password = message['password'] async with db.db_pool.get() as conn: cursor = await conn.cursor() player_id, login, steamid = await self.check_user_login(cursor, login, password) server.stats.incr('user.logins') server.stats.gauge('users.online', len(self.player_service)) await cursor.execute("UPDATE login SET ip = %(ip)s, user_agent = %(user_agent)s WHERE id = %(player_id)s", { "ip": self.peer_address.host, "user_agent": self.user_agent, "player_id": player_id }) if not self.player_service.is_uniqueid_exempt(player_id) and steamid is None: conforms_policy = await self.check_policy_conformity(player_id, message['unique_id'], self.session) if not conforms_policy: return # Update the user's IRC registration (why the f**k is this here?!) m = hashlib.md5() m.update(password.encode()) passwordmd5 = m.hexdigest() m = hashlib.md5() # Since the password is hashed on the client, what we get at this point is really # md5(md5(sha256(password))). This is entirely insane. m.update(passwordmd5.encode()) irc_pass = "******" + str(m.hexdigest()) try: await cursor.execute("UPDATE anope.anope_db_NickCore SET pass = %s WHERE display = %s", (irc_pass, login)) except (pymysql.OperationalError, pymysql.ProgrammingError): self._logger.error("Failure updating NickServ password for %s", login) permission_group = self.player_service.get_permission_group(player_id) self.player = Player(login=str(login), session=self.session, ip=self.peer_address.host, port=None, id=player_id, permissionGroup=permission_group, lobby_connection=self) self.connectivity = Connectivity(self, self.peer_address.host, self.player) if self.player.id in self.player_service and self.player_service[self.player.id].lobby_connection: old_conn = self.player_service[self.player.id].lobby_connection old_conn.send_warning("You have been signed out because you signed in elsewhere.", fatal=True) await self.player_service.fetch_player_data(self.player) self.player_service[self.player.id] = self.player self._authenticated = True # Country # ------- try: self.player.country = str(gi.country(self.peer_address.host).country.iso_code) except (geoip2.errors.AddressNotFoundError,ValueError): self.player.country = '' ## AVATARS ## ------------------- async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute( "SELECT url, tooltip FROM `avatars` " "LEFT JOIN `avatars_list` ON `idAvatar` = `avatars_list`.`id` " "WHERE `idUser` = %s AND `selected` = 1", (self.player.id, )) avatar = await cursor.fetchone() if avatar: url, tooltip = avatar self.player.avatar = {"url": url, "tooltip": tooltip} # Send the player their own player info. self.sendJSON({ "command": "welcome", "me": self.player.to_dict(), # For backwards compatibility for old clients. For now. "id": self.player.id, "login": login }) # Tell player about everybody online. This must happen after "welcome". self.sendJSON( { "command": "player_info", "players": [player.to_dict() for player in self.player_service] } ) # Tell everyone else online about us. This must happen after all the player_info messages. # This ensures that no other client will perform an operation that interacts with the # incoming user, allowing the client to make useful assumptions: it can be certain it has # initialised its local player service before it is going to get messages that want to # query it. self.player_service.mark_dirty(self.player) friends = [] foes = [] async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute("SELECT `subject_id`, `status` " "FROM friends_and_foes WHERE user_id = %s", (self.player.id,)) for target_id, status in await cursor.fetchall(): if status == "FRIEND": friends.append(target_id) else: foes.append(target_id) self.player.friends = set(friends) self.player.foes = set(foes) channels = [] if self.player.mod: channels.append("#moderators") if self.player.clan is not None: channels.append("#%s_clan" % self.player.clan) jsonToSend = {"command": "social", "autojoin": channels, "channels": channels, "friends": friends, "foes": foes, "power": permission_group} self.sendJSON(jsonToSend) self.send_mod_list() self.send_game_list() self.send_tutorial_section() @timed def command_ask_session(self, message): if self.check_version(message): self.sendJSON({ "command": "session", "session": self.session }) async def command_avatar(self, message): action = message['action'] if action == "list_avatar": avatarList = [] async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute( "SELECT url, tooltip FROM `avatars` " "LEFT JOIN `avatars_list` ON `idAvatar` = `avatars_list`.`id` WHERE `idUser` = %s", (self.player.id,)) avatars = await cursor.fetchall() for url, tooltip in avatars: avatar = {"url": url, "tooltip": tooltip} avatarList.append(avatar) if len(avatarList) > 0: self.sendJSON({"command": "avatar", "avatarlist": avatarList}) elif action == "select": avatar = message['avatar'] async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute( "UPDATE `avatars` SET `selected` = 0 WHERE `idUser` = %s", (self.player.id, )) if avatar is not None: await cursor.execute( "UPDATE `avatars` SET `selected` = 1 WHERE `idAvatar` =" "(SELECT id FROM avatars_list WHERE avatars_list.url = %s) and " "`idUser` = %s", (avatar, self.player.id)) else: raise KeyError('invalid action') @property def able_to_launch_game(self): return self.connectivity.result @timed def command_game_join(self, message): """ We are going to join a game. """ assert isinstance(self.player, Player) if not self.able_to_launch_game: raise ClientError("You are already in a game or haven't run the connectivity test yet") if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) uuid = message['uid'] port = message['gameport'] password = message.get('password', None) self._logger.debug("joining: %d:%d with pw: %s", uuid, port, password) try: game = self.game_service[uuid] if not game or game.state != GameState.LOBBY: self._logger.debug("Game not in lobby state: %s", game) self.sendJSON(dict(command="notice", style="info", text="The game you are trying to join is not ready.")) return if game.password != password: self.sendJSON(dict(command="notice", style="info", text="Bad password (it's case sensitive)")) return self.launch_game(game, port, False) except KeyError: self.sendJSON(dict(command="notice", style="info", text="The host has left the game")) @asyncio.coroutine def command_game_matchmaking(self, message): mod = message.get('mod', 'ladder1v1') port = message.get('gameport', None) state = message['state'] if not self.able_to_launch_game: raise ClientError("You are already in a game or are otherwise having connection problems. Please report this issue using HELP -> Tech support.") if state == "stop": if self.search: self._logger.info("%s stopped searching for ladder: %s", self.player, self.search) self.search.cancel() return if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) if port: self.player.game_port = port with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("SELECT id FROM matchmaker_ban WHERE `userid` = %s", (self.player.id)) if cursor.rowcount > 0: self.sendJSON(dict(command="notice", style="error", text="You are banned from the matchmaker. Contact an admin to have the reason.")) return if mod == "ladder1v1": if state == "start": if self.search: self.search.cancel() assert self.player is not None self.search = Search(self.player) self.player.faction = message['faction'] self.game_service.ladder_service.inform_player(self.player) self._logger.info("%s is searching for ladder: %s", self.player, self.search) asyncio.ensure_future(self.player_service.ladder_queue.search(self.player, search=self.search)) def command_coop_list(self, message): """ Request for coop map list""" asyncio.ensure_future(self.send_coop_maps()) @timed() def command_game_host(self, message): if not self.able_to_launch_game: raise ClientError("You are already in a game or haven't run the connectivity test yet") if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) assert isinstance(self.player, Player) title = cgi.escape(message.get('title', '')) port = message.get('gameport') visibility = VisibilityState.from_string(message.get('visibility')) if not isinstance(visibility, VisibilityState): # Protocol violation. self.abort("%s sent a nonsense visibility code: %s" % (self.player.login, message.get('visibility'))) return mod = message.get('mod') try: title.encode('ascii') except UnicodeEncodeError: self.sendJSON(dict(command="notice", style="error", text="Non-ascii characters in game name detected.")) return mapname = message.get('mapname') password = message.get('password') game = self.game_service.create_game(**{ 'visibility': visibility, 'game_mode': mod.lower(), 'host': self.player, 'name': title if title else self.player.login, 'mapname': mapname, 'password': password }) self.launch_game(game, port, True) server.stats.incr('game.hosted') def launch_game(self, game, port, is_host=False, use_map=None): # FIXME: Setting up a ridiculous amount of cyclic pointers here if self.game_connection: self.game_connection.abort("Player launched a new game") self.game_connection = GameConnection(self.loop, self, self.player_service, self.game_service) self.game_connection.player = self.player self.player.game_connection = self.game_connection self.game_connection.game = game if is_host: game.host = self.player self.player.state = PlayerState.HOSTING if is_host else PlayerState.JOINING self.player.game = game self.player.game_port = port cmd = {"command": "game_launch", "mod": game.game_mode, "uid": game.id, "args": ["/numgames " + str(self.player.numGames)]} if use_map: cmd['mapname'] = use_map self.sendJSON(cmd) @asyncio.coroutine def command_modvault(self, message): type = message["type"] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() if type == "start": yield from cursor.execute("SELECT uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon FROM table_mod ORDER BY likes DESC LIMIT 100") for i in range(0, cursor.rowcount): uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon = yield from cursor.fetchone() try: link = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/" + filename) thumbstr = "" if icon != "": thumbstr = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/mods_thumbs/" + urllib.parse.quote(icon)) out = dict(command="modvault_info", thumbnail=thumbstr, link=link, bugreports=[], comments=[], description=description, played=played, likes=likes, downloads=downloads, date=int(date.timestamp()), uid=uid, name=name, version=version, author=author, ui=ui) self.sendJSON(out) except: self._logger.error("Error handling table_mod row (uid: {})".format(uid), exc_info=True) pass elif type == "like": canLike = True uid = message['uid'] yield from cursor.execute("SELECT uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon, likers FROM `table_mod` WHERE uid = %s LIMIT 1", (uid,)) uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon, likerList = yield from cursor.fetchone() link = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/" + filename) thumbstr = "" if icon != "": thumbstr = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/mods_thumbs/" + urllib.parse.quote(icon)) out = dict(command="modvault_info", thumbnail=thumbstr, link=link, bugreports=[], comments=[], description=description, played=played, likes=likes + 1, downloads=downloads, date=int(date.timestamp()), uid=uid, name=name, version=version, author=author, ui=ui) try: likers = json.loads(likerList) if self.player.id in likers: canLike = False else: likers.append(self.player.id) except: likers = [] # TODO: Avoid sending all the mod info in the world just because we liked it? if canLike: yield from cursor.execute("UPDATE mod_stats s " "JOIN mod_version v ON v.mod_id = s.mod_id " "SET s.likes = s.likes + 1, likers=%s WHERE v.uid = %s", json.dumps(likers), uid) self.sendJSON(out) elif type == "download": uid = message["uid"] yield from cursor.execute("UPDATE mod_stats s " "JOIN mod_version v ON v.mod_id = s.mod_id " "SET downloads=downloads+1 WHERE v.uid = %s", uid) else: raise ValueError('invalid type argument') def send_warning(self, message: str, fatal: bool=False): """ Display a warning message to the client :param message: Warning message to display :param fatal: Whether or not the warning is fatal. If the client receives a fatal warning it should disconnect and not attempt to reconnect. :return: None """ self.sendJSON({'command': 'notice', 'style': 'info' if not fatal else 'error', 'text': message}) if fatal: self.abort(message) def send(self, message): """ :param message: :return: """ self._logger.debug(">>: %s", message) self.protocol.send_message(message) async def drain(self): await self.protocol.drain() def sendJSON(self, data_dictionary): """ Deprecated alias for send """ self.send(data_dictionary) async def on_connection_lost(self): async def nopdrain(message): return self.drain = nopdrain self.send = lambda m: None if self.game_connection: await self.game_connection.on_connection_lost() if self.search and not self.search.done(): self.search.cancel() if self.player: self.player_service.remove_player(self.player)
def test_search_no_match(mocker, loop, matchmaker_players): p1, p2, _, _, _ = matchmaker_players s1, s2 = Search(p1), Search(p2) assert not s1.matches_with(s2)
def test_search_match(mocker, loop, matchmaker_players): p1, _, _, p4, _ = matchmaker_players s1, s4 = Search(p1), Search(p4) assert s1.matches_with(s4)
class LobbyConnection: @timed() def __init__(self, loop, context=None, games: GameService=None, players: PlayerService=None, db=None): super(LobbyConnection, self).__init__() self.loop = loop self.db = db self.game_service = games self.player_service = players # type: PlayerService self.context = context self.ladderPotentialPlayers = [] self.warned = False self._authenticated = False self.player = None # type: Player self.game_connection = None # type: GameConnection self.connectivity = None # type: Connectivity self.leagueAvatar = None self.peer_address = None # type: Optional[Address] self.session = int(random.randrange(0, 4294967295)) self.protocol = None self._logger.debug("LobbyConnection initialized") self.search = None @property def authenticated(self): return self._authenticated @asyncio.coroutine def on_connection_made(self, protocol: QDataStreamProtocol, peername: Address): self.protocol = protocol self.peer_address = peername server.stats.incr("server.connections") def abort(self, logspam=""): if self.player: self._logger.warning("Client %s dropped. %s" % (self.player.login, logspam)) else: self._logger.warning("Aborting %s. %s" % (self.peer_address.host, logspam)) self._authenticated = False self.protocol.writer.close() def ensure_authenticated(self, cmd): if not self._authenticated: if cmd not in ['hello', 'ask_session', 'create_account', 'ping', 'pong']: self.abort("Message invalid for unauthenticated connection: %s" % cmd) return False return True async def on_message_received(self, message): """ Dispatches incoming messages """ try: cmd = message['command'] if not self.ensure_authenticated(cmd): return target = message.get('target') if target == 'game': if not self.game_connection: return await self.game_connection.handle_action(cmd, message.get('args', [])) return elif target == 'connectivity': if not self.connectivity: return await self.connectivity.on_message_received(message) return handler = getattr(self, 'command_{}'.format(cmd)) if asyncio.iscoroutinefunction(handler): await handler(message) else: handler(message) except AuthenticationError as ex: self.protocol.send_message( {'command': 'authentication_failed', 'text': ex.message} ) except ClientError as ex: self.protocol.send_message( {'command': 'notice', 'style': 'error', 'text': ex.message} ) if not ex.recoverable: self.abort(ex.message) except (KeyError, ValueError) as ex: self._logger.exception(ex) self.abort("Garbage command: {}".format(message)) except Exception as ex: self.protocol.send_message({'command': 'invalid'}) self._logger.exception(ex) self.abort("Error processing command") def command_ping(self, msg): self.protocol.send_raw(self.protocol.pack_message('PONG')) def command_pong(self, msg): pass @staticmethod def generate_expiring_request(lifetime, plaintext): """ Generate the parameters needed for an expiring email request with the given payload. Payload should be comma-delimited, and the consumer should expect to find and verify a timestamp and nonce appended to the given plaintext. """ # Add nonce rng = Random.new() nonce = ''.join(choice(string.ascii_uppercase + string.digits) for _ in range(256)) expiry = str(time.time() + lifetime) plaintext = (plaintext + "," + expiry + "," + nonce).encode('utf-8') # Pad the plaintext to the next full block with commas, because I can't be arsed to # write an actually clever parser. bs = Blowfish.block_size paddinglen = bs - (len(plaintext) % bs) plaintext += b',' * paddinglen # Generate random IV of size one block. iv = rng.read(bs) cipher = Blowfish.new(VERIFICATION_SECRET_KEY, Blowfish.MODE_CBC, iv) ciphertext = cipher.encrypt(plaintext) # Generate the verification hash. verification = hashlib.sha256() verification.update(plaintext + VERIFICATION_HASH_SECRET.encode('utf-8')) verify_hex = verification.hexdigest() return base64.urlsafe_b64encode(iv), base64.urlsafe_b64encode(ciphertext), verify_hex @asyncio.coroutine def command_create_account(self, message): login = message['login'] user_email = message['email'] password = message['password'] username_pattern = re.compile(r"^[^,]{1,20}$") email_pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$") def reply_no(error_msg): self.sendJSON({ "command": "registration_response", "result": "FAILURE", "error": error_msg }) if not email_pattern.match(user_email): reply_no("Please use a valid email address.") return if not username_pattern.match(login): reply_no("Please don't use \",\" in your username.") return with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("SELECT id FROM `login` WHERE LOWER(`login`) = %s", (login.lower(),)) if cursor.rowcount: reply_no("Sorry, that username is not available.") return if self.player_service.has_blacklisted_domain(user_email): # We don't like disposable emails. text = "Dear " + login + ",\n\n\ Please use a non-disposable email address.\n\n" yield from self.send_email(text, login, user_email, 'Forged Alliance Forever - Account validation') return # We want the user to validate their email address before we create their account. # # We want to email them a link to click which will lead to their account being # created, but without storing any data on the server in the meantime. # # This is done by sending a link of the form: # *.php?data=E(username+password+email+expiry+nonce, K)&token=$VERIFICATION_CODE # where E(P, K) is a symmetric encryption function with plaintext P and secret key K, # and # VERIFICATION_CODE = sha256(username + password + email + expiry + K + nonce) # # The receiving php script decrypts `data`, verifies it (username still free? etc.), # recalculates the verification code, and creates the account if it matches up. # # As AES is not readily available for both Python and PHP, Blowfish is used. # # We thus avoid a SYN-flood-like attack on the registration system. iv, ciphertext, verification_hex = self.generate_expiring_request(3600 * 25, login + "," + password + "," + user_email) link = {'a': 'v', 'iv': iv, 'c': ciphertext, 'v': verification_hex} passwordLink = urllib.parse.urljoin(config.APP_URL, "faf/validateAccount.php?" + urllib.parse.urlencode(link)) text = "Dear " + login + ",\n\n\ Please visit the following link to validate your FAF account:\n\ -----------------------\n\ " + passwordLink + "\n\ -----------------------\n\n\ Thanks,\n\ -- The FA Forever team" yield from self.send_email(text, login, user_email, 'Forged Alliance Forever - Account validation') self.sendJSON(dict(command="notice", style="info", text="A e-mail has been sent with the instructions to validate your account")) self._logger.debug("Sent mail") self.sendJSON(dict(command="registration_response", result="SUCCESS")) async def send_email(self, text, to_name, to_email, subject): msg = MIMEText(text) msg['Subject'] = subject msg['From'] = email.utils.formataddr(('Forged Alliance Forever', "*****@*****.**")) msg['To'] = email.utils.formataddr((to_name, to_email)) self._logger.debug("Sending mail to " + to_email) url = config.MANDRILL_API_URL + "/messages/send-raw.json" headers = {'content-type': 'application/json'} resp = await aiohttp.post(url, data=json.dumps({ "key": config.MANDRILL_API_KEY, "raw_message": msg.as_string(), "from_email": '*****@*****.**', "from_name": "Forged Alliance Forever", "to": [ to_email ], "async": False }), headers=headers) resp_text = await resp.text() self._logger.info("Mandrill response: {}".format(resp_text)) @timed() def send_tutorial_section(self): reply = [] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() # Can probably replace two queries with one here if we're smart enough. yield from cursor.execute("SELECT `section`,`description` FROM `tutorial_sections`") for i in range(0, cursor.rowcount): section, description = yield from cursor.fetchone() reply.append( {"command": "tutorials_info", "section": section, "description": description}) yield from cursor.execute("SELECT tutorial_sections.`section`, `name`, `url`, `tutorials`.`description`, `map` FROM `tutorials` LEFT JOIN tutorial_sections ON tutorial_sections.id = tutorials.section ORDER BY `tutorials`.`section`, name") for i in range(0, cursor.rowcount): section, tutorial_name, url, description, map_name = yield from cursor.fetchone() reply.append({"command": "tutorials_info", "tutorial": tutorial_name, "url": url, "tutorial_section": section, "description": description, "mapname": map_name}) self.protocol.send_messages(reply) async def send_coop_maps(self): async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute("SELECT name, description, filename, type, id FROM `coop_map`") maps = [] for i in range(0, cursor.rowcount): name, description, filename, type, id = await cursor.fetchone() jsonToSend = {"command": "coop_info", "name": name, "description": description, "filename": filename, "featured_mod": "coop"} if type == 0: jsonToSend["type"] = "FA Campaign" elif type == 1: jsonToSend["type"] = "Aeon Vanilla Campaign" elif type == 2: jsonToSend["type"] = "Cybran Vanilla Campaign" elif type == 3: jsonToSend["type"] = "UEF Vanilla Campaign" elif type == 4: jsonToSend["type"] = "Custom Missions" else: # Don't sent corrupt data to the client... self._logger.error("Unknown coop type!") return jsonToSend["uid"] = id maps.append(jsonToSend) self.protocol.send_messages(maps) @timed def send_mod_list(self): self.protocol.send_messages(self.game_service.all_game_modes()) @timed() def send_game_list(self): self.sendJSON({ 'command': 'game_info', 'games': [game.to_dict() for game in self.game_service.open_games] }) @asyncio.coroutine def command_social_remove(self, message): if "friend" in message: target_id = message['friend'] elif "foe" in message: target_id = message['foe'] else: self.abort("No-op social_remove.") return with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("DELETE FROM friends_and_foes WHERE user_id = %s AND subject_id = %s", (self.player.id, target_id)) @timed() @asyncio.coroutine def command_social_add(self, message): if "friend" in message: status = "FRIEND" target_id = message['friend'] elif "foe" in message: status = "FOE" target_id = message['foe'] else: return with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("INSERT INTO friends_and_foes(user_id, subject_id, `status`) VALUES(%s, %s, %s)", (self.player.id, target_id, status)) def kick(self, message=None): self.sendJSON(dict(command="notice", style="kick")) if message: self.sendJSON(dict(command="notice", style="info", text=message)) self.abort() def send_updated_achievements(self, updated_achievements): self.sendJSON(dict(command="updated_achievements", updated_achievements=updated_achievements)) @asyncio.coroutine def command_admin(self, message): action = message['action'] if self.player.admin: if action == "closeFA": player = self.player_service[message['user_id']] if player: self._logger.info('Administrative action: {} closed game for {}'.format(self.player, player)) player.lobby_connection.sendJSON(dict(command="notice", style="kill")) player.lobby_connection.sendJSON(dict(command="notice", style="info", text=("Your game was closed by an administrator ({admin_name}). " "Please refer to our rules for the lobby/game here {rule_link}." .format(admin_name=self.player.login, rule_link=config.RULE_LINK)))) elif action == "closelobby": player = self.player_service[message['user_id']] if player: self._logger.info('Administrative action: {} closed client for {}'.format(self.player, player)) player.lobby_connection.kick( message=("Your client was closed by an administrator ({admin_name}). " "Please refer to our rules for the lobby/game here {rule_link}." .format(admin_name=self.player.login, rule_link=config.RULE_LINK))) elif action == "requestavatars": with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("SELECT url, tooltip FROM `avatars_list`") avatars = yield from cursor.fetchall() data = {"command": "admin", "avatarlist": []} for url, tooltip in avatars: data['avatarlist'].append({"url": url, "tooltip": tooltip}) self.sendJSON(data) elif action == "remove_avatar": idavatar = message["idavatar"] iduser = message["iduser"] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("DELETE FROM `avatars` " "WHERE `idUser` = %s " "AND `idAvatar` = %s", (idavatar, iduser)) elif action == "add_avatar": who = message['user'] avatar = message['avatar'] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() if avatar is None: yield from cursor.execute( "DELETE FROM `avatars` " "WHERE `idUser` = " "(SELECT `id` FROM `login` WHERE `login`.`login` = %s)", (who, )) else: yield from cursor.execute( "INSERT INTO `avatars`(`idUser`, `idAvatar`) " "VALUES ((SELECT id FROM login WHERE login.login = %s)," "(SELECT id FROM avatars_list WHERE avatars_list.url = %s)) " "ON DUPLICATE KEY UPDATE `idAvatar` = (SELECT id FROM avatars_list WHERE avatars_list.url = %s)", (who, avatar, avatar)) elif self.player.mod: if action == "join_channel": user_ids = message['user_ids'] channel = message['channel'] for user_id in user_ids: player = self.player_service[message[user_id]] if player: player.lobby_connection.sendJSON(dict(command="social", autojoin=[channel])) async def check_user_login(self, cursor, login, password): # TODO: Hash passwords server-side so the hashing actually *does* something. await cursor.execute("SELECT login.id as id," "login.login as username," "login.password as password," "login.steamid as steamid," "lobby_ban.reason as reason," "lobby_ban.expires_at as expires_at " "FROM login " "LEFT JOIN lobby_ban ON login.id = lobby_ban.idUser " "WHERE LOWER(login)=%s", (login.lower(), )) if cursor.rowcount != 1: raise AuthenticationError("Login not found or password incorrect. They are case sensitive.") player_id, real_username, dbPassword, steamid, ban_reason, ban_expiry = await cursor.fetchone() if dbPassword != password: raise AuthenticationError("Login not found or password incorrect. They are case sensitive.") if ban_reason is not None and datetime.datetime.now() < ban_expiry: raise ClientError("You are banned from FAF.\n Reason :\n {}".format(ban_reason)) self._logger.debug("Login from: {}, {}, {}".format(player_id, login, self.session)) return player_id, real_username, steamid def decodeUniqueId(self, serialized_uniqueid): try: message = (base64.b64decode(serialized_uniqueid)).decode('utf-8') trailing = ord(message[0]) message = message[1:] iv = (base64.b64decode(message[:24])) encoded = message[24:-40] key = (base64.b64decode(message[-40:])) # The JSON string is AES encrypted # first decrypt the AES key with our rsa private key AESkey = rsa.decrypt(key, PRIVATE_KEY) # now decrypt the message cipher = AES.new(AESkey, AES.MODE_CBC, iv) DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).decode('utf-8') decoded = DecodeAES(cipher, encoded)[:-trailing] # since the legacy uid.dll generated JSON is flawed, # there's a new JSON format, starting with '2' as magic byte if decoded.startswith('2'): data = json.loads(decoded[1:]) if str(data['session']) != str(self.session) : self.sendJSON(dict(command="notice", style="error", text="Your session is corrupted. Try relogging")) return None # We're bound to generate to _old_ hashes from the new JSON structure, # so we still use hashlib.md5().update() to generate the MD5 hash from concatenated bytearrays. # Therefore all needed JSON elements are converted to strings and encoded to bytearrays. UUID = str(data['machine']['uuid']).encode() mem_SerialNumber = str(data['machine']['memory']['serial0']).encode() DeviceID = str(data['machine']['disks']['controller_id']).encode() Manufacturer = str(data['machine']['bios']['manufacturer']).encode() Name = str(data['machine']['processor']['name']).encode() ProcessorId = str(data['machine']['processor']['id']).encode() SMBIOSBIOSVersion = str(data['machine']['bios']['smbbversion']).encode() SerialNumber = str(data['machine']['bios']['serial']).encode() VolumeSerialNumber = str(data['machine']['disks']['vserial']).encode() else: # the old JSON format contains unescaped backspaces in the device id # of the IDE controller, which now needs to be corrected to get valid JSON regexp = re.compile(r'[0-9a-zA-Z\\]("")') decoded = regexp.sub('"', decoded) decoded = decoded.replace("\\", "\\\\") regexp = re.compile('[^\x09\x0A\x0D\x20-\x7F]') decoded = regexp.sub('', decoded) jstring = json.loads(decoded) if str(jstring["session"]) != str(self.session) : self.sendJSON(dict(command="notice", style="error", text="Your session is corrupted. Try relogging")) return None machine = jstring["machine"] UUID = str(machine.get('UUID', 0)).encode() mem_SerialNumber = str(machine.get('mem_SerialNumber', 0)).encode() # serial number of first memory module DeviceID = str(machine.get('DeviceID', 0)).encode() # device id of the IDE controller Manufacturer = str(machine.get('Manufacturer', 0)).encode() # BIOS manufacturer Name = str(machine.get('Name', 0)).encode() # verbose processor name ProcessorId = str(machine.get('ProcessorId', 0)).encode() SMBIOSBIOSVersion = str(machine.get('SMBIOSBIOSVersion', 0)).encode() SerialNumber = str(machine.get('SerialNumber', 0)).encode() # BIOS serial number VolumeSerialNumber = str(machine.get('VolumeSerialNumber', 0)).encode() # https://www.raymond.cc/blog/changing-or-spoofing-hard-disk-hardware-serial-number-and-volume-id/ for i in machine.values() : low = i.lower() if "vmware" in low or "virtual" in low or "innotek" in low or "qemu" in low or "parallels" in low or "bochs" in low : return "VM" m = hashlib.md5() m.update(UUID + mem_SerialNumber + DeviceID + Manufacturer + Name + ProcessorId + SMBIOSBIOSVersion + SerialNumber + VolumeSerialNumber) return m.hexdigest(), (UUID, mem_SerialNumber, DeviceID, Manufacturer, Name, ProcessorId, SMBIOSBIOSVersion, SerialNumber, VolumeSerialNumber) except Exception as ex: self._logger.exception(ex) async def validate_unique_id(self, cursor, player_id, steamid, encoded_unique_id): # Accounts linked to steam are exempt from uniqueId checking. if steamid: return True uid_hash, hardware_info = self.decodeUniqueId(encoded_unique_id) # VM users must use steam. if uid_hash == "VM": self.sendJSON(dict(command="notice", style="error", text="You need to link your account to Steam in order to use FAF in a Virtual Machine. You can contact an admin on the forums.")) return False # check for other accounts using the same uniqueId as us. await cursor.execute("SELECT user_id FROM unique_id_users WHERE uniqueid_hash = %s", (uid_hash, )) users = [] for id, in await cursor.fetchall(): users.append(id) # Is the user we're logging in with not currently associated with this uid? if player_id not in users: # Do we have a spare slot into which we can allocate this new account? if len(users) > 1: #self.sendJSON(dict(command="notice", style="error", # text="This computer is already associated with too many FAF accounts.<br><br>You might want to try linking your account with Steam: <a href='" + # config.APP_URL + "/faf/steam.php'>" + # config.APP_URL + "/faf/steam.php</a>")) self._logger.warning("UID hit: {}: {}".format(player_id, uid_hash)) # Is this a uuid we have never seen before? if len(users) == 0: # Store its component parts in the table for doing that sort of thing. (just for # human-reading, really) try: await cursor.execute("INSERT INTO `uniqueid` (`hash`, `uuid`, `mem_SerialNumber`, `deviceID`, `manufacturer`, `name`, `processorId`, `SMBIOSBIOSVersion`, `serialNumber`, `volumeSerialNumber`)" "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", (uid_hash, *hardware_info)) except Exception as e: self._logger.warning("UID dupe: {}: {}".format(player_id, uid_hash)) # Associate this account with this hardware hash. try: await cursor.execute("INSERT INTO unique_id_users(user_id, uniqueid_hash) VALUES(%s, %s)", (player_id, uid_hash)) except Exception as e: self._logger.warning("UID association dupe: {}: {}".format(player_id, uid_hash)) # TODO: Mildly unpleasant await cursor.execute("UPDATE login SET ip = %s WHERE id = %s", (self.peer_address.host, player_id)) return True def check_version(self, message): versionDB, updateFile = self.player_service.client_version_info update_msg = dict(command="update", update=updateFile, new_version=versionDB) if 'version' not in message or 'user_agent' not in message: update_msg['command'] = 'welcome' # For compatibility with 0.10.x updating mechanism self.sendJSON(update_msg) return False version = message.get('version') # Check their client is reporting the right version number. if message.get('user_agent', None) != 'downlords-faf-client': try: if "-" in version: version = version.split('-')[0] if "+" in version: version = version.split('+')[0] if semver.compare(versionDB, version) > 0: self.sendJSON(update_msg) return False except ValueError: self.sendJSON(update_msg) return False return True async def command_hello(self, message): login = message['login'].strip() password = message['password'] # Check their client is reporting the right version number. async with db.db_pool.get() as conn: cursor = await conn.cursor() player_id, login, steamid = await self.check_user_login(cursor, login, password) server.stats.incr('user.logins') server.stats.gauge('users.online', len(self.player_service)) if not self.player_service.is_uniqueid_exempt(player_id): # UniqueID check was rejected (too many accounts or tamper-evident madness) uniqueid_pass = await self.validate_unique_id(cursor, player_id, steamid, message['unique_id']) if not uniqueid_pass: return # Update the user's IRC registration (why the f**k is this here?!) m = hashlib.md5() m.update(password.encode()) passwordmd5 = m.hexdigest() m = hashlib.md5() # Since the password is hashed on the client, what we get at this point is really # md5(md5(sha256(password))). This is entirely insane. m.update(passwordmd5.encode()) irc_pass = "******" + str(m.hexdigest()) try: await cursor.execute("UPDATE anope.anope_db_NickCore SET pass = %s WHERE display = %s", (irc_pass, login)) except (pymysql.OperationalError, pymysql.ProgrammingError): self._logger.info("Failure updating NickServ password for {}".format(login)) permission_group = self.player_service.get_permission_group(player_id) self.player = Player(login=str(login), session=self.session, ip=self.peer_address.host, port=None, id=player_id, permissionGroup=permission_group, lobby_connection=self) self.connectivity = Connectivity(self, self.peer_address.host, self.player) if self.player.id in self.player_service and self.player_service[self.player.id].lobby_connection: old_conn = self.player_service[self.player.id].lobby_connection old_conn.send_warning("You have been signed out because you signed in elsewhere.", fatal=True) await self.player_service.fetch_player_data(self.player) self.player_service[self.player.id] = self.player self._authenticated = True # Country # ------- country = gi.country_code_by_addr(self.peer_address.host) if country is not None: self.player.country = str(country) ## AVATARS ## ------------------- async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute( "SELECT url, tooltip FROM `avatars` " "LEFT JOIN `avatars_list` ON `idAvatar` = `avatars_list`.`id` " "WHERE `idUser` = %s AND `selected` = 1", (self.player.id, )) avatar = await cursor.fetchone() if avatar: url, tooltip = avatar self.player.avatar = {"url": url, "tooltip": tooltip} self.sendJSON(dict(command="welcome", id=self.player.id, login=login)) # Tell player about everybody online self.sendJSON( { "command": "player_info", "players": [player.to_dict() for player in self.player_service] } ) # Tell everyone else online about us self.player_service.mark_dirty(self.player) friends = [] foes = [] async with db.db_pool.get() as conn: cursor = await conn.cursor() await cursor.execute("SELECT `subject_id`, `status` " "FROM friends_and_foes WHERE user_id = %s", (self.player.id,)) for target_id, status in await cursor.fetchall(): if status == "FRIEND": friends.append(target_id) else: foes.append(target_id) self.player.friends = set(friends) self.player.foes = set(foes) self.send_mod_list() self.send_game_list() self.send_tutorial_section() channels = [] if self.player.mod: channels.append("#moderators") if self.player.clan is not None: channels.append("#%s_clan" % self.player.clan) jsonToSend = {"command": "social", "autojoin": channels, "channels": channels, "friends": friends, "foes": foes, "power": permission_group} self.sendJSON(jsonToSend) @timed def command_ask_session(self, message): if self.check_version(message): self.sendJSON({ "command": "session", "session": self.session }) @asyncio.coroutine def command_avatar(self, message): action = message['action'] if action == "list_avatar": avatarList = [] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute( "SELECT url, tooltip FROM `avatars` " "LEFT JOIN `avatars_list` ON `idAvatar` = `avatars_list`.`id` WHERE `idUser` = %s", (self.player.id, )) avatars = yield from cursor.fetchall() for url, tooltip in avatars: avatar = {"url": url, "tooltip": tooltip} avatarList.append(avatar) if len(avatarList) > 0: jsonToSend = {"command": "avatar", "avatarlist": avatarList} self.sendJSON(jsonToSend) elif action == "select": avatar = message['avatar'] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute( "UPDATE `avatars` SET `selected` = 0 WHERE `idUser` = %s", (self.player.id, )) if avatar is not None: yield from cursor.execute( "UPDATE `avatars` SET `selected` = 1 WHERE `idAvatar` =" "(SELECT id FROM avatars_list WHERE avatars_list.url = %s) and " "`idUser` = %s", (avatar, self.player.id)) else: raise KeyError('invalid action') @property def able_to_launch_game(self): return self.connectivity.result @timed def command_game_join(self, message): """ We are going to join a game. """ assert isinstance(self.player, Player) if not self.able_to_launch_game: raise ClientError("You are already in a game or haven't run the connectivity test yet") if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) uuid = message['uid'] port = message['gameport'] password = message.get('password', None) self._logger.debug("joining: {}:{} with pw: {}".format(uuid, port, password)) try: game = self.game_service[uuid] if not game or game.state != GameState.LOBBY: self._logger.debug("Game not in lobby state: {}".format(game)) self.sendJSON(dict(command="notice", style="info", text="The game you are trying to join is not ready.")) return if game.password != password: self.sendJSON(dict(command="notice", style="info", text="Bad password (it's case sensitive)")) return self.launch_game(game, port, False) except KeyError: self.sendJSON(dict(command="notice", style="info", text="The host has left the game")) @asyncio.coroutine def command_game_matchmaking(self, message): mod = message.get('mod', 'ladder1v1') port = message.get('gameport', None) state = message['state'] if not self.able_to_launch_game: raise ClientError("You are already in a game or are otherwise having connection problems. Please report this issue using HELP -> Tech support.") if state == "stop": if self.search: self._logger.info("{} stopped searching for ladder: {}".format(self.player, self.search)) self.search.cancel() return if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) if port: self.player.game_port = port with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() yield from cursor.execute("SELECT id FROM matchmaker_ban WHERE `userid` = %s", (self.player.id)) if cursor.rowcount > 0: self.sendJSON(dict(command="notice", style="error", text="You are banned from the matchmaker. Contact an admin to have the reason.")) return if mod == "ladder1v1": if state == "start": if self.search: self.search.cancel() assert self.player is not None self.search = Search(self.player) self.player.faction = message['faction'] self.game_service.ladder_service.inform_player(self.player) self._logger.info("{} is searching for ladder: {}".format(self.player, self.search)) asyncio.ensure_future(self.player_service.ladder_queue.search(self.player, search=self.search)) def command_coop_list(self, message): """ Request for coop map list""" asyncio.ensure_future(self.send_coop_maps()) @timed() def command_game_host(self, message): if not self.able_to_launch_game: raise ClientError("You are already in a game or haven't run the connectivity test yet") if self.connectivity.result.state == ConnectivityState.STUN: self.connectivity.relay_address = Address(*message['relay_address']) assert isinstance(self.player, Player) title = cgi.escape(message.get('title', '')) port = message.get('gameport') visibility = VisibilityState.from_string(message.get('visibility')) if not isinstance(visibility, VisibilityState): # Protocol violation. self.abort("%s sent a nonsense visibility code: %s" % (self.player.login, message.get('visibility'))) return mod = message.get('mod') try: title.encode('ascii') except UnicodeEncodeError: self.sendJSON(dict(command="notice", style="error", text="Non-ascii characters in game name detected.")) return mapname = message.get('mapname') password = message.get('password') game = self.game_service.create_game(**{ 'visibility': VisibilityState.to_string(visibility), 'game_mode': mod.lower(), 'host': self.player, 'name': title if title else self.player.login, 'mapname': mapname, 'password': password }) self.launch_game(game, port, True) server.stats.incr('game.hosted') def launch_game(self, game, port, is_host=False, use_map=None): # FIXME: Setting up a ridiculous amount of cyclic pointers here if self.game_connection: self.game_connection.abort("Player launched a new game") self.game_connection = GameConnection(self.loop, self, self.player_service, self.game_service) self.game_connection.player = self.player self.player.game_connection = self.game_connection self.game_connection.game = game if is_host: game.host = self.player self.player.state = PlayerState.HOSTING if is_host else PlayerState.JOINING self.player.game = game self.player.game_port = port cmd = {"command": "game_launch", "mod": game.game_mode, "uid": game.id, "args": ["/numgames " + str(self.player.numGames)]} if use_map: cmd['mapname'] = use_map self.sendJSON(cmd) @asyncio.coroutine def command_modvault(self, message): type = message["type"] with (yield from db.db_pool) as conn: cursor = yield from conn.cursor() if type == "start": yield from cursor.execute("SELECT uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon FROM table_mod ORDER BY likes DESC LIMIT 100") for i in range(0, cursor.rowcount): uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon = yield from cursor.fetchone() link = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/" + filename) thumbstr = "" if icon != "": thumbstr = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/mods_thumbs/" + urllib.parse.quote(icon)) out = dict(command="modvault_info", thumbnail=thumbstr, link=link, bugreports=[], comments=[], description=description, played=played, likes=likes, downloads=downloads, date=int(date.timestamp()), uid=uid, name=name, version=version, author=author, ui=ui) self.sendJSON(out) elif type == "like": canLike = True uid = message['uid'] yield from cursor.execute("SELECT uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon, likers FROM `table_mod` WHERE uid = %s LIMIT 1", (uid,)) uid, name, version, author, ui, date, downloads, likes, played, description, filename, icon, likerList = yield from cursor.fetchone() link = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/" + filename) thumbstr = "" if icon != "": thumbstr = urllib.parse.urljoin(config.CONTENT_URL, "faf/vault/mods_thumbs/" + urllib.parse.quote(icon)) out = dict(command="modvault_info", thumbnail=thumbstr, link=link, bugreports=[], comments=[], description=description, played=played, likes=likes + 1, downloads=downloads, date=int(date.timestamp()), uid=uid, name=name, version=version, author=author, ui=ui) try: likers = json.loads(likerList) if self.player.id in likers: canLike = False else: likers.append(self.player.id) except: likers = [] # TODO: Avoid sending all the mod info in the world just because we liked it? if canLike: yield from cursor.execute("UPDATE `table_mod` SET likes=likes+1, likers=%s WHERE uid = %s", json.dumps(likers), uid) self.sendJSON(out) elif type == "download": uid = message["uid"] yield from cursor.execute("UPDATE `table_mod` SET downloads=downloads+1 WHERE uid = %s", uid) else: raise ValueError('invalid type argument') def send_warning(self, message: str, fatal: bool=False): """ Display a warning message to the client :param message: Warning message to display :param fatal: Whether or not the warning is fatal. If the client receives a fatal warning it should disconnect and not attempt to reconnect. :return: None """ self.sendJSON({'command': 'notice', 'style': 'info' if not fatal else 'error', 'text': message}) if fatal: self.abort(message) def send(self, message): """ :param message: :return: """ self._logger.debug(">>: {}".format(message)) self.protocol.send_message(message) async def drain(self): await self.protocol.drain() def sendJSON(self, data_dictionary): """ Deprecated alias for send """ self.send(data_dictionary) async def on_connection_lost(self): async def nopdrain(message): return self.drain = nopdrain self.send = lambda m: None if self.game_connection: await self.game_connection.on_connection_lost() if self.search and not self.search.done(): self.search.cancel() if self.player: self.player_service.remove_player(self.player)