Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #2840 #2843

Merged
merged 15 commits into from
Mar 17, 2023
6 changes: 6 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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
"allowedVersions": "<= 3.1.17",
"matchManagers": [ "nuget" ],
"matchPackageNames": [ "protobuf-net" ]
}
]
}
6 changes: 5 additions & 1 deletion ArchiSteamFarm/ArchiSteamFarm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Nito.AsyncEx.Coordination" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="SteamKit2" />
<PackageReference Include="protobuf-net" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
<PackageReference Include="System.Composition" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="zxcvbn-core" />

<Reference Include="SteamKit2">
<HintPath>Temp\SteamKit2.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net481'">
Expand Down
2 changes: 1 addition & 1 deletion ArchiSteamFarm/Core/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public static async void InBackground(Action action, bool longRunning = false) {
public static void InBackground<T>(Func<T> function, bool longRunning = false) {
ArgumentNullException.ThrowIfNull(function);

InBackground(void () => function(), longRunning);
InBackground(void() => function(), longRunning);
}

[PublicAPI]
Expand Down
149 changes: 100 additions & 49 deletions ArchiSteamFarm/Steam/Bot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
private readonly SemaphoreSlim MessagingSemaphore = new(1, 1);
private readonly ConcurrentDictionary<UserNotificationsCallback.EUserNotification, uint> PastNotifications = new();
private readonly SemaphoreSlim SendCompleteTypesSemaphore = new(1, 1);
private readonly SteamAuthentication SteamAuthentication;
private readonly SteamClient SteamClient;
private readonly ConcurrentHashSet<ulong> SteamFamilySharingIDs = new();
private readonly SteamUser SteamUser;
Expand All @@ -178,11 +179,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
}

/// <remarks>
/// Login keys are not guaranteed to be valid, we should use them only if we don't have full details available from the user
/// </remarks>
private bool ShouldUseLoginKeys => BotConfig.UseLoginKeys && (!BotConfig.IsSteamPasswordSet || !HasMobileAuthenticator);
JustArchi marked this conversation as resolved.
Show resolved Hide resolved

[JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")]
private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture);

Expand Down Expand Up @@ -302,6 +298,8 @@ private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) {
CallbackManager.Subscribe<SteamApps.GuestPassListCallback>(OnGuestPassList);
CallbackManager.Subscribe<SteamApps.LicenseListCallback>(OnLicenseList);

SteamAuthentication = SteamClient.GetHandler<SteamAuthentication>() ?? throw new InvalidOperationException(nameof(SteamAuthentication));

SteamFriends = SteamClient.GetHandler<SteamFriends>() ?? throw new InvalidOperationException(nameof(SteamFriends));
CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
CallbackManager.Subscribe<SteamFriends.PersonaStateCallback>(OnPersonaState);
Expand All @@ -311,7 +309,6 @@ private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) {
SteamUser = SteamClient.GetHandler<SteamUser>() ?? throw new InvalidOperationException(nameof(SteamUser));
CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
CallbackManager.Subscribe<SteamUser.LoginKeyCallback>(OnLoginKey);
CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
CallbackManager.Subscribe<SteamUser.VanityURLChangedCallback>(OnVanityURLChangedCallback);
CallbackManager.Subscribe<SteamUser.WalletInfoCallback>(OnWalletUpdate);
Expand Down Expand Up @@ -1637,6 +1634,50 @@ internal async Task<bool> Rename(string newBotName) {
return true;
}

internal async Task<string?> RequestInput(ASF.EUserInputType inputType) {
if ((inputType == ASF.EUserInputType.None) || !Enum.IsDefined(inputType)) {
throw new InvalidEnumArgumentException(nameof(inputType), (int) inputType, typeof(ASF.EUserInputType));
}

while (true) {
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:
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;
}
}
}

internal void RequestPersonaStateUpdate() {
if (!IsConnectedAndLoggedOn) {
return;
Expand Down Expand Up @@ -2265,22 +2306,17 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) {
}
}

string? loginKey = null;
string? refreshToken = BotDatabase.RefreshToken;

if (ShouldUseLoginKeys && string.IsNullOrEmpty(AuthCode) && string.IsNullOrEmpty(TwoFactorCode)) {
loginKey = BotDatabase.LoginKey;

// 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;
Expand Down Expand Up @@ -2318,22 +2354,55 @@ 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 {
SteamAuthentication.CredentialsAuthSession authSession = await SteamAuthentication.BeginAuthSessionViaCredentials(
new SteamAuthentication.AuthSessionDetails {
Authenticator = new BotCredentialsProvider(this),
DeviceFriendlyName = SharedInfo.PublicIdentifier,
GuardData = BotConfig.UseLoginKeys ? BotDatabase.SteamGuardData : null,
IsPersistentSession = true,
Password = password,
Username = username,
WebsiteID = "Client"
}
).ConfigureAwait(false);

pollResponse = await authSession.StartPolling().ConfigureAwait(false);
} catch (AuthenticationException e) {
ArchiLogger.LogGenericWarningException(e);

LastLogOnResult = e.Result;
ReconnectOnUserInitiated = true;
SteamClient.Disconnect();

return;
} catch (InvalidOperationException e) when (e.Message == "No code was provided by the authenticator.") {
// 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
};

Expand Down Expand Up @@ -2380,9 +2449,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;
Expand Down Expand Up @@ -2906,7 +2975,7 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
}

break;
case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.LoginKey) && (++LoginFailures >= MaxLoginFailures):
case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.RefreshToken) && (++LoginFailures >= MaxLoginFailures):
LoginFailures = 0;
ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidPasswordDuringLogin, MaxLoginFailures));
Stop();
Expand All @@ -2925,24 +2994,6 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
}
}

private void OnLoginKey(SteamUser.LoginKeyCallback callback) {
ArgumentNullException.ThrowIfNull(callback);
ArgumentNullException.ThrowIfNull(callback.LoginKey);

if (!ShouldUseLoginKeys) {
return;
}

string? loginKey = callback.LoginKey;

if (BotConfig.PasswordFormat.HasTransformation()) {
loginKey = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey);
}

BotDatabase.LoginKey = loginKey;
SteamUser.AcceptNewLoginKey(callback);
}

private async void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
ArgumentNullException.ThrowIfNull(callback);

Expand Down
43 changes: 43 additions & 0 deletions ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: [email protected]
// |
// 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.Threading.Tasks;
using ArchiSteamFarm.Core;
using SteamKit2;

namespace ArchiSteamFarm.Steam.Integration;

internal sealed class BotCredentialsProvider : IAuthenticator {
JustArchi marked this conversation as resolved.
Show resolved Hide resolved
private readonly Bot Bot;

internal BotCredentialsProvider(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);

Bot = bot;
}

public Task<bool> AcceptDeviceConfirmation() => Task.FromResult(false);

public async Task<string> ProvideDeviceCode() => await Bot.RequestInput(ASF.EUserInputType.TwoFactorAuthentication).ConfigureAwait(false) ?? "";

public async Task<string> ProvideEmailCode(string email) => await Bot.RequestInput(ASF.EUserInputType.SteamGuard).ConfigureAwait(false) ?? "";
}
43 changes: 31 additions & 12 deletions ArchiSteamFarm/Steam/Storage/BotDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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;
Expand Down
Binary file added ArchiSteamFarm/Temp/SteamKit2.dll
Binary file not shown.
Loading