Skip to content

Commit

Permalink
Merge pull request #5 from detiam/solsticegamestudios_fork
Browse files Browse the repository at this point in the history
Fixes #4
  • Loading branch information
WinterPhoenix authored Oct 22, 2024
2 parents 29c5cbc + 42aec98 commit cc8310a
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 55 deletions.
12 changes: 12 additions & 0 deletions protobufs/steammessages_contentsystem.proto
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,23 @@ message CContentServerDirectory_GetManifestRequestCode_Response {
optional uint64 manifest_request_code = 1;
}

message CContentServerDirectory_GetCDNAuthToken_Request {
optional uint32 depot_id = 1;
optional string host_name = 2;
optional uint32 app_id = 3;
}

message CContentServerDirectory_GetCDNAuthToken_Response {
optional string token = 1;
optional uint32 expiration_time = 2;
}

service ContentServerDirectory {
option (service_description) = "Content Server and CDN directory";

rpc GetServersForSteamPipe (.CContentServerDirectory_GetServersForSteamPipe_Request) returns (.CContentServerDirectory_GetServersForSteamPipe_Response);
rpc GetDepotPatchInfo (.CContentServerDirectory_GetDepotPatchInfo_Request) returns (.CContentServerDirectory_GetDepotPatchInfo_Response);
rpc GetClientUpdateHosts (.CContentServerDirectory_GetClientUpdateHosts_Request) returns (.CContentServerDirectory_GetClientUpdateHosts_Response);
rpc GetManifestRequestCode (.CContentServerDirectory_GetManifestRequestCode_Request) returns (.CContentServerDirectory_GetManifestRequestCode_Response);
rpc GetCDNAuthToken (.CContentServerDirectory_GetCDNAuthToken_Request) returns (.CContentServerDirectory_GetCDNAuthToken_Response);
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ cachetools>=3.0.0
gevent>=1.3.0
protobuf~=3.0
gevent-eventemitter~=2.1
wsproto~=1.2.0
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'gevent>=1.3.0',
'protobuf~=3.0',
'gevent-eventemitter~=2.1',
'wsproto~=1.2.0',
],
}

Expand Down
4 changes: 2 additions & 2 deletions steam/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class SteamClient(CMClient, BuiltinBase):
username = None #: username when logged on
chat_mode = 2 #: chat mode (0=old chat, 2=new chat)

def __init__(self):
CMClient.__init__(self)
def __init__(self, protocol=CMClient.PROTOCOL_TCP):
CMClient.__init__(self, protocol=protocol)

