From 242284559e1dc83405c60a410425d98120fac234 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Tue, 6 Nov 2018 21:41:59 +0100 Subject: [PATCH] Implement hybrid session logic In ASF V3.0 and before, we had a very naive mechanism with session handling that used to work fine but send a lot of requests as we always had to check session before sending a request. Since ASF V3.1 until now we used new mechanism which refreshed session only when it indeed got invalidated, which worked good until now, but apparently due to some changed at Valve it stopped redirecting appropriately in all POST requests and non-/my GET/HEAD ones. Therefore, implement hybrid session logic whick works V3.1+ for /my requests, and V3.0- for everything else. --- ArchiSteamFarm/ArchiWebHandler.cs | 240 +++++++++++++++++++++++------- ArchiSteamFarm/GlobalConfig.cs | 3 +- 2 files changed, 185 insertions(+), 58 deletions(-) diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 4765eb25c..e0e89ebbd 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -43,6 +43,7 @@ namespace ArchiSteamFarm { private const string ISteamApps = "ISteamApps"; private const string ISteamUserAuth = "ISteamUserAuth"; private const string ITwoFactorService = "ITwoFactorService"; + private const byte MinSessionValidityInSeconds = GlobalConfig.DefaultConnectionTimeout / 6; private const string SteamCommunityHost = "steamcommunity.com"; private const string SteamCommunityURL = "https://" + SteamCommunityHost; private const string SteamStoreHost = "store.steampowered.com"; @@ -66,6 +67,7 @@ namespace ArchiSteamFarm { private string CachedApiKey; private bool? CachedPublicInventory; private string CachedTradeToken; + private DateTime LastSessionCheck; private DateTime LastSessionRefresh; private bool MarkingInventoryScheduled; private ulong SteamID; @@ -413,7 +415,7 @@ namespace ArchiSteamFarm { } string request = "/my/badges?l=english&p=" + page; - return await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); } internal async Task GetConfirmationDetails(string deviceID, string confirmationHash, uint time, MobileAuthenticator.Confirmation confirmation) { @@ -538,7 +540,7 @@ namespace ArchiSteamFarm { } string request = "/my/gamecards/" + appID + "?l=english"; - return await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); } [SuppressMessage("ReSharper", "FunctionComplexityOverflow")] @@ -665,7 +667,7 @@ namespace ArchiSteamFarm { internal async Task> GetMyOwnedGames() { const string request = "/my/games?l=english&xml=1"; - XmlDocument response = await UrlGetToXmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false); + XmlDocument response = await UrlGetToXmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); XmlNodeList xmlNodeList = response?.SelectNodes("gamesList/games/game"); if ((xmlNodeList == null) || (xmlNodeList.Count == 0)) { @@ -900,7 +902,7 @@ namespace ArchiSteamFarm { } const string request = "/my/tradeoffers/privacy?l=english"; - HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false); + HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); if (htmlDocument == null) { return null; @@ -1117,7 +1119,7 @@ namespace ArchiSteamFarm { } SteamID = steamID; - LastSessionRefresh = DateTime.UtcNow; + LastSessionCheck = LastSessionRefresh = DateTime.UtcNow; return true; } @@ -1154,7 +1156,7 @@ namespace ArchiSteamFarm { } const string request = "/my/inventory"; - await UrlHeadWithSession(SteamCommunityURL, request).ConfigureAwait(false); + await UrlHeadWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); } finally { if (Program.GlobalConfig.InventoryLimiterDelay == 0) { InventorySemaphore.Release(); @@ -1171,7 +1173,7 @@ namespace ArchiSteamFarm { internal async Task MarkSentTrades() { const string request = "/my/tradeoffers/sent"; - return await UrlHeadWithSession(SteamCommunityURL, request).ConfigureAwait(false); + return await UrlHeadWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); } internal void OnDisconnected() { @@ -1466,7 +1468,7 @@ namespace ArchiSteamFarm { private async Task IsInventoryPublic() { const string request = "/my/edit/settings?l=english"; - HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false); + HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false); if (htmlDocument == null) { return null; @@ -1528,6 +1530,42 @@ namespace ArchiSteamFarm { return uri.AbsolutePath.Equals(profileURL); } + private async Task IsSessionExpired() { + if (LastSessionCheck.AddSeconds(MinSessionValidityInSeconds) < DateTime.UtcNow) { + return LastSessionCheck != LastSessionRefresh; + } + + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + + try { + if (LastSessionCheck.AddSeconds(MinSessionValidityInSeconds) < DateTime.UtcNow) { + return LastSessionCheck != LastSessionRefresh; + } + + // It would make sense to use /my/profile here, but it dismisses notifications related to profile comments + // So instead, we'll use some less invasive /my link that ensures the session validation, doesn't cause issues and is fast enough + const string request = SteamCommunityURL + "/my/badges/2"; + + WebBrowser.BasicResponse response = await WebBrowser.UrlHead(request).ConfigureAwait(false); + if (response?.FinalUri == null) { + return null; + } + + bool result = IsSessionExpiredUri(response.FinalUri); + + DateTime now = DateTime.UtcNow; + + if (!result) { + LastSessionRefresh = now; + } + + LastSessionCheck = now; + return result; + } finally { + SessionSemaphore.Release(); + } + } + private static bool IsSessionExpiredUri(Uri uri) { if (uri == null) { ASF.ArchiLogger.LogNullError(nameof(uri)); @@ -1605,7 +1643,7 @@ namespace ArchiSteamFarm { bool result = await Bot.RefreshSession().ConfigureAwait(false); if (result) { - LastSessionRefresh = DateTime.UtcNow; + LastSessionCheck = LastSessionRefresh = DateTime.UtcNow; } return result; @@ -1681,7 +1719,7 @@ namespace ArchiSteamFarm { return true; } - private async Task UrlGetToHtmlDocumentWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) { + private async Task UrlGetToHtmlDocumentWithSession(string host, string request, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return null; @@ -1693,9 +1731,20 @@ namespace ArchiSteamFarm { return null; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlGetToHtmlDocumentWithSession(host, request, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -1716,7 +1765,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToHtmlDocumentWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1727,13 +1776,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlGetToHtmlDocumentWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return response.Content; } - private async Task UrlGetToJsonObjectWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) where T : class { + private async Task UrlGetToJsonObjectWithSession(string host, string request, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) where T : class { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return default; @@ -1745,9 +1794,20 @@ namespace ArchiSteamFarm { return default; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlGetToJsonObjectWithSession(host, request, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -1768,7 +1828,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToJsonObjectWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlGetToJsonObjectWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1779,13 +1839,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlGetToJsonObjectWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlGetToJsonObjectWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return response.Content; } - private async Task UrlGetToXmlDocumentWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) { + private async Task UrlGetToXmlDocumentWithSession(string host, string request, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return null; @@ -1797,9 +1857,20 @@ namespace ArchiSteamFarm { return null; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlGetToXmlDocumentWithSession(host, request, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -1820,7 +1891,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToXmlDocumentWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlGetToXmlDocumentWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1831,13 +1902,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlGetToXmlDocumentWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlGetToXmlDocumentWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return response.Content; } - private async Task UrlHeadWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) { + private async Task UrlHeadWithSession(string host, string request, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return false; @@ -1849,9 +1920,20 @@ namespace ArchiSteamFarm { return false; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlHeadWithSession(host, request, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -1872,7 +1954,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlHeadWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlHeadWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1883,13 +1965,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlHeadWithSession(host, request, --maxTries).ConfigureAwait(false); + return await UrlHeadWithSession(host, request, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return true; } - private async Task UrlPostToHtmlDocumentWithSession(string host, string request, Dictionary data = null, string referer = null, ESession session = ESession.Lowercase, byte maxTries = WebBrowser.MaxTries) { + private async Task UrlPostToHtmlDocumentWithSession(string host, string request, Dictionary data = null, string referer = null, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return null; @@ -1901,9 +1983,20 @@ namespace ArchiSteamFarm { return null; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlPostToHtmlDocumentWithSession(host, request, data, referer, session, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -1953,7 +2046,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToHtmlDocumentWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostToHtmlDocumentWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1964,13 +2057,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlPostToHtmlDocumentWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostToHtmlDocumentWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return response.Content; } - private async Task UrlPostToJsonObjectWithSession(string host, string request, Dictionary data = null, string referer = null, ESession session = ESession.Lowercase, byte maxTries = WebBrowser.MaxTries) where T : class { + private async Task UrlPostToJsonObjectWithSession(string host, string request, Dictionary data = null, string referer = null, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) where T : class { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return null; @@ -1982,9 +2075,20 @@ namespace ArchiSteamFarm { return null; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -2034,7 +2138,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -2045,13 +2149,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return response.Content; } - private async Task UrlPostToJsonObjectWithSession(string host, string request, List> data = null, string referer = null, ESession session = ESession.Lowercase, byte maxTries = WebBrowser.MaxTries) where T : class { + private async Task UrlPostToJsonObjectWithSession(string host, string request, List> data = null, string referer = null, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) where T : class { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return null; @@ -2063,9 +2167,20 @@ namespace ArchiSteamFarm { return null; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -2118,7 +2233,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -2129,13 +2244,13 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return response.Content; } - private async Task UrlPostWithSession(string host, string request, Dictionary data = null, string referer = null, ESession session = ESession.Lowercase, byte maxTries = WebBrowser.MaxTries) { + private async Task UrlPostWithSession(string host, string request, Dictionary data = null, string referer = null, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries) { if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) { Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request)); return false; @@ -2147,9 +2262,20 @@ namespace ArchiSteamFarm { return false; } - // If session refresh is already in progress, wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); - SessionSemaphore.Release(); + if (checkSessionPreemptively) { + // Check session preemptively as this request might not get redirected to expiration + bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false); + + if (sessionExpired.GetValueOrDefault(true)) { + if (await RefreshSession().ConfigureAwait(false)) { + return await UrlPostWithSession(host, request, data, referer, session, true, --maxTries).ConfigureAwait(false); + } + } + } else { + // If session refresh is already in progress, just wait for it + await SessionSemaphore.WaitAsync().ConfigureAwait(false); + SessionSemaphore.Release(); + } if (SteamID == 0) { for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) { @@ -2199,7 +2325,7 @@ namespace ArchiSteamFarm { if (IsSessionExpiredUri(response.FinalUri)) { if (await RefreshSession().ConfigureAwait(false)) { - return await UrlPostWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -2210,7 +2336,7 @@ namespace ArchiSteamFarm { // Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case if (await IsProfileUri(response.FinalUri).ConfigureAwait(false)) { Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.WarningWorkaroundTriggered, nameof(IsProfileUri))); - return await UrlPostWithSession(host, request, data, referer, session, --maxTries).ConfigureAwait(false); + return await UrlPostWithSession(host, request, data, referer, session, checkSessionPreemptively, --maxTries).ConfigureAwait(false); } return true; diff --git a/ArchiSteamFarm/GlobalConfig.cs b/ArchiSteamFarm/GlobalConfig.cs index b365952b0..5544babd0 100644 --- a/ArchiSteamFarm/GlobalConfig.cs +++ b/ArchiSteamFarm/GlobalConfig.cs @@ -33,10 +33,11 @@ using SteamKit2; namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class GlobalConfig { + internal const byte DefaultConnectionTimeout = 60; + private const bool DefaultAutoRestart = true; private const string DefaultCommandPrefix = "!"; private const byte DefaultConfirmationsLimiterDelay = 10; - private const byte DefaultConnectionTimeout = 60; private const string DefaultCurrentCulture = null; private const bool DefaultDebug = false; private const byte DefaultFarmingDelay = 15;