diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs index 58380db9b..6ba34da6e 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs @@ -37,7 +37,7 @@ using ArchiSteamFarm.Web.Responses; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; internal static class Backend { - internal static async Task AnnounceForListing(Bot bot, IReadOnlyList inventory, IReadOnlyCollection acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) { + internal static async Task AnnounceForListing(Bot bot, IReadOnlyList inventory, IReadOnlyCollection acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) { ArgumentNullException.ThrowIfNull(bot); if ((inventory == null) || (inventory.Count == 0)) { @@ -60,9 +60,7 @@ internal static class Backend { AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, tradeToken, inventory, acceptedMatchableTypes, bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything), ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, nickname, avatarHash); - BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); - - return response?.StatusCode; + return await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); } internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, IReadOnlyCollection inventory, IReadOnlyCollection acceptedMatchableTypes, string tradeToken) { diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index 5232d1768..8df491129 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -35,9 +35,11 @@ using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Cards; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; +using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; +using ArchiSteamFarm.Web.Responses; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; @@ -66,6 +68,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { private DateTime LastPersonaStateRequest; private bool ShouldSendAnnouncementEarlier; private bool ShouldSendHeartBeats; + private bool SignedInWithSteam; internal RemoteCommunication(Bot bot) { ArgumentNullException.ThrowIfNull(bot); @@ -202,34 +205,66 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { return; } + if (!SignedInWithSteam) { + HttpStatusCode? signInWithSteam = await ArchiNet.SignInWithSteam(Bot).ConfigureAwait(false); + + if (signInWithSteam == null) { + // This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check + ShouldSendHeartBeats = false; + + return; + } + + if (!signInWithSteam.Value.IsSuccessCode()) { + // SignIn procedure failed and it wasn't a network error, hold off with future tries at least for a full day + LastAnnouncement = DateTime.UtcNow.AddDays(1); + ShouldSendHeartBeats = false; + + return; + } + + SignedInWithSteam = true; + } + Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, inventory.Count)); // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - HttpStatusCode? response = await Backend.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false); + BasicResponse? response = await Backend.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false); - if (!response.HasValue) { + if (response == null) { // This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check ShouldSendHeartBeats = false; return; } - // We've got the response, regardless what happened, we've succeeded in a valid check - LastAnnouncement = DateTime.UtcNow; - ShouldSendAnnouncementEarlier = false; + if (response.StatusCode.IsRedirectionCode()) { + ShouldSendHeartBeats = false; - if (response.Value.IsClientErrorCode()) { + if (response.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) { + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(response.FinalUri), response.FinalUri)); + + return; + } + + // We've expected the result, not the redirection to the sign in, we need to authenticate again + SignedInWithSteam = false; + + return; + } + + if (response.StatusCode.IsClientErrorCode()) { // ArchiNet told us that we've sent a bad request, so the process should restart from the beginning at later time ShouldSendHeartBeats = false; Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response)); - switch (response) { + switch (response.StatusCode) { case HttpStatusCode.Forbidden: // ArchiNet told us to stop submitting data for now LastAnnouncement = DateTime.UtcNow.AddYears(1); - break; + return; #if NETFRAMEWORK case (HttpStatusCode) 429: #else @@ -239,13 +274,17 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { // ArchiNet told us to try again later LastAnnouncement = DateTime.UtcNow.AddDays(1); - break; - } + return; + default: + // There is something wrong with our payload or the server, we shouldn't retry for at least several hours + LastAnnouncement = DateTime.UtcNow.AddHours(6); - return; + return; + } } - LastHeartBeat = DateTime.UtcNow; + LastAnnouncement = LastHeartBeat = DateTime.UtcNow; + ShouldSendAnnouncementEarlier = false; ShouldSendHeartBeats = true; Bot.ArchiLogger.LogGenericInfo(Strings.Success); diff --git a/ArchiSteamFarm/Core/ArchiNet.cs b/ArchiSteamFarm/Core/ArchiNet.cs index 0a4f49b2c..4449c8587 100644 --- a/ArchiSteamFarm/Core/ArchiNet.cs +++ b/ArchiSteamFarm/Core/ArchiNet.cs @@ -23,9 +23,15 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; +using AngleSharp.Dom; using ArchiSteamFarm.Helpers; using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Steam; +using ArchiSteamFarm.Steam.Integration; +using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; using SteamKit2; @@ -68,6 +74,97 @@ internal static class ArchiNet { return badBots?.Contains(steamID); } + internal static async Task SignInWithSteam(Bot bot) { + ArgumentNullException.ThrowIfNull(bot); + + if (!bot.IsConnectedAndLoggedOn) { + return null; + } + + // We expect data or redirection to Steam OpenID + Uri authenticateRequest = new(URL, $"/Api/Steam/Authenticate?steamID={bot.SteamID}"); + + ObjectResponse>? authenticateResponse = await bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject>(authenticateRequest, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); + + if (authenticateResponse == null) { + return null; + } + + if (authenticateResponse.StatusCode.IsClientErrorCode()) { + return authenticateResponse.StatusCode; + } + + if (authenticateResponse.StatusCode.IsSuccessCode()) { + return authenticateResponse.Content?.Result == bot.SteamID ? HttpStatusCode.OK : HttpStatusCode.Unauthorized; + } + + // We've got a redirection, initiate OpenID procedure by following it + using HtmlDocumentResponse? challengeResponse = await bot.ArchiWebHandler.UrlGetToHtmlDocumentWithSession(authenticateResponse.FinalUri).ConfigureAwait(false); + + if (challengeResponse?.Content == null) { + return null; + } + + IAttr? paramsNode = challengeResponse.Content.SelectSingleNode("//input[@name='openidparams']/@value"); + + if (paramsNode == null) { + ASF.ArchiLogger.LogNullError(paramsNode); + + return null; + } + + string paramsValue = paramsNode.Value; + + if (string.IsNullOrEmpty(paramsValue)) { + ASF.ArchiLogger.LogNullError(paramsValue); + + return null; + } + + IAttr? nonceNode = challengeResponse.Content.SelectSingleNode("//input[@name='nonce']/@value"); + + if (nonceNode == null) { + ASF.ArchiLogger.LogNullError(nonceNode); + + return null; + } + + string nonceValue = nonceNode.Value; + + if (string.IsNullOrEmpty(nonceValue)) { + ASF.ArchiLogger.LogNullError(nonceValue); + + return null; + } + + Uri loginRequest = new(ArchiWebHandler.SteamCommunityURL, "/openid/login"); + + using StringContent actionContent = new("steam_openid_login"); + using StringContent modeContent = new("checkid_setup"); + using StringContent paramsContent = new(paramsValue); + using StringContent nonceContent = new(nonceValue); + + using MultipartFormDataContent data = new(); + + data.Add(actionContent, "action"); + data.Add(modeContent, "openid.mode"); + data.Add(paramsContent, "openidparams"); + data.Add(nonceContent, "nonce"); + + // Accept OpenID request presented and follow redirection back to the data we initially expected + authenticateResponse = await bot.ArchiWebHandler.WebBrowser.UrlPostToJsonObject, MultipartFormDataContent>(loginRequest, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); + + if (authenticateResponse == null) { + return null; + } + + if (authenticateResponse.StatusCode.IsClientErrorCode()) { + return authenticateResponse.StatusCode; + } + + return authenticateResponse.Content?.Result == bot.SteamID ? HttpStatusCode.OK : HttpStatusCode.Unauthorized; + } + private static async Task<(bool Success, IReadOnlyCollection? Result)> ResolveCachedBadBots() { if (ASF.GlobalDatabase == null) { throw new InvalidOperationException(nameof(ASF.WebBrowser)); diff --git a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs index 0706c789e..07459b505 100644 --- a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs @@ -25,7 +25,7 @@ using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Responses; -public sealed class GenericResponse : GenericResponse where T : class { +public sealed class GenericResponse : GenericResponse { /// /// The actual result of the request, if available. /// @@ -35,7 +35,7 @@ public sealed class GenericResponse : GenericResponse where T : class { [JsonProperty] public T? Result { get; private set; } - public GenericResponse(T? result) : base(result != null) => Result = result; + public GenericResponse(T? result) : base(result is not null) => Result = result; public GenericResponse(bool success, string? message) : base(success, message) { } public GenericResponse(bool success, T? result) : base(success) => Result = result; public GenericResponse(bool success, string? message, T? result) : base(success, message) => Result = result;