# register listners
self.on(self.EVENT_DISCONNECTED, self._handle_disconnect)
Expand Down
4 changes: 2 additions & 2 deletions steam/client/builtins/friends.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from eventemitter import EventEmitter
from steam.steamid import SteamID, intBase
from steam.steamid import SteamID
from steam.enums import EResult, EFriendRelationship
from steam.enums.emsg import EMsg
from steam.core.msg import MsgProto
Expand Down Expand Up @@ -156,7 +156,7 @@ def add(self, steamid_or_accountname_or_email):
"""
m = MsgProto(EMsg.ClientAddFriend)

if isinstance(steamid_or_accountname_or_email, (intBase, int)):
if isinstance(steamid_or_accountname_or_email, int):
m.body.steamid_to_add = steamid_or_accountname_or_email
elif isinstance(steamid_or_accountname_or_email, SteamUser):
m.body.steamid_to_add = steamid_or_accountname_or_email.steam_id
Expand Down
4 changes: 2 additions & 2 deletions steam/client/builtins/leaderboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from steam.core.msg import MsgProto
from steam.enums import EResult, ELeaderboardDataRequest, ELeaderboardSortMethod, ELeaderboardDisplayType
from steam.enums.emsg import EMsg
from steam.utils import _range, chunks
from steam.utils import chunks
from steam.utils.throttle import ConstantRateLimit


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

if isinstance(x, slice):
return [entries[i] for i in _range(0, len(entries), step)]
return [entries[i] for i in range(0, len(entries), step)]
else:
return entries[0]

Expand Down
66 changes: 61 additions & 5 deletions steam/client/cdn.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ def __init__(self, client):
self.cell_id = self.steam.cell_id

self.web = make_requests_session()
self.cdn_auth_tokens = {} #: CDN authentication token
self.depot_keys = {} #: depot decryption keys
self.manifests = {} #: CDNDepotManifest instances
self.app_depots = {} #: app depot info
Expand Down Expand Up @@ -526,6 +527,51 @@ def get_content_server(self, rotate=False):
self.servers.rotate(-1)
return self.servers[0]

def get_cdn_auth_token(self, app_id, depot_id, hostname):
"""Get CDN authentication token
:param app_id: app id
:type app_id: :class:`int`
:param depot_id: depot id
:type depot_id: :class:`int`
:param hostname: cdn hostname
:type hostname: :class:`str`
:return: CDN authentication token
:rtype: str
"""
def update_cdn_auth_tokens():
resp = self.steam.send_um_and_wait('ContentServerDirectory.GetCDNAuthToken#1', {
'app_id': app_id,
'depot_id': depot_id,
'host_name': hostname
}, timeout=10)

if resp is None or resp.header.eresult != EResult.OK:
if resp.header.eresult == EResult.Fail:
# no need authtoken?
pass
else:
raise SteamError(f"Failed to get CDNAuthToken for {app_id}, {depot_id}, {hostname}",
EResult.Timeout if resp is None else EResult(resp.header.eresult))

self.cdn_auth_tokens.update({app_id:{depot_id:{hostname: {
'eresult': resp.header.eresult,
'token': resp.body.token or '',
'expiration_time': resp.body.expiration_time or 0
}}}})

if app_id not in self.cdn_auth_tokens or \
depot_id not in self.cdn_auth_tokens[app_id] or \
hostname not in self.cdn_auth_tokens[app_id][depot_id]:
update_cdn_auth_tokens()
else:
if self.cdn_auth_tokens[app_id][depot_id][hostname]['eresult'] != EResult.OK:
pass
elif datetime.fromtimestamp(self.cdn_auth_tokens[app_id][depot_id][hostname]['expiration_time'] - 60) < datetime.now():
update_cdn_auth_tokens()

return self.cdn_auth_tokens[app_id][depot_id][hostname]['token']

def get_depot_key(self, app_id, depot_id):
"""Get depot key, which is needed to decrypt files
Expand All @@ -548,26 +594,31 @@ def get_depot_key(self, app_id, depot_id):

return self.depot_keys[depot_id]

def cdn_cmd(self, command, args):
def cdn_cmd(self, command, args, app_id=None, depot_id=None):
"""Run CDN command request
:param command: command name
:type command: str
:param args: args
:type args: str
:param args: app_id: (optional) required for CDN authentication token
:type args: int
:param args: depot_id: (optional) required for CDN authentication token
:type args: int
:returns: requests response
:rtype: :class:`requests.Response`
:raises SteamError: on error
"""
server = self.get_content_server()

