Skip to content

Commit eb8930a

Browse files
implement manifest request codes
1 parent 2a572c9 commit eb8930a

File tree

2 files changed

+144
-31
lines changed

2 files changed

+144
-31
lines changed

steam/client/cdn.py

+106-31
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
from gevent.pool import Pool as GPool
107107
from cachetools import LRUCache
108108
from steam import webapi
109-
from steam.exceptions import SteamError
109+
from steam.exceptions import SteamError, ManifestError
110110
from steam.core.msg import MsgProto
111111
from steam.enums import EResult, EType
112112
from steam.enums.emsg import EMsg
@@ -628,7 +628,48 @@ def get_chunk(self, app_id, depot_id, chunk_id):
628628

629629
return self._chunk_cache[(depot_id, chunk_id)]
630630

631-
def get_manifest(self, app_id, depot_id, manifest_gid, decrypt=True):
631+
def get_manifest_request_code(self, app_id, depot_id, manifest_gid, branch='public', branch_password_hash=None):
632+
"""Get manifest request code for authenticating manifest download
633+
634+
:param app_id: App ID
635+
:type app_id: int
636+
:param depot_id: Depot ID
637+
:type depot_id: int
638+
:param manifest_gid: Manifest gid
639+
:type manifest_gid: int
640+
:param branch: (optional) branch name
641+
:type branch: str
642+
:param branch_password_hash: (optional) branch password hash
643+
:type branch_password_hash: str
644+
:returns: manifest request code
645+
:rtype: int
646+
"""
647+
648+
body = {
649+
"app_id": int(app_id),
650+
"depot_id": int(depot_id),
651+
"manifest_id": int(manifest_gid),
652+
}
653+
654+
if branch and branch.lower() != 'public':
655+
body['app_branch'] = branch
656+
657+
if branch_password_hash:
658+
body['branch_password_hash'] = branch_password_hash
659+
660+
resp = self.steam.send_um_and_wait(
661+
'ContentServerDirectory.GetManifestRequestCode#1',
662+
body,
663+
timeout=5,
664+
)
665+
666+
if resp is None or resp.header.eresult != EResult.OK:
667+
raise SteamError("Failed to get manifest code for %s, %s, %s" % (app_id, depot_id, manifest_gid),
668+
EResult.Timeout if resp is None else EResult(resp.header.eresult))
669+
670+
return resp.body.manifest_request_code
671+
672+
def get_manifest(self, app_id, depot_id, manifest_gid, decrypt=True, manifest_request_code=0):
632673
"""Download a manifest file
633674
634675
:param app_id: App ID
@@ -639,11 +680,16 @@ def get_manifest(self, app_id, depot_id, manifest_gid, decrypt=True):
639680
:type manifest_gid: int
640681
:param decrypt: Decrypt manifest filenames
641682
:type decrypt: bool
683+
:param manifest_request_code: Manifest request code, authenticates the download
684+
:type manifest_request_code: int
642685
:returns: manifest instance
643686
:rtype: :class:`.CDNDepotManifest`
644687
"""
645688
if (app_id, depot_id, manifest_gid) not in self.manifests:
646-
resp = self.cdn_cmd('depot', '%s/manifest/%s/5' % (depot_id, manifest_gid))
689+
if manifest_request_code:
690+
resp = self.cdn_cmd('depot', '%s/manifest/%s/5/%s' % (depot_id, manifest_gid, manifest_request_code))
691+
else:
692+
resp = self.cdn_cmd('depot', '%s/manifest/%s/5' % (depot_id, manifest_gid))
647693

648694
if resp.ok:
649695
manifest = self.DepotManifestClass(self, app_id, resp.content)
@@ -681,6 +727,19 @@ def get_app_depot_info(self, app_id):
681727
self.app_depots[app_id] = self.steam.get_product_info([app_id])['apps'][app_id]['depots']
682728
return self.app_depots[app_id]
683729

730+
def has_license_for_depot(self, depot_id):
731+
""" Check if there is license for depot
732+
733+
:param depot_id: depot ID
734+
:type depot_id: int
735+
:returns: True if we have license
736+
:rtype: bool
737+
"""
738+
if depot_id in self.licensed_depot_ids or depot_id in self.licensed_app_ids:
739+
return True
740+
else:
741+
return False
742+
684743
def get_manifests(self, app_id, branch='public', password=None, filter_func=None, decrypt=True):
685744
"""Get a list of CDNDepotManifest for app
686745
@@ -694,7 +753,7 @@ def get_manifests(self, app_id, branch='public', password=None, filter_func=None
694753
Function to filter depots. ``func(depot_id, depot_info)``
695754
:returns: list of :class:`.CDNDepotManifest`
696755
:rtype: :class:`list` [:class:`.CDNDepotManifest`]
697-
:raises SteamError: error message
756+
:raises: ManifestError, SteamError
698757
"""
699758
depots = self.get_app_depot_info(app_id)
700759

