ArchiSteamFarm/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs

402 lines
15 KiB
C#
Raw Normal View History

2024-03-16 23:06:13 +00:00
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2024-03-16 23:06:13 +00:00
// ----------------------------------------------------------------------------------------------
2024-03-17 01:35:40 +00:00
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
2024-03-17 01:35:40 +00:00
// |
// 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
2024-03-17 01:35:40 +00:00
// |
// http://www.apache.org/licenses/LICENSE-2.0
2024-03-17 01:35:40 +00:00
// |
// 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.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
Closes #3061 (#3145) * Good start * Misc * Make ApiAuthenticationMiddleware use new json * Remove first newtonsoft dependency * Pull latest ASFB json enhancements * Start reimplementing newtonsoft! * One thing at a time * Keep doing all kind of breaking changes which need to be tested later * Add back ShouldSerialize() support * Misc * Eradicate remaining parts of newtonsoft * WIP * Workaround STJ stupidity in regards to derived types STJ can't serialize derived type properties by default, so we'll use another approach in our serializable file function * Make CI happy * Bunch of further fixes * Fix AddFreeLicense() after rewrite * Add full support for JsonDisallowNullAttribute * Optimize our json utilities even further * Misc * Add support for fields in disallow null * Misc optimization * Fix deserialization of GlobalCache in STD * Fix non-public [JsonExtensionData] * Fix IM missing method exception, correct db storage helpers * Fix saving into generic databases Thanks STJ * Make Save() function abstract to force inheritors to implement it properly * Correct ShouldSerializeAdditionalProperties to be a method * Misc cleanup * Code review * Allow JSON comments in configs, among other * Allow trailing commas in configs Users very often add them accidentally, no reason to throw on them * Fix confirmation ID Probably needs further fixes, will need to check later * Correct confirmations deserialization * Use JsonNumberHandling * Misc * Misc * [JsonDisallowNull] corrections * Forbid [JsonDisallowNull] on non-nullable structs * Not really but okay * Add and use ToJson() helpers * Misc * Misc
2024-02-21 02:09:36 +00:00
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using SteamKit2;
using SteamKit2.Internal;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal static class Commands {
private const byte MaxFinalizationAttempts = 900 / Steam.Security.MobileAuthenticator.CodeInterval;
internal static async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentException.ThrowIfNullOrEmpty(message);
if ((args == null) || (args.Length == 0)) {
throw new ArgumentNullException(nameof(args));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
switch (args.Length) {
case 1:
switch (args[0].ToUpperInvariant()) {
case "2FAFINALIZEDFORCE":
2023-04-20 19:31:30 +00:00
return await ResponseTwoFactorFinalized(access, bot).ConfigureAwait(false);
case "2FAINIT":
return await ResponseTwoFactorInit(access, bot).ConfigureAwait(false);
}
break;
default:
switch (args[0].ToUpperInvariant()) {
2023-02-09 23:39:28 +00:00
case "2FAFINALIZE" when args.Length > 2:
return await ResponseTwoFactorFinalize(access, args[1], Utilities.GetArgsAsText(message, 2), steamID).ConfigureAwait(false);
2023-02-09 23:39:28 +00:00
case "2FAFINALIZE":
return await ResponseTwoFactorFinalize(access, bot, args[1]).ConfigureAwait(false);
case "2FAFINALIZED" when args.Length > 2:
return await ResponseTwoFactorFinalized(access, args[1], Utilities.GetArgsAsText(message, 2), steamID).ConfigureAwait(false);
2023-04-20 19:31:30 +00:00
case "2FAFINALIZED":
return await ResponseTwoFactorFinalized(access, bot, args[1]).ConfigureAwait(false);
case "2FAFINALIZEDFORCE":
return await ResponseTwoFactorFinalized(access, Utilities.GetArgsAsText(args, 1, ","), steamID: steamID).ConfigureAwait(false);
2023-02-09 23:39:28 +00:00
case "2FAINIT":
return await ResponseTwoFactorInit(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false);
}
break;
}
return null;
}
2023-02-09 23:39:28 +00:00
private static async Task<string?> ResponseTwoFactorFinalize(EAccess access, Bot bot, string activationCode) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
ArgumentException.ThrowIfNullOrEmpty(activationCode);
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
}
if (bot.HasMobileAuthenticator) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator)));
}
if (!bot.IsConnectedAndLoggedOn) {
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
}
string maFilePath = bot.GetFilePath(Bot.EFileType.MobileAuthenticator);
string maFilePendingPath = $"{maFilePath}.PENDING";
if (!File.Exists(maFilePendingPath)) {
return bot.Commands.FormatBotResponse(Strings.NothingFound);
}
string json;
try {
json = await File.ReadAllTextAsync(maFilePendingPath).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
if (string.IsNullOrEmpty(json)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
Closes #3061 (#3145) * Good start * Misc * Make ApiAuthenticationMiddleware use new json * Remove first newtonsoft dependency * Pull latest ASFB json enhancements * Start reimplementing newtonsoft! * One thing at a time * Keep doing all kind of breaking changes which need to be tested later * Add back ShouldSerialize() support * Misc * Eradicate remaining parts of newtonsoft * WIP * Workaround STJ stupidity in regards to derived types STJ can't serialize derived type properties by default, so we'll use another approach in our serializable file function * Make CI happy * Bunch of further fixes * Fix AddFreeLicense() after rewrite * Add full support for JsonDisallowNullAttribute * Optimize our json utilities even further * Misc * Add support for fields in disallow null * Misc optimization * Fix deserialization of GlobalCache in STD * Fix non-public [JsonExtensionData] * Fix IM missing method exception, correct db storage helpers * Fix saving into generic databases Thanks STJ * Make Save() function abstract to force inheritors to implement it properly * Correct ShouldSerializeAdditionalProperties to be a method * Misc cleanup * Code review * Allow JSON comments in configs, among other * Allow trailing commas in configs Users very often add them accidentally, no reason to throw on them * Fix confirmation ID Probably needs further fixes, will need to check later * Correct confirmations deserialization * Use JsonNumberHandling * Misc * Misc * [JsonDisallowNull] corrections * Forbid [JsonDisallowNull] on non-nullable structs * Not really but okay * Add and use ToJson() helpers * Misc * Misc
2024-02-21 02:09:36 +00:00
Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject<Steam.Security.MobileAuthenticator>();
if (mobileAuthenticator == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
mobileAuthenticator.Init(bot);
MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler<MobileAuthenticatorHandler>();
if (mobileAuthenticatorHandler == null) {
throw new InvalidOperationException(nameof(mobileAuthenticatorHandler));
}
ulong steamTime = await mobileAuthenticator.GetSteamTime().ConfigureAwait(false);
bool successFinalizing = false;
for (byte i = 0; i < MaxFinalizationAttempts; i++) {
if (i > 0) {
steamTime += Steam.Security.MobileAuthenticator.CodeInterval;
}
string? code = mobileAuthenticator.GenerateTokenForTime(steamTime);
if (string.IsNullOrEmpty(code)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticator.GenerateTokenForTime)));
}
2023-11-14 19:01:29 +00:00
CTwoFactor_FinalizeAddAuthenticator_Response? response = await mobileAuthenticatorHandler.FinalizeAuthenticator(bot.SteamID, activationCode, code, steamTime).ConfigureAwait(false);
if (response == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticatorHandler.FinalizeAuthenticator)));
}
if (response.want_more) {
// OK, whatever
continue;
}
if (!response.success) {
EResult result = (EResult) response.status;
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
successFinalizing = true;
break;
}
if (!successFinalizing) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, MaxFinalizationAttempts));
}
if (!bot.TryImportAuthenticator(mobileAuthenticator)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.TryImportAuthenticator)));
}
string maFileFinishedPath = $"{maFilePath}.NEW";
try {
File.Move(maFilePendingPath, maFileFinishedPath, true);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
return bot.Commands.FormatBotResponse(Strings.Done);
}
2023-02-09 23:39:28 +00:00
private static async Task<string?> ResponseTwoFactorFinalize(EAccess access, string botNames, string activationCode, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentException.ThrowIfNullOrEmpty(botNames);
ArgumentException.ThrowIfNullOrEmpty(activationCode);
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
2023-02-09 23:39:28 +00:00
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalize(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
2023-12-11 22:55:13 +00:00
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
private static async Task<string?> ResponseTwoFactorFinalized(EAccess access, Bot bot, string? activationCode = null) {
2023-04-20 19:31:30 +00:00
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
}
if (bot.HasMobileAuthenticator) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator)));
}
string maFilePath = bot.GetFilePath(Bot.EFileType.MobileAuthenticator);
string maFilePendingPath = $"{maFilePath}.PENDING";
if (!File.Exists(maFilePendingPath)) {
return bot.Commands.FormatBotResponse(Strings.NothingFound);
}
string json;
try {
json = await File.ReadAllTextAsync(maFilePendingPath).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
if (string.IsNullOrEmpty(json)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
Closes #3061 (#3145) * Good start * Misc * Make ApiAuthenticationMiddleware use new json * Remove first newtonsoft dependency * Pull latest ASFB json enhancements * Start reimplementing newtonsoft! * One thing at a time * Keep doing all kind of breaking changes which need to be tested later * Add back ShouldSerialize() support * Misc * Eradicate remaining parts of newtonsoft * WIP * Workaround STJ stupidity in regards to derived types STJ can't serialize derived type properties by default, so we'll use another approach in our serializable file function * Make CI happy * Bunch of further fixes * Fix AddFreeLicense() after rewrite * Add full support for JsonDisallowNullAttribute * Optimize our json utilities even further * Misc * Add support for fields in disallow null * Misc optimization * Fix deserialization of GlobalCache in STD * Fix non-public [JsonExtensionData] * Fix IM missing method exception, correct db storage helpers * Fix saving into generic databases Thanks STJ * Make Save() function abstract to force inheritors to implement it properly * Correct ShouldSerializeAdditionalProperties to be a method * Misc cleanup * Code review * Allow JSON comments in configs, among other * Allow trailing commas in configs Users very often add them accidentally, no reason to throw on them * Fix confirmation ID Probably needs further fixes, will need to check later * Correct confirmations deserialization * Use JsonNumberHandling * Misc * Misc * [JsonDisallowNull] corrections * Forbid [JsonDisallowNull] on non-nullable structs * Not really but okay * Add and use ToJson() helpers * Misc * Misc
2024-02-21 02:09:36 +00:00
Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject<Steam.Security.MobileAuthenticator>();
2023-04-20 19:31:30 +00:00
if (mobileAuthenticator == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
mobileAuthenticator.Init(bot);
if (!string.IsNullOrEmpty(activationCode)) {
string? generatedCode = await mobileAuthenticator.GenerateToken().ConfigureAwait(false);
if (generatedCode != activationCode) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{generatedCode} != {activationCode}"));
}
}
2023-04-20 19:31:30 +00:00
if (!bot.TryImportAuthenticator(mobileAuthenticator)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.TryImportAuthenticator)));
}
string maFileFinishedPath = $"{maFilePath}.NEW";
try {
File.Move(maFilePendingPath, maFileFinishedPath, true);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
return bot.Commands.FormatBotResponse(Strings.Done);
}
private static async Task<string?> ResponseTwoFactorFinalized(EAccess access, string botNames, string? activationCode = null, ulong steamID = 0) {
2023-04-20 19:31:30 +00:00
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentException.ThrowIfNullOrEmpty(botNames);
2023-04-20 19:31:30 +00:00
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalized(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
2023-04-20 19:31:30 +00:00
2023-12-11 22:55:13 +00:00
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
2023-04-20 19:31:30 +00:00
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
private static async Task<string?> ResponseTwoFactorInit(EAccess access, Bot bot) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
}
if (bot.HasMobileAuthenticator) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator)));
}
if (!bot.IsConnectedAndLoggedOn) {
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
}
MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler<MobileAuthenticatorHandler>();
if (mobileAuthenticatorHandler == null) {
throw new InvalidOperationException(nameof(mobileAuthenticatorHandler));
}
string deviceID = $"android:{Guid.NewGuid()}";
CTwoFactor_AddAuthenticator_Response? response = await mobileAuthenticatorHandler.AddAuthenticator(bot.SteamID, deviceID).ConfigureAwait(false);
if (response == null) {
return bot.Commands.FormatBotResponse(Strings.WarningFailed);
}
EResult result = (EResult) response.status;
if (result != EResult.OK) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
2023-12-02 16:00:50 +00:00
MaFileData maFileData = new(response, bot.SteamID, deviceID);
string maFilePendingPath = $"{bot.GetFilePath(Bot.EFileType.MobileAuthenticator)}.PENDING";
Closes #3061 (#3145) * Good start * Misc * Make ApiAuthenticationMiddleware use new json * Remove first newtonsoft dependency * Pull latest ASFB json enhancements * Start reimplementing newtonsoft! * One thing at a time * Keep doing all kind of breaking changes which need to be tested later * Add back ShouldSerialize() support * Misc * Eradicate remaining parts of newtonsoft * WIP * Workaround STJ stupidity in regards to derived types STJ can't serialize derived type properties by default, so we'll use another approach in our serializable file function * Make CI happy * Bunch of further fixes * Fix AddFreeLicense() after rewrite * Add full support for JsonDisallowNullAttribute * Optimize our json utilities even further * Misc * Add support for fields in disallow null * Misc optimization * Fix deserialization of GlobalCache in STD * Fix non-public [JsonExtensionData] * Fix IM missing method exception, correct db storage helpers * Fix saving into generic databases Thanks STJ * Make Save() function abstract to force inheritors to implement it properly * Correct ShouldSerializeAdditionalProperties to be a method * Misc cleanup * Code review * Allow JSON comments in configs, among other * Allow trailing commas in configs Users very often add them accidentally, no reason to throw on them * Fix confirmation ID Probably needs further fixes, will need to check later * Correct confirmations deserialization * Use JsonNumberHandling * Misc * Misc * [JsonDisallowNull] corrections * Forbid [JsonDisallowNull] on non-nullable structs * Not really but okay * Add and use ToJson() helpers * Misc * Misc
2024-02-21 02:09:36 +00:00
string json = maFileData.ToJsonText(true);
try {
await File.WriteAllTextAsync(maFilePendingPath, json).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
return bot.Commands.FormatBotResponse(Strings.Done);
}
private static async Task<string?> ResponseTwoFactorInit(EAccess access, string botNames, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentException.ThrowIfNullOrEmpty(botNames);
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorInit(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot))).ConfigureAwait(false);
2023-12-11 22:55:13 +00:00
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
}