From 99569ee3feca622cc7108ed40ec2e2a2888a075a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Thu, 18 Nov 2021 21:16:47 +0100 Subject: [PATCH] Implement additional checksum verification for ASF builds (#2453) * #2452 * Fix netf * Apply feedback * Misc * Misc * Apply feedback --- ArchiSteamFarm/Core/ASF.cs | 29 +- ArchiSteamFarm/Core/ArchiNet.cs | 276 ++++++++++++++++++ ArchiSteamFarm/Core/Statistics.cs | 183 +----------- .../Localization/Strings.Designer.cs | 24 ++ ArchiSteamFarm/Localization/Strings.resx | 12 + ArchiSteamFarm/SharedInfo.cs | 1 - ArchiSteamFarm/Steam/Storage/BotConfig.cs | 3 +- Directory.Build.props | 1 + 8 files changed, 352 insertions(+), 177 deletions(-) create mode 100644 ArchiSteamFarm/Core/ArchiNet.cs diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index e138ef4d6..3e29cddda 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -285,6 +285,31 @@ public static class ASF { return null; } + ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer); + + string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false); + + switch (remoteChecksum) { + case null: + // Timeout or error, refuse to update as a security measure + return null; + case "": + // Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first + ArchiLogger.LogGenericWarning(Strings.ChecksumMissing); + + return SharedInfo.Version; + } + + byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); + + string checksum = BitConverter.ToString(SHA512.HashData(responseBytes)).Replace("-", "", StringComparison.Ordinal); + + if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) { + ArchiLogger.LogGenericError(Strings.ChecksumWrong); + + return SharedInfo.Version; + } + try { // We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash // TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update @@ -293,7 +318,9 @@ public static class ASF { ArchiLogger.LogGenericWarningException(e); } - MemoryStream ms = new(response.Content as byte[] ?? response.Content.ToArray()); + ArchiLogger.LogGenericInfo(Strings.PatchingFiles); + + MemoryStream ms = new(responseBytes); try { await using (ms.ConfigureAwait(false)) { diff --git a/ArchiSteamFarm/Core/ArchiNet.cs b/ArchiSteamFarm/Core/ArchiNet.cs new file mode 100644 index 000000000..1c837d015 --- /dev/null +++ b/ArchiSteamFarm/Core/ArchiNet.cs @@ -0,0 +1,276 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2021 Ɓukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using ArchiSteamFarm.Localization; +using ArchiSteamFarm.Steam; +using ArchiSteamFarm.Steam.Data; +using ArchiSteamFarm.Steam.Storage; +using ArchiSteamFarm.Web; +using ArchiSteamFarm.Web.Responses; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.Core; + +internal static class ArchiNet { + private static Uri URL => new("https://asf.JustArchi.net"); + + internal static async Task AnnounceForListing(Bot bot, IReadOnlyCollection inventory, IReadOnlyCollection acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) { + if (bot == null) { + throw new ArgumentNullException(nameof(bot)); + } + + if ((inventory == null) || (inventory.Count == 0)) { + throw new ArgumentNullException(nameof(inventory)); + } + + if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) { + throw new ArgumentNullException(nameof(acceptedMatchableTypes)); + } + + if (string.IsNullOrEmpty(tradeToken)) { + throw new ArgumentNullException(nameof(tradeToken)); + } + + if (tradeToken.Length != BotConfig.SteamTradeTokenLength) { + throw new ArgumentOutOfRangeException(nameof(tradeToken)); + } + + Uri request = new(URL, "/Api/Announce"); + + Dictionary data = new(9, StringComparer.Ordinal) { + { "AvatarHash", avatarHash ?? "" }, + { "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) }, + { "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") }, + { "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) }, + { "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) }, + { "MatchEverything", bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" }, + { "Nickname", nickname ?? "" }, + { "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) }, + { "TradeToken", tradeToken } + }; + + BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); + + return response?.StatusCode; + } + + internal static async Task FetchBuildChecksum(Version version, string variant) { + if (version == null) { + throw new ArgumentNullException(nameof(version)); + } + + if (string.IsNullOrEmpty(variant)) { + throw new ArgumentNullException(nameof(variant)); + } + + if (ASF.WebBrowser == null) { + throw new InvalidOperationException(nameof(ASF.WebBrowser)); + } + + Uri request = new(URL, $"/Api/Checksum/{version}/{variant}"); + + ObjectResponse? response = await ASF.WebBrowser.UrlGetToJsonObject(request).ConfigureAwait(false); + + if (response == null) { + return null; + } + + return response.Content.Checksum ?? ""; + } + + internal static async Task?> GetListedUsers(Bot bot) { + if (bot == null) { + throw new ArgumentNullException(nameof(bot)); + } + + Uri request = new(URL, "/Api/Bots"); + + ObjectResponse>? response = await bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject>(request).ConfigureAwait(false); + + return response?.Content; + } + + internal static async Task HeartBeatForListing(Bot bot) { + if (bot == null) { + throw new ArgumentNullException(nameof(bot)); + } + + Uri request = new(URL, "/Api/HeartBeat"); + + Dictionary data = new(2, StringComparer.Ordinal) { + { "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") }, + { "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) } + }; + + BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); + + return response?.StatusCode; + } + + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + internal sealed class ListedUser { +#pragma warning disable CS0649 // False positive, it's a field set during json deserialization + [JsonProperty(PropertyName = "items_count", Required = Required.Always)] + internal readonly ushort ItemsCount; +#pragma warning restore CS0649 // False positive, it's a field set during json deserialization + + internal readonly HashSet MatchableTypes = new(); + +#pragma warning disable CS0649 // False positive, it's a field set during json deserialization + [JsonProperty(PropertyName = "steam_id", Required = Required.Always)] + internal readonly ulong SteamID; +#pragma warning restore CS0649 // False positive, it's a field set during json deserialization + + [JsonProperty(PropertyName = "trade_token", Required = Required.Always)] + internal readonly string TradeToken = ""; + + internal float Score => GamesCount / (float) ItemsCount; + +#pragma warning disable CS0649 // False positive, it's a field set during json deserialization + [JsonProperty(PropertyName = "games_count", Required = Required.Always)] + private readonly ushort GamesCount; +#pragma warning restore CS0649 // False positive, it's a field set during json deserialization + + internal bool MatchEverything { get; private set; } + + [JsonProperty(PropertyName = "matchable_backgrounds", Required = Required.Always)] + private byte MatchableBackgroundsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Asset.EType.ProfileBackground); + + break; + case 1: + MatchableTypes.Add(Asset.EType.ProfileBackground); + + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + + return; + } + } + } + + [JsonProperty(PropertyName = "matchable_cards", Required = Required.Always)] + private byte MatchableCardsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Asset.EType.TradingCard); + + break; + case 1: + MatchableTypes.Add(Asset.EType.TradingCard); + + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + + return; + } + } + } + + [JsonProperty(PropertyName = "matchable_emoticons", Required = Required.Always)] + private byte MatchableEmoticonsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Asset.EType.Emoticon); + + break; + case 1: + MatchableTypes.Add(Asset.EType.Emoticon); + + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + + return; + } + } + } + + [JsonProperty(PropertyName = "matchable_foil_cards", Required = Required.Always)] + private byte MatchableFoilCardsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Asset.EType.FoilTradingCard); + + break; + case 1: + MatchableTypes.Add(Asset.EType.FoilTradingCard); + + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + + return; + } + } + } + + [JsonProperty(PropertyName = "match_everything", Required = Required.Always)] + private byte MatchEverythingNumber { + set { + switch (value) { + case 0: + MatchEverything = false; + + break; + case 1: + MatchEverything = true; + + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + + return; + } + } + } + + [JsonConstructor] + private ListedUser() { } + } + + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + private sealed class ChecksumResponse { +#pragma warning disable CS0649 // False positive, the field is used during json deserialization + [JsonProperty("checksum", Required = Required.AllowNull)] + internal readonly string? Checksum; +#pragma warning restore CS0649 // False positive, the field is used during json deserialization + + [JsonConstructor] + private ChecksumResponse() { } + } +} diff --git a/ArchiSteamFarm/Core/Statistics.cs b/ArchiSteamFarm/Core/Statistics.cs index 65b2feec8..5f93109f5 100644 --- a/ArchiSteamFarm/Core/Statistics.cs +++ b/ArchiSteamFarm/Core/Statistics.cs @@ -22,9 +22,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -38,8 +38,6 @@ using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; -using ArchiSteamFarm.Web.Responses; -using Newtonsoft.Json; namespace ArchiSteamFarm.Core; @@ -51,7 +49,6 @@ internal sealed class Statistics : IAsyncDisposable { private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing private const byte MinPersonaStateTTL = 8; // Minimum amount of hours we must wait before requesting persona state update - private const string URL = $"https://{SharedInfo.StatisticsServer}"; private static readonly ImmutableHashSet AcceptedMatchableTypes = ImmutableHashSet.Create( Asset.EType.Emoticon, @@ -108,20 +105,13 @@ internal sealed class Statistics : IAsyncDisposable { } try { - Uri request = new($"{URL}/Api/HeartBeat"); + HttpStatusCode? response = await ArchiNet.HeartBeatForListing(Bot).ConfigureAwait(false); - Dictionary data = new(2, StringComparer.Ordinal) { - { "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") }, - { "SteamID", Bot.SteamID.ToString(CultureInfo.InvariantCulture) } - }; - - BasicResponse? response = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); - - if (response == null) { + if (!response.HasValue) { return; } - if (response.StatusCode.IsClientErrorCode()) { + if (response.Value.IsClientErrorCode()) { LastHeartBeat = DateTime.MinValue; ShouldSendHeartBeats = false; @@ -217,29 +207,14 @@ internal sealed class Statistics : IAsyncDisposable { return; } - Uri request = new($"{URL}/Api/Announce"); + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + HttpStatusCode? response = await ArchiNet.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false); - Dictionary data = new(9, StringComparer.Ordinal) { - { "AvatarHash", avatarHash ?? "" }, - { "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) }, - { "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") }, - { "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) }, - { "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) }, - { "MatchEverything", Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" }, - { "Nickname", nickname ?? "" }, - { "SteamID", Bot.SteamID.ToString(CultureInfo.InvariantCulture) }, - - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - { "TradeToken", tradeToken! } - }; - - BasicResponse? response = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); - - if (response == null) { + if (!response.HasValue) { return; } - if (response.StatusCode.IsClientErrorCode()) { + if (response.Value.IsClientErrorCode()) { LastHeartBeat = DateTime.MinValue; ShouldSendHeartBeats = false; @@ -253,14 +228,6 @@ internal sealed class Statistics : IAsyncDisposable { } } - private async Task?> GetListedUsers() { - Uri request = new($"{URL}/Api/Bots"); - - ObjectResponse>? response = await Bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject>(request).ConfigureAwait(false); - - return response?.Content; - } - private async Task IsEligibleForListing() { bool? isEligibleForMatching = await IsEligibleForMatching().ConfigureAwait(false); @@ -408,7 +375,7 @@ internal sealed class Statistics : IAsyncDisposable { return (false, false); } - ImmutableHashSet? listedUsers = await GetListedUsers().ConfigureAwait(false); + ImmutableHashSet? listedUsers = await ArchiNet.GetListedUsers(Bot).ConfigureAwait(false); if ((listedUsers == null) || (listedUsers.Count == 0)) { Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(listedUsers))); @@ -421,7 +388,7 @@ internal sealed class Statistics : IAsyncDisposable { HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisRound = new(); - foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet? GivenAssetIDs, ISet? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet? GivenAssetIDs, ISet? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) { + foreach (ArchiNet.ListedUser? listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet? GivenAssetIDs, ISet? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet? GivenAssetIDs, ISet? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) { HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet(); if (wantedSets.Count == 0) { @@ -715,134 +682,4 @@ internal sealed class Statistics : IAsyncDisposable { // Keep matching when we either traded something this round (so it makes sense for a refresh) or if we didn't try all available bots yet (so it makes sense to keep going) return ((totalMatches > 0) && ((skippedSetsThisRound.Count > 0) || triedSteamIDs.Values.All(static data => data.Tries < 2)), skippedSetsThisRound.Count > 0); } - - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - private sealed class ListedUser { -#pragma warning disable CS0649 // False positive, it's a field set during json deserialization - [JsonProperty(PropertyName = "items_count", Required = Required.Always)] - internal readonly ushort ItemsCount; -#pragma warning restore CS0649 // False positive, it's a field set during json deserialization - - internal readonly HashSet MatchableTypes = new(); - -#pragma warning disable CS0649 // False positive, it's a field set during json deserialization - [JsonProperty(PropertyName = "steam_id", Required = Required.Always)] - internal readonly ulong SteamID; -#pragma warning restore CS0649 // False positive, it's a field set during json deserialization - - [JsonProperty(PropertyName = "trade_token", Required = Required.Always)] - internal readonly string TradeToken = ""; - - internal float Score => GamesCount / (float) ItemsCount; - -#pragma warning disable CS0649 // False positive, it's a field set during json deserialization - [JsonProperty(PropertyName = "games_count", Required = Required.Always)] - private readonly ushort GamesCount; -#pragma warning restore CS0649 // False positive, it's a field set during json deserialization - - internal bool MatchEverything { get; private set; } - - [JsonProperty(PropertyName = "matchable_backgrounds", Required = Required.Always)] - private byte MatchableBackgroundsNumber { - set { - switch (value) { - case 0: - MatchableTypes.Remove(Asset.EType.ProfileBackground); - - break; - case 1: - MatchableTypes.Add(Asset.EType.ProfileBackground); - - break; - default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); - - return; - } - } - } - - [JsonProperty(PropertyName = "matchable_cards", Required = Required.Always)] - private byte MatchableCardsNumber { - set { - switch (value) { - case 0: - MatchableTypes.Remove(Asset.EType.TradingCard); - - break; - case 1: - MatchableTypes.Add(Asset.EType.TradingCard); - - break; - default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); - - return; - } - } - } - - [JsonProperty(PropertyName = "matchable_emoticons", Required = Required.Always)] - private byte MatchableEmoticonsNumber { - set { - switch (value) { - case 0: - MatchableTypes.Remove(Asset.EType.Emoticon); - - break; - case 1: - MatchableTypes.Add(Asset.EType.Emoticon); - - break; - default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); - - return; - } - } - } - - [JsonProperty(PropertyName = "matchable_foil_cards", Required = Required.Always)] - private byte MatchableFoilCardsNumber { - set { - switch (value) { - case 0: - MatchableTypes.Remove(Asset.EType.FoilTradingCard); - - break; - case 1: - MatchableTypes.Add(Asset.EType.FoilTradingCard); - - break; - default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); - - return; - } - } - } - - [JsonProperty(PropertyName = "match_everything", Required = Required.Always)] - private byte MatchEverythingNumber { - set { - switch (value) { - case 0: - MatchEverything = false; - - break; - case 1: - MatchEverything = true; - - break; - default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value)); - - return; - } - } - } - - [JsonConstructor] - private ListedUser() { } - } } diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index b774ae015..6c015395f 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1178,5 +1178,29 @@ namespace ArchiSteamFarm.Localization { return ResourceManager.GetString("WarningRunningInUnsupportedEnvironment", resourceCulture); } } + + public static string VerifyingChecksumWithRemoteServer { + get { + return ResourceManager.GetString("VerifyingChecksumWithRemoteServer", resourceCulture); + } + } + + public static string ChecksumMissing { + get { + return ResourceManager.GetString("ChecksumMissing", resourceCulture); + } + } + + public static string ChecksumWrong { + get { + return ResourceManager.GetString("ChecksumWrong", resourceCulture); + } + } + + public static string PatchingFiles { + get { + return ResourceManager.GetString("PatchingFiles", resourceCulture); + } + } } } diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index bb0e6c965..13b658167 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -729,4 +729,16 @@ Process uptime: {1} You're running ASF in unsupported environment, supplying --ignore-unsupported-environment argument. Please note that we do not offer any kind of support for this scenario and you're doing it entirely at your own risk. You've been warned. + + Verifying checksum of the downloaded binary with ASF's remote server... + + + Remote server doesn't know anything about the release we're updating to. This situation is possible if the release was published recently - holding off with the update procedure until a bit later as an additional security measure. + + + Remote server has replied with a different checksum, this might indicate corrupted download or MITM attack, refusing to proceed with the update procedure! + + + Patching ASF files... + diff --git a/ArchiSteamFarm/SharedInfo.cs b/ArchiSteamFarm/SharedInfo.cs index 4c0c486f1..f74aa2f7e 100644 --- a/ArchiSteamFarm/SharedInfo.cs +++ b/ArchiSteamFarm/SharedInfo.cs @@ -64,7 +64,6 @@ public static class SharedInfo { internal const string ProjectURL = $"https://github.com/{GithubRepo}"; internal const string SentryHashExtension = ".bin"; internal const ushort ShortInformationDelay = InformationDelay / 2; - internal const string StatisticsServer = "asf.justarchi.net"; internal const string UlongCompatibilityStringPrefix = "s_"; internal const string UpdateDirectory = "_old"; internal const string WebsiteDirectory = "www"; diff --git a/ArchiSteamFarm/Steam/Storage/BotConfig.cs b/ArchiSteamFarm/Steam/Storage/BotConfig.cs index 0223b82df..18e192cba 100644 --- a/ArchiSteamFarm/Steam/Storage/BotConfig.cs +++ b/ArchiSteamFarm/Steam/Storage/BotConfig.cs @@ -117,8 +117,7 @@ public sealed class BotConfig { public const ArchiHandler.EUserInterfaceMode DefaultUserInterfaceMode = ArchiHandler.EUserInterfaceMode.Default; internal const byte SteamParentalCodeLength = 4; - - private const byte SteamTradeTokenLength = 8; + internal const byte SteamTradeTokenLength = 8; [PublicAPI] public static readonly ImmutableHashSet DefaultCompleteTypesToSend = ImmutableHashSet.Empty; diff --git a/Directory.Build.props b/Directory.Build.props index bb21e7d9f..f2478c6ed 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -41,6 +41,7 @@ +