def cmd_cloud_list(args): with init_client(args) as s: job_id = s.send_um('Cloud.EnumerateUserFiles#1', { 'appid': args.app_id, 'extended_details': True, }) total_files, n_files = None, 0 while total_files != n_files: msg = s.wait_msg(job_id, timeout=10) if not msg: raise SteamError("Failed listing UFS files", EResult.Timeout) if msg.header.eresult != EResult.OK: raise SteamError("Failed listing UFS files", EResult(msg.header.eresult)) total_files = msg.body.total_files n_files += len(msg.body.files) for entry in msg.body.files: if not args.long: print(entry.filename) else: print("{} - size:{:,d} sha1:{}".format( entry.filename, entry.file_size, entry.file_sha, ))
def get_cloud_files(s, app_id): job_id = s.send_um('Cloud.EnumerateUserFiles#1', { 'appid': app_id, 'extended_details': True, }) files = [] total_files, n_files, total_size = None, 0, 0 while total_files != n_files: msg = s.wait_msg(job_id, timeout=10) if not msg: raise SteamError("Failed listing UFS files", EResult.Timeout) if msg.header.eresult != EResult.OK: raise SteamError("Failed listing UFS files", EResult(msg.header.eresult)) total_files = msg.body.total_files n_files += len(msg.body.files) for entry in msg.body.files: files.append(entry) total_size += entry.file_size return files, total_files, total_size
def get_app_depot_info(self, app_id): if app_id not in self.app_depots: cached_appinfo = UserCacheFile("appinfo/{}.json".format(app_id)) appinfo = None if cached_appinfo.exists(): appinfo = cached_appinfo.read_json() if not appinfo: app_req = {'appid': app_id} token = self.get_app_access_token(app_id) if token: app_req['access_token'] = token try: appinfo = self.steam.get_product_info([app_req])['apps'][app_id] except KeyError: raise SteamError("Invalid app id") if appinfo['_missing_token']: raise SteamError("No access token available") cached_appinfo.write_json(appinfo) self.app_depots[app_id] = appinfo.get('depots', {}) return self.app_depots[app_id]
def get_manifest_for_workshop_item(self, item_id): """Get the manifest file for a worshop item that is hosted on SteamPipe :param item_id: Workshop ID :type item_id: int :returns: manifest instance :rtype: :class:`.CDNDepotManifest` :raises: ManifestError, SteamError """ resp = self.steam.send_um_and_wait('PublishedFile.GetDetails#1', { 'publishedfileids': [item_id], 'includetags': False, 'includeadditionalpreviews': False, 'includechildren': False, 'includekvtags': False, 'includevotes': False, 'short_description': True, 'includeforsaledata': False, 'includemetadata': False, 'language': 0 }, timeout=7) if resp.header.eresult != EResult.OK: raise SteamError(resp.header.error_message or 'No message', resp.header.eresult) wf = None if resp is None else resp.body.publishedfiledetails[0] if wf is None or wf.result != EResult.OK: raise SteamError( "Failed getting workshop file info", EResult.Timeout if resp is None else EResult(wf.result)) elif not wf.hcontent_file: raise SteamError("Workshop file is not on SteamPipe", EResult.FileNotFound) app_id = ws_app_id = wf.consumer_appid try: manifest_code = self.get_manifest_request_code( app_id, ws_app_id, int(wf.hcontent_file)) manifest = self.get_manifest(app_id, ws_app_id, wf.hcontent_file, manifest_request_code=manifest_code) except SteamError as exc: return ManifestError("Failed to acquire manifest", app_id, depot_id, manifest_gid, exc) manifest.name = wf.title return manifest
def get_app_depot_info(self, app_id): if app_id not in self.app_depots: try: appinfo = self.steam.get_product_info([app_id])['apps'][app_id] except KeyError: raise SteamError("Invalid app id") if appinfo['_missing_token']: raise SteamError("No access token available") self.app_depots[app_id] = appinfo.get('depots', {}) return self.app_depots[app_id]
def get_ugc_details(self, ugc_id): if 0 > ugc_id > 2**64: raise SteamError("Invalid UGC ID") result = self.send_job_and_wait(MsgProto(EMsg.ClientUFSGetUGCDetails), {'hcontent': ugc_id}, timeout=5) if not result or result.eresult != EResult.OK: raise SteamError( "Failed getting UGC details", EResult(result.eresult) if result else EResult.Timeout) return result
def get_chunk(self, app_id, depot_id, chunk_id): """Download a single content chunk :param app_id: App ID :type app_id: int :param depot_id: Depot ID :type depot_id: int :param chunk_id: Chunk ID :type chunk_id: int :returns: chunk data :rtype: bytes :raises SteamError: error message """ if (depot_id, chunk_id) not in self._chunk_cache: resp = self.cdn_cmd('depot', '%s/chunk/%s' % (depot_id, chunk_id)) data = symmetric_decrypt(resp.content, self.get_depot_key(app_id, depot_id)) if data[:2] == b'VZ': if data[-2:] != b'zv': raise SteamError("VZ: Invalid footer: %s" % repr(data[-2:])) if data[2:3] != b'a': raise SteamError("VZ: Invalid version: %s" % repr(data[2:3])) vzfilter = lzma._decode_filter_properties( lzma.FILTER_LZMA1, data[7:12]) vzdec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[vzfilter]) checksum, decompressed_size = struct.unpack( '<II', data[-10:-2]) # decompress_size is needed since lzma will sometime produce longer output # [12:-9] is need as sometimes lzma will produce shorter output # together they get us the right data data = vzdec.decompress(data[12:-9])[:decompressed_size] if crc32(data) != checksum: raise SteamError( "VZ: CRC32 checksum doesn't match for decompressed data" ) else: with ZipFile(BytesIO(data)) as zf: data = zf.read(zf.filelist[0]) self._chunk_cache[(depot_id, chunk_id)] = data return self._chunk_cache[(depot_id, chunk_id)]
def get_steamids_from_ips(self, server_ips, timeout=30): """Resolve SteamIDs from IPs :param steam_ids: a list of ips (e.g. ``['1.2.3.4:27015',...]``) :type steam_ids: list :param timeout: (optional) timeout for request in seconds :type timeout: int :return: map of steamids to ips :rtype: dict :raises: :class:`.SteamError` Sample response: .. code:: python {'1.2.3.4:27060': SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234)} """ resp = self._s.send_um_and_wait( "GameServers.GetServerSteamIDsByIP#1", {"server_ips": server_ips}, timeout=timeout, ) if resp is None: return None if resp.header.eresult != EResult.OK: raise SteamError(resp.header.error_message, resp.header.eresult) return { server.addr: SteamID(server.steamid) for server in resp.body.servers }
def cmd_depot_decrypt_gid(args): args.cell_id = 0 args.no_manifests = True args.skip_login = False args.depot = None valid_gids = [] for egid in args.manifest_gid: if not re.match(r'[0-9A-Za-z]{32}$', egid): LOG.error("Skipping invalid gid: %s", egid) else: valid_gids.append(egid) if not valid_gids: LOG.error("No valid gids left to check") return 1 # error with init_clients(args) as (s, _, _): resp = s.send_job_and_wait(MsgProto(EMsg.ClientCheckAppBetaPassword), {'app_id': args.app, 'betapassword': args.password}) if resp.eresult == EResult.OK: LOG.debug("Unlocked following beta branches: %s", ', '.join(map(lambda x: x.betaname.lower(), resp.betapasswords))) for entry in resp.betapasswords: print("Password is valid for branch:", entry.betaname) for egid in valid_gids: gid = decrypt_manifest_gid_2(unhexlify(egid), unhexlify(entry.betapassword)) print(' ', egid, '=', gid) else: raise SteamError("App beta password check failed.", EResult(resp.eresult))
def cdn_cmd(self, command, args): """Run CDN command request :param command: command name :type command: str :param args: args :type args: str :returns: requests response :rtype: :class:`requests.Response` :raises SteamError: on error """ server = self.get_content_server() while True: url = "%s://%s:%s/%s/%s" % ( 'https' if server.https else 'http', server.host, server.port, command, args, ) try: resp = self.web.get(url, timeout=10) except Exception as exp: self._LOG.debug("Request error: %s", exp) else: if resp.ok: return resp elif 400 <= resp.status_code < 500: self._LOG.debug("Got HTTP %s", resp.status_code) raise SteamError("HTTP Error %s" % resp.status_code) self.steam.sleep(0.5) server = self.get_content_server(rotate=True)
def download_to(self, target, no_make_dirs=False, pbar=None, verify=True): relpath = sanitizerelpath(self.filename) if no_make_dirs: relpath = os.path.basename(relpath) relpath = os.path.join(target, relpath) filepath = os.path.abspath(relpath) ensure_dir(filepath) checksum = self.file_mapping.sha_content.hex() # don't bother verifying if file doesn't already exist if not os.path.exists(filepath): verify = False with open(filepath, 'r+b' if verify else 'wb') as fp: fp.seek(0, 2) # pre-allocate space if fp.tell() != self.size: newsize = fp.truncate(self.size) if newsize != self.size: raise SteamError( "Failed allocating space for {}".format(filepath)) # self._LOG.info('{} {} ({}, sha1:{})'.format( # 'Verifying' if verify else 'Downloading', # relpath, # fmt_size(self.size), # checksum # )) fp.seek(0) for chunk in self.chunks: # verify chunk sha hash if verify: cur_data = fp.read(chunk.cb_original) if sha1_hash(cur_data) == chunk.sha: if pbar: pbar.update(chunk.cb_original) continue fp.seek(chunk.offset) # rewind before write # download and write chunk data = self.manifest.cdn_client.get_chunk( self.manifest.app_id, self.manifest.depot_id, chunk.sha.hex(), ) fp.write(data) if pbar: pbar.update(chunk.cb_original)
def fetch_content_servers(self, num_servers=20): """Update CS server list :param num_servers: numbers of CS server to fetch :type num_servers: int """ self.servers.clear() self._LOG.debug("Trying to fetch content servers from Steam API") servers = get_content_servers_from_webapi(self.cell_id) self.servers.extend(servers) if not self.servers: raise SteamError("Failed to fetch content servers")
def get_manifest_request_code(self, app_id, depot_id, manifest_gid, branch='public', branch_password_hash=None): """Get manifest request code for authenticating manifest download :param app_id: App ID :type app_id: int :param depot_id: Depot ID :type depot_id: int :param manifest_gid: Manifest gid :type manifest_gid: int :param branch: (optional) branch name :type branch: str :param branch_password_hash: (optional) branch password hash :type branch_password_hash: str :returns: manifest request code :rtype: int """ body = { "app_id": int(app_id), "depot_id": int(depot_id), "manifest_id": int(manifest_gid), } if branch and branch.lower() != 'public': body['app_branch'] = branch if branch_password_hash: body['branch_password_hash'] = branch_password_hash resp = self.steam.send_um_and_wait( 'ContentServerDirectory.GetManifestRequestCode#1', body, timeout=5, ) if resp is None or resp.header.eresult != EResult.OK: raise SteamError( "Failed to get manifest code for %s, %s, %s" % (app_id, depot_id, manifest_gid), EResult.Timeout if resp is None else EResult(resp.header.eresult)) return resp.body.manifest_request_code
def get_depot_key(self, app_id, depot_id): """Get depot key, which is needed to decrypt files :param app_id: app id :type app_id: int :param depot_id: depot id :type depot_id: int :return: returns decryption key :rtype: bytes :raises SteamError: error message """ if depot_id not in self.depot_keys: msg = self.steam.get_depot_key(app_id, depot_id) if msg and msg.eresult == EResult.OK: self.depot_keys[depot_id] = msg.depot_encryption_key else: raise SteamError( "Failed getting depot key", EResult.Timeout if msg is None else EResult(msg.eresult)) return self.depot_keys[depot_id]
def init_clients(args): if getattr(args, 'file', None): manifest = CTLDepotManifest(None, -1, args.file.read()) yield None, None, [manifest] else: s = CachingSteamClient() if args.cell_id is not None: s.cell_id = args.cell_id cdn = s.get_cdnclient() # only login if we dont have depot decryption key if (not args.app or not args.depot or not args.manifest or args.depot not in cdn.depot_keys): result = s.login_from_args(args) if result == EResult.OK: LOG.info("Login to Steam successful") else: LOG.error("Failed to login: %r" % result) return 1 # error if args.app and args.depot and args.manifest: try: manifests = [cdn.get_manifest(args.app, args.depot, args.manifest)] except SteamError as exp: if exp.eresult == EResult.AccessDenied: raise SteamError("This account doesn't have access to the app depot", exp.eresult) else: raise else: LOG.info("Checking licenses") cdn.load_licenses() if args.app not in cdn.licensed_app_ids: raise SteamError("No license available for App ID: %s" % args.app, EResult.AccessDenied) LOG.info("Checking change list") cdn.check_for_changes() def branch_filter(depot_id, info): if args.depot is not None: if args.depot != depot_id: return False if args.os != 'any': if args.os[-2:] == '64': os, arch = args.os[:-2], args.os[-2:] else: os, arch = args.os, None config = info.get('config', {}) if 'oslist' in config and (os not in config['oslist'].split(',')): return False if 'osarch' in config and config['osarch'] != arch: return False return True LOG.info("Getting manifests for 'public' branch") manifests = [] for manifest in cdn.get_manifests(args.app, filter_func=branch_filter, decrypt=False): if manifest.depot_id not in cdn.licensed_depot_ids: LOG.error("No license for depot: %r" % manifest) continue if manifest.filenames_encrypted: try: manifest.decrypt_filenames(cdn.get_depot_key(manifest.app_id, manifest.depot_id)) except Exception as exp: LOG.error("Failed to decrypt manifest: %s" % str(exp)) continue manifests.append(manifest) LOG.debug("Got manifests: %r", manifests) yield s, cdn, manifests # clean and exit cdn.save_cache() s.disconnect()
def get_server_list(self, filter_text, max_servers=10, timeout=20): """ Get list of servers. Works similiarly to :meth:`query`, but the response has more details. :param filter_text: filter for servers :type filter_text: str :param max_servers: (optional) number of servers to return :type max_servers: int :param timeout: (optional) timeout for request in seconds :type timeout: int :returns: list of servers, see below. (``None`` is returned steam doesn't respond) :rtype: :class:`list`, :class:`None` :raises: :class:`.SteamError` Sample response: .. code:: python [{'addr': '1.2.3.4:27067', 'appid': 730, 'bots': 0, 'dedicated': True, 'gamedir': 'csgo', 'gameport': 27067, 'gametype': 'valve_ds,empty,secure', 'map': 'de_dust2', 'max_players': 10, 'name': 'Valve CS:GO Asia Server (srcdsXXX.XXX.XXX)', 'os': 'l', 'players': 0, 'product': 'csgo', 'region': 5, 'secure': True, 'steamid': SteamID(id=3279818759, type='AnonGameServer', universe='Public', instance=7011), 'version': '1.35.4.0'} ] """ resp = self._s.send_um_and_wait( "GameServers.GetServerList#1", { "filter": filter_text, "limit": max_servers, }, timeout=20, ) if resp is None: return None if resp.header.eresult != EResult.OK: raise SteamError(resp.header.error_message, resp.header.eresult) resp = proto_to_dict(resp.body) if not resp: return [] else: for server in resp['servers']: server['steamid'] = SteamID(server['steamid']) return resp['servers']
def get_file(self, path, *args, **kwargs): ref = self._locate_file_mapping(path) if ref: return CTLDepotFile(*ref) raise SteamError("File not found: {}".format(path))
def cmd_depot_download(args): pbar = fake_tqdm() pbar2 = fake_tqdm() try: with init_clients(args) as (_, _, manifests): fileindex = ManifestFileIndex(manifests) # pre-index vpk file to speed up lookups if args.vpk: fileindex.index('*.vpk') # calculate total size total_files = 0 total_size = 0 LOG.info("Locating and counting files...") for manifest in manifests: for depotfile in manifest: if not depotfile.is_file: continue filepath = depotfile.filename_raw # list files inside vpk if args.vpk and filepath.endswith('.vpk'): # fast skip VPKs that can't possibly match if args.name and ':' in args.name: pre = args.name.split(':', 1)[0] if not fnmatch(filepath, pre): continue if args.regex and ':' in args.regex: pre = args.regex.split(':', 1)[0] if not re_search(pre + '$', filepath): continue # scan VPKs, but skip data only ones if filepath.endswith('_dir.vpk') or not re.search( "_\d+\.vpk$", filepath): LOG.debug("Scanning VPK file: %s", filepath) try: fvpk = fileindex.get_vpk(filepath) except ValueError as exp: LOG.error("VPK read error: %s", str(exp)) else: for vpkfile_path, ( _, _, _, _, _, size) in fvpk.c_iter_index(): complete_path = "{}:{}".format( filepath, vpkfile_path) if args.name and not fnmatch( complete_path, args.name): continue if args.regex and not re_search( args.regex, complete_path): continue total_files += 1 total_size += size # account for depot files if args.name and not fnmatch(filepath, args.name): continue if args.regex and not re_search(args.regex, filepath): continue total_files += 1 total_size += depotfile.size if not total_files: raise SteamError("No files found to download") # enable progress bar if not args.no_progress and sys.stderr.isatty(): pbar = tqdm(desc='Data ', mininterval=0.5, maxinterval=1, total=total_size, unit='B', unit_scale=True) pbar2 = tqdm(desc='Files', mininterval=0.5, maxinterval=1, total=total_files, position=1, unit=' file', unit_scale=False) gevent.spawn(pbar.gevent_refresh_loop) gevent.spawn(pbar2.gevent_refresh_loop) # download files tasks = GPool(6) for manifest in manifests: if pbar2.n == total_files: break LOG.info("Processing manifest (%s) '%s' ..." % (manifest.gid, manifest.name or "<Unknown>")) for depotfile in manifest: if pbar2.n == total_files: break if not depotfile.is_file: continue filepath = depotfile.filename_raw if args.vpk and filepath.endswith('.vpk'): # fast skip VPKs that can't possibly match if args.name and ':' in args.name: pre = args.name.split(':', 1)[0] if not fnmatch(filepath, pre): continue if args.regex and ':' in args.regex: pre = args.regex.split(':', 1)[0] if not re_search(pre + '$', filepath): continue # scan VPKs, but skip data only ones if filepath.endswith('_dir.vpk') or not re.search( "_\d+\.vpk$", filepath): LOG.debug("Scanning VPK file: %s", filepath) try: fvpk = fileindex.get_vpk(filepath) except ValueError as exp: LOG.error("VPK read error: %s", str(exp)) else: for vpkfile_path, metadata in fvpk.c_iter_index( ): complete_path = "{}:{}".format( filepath, vpkfile_path) if args.name and not fnmatch( complete_path, args.name): continue if args.regex and not re_search( args.regex, complete_path): continue tasks.spawn( vpkfile_download_to, depotfile.filename, fvpk.get_vpkfile_instance( vpkfile_path, fvpk._make_meta_dict(metadata)), args.output, no_make_dirs=args.no_directories, pbar=pbar, ) pbar2.update(1) # break out of vpk file loop if pbar2.n == total_files: break # break out of depotfile loop if pbar2.n == total_files: break # filepath filtering if args.name and not fnmatch(filepath, args.name): continue if args.regex and not re_search(args.regex, filepath): continue tasks.spawn( depotfile.download_to, args.output, no_make_dirs=args.no_directories, pbar=pbar, verify=(not args.skip_verify), ) pbar2.update(1) # wait on all downloads to finish tasks.join() gevent.sleep(0.5) except KeyboardInterrupt: pbar.close() LOG.info("Download canceled") return 1 # error except SteamError as exp: pbar.close() pbar.write(str(exp)) return 1 # error else: pbar.close() if not args.no_progress: pbar2.close() pbar2.write('\n') LOG.info('Download complete')
def get_manifests(self, app_id, branch='public', password=None, filter_func=None, decrypt=True): """Get a list of CDNDepotManifest for app :param app_id: App ID :type app_id: int :param branch: branch name :type branch: str :param password: branch password for locked branches :type password: str :param filter_func: Function to filter depots. ``func(depot_id, depot_info)`` :returns: list of :class:`.CDNDepotManifest` :rtype: :class:`list` [:class:`.CDNDepotManifest`] :raises: ManifestError, SteamError """ depots = self.get_app_depot_info(app_id) is_enc_branch = False if branch not in depots.get('branches', {}): raise SteamError("No branch named %s for app_id %s" % (repr(branch), app_id)) elif int(depots['branches'][branch].get('pwdrequired', 0)) > 0: is_enc_branch = True if (app_id, branch) not in self.beta_passwords: if not password: raise SteamError("Branch %r requires a password" % branch) result = self.check_beta_password(app_id, password) if result != EResult.OK: raise SteamError("Branch password is not valid. %r" % result) if (app_id, branch) not in self.beta_passwords: raise SteamError("Incorrect password for branch %r" % branch) def async_fetch_manifest(app_id, depot_id, manifest_gid, decrypt, depot_name, branch_name, branch_pass): try: manifest_code = self.get_manifest_request_code( app_id, depot_id, int(manifest_gid), branch_name, branch_pass) except SteamError as exc: return ManifestError("Failed to acquire manifest code", app_id, depot_id, manifest_gid, exc) try: manifest = self.get_manifest( app_id, depot_id, manifest_gid, decrypt=decrypt, manifest_request_code=manifest_code) except Exception as exc: return ManifestError("Failed download", app_id, depot_id, manifest_gid, exc) manifest.name = depot_name return manifest tasks = [] shared_depots = {} for depot_id, depot_info in iteritems(depots): if not depot_id.isdigit(): continue depot_id = int(depot_id) # if filter_func set, use it to filter the list the depots if filter_func and not filter_func(depot_id, depot_info): continue # if we have no license for the depot, no point trying as we won't get depot_key if not self.has_license_for_depot(depot_id): self._LOG.debug( "No license for depot %s (%s). Skipped", repr(depot_info.get('name', depot_id)), depot_id, ) continue # accumulate the shared depots if 'depotfromapp' in depot_info: shared_depots.setdefault(int(depot_info['depotfromapp']), set()).add(depot_id) continue # process depot, and get manifest for branch if is_enc_branch: egid = depot_info.get('encryptedmanifests', {}).get(branch, {}).get('encrypted_gid_2') if egid is not None: manifest_gid = decrypt_manifest_gid_2( unhexlify(egid), self.beta_passwords[(app_id, branch)]) else: manifest_gid = depot_info.get('manifests', {}).get('public') else: manifest_gid = depot_info.get('manifests', {}).get(branch) if manifest_gid is not None: tasks.append( self.gpool.spawn( async_fetch_manifest, app_id, depot_id, manifest_gid, decrypt, depot_info.get('name', depot_id), branch_name=branch, branch_pass= None, # TODO: figure out how to pass this correctly )) # collect results manifests = [] for task in tasks: result = task.get() if isinstance(result, ManifestError): raise result manifests.append(result) # load shared depot manifests for app_id, depot_ids in iteritems(shared_depots): def nested_ffunc(depot_id, depot_info, depot_ids=depot_ids, ffunc=filter_func): return (int(depot_id) in depot_ids and (ffunc is None or ffunc(depot_id, depot_info))) manifests += self.get_manifests(app_id, filter_func=nested_ffunc) return manifests
def get_manifests(self, app_id, branch='public', password=None, filter_func=None, decrypt=True): """Get a list of CDNDepotManifest for app :param app_id: App ID :type app_id: int :param branch: branch name :type branch: str :param password: branch password for locked branches :type password: str :param filter_func: Function to filter depots. ``func(depot_id, depot_info)`` :returns: list of :class:`.CDNDepotManifest` :rtype: :class:`list` [:class:`.CDNDepotManifest`] :raises SteamError: error message """ depots = self.get_app_depot_info(app_id) is_enc_branch = False if branch not in depots['branches']: raise SteamError("No branch named %s for app_id %s" % (repr(branch), app_id)) elif int(depots['branches'][branch].get('pwdrequired', 0)) > 0: is_enc_branch = True if (app_id, branch) not in self.beta_passwords: if not password: raise SteamError("Branch %r requires a password" % branch) result = self.check_beta_password(app_id, password) if result != EResult.OK: raise SteamError("Branch password is not valid. %r" % result) if (app_id, branch) not in self.beta_passwords: raise SteamError("Incorrect password for branch %r" % branch) def async_fetch_manifest(app_id, depot_id, manifest_gid, decrypt, name): manifest = self.get_manifest(app_id, depot_id, manifest_gid, decrypt) manifest.name = name return manifest tasks = [] shared_depots = {} for depot_id, depot_info in iteritems(depots): if not depot_id.isdigit(): continue depot_id = int(depot_id) # if filter_func set, use it to filter the list the depots if filter_func and not filter_func(depot_id, depot_info): continue # if we have no license for the depot, no point trying as we won't get depot_key if decrypt and depot_id not in self.licensed_depot_ids: self._LOG.debug( "No license for depot %s (%s). Skipping...", repr(depot_info['name']), depot_id, ) continue # accumulate the shared depots if 'depotfromapp' in depot_info: shared_depots.setdefault(int(depot_info['depotfromapp']), set()).add(depot_id) continue # process depot, and get manifest for branch if is_enc_branch: egid = depot_info.get('encryptedmanifests', {}).get(branch, {}).get('encrypted_gid_2') if egid is not None: manifest_gid = decrypt_manifest_gid_2( unhexlify(egid), self.beta_passwords[(app_id, branch)]) else: manifest_gid = depot_info.get('manifests', {}).get('public') else: manifest_gid = depot_info.get('manifests', {}).get(branch) if manifest_gid is not None: tasks.append( self.gpool.spawn( async_fetch_manifest, app_id, depot_id, manifest_gid, decrypt, depot_info['name'], )) # collect results manifests = [] for task in tasks: manifests.append(task.get()) # try: # result = task.get() # except SteamError as exp: # self._LOG.error("Error: %s", exp) # raise # else: # if isinstance(result, list): # manifests.extend(result) # else: # manifests.append(result) # load shared depot manifests for app_id, depot_ids in iteritems(shared_depots): def nested_ffunc(depot_id, depot_info, depot_ids=depot_ids, ffunc=filter_func): return (int(depot_id) in depot_ids and (ffunc is None or ffunc(depot_id, depot_info))) manifests += self.get_manifests(app_id, filter_func=nested_ffunc) return manifests
def init_clients(args): s = CachingSteamClient() if args.cell_id is not None: s.cell_id = args.cell_id cdn = s.get_cdnclient() # short-curcuit everything, if we pass manifest file(s) if getattr(args, 'file', None): manifests = [] for fp in args.file: manifests.append(CTLDepotManifest(cdn, args.app or -1, fp.read())) yield None, None, manifests return # for everything else we need SteamClient and CDNClient if not args.app: raise SteamError("No app id specified") # only login when we may need it if (not args.skip_login # user requested no login and (not args.app or not args.depot or not args.manifest or args.depot not in cdn.depot_keys)): result = s.login_from_args(args) if result == EResult.OK: LOG.info("Login to Steam successful") else: raise SteamError("Failed to login: %r" % result) else: LOG.info("Skipping login") # when app, depot, and manifest are specified, we can just go to CDN if args.app and args.depot and args.manifest: # we can only decrypt if SteamClient is logged in, or we have depot key cached if args.skip_login and args.depot not in cdn.depot_keys: decrypt = False else: decrypt = True # load the manifest try: manifests = [ cdn.get_manifest(args.app, args.depot, args.manifest, decrypt=decrypt) ] except SteamError as exp: if exp.eresult == EResult.AccessDenied: raise SteamError( "This account doesn't have access to the app depot", exp.eresult) elif 'HTTP Error 404' in str(exp): raise SteamError("Manifest not found on CDN") else: raise # if only app is specified, or app and depot, we need product info to figure out manifests else: # no license, means no depot keys, and possibly not product info if not args.skip_licenses: LOG.info("Checking licenses") if s.logged_on and not s.licenses and s.steam_id.type != s.steam_id.EType.AnonUser: s.wait_event(EMsg.ClientLicenseList, raises=False, timeout=10) cdn.load_licenses() if args.app not in cdn.licensed_app_ids: raise SteamError( "No license available for App ID: %s" % args.app, EResult.AccessDenied) # check if we need to invalidate the cache data if not args.skip_login: LOG.info("Checking change list") cdn.check_for_changes() # handle any filtering on depot list def depot_filter(depot_id, info): if args.depot is not None: if args.depot != depot_id: return False if args.os != 'any': if args.os[-2:] == '64': os, arch = args.os[:-2], args.os[-2:] else: os, arch = args.os, None config = info.get('config', {}) if 'oslist' in config and (os not in config['oslist'].split(',')): return False if 'osarch' in config and config['osarch'] != arch: return False return True if args.skip_login: if cdn.has_cached_app_depot_info(args.app): LOG.info("Using cached app info") else: raise SteamError("No cached app info. Login to Steam") branch = args.branch password = args.password LOG.info("Getting manifests for %s branch", repr(branch)) # enumerate manifests manifests = [] for manifest in cdn.get_manifests(args.app, branch=branch, password=password, filter_func=depot_filter, decrypt=False): if not args.skip_licenses and manifest.depot_id not in cdn.licensed_depot_ids: LOG.error("No license for depot: %r" % manifest) continue if manifest.filenames_encrypted: if not args.skip_login: try: manifest.decrypt_filenames( cdn.get_depot_key(manifest.app_id, manifest.depot_id)) except Exception as exp: LOG.error( "Failed to decrypt manifest %s (depot %s): %s", manifest.gid, manifest.depot_id, str(exp)) if not args.skip_licenses: continue manifests.append(manifest) LOG.debug("Got manifests: %r", manifests) yield s, cdn, manifests # clean and exit cdn.save_cache() s.disconnect()