diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 200c54878961d..1d2294df409d8 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -19,6 +19,12 @@ "allowedVersions": "<= 3.1", "matchManagers": [ "nuget" ], "matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ] + }, + { + // TODO: Stick with SK2 version until we can remove the submodule + "enabled": false, + "matchManagers": [ "nuget" ], + "matchPackageNames": [ "Microsoft.Win32.Registry", "protobuf-net" ] } ] } diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index e2717031a294e..81725cbc9261f 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -13,16 +13,21 @@ + - + + + + Temp\SteamKit2.dll + diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index a4de903c6171d..a564550b768a0 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -112,7 +112,7 @@ public static async void InBackground(Action action, bool longRunning = false) { public static void InBackground(Func function, bool longRunning = false) { ArgumentNullException.ThrowIfNull(function); - InBackground(void () => function(), longRunning); + InBackground(void() => function(), longRunning); } [PublicAPI] diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index e39730f0befbc..1cf416265683a 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -157,6 +157,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private readonly SemaphoreSlim MessagingSemaphore = new(1, 1); private readonly ConcurrentDictionary PastNotifications = new(); private readonly SemaphoreSlim SendCompleteTypesSemaphore = new(1, 1); + private readonly SteamAuthentication SteamAuthentication; private readonly SteamClient SteamClient; private readonly ConcurrentHashSet SteamFamilySharingIDs = new(); private readonly SteamUser SteamUser; @@ -178,11 +179,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } } - /// - /// Login keys are not guaranteed to be valid, we should use them only if we don't have full details available from the user - /// - private bool ShouldUseLoginKeys => BotConfig.UseLoginKeys && (!BotConfig.IsSteamPasswordSet || !HasMobileAuthenticator); - [JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")] private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture); @@ -302,6 +298,8 @@ private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) { CallbackManager.Subscribe(OnGuestPassList); CallbackManager.Subscribe(OnLicenseList); + SteamAuthentication = SteamClient.GetHandler() ?? throw new InvalidOperationException(nameof(SteamAuthentication)); + SteamFriends = SteamClient.GetHandler() ?? throw new InvalidOperationException(nameof(SteamFriends)); CallbackManager.Subscribe(OnFriendsList); CallbackManager.Subscribe(OnPersonaState); @@ -311,7 +309,6 @@ private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) { SteamUser = SteamClient.GetHandler() ?? throw new InvalidOperationException(nameof(SteamUser)); CallbackManager.Subscribe(OnLoggedOff); CallbackManager.Subscribe(OnLoggedOn); - CallbackManager.Subscribe(OnLoginKey); CallbackManager.Subscribe(OnMachineAuth); CallbackManager.Subscribe(OnVanityURLChangedCallback); CallbackManager.Subscribe(OnWalletUpdate); @@ -1637,6 +1634,57 @@ internal async Task Rename(string newBotName) { return true; } + internal async Task RequestInput(ASF.EUserInputType inputType, bool previousCodeWasIncorrect) { + if ((inputType == ASF.EUserInputType.None) || !Enum.IsDefined(inputType)) { + throw new InvalidEnumArgumentException(nameof(inputType), (int) inputType, typeof(ASF.EUserInputType)); + } + + switch (inputType) { + case ASF.EUserInputType.SteamGuard when !string.IsNullOrEmpty(AuthCode): + string? savedAuthCode = AuthCode; + + AuthCode = null; + + return savedAuthCode; + case ASF.EUserInputType.TwoFactorAuthentication when !string.IsNullOrEmpty(TwoFactorCode): + string? savedTwoFactorCode = TwoFactorCode; + + TwoFactorCode = null; + + return savedTwoFactorCode; + case ASF.EUserInputType.TwoFactorAuthentication when BotDatabase.MobileAuthenticator != null: + if (previousCodeWasIncorrect) { + // There is a possibility that our cached time is no longer appropriate, so we should reset the cache in this case in order to fetch it upon the next login attempt + // Yes, this might as well be just invalid 2FA credentials, but we can't be sure about that, and we have LoginFailures designed to verify that for us + await MobileAuthenticator.ResetSteamTimeDifference().ConfigureAwait(false); + } + + string? generatedTwoFactorCode = await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false); + + if (!string.IsNullOrEmpty(generatedTwoFactorCode)) { + return generatedTwoFactorCode; + } + + break; + } + + RequiredInput = inputType; + + string? input = await Logging.GetUserInput(inputType, BotName).ConfigureAwait(false); + + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + if (string.IsNullOrEmpty(input) || !SetUserInput(inputType, input!)) { + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(input))); + + Stop(); + + return null; + } + + // We keep user input set in case we need to use it again due to disconnection, OnLoggedOn() will reset it for us + return input; + } + internal void RequestPersonaStateUpdate() { if (!IsConnectedAndLoggedOn) { return; @@ -1938,6 +1986,102 @@ private async Task HandleCallbacks() { } } + private async Task HandleLoginResult(EResult result, EResult extendedResult) { + if (!Enum.IsDefined(result)) { + throw new InvalidEnumArgumentException(nameof(result), (int) result, typeof(EResult)); + } + + if (!Enum.IsDefined(extendedResult)) { + throw new InvalidEnumArgumentException(nameof(extendedResult), (int) extendedResult, typeof(EResult)); + } + + // Keep LastLogOnResult for OnDisconnected() + LastLogOnResult = result; + + HeartBeatFailures = 0; + StopConnectionFailureTimer(); + + switch (result) { + case EResult.AccountDisabled: + // Those failures are permanent, we should Stop() the bot if any of those happen + ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, result, extendedResult)); + Stop(); + + break; + case EResult.AccountLogonDenied: + case EResult.InvalidLoginAuthCode: + RequiredInput = ASF.EUserInputType.SteamGuard; + + string? authCode = await Logging.GetUserInput(ASF.EUserInputType.SteamGuard, BotName).ConfigureAwait(false); + + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + if (string.IsNullOrEmpty(authCode) || !SetUserInput(ASF.EUserInputType.SteamGuard, authCode!)) { + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(authCode))); + + Stop(); + } + + break; + case EResult.AccountLoginDeniedNeedTwoFactor when !HasMobileAuthenticator: + case EResult.TwoFactorCodeMismatch when !HasMobileAuthenticator: + RequiredInput = ASF.EUserInputType.TwoFactorAuthentication; + + string? twoFactorCode = await Logging.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName).ConfigureAwait(false); + + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + if (string.IsNullOrEmpty(twoFactorCode) || !SetUserInput(ASF.EUserInputType.TwoFactorAuthentication, twoFactorCode!)) { + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(twoFactorCode))); + + Stop(); + } + + break; + case EResult.AccountLoginDeniedNeedTwoFactor: + case EResult.InvalidPassword: + case EResult.NoConnection: + case EResult.PasswordRequiredToKickSession: // Not sure about this one, it seems to be just generic "try again"? #694 + case EResult.RateLimitExceeded: + case EResult.ServiceUnavailable: + case EResult.Timeout: + case EResult.TryAnotherCM: + case EResult.TwoFactorCodeMismatch: + ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, result, extendedResult)); + + switch (result) { + case EResult.AccountLoginDeniedNeedTwoFactor: + case EResult.TwoFactorCodeMismatch: + // There is a possibility that our cached time is no longer appropriate, so we should reset the cache in this case in order to fetch it upon the next login attempt + // Yes, this might as well be just invalid 2FA credentials, but we can't be sure about that, and we have LoginFailures designed to verify that for us + await MobileAuthenticator.ResetSteamTimeDifference().ConfigureAwait(false); + + if (++LoginFailures >= MaxLoginFailures) { + LoginFailures = 0; + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidAuthenticatorDuringLogin, MaxLoginFailures)); + Stop(); + } + + break; + case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.RefreshToken) && (++LoginFailures >= MaxLoginFailures): + LoginFailures = 0; + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidPasswordDuringLogin, MaxLoginFailures)); + Stop(); + + break; + } + + break; + case EResult.OK: + break; + default: + // Unexpected result, shutdown immediately + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(result), result)); + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, result, extendedResult)); + Stop(); + + break; + } + } + private async void HeartBeat(object? state = null) { if (!KeepRunning || !IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) { return; @@ -2265,22 +2409,17 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) { } } - string? loginKey = null; - - if (ShouldUseLoginKeys && string.IsNullOrEmpty(AuthCode) && string.IsNullOrEmpty(TwoFactorCode)) { - loginKey = BotDatabase.LoginKey; + string? refreshToken = BotDatabase.RefreshToken; - // Decrypt login key if needed - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (!string.IsNullOrEmpty(loginKey) && (loginKey!.Length > 19) && BotConfig.PasswordFormat.HasTransformation()) { - loginKey = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey).ConfigureAwait(false); + if (!string.IsNullOrEmpty(refreshToken)) { + // Decrypt refreshToken if needed + if (BotConfig.PasswordFormat.HasTransformation()) { + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + refreshToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, refreshToken!).ConfigureAwait(false); } - } else { - // If we're not using login keys, ensure we don't have any saved - BotDatabase.LoginKey = null; } - if (!await InitLoginAndPassword(string.IsNullOrEmpty(loginKey)).ConfigureAwait(false)) { + if (!await InitLoginAndPassword(string.IsNullOrEmpty(refreshToken)).ConfigureAwait(false)) { Stop(); return; @@ -2318,22 +2457,57 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) { ArchiLogger.LogGenericInfo(Strings.BotLoggingIn); - if (string.IsNullOrEmpty(TwoFactorCode) && (BotDatabase.MobileAuthenticator != null)) { - // We should always include 2FA token, even if it's not required - TwoFactorCode = await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false); - } - InitConnectionFailureTimer(); + if (string.IsNullOrEmpty(refreshToken)) { + SteamAuthentication.AuthPollResult pollResponse; + + try { + using CancellationTokenSource authCancellationTokenSource = new(); + + SteamAuthentication.CredentialsAuthSession authSession = await SteamAuthentication.BeginAuthSessionViaCredentials( + new SteamAuthentication.AuthSessionDetails { + Authenticator = new BotCredentialsProvider(this, authCancellationTokenSource), + DeviceFriendlyName = SharedInfo.PublicIdentifier, + GuardData = BotConfig.UseLoginKeys ? BotDatabase.SteamGuardData : null, + IsPersistentSession = true, + Password = password, + Username = username + } + ).ConfigureAwait(false); + + pollResponse = await authSession.StartPolling(authCancellationTokenSource.Token).ConfigureAwait(false); + } catch (AuthenticationException e) { + ArchiLogger.LogGenericWarningException(e); + + await HandleLoginResult(e.Result, e.Result).ConfigureAwait(false); + + ReconnectOnUserInitiated = true; + SteamClient.Disconnect(); + + return; + } catch (OperationCanceledException) { + // This is okay, we already took care of that and can ignore it here + return; + } + + refreshToken = pollResponse.RefreshToken; + + if (BotConfig.UseLoginKeys) { + BotDatabase.RefreshToken = BotConfig.PasswordFormat.HasTransformation() ? ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken) : refreshToken; + + if (!string.IsNullOrEmpty(pollResponse.NewGuardData)) { + BotDatabase.SteamGuardData = pollResponse.NewGuardData; + } + } + } + SteamUser.LogOnDetails logOnDetails = new() { - AuthCode = AuthCode, + AccessToken = refreshToken, CellID = ASF.GlobalDatabase?.CellID, LoginID = LoginID, - LoginKey = loginKey, - Password = password, SentryFileHash = sentryFileHash, - ShouldRememberPassword = ShouldUseLoginKeys, - TwoFactorCode = TwoFactorCode, + ShouldRememberPassword = BotConfig.UseLoginKeys, Username = username }; @@ -2380,9 +2554,9 @@ private async void OnDisconnected(SteamClient.DisconnectedCallback callback) { case EResult.AccountDisabled: // Do not attempt to reconnect, those failures are permanent return; - case EResult.InvalidPassword when !string.IsNullOrEmpty(BotDatabase.LoginKey): + case EResult.InvalidPassword when !string.IsNullOrEmpty(BotDatabase.RefreshToken): // We can retry immediately - BotDatabase.LoginKey = null; + BotDatabase.RefreshToken = null; ArchiLogger.LogGenericInfo(Strings.BotRemovedExpiredLoginKey); break; @@ -2681,6 +2855,7 @@ private async void OnLicenseList(SteamApps.LicenseListCallback callback) { private void OnLoggedOff(SteamUser.LoggedOffCallback callback) { ArgumentNullException.ThrowIfNull(callback); + // Keep LastLogOnResult for OnDisconnected() LastLogOnResult = callback.Result; ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOff, callback.Result)); @@ -2716,231 +2891,133 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) { // Always reset one-time-only access tokens when we get OnLoggedOn() response AuthCode = TwoFactorCode = null; - // Keep LastLogOnResult for OnDisconnected() - LastLogOnResult = callback.Result; - - HeartBeatFailures = 0; - StopConnectionFailureTimer(); - - switch (callback.Result) { - case EResult.AccountDisabled: - // Those failures are permanent, we should Stop() the bot if any of those happen - ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult)); - Stop(); - - break; - case EResult.AccountLogonDenied: - case EResult.InvalidLoginAuthCode: - RequiredInput = ASF.EUserInputType.SteamGuard; - - string? authCode = await Logging.GetUserInput(ASF.EUserInputType.SteamGuard, BotName).ConfigureAwait(false); - - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (string.IsNullOrEmpty(authCode) || !SetUserInput(ASF.EUserInputType.SteamGuard, authCode!)) { - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(authCode))); - - Stop(); - } - - break; - case EResult.AccountLoginDeniedNeedTwoFactor when !HasMobileAuthenticator: - case EResult.TwoFactorCodeMismatch when !HasMobileAuthenticator: - RequiredInput = ASF.EUserInputType.TwoFactorAuthentication; - - string? twoFactorCode = await Logging.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName).ConfigureAwait(false); - - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (string.IsNullOrEmpty(twoFactorCode) || !SetUserInput(ASF.EUserInputType.TwoFactorAuthentication, twoFactorCode!)) { - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(twoFactorCode))); - - Stop(); - } - - break; - case EResult.OK: - AccountFlags = callback.AccountFlags; - SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID)); - - ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}")); + await HandleLoginResult(callback.Result, callback.ExtendedResult).ConfigureAwait(false); - // Old status for these doesn't matter, we'll update them if needed - LoginFailures = 0; - LibraryLocked = PlayingBlocked = false; - - if (PlayingWasBlocked && (PlayingWasBlockedTimer == null)) { - InitPlayingWasBlockedTimer(); - } + if (callback.Result != EResult.OK) { + return; + } - if (IsAccountLimited) { - ArchiLogger.LogGenericWarning(Strings.BotAccountLimited); - } + AccountFlags = callback.AccountFlags; + SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID)); - if (IsAccountLocked) { - ArchiLogger.LogGenericWarning(Strings.BotAccountLocked); - } + ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}")); - if ((callback.CellID != 0) && (ASF.GlobalDatabase != null) && (callback.CellID != ASF.GlobalDatabase.CellID)) { - ASF.GlobalDatabase.CellID = callback.CellID; - } + // Old status for these doesn't matter, we'll update them if needed + LoginFailures = 0; + LibraryLocked = PlayingBlocked = false; - // Handle steamID-based maFile - if (!HasMobileAuthenticator) { - string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, $"{SteamID}{SharedInfo.MobileAuthenticatorExtension}"); + if (PlayingWasBlocked && (PlayingWasBlockedTimer == null)) { + InitPlayingWasBlockedTimer(); + } - if (File.Exists(maFilePath)) { - await ImportAuthenticatorFromFile(maFilePath).ConfigureAwait(false); - } - } + if (IsAccountLimited) { + ArchiLogger.LogGenericWarning(Strings.BotAccountLimited); + } - if (callback.ParentalSettings != null) { - (SteamParentalActive, string? steamParentalCode) = ValidateSteamParental(callback.ParentalSettings, BotConfig.SteamParentalCode, Program.SteamParentalGeneration); + if (IsAccountLocked) { + ArchiLogger.LogGenericWarning(Strings.BotAccountLocked); + } - if (SteamParentalActive) { - // Steam parental enabled - if (!string.IsNullOrEmpty(steamParentalCode)) { - // We were able to automatically generate it, potentially with help of the config - if (BotConfig.SteamParentalCode != steamParentalCode) { - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (!SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) { - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode))); + if ((callback.CellID != 0) && (ASF.GlobalDatabase != null) && (callback.CellID != ASF.GlobalDatabase.CellID)) { + ASF.GlobalDatabase.CellID = callback.CellID; + } - Stop(); + // Handle steamID-based maFile + if (!HasMobileAuthenticator) { + string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, $"{SteamID}{SharedInfo.MobileAuthenticatorExtension}"); - break; - } - } - } else { - // We failed to generate the pin ourselves, ask the user - RequiredInput = ASF.EUserInputType.SteamParentalCode; + if (File.Exists(maFilePath)) { + await ImportAuthenticatorFromFile(maFilePath).ConfigureAwait(false); + } + } - steamParentalCode = await Logging.GetUserInput(ASF.EUserInputType.SteamParentalCode, BotName).ConfigureAwait(false); + if (callback.ParentalSettings != null) { + (SteamParentalActive, string? steamParentalCode) = ValidateSteamParental(callback.ParentalSettings, BotConfig.SteamParentalCode, Program.SteamParentalGeneration); - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (string.IsNullOrEmpty(steamParentalCode) || !SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) { - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode))); + if (SteamParentalActive) { + // Steam parental enabled + if (!string.IsNullOrEmpty(steamParentalCode)) { + // We were able to automatically generate it, potentially with help of the config + if (BotConfig.SteamParentalCode != steamParentalCode) { + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + if (!SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) { + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode))); - Stop(); + Stop(); - break; - } + return; } } } else { - // Steam parental disabled - SteamParentalActive = false; - } + // We failed to generate the pin ourselves, ask the user + RequiredInput = ASF.EUserInputType.SteamParentalCode; - ArchiWebHandler.OnVanityURLChanged(callback.VanityURL); + steamParentalCode = await Logging.GetUserInput(ASF.EUserInputType.SteamParentalCode, BotName).ConfigureAwait(false); - if (!await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce ?? throw new InvalidOperationException(nameof(callback.WebAPIUserNonce)), SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) { - if (!await RefreshSession().ConfigureAwait(false)) { - break; - } - } + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + if (string.IsNullOrEmpty(steamParentalCode) || !SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) { + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode))); - // Pre-fetch API key for future usage if possible - Utilities.InBackground(ArchiWebHandler.HasValidApiKey); + Stop(); - if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground) { - Utilities.InBackground(() => RedeemGamesInBackground()); + return; + } } + } + } else { + // Steam parental disabled + SteamParentalActive = false; + } - ArchiHandler.SetCurrentMode(BotConfig.UserInterfaceMode); - ArchiHandler.RequestItemAnnouncements(); - - // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly - RequestPersonaStateUpdate(); - - Utilities.InBackground(InitializeFamilySharing); - - ResetPersonaState(); - - if (BotConfig.SteamMasterClanID != 0) { - Utilities.InBackground( - async () => { - if (!await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false)) { - ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup))); - } - - await JoinMasterChatGroupID().ConfigureAwait(false); - } - ); - } + ArchiWebHandler.OnVanityURLChanged(callback.VanityURL); - if (BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) { - Utilities.InBackground(() => ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID)); - } + if (!await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce ?? throw new InvalidOperationException(nameof(callback.WebAPIUserNonce)), SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) { + if (!await RefreshSession().ConfigureAwait(false)) { + return; + } + } - if (CardsFarmer.Paused) { - // Emit initial game playing status in this case - Utilities.InBackground(ResetGamesPlayed); - } + // Pre-fetch API key for future usage if possible + Utilities.InBackground(ArchiWebHandler.HasValidApiKey); - SteamPICSChanges.OnBotLoggedOn(); + if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground) { + Utilities.InBackground(() => RedeemGamesInBackground()); + } - await PluginsCore.OnBotLoggedOn(this).ConfigureAwait(false); + ArchiHandler.SetCurrentMode(BotConfig.UserInterfaceMode); + ArchiHandler.RequestItemAnnouncements(); - break; - case EResult.AccountLoginDeniedNeedTwoFactor: - case EResult.InvalidPassword: - case EResult.NoConnection: - case EResult.PasswordRequiredToKickSession: // Not sure about this one, it seems to be just generic "try again"? #694 - case EResult.RateLimitExceeded: - case EResult.ServiceUnavailable: - case EResult.Timeout: - case EResult.TryAnotherCM: - case EResult.TwoFactorCodeMismatch: - ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult)); + // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly + RequestPersonaStateUpdate(); - switch (callback.Result) { - case EResult.AccountLoginDeniedNeedTwoFactor: - case EResult.TwoFactorCodeMismatch: - // There is a possibility that our cached time is no longer appropriate, so we should reset the cache in this case in order to fetch it upon the next login attempt - // Yes, this might as well be just invalid 2FA credentials, but we can't be sure about that, and we have LoginFailures designed to verify that for us - await MobileAuthenticator.ResetSteamTimeDifference().ConfigureAwait(false); + Utilities.InBackground(InitializeFamilySharing); - if (++LoginFailures >= MaxLoginFailures) { - LoginFailures = 0; - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidAuthenticatorDuringLogin, MaxLoginFailures)); - Stop(); - } + ResetPersonaState(); - break; - case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.LoginKey) && (++LoginFailures >= MaxLoginFailures): - LoginFailures = 0; - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidPasswordDuringLogin, MaxLoginFailures)); - Stop(); + if (BotConfig.SteamMasterClanID != 0) { + Utilities.InBackground( + async () => { + if (!await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false)) { + ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup))); + } - break; + await JoinMasterChatGroupID().ConfigureAwait(false); } - - break; - default: - // Unexpected result, shutdown immediately - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(callback.Result), callback.Result)); - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult)); - Stop(); - - break; + ); } - } - - private void OnLoginKey(SteamUser.LoginKeyCallback callback) { - ArgumentNullException.ThrowIfNull(callback); - ArgumentNullException.ThrowIfNull(callback.LoginKey); - if (!ShouldUseLoginKeys) { - return; + if (BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) { + Utilities.InBackground(() => ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID)); } - string? loginKey = callback.LoginKey; - - if (BotConfig.PasswordFormat.HasTransformation()) { - loginKey = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey); + if (CardsFarmer.Paused) { + // Emit initial game playing status in this case + Utilities.InBackground(ResetGamesPlayed); } - BotDatabase.LoginKey = loginKey; - SteamUser.AcceptNewLoginKey(callback); + SteamPICSChanges.OnBotLoggedOn(); + + await PluginsCore.OnBotLoggedOn(this).ConfigureAwait(false); } private async void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) { diff --git a/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs b/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs new file mode 100644 index 0000000000000..ee1cce833d71b --- /dev/null +++ b/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs @@ -0,0 +1,80 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Ł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.ComponentModel; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.Localization; +using SteamKit2; + +namespace ArchiSteamFarm.Steam.Integration; + +internal sealed class BotCredentialsProvider : IAuthenticator { + private const byte MaxLoginFailures = 5; + + private readonly Bot Bot; + private readonly CancellationTokenSource CancellationTokenSource; + + private byte LoginFailures; + + internal BotCredentialsProvider(Bot bot, CancellationTokenSource cancellationTokenSource) { + ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(cancellationTokenSource); + + Bot = bot; + CancellationTokenSource = cancellationTokenSource; + } + + public Task AcceptDeviceConfirmation() => Task.FromResult(false); + + public async Task ProvideDeviceCode(bool previousCodeWasIncorrect) => await ProvideInput(ASF.EUserInputType.TwoFactorAuthentication, previousCodeWasIncorrect).ConfigureAwait(false); + + public async Task ProvideEmailCode(string email, bool previousCodeWasIncorrect) => await ProvideInput(ASF.EUserInputType.SteamGuard, previousCodeWasIncorrect).ConfigureAwait(false); + + private async Task ProvideInput(ASF.EUserInputType inputType, bool previousCodeWasIncorrect) { + if (!Enum.IsDefined(inputType)) { + throw new InvalidEnumArgumentException(nameof(inputType), (int) inputType, typeof(ASF.EUserInputType)); + } + + if (previousCodeWasIncorrect && (++LoginFailures >= MaxLoginFailures)) { + EResult reason = inputType == ASF.EUserInputType.TwoFactorAuthentication ? EResult.TwoFactorCodeMismatch : EResult.InvalidLoginAuthCode; + + Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, reason, reason)); + + if (++LoginFailures >= MaxLoginFailures) { + CancellationTokenSource.Cancel(); + + return ""; + } + } + + string? result = await Bot.RequestInput(inputType, previousCodeWasIncorrect).ConfigureAwait(false); + + if (string.IsNullOrEmpty(result)) { + CancellationTokenSource.Cancel(); + } + + return result ?? ""; + } +} diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs index df3f0c7edf321..6cebd9c186646 100644 --- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs +++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs @@ -62,38 +62,54 @@ internal uint GamesToRedeemInBackgroundCount { [JsonProperty(Required = Required.DisallowNull)] private readonly OrderedDictionary GamesToRedeemInBackground = new(); - internal string? LoginKey { - get => BackingLoginKey; + internal MobileAuthenticator? MobileAuthenticator { + get => BackingMobileAuthenticator; set { - if (BackingLoginKey == value) { + if (BackingMobileAuthenticator == value) { return; } - BackingLoginKey = value; + BackingMobileAuthenticator = value; Utilities.InBackground(Save); } } - internal MobileAuthenticator? MobileAuthenticator { - get => BackingMobileAuthenticator; + internal string? RefreshToken { + get => BackingRefreshToken; set { - if (BackingMobileAuthenticator == value) { + if (BackingRefreshToken == value) { return; } - BackingMobileAuthenticator = value; + BackingRefreshToken = value; Utilities.InBackground(Save); } } - [JsonProperty($"_{nameof(LoginKey)}")] - private string? BackingLoginKey; + internal string? SteamGuardData { + get => BackingSteamGuardData; + + set { + if (BackingSteamGuardData == value) { + return; + } + + BackingSteamGuardData = value; + Utilities.InBackground(Save); + } + } [JsonProperty($"_{nameof(MobileAuthenticator)}")] private MobileAuthenticator? BackingMobileAuthenticator; + [JsonProperty] + private string? BackingRefreshToken; + + [JsonProperty] + private string? BackingSteamGuardData; + private BotDatabase(string filePath) : this() { if (string.IsNullOrEmpty(filePath)) { throw new ArgumentNullException(nameof(filePath)); @@ -111,10 +127,13 @@ private BotDatabase() { } [UsedImplicitly] - public bool ShouldSerializeBackingLoginKey() => !string.IsNullOrEmpty(BackingLoginKey); + public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null; [UsedImplicitly] - public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null; + public bool ShouldSerializeBackingRefreshToken() => !string.IsNullOrEmpty(BackingRefreshToken); + + [UsedImplicitly] + public bool ShouldSerializeBackingSteamGuardData() => !string.IsNullOrEmpty(BackingSteamGuardData); [UsedImplicitly] public bool ShouldSerializeFarmingBlacklistAppIDs() => FarmingBlacklistAppIDs.Count > 0; diff --git a/ArchiSteamFarm/Temp/SteamKit2.dll b/ArchiSteamFarm/Temp/SteamKit2.dll new file mode 100644 index 0000000000000..205d64420020b Binary files /dev/null and b/ArchiSteamFarm/Temp/SteamKit2.dll differ diff --git a/Directory.Packages.props b/Directory.Packages.props index 7cb7348f59643..2f7a9a73cd7f6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,11 +7,13 @@ + +