Skip to content

Commit cc8310a

Browse files
Merge pull request #5 from detiam/solsticegamestudios_fork
Fixes #4
2 parents 29c5cbc + 42aec98 commit cc8310a

10 files changed

Lines changed: 372 additions & 55 deletions

File tree

protobufs/steammessages_contentsystem.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,23 @@ message CContentServerDirectory_GetManifestRequestCode_Response {
6868
optional uint64 manifest_request_code = 1;
6969
}
7070

71+
message CContentServerDirectory_GetCDNAuthToken_Request {
72+
optional uint32 depot_id = 1;
73+
optional string host_name = 2;
74+
optional uint32 app_id = 3;
75+
}
76+
77+
message CContentServerDirectory_GetCDNAuthToken_Response {
78+
optional string token = 1;
79+
optional uint32 expiration_time = 2;
80+
}
81+
7182
service ContentServerDirectory {
7283
option (service_description) = "Content Server and CDN directory";
7384

7485
rpc GetServersForSteamPipe (.CContentServerDirectory_GetServersForSteamPipe_Request) returns (.CContentServerDirectory_GetServersForSteamPipe_Response);
7586
rpc GetDepotPatchInfo (.CContentServerDirectory_GetDepotPatchInfo_Request) returns (.CContentServerDirectory_GetDepotPatchInfo_Response);
7687
rpc GetClientUpdateHosts (.CContentServerDirectory_GetClientUpdateHosts_Request) returns (.CContentServerDirectory_GetClientUpdateHosts_Response);
7788
rpc GetManifestRequestCode (.CContentServerDirectory_GetManifestRequestCode_Request) returns (.CContentServerDirectory_GetManifestRequestCode_Response);
89+
rpc GetCDNAuthToken (.CContentServerDirectory_GetCDNAuthToken_Request) returns (.CContentServerDirectory_GetCDNAuthToken_Response);
7890
}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ cachetools>=3.0.0
77
gevent>=1.3.0
88
protobuf~=3.0
99
gevent-eventemitter~=2.1
10+
wsproto~=1.2.0

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'gevent>=1.3.0',
2727
'protobuf~=3.0',
2828
'gevent-eventemitter~=2.1',
29+
'wsproto~=1.2.0',
2930
],
3031
}
3132

steam/client/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ class SteamClient(CMClient, BuiltinBase):
4343
username = None #: username when logged on
4444
chat_mode = 2 #: chat mode (0=old chat, 2=new chat)
4545

46-
def __init__(self):
47-
CMClient.__init__(self)
46+
def __init__(self, protocol=CMClient.PROTOCOL_TCP):
47+
CMClient.__init__(self, protocol=protocol)
4848

4949
# register listners
5050
self.on(self.EVENT_DISCONNECTED, self._handle_disconnect)

steam/client/builtins/friends.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from eventemitter import EventEmitter
3-
from steam.steamid import SteamID, intBase
3+
from steam.steamid import SteamID
44
from steam.enums import EResult, EFriendRelationship
55
from steam.enums.emsg import EMsg
66
from steam.core.msg import MsgProto
@@ -156,7 +156,7 @@ def add(self, steamid_or_accountname_or_email):
156156
"""
157157
m = MsgProto(EMsg.ClientAddFriend)
158158

159-
if isinstance(steamid_or_accountname_or_email, (intBase, int)):
159+
if isinstance(steamid_or_accountname_or_email, int):
160160
m.body.steamid_to_add = steamid_or_accountname_or_email
161161
elif isinstance(steamid_or_accountname_or_email, SteamUser):
162162
m.body.steamid_to_add = steamid_or_accountname_or_email.steam_id

steam/client/builtins/leaderboards.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from steam.core.msg import MsgProto
66
from steam.enums import EResult, ELeaderboardDataRequest, ELeaderboardSortMethod, ELeaderboardDisplayType
77
from steam.enums.emsg import EMsg
8-
from steam.utils import _range, chunks
8+
from steam.utils import chunks
99
from steam.utils.throttle import ConstantRateLimit
1010

1111

@@ -158,7 +158,7 @@ def __getitem__(self, x):
158158
entries = self.get_entries(start+1, stop)
159159

160160
if isinstance(x, slice):
161-
return [entries[i] for i in _range(0, len(entries), step)]
161+
return [entries[i] for i in range(0, len(entries), step)]
162162
else:
163163
return entries[0]
164164

steam/client/cdn.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ def __init__(self, client):
462462
self.cell_id = self.steam.cell_id
463463

464464
self.web = make_requests_session()
465+
self.cdn_auth_tokens = {} #: CDN authentication token
465466
self.depot_keys = {} #: depot decryption keys
466467
self.manifests = {} #: CDNDepotManifest instances
467468
self.app_depots = {} #: app depot info
@@ -526,6 +527,51 @@ def get_content_server(self, rotate=False):
526527
self.servers.rotate(-1)
527528
return self.servers[0]
528529

530+
def get_cdn_auth_token(self, app_id, depot_id, hostname):
531+
"""Get CDN authentication token
532+
533+
:param app_id: app id
534+
:type app_id: :class:`int`
535+
:param depot_id: depot id
536+
:type depot_id: :class:`int`
537+
:param hostname: cdn hostname
538+
:type hostname: :class:`str`
539+
:return: CDN authentication token
540+
:rtype: str
541+
"""
542+
def update_cdn_auth_tokens():
543+
resp = self.steam.send_um_and_wait('ContentServerDirectory.GetCDNAuthToken#1', {
544+
'app_id': app_id,
545+
'depot_id': depot_id,
546+
'host_name': hostname
547+
}, timeout=10)
548+
549+
if resp is None or resp.header.eresult != EResult.OK:
550+
if resp.header.eresult == EResult.Fail:
551+
# no need authtoken?
552+
pass
553+
else:
554+
raise SteamError(f"Failed to get CDNAuthToken for {app_id}, {depot_id}, {hostname}",
555+
EResult.Timeout if resp is None else EResult(resp.header.eresult))
556+
557+
self.cdn_auth_tokens.update({app_id:{depot_id:{hostname: {
558+
'eresult': resp.header.eresult,
559+
'token': resp.body.token or '',
560+
'expiration_time': resp.body.expiration_time or 0
561+
}}}})
562+
563+
if app_id not in self.cdn_auth_tokens or \
564+
depot_id not in self.cdn_auth_tokens[app_id] or \
565+
hostname not in self.cdn_auth_tokens[app_id][depot_id]:
566+
update_cdn_auth_tokens()
567+
else:
568+
if self.cdn_auth_tokens[app_id][depot_id][hostname]['eresult'] != EResult.OK:
569+
pass
570+
elif datetime.fromtimestamp(self.cdn_auth_tokens[app_id][depot_id][hostname]['expiration_time'] - 60) < datetime.now():
571+
update_cdn_auth_tokens()
572+
573+
return self.cdn_auth_tokens[app_id][depot_id][hostname]['token']
574+
529575
def get_depot_key(self, app_id, depot_id):
530576
"""Get depot key, which is needed to decrypt files
531577
@@ -548,26 +594,31 @@ def get_depot_key(self, app_id, depot_id):
548594

