async def test_auth_lost(http_client, http_request, oauth_response): http_request.return_value = oauth_response await http_client.authenticate_with_refresh_token("TOKEN") http_request.reset_mock() http_client._authorized_get = MagicMock() http_client._authorized_get.side_effect = AuthenticationRequired() http_client.__refresh_tokens = MagicMock() http_client.__refresh_tokens.side_effects = AuthenticationRequired() with pytest.raises(AuthenticationRequired): await http_client.get("url")
async def get_game_times(self): if not self.client.is_authenticated(): raise AuthenticationRequired() game_times = [] games_with_space = [ game for game in self.games_collection if game.space_id ] try: tasks = [ self.client.get_game_stats(game.space_id) for game in games_with_space ] stats = await asyncio.gather(*tasks) for st, game in zip(stats, games_with_space): statscards = st.get('Statscards', None) if statscards is None: continue playtime, last_played = find_playtime(statscards, default_total_time=0, default_last_played=0) log.info( f'Stats for {game.name}: playtime: {playtime}, last_played: {last_played}' ) if playtime is not None and last_played is not None: game_times.append( GameTime(game.space_id, playtime, last_played)) except ApplicationError as e: log.exception("Game times:" + repr(e)) raise e except Exception as e: log.exception("Game times:" + repr(e)) finally: return game_times
async def request(self, method, url, *args, **kwargs): try: response = await self._session.request(method, url, *args, **kwargs) except asyncio.TimeoutError: raise BackendTimeout() except aiohttp.ServerDisconnectedError: raise BackendNotAvailable() except aiohttp.ClientConnectionError: raise NetworkError() except aiohttp.ContentTypeError: raise UnknownBackendResponse() except aiohttp.ClientError: logging.exception( "Caught exception while running {} request for {}".format( method, url)) raise UnknownError() if response.status == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired() if response.status == HTTPStatus.FORBIDDEN: raise AccessDenied() if response.status == HTTPStatus.SERVICE_UNAVAILABLE: raise BackendNotAvailable() if response.status == HTTPStatus.TOO_MANY_REQUESTS: raise TooManyRequests() if response.status >= 500: raise BackendError() if response.status >= 400: logging.warning( "Got status {} while running {} request for {}".format( response.status, method, url)) raise UnknownError() return response
async def get(self, *args, **kwargs): if not self.authenticated: raise AuthenticationRequired() try: return await self._authorized_get(*args, **kwargs) except AuthenticationRequired: try: await self._refresh_tokens() except Exception: logging.exception("Failed to refresh tokens") if self._auth_lost_callback: self._auth_lost_callback() raise AuthenticationRequired() return await self._authorized_get(*args, **kwargs)
async def _validate_graph_response(self, response): response = await response.json() if "errors" in response: for error in response["errors"]: if '401' in error["message"]: raise AuthenticationRequired() return response
async def get_owned_games(self): if not self.client.is_authenticated(): raise AuthenticationRequired() if SYSTEM == System.WINDOWS: self._parse_local_games() self._parse_local_game_ownership() await self._parse_club_games() try: await self._parse_subscription_games() except Exception as e: log.warning( f"Parsing subscriptions failed, most likely account without subscription {repr(e)}" ) self.owned_games_sent = True for game in self.games_collection: game.considered_for_sending = True return [ game.as_galaxy_game() for game in self.games_collection if game.owned ]
async def get_owned_games(self): if self._steam_id is None: raise AuthenticationRequired() games = await self._client.get_games(self._steam_id) owned_games = [] try: for game in games: owned_games.append( Game(str(game["appid"]), game["name"], [], LicenseInfo(LicenseType.SinglePurchase, None))) except (KeyError, ValueError): logging.exception("Can not parse backend response") raise UnknownBackendResponse() self._own_games = games game_ids = list(map(lambda x: x.game_id, owned_games)) other_games = await self.get_steam_sharing_games(game_ids) for i in other_games: owned_games.append(i) return owned_games
async def install_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() installed_game = self.installed_games.get(game_id, None) if installed_game and os.access(installed_game.install_path, os.F_OK): log.warning("Received install command on an already installed game") return await self.launch_game(game_id) if game_id in Blizzard.legacy_game_ids: if SYSTEM == pf.WINDOWS: platform = 'windows' elif SYSTEM == pf.MACOS: platform = 'macos' webbrowser.open(f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}") return try: self.local_client.refresh() log.info(f'Installing game of id {game_id}') self.local_client.install_game(game_id) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except Exception as e: log.exception(f"Installing game {game_id} failed: {e}")
async def uninstall_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() if game_id == 'wow_classic': # attempting to uninstall classic wow through protocol gives you a message that the game cannot # be uninstalled through protocol and you should use battle.net return self._open_battlenet_at_id(game_id) if SYSTEM == pf.MACOS: self._open_battlenet_at_id(game_id) else: try: installed_game = self.installed_games.get(game_id, None) if installed_game is None or not os.access(installed_game.install_path, os.F_OK): log.error(f'Cannot uninstall {Blizzard[game_id].uid}') self.update_local_game_status(LocalGame(game_id, LocalGameState.None_)) return if not isinstance(installed_game.info, ClassicGame): if self.uninstaller is None: raise FileNotFoundError('Uninstaller not found') uninstall_tag = installed_game.uninstall_tag client_lang = self.config_parser.locale_language self.uninstaller.uninstall_game(installed_game, uninstall_tag, client_lang) except Exception as e: log.exception(f'Uninstalling game {game_id} failed: {e}')
async def do_request(self, method, *args, **kwargs): if not self.authenticated: raise AuthenticationRequired() try: if 'graph' in kwargs: return await self._validate_graph_response(await method( *args, **kwargs)) return await method(*args, **kwargs) except Exception as e: logging.exception( f"Received exception on authorized request: {repr(e)}") try: if not self._refreshing_task or self._refreshing_task.done(): self._refreshing_task = asyncio.create_task( self._refresh_tokens()) await self._refreshing_task while not self._refreshing_task.done(): await asyncio.sleep(0.2) except AuthenticationRequired as e: logging.exception( f"Failed to refresh tokens, received: {repr(e)}") if self._auth_lost_callback: self._auth_lost_callback() raise except Exception as e: logging.exception(f"Got exception {repr(e)}") raise if 'graph' in kwargs: return await self._validate_graph_response(await method( *args, **kwargs)) return await method(*args, **kwargs)
def handle_exception(): """ Context manager translating network related exceptions to custom :mod:`~galaxy.api.errors`. """ try: yield except asyncio.TimeoutError: raise BackendTimeout() except aiohttp.ServerDisconnectedError: raise BackendNotAvailable() except aiohttp.ClientConnectionError: raise NetworkError() except aiohttp.ContentTypeError as error: raise UnknownBackendResponse(error.message) except aiohttp.ClientResponseError as error: if error.status == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired(error.message) if error.status == HTTPStatus.FORBIDDEN: raise AccessDenied(error.message) if error.status == HTTPStatus.SERVICE_UNAVAILABLE: raise BackendNotAvailable(error.message) if error.status == HTTPStatus.TOO_MANY_REQUESTS: raise TooManyRequests(error.message) if error.status >= 500: raise BackendError(error.message) if error.status >= 400: logger.warning("Got status %d while performing %s request for %s", error.status, error.request_info.method, str(error.request_info.url)) raise UnknownError(error.message) except aiohttp.ClientError as e: logger.exception("Caught exception while performing request") raise UnknownError(repr(e))
async def _authenticated_request(self, method, *args, **kwargs) -> ClientResponse: response = await super().request(method, *args, **kwargs) if response.status == HTTPStatus.FOUND: self._auth_lost_callback() raise AuthenticationRequired() return response
async def get_owned_games(self): if not self._api.is_authenticated: raise AuthenticationRequired() async with self._getting_owned_games: logging.debug('getting owned games') self._owned_games = await self._library_resolver() return [g.in_galaxy_format() for g in self._owned_games.values()]
async def request(self, method, *args, **kwargs): if not self._access_token: raise AuthenticationRequired() try: return await self._oauth_request(method, *args, **kwargs) except AuthenticationRequired: await self._refresh_access_token() return await self._oauth_request(method, *args, **kwargs)
async def get_friends(self): if self._steam_id is None: raise AuthenticationRequired() return [ FriendInfo(user_id=user_id, user_name=user_name) for user_id, user_name in (await self._client.get_friends(self._steam_id)).items() ]
async def get_friends(self): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() friends_list = await self.social_features.get_friends() return [ FriendInfo(user_id=friend.id.low, user_name='') for friend in friends_list ]
async def get_friends(self): if not self._http_client.is_authenticated(): raise AuthenticationRequired() return [ FriendInfo(user_id=str(user_id), user_name=str(user_name)) for user_id, user_name in ( await self._backend_client.get_friends(self._pid)).items() ]
async def get_owned_games(self): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() def _parse_classic_games(classic_games): for classic_game in classic_games["classicGames"]: log.info(f"looking for {classic_game} in classic games") try: blizzard_game = Blizzard[classic_game["localizedGameName"].replace(u'\xa0', ' ')] log.info(f"match! {blizzard_game}") classic_game["titleId"] = blizzard_game.uid classic_game["gameAccountStatus"] = "Good" except KeyError: continue return classic_games def _get_not_added_free_games(owned_games): owned_games_ids = [] for game in owned_games: if "titleId" in game: owned_games_ids.append(str(game["titleId"])) return [{"titleId": game.blizzard_id, "localizedGameName": game.name, "gameAccountStatus": "Free"} for game in Blizzard.free_games if game.blizzard_id not in owned_games_ids] try: games = await self.backend_client.get_owned_games() classic_games = _parse_classic_games(await self.backend_client.get_owned_classic_games()) owned_games = games["gameAccounts"] + classic_games["classicGames"] # Add wow classic if retail wow is present in owned games for owned_game in owned_games.copy(): if 'titleId' in owned_game: if owned_game['titleId'] == 5730135: owned_games.append({'titleId': 'wow_classic', 'localizedGameName': 'World of Warcraft Classic', 'gameAccountStatus': owned_game['gameAccountStatus']}) free_games_to_add = _get_not_added_free_games(owned_games) owned_games += free_games_to_add log.info(f"Owned games {owned_games} with free games") self.owned_games_cache = owned_games return [ Game( str(game["titleId"]), game["localizedGameName"], [], LicenseInfo(License_Map[game["gameAccountStatus"]]), ) for game in self.owned_games_cache if "titleId" in game ] except Exception as e: log.exception(f"failed to get owned games: {repr(e)}") raise
async def get_owned_games(self, owned_title_ids=None, online_check_success=False): # Here is the actual implementation of getting the user's owned games: # -Get the list of games_played from rockstargames.com/auth/get-user.json. # -If possible, use the launcher log to confirm which games are actual launcher games and which are # Steam/Retail games. # -If it is not possible to use the launcher log, then just use the list provided by the website. if owned_title_ids is None: owned_title_ids = [] if not self.is_authenticated(): raise AuthenticationRequired() # The log is in the Documents folder. current_log_count = 0 log_file = None log_file_append = "" # The Rockstar Games Launcher generates 10 log files before deleting them in a FIFO fashion. Old log files are # given a number ranging from 1 to 9 in their name. In case the first log file does not have all of the games, # we need to check the other log files, if possible. while current_log_count < 10: # We need to prevent the log file check for Mac users. if not IS_WINDOWS: break try: if current_log_count != 0: log_file_append = ".0" + str(current_log_count) log_file = os.path.join(self.documents_location, "Rockstar Games\\Launcher\\launcher" + log_file_append + ".log") if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_LOG_LOCATION: Checking the file " + log_file + "...") else: log.debug("ROCKSTAR_LOG_LOCATION: Checking the file ***...") # The path to the Launcher log file # likely contains the user's PC profile name (C:\Users\[Name]\Documents...). owned_title_ids = await self.parse_log_file(log_file, owned_title_ids, online_check_success) break except NoGamesInLogException: log.warning("ROCKSTAR_LOG_WARNING: There are no owned games listed in " + str(log_file) + ". Moving to " "the next log file...") current_log_count += 1 except NoLogFoundException: log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " "from. Assuming that the online list is correct...") break except Exception: # This occurs after ROCKSTAR_LOG_ERROR. break if current_log_count == 10: log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " "from. Assuming that the online list is correct...") for title_id in owned_title_ids: game = self.create_game_from_title_id(title_id) if game not in self.owned_games_cache: log.debug("ROCKSTAR_ADD_GAME: Adding " + title_id + " to owned games cache...") self.owned_games_cache.append(game) return self.owned_games_cache
async def start_game_times_import(self, game_ids): if not self._http_client.is_authenticated(): raise AuthenticationRequired() _, self._last_played_games = await asyncio.gather( self._get_offers( game_ids), # update local cache ignoring return value self._backend_client.get_lastplayed_games(self._pid)) await super().start_game_times_import(game_ids)
async def prepare_game_times_context(self, game_ids: List[str]) -> Any: if self._user_info_cache.steam_id is None: raise AuthenticationRequired() if not self._times_cache.import_in_progress: await self._steam_client.refresh_game_times() else: logger.info("Game stats import already in progress") await self._times_cache.wait_ready(10 * 60) # Don't block future imports in case we somehow don't receive one of the responses logger.info("Finished game times context prepare")
def handle_status_code(status_code): if status_code == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired() if status_code == HTTPStatus.FORBIDDEN: raise AccessDenied() if status_code == HTTPStatus.SERVICE_UNAVAILABLE: raise BackendNotAvailable() if status_code >= 500: raise BackendError() if status_code >= 400: raise UnknownError()
async def get_owned_games(self): if not self._http_client.is_authenticated(): raise AuthenticationRequired() owned_offers = await self._get_owned_offers() games = [] for offer in owned_offers: game = Game(offer["offerId"], offer["i18n"]["displayName"], None, LicenseInfo(LicenseType.SinglePurchase, None)) games.append(game) return games
async def _refresh_access_token(self): try: self._access_token = await self._get_access_token() if not self._access_token: raise UnknownBackendResponse("Empty access token") except (BackendNotAvailable, BackendTimeout, BackendError, NetworkError): logging.warning("Failed to refresh token for independent reasons") raise except InvalidCredentials: logging.exception("Failed to refresh token") self._auth_lost() raise AuthenticationRequired()
def request_url(self, url): session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36' }) r = session.get(url, cookies=self._auth_cookies, allow_redirects=True) logger.info('url: %s (%s)' % (r.url, len(r.history))) if r.url.startswith('https://accounts.google.com/'): #if not r.url.startswith(url): logger.info('%s <=> %s' % (r.url, url)) raise AuthenticationRequired() return r
async def do_request(self, method, url, data=None, json=True, headers=None, ignore_failure=False): loop = asyncio.get_event_loop() if not headers: headers = self._authentication_client.session.headers try: if data is None: data = {} params = { "method": method, "url": url, "data": data, "timeout": self._authentication_client.timeout, "headers": headers } try: response = await loop.run_in_executor( None, functools.partial( self._authentication_client.session.request, **params)) except requests.Timeout: raise BackendTimeout() if not ignore_failure: if response.status_code == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired() if response.status_code == HTTPStatus.FORBIDDEN: raise AccessDenied() if response.status_code == HTTPStatus.SERVICE_UNAVAILABLE: raise BackendNotAvailable() if response.status_code >= 500: raise BackendError() if response.status_code >= 400: raise UnknownError() if json: return response.json() else: return response except Exception as e: log.exception( f"Request exception: {str(e)}; url: {url}, method: {method}, data: {data}, headers: {headers}" ) raise
async def test_refresh_token(http_client, http_request, oauth_response): http_request.return_value = oauth_response await http_client.authenticate_with_refresh_token("TOKEN") http_request.reset_mock() authorized_response = MagicMock() authorized_response.status = 200 http_client._authorized_get = AsyncMock() http_client._authorized_get.side_effect = [AuthenticationRequired(), authorized_response] http_client._authenticate = AsyncMock() response = await http_client.get("url") assert response.status == authorized_response.status
async def _refresh_access_token(self): try: self._access_token = await self.get_access_token( self._refresh_token) if not self._access_token: raise Exception except (BackendNotAvailable, BackendTimeout, BackendError, NetworkError): logging.warning("Failed to refresh token for independent reasons") raise except Exception: logging.exception("Failed to refresh token") if self._auth_lost_callback: self._auth_lost_callback() raise AuthenticationRequired()
async def get_friends(self): if self._user_info_cache.steam_id is None: raise AuthenticationRequired() friends_ids = await self._steam_client.get_friends() friends_infos = await self._steam_client.get_friends_info(friends_ids) friends_nicknames = await self._steam_client.get_friends_nicknames() friends = [] for friend_id in friends_infos: friend = galaxy_user_info_from_user_info(str(friend_id), friends_infos[friend_id]) if str(friend_id) in friends_nicknames: friend.user_name += f" ({friends_nicknames[friend_id]})" friends.append(friend) return friends
async def _authenticate(self, grant_type, secret): headers = { "Authorization": "basic " + self._BASIC_AUTH_CREDENTIALS, "User-Agent": self.LAUNCHER_USER_AGENT } data = {"grant_type": grant_type, "token_type": "eg1"} data[grant_type] = secret try: with handle_exception(): try: response = await self._session.request("POST", self._OAUTH_URL, headers=headers, data=data) except aiohttp.ClientResponseError as e: logging.error(e) if e.status == 400: # override 400 meaning for auth purpose raise AuthenticationRequired() except AuthenticationRequired as e: logging.exception( f"Authentication failed, grant_type: {grant_type}, exception: {repr(e)}" ) raise AuthenticationRequired() result = await response.json() try: self._access_token = result["access_token"] self._refresh_token = result["refresh_token"] self._account_id = result["account_id"] credentials = {"refresh_token": self._refresh_token} self._store_credentials(credentials) except KeyError: logging.exception( "Could not parse backend response when authenticating") raise UnknownBackendResponse()