mirror of
https://github.com/JustArchiNET/ArchiSteamFarm
synced 2024-11-10 07:04:27 +00:00
* Implement support for access tokens A bit more work and testing is needed * Make ValidUntil computed, fix netf, among others * netf fixes as always * Allow AWH to forcefully refresh session * Unify access token lifetime
This commit is contained in:
parent
4106c5f41a
commit
d571cd9580
7 changed files with 244 additions and 118 deletions
|
@ -21,6 +21,7 @@
|
|||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
|
||||
<PackageReference Include="System.Composition" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="zxcvbn-core" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -24,6 +24,7 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -48,6 +49,8 @@ public static class Utilities {
|
|||
// normally we'd just use words like "steam" and "farm", but the library we're currently using is a bit iffy about banned words, so we need to also add combinations such as "steamfarm"
|
||||
private static readonly ImmutableHashSet<string> ForbiddenPasswordPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password");
|
||||
|
||||
private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new();
|
||||
|
||||
[PublicAPI]
|
||||
public static string GetArgsAsText(string[] args, byte argsToSkip, string delimiter) {
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
|
@ -187,6 +190,21 @@ public static class Utilities {
|
|||
return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static JwtSecurityToken? ReadJwtToken(string token) {
|
||||
if (string.IsNullOrEmpty(token)) {
|
||||
throw new ArgumentNullException(nameof(token));
|
||||
}
|
||||
|
||||
try {
|
||||
return JwtSecurityTokenHandler.ReadJwtToken(token);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
|
|
@ -27,6 +27,7 @@ using System.Collections.Immutable;
|
|||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
@ -67,6 +68,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
private const byte LoginCooldownInMinutes = 25; // Captcha disappears after around 20 minutes, so we make it 25
|
||||
private const uint LoginID = 1242; // This must be the same for all ASF bots and all ASF processes
|
||||
private const byte MaxLoginFailures = WebBrowser.MaxTries; // Max login failures in a row before we determine that our credentials are invalid (because Steam wrongly returns those, of course)course)
|
||||
private const byte MinimumAccessTokenValidityMinutes = 10;
|
||||
private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam
|
||||
|
||||
[PublicAPI]
|
||||
|
@ -229,11 +231,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
internal bool PlayingBlocked { get; private set; }
|
||||
internal bool PlayingWasBlocked { get; private set; }
|
||||
|
||||
private DateTime? AccessTokenValidUntil;
|
||||
private string? AuthCode;
|
||||
|
||||
[JsonProperty]
|
||||
private string? AvatarHash;
|
||||
|
||||
private string? BackingAccessToken;
|
||||
private Timer? ConnectionFailureTimer;
|
||||
private bool FirstTradeSent;
|
||||
private Timer? GamesRedeemerInBackgroundTimer;
|
||||
|
@ -244,6 +248,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
private ulong MasterChatGroupID;
|
||||
private Timer? PlayingWasBlockedTimer;
|
||||
private bool ReconnectOnUserInitiated;
|
||||
private string? RefreshToken;
|
||||
private Timer? RefreshTokensTimer;
|
||||
private bool SendCompleteTypesScheduled;
|
||||
private Timer? SendItemsTimer;
|
||||
private bool SteamParentalActive;
|
||||
|
@ -251,6 +257,30 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
private Timer? TradeCheckTimer;
|
||||
private string? TwoFactorCode;
|
||||
|
||||
private string? AccessToken {
|
||||
get => BackingAccessToken;
|
||||
|
||||
set {
|
||||
AccessTokenValidUntil = null;
|
||||
BackingAccessToken = value;
|
||||
|
||||
if (string.IsNullOrEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
JwtSecurityToken? jwtToken = Utilities.ReadJwtToken(value!);
|
||||
|
||||
if (jwtToken == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwtToken.ValidTo > DateTime.MinValue) {
|
||||
AccessTokenValidUntil = jwtToken.ValidTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) {
|
||||
BotName = !string.IsNullOrEmpty(botName) ? botName : throw new ArgumentNullException(nameof(botName));
|
||||
BotConfig = botConfig ?? throw new ArgumentNullException(nameof(botConfig));
|
||||
|
@ -357,6 +387,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
ConnectionFailureTimer?.Dispose();
|
||||
GamesRedeemerInBackgroundTimer?.Dispose();
|
||||
PlayingWasBlockedTimer?.Dispose();
|
||||
RefreshTokensTimer?.Dispose();
|
||||
SendItemsTimer?.Dispose();
|
||||
SteamSaleEvent?.Dispose();
|
||||
TradeCheckTimer?.Dispose();
|
||||
|
@ -390,6 +421,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
await PlayingWasBlockedTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (RefreshTokensTimer != null) {
|
||||
await RefreshTokensTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (SendItemsTimer != null) {
|
||||
await SendItemsTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
@ -1492,32 +1527,55 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
await PluginsCore.OnBotFarmingStopped(this).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task<bool> RefreshSession() {
|
||||
internal async Task<bool> RefreshWebSession(bool force = false) {
|
||||
if (!IsConnectedAndLoggedOn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SteamUser.WebAPIUserNonceCallback callback;
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
try {
|
||||
callback = await SteamUser.RequestWebAPIUserNonce().ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
if (!force && !string.IsNullOrEmpty(AccessToken) && AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > now.AddMinutes(MinimumAccessTokenValidityMinutes))) {
|
||||
// We can use the tokens we already have
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, AccessToken!, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
InitRefreshTokensTimer(AccessTokenValidUntil.Value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to refresh our session, access token is no longer valid
|
||||
BotDatabase.AccessToken = AccessToken = null;
|
||||
|
||||
if (string.IsNullOrEmpty(RefreshToken)) {
|
||||
// Without refresh token we can't get fresh access tokens, relog needed
|
||||
await Connect(true).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(callback.Nonce)) {
|
||||
CAuthentication_AccessToken_GenerateForApp_Response? response = await ArchiHandler.GenerateAccessTokens(RefreshToken!).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
// The request has failed, in almost all cases this means our refresh token is no longer valid, relog needed
|
||||
BotDatabase.RefreshToken = RefreshToken = null;
|
||||
|
||||
await Connect(true).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.Nonce, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
// TODO: Handle update of refresh token with next SK2 release
|
||||
UpdateTokens(response.access_token, RefreshToken!);
|
||||
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, response.access_token, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
InitRefreshTokensTimer(AccessTokenValidUntil ?? now.AddDays(1));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// We got the tokens, but failed to authorize? Purge them just to be sure and reconnect
|
||||
BotDatabase.AccessToken = AccessToken = null;
|
||||
|
||||
await Connect(true).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
|
@ -2274,6 +2332,19 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
WalletBalance = 0;
|
||||
WalletCurrency = ECurrencyCode.Invalid;
|
||||
|
||||
AccessToken = BotDatabase.AccessToken;
|
||||
RefreshToken = BotDatabase.RefreshToken;
|
||||
|
||||
if (BotConfig.PasswordFormat.HasTransformation()) {
|
||||
if (!string.IsNullOrEmpty(AccessToken)) {
|
||||
AccessToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, AccessToken!).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(RefreshToken)) {
|
||||
AccessToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, RefreshToken!).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CardsFarmer.SetInitialState(BotConfig.Paused);
|
||||
|
||||
if (SendItemsTimer != null) {
|
||||
|
@ -2344,6 +2415,42 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
);
|
||||
}
|
||||
|
||||
private void InitRefreshTokensTimer(DateTime validUntil) {
|
||||
if (validUntil == DateTime.MinValue) {
|
||||
throw new ArgumentOutOfRangeException(nameof(validUntil));
|
||||
}
|
||||
|
||||
if (validUntil == DateTime.MaxValue) {
|
||||
// OK, tokens do not require refreshing
|
||||
StopRefreshTokensTimer();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan delay = validUntil - DateTime.UtcNow;
|
||||
|
||||
// Start refreshing token before it's invalid
|
||||
if (delay.TotalMinutes > MinimumAccessTokenValidityMinutes) {
|
||||
delay -= TimeSpan.FromMinutes(MinimumAccessTokenValidityMinutes);
|
||||
} else {
|
||||
delay = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
// Timer can accept only dueTimes up to 2^32 - 2
|
||||
uint dueTime = (uint) Math.Min(uint.MaxValue - 1, (ulong) delay.TotalMilliseconds);
|
||||
|
||||
if (RefreshTokensTimer == null) {
|
||||
RefreshTokensTimer = new Timer(
|
||||
OnRefreshTokensTimer,
|
||||
null,
|
||||
TimeSpan.FromMilliseconds(dueTime), // Delay
|
||||
TimeSpan.FromMinutes(1) // Period
|
||||
);
|
||||
} else {
|
||||
RefreshTokensTimer.Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitStart() {
|
||||
if (!BotConfig.Enabled) {
|
||||
ArchiLogger.LogGenericWarning(Strings.BotInstanceNotStartingBecauseDisabled);
|
||||
|
@ -2482,17 +2589,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
string? refreshToken = BotDatabase.RefreshToken;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!await InitLoginAndPassword(string.IsNullOrEmpty(refreshToken)).ConfigureAwait(false)) {
|
||||
if (!await InitLoginAndPassword(string.IsNullOrEmpty(RefreshToken)).ConfigureAwait(false)) {
|
||||
Stop();
|
||||
|
||||
return;
|
||||
|
@ -2537,7 +2634,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
|
||||
InitConnectionFailureTimer();
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
if (string.IsNullOrEmpty(RefreshToken)) {
|
||||
AuthPollResult pollResult;
|
||||
|
||||
try {
|
||||
|
@ -2569,19 +2666,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
return;
|
||||
}
|
||||
|
||||
refreshToken = pollResult.RefreshToken;
|
||||
|
||||
if (BotConfig.UseLoginKeys) {
|
||||
BotDatabase.RefreshToken = BotConfig.PasswordFormat.HasTransformation() ? ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken) : refreshToken;
|
||||
|
||||
if (!string.IsNullOrEmpty(pollResult.NewGuardData)) {
|
||||
BotDatabase.SteamGuardData = pollResult.NewGuardData;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(pollResult.NewGuardData) && BotConfig.UseLoginKeys) {
|
||||
BotDatabase.SteamGuardData = pollResult.NewGuardData;
|
||||
}
|
||||
|
||||
UpdateTokens(pollResult.AccessToken, pollResult.RefreshToken);
|
||||
}
|
||||
|
||||
SteamUser.LogOnDetails logOnDetails = new() {
|
||||
AccessToken = refreshToken,
|
||||
AccessToken = RefreshToken,
|
||||
CellID = ASF.GlobalDatabase?.CellID,
|
||||
LoginID = LoginID,
|
||||
SentryFileHash = sentryFileHash,
|
||||
|
@ -2606,6 +2699,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
HeartBeatFailures = 0;
|
||||
StopConnectionFailureTimer();
|
||||
StopPlayingWasBlockedTimer();
|
||||
StopRefreshTokensTimer();
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.BotDisconnected);
|
||||
|
||||
|
@ -3087,11 +3181,9 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
|
||||
ArchiWebHandler.OnVanityURLChanged(callback.VanityURL);
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
if (string.IsNullOrEmpty(callback.WebAPIUserNonce) || !await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce!, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
if (!await RefreshSession().ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
// Establish web session
|
||||
if (!await RefreshWebSession().ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-fetch API key for future usage if possible
|
||||
|
@ -3236,6 +3328,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
await CheckOccupationStatus().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnRefreshTokensTimer(object? state = null) {
|
||||
if (AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > DateTime.UtcNow.AddMinutes(MinimumAccessTokenValidityMinutes))) {
|
||||
// We don't need to refresh just yet
|
||||
InitRefreshTokensTimer(AccessTokenValidUntil.Value);
|
||||
}
|
||||
|
||||
await RefreshWebSession().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnSendItemsTimer(object? state = null) => await Actions.SendInventory(filterFunction: item => BotConfig.LootableTypes.Contains(item.Type)).ConfigureAwait(false);
|
||||
|
||||
private async void OnServiceMethod(SteamUnifiedMessages.ServiceMethodNotification notification) {
|
||||
|
@ -3708,6 +3809,38 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
|||
PlayingWasBlockedTimer = null;
|
||||
}
|
||||
|
||||
private void StopRefreshTokensTimer() {
|
||||
if (RefreshTokensTimer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshTokensTimer.Dispose();
|
||||
RefreshTokensTimer = null;
|
||||
}
|
||||
|
||||
private void UpdateTokens(string accessToken, string refreshToken) {
|
||||
if (string.IsNullOrEmpty(accessToken)) {
|
||||
throw new ArgumentNullException(nameof(accessToken));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
throw new ArgumentNullException(nameof(refreshToken));
|
||||
}
|
||||
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
|
||||
if (BotConfig.UseLoginKeys) {
|
||||
if (BotConfig.PasswordFormat.HasTransformation()) {
|
||||
BotDatabase.AccessToken = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, accessToken);
|
||||
BotDatabase.RefreshToken = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken);
|
||||
} else {
|
||||
BotDatabase.AccessToken = accessToken;
|
||||
BotDatabase.RefreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (bool IsSteamParentalEnabled, string? SteamParentalCode) ValidateSteamParental(ParentalSettings settings, string? steamParentalCode = null, bool allowGeneration = true) {
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
|||
internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network
|
||||
|
||||
private readonly ArchiLogger ArchiLogger;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<IAuthentication> UnifiedAuthenticationService;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<IChatRoom> UnifiedChatRoomService;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<IClanChatRooms> UnifiedClanChatRoomsService;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<ICredentials> UnifiedCredentialsService;
|
||||
|
@ -53,6 +54,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
|||
ArgumentNullException.ThrowIfNull(steamUnifiedMessages);
|
||||
|
||||
ArchiLogger = archiLogger ?? throw new ArgumentNullException(nameof(archiLogger));
|
||||
UnifiedAuthenticationService = steamUnifiedMessages.CreateService<IAuthentication>();
|
||||
UnifiedChatRoomService = steamUnifiedMessages.CreateService<IChatRoom>();
|
||||
UnifiedClanChatRoomsService = steamUnifiedMessages.CreateService<IClanChatRooms>();
|
||||
UnifiedCredentialsService = steamUnifiedMessages.CreateService<ICredentials>();
|
||||
|
@ -358,6 +360,37 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
|||
Client.Send(request);
|
||||
}
|
||||
|
||||
internal async Task<CAuthentication_AccessToken_GenerateForApp_Response?> GenerateAccessTokens(string refreshToken) {
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
throw new ArgumentNullException(nameof(refreshToken));
|
||||
}
|
||||
|
||||
if (Client == null) {
|
||||
throw new InvalidOperationException(nameof(Client));
|
||||
}
|
||||
|
||||
if (!Client.IsConnected || (Client.SteamID == null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CAuthentication_AccessToken_GenerateForApp_Request request = new() {
|
||||
refresh_token = refreshToken,
|
||||
steamid = Client.SteamID
|
||||
};
|
||||
|
||||
SteamUnifiedMessages.ServiceMethodResponse response;
|
||||
|
||||
try {
|
||||
response = await UnifiedAuthenticationService.SendMessage(x => x.GenerateAccessTokenForApp(request)).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Result == EResult.OK ? response.GetDeserializedResponse<CAuthentication_AccessToken_GenerateForApp_Response>() : null;
|
||||
}
|
||||
|
||||
internal async Task<ulong> GetClanChatGroupID(ulong steamID) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsClanAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
|
|
|
@ -52,7 +52,6 @@ public sealed class ArchiWebHandler : IDisposable {
|
|||
private const ushort MaxItemsInSingleInventoryRequest = 5000;
|
||||
private const byte MinimumSessionValidityInSeconds = 10;
|
||||
private const string SteamAppsService = "ISteamApps";
|
||||
private const string SteamUserAuthService = "ISteamUserAuth";
|
||||
private const string SteamUserService = "ISteamUser";
|
||||
private const string TwoFactorService = "ITwoFactorService";
|
||||
|
||||
|
@ -2290,7 +2289,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
|||
return response?.Content?.Success;
|
||||
}
|
||||
|
||||
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string? parentalCode = null) {
|
||||
internal async Task<bool> Init(ulong steamID, EUniverse universe, string accessToken, string? parentalCode = null) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
@ -2299,83 +2298,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
|||
throw new InvalidEnumArgumentException(nameof(universe), (int) universe, typeof(EUniverse));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(webAPIUserNonce)) {
|
||||
throw new ArgumentNullException(nameof(webAPIUserNonce));
|
||||
}
|
||||
|
||||
byte[]? publicKey = KeyDictionary.GetPublicKey(universe);
|
||||
|
||||
if ((publicKey == null) || (publicKey.Length == 0)) {
|
||||
Bot.ArchiLogger.LogNullError(publicKey);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate a random 32-byte session key
|
||||
byte[] sessionKey = CryptoHelper.GenerateRandomBlock(32);
|
||||
|
||||
// RSA encrypt our session key with the public key for the universe we're on
|
||||
byte[] encryptedSessionKey;
|
||||
|
||||
using (RSACrypto rsa = new(publicKey)) {
|
||||
encryptedSessionKey = rsa.Encrypt(sessionKey);
|
||||
}
|
||||
|
||||
// Generate login key from the user nonce that we've received from Steam network
|
||||
byte[] loginKey = Encoding.UTF8.GetBytes(webAPIUserNonce);
|
||||
|
||||
// AES encrypt our login key with our session key
|
||||
byte[] encryptedLoginKey = CryptoHelper.SymmetricEncrypt(loginKey, sessionKey);
|
||||
|
||||
Dictionary<string, object?> arguments = new(3, StringComparer.Ordinal) {
|
||||
{ "encrypted_loginkey", encryptedLoginKey },
|
||||
{ "sessionkey", encryptedSessionKey },
|
||||
{ "steamid", steamID }
|
||||
};
|
||||
|
||||
// We're now ready to send the data to Steam API
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.LoggingIn, SteamUserAuthService));
|
||||
|
||||
KeyValue? response;
|
||||
|
||||
// We do not use usual retry pattern here as webAPIUserNonce is valid only for a single request
|
||||
// Even during timeout, webAPIUserNonce is most likely already invalid
|
||||
// Instead, the caller is supposed to ask for new webAPIUserNonce and call Init() again on failure
|
||||
using (WebAPI.AsyncInterface steamUserAuthService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(SteamUserAuthService)) {
|
||||
steamUserAuthService.Timeout = WebBrowser.Timeout;
|
||||
|
||||
try {
|
||||
response = await WebLimitRequest(
|
||||
WebAPI.DefaultBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await steamUserAuthService.CallAsync(HttpMethod.Post, "AuthenticateUser", args: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string? steamLogin = response["token"].AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(steamLogin)) {
|
||||
Bot.ArchiLogger.LogNullError(steamLogin);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? steamLoginSecure = response["tokensecure"].AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(steamLoginSecure)) {
|
||||
Bot.ArchiLogger.LogNullError(steamLoginSecure);
|
||||
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(accessToken)) {
|
||||
throw new ArgumentNullException(nameof(accessToken));
|
||||
}
|
||||
|
||||
string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString(CultureInfo.InvariantCulture)));
|
||||
|
@ -2385,10 +2309,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
|||
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamHelpURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamStoreURL.Host}"));
|
||||
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamCheckoutURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamCommunityURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamHelpURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamStoreURL.Host}"));
|
||||
string steamLoginSecure = $"{steamID}||{accessToken}";
|
||||
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", $".{SteamCheckoutURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", $".{SteamCommunityURL.Host}"));
|
||||
|
@ -2782,7 +2703,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
|||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.RefreshingOurSession);
|
||||
bool result = await Bot.RefreshSession().ConfigureAwait(false);
|
||||
bool result = await Bot.RefreshWebSession(true).ConfigureAwait(false);
|
||||
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
|
|
|
@ -68,6 +68,19 @@ public sealed class BotDatabase : GenericDatabase {
|
|||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly OrderedDictionary GamesToRedeemInBackground = new();
|
||||
|
||||
internal string? AccessToken {
|
||||
get => BackingAccessToken;
|
||||
|
||||
set {
|
||||
if (BackingAccessToken == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingAccessToken = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
}
|
||||
|
||||
internal MobileAuthenticator? MobileAuthenticator {
|
||||
get => BackingMobileAuthenticator;
|
||||
|
||||
|
@ -107,6 +120,9 @@ public sealed class BotDatabase : GenericDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
[JsonProperty]
|
||||
private string? BackingAccessToken;
|
||||
|
||||
[JsonProperty($"_{nameof(MobileAuthenticator)}")]
|
||||
private MobileAuthenticator? BackingMobileAuthenticator;
|
||||
|
||||
|
@ -134,6 +150,9 @@ public sealed class BotDatabase : GenericDatabase {
|
|||
TradingBlacklistSteamIDs.OnModified += OnObjectModified;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingAccessToken() => !string.IsNullOrEmpty(BackingAccessToken);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null;
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
|
||||
<PackageVersion Include="System.Composition" Version="7.0.0" />
|
||||
<PackageVersion Include="System.Composition.AttributedModel" Version="7.0.0" />
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="zxcvbn-core" Version="7.0.92" />
|
||||
</ItemGroup>
|
||||
|
|
Loading…
Reference in a new issue