while True:
url = "{}://{}:{}/{}/{}".format(
url = "{}://{}:{}/{}/{}{}".format(
'https' if server.https else 'http',
server.host,
server.port,
command,
args,
self.get_cdn_auth_token(app_id, depot_id, str(server.host))
)

try:
Expand Down Expand Up @@ -598,7 +649,7 @@ def get_chunk(self, app_id, depot_id, chunk_id):
:raises SteamError: error message
"""
if (depot_id, chunk_id) not in self._chunk_cache:
resp = self.cdn_cmd('depot', f'{depot_id}/chunk/{chunk_id}')
resp = self.cdn_cmd('depot', f'{depot_id}/chunk/{chunk_id}', app_id, depot_id)

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

Expand Down Expand Up @@ -684,9 +735,9 @@ def get_manifest(self, app_id, depot_id, manifest_gid, decrypt=True, manifest_re
"""
if (app_id, depot_id, manifest_gid) not in self.manifests:
if manifest_request_code:
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5/{manifest_request_code}')
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5/{manifest_request_code}', app_id, depot_id)
else:
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5')
resp = self.cdn_cmd('depot', f'{depot_id}/manifest/{manifest_gid}/5', app_id, depot_id)

if resp.ok:
manifest = self.DepotManifestClass(self, app_id, resp.content)
Expand Down Expand Up @@ -776,6 +827,11 @@ def get_manifests(self, app_id, branch='public', password=None, filter_func=None
def async_fetch_manifest(
app_id, depot_id, manifest_gid, decrypt, depot_name, branch_name, branch_pass
):
if isinstance(manifest_gid, dict):
# For some depots, Steam has started returning a dict
# {"public": {"gid": GID, "size": ..., "download": ...}, ...}
# instead of a simple map {"public": GID, ...}
manifest_gid = manifest_gid['gid']
try:
manifest_code = self.get_manifest_request_code(
app_id, depot_id, int(manifest_gid), branch_name, branch_pass
Expand Down
45 changes: 32 additions & 13 deletions steam/core/cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from steam.enums import EResult, EUniverse
from steam.enums.emsg import EMsg
from steam.core import crypto
from steam.core.connection import TCPConnection
from steam.core.connection import TCPConnection, WebsocketConnection
from steam.core.msg import Msg, MsgProto
from eventemitter import EventEmitter
from steam.utils import ip4_from_int
Expand Down Expand Up @@ -59,6 +59,7 @@ class CMClient(EventEmitter):

PROTOCOL_TCP = 0 #: TCP protocol enum
PROTOCOL_UDP = 1 #: UDP protocol enum
PROTOCOL_WEBSOCKET = 2 #: WEBSOCKET protocol enum
verbose_debug = False #: print message connects in debug

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

if protocol == CMClient.PROTOCOL_TCP:
if protocol == CMClient.PROTOCOL_WEBSOCKET:
self.connection = WebsocketConnection()
elif protocol == CMClient.PROTOCOL_TCP:
self.connection = TCPConnection()
else:
raise ValueError("Only TCP is supported")
raise ValueError("Only Websocket and TCP are supported")

self.on(EMsg.ChannelEncryptRequest, self.__handle_encrypt_request),
self.on(EMsg.Multi, self.__handle_multi),
Expand Down Expand Up @@ -132,8 +135,11 @@ def connect(self, retry=0, delay=0):
self._connecting = False
return False

if not self.cm_servers.bootstrap_from_webapi():
self.cm_servers.bootstrap_from_dns()
if isinstance(self.connection, WebsocketConnection):
self.cm_servers.bootstrap_from_webapi(cmtype='websockets')
elif isinstance(self.connection, TCPConnection):
if not self.cm_servers.bootstrap_from_webapi():
self.cm_servers.bootstrap_from_dns()

for i, server_addr in enumerate(cycle(self.cm_servers), start=next(i)-1):
if retry and i >= retry:
Expand All @@ -154,6 +160,12 @@ def connect(self, retry=0, delay=0):
self.current_server_addr = server_addr
self.connected = True
self.emit(self.EVENT_CONNECTED)

# WebsocketConnection secures itself
if isinstance(self.connection, WebsocketConnection):
self.channel_secured = True
self.emit(self.EVENT_CHANNEL_SECURED)

self._recv_loop = gevent.spawn(self._recv_messages)
self._connecting = False
return True
Expand Down Expand Up @@ -472,26 +484,33 @@ def bootstrap_from_dns(self):
self._LOG.error("DNS boostrap: cm0.steampowered.com resolved no A records")
return False

def bootstrap_from_webapi(self, cell_id=0):
def bootstrap_from_webapi(self, cell_id=0, cmtype='netfilter'):
"""
Fetches CM server list from WebAPI and replaces the current one
:param cellid: cell id (0 = global)
:type cellid: :class:`int`
:param cmtype: CM type filter
:type cellid: :class:`str`
:return: booststrap success
:rtype: :class:`bool`
"""
self._LOG.debug("Attempting bootstrap via WebAPI")
self._LOG.debug("Attempting bootstrap via WebAPI for %s" % cmtype)

from steam import webapi
try:
resp = webapi.get('ISteamDirectory', 'GetCMList', 1, params={'cellid': cell_id,
'http_timeout': 3})
resp = webapi.get('ISteamDirectory', 'GetCMListForConnect', 1,
params={
'cellid': cell_id,
'cmtype': cmtype,
'http_timeout': 3
}
)
except Exception as exp:
self._LOG.error("WebAPI boostrap failed: %s" % str(exp))
return False

result = EResult(resp['response']['result'])
result = EResult(resp['response']['success'])

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

def str_to_tuple(serveraddr):
ip, port = serveraddr.split(':')
def str_to_tuple(serverinfo):
ip, port = serverinfo['endpoint'].split(':')
return str(ip), int(port)

self.clear()
self.cell_id = cell_id
self.merge_list(map(str_to_tuple, serverlist))

return True

def __iter__(self):
def cm_server_iter():
if not self.list:
Expand Down
Loading

0 comments on commit cc8310a

Please sign in to comment.