예제 #1
0
파일: gcmds.py 프로젝트: int3l/steamctl
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,
                    ))
예제 #2
0
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
예제 #3
0
    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]
예제 #4
0
    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
예제 #5
0
    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]
예제 #6
0
    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
예제 #7
0
파일: cdn.py 프로젝트: oczkers/steam-httpx
    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)]
예제 #8
0
    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
        }
예제 #9
0
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))
예제 #10
0
파일: cdn.py 프로젝트: oczkers/steam-httpx
    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)
예제 #11
0
    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)
예제 #12
0
파일: cdn.py 프로젝트: oczkers/steam-httpx
    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")
예제 #13
0
    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
예제 #14
0
파일: cdn.py 프로젝트: oczkers/steam-httpx
    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]
예제 #15
0
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()
예제 #16
0
    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']
예제 #17
0
 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))
예제 #18
0
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')
예제 #19
0
    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
예제 #20
0
파일: cdn.py 프로젝트: oczkers/steam-httpx
    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
예제 #21
0
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()