@@ -717,9 +776,24 @@ def get_manifests(self, app_id, branch='public', password=None, filter_func=None
717776
if (app_id, branch) not in self.beta_passwords:
718777
raise SteamError("Incorrect password for branch %r" % branch)
719778

720-
def async_fetch_manifest(app_id, depot_id, manifest_gid, decrypt, name):
721-
manifest = self.get_manifest(app_id, depot_id, manifest_gid, decrypt)
722-
manifest.name = name
779+
def async_fetch_manifest(
780+
app_id, depot_id, manifest_gid, decrypt, depot_name, branch_name, branch_pass
781+
):
782+
try:
783+
manifest_code = self.get_manifest_request_code(
784+
app_id, depot_id, int(manifest_gid), branch_name, branch_pass
785+
)
786+
except SteamError as exc:
787+
return ManifestError("Failed to acquire manifest code", app_id, depot_id, manifest_gid, exc)
788+
789+
try:
790+
manifest = self.get_manifest(
791+
app_id, depot_id, manifest_gid, decrypt=decrypt, manifest_request_code=manifest_code
792+
)
793+
except Exception as exc:
794+
return ManifestError("Failed download", app_id, depot_id, manifest_gid, exc)
795+
796+
manifest.name = depot_name
723797
return manifest
724798

725799
tasks = []
@@ -736,10 +810,8 @@ def async_fetch_manifest(app_id, depot_id, manifest_gid, decrypt, name):
736810
continue
737811

738812
# if we have no license for the depot, no point trying as we won't get depot_key
739-
if (decrypt
740-
and depot_id not in self.licensed_depot_ids
741-
and depot_id not in self.licensed_app_ids):
742-
self._LOG.debug("No license for depot %s (%s). Skipping...",
813+
if not self.has_license_for_depot(depot_id):
814+
self._LOG.debug("No license for depot %s (%s). Skipped",
743815
repr(depot_info.get('name', depot_id)),
744816
depot_id,
745817
)
@@ -764,29 +836,27 @@ def async_fetch_manifest(app_id, depot_id, manifest_gid, decrypt, name):
764836
manifest_gid = depot_info.get('manifests', {}).get(branch)
765837

766838
if manifest_gid is not None:
767-
tasks.append(self.gpool.spawn(async_fetch_manifest,
768-
app_id,
769-
depot_id,
770-
manifest_gid,
771-
decrypt,
772-
depot_info.get('name', depot_id),
773-
))
839+
tasks.append(
840+
self.gpool.spawn(
841+
async_fetch_manifest,
842+
app_id,
843+
depot_id,
844+
manifest_gid,
845+
decrypt,
846+
depot_info.get('name', depot_id),
847+
branch_name=branch,
848+
branch_pass=None, # TODO: figure out how to pass this correctly
849+
)
850+
)
774851

775852
# collect results
776853
manifests = []
777854

778855
for task in tasks:
779-
manifests.append(task.get())
780-
# try:
781-
# result = task.get()
782-
# except SteamError as exp:
783-
# self._LOG.error("Error: %s", exp)
784-
# raise
785-
# else:
786-
# if isinstance(result, list):
787-
# manifests.extend(result)
788-
# else:
789-
# manifests.append(result)
856+
result = task.get()
857+
if isinstance(result, ManifestError):
858+
raise result
859+
manifests.append(result)
790860

791861
# load shared depot manifests
792862
for app_id, depot_ids in iteritems(shared_depots):
@@ -826,7 +896,7 @@ def get_manifest_for_workshop_item(self, item_id):
826896
:type item_id: int
827897
:returns: manifest instance
828898
:rtype: :class:`.CDNDepotManifest`
829-
:raises SteamError: error message
899+
:raises: ManifestError, SteamError
830900
"""
831901
resp = self.steam.send_um_and_wait('PublishedFile.GetDetails#1', {
832902
'publishedfileids': [item_id],
@@ -854,7 +924,12 @@ def get_manifest_for_workshop_item(self, item_id):
854924

855925
app_id = ws_app_id = wf.consumer_appid
856926

857-
manifest = self.get_manifest(app_id, ws_app_id, wf.hcontent_file)
927+
try:
928+
manifest_code = self.get_manifest_request_code(app_id, ws_app_id, int(wf.hcontent_file))
929+
manifest = self.get_manifest(app_id, ws_app_id, wf.hcontent_file, manifest_request_code=manifest_code)
930+
except SteamError as exc:
931+
return ManifestError("Failed to acquire manifest", app_id, depot_id, manifest_gid, exc)
932+
858933
manifest.name = wf.title
859934
return manifest
860935

steam/exceptions.py

+38
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,48 @@
22
from steam.enums import EResult
33

44
class SteamError(Exception):
5+
""" General error that also carries EResult code """
56
def __init__(self, message, eresult=EResult.Fail):
67
Exception.__init__(self, message, eresult)
78
self.message = message
89
self.eresult = EResult(eresult) #: :class:`.EResult`
910

1011
def __str__(self):
1112
return "(%s) %s" % (self.eresult, self.message)
13+
14+
class ManifestError(SteamError):
15+
"""
16+
Raised when there a problem getting a manifest by :class:`CDNClient`
17+
Encapsulates original exception in :attr:`.error` and includes manifest details
18+
"""
19+
def __init__(self, message, app_id, depot_id, manifest_gid, error=None):
20+
self.message = message
21+
self.app_id = app_id
22+
self.depot_id = depot_id
23+
self.manifest_gid = manifest_gid
24+
self.error = error
25+
26+
if isinstance(error, SteamError):
27+
self.eresult = error.eresult
28+
else:
29+
self.eresult = EResult.Fail
30+
31+
def __repr__(self):
32+
return "%s(%s, app=%s, depot=%s, manifest=%s, error=%s)" % (
33+
self.__class__.__name__,
34+
repr(self.message),
35+
self.app_id,
36+
self.depot_id,
37+
self.manifest_gid,
38+
repr(self.error),
39+
)
40+
41+
def __str__(self):
42+
return "(%s) %s (app=%s depot=%s manifest=%s)" % (
43+
self.eresult,
44+
self.message,
45+
self.app_id,
46+
self.depot_id,
47+
self.manifest_gid,
48+
)
49+

0 commit comments

Comments
 (0)