diff --git a/TEKSteamClient.csproj b/TEKSteamClient.csproj index 8b37b9e..4e8c843 100644 --- a/TEKSteamClient.csproj +++ b/TEKSteamClient.csproj @@ -1,11 +1,11 @@ net8.0 - 1.0.1 + 1.1.0 Nuclearist TEK Steam Client library TEK Steam Client - Copyright © 2023 Nuclearist + Copyright © 2023-2024 Nuclearist MIT README.md steam;tek diff --git a/protos/cm/messages/bodies/pics_access_token.proto b/protos/cm/messages/bodies/pics_access_token.proto new file mode 100644 index 0000000..d61fa41 --- /dev/null +++ b/protos/cm/messages/bodies/pics_access_token.proto @@ -0,0 +1,18 @@ +syntax="proto3"; + +package teksteamclient.cm.messages.bodies; +option csharp_namespace = "TEKSteamClient.CM.Messages.Bodies"; + +message PicsAccessToken +{ + repeated uint32 app_ids = 2; +} +message PicsAccessTokenResponse +{ + message AppToken + { + uint32 app_id = 1; + uint64 token = 2; + } + repeated AppToken apps = 3; +} \ No newline at end of file diff --git a/protos/cm/messages/message_type.proto b/protos/cm/messages/message_type.proto index 0b9e3c2..a4f9b3c 100644 --- a/protos/cm/messages/message_type.proto +++ b/protos/cm/messages/message_type.proto @@ -18,4 +18,6 @@ enum MessageType LOG_ON = 5514; PRODUCT_INFO = 8903; PRODUCT_INFO_RESPONSE = 8904; + PICS_ACCESS_TOKEN = 8905; + PICS_ACCESS_TOKEN_RESPONSE = 8906; } \ No newline at end of file diff --git a/src/CDNClient.cs b/src/CDNClient.cs index a52e792..78331a4 100644 --- a/src/CDNClient.cs +++ b/src/CDNClient.cs @@ -1,5 +1,4 @@ -using System.Collections.Frozen; -using System.IO.Hashing; +using System.IO.Hashing; using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/src/CM/CMClient.cs b/src/CM/CMClient.cs index 383a0ff..b3e6b56 100644 --- a/src/CM/CMClient.cs +++ b/src/CM/CMClient.cs @@ -77,6 +77,13 @@ static CMClient() private readonly WebSocketConnection _connection; /// OS type value included into logon messages. private static readonly int s_osType; + /// HTTP client that downloads PICS app info. + private static readonly HttpClient s_clientConfigClient = new() + { + BaseAddress = new("http://clientconfig.akamai.steamstatic.com/appinfo/"), + DefaultRequestVersion = HttpVersion.Version20, + Timeout = TimeSpan.FromSeconds(10) + }; /// When , ensures that the client is connected and logged on before every request and logs on with anonymous user credentials if it's not. public bool EnsureLogOn { get; set; } /// Steam cell ID used in certain requests. @@ -301,11 +308,38 @@ public FrozenDictionary GetDepotManifestIds(uint appId) message.Body.Apps.Add(new ProductInfo.Types.AppInfo { AppId = appId, AccessToken = 0 }); message.Body.MetadataOnly = false; var response = _connection.TransceiveMessage(message, MessageType.ProductInfoResponse, jobId); - if (response is null || response.Body.Apps.Count is 0 || !MemoryMarshal.TryGetArray(response.Body.Apps[0].Buffer.Memory, out var segment)) + if (response is null || response.Body.Apps.Count is 0) throw new SteamException(SteamException.ErrorType.CMFailedToGetManifestIds); + if (response.Body.Apps[0].MissingToken) + { + jobId = GlobalId.NextJobId; + var tokenMessage = new Message(MessageType.PicsAccessToken) { Header = new() { SourceJobId = jobId } }; + tokenMessage.Body.AppIds.Add(appId); + var tokenResponse = _connection.TransceiveMessage(tokenMessage, MessageType.PicsAccessTokenResponse, jobId); + if (tokenResponse is null || tokenResponse.Body.Apps.Count is 0) + throw new SteamException(SteamException.ErrorType.CMFailedToGetPicsAccessToken); + jobId = GlobalId.NextJobId; + message.Header.SourceJobId = jobId; + message.Body.Apps[0].AccessToken = tokenResponse.Body.Apps[0].Token; + response = _connection.TransceiveMessage(message, MessageType.ProductInfoResponse, jobId); + if (response is null || response.Body.Apps.Count is 0) + throw new SteamException(SteamException.ErrorType.CMFailedToGetManifestIds); + } + var appInfo = response.Body.Apps[0]; List? entries; - using (var reader = new StreamReader(new MemoryStream(segment.Array!, false))) - entries = new VDFEntry(reader)["depots"]?.Children; + if (MemoryMarshal.TryGetArray(appInfo.Buffer.Memory, out var segment)) + using (var reader = new StreamReader(new MemoryStream(segment.Array!, false))) + entries = new VDFEntry(reader)["depots"]?.Children; + else + try + { + var httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri($"{appId}/sha/{Convert.ToHexString(appInfo.Sha.Span)}.txt.gz")) { Version = HttpVersion.Version20 }; + using var httpResponse = s_clientConfigClient.SendAsync(httpRequest, HttpCompletionOption.ResponseContentRead, CancellationToken.None).Result.EnsureSuccessStatusCode(); + using var content = httpResponse.Content; + using var reader = new StreamReader(content.ReadAsStream()); + entries = new VDFEntry(reader)["depots"]?.Children; + } + catch (HttpRequestException e) { throw new SteamException(SteamException.ErrorType.CMFailedToGetManifestIds, e); } if (entries is null) return FrozenDictionary.Empty; entries.RemoveAll(e => !uint.TryParse(e.Key, out _) || e["manifests"]?["public"] is null); diff --git a/src/SteamException.cs b/src/SteamException.cs index f8ae298..89a6213 100644 --- a/src/SteamException.cs +++ b/src/SteamException.cs @@ -37,6 +37,8 @@ public enum ErrorType CMFailedToGetManifestRequestCode, /// [CM Client] Failed to get depot patch availability. CMFailedToGetPatchAvailablity, + /// [CM Client] Failed to get PICS access token for app. + CMFailedToGetPicsAccessToken, /// [CM Client] Failed to get workshop item details. CMFailedToGetWorkshopItemDetails, /// [CM Client] Log on failed.