549595
return self.depot_keys[depot_id]
550596

551-
def cdn_cmd(self, command, args):
597+
def cdn_cmd(self, command, args, app_id=None, depot_id=None):
552598
"""Run CDN command request
553599
554600
:param command: command name
555601
:type command: str
556602
:param args: args
557603
:type args: str
604+
:param args: app_id: (optional) required for CDN authentication token
605+
:type args: int
606+
:param args: depot_id: (optional) required for CDN authentication token
607+
:type args: int
558608
:returns: requests response
559609
:rtype: :class:`requests.Response`
560610
:raises SteamError: on error
561611
"""
562612
server = self.get_content_server()
563613

564614
while True:
565-
url = "{}://{}:{}/{}/{}".format(
615+
url = "{}://{}:{}/{}/{}{}".format(
566616
'https' if server.https else 'http',
567617
server.host,
568618
server.port,
569619
command,
570620
args,
621+
self.get_cdn_auth_token(app_id, depot_id, str(server.host))
571622
)
572623

573624
try:
@@ -598,7 +649,7 @@ def get_chunk(self, app_id, depot_id, chunk_id):
598649
:raises SteamError: error message
599650
"""
600651
if (depot_id, chunk_id) not in self._chunk_cache:
601-
resp = self.cdn_cmd('depot', f'{depot_id}/chunk/{chunk_id}')
652+
resp = self.cdn_cmd('depot', f'{depot_id}/chunk/{chunk_id}', app_id, depot_id)
602653

603654
data = symmetric_decrypt(resp.content, self.get_depot_key(app_id, depot_id))
604655

@@ -684,9 +735,9 @@ def get_manifest(self, app_id, depot_id, manifest_gid, decrypt=True, manifest_re
684735
"""
685736
if (app_id, depot_id, manifest_gid) not in self.manifests:
686737
if manifest_request_code:
687-
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5/{manifest_request_code}')
738+
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5/{manifest_request_code}', app_id, depot_id)
688739
else:
689-
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5')
740+
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5', app_id, depot_id)
690741

691742
if resp.ok:
692743
manifest = self.DepotManifestClass(self, app_id, resp.content)
@@ -776,6 +827,11 @@ def get_manifests(self, app_id, branch='public', password=None, filter_func=None
776827
def async_fetch_manifest(
777828
app_id, depot_id, manifest_gid, decrypt, depot_name, branch_name, branch_pass
778829
):
830+
if isinstance(manifest_gid, dict):
831+
# For some depots, Steam has started returning a dict
832+
# {"public": {"gid": GID, "size": ..., "download": ...}, ...}
833+
# instead of a simple map {"public": GID, ...}
834+
manifest_gid = manifest_gid['gid']
779835
try:
780836
manifest_code = self.get_manifest_request_code(
781837
app_id, depot_id, int(manifest_gid), branch_name, branch_pass

steam/core/cm.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from steam.enums import EResult, EUniverse
1717
from steam.enums.emsg import EMsg
1818
from steam.core import crypto
19-
from steam.core.connection import TCPConnection
19+
from steam.core.connection import TCPConnection, WebsocketConnection
2020
from steam.core.msg import Msg, MsgProto
2121
from eventemitter import EventEmitter
2222
from steam.utils import ip4_from_int
@@ -59,6 +59,7 @@ class CMClient(EventEmitter):
5959

6060
PROTOCOL_TCP = 0 #: TCP protocol enum
6161
PROTOCOL_UDP = 1 #: UDP protocol enum
62+
PROTOCOL_WEBSOCKET = 2 #: WEBSOCKET protocol enum
6263
verbose_debug = False #: print message connects in debug
6364

6465
auto_discovery = True #: enables automatic CM discovery
@@ -83,10 +84,12 @@ class CMClient(EventEmitter):
8384
def __init__(self, protocol=PROTOCOL_TCP):
8485
self.cm_servers = CMServerList()
8586

86-
if protocol == CMClient.PROTOCOL_TCP:
87+
if protocol == CMClient.PROTOCOL_WEBSOCKET:
88+
self.connection = WebsocketConnection()
89+
elif protocol == CMClient.PROTOCOL_TCP:
8790
self.connection = TCPConnection()
8891
else:
89-
raise ValueError("Only TCP is supported")
92+
raise ValueError("Only Websocket and TCP are supported")
9093

9194
self.on(EMsg.ChannelEncryptRequest, self.__handle_encrypt_request),
9295
self.on(EMsg.Multi, self.__handle_multi),
@@ -132,8 +135,11 @@ def connect(self, retry=0, delay=0):
132135
self._connecting = False
133136
return False
134137

135-
if not self.cm_servers.bootstrap_from_webapi():
136-
self.cm_servers.bootstrap_from_dns()
138+
if isinstance(self.connection, WebsocketConnection):
139+
self.cm_servers.bootstrap_from_webapi(cmtype='websockets')
140+
elif isinstance(self.connection, TCPConnection):
141+
if not self.cm_servers.bootstrap_from_webapi():
142+
self.cm_servers.bootstrap_from_dns()
137143

138144
for i, server_addr in enumerate(cycle(self.cm_servers), start=next(i)-1):
139145
if retry and i >= retry:
@@ -154,6 +160,12 @@ def connect(self, retry=0, delay=0):
154160
self.current_server_addr = server_addr
155161
self.connected = True
156162
self.emit(self.EVENT_CONNECTED)
163+
164+
# WebsocketConnection secures itself
165+
if isinstance(self.connection, WebsocketConnection):
166+
self.channel_secured = True
167+
self.emit(self.EVENT_CHANNEL_SECURED)
168+
157169
self._recv_loop = gevent.spawn(self._recv_messages)
158170
self._connecting = False
159171
return True
@@ -472,26 +484,33 @@ def bootstrap_from_dns(self):
472484
self._LOG.error("DNS boostrap: cm0.steampowered.com resolved no A records")
473485
return False
474486

475-
def bootstrap_from_webapi(self, cell_id=0):
487+
def bootstrap_from_webapi(self, cell_id=0, cmtype='netfilter'):
476488
"""
477489
Fetches CM server list from WebAPI and replaces the current one
478490
479491
:param cellid: cell id (0 = global)
480492
:type cellid: :class:`int`
493+
:param cmtype: CM type filter
494+
:type cellid: :class:`str`
481495
:return: booststrap success
482496
:rtype: :class:`bool`
483497
"""
484-
self._LOG.debug("Attempting bootstrap via WebAPI")
498+
self._LOG.debug("Attempting bootstrap via WebAPI for %s" % cmtype)
485499

486500
from steam import webapi
487501
try:
488-
resp = webapi.get('ISteamDirectory', 'GetCMList', 1, params={'cellid': cell_id,
489-
'http_timeout': 3})
502+
resp = webapi.get('ISteamDirectory', 'GetCMListForConnect', 1,
503+
params={
504+
'cellid': cell_id,
505+
'cmtype': cmtype,
506+
'http_timeout': 3
507+
}
508+
)
490509
except Exception as exp:
491510
self._LOG.error("WebAPI boostrap failed: %s" % str(exp))
492511
return False
493512

494-
result = EResult(resp['response']['result'])
513+
result = EResult(resp['response']['success'])
495514

496515
if result != EResult.OK:
497516
self._LOG.error("GetCMList failed with %s" % repr(result))
@@ -500,16 +519,16 @@ def bootstrap_from_webapi(self, cell_id=0):
500519
serverlist = resp['response']['serverlist']
501520
self._LOG.debug("Received %d servers from WebAPI" % len(serverlist))
502521

503-
def str_to_tuple(serveraddr):
504-
ip, port = serveraddr.split(':')
522+
def str_to_tuple(serverinfo):
523+
ip, port = serverinfo['endpoint'].split(':')
505524
return str(ip), int(port)
506525

507526
self.clear()
508527
self.cell_id = cell_id
509528
self.merge_list(map(str_to_tuple, serverlist))
510529

511530
return True
512-
531+
513532
def __iter__(self):
514533
def cm_server_iter():
515534
if not self.list:

0 commit comments

Comments
 (0)