diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj b/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj new file mode 100644 index 0000000..fa02bbb --- /dev/null +++ b/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/NebulaAuth.LegacyConverter/Program.cs b/NebulaAuth.LegacyConverter/Program.cs new file mode 100644 index 0000000..a1076ad --- /dev/null +++ b/NebulaAuth.LegacyConverter/Program.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using SteamLib.Utility.MaFiles; + +var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); +if (currentPath != null) + Environment.CurrentDirectory = currentPath; + + +Console.WriteLine(currentPath); +foreach (var path in args) +{ + + if (Path.Exists(path) == false) + { + Console.WriteLine($"NOT VALID PATH: '{path}'"); + continue; + } + Console.WriteLine("Reading: " + path); + try + { + var text = File.ReadAllText(path); + var maf = MafileSerializer.Deserialize(text, out _); + var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented); + var name = Path.GetFileNameWithoutExtension(path); + Write(legacy, name); + + Console.WriteLine("DONE: " + name); + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: {ex.Message}"); + Console.WriteLine(ex); + } + finally + { + Console.WriteLine("-----------------------------------------"); + } + + +} + +Console.WriteLine("Press any key to exit..."); +Console.ReadKey(); + + +void Write(string maf, string name) +{ + + var path = Path.Combine(name + "_legacy.mafile"); + File.WriteAllText(path, maf); +} \ No newline at end of file diff --git a/NebulaAuth.sln b/NebulaAuth.sln new file mode 100644 index 0000000..9f72133 --- /dev/null +++ b/NebulaAuth.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NebulaAuth", "NebulaAuth\NebulaAuth.csproj", "{0FD01700-6D5C-451B-93BA-0860647E8F13}" + ProjectSection(ProjectDependencies) = postProject + {09F02072-F91D-4DAA-87BC-A34D3E150570} = {09F02072-F91D-4DAA-87BC-A34D3E150570} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NebulaAuth.LegacyConverter", "NebulaAuth.LegacyConverter\NebulaAuth.LegacyConverter.csproj", "{2D78A7D9-986A-4890-8A91-7ABD57A91830}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamLibForked", "SteamLibForked\SteamLibForked.csproj", "{09F02072-F91D-4DAA-87BC-A34D3E150570}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Release|Any CPU.Build.0 = Release|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Release|Any CPU.Build.0 = Release|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FA34DDD5-BDC0-4C0A-B347-4ECDBF446900} + EndGlobalSection +EndGlobal diff --git a/NebulaAuth.sln.DotSettings b/NebulaAuth.sln.DotSettings new file mode 100644 index 0000000..5b88222 --- /dev/null +++ b/NebulaAuth.sln.DotSettings @@ -0,0 +1,2 @@ + + DO_NOT_SHOW \ No newline at end of file diff --git a/NebulaAuth/App.xaml b/NebulaAuth/App.xaml new file mode 100644 index 0000000..8f47884 --- /dev/null +++ b/NebulaAuth/App.xaml @@ -0,0 +1,50 @@ + + + + #1E2025 + pack://application:,,,/Fonts/Roboto/#Roboto + pack://application:,,,/Fonts/Roboto/#Roboto Symbols + + + + + + + + + + + + True + False + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/App.xaml.cs b/NebulaAuth/App.xaml.cs new file mode 100644 index 0000000..bf9d06f --- /dev/null +++ b/NebulaAuth/App.xaml.cs @@ -0,0 +1,35 @@ +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Exceptions; +using System; +using System.Windows; +using CodingSeb.Localization; + +namespace NebulaAuth; + + +public partial class App : Application +{ + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + LocManager.Init(); + LocManager.SetApplicationLocalization(Settings.Instance.Language); + try + { + Shell.Initialize(); + } + catch (Exception ex) + { + var msg = ex.Message; + if (ex is CantAlignTimeException) + { + msg = Loc.Tr(LocManager.GetCodeBehind("CantAlignTimeError")); + } + + MessageBox.Show(msg); + throw; + } + } +} diff --git a/NebulaAuth/AssemblyInfo.cs b/NebulaAuth/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/NebulaAuth/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs b/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs new file mode 100644 index 0000000..d886d19 --- /dev/null +++ b/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs @@ -0,0 +1,20 @@ +using NebulaAuth.Model; +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace NebulaAuth.Converters.Background; + +public class BackgroundImageVisibleConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is not BackgroundMode.Color ? Visibility.Visible : Visibility.Hidden; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs b/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs new file mode 100644 index 0000000..b66349a --- /dev/null +++ b/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using System.IO; +using System.Windows.Data; +using System.Windows.Media.Imaging; +using NebulaAuth.Model; + +namespace NebulaAuth.Converters.Background; + +public class BackgroundSourceConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is BackgroundMode.Custom) + { + if(File.Exists("Background.png")) + return new BitmapImage(new Uri(Path.GetFullPath("Background.png"))); + } + + + return new BitmapImage(new Uri("pack://application:,,,/Theme/Background.jpg")); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/CoefficientConverter.cs b/NebulaAuth/Converters/CoefficientConverter.cs new file mode 100644 index 0000000..e79ddc8 --- /dev/null +++ b/NebulaAuth/Converters/CoefficientConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +[ValueConversion(typeof(double), typeof(double))] +public class CoefficientConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return System.Convert.ToDouble(value) * System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return (double)value / (double)parameter; + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ColorToBrushConverter.cs b/NebulaAuth/Converters/ColorToBrushConverter.cs new file mode 100644 index 0000000..c84794b --- /dev/null +++ b/NebulaAuth/Converters/ColorToBrushConverter.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; +using System; + +namespace NebulaAuth.Converters; + +[ValueConversion(typeof(Color), typeof(Brush))] +public class ColorToBrushConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Color color) + { + SolidColorBrush rv = new(color); + rv.Freeze(); + return rv; + } + return Binding.DoNothing; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is SolidColorBrush brush) + { + return brush.Color; + } + return default(Color); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/MultiCommandParamaterConverter.cs b/NebulaAuth/Converters/MultiCommandParamaterConverter.cs new file mode 100644 index 0000000..d228e06 --- /dev/null +++ b/NebulaAuth/Converters/MultiCommandParamaterConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class MultiCommandParameterConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + return values.Clone(); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/OnOffBoolTextConverter.cs b/NebulaAuth/Converters/OnOffBoolTextConverter.cs new file mode 100644 index 0000000..73e4b47 --- /dev/null +++ b/NebulaAuth/Converters/OnOffBoolTextConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class OnOffBoolTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not bool b) + { + throw new InvalidCastException($"Can't cast value {value} to 'bool'"); + } + + return b ? "вкл" : "выкл"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ProxyTextConverter.cs b/NebulaAuth/Converters/ProxyTextConverter.cs new file mode 100644 index 0000000..889a351 --- /dev/null +++ b/NebulaAuth/Converters/ProxyTextConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using NebulaAuth.Model.Entities; + +namespace NebulaAuth.Converters; + +public class ProxyTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not MaProxy p) + { + return string.Empty; + } + + return $"{p.Id}: {p.Data.Address}"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ReverseBooleanConverter.cs b/NebulaAuth/Converters/ReverseBooleanConverter.cs new file mode 100644 index 0000000..020a17b --- /dev/null +++ b/NebulaAuth/Converters/ReverseBooleanConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class ReverseBooleanConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool)value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool)value; + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/SelectedProxyTextConverter.cs b/NebulaAuth/Converters/SelectedProxyTextConverter.cs new file mode 100644 index 0000000..5885ce8 --- /dev/null +++ b/NebulaAuth/Converters/SelectedProxyTextConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using NebulaAuth.Model.Entities; + +namespace NebulaAuth.Converters; + +public class SelectedProxyTextConverter : IMultiValueConverter +{ + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is not MaProxy proxy) + { + return string.Empty; + } + + var proxyExist = (bool)values[1]; + return proxyExist ? $"{proxy.Id}: {proxy.Data.Address}" : ""; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ValueConverterGroup.cs b/NebulaAuth/Converters/ValueConverterGroup.cs new file mode 100644 index 0000000..4d7b5fd --- /dev/null +++ b/NebulaAuth/Converters/ValueConverterGroup.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class ValueConverterGroup : List, IValueConverter +{ + #region IValueConverter Members + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture)); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + + #endregion +} \ No newline at end of file diff --git a/NebulaAuth/Core/DialogsController.cs b/NebulaAuth/Core/DialogsController.cs new file mode 100644 index 0000000..349f7d8 --- /dev/null +++ b/NebulaAuth/Core/DialogsController.cs @@ -0,0 +1,76 @@ +using System.Threading.Tasks; +using MaterialDesignThemes.Wpf; +using NebulaAuth.Model.Entities; +using NebulaAuth.View; +using NebulaAuth.View.Dialogs; +using NebulaAuth.ViewModel.Other; + +namespace NebulaAuth.Core; + +public static class DialogsController +{ + + #region CommonDialogs + + public static async Task ShowConfirmCancelDialog(string? msg = null) + { + + var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg); + + var result = await DialogHost.Show(content); + return result != null && (bool) result; + } + + //public static async Task ShowTextFieldDialog(string? msg = null) + //{ + // var content = msg == null ? new TextFieldDialog() : new TextFieldDialog(msg); + // var result = await DialogHost.Show(content); + // return result as string; + //} + + #endregion + + public static async Task ShowLoginAgainDialog(string username) + { + var vm = new LoginAgainVM + { + UserName = username + }; + var content = new LoginAgainDialog() + { + DataContext = vm + }; + var result = await DialogHost.Show(content); + if (result is true) + { + return vm; + } + + return null; + } + + public static async Task ShowProxyManager(MaProxy? currentProxy) + { + var vm = new ProxyManagerVM(); + var view = new ProxyManagerView + { + DataContext = vm + }; + await DialogHost.Show(view); + } + public static void CloseDialog() + { + DialogHost.Close(null); + } + public static async Task ShowLinkerDialog() + { + var vm = new LinkAccountVM(); + var view = new LinkerView() + { + DataContext = vm + }; + await DialogHost.Show(view); + } + + +} \ No newline at end of file diff --git a/NebulaAuth/Core/LocalizationManager.cs b/NebulaAuth/Core/LocalizationManager.cs new file mode 100644 index 0000000..7b30221 --- /dev/null +++ b/NebulaAuth/Core/LocalizationManager.cs @@ -0,0 +1,105 @@ +using CodingSeb.Localization; +using CodingSeb.Localization.Loaders; +using System; +using System.IO; +using System.Reflection; +using System.Text; + +namespace NebulaAuth.Core; + +public static class LocManager +{ + public const string CODE_BEHIND_PATH_PART = "CodeBehind"; + public const string COMMON_PATH_PART = "Common"; + public static void SetApplicationLocalization(LocalizationLanguage language) + { + Loc.Instance.CurrentLanguage = GetLanguageCode(language); + //Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(GetLanguageCode(language)); + //Thread.CurrentThread.CurrentCulture= CultureInfo.GetCultureInfo(GetLanguageCode(language)); + } + + + public static string GetLanguageCode(LocalizationLanguage language) + { + return language switch + { + LocalizationLanguage.English => "en", + LocalizationLanguage.Russian => "ru", + LocalizationLanguage.Ukrainian => "ua", + _ => throw new ArgumentOutOfRangeException(nameof(language), language, null) + }; + } + + + + public static void Init() + { + Loc.LogOutMissingTranslations = true; + + LocalizationLoader.Instance.FileLanguageLoaders.Add(new JsonFileLoader()); + + ReloadFiles(); + } + + public static void ReloadFiles() + { + string exampleFileFileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "localization.loc.json"); + LocalizationLoader.Instance.ClearAllTranslations(); + LocalizationLoader.Instance.AddFile(exampleFileFileName); + } + + public static string? GetCodeBehind(params string[] path) + { + return GetConcat(CODE_BEHIND_PATH_PART, path); + } + + public static string GetCodeBehindOrDefault(string def, params string[] path) + { + return GetCodeBehind(path) ?? def; + } + + public static string? GetCommon(params string[] path) + { + return GetConcat(COMMON_PATH_PART, path); + } + + public static string GetCommonOrDefault(string def, params string[] path) + { + return GetCommon(path) ?? def; + } + + + public static string? Get(params string[] path) + { + var sb = new StringBuilder(); + foreach (var part in path) + { + sb.Append('.'); + sb.Append(part); + } + + sb.Remove(0, 1); + var fullPath = sb.ToString(); + var message = Loc.Tr(fullPath, "~|*ABSENT|~"); + return message == "~|*ABSENT|~" ? null : message; + } + + public static string GetOrDefault(string def, params string[] path) + { + return Get(path) ?? def; + } + private static string? GetConcat(string first, string[] path) + { + string[] newArray = new string[path.Length + 1]; + newArray[0] = first; + Array.Copy(path, 0, newArray, 1, path.Length); + return Get(newArray); + } +} + +public enum LocalizationLanguage +{ + English, + Russian, + Ukrainian +} \ No newline at end of file diff --git a/NebulaAuth/Core/MyUpdateManager.cs b/NebulaAuth/Core/MyUpdateManager.cs new file mode 100644 index 0000000..dbe48de --- /dev/null +++ b/NebulaAuth/Core/MyUpdateManager.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + + +namespace NebulaAuth.Core; + +public static class UpdateManager +{ + public static async Task CheckForUpdates() + { + + } +} \ No newline at end of file diff --git a/NebulaAuth/Core/SnackbarController.cs b/NebulaAuth/Core/SnackbarController.cs new file mode 100644 index 0000000..fc0accb --- /dev/null +++ b/NebulaAuth/Core/SnackbarController.cs @@ -0,0 +1,56 @@ +using System; +using MaterialDesignThemes.Wpf; + +namespace NebulaAuth.Core; + +public class SnackbarController +{ + + public static SnackbarMessageQueue MessageQueue { get; } = new() { DiscardDuplicates = true}; + private const int MIN_SNACKBAR_TIME = 1000; + public SnackbarController() + { + + } + /// + /// + /// + /// + /// Default duration is 1 second + public static void SendSnackbar(string text, TimeSpan? duration = null) + { + duration ??= GetSnackbarTime(text); + MessageQueue.Enqueue(text, null, null, null, false, false, duration.Value); + } + + /// + /// + /// + /// + /// Default duration is 1 second + public static void SendSnackbarWithButton(string text, string actionText = "OK", Action? action = null, TimeSpan? duration = null) + { + duration ??= GetSnackbarTime(text); + Action argAction; + if (action == null) + { + argAction = _ => { }; + } + else + { + argAction = _ => action(); + } + + MessageQueue.Enqueue(text, actionText, argAction, null, false, false, duration); + } + + private static TimeSpan GetSnackbarTime(string str) + { + if (str.Length <= 100) return TimeSpan.FromMilliseconds(MIN_SNACKBAR_TIME); + + var length = str.Length / 0.07; + return TimeSpan.FromMilliseconds(length); + + } + +} \ No newline at end of file diff --git a/NebulaAuth/Core/ThemeManager.cs b/NebulaAuth/Core/ThemeManager.cs new file mode 100644 index 0000000..ffbe285 --- /dev/null +++ b/NebulaAuth/Core/ThemeManager.cs @@ -0,0 +1,70 @@ +using NebulaAuth.Model; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Color = System.Drawing.Color; + +namespace NebulaAuth.Core; + +public static class ThemeManager +{ + public static System.Windows.Media.Color DefaultBackgroundColor = System.Windows.Media.Color.FromRgb(30, 32, 37); + private static readonly Window MainWindow = Application.Current.MainWindow!; + static ThemeManager() + { + Settings.Instance.PropertyChanged += SettingsOnPropertyChanged; + } + + private static void SettingsOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Settings.BackgroundColor)) + { + UpdateBackground(); + } + else if (e.PropertyName == nameof(Settings.IconColor)) + { + UpdateIcon(); + } + } + + private static void UpdateIcon() + { + var color = Settings.Instance.IconColor; + if (color == null) + { + MainWindow.TaskbarItemInfo = null; + } + else + { + var c = color.Value; + var diameter = 14; + var bitmap = new Bitmap(diameter, diameter); + var graphics = Graphics.FromImage(bitmap); + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + var brush = new SolidBrush(Color.FromArgb(c.A, c.R, c.G, c.B)); + graphics.FillEllipse(brush, 0, 0, diameter, diameter); + var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + + MainWindow.TaskbarItemInfo = new(); + MainWindow.TaskbarItemInfo.Overlay = bitmapSource; + } + } + + private static void UpdateBackground() + { + var color = Settings.Instance.BackgroundColor ?? DefaultBackgroundColor; + Application.Current.Resources["WindowBackground"] = new SolidColorBrush(color); + + } + + public static void InitializeTheme() + { + UpdateIcon(); + UpdateBackground(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Core/TrayManager.cs b/NebulaAuth/Core/TrayManager.cs new file mode 100644 index 0000000..0b2e580 --- /dev/null +++ b/NebulaAuth/Core/TrayManager.cs @@ -0,0 +1,77 @@ +using System; +using System.Drawing; +using System.IO; +using System.Windows; +using System.Windows.Forms; +using NebulaAuth.Model; +using Application = System.Windows.Application; + +namespace NebulaAuth.Core; + +public static class TrayManager +{ + private static NotifyIcon _notifyIcon; + private static readonly Window MainWindow = Application.Current.MainWindow!; + public static bool IsEnabled => Settings.Instance.HideToTray; + + + public static void InitializeTray() + { + _notifyIcon = new NotifyIcon(); + _notifyIcon.Text = "NebulaAuth"; + + Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/Theme/nebula lock.ico"))!.Stream; + _notifyIcon.Icon = new Icon(iconStream); + + _notifyIcon.MouseDoubleClick += NotifyIcon_MouseDoubleClick; + + var contextMenu = new ContextMenuStrip(); + + contextMenu.Items.Add("Выйти", null!, onClick: OnExitClick); + + _notifyIcon.ContextMenuStrip = contextMenu; + + MainWindow.StateChanged += MainWindow_StateChanged; + } + + private static void OnExitClick(object? sender, EventArgs e) + { + ExitApplication(); + } + + private static void NotifyIcon_MouseDoubleClick(object? sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + ShowMainWindow(); + } + } + + private static void MainWindow_StateChanged(object? sender, EventArgs e) + { + if (MainWindow.WindowState == WindowState.Minimized) + { + HideMainWindow(); + } + } + + private static void ShowMainWindow() + { + MainWindow.Show(); + MainWindow.WindowState = WindowState.Normal; + _notifyIcon.Visible = false; + } + + private static void HideMainWindow() + { + if(IsEnabled == false) return; + _notifyIcon.Visible = true; + MainWindow.Hide(); + } + + private static void ExitApplication() + { + _notifyIcon.Dispose(); + Application.Current.Shutdown(); + } +} \ No newline at end of file diff --git a/NebulaAuth/FodyWeavers.xml b/NebulaAuth/FodyWeavers.xml new file mode 100644 index 0000000..4025f81 --- /dev/null +++ b/NebulaAuth/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/NebulaAuth/MainWindow.xaml b/NebulaAuth/MainWindow.xaml new file mode 100644 index 0000000..44b26a8 --- /dev/null +++ b/NebulaAuth/MainWindow.xaml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + Отпустите, чтобы добавить mafile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs b/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs new file mode 100644 index 0000000..032e0c9 --- /dev/null +++ b/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs @@ -0,0 +1,36 @@ +using System.Windows; + +namespace NebulaAuth.Theme.WindowStyle +{ + /// + /// Логика взаимодействия для WindowStyle.xaml + /// + partial class WindowStyle + { + public WindowStyle() + { + InitializeComponent(); + } + + private void OnCloseClick(object sender, RoutedEventArgs e) + { + var window = (Window)((FrameworkElement)sender).TemplatedParent; + + window.Close(); + } + + private void OnMaximizeRestoreClick(object sender, RoutedEventArgs e) + { + var window = (Window)((FrameworkElement)sender).TemplatedParent; + + window.WindowState = window.WindowState == WindowState.Normal ? WindowState.Maximized : WindowState.Normal; + } + + private void OnMinimizeClick(object sender, RoutedEventArgs e) + { + var window = (Window)((FrameworkElement)sender).TemplatedParent; + + window.WindowState = WindowState.Minimized; + } + } +} diff --git a/NebulaAuth/Theme/nebula lock.ico b/NebulaAuth/Theme/nebula lock.ico new file mode 100644 index 0000000..c0d5d5c Binary files /dev/null and b/NebulaAuth/Theme/nebula lock.ico differ diff --git a/NebulaAuth/Theme/nebula.ico b/NebulaAuth/Theme/nebula.ico new file mode 100644 index 0000000..0fb6290 Binary files /dev/null and b/NebulaAuth/Theme/nebula.ico differ diff --git a/NebulaAuth/Utility/ErrorTranslatorHelper.cs b/NebulaAuth/Utility/ErrorTranslatorHelper.cs new file mode 100644 index 0000000..b024bc6 --- /dev/null +++ b/NebulaAuth/Utility/ErrorTranslatorHelper.cs @@ -0,0 +1,39 @@ +using CodingSeb.Localization; +using NebulaAuth.Core; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore.Enums; + +namespace NebulaAuth.Utility; + +public static class ErrorTranslatorHelper +{ + + public static string TranslateLoginError(LoginError error) + { + var result = GetMessage("Login", error.ToString()); + return result ?? error.ToString(); + } + + + public static string TranslateEResult(EResult eResult) + { + var result = GetMessage("EResult", eResult.ToString()); + return result ?? eResult.ToString(); + + } + + public static string TranslateLinkerError(AuthenticatorLinkerError error) + { + return GetMessage("AuthenticatorLinkerError", error.ToString()) ?? error.ToString(); + } + + public static string? GetMessage(string path, string name) + { + var fullPath = LocManager.CODE_BEHIND_PATH_PART + "." + "ErrorTranslator" + "." + path + "." + name; + var message = Loc.Tr(fullPath, "|ABSENT|"); + if (message == "|ABSENT|") return null; + return message; + + } +} \ No newline at end of file diff --git a/NebulaAuth/Utility/ExceptionHandler.cs b/NebulaAuth/Utility/ExceptionHandler.cs new file mode 100644 index 0000000..901d92d --- /dev/null +++ b/NebulaAuth/Utility/ExceptionHandler.cs @@ -0,0 +1,97 @@ +using NebulaAuth.Core; +using NebulaAuth.Model; +using SteamLib.Exceptions; +using SteamLib.Exceptions.General; +using SteamLib.ProtoCore.Exceptions; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace NebulaAuth.Utility; + +public static class ExceptionHandler +{ + private const string EXCEPTION_HANDLER_LOC_PATH = "ExceptionHandler"; + public static bool Handle(Exception ex, string? prefix = null, string? postfix = null, bool handleAllExceptions = false) + { + string msg; + Shell.Logger.Error(ex); + switch (ex) + { + case SessionExpiredException: + { + msg = "SessionExpiredException".GetCodeBehindLocalization(); + break; + } + case SessionInvalidException: + { + msg = "SessionExpiredException".GetCodeBehindLocalization(); + break; + } + case TaskCanceledException e: + { + msg = e.InnerException is TimeoutException + ? "TimeoutException".GetCodeBehindLocalization() + : "TaskCanceledException".GetCodeBehindLocalization(); + break; + } + case HttpRequestException e: + { + var str = "RequestError".GetCommonLocalization() + ": "; + if (e.StatusCode != null) + { + msg = str + e.StatusCode; + } + else if (e.InnerException != null) + { + msg = (str + e.InnerException.Message); + } + else + { + msg = (str + e.Message); + } + + break; + } + case UnsupportedResponseException: + { + msg = "UnsupportedResponseException".GetCodeBehindLocalization(); + break; + } + case CantLoadConfirmationsException e: + { + msg = e.Message; + break; + } + case EResultException e: + { + msg = "Error".GetCommonLocalization() + ": " + ErrorTranslatorHelper.TranslateEResult(e.Result); + break; + } + case LoginException e: + { + msg = "LoginException".GetCodeBehindLocalization() + ": " + ErrorTranslatorHelper.TranslateLoginError(e.Error); + break; + } + case not null when handleAllExceptions: + { + msg = "UnknownException".GetCodeBehindLocalization() + ex.Message; + break; + } + default: + return false; + } + + SnackbarController.SendSnackbar(prefix + msg + postfix); + return true; + } + private static string GetCommonLocalization(this string key) + { + return LocManager.GetCommonOrDefault(key, key); + } + + private static string GetCodeBehindLocalization(this string key) + { + return LocManager.GetCodeBehindOrDefault(key, EXCEPTION_HANDLER_LOC_PATH, key); + } +} \ No newline at end of file diff --git a/NebulaAuth/View/ConfirmationTemplates.xaml b/NebulaAuth/View/ConfirmationTemplates.xaml new file mode 100644 index 0000000..b4ec36f --- /dev/null +++ b/NebulaAuth/View/ConfirmationTemplates.xaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml new file mode 100644 index 0000000..85b364b --- /dev/null +++ b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs new file mode 100644 index 0000000..9a8999b --- /dev/null +++ b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs @@ -0,0 +1,21 @@ +using System.Windows.Controls; + +namespace NebulaAuth.View.Dialogs +{ + /// + /// Логика взаимодействия для ConfirmCancelDialog.xaml + /// + public partial class ConfirmCancelDialog : UserControl + { + public ConfirmCancelDialog() + { + InitializeComponent(); + } + + public ConfirmCancelDialog(string msg) + { + InitializeComponent(); + ConfirmTextBlock.Text = msg; + } + } +} diff --git a/NebulaAuth/View/Dialogs/LoginAgainDialog.xaml b/NebulaAuth/View/Dialogs/LoginAgainDialog.xaml new file mode 100644 index 0000000..464abce --- /dev/null +++ b/NebulaAuth/View/Dialogs/LoginAgainDialog.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs b/NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs new file mode 100644 index 0000000..24483c8 --- /dev/null +++ b/NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; + +namespace NebulaAuth.View.Dialogs +{ + /// + /// Логика взаимодействия для WaitLoginDialog.xaml + /// + public partial class WaitLoginDialog : ICaptchaResolver + { + private TaskCompletionSource _tcs = new(); + public WaitLoginDialog() + { + InitializeComponent(); + } + + public async Task Resolve(Uri imageUrl, HttpClient client) + { + + CaptchaGrid.Visibility = Visibility.Visible; + var stream = await client.GetStreamAsync(imageUrl); + return await Application.Current.Dispatcher.Invoke(async () => + { + var image = await LoadImage(stream); + CaptchaImage.Source = image; + try + { + return await _tcs.Task; + } + catch (TaskCanceledException) + { + throw new LoginException(LoginError.CaptchaRequired); + } + }); + } + + private async Task LoadImage(Stream stream) + { + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + ms.Position = 0; + + var image = new BitmapImage(); + + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.StreamSource = ms; + image.EndInit(); + await stream.DisposeAsync(); + return image; + } + + private void CancelButton_OnClick(object sender, RoutedEventArgs e) + { + _tcs.SetCanceled(); + } + + private void SendCaptchaBtn_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrWhiteSpace(CaptchaTB.Text)) return; + var oldTcs = _tcs; + _tcs = new TaskCompletionSource(); + oldTcs.SetResult(CaptchaTB.Text); + } + } +} diff --git a/NebulaAuth/View/LinkerView.xaml b/NebulaAuth/View/LinkerView.xaml new file mode 100644 index 0000000..c6954ba --- /dev/null +++ b/NebulaAuth/View/LinkerView.xaml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/ProxyManagerView.xaml.cs b/NebulaAuth/View/ProxyManagerView.xaml.cs new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/NebulaAuth/View/ProxyManagerView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace NebulaAuth.View +{ + /// + /// Логика взаимодействия для ProxyManagerView.xaml + /// + public partial class ProxyManagerView : UserControl + { + public ProxyManagerView() + { + InitializeComponent(); + } + } +} diff --git a/NebulaAuth/View/SettingsView.xaml b/NebulaAuth/View/SettingsView.xaml new file mode 100644 index 0000000..d5e032b --- /dev/null +++ b/NebulaAuth/View/SettingsView.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/SettingsView.xaml.cs b/NebulaAuth/View/SettingsView.xaml.cs new file mode 100644 index 0000000..6e12243 --- /dev/null +++ b/NebulaAuth/View/SettingsView.xaml.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace NebulaAuth.View +{ + /// + /// Логика взаимодействия для SettingsView.xaml + /// + public partial class SettingsView : UserControl + { + public SettingsView() + { + InitializeComponent(); + } + + private void ColorPicker_OnColorChanged(object sender, RoutedPropertyChangedEventArgs e) + { + Debug.WriteLine(e.NewValue); + } + } +} diff --git a/NebulaAuth/ViewModel/MainVM.cs b/NebulaAuth/ViewModel/MainVM.cs new file mode 100644 index 0000000..0aea031 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM.cs @@ -0,0 +1,191 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MaterialDesignThemes.Wpf; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using SteamLib.Exceptions; +using SteamLib.SteamMobile; +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NebulaAuth.View.Dialogs; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM : ObservableObject +{ + [ObservableProperty] + private ObservableCollection _maFiles = Storage.MaFiles; + public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue; + + public Mafile? SelectedMafile + { + get => _selectedMafile; + set => SetMafile(value); + } + private Mafile? _selectedMafile; + + + public MainVM() + { + CreateCodeTimer(); + _confirmTimer = new Timer(ConfirmByTimer, null, TimeSpan.FromSeconds(_timerCheckSeconds), TimeSpan.FromSeconds(_timerCheckSeconds)); + Proxies = new ObservableCollection(ProxyStorage.Proxies.Select(kvp => + new MaProxy(kvp.Key, kvp.Value))); + Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged; + QueryGroups(); + SessionHandler.LoginStarted += SessionHandlerOnLoginStarted; + SessionHandler.LoginCompleted += SessionHandlerOnLoginCompleted; + } + + + private void SetMafile(Mafile? mafile) + { + if (mafile != SelectedMafile) + { + _selectedMafile = mafile; + OnPropertyChanged(nameof(SelectedMafile)); + MaClient.SetAccount(mafile); + OnPropertyChanged(nameof(ConfirmationsVisible)); + if (Settings.DisableTimersOnChange) OffTimer(dispatcher: false); + SetCurrentProxy(); + OnPropertyChanged(nameof(IsDefaultProxy)); + if (mafile != null) Code = SteamGuardCodeGenerator.GenerateCode(mafile.SharedSecret); + } + } + + + [RelayCommand] + public async Task LoginAgain() + { + if (SelectedMafile == null) + { + return; + } + var loginAgainVm = await DialogsController.ShowLoginAgainDialog(SelectedMafile.AccountName); + if (loginAgainVm == null) + { + return; + } + var password = loginAgainVm.Password; + var waitDialog = new WaitLoginDialog(); + var wait = DialogHost.Show(waitDialog); + try + { + await MaClient.LoginAgain(SelectedMafile, password, loginAgainVm.SavePassword, waitDialog); + SnackbarController.SendSnackbar(GetLocalizationOrDefault("SuccessfulLogin")); + } + catch (LoginException ex) + { + SnackbarController.SendSnackbar(ErrorTranslatorHelper.TranslateLoginError(ex.Error), TimeSpan.FromSeconds(1.5)); + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex)) + { + Shell.Logger.Error(ex); + } + finally + { + DialogsController.CloseDialog(); + await wait; + } + } + private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + SearchText = string.Empty; + } + + + [RelayCommand] + private async Task RefreshSession() + { + if (SelectedMafile == null) return; + try + { + await MaClient.RefreshSession(SelectedMafile); + SnackbarController.SendSnackbar(GetLocalizationOrDefault("SessionRefreshed")); + } + catch (Exception ex) when (ExceptionHandler.Handle(ex)) + { + Shell.Logger.Error(ex); + } + } + + [RelayCommand] + public async Task LinkAccount() + { + OffTimer(false); + await DialogsController.ShowLinkerDialog(); + } + + [RelayCommand] + private async Task RemoveAuthenticator() + { + var selectedMafile = SelectedMafile; + if (selectedMafile == null) return; + if (string.IsNullOrWhiteSpace(selectedMafile.RevocationCode)) + { + SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error") + ": " + GetLocalizationOrDefault("MissingRCode")); + return; + } + try + { + if (await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("ConfirmRemovingAuthenticator"))) + { + var result = await SessionHandler.Handle(() => MaClient.RemoveAuthenticator(SelectedMafile), SelectedMafile); + SnackbarController.SendSnackbar( + result.Success ? GetLocalizationOrDefault("AuthenticatorRemoved") : GetLocalizationOrDefault("AuthenticatorNotRemoved")); + + if (result.Success) + { + Storage.MoveToRemoved(selectedMafile); + MaFiles.Remove(selectedMafile); + } + } + else + { + SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Canceled", "Canceled")); + } + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex, handleAllExceptions: true)) + { + Shell.Logger.Error(ex); + } + } + + [RelayCommand] + private async Task ConfirmLogin() + { + if (SelectedMafile == null) return; + + try + { + LoginConfirmationResult res = await SessionHandler.Handle(() => MaClient.ConfirmLoginRequest(SelectedMafile), SelectedMafile); + if (res.Success) + { + SnackbarController.SendSnackbar($"{GetLocalizationOrDefault("ConfirmLoginSuccess")} {res.IP} ({res.Country})"); + } + else + { + string msg = res.Error switch + { + LoginConfirmationError.NoRequests => GetLocalizationOrDefault("ConfirmLoginFailedNoRequests"), + LoginConfirmationError.MoreThanOneRequest => GetLocalizationOrDefault("ConfirmLoginFailedMoreThanOneRequest"), //TODO + _ => throw new ArgumentOutOfRangeException() + }; + SnackbarController.SendSnackbar(msg); + } + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex, handleAllExceptions: true)) + { + Shell.Logger.Error(ex); + } + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Code.cs b/NebulaAuth/ViewModel/MainVM_Code.cs new file mode 100644 index 0000000..3496e9d --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Code.cs @@ -0,0 +1,43 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using SteamLib.SteamMobile; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Windows; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM +{ + private Timer _codeTimer; + [ObservableProperty] private double _codeProgress; + [ObservableProperty] private string _code; + + + [MemberNotNull(nameof(_codeTimer))] + [MemberNotNull(nameof(_code))] + private void CreateCodeTimer() + { + var currentTime = TimeAligner.GetSteamTime(); + _codeTimer = new Timer(UpdateCode, null, 0, 1000); + } + + private void UpdateCode(object? state = null) + { + var currentTime = TimeAligner.GetSteamTime(); + var untilChange = currentTime - currentTime / 30L * 30L; + var codeProgress = untilChange / 30D * 100; + + string? code = null; + if (untilChange == 0 && SelectedMafile != null) + { + code = SteamGuardCodeGenerator.GenerateCode(SelectedMafile!.SharedSecret); + } + + if(Application.Current == null) return; + Application.Current.Dispatcher.BeginInvoke(() => + { + CodeProgress = codeProgress; + if (code != null) Code = code; + }); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Confirmations.cs b/NebulaAuth/ViewModel/MainVM_Confirmations.cs new file mode 100644 index 0000000..041cd0e --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Confirmations.cs @@ -0,0 +1,105 @@ +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using SteamLib.SteamMobile.Confirmations; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Confirmations +{ + public ObservableCollection Confirmations { get; } = new(); + private Mafile? _confirmationsLoadedForMafile; + public bool ConfirmationsVisible => SelectedMafile == _confirmationsLoadedForMafile; + + [RelayCommand] + public async Task GetConfirmations() + { + if (SelectedMafile == null) return; + var maf = SelectedMafile; + List conf; + try + { + var confI = await SessionHandler.Handle(() => MaClient.GetConfirmations(SelectedMafile), SelectedMafile); + conf = confI.ToList(); + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex)) + { + return; + } + + + _confirmationsLoadedForMafile = maf; + OnPropertyChanged(nameof(ConfirmationsVisible)); + Confirmations.Clear(); + var marketConfirmations = conf.Where(c => c.ConfType == ConfirmationType.MarketSellTransaction).ToList(); + + if (marketConfirmations.Count > 1) + { + var indexOfLast = conf.IndexOf(marketConfirmations.First()); + foreach (var mCon in marketConfirmations) + { + conf.Remove(mCon); + } + + var mConf = new MarketMultiConfirmation(marketConfirmations.Cast()); + conf.Insert(indexOfLast, mConf); + } + + foreach (var con in conf) + { + Confirmations.Add(con); + } + } + + private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm) + { + bool result; + try + { + if (confirmation is MarketMultiConfirmation multi) + { + result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm); + } + else + { + result = await MaClient.SendConfirmation(mafile, confirmation, confirm); + } + } + catch (Exception ex) + when(ExceptionHandler.Handle(ex)) + { + return; + } + + if (result) + { + Confirmations.Remove(confirmation); + } + else + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("ConfirmationError")); + } + } + + [RelayCommand] + private Task Confirm(Confirmation? confirmation) + { + if (SelectedMafile == null || confirmation == null) return Task.CompletedTask; + return SendConfirmation(SelectedMafile, confirmation, true); + } + [RelayCommand] + private Task Cancel(Confirmation? confirmation) + { + if (SelectedMafile == null || confirmation == null) return Task.CompletedTask; + return SendConfirmation(SelectedMafile, confirmation, false); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_File.cs b/NebulaAuth/ViewModel/MainVM_File.cs new file mode 100644 index 0000000..d115f86 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_File.cs @@ -0,0 +1,207 @@ +using CommunityToolkit.Mvvm.Input; +using MaterialDesignThemes.Wpf; +using Microsoft.Win32; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.View; +using NebulaAuth.ViewModel.Other; +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using AchiesUtilities.Extensions; +using NebulaAuth.Model.Entities; +using SteamLib.Exceptions; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //File //TODO: Refactor +{ + + public Settings Settings => Settings.Instance; + + [RelayCommand] + private void OpenMafileFolder() + { var mafile = SelectedMafile; + + var path = Storage.MafileFolder; + if (mafile?.SessionData != null) + { + var mafPath = Path.Combine(Storage.MafileFolder, mafile.SessionData.SteamId.Steam64 + ".maFile"); + if (File.Exists(mafPath)) + { + path = $"/e, /select, \"{mafPath}\""; ; + } + } + + Process.Start("explorer", path); + } + + + [RelayCommand] + private Task AddMafile() + { + var openFileDialog = new OpenFileDialog + { + Filter = "Mafile|*.mafile;*.maFile", + Multiselect = false, + + }; + var fs = openFileDialog.ShowDialog(); + if (fs != true) return Task.CompletedTask; + var path = openFileDialog.FileName; + return AddMafile(new[] { path }); + + } + public async Task AddMafile(string[] path) + { + bool? confirmOverwrite = null; + var added = 0; + var notAdded = 0; + var errors = 0; + foreach (var str in path) + { + try + { + Storage.AddNewMafile(str, confirmOverwrite ?? false); + added++; + } + catch (FormatException) + { + errors++; + } + catch (IOException) + { + confirmOverwrite ??= await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("ConfirmMafileOverwrite")); + + if (confirmOverwrite == true) + { + Storage.AddNewMafile(str, true); + added++; + } + else if (confirmOverwrite == false) + { + notAdded++; + } + } + catch (SessionInvalidException ex) + { + if (path.Length == 1 && ex.Data.Contains("mafile")) + { + var mafile = (Mafile)ex.Data["mafile"]!; + if (await HandleAddMafileWithoutSession(mafile)) + { + added++; + } + else + { + errors++; + } + } + else + { + SnackbarController.SendSnackbar($"{GetLocalizationOrDefault("MafileImportError")} {Path.GetFileName(str)}{GetLocalizationOrDefault("MissingSessionInMafile")}", TimeSpan.FromSeconds(4)); + } + } + } + + var msg = GetLocalizationOrDefault("Import"); + if (added > 0) + { + msg += $" {GetLocalizationOrDefault("ImportAdded")} {added}."; + } + if (notAdded > 0) + { + msg += $" {GetLocalizationOrDefault("ImportSkipped")} {notAdded}."; + } + + if (errors > 0) + { + msg += $" {GetLocalizationOrDefault("ImportErrors")} {errors}."; + } + SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2)); + } + + private async Task HandleAddMafileWithoutSession(Mafile data) + { + var oldMafile = SelectedMafile; + SelectedMafile = data; + await LoginAgain(); + var result = data.SessionData != null; + if (result) + { + var existed = MaFiles.FirstOrDefault(m => m.AccountName == data.AccountName); //TODO: more elegant way to handle overwrite + if (existed != null) + { + MaFiles.Remove(existed); + } + MaFiles.Add(data); + SelectedMafile = data; + } + else + { + SelectedMafile = oldMafile; + } + return result; + + } + + [RelayCommand] + private async Task RemoveMafile() + { + if (SelectedMafile == null) return; + var confirm = + await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("RemoveMafileConfirmation")); + + if (!confirm) return; + try + { + Storage.MoveToRemoved(SelectedMafile); + } + catch (UnauthorizedAccessException) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("CantRemoveAlreadyExist")); + } + catch (Exception ex) + { + SnackbarController.SendSnackbar( + $"{GetLocalizationOrDefault("CantRemoveMafile")} {ex.Message}"); + } + } + + [RelayCommand] + private async Task OpenSettingsDialog() + { + var vm = new SettingsVM(); + var view = new SettingsView() + { + DataContext = vm + }; + await DialogHost.Show(view); + } + + [RelayCommand] + private async Task CopyMafileFromBuffer() + { + StringCollection files; + try + { + files = Clipboard.GetFileDropList(); + } + catch (Exception ex) + { + Shell.Logger.Error(ex); + return; + } + + var arr = files.Cast().ToArray(); + if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return; + + + + await AddMafile(arr); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Groups.cs b/NebulaAuth/ViewModel/MainVM_Groups.cs new file mode 100644 index 0000000..5bf60b3 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Groups.cs @@ -0,0 +1,139 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Groups +{ + [ObservableProperty] + private ObservableCollection _groups = new(); + + + public string? SelectedGroup + { + get => _selectedGroup; + set + { + if (SetProperty(ref _selectedGroup, value)) + PerformSearch(); + } + } + + private string? _selectedGroup; + + public string SearchText + { + get => _searchText; + set + { + if(SetProperty(ref _searchText, value)) + PerformSearch(); + } + + } + private string _searchText = string.Empty; + + + + + [RelayCommand] + private void AddGroup(string? value) + { + var mafile = SelectedMafile; + if (mafile == null) return; + if (string.IsNullOrEmpty(value)) return; + mafile.Group = value; + Storage.UpdateMafile(mafile); + QueryGroups(); + SelectedGroup = value; + OnPropertyChanged(nameof(SelectedMafile)); //For bindings + + } + + [RelayCommand] + private void AddToGroup(object[]? value) + { + if (value == null) return; + if (value.Length < 2) return; + var group = (string?)value[0]; + var mafile = (Mafile?)value[1]; + + if (group == null || mafile == null) return; + mafile.Group = group; + Storage.UpdateMafile(mafile); + QueryGroups(); + PerformSearch(); + OnPropertyChanged(nameof(SelectedMafile)); + } + + [RelayCommand] + private void RemoveGroup(Mafile? mafile) + { + if (mafile?.Group == null) return; + var mafGroup = mafile.Group; + mafile.Group = null; + Storage.UpdateMafile(mafile); + OnPropertyChanged(nameof(SelectedMafile)); //For bindings + QueryGroups(); + if (Groups.All(g => g.Equals(mafGroup) == false)) + { + SelectedGroup = null; + } + PerformSearch(); + } + + + private void QueryGroups() + { + var groups = Storage.MaFiles + .Where(m => string.IsNullOrWhiteSpace(m.Group) == false) + .Select(m => m.Group) + .Distinct() + .Order() + .ToList(); + + Groups = new ObservableCollection(groups!); + } + private void PerformSearch() + { + if (string.IsNullOrWhiteSpace(SelectedGroup) && string.IsNullOrWhiteSpace(SearchText)) + { + MaFiles = Storage.MaFiles; + return; + } + long? searchSteamId = null; + if (long.TryParse(SearchText, out var steamId)) + { + searchSteamId = steamId; + } + var query = Storage.MaFiles.AsEnumerable(); + if (!string.IsNullOrWhiteSpace(SearchText)) + { + query = query.Where(SearchPredicate); + } + if (!string.IsNullOrWhiteSpace(SelectedGroup)) + { + query = query.Where(m => m.Group != null && m.Group.Equals(SelectedGroup)); + } + var perform = query.ToList(); + MaFiles = new ObservableCollection(perform); + if (SelectedMafile != null && !MaFiles.Contains(SelectedMafile)) + { + SelectedMafile = MaFiles.FirstOrDefault(); + } + bool SearchPredicate(Mafile mafile) + { + if (!mafile.AccountName.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase)) + { + return mafile.SessionData != null && mafile.SessionData.SteamId.Steam64.Id.Equals(searchSteamId); + } + return true; + } + } + +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Localization.cs b/NebulaAuth/ViewModel/MainVM_Localization.cs new file mode 100644 index 0000000..b89b8c9 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Localization.cs @@ -0,0 +1,17 @@ +using NebulaAuth.Core; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM +{ + private const string LOC_PATH = "MainVM"; + private static string? GetLocalization(string key) + { + return LocManager.GetCodeBehind(LOC_PATH, key); + } + + private static string GetLocalizationOrDefault(string key) + { + return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Other.cs b/NebulaAuth/ViewModel/MainVM_Other.cs new file mode 100644 index 0000000..af830ba --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Other.cs @@ -0,0 +1,37 @@ +using MaterialDesignThemes.Wpf; +using System; +using System.Windows; +using NebulaAuth.View.Dialogs; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Other +{ + private void SessionHandlerOnLoginCompleted(object? sender, EventArgs e) + { + var currentSession = DialogHost.GetDialogSession(null); + Application.Current.Dispatcher.BeginInvoke(() => + { + if (currentSession is { Content: WaitLoginDialog, IsEnded: false }) + { + try + { + currentSession.Close(); + } + catch + { + //Ignored + } + } + }); + } + + private async void SessionHandlerOnLoginStarted(object? sender, EventArgs e) + { + if (DialogHost.IsDialogOpen(null)) return; + await Application.Current.Dispatcher.BeginInvoke(async () => + { + await DialogHost.Show(new WaitLoginDialog()); + }); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Proxy.cs b/NebulaAuth/ViewModel/MainVM_Proxy.cs new file mode 100644 index 0000000..8bd7936 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Proxy.cs @@ -0,0 +1,121 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using NebulaAuth.Model.Comparers; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM +{ + public ObservableCollection Proxies { get; } + + public MaProxy? SelectedProxy + { + get => _selectedProxy; + set + { + if (SetProperty(ref _selectedProxy, value)) + { + OnPropertyChanged(nameof(IsDefaultProxy)); + OnProxyChanged(); + }; + } + + } + private MaProxy? _selectedProxy; + + [ObservableProperty] private bool _proxyExist = true; + public bool IsDefaultProxy => SelectedProxy == null && MaClient.DefaultProxy != null; + private bool _handleProxyChange; + + private void SetCurrentProxy() + { + if (ReferenceEquals(_selectedProxy, SelectedMafile?.Proxy) == false && _selectedProxy?.Equals(SelectedMafile?.Proxy) == false) + _handleProxyChange = true; + + if (SelectedMafile == null) + { + SelectedProxy = null; + return; + } + if (SelectedMafile.Proxy == null) + { + SelectedProxy = SelectedMafile.Proxy; + } + else + { + var existed = Proxies.FirstOrDefault(p => p.Id == SelectedMafile.Proxy.Id); + + + if (existed == null || ProxyDataComparer.Equal(existed.Data, SelectedMafile.Proxy.Data) == false) + { + SelectedProxy = SelectedMafile.Proxy; + } + else + { + SelectedProxy = existed; + } + } + + CheckProxyExist(); + + } + + private void CheckProxyExist() + { + if (SelectedProxy == null) + { + ProxyExist = true; + return; + } + + var selectedId = SelectedProxy.Id; + ProxyExist = ProxyStorage.Proxies.TryGetValue(selectedId, out var existedProxy) + && ProxyStorage.CompareProxy(SelectedProxy.Data, existedProxy); + } + + [RelayCommand] + private async Task OpenProxyManager() + { + await DialogsController.ShowProxyManager(SelectedProxy); + var oldSelection = SelectedProxy; + Proxies.Clear(); + foreach (var kvp in ProxyStorage.Proxies) + { + Proxies.Add(new MaProxy(kvp.Key, kvp.Value)); + } + SelectedProxy = oldSelection; + SetCurrentProxy(); + OnPropertyChanged(nameof(IsDefaultProxy)); + _handleProxyChange = false; + } + + [RelayCommand] + private void RemoveProxy() + { + if (SelectedMafile == null) return; + SelectedMafile.Proxy = null; + SelectedProxy = null; + Storage.UpdateMafile(SelectedMafile); + ProxyExist = true; + } + + private void OnProxyChanged() + { + if (_handleProxyChange) + { + _handleProxyChange = false; + return; + } + + if (SelectedMafile == null) return; + ProxyExist = true; + SelectedMafile.Proxy = SelectedProxy; + Storage.UpdateMafile(SelectedMafile); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Timer.cs b/NebulaAuth/ViewModel/MainVM_Timer.cs new file mode 100644 index 0000000..f16a99e --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Timer.cs @@ -0,0 +1,140 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using SteamLib.Exceptions; +using SteamLib.SteamMobile.Confirmations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Timer +{ + private readonly Timer _confirmTimer; + [ObservableProperty] private bool _marketTimerEnabled; + [ObservableProperty] private bool _tradeTimerEnabled; + private int _timerCheckSeconds = Settings.Instance.TimerSeconds; + public int TimerCheckSeconds + { + get => _timerCheckSeconds; + set => SetTimer(value); + } + + + private async void ConfirmByTimer(object? state = null) + { + if (SelectedMafile == null) + return; + var selected = SelectedMafile; + if (InterruptTimer(selected, null)) return; + List conf; + try + { + conf = (await HandleTimerRequest(() => MaClient.GetConfirmations(selected), SelectedMafile)).ToList(); + } + catch (ApplicationException ex) + { + Shell.Logger.Warn(ex, "Error GetConf in timer."); + return; + } + if (InterruptTimer(selected, conf)) return; + var toConfirm = new List(); + if (MarketTimerEnabled) + { + var market = conf.Where(c => c.ConfType == ConfirmationType.MarketSellTransaction); + toConfirm.AddRange(market); + } + if (TradeTimerEnabled) + { + var trade = conf.Where(c => c.ConfType == ConfirmationType.Trade); + toConfirm.AddRange(trade); + } + if (InterruptTimer(selected, toConfirm)) return; + bool result; + try + { + Shell.Logger.Debug("Sending confirmations. Count: {count}", toConfirm.Count); + result = await HandleTimerRequest(() => MaClient.SendMultipleConfirmation(SelectedMafile, toConfirm, confirm: true), SelectedMafile); + } + catch (ApplicationException ex) + { + Shell.Logger.Warn(ex, "MultiConf error in Timer."); + return; + } + SnackbarController.SendSnackbar(result ? $"{GetLocalizationOrDefault("TimerConfirmed")} {toConfirm.Count}" : GetLocalizationOrDefault("TimerNotConfirmed")); + } + + private bool InterruptTimer(Mafile cachedValue, List? confirmations) + { + return SelectedMafile == null + || (MarketTimerEnabled || TradeTimerEnabled) == false + || SelectedMafile != cachedValue + || confirmations is { Count: 0 }; + + } + + private void OffTimer(bool dispatcher) + { + if (dispatcher) + { + Application.Current.Dispatcher.BeginInvoke(new Action(OffTimerInline)); + } + else + { + OffTimerInline(); + } + return; + + void OffTimerInline() + { + MarketTimerEnabled = false; + TradeTimerEnabled = false; + } + } + + private async Task HandleTimerRequest(Func> func, Mafile mafile) + { + Exception innerException; + try + { + return await SessionHandler.Handle(func, mafile); + } + catch (SessionInvalidException ex) + { + Shell.Logger.Warn("Session error while requesting in timer. Timer disabled"); + SnackbarController.SendSnackbar(GetLocalizationOrDefault("TimerSessionError")); + OffTimer(dispatcher: true); + innerException = ex; + } + catch (Exception ex) when (ExceptionHandler.Handle(ex, GetLocalizationOrDefault("TimerPrefix"))) + { + innerException = ex; + } + throw new ApplicationException("Swallowed", innerException); + } + + + + + private void SetTimer(int value) + { + if (_timerCheckSeconds == value) return; + if (value < 10) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("TimerTooFast")); + OnPropertyChanged(nameof(TimerCheckSeconds)); + return; + } + + Settings.TimerSeconds = value; + _timerCheckSeconds = value; + OnPropertyChanged(nameof(TimerCheckSeconds)); + _confirmTimer.Change(value * 1000, value * 1000); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/LinkAccountVM.cs b/NebulaAuth/ViewModel/Other/LinkAccountVM.cs new file mode 100644 index 0000000..9697241 --- /dev/null +++ b/NebulaAuth/ViewModel/Other/LinkAccountVM.cs @@ -0,0 +1,418 @@ +using AchiesUtilities.Collections; +using AchiesUtilities.Web.Proxy; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MaterialDesignThemes.Wpf; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using Newtonsoft.Json; +using NLog; +using SteamLib; +using SteamLib.Account; +using SteamLib.Authentication; +using SteamLib.Authentication.LoginV2; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.SteamMobile.AuthenticatorLinker; +using SteamLib.Web; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; +using NebulaAuth.Core; + +namespace NebulaAuth.ViewModel.Other; + +public partial class LinkAccountVM : ObservableObject, IEmailProvider, IPhoneNumberProvider, ISmsCodeProvider +{ + private const string LOCALIZATION_KEY = "LinkVM"; + private static Logger Logger => Shell.Logger; + private static Microsoft.Extensions.Logging.ILogger Logger2 => Shell.ExtensionsLogger; + #region Properties + + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsPasswordFieldVisible))] + private bool _isLogin; + + + [ObservableProperty] private bool _isEmailCode; + [ObservableProperty] private bool _isPhoneNumber; + [ObservableProperty] private bool _isEmailConfirmation; + [ObservableProperty] private bool _isLinkCode; + [ObservableProperty] private bool _isCompleted; + + [ObservableProperty] private bool _isFieldVisible = true; + + + + [ObservableProperty] private string _fieldText; + [ObservableProperty] private string _passFieldText; + [ObservableProperty] private string _hintText = GetLocalizationOrDefault("EnterLoginAndPassword"); + + private TaskCompletionSource _emailCodeTcs = new(); + private TaskCompletionSource _phoneNumberTcs = new(); + private TaskCompletionSource _emailConfTcs = new(); + private TaskCompletionSource _linkCodeTcs = new(); + + private bool isLinkStarted; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(ProceedCommand))] + private bool _canProceed = true; + public bool IsPasswordFieldVisible => !IsLogin; + + private LoginV2ExecutorOptions _loginV2ExecutorOptions; + private SteamAuthenticatorLinker _linker; + private MobileSessionData _sessionData; + + #endregion + + #region HttpClient + + private static HttpClient Client { get; } + private static HttpClientHandler Handler { get; } + private static DynamicProxy Proxy { get; } + public ObservableDictionary Proxies => ProxyStorage.Proxies; + + public KeyValuePair? SelectedProxy + { + get => _selectedProxy; + set + { + SetProperty(ref _selectedProxy, value); + SetProxy(); + } + } + private KeyValuePair? _selectedProxy; + + + #endregion + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public LinkAccountVM() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + if (MaClient.DefaultProxy != null) + { + var def = Proxies.FirstOrDefault(p => p.Value.Equals(MaClient.DefaultProxy)); + if (def.Value != null!) + { + SelectedProxy = def; + } + } + } + + [RelayCommand(AllowConcurrentExecutions = true, CanExecute = nameof(CanProceed))] + public async Task Proceed() + { + if (IsCompleted) + DialogHost.Close(null); + + CanProceed = false; + + #region Login + + if (IsLogin == false) + { + SetProxy(); + ClearCookies(); + _loginV2ExecutorOptions = new LoginV2ExecutorOptions(LoginV2Executor.NullConsumer, Client) + { + DeviceDetails = LoginV2ExecutorOptions.GetMobileDefaultDevice(), + WebsiteId = "Mobile", + EmailAuthProvider = this, + Logger = Logger2 + }; + + try + { + IsLogin = true; + var userName = FieldText; + var pass = PassFieldText; + FieldText = string.Empty; + IsFieldVisible = false; + HintText = string.Empty; + _sessionData = (MobileSessionData)await LoginV2Executor.DoLogin(_loginV2ExecutorOptions, userName, pass); + Handler.CookieContainer.SetSteamMobileCookies(_sessionData); + IsEmailCode = true; + } + catch (EResultException ex) + { + Logger.Error(ex, "Link exception on login"); + HintText = GetLocalizationOrDefault("CantLogin") + ErrorTranslatorHelper.TranslateEResult(ex.Result); + InvokeOnDispatcher(ResetState); + return; + } + catch (LoginException ex) + { + Logger.Error(ex, "Link exception on login"); + HintText = GetLocalizationOrDefault("CantLogin") + ErrorTranslatorHelper.TranslateLoginError(ex.Error); + InvokeOnDispatcher(ResetState); + return; + } + catch (Exception ex) + { + Logger.Error(ex, "Link exception on login"); + HintText = GetLocalizationOrDefault("CantLogin") + ex.Message; + InvokeOnDispatcher(ResetState); + return; + } + + } + + if (IsEmailCode == false) + { + _emailCodeTcs.SetResult(FieldText); + HintText = string.Empty; + FieldText = string.Empty; + _emailCodeTcs = new TaskCompletionSource(); + IsFieldVisible = false; + return; + } + + #endregion + + if (isLinkStarted) + goto linkStarted; + + try + { + isLinkStarted = true; + var linkOptions = new LinkOptions(Client, LoginV2Executor.NullConsumer, this, + null, this, this, backupHandler: Backup, Logger2); + _linker = new SteamAuthenticatorLinker(linkOptions); + var result = await _linker.LinkAccount(_sessionData); + IsLinkCode = true; + IsCompleted = true; + var mafile = Mafile.FromMobileDataExtended(result); + Storage.SaveMafile(mafile); + File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile")); + HintText = + string.Format(GetLocalizationOrDefault("MafileLinked"), + mafile.RevocationCode, + mafile.SessionData?.SteamId.Steam64); + + CanProceed = true; + return; + } + catch (AuthenticatorLinkerException ex) + { + Logger.Error(ex, "Link exception"); + HintText = $"{GetLocalizationCommon("Error")}: {ErrorTranslatorHelper.TranslateLinkerError(ex.Error)}"; + InvokeOnDispatcher(ResetState); + return; + + } + catch (HttpRequestException ex) + { + var msg = ex.StatusCode?.ToString() ?? ex.Message; + HintText = $"{GetLocalizationCommon("RequestError")}: {msg}"; + InvokeOnDispatcher(ResetState); + return; + } + catch (EResultException ex) + { + Logger.Error(ex, "Link exception"); + HintText = GetLocalizationOrDefault("ErrorWithCode") + ErrorTranslatorHelper.TranslateEResult(ex.Result); + InvokeOnDispatcher(ResetState); + return; + } + catch (Exception ex) + { + Logger.Error(ex, "Link exception"); + HintText = GetLocalizationOrDefault("UnknownError") + ex.Message; + InvokeOnDispatcher(ResetState); + return; + } + + + linkStarted: + if (IsPhoneNumber == false) + { + var phoneText = FieldText; + FieldText = string.Empty; + + if (string.IsNullOrWhiteSpace(phoneText)) + { + HintText = string.Empty; + IsFieldVisible = false; + _phoneNumberTcs.SetResult(null); + _phoneNumberTcs = new(); + return; + } + + if (!string.IsNullOrWhiteSpace(phoneText) && phoneText.Length >= 4 && long.TryParse(phoneText, out var phone)) + { + HintText = string.Empty; + IsFieldVisible = false; + _phoneNumberTcs.SetResult(phone); + _phoneNumberTcs = new(); + return; + } + else + { + HintText = GetLocalizationOrDefault("PleaseEnterCorrectPhone"); + CanProceed = true; + return; + } + } + + if (IsEmailConfirmation == false) + { + HintText = string.Empty; + _emailConfTcs.SetResult(); + _emailConfTcs = new(); + CanProceed = false; + return; + } + + if (IsLinkCode == false) + { + var linkCode = FieldText; + FieldText = string.Empty; + if (!string.IsNullOrWhiteSpace(linkCode) && linkCode.Length >= 4) + { + HintText = string.Empty; + IsFieldVisible = false; + _linkCodeTcs.SetResult(linkCode); + _linkCodeTcs = new(); + return; + } + else + { + HintText = GetLocalizationOrDefault("PleaseEnterCorrectCode"); + CanProceed = true; + return; + } + } + + } + + [RelayCommand] + public void ResetProxy() + { + if (IsPasswordFieldVisible == false) return; + SelectedProxy = null; + } + + private void InvokeOnDispatcher(Action action) + { + Application.Current.Dispatcher.BeginInvoke(action, null); + } + + private void ResetState() + { + PassFieldText = string.Empty; + IsLogin = false; + IsFieldVisible = true; + IsEmailCode = false; + isLinkStarted = false; + IsPhoneNumber = false; + IsEmailConfirmation = false; + CanProceed = true; + _emailCodeTcs = new TaskCompletionSource(); + } + + private void Backup(MobileDataExtended data) + { + if (Directory.Exists("mafiles_backup") == false) + { + Directory.CreateDirectory("mafiles_backup"); + } + + var json = JsonConvert.SerializeObject(data, Formatting.Indented); + File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json); + } + + #region Providers + + public int MaxRetryCount { get; } + public Task GetEmailAuthCode(ILoginConsumer caller) + { + CanProceed = true; + HintText = GetLocalizationOrDefault("EnterEmailCode"); + IsFieldVisible = true; + return _emailCodeTcs.Task; + } + + public Task GetAddAuthenticatorCode(ILoginConsumer caller) + { + IsPhoneNumber = true; + IsEmailConfirmation = true; + CanProceed = true; + HintText = GetLocalizationOrDefault("EnterEmailCode"); + IsFieldVisible = true; + return _linkCodeTcs.Task; + } + + public Task ConfirmEmailLink(ILoginConsumer caller, EmailConfirmationType confirmationType) + { + IsPhoneNumber = true; + CanProceed = true; + HintText = GetLocalizationOrDefault("ClickOnEmailLink"); + return _emailConfTcs.Task; + } + + public Task GetPhoneNumber(ILoginConsumer caller) + { + CanProceed = true; + HintText = GetLocalizationOrDefault("EnterPhoneNumber"); + IsFieldVisible = true; + return _phoneNumberTcs.Task; + } + public async Task GetSmsCode(ILoginConsumer caller, long? phoneNumber, string? hint) + { + IsPhoneNumber = true; + IsEmailConfirmation = true; + CanProceed = true; + HintText = string.Format(GetLocalizationOrDefault("PhoneHint"), hint); + IsFieldVisible = true; + var code = await _linkCodeTcs.Task; + return int.Parse(code); + } + + #endregion + + + #region Client + + static LinkAccountVM() + { + Proxy = new DynamicProxy(null); + var clientPair = ClientBuilder.BuildMobileClient(Proxy, null); + Client = clientPair.Client; + Handler = clientPair.Handler; + } + + private void ClearCookies() + { + Handler.CookieContainer.ClearMobileSessionCookies(); + } + + private void SetProxy() + { + Proxy.SetData(SelectedProxy?.Value); + } + + #endregion + + + private static string GetLocalizationOrDefault(string key) + { + return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key); + } + + private static string GetLocalizationCommon(string key) + { + return LocManager.GetCommonOrDefault(key, key); + } + + +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/LoginAgainVM.cs b/NebulaAuth/ViewModel/Other/LoginAgainVM.cs new file mode 100644 index 0000000..a9c07b6 --- /dev/null +++ b/NebulaAuth/ViewModel/Other/LoginAgainVM.cs @@ -0,0 +1,10 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace NebulaAuth.ViewModel.Other; + +public partial class LoginAgainVM : ObservableObject +{ + [ObservableProperty] private string _password = null!; + [ObservableProperty] private bool _savePassword; + [ObservableProperty] private string _userName = null!; +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/ProxyManagerVM.cs b/NebulaAuth/ViewModel/Other/ProxyManagerVM.cs new file mode 100644 index 0000000..ba668e1 --- /dev/null +++ b/NebulaAuth/ViewModel/Other/ProxyManagerVM.cs @@ -0,0 +1,160 @@ +using AchiesUtilities.Collections; +using AchiesUtilities.Web.Proxy; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Core; +using NebulaAuth.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows; + +namespace NebulaAuth.ViewModel.Other; + +public partial class ProxyManagerVM : ObservableObject +{ + private const string LOCALIZATION_KEY = "ProxyManagerVM"; + + [ObservableProperty] private KeyValuePair? _selectedProxy; + [ObservableProperty] private string _addProxyField = string.Empty; + [ObservableProperty] private KeyValuePair? _defaultProxy; + public ObservableDictionary Proxies => ProxyStorage.Proxies; + + private static readonly Regex IdRegex = new(@"(?:\{(\d+)\})"); + + + public ProxyManagerVM() + { + if (MaClient.DefaultProxy != null) + DefaultProxy = Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy)); + } + + [RelayCommand] + private void AddProxy() + { + if (string.IsNullOrEmpty(AddProxyField)) return; + if (AddProxyField.Contains(Environment.NewLine)) + { + var split = AddProxyField.Split(Environment.NewLine); + var idPresent = (bool?)null; + var proxies = new List>(); + var i = 0; + foreach (var str in split) + { + i++; + int? id = null; + var match = IdRegex.Match(str); + if (match.Success) id = int.Parse(match.Groups[1].Value); + idPresent ??= match.Success; + + if (idPresent.Value != match.Success) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("WrongFormatSomeIdsMissing")); + return; + } + + + + try + { + var proxy = ProxyData.Parse(str, ProxyStorage.FORMAT); + if (id != null && proxies.Any(kvp => kvp.Key == id)) + { + SnackbarController.SendSnackbar(string.Format(GetLocalizationOrDefault("DuplicateId"), id)); + return; + } + proxies.Add(new KeyValuePair(id, proxy)); + } + catch (FormatException) + { + SnackbarController.SendSnackbar(string.Format(GetLocalizationOrDefault("WrongFormatOnLine"), i)); + return; + } + } + + foreach (var kvp in proxies) + { + ProxyStorage.SetProxy(kvp.Key, kvp.Value); + } + } + else + { + int? id = null; + var input = AddProxyField; + if (IdRegex.IsMatch(AddProxyField)) + { + id = int.Parse(IdRegex.Match(AddProxyField).Groups[1].Value); + input = IdRegex.Replace(input, ""); + } + + ProxyData data; + try + { + data = ProxyData.Parse(input, ProxyStorage.FORMAT); + } + catch (FormatException) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("WrongFormat")); + return; + } + + ProxyStorage.SetProxy(id, data); + + + } + AddProxyField = string.Empty; + CheckIfDefaultProxyStay(); + } + + private void CheckIfDefaultProxyStay() + { + if (!DefaultProxy.HasValue || Proxies.Any(kvp => kvp.Equals(DefaultProxy.Value))) return; + DefaultProxy = null; + MaClient.DefaultProxy = null; + } + + [RelayCommand] + private void RemoveProxy() + { + if (SelectedProxy == null) return; + ProxyStorage.RemoveProxy(SelectedProxy.Value.Key); + CheckIfDefaultProxyStay(); + } + + [RelayCommand] + private void SetDefault() + { + if (SelectedProxy == null) return; + DefaultProxy = SelectedProxy; + MaClient.DefaultProxy = SelectedProxy.Value.Value; + ProxyStorage.Save(); + } + + [RelayCommand] + private void RemoveDefault() + { + DefaultProxy = null; + MaClient.DefaultProxy = null; + ProxyStorage.Save(); + } + + [RelayCommand] + private void CopyProxy(ProxyData? obj) + { + if (obj == null) return; + Clipboard.SetText(obj.ToString(ProxyStorage.FORMAT)); + } + + [RelayCommand] + private void CopyProxyAddress(ProxyData? obj) + { + if (obj == null) return; + Clipboard.SetText(obj.ToString(ProxyStorage.ADDRESS_FORMAT)); + } + + private static string GetLocalizationOrDefault(string key) + { + return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs b/NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs new file mode 100644 index 0000000..a2d1efa --- /dev/null +++ b/NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace NebulaAuth.ViewModel.Other; + +public partial class SetEncryptPasswordVM : ObservableObject +{ + [ObservableProperty] private string? _password; +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/SettingsVM.cs b/NebulaAuth/ViewModel/Other/SettingsVM.cs new file mode 100644 index 0000000..073f0ef --- /dev/null +++ b/NebulaAuth/ViewModel/Other/SettingsVM.cs @@ -0,0 +1,122 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Model; +using System.Collections.Generic; +using System.Windows.Media; +using NebulaAuth.Core; + +namespace NebulaAuth.ViewModel.Other; + +public partial class SettingsVM : ObservableObject +{ + public Settings Settings => Settings.Instance; + + public bool DisableTimersOnChange + { + get => Settings.DisableTimersOnChange; + set => Settings.DisableTimersOnChange = value; + } + public BackgroundMode BackgroundMode + { + get => Settings.BackgroundMode; + set => Settings.BackgroundMode = value; + } + + public bool HideToTray + { + get => Settings.HideToTray; + set => Settings.HideToTray = value; + } + + public Dictionary BackgroundModes => new() + { + {BackgroundMode.Default, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "Default")}, + {BackgroundMode.Custom, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "Custom")}, + {BackgroundMode.Color, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "NoBackground")}, + }; + + public Dictionary Languages { get; } = new() + { + {LocalizationLanguage.English, "English"}, + {LocalizationLanguage.Russian, "Русский"}, + {LocalizationLanguage.Ukrainian, "Українська"} + }; + + public Color? BackgroundColor + { + get => Settings.BackgroundColor; + set => Settings.BackgroundColor = value; + } + + public Color? IconColor + { + get => Settings.IconColor; + set => Settings.IconColor = value; + + } + + public bool UseIcon + { + get => IconColor != null; + set + { + if (value == false) + IconColor = null; + else + IconColor = Color.FromRgb(202, 39, 39); + + OnPropertyChanged(); + OnPropertyChanged(nameof(IconColor)); + } + } + + public bool UseBackground + { + get => BackgroundColor != null; + set + { + if (value == false) + BackgroundColor = null; + else + BackgroundColor = Color.FromRgb(202, 39, 39); + + OnPropertyChanged(); + OnPropertyChanged(nameof(BackgroundColor)); + } + } + + + + public LocalizationLanguage Language + { + get => Settings.Language; + set + { + Settings.Language = value; + LocManager.SetApplicationLocalization(value); + } + } + + public bool LegacyMode + { + get => Settings.LegacyMode; + set => Settings.LegacyMode = value; + } + + public bool AllowAutoUpdate + { + get => Settings.AllowAutoUpdate; + set => Settings.AllowAutoUpdate = value; + } + + [ObservableProperty] private string _password; + + + [RelayCommand] + public void SetPassword() + { + PHandler.SetPassword(Password); + Settings.IsPasswordSet = PHandler.IsPasswordSet; + } + +} \ No newline at end of file diff --git a/NebulaAuth/localization.loc.json b/NebulaAuth/localization.loc.json new file mode 100644 index 0000000..3119657 --- /dev/null +++ b/NebulaAuth/localization.loc.json @@ -0,0 +1,949 @@ +{ + "LanguageName": { + "en": "English", + "ru": "Русский", + "ua": "Українська" + }, + "LanguageWord": { + "en": "Language", + "ru": "Язык", + "ua": "Мова" + }, + "Common": { + "No": { + "en": "No", + "ru": "Нет", + "ua": "Ні" + }, + "Yes": { + "en": "Yes", + "ru": "Да", + "ua": "Так" + }, + "Error": { + "en": "Error", + "ru": "Ошибка", + "ua": "Помилка" + }, + "RequestError": { + "en": "Request error", + "ru": "Ошибка запроса", + "ua": "Помилка запиту" + }, + "Canceled": { + "en": "Canceled", + "ru": "Отменено", + "ua": "Скасовано" + }, + "Abbreviations": { + "Time": { + "Seconds": { + "en": "sec", + "ru": "сек", + "ua": "сек" + } + }, + "Count": { + "Items": { + "en": "items", + "ru": "шт", + "ua": "шт" + } + } + } + }, + "MainWindow": { + "Menu": { + "File": { + "Caption": { + "en": "File", + "ru": "Файл", + "ua": "Файл" + }, + "Import": { + "en": "Import", + "ru": "Импорт", + "ua": "Імпорт" + }, + "Remove": { + "en": "Remove", + "ru": "Удалить", + "ua": "Видалити" + }, + "OpenFolder": { + "en": "Open folder", + "ru": "Открыть папку", + "ua": "Відкрити папку" + }, + "Settings": { + "en": "Settings", + "ru": "Настройки", + "ua": "Налаштування" + } + }, + "Account": { + "Caption": { + "en": "Account", + "ru": "Аккаунт", + "ua": "Аккаунт" + }, + "Link": { + "en": "Link", + "ru": "Привязать", + "ua": "Прив'язати" + }, + "Unlink": { + "en": "Unlink", + "ru": "Отвязать", + "ua": "Відв'язати" + }, + "RefreshSession": { + "en": "Refresh session", + "ru": "Обновить сессию", + "ua": "Оновити сесію" + }, + "LoginAgain": { + "en": "Login", + "ru": "Войти", + "ua": "Увійти" + } + } + }, + "AppBar": { + "GroupsHint": { + "en": "Groups", + "ru": "Группы", + "ua": "Групи" + }, + "Proxy": { + "ProxyHint": { + "en": "Proxy", + "ru": "Прокси", + "ua": "Проксі" + }, + "ProxyOpenManagerHint": { + "en": "Open proxy manager", + "ru": "Открыть менеджер прокси", + "ua": "Відкрити менеджер проксі" + }, + "ProxyAlert": { + "DefaultInUse": { + "en": "Default proxy is in use", + "ru": "Используется прокси по умолчанию", + "ua": "Використовується проксі за замовчуванням" + }, + "MafileProxyInUse": { + "en": "Mafile proxy is in use", + "ru": "Используется прокси из mafile", + "ua": "Використовується проксі з mafile" + } + } + }, + + "TradeTimerHint": { + "en": "Trade", + "ru": "Трейд", + "ua": "Трейд" + }, + "MarketTimerHint": { + "en": "Market", + "ru": "Маркет", + "ua": "Маркет" + } + }, + "LeftPart": { + "SearchBoxHint": { + "en": "Search", + "ru": "Поиск", + "ua": "Пошук" + } + }, + "RightPart": { + "LoadConfirmations": { + "en": "Load confirmations", + "ru": "Загрузить подтверждения", + "ua": "Завантажити підтвердження" + }, + "ConfirmLogin": { + "en": "Confirm login", + "ru": "Подтвердить вход", + "ua": "Підтвердити вхід" + } + }, + "Footer": { + "Account": { + "en": "Account: ", + "ru": "Аккаунт: ", + "ua": "Аккаунт: " + }, + "Group": { + "en": "Group: ", + "ru": "Группа: ", + "ua": "Група: " + } + }, + "ConfirmationTemplates": { + "TradeWith": { + "en": "Trade with", + "ru": "Трейд с", + "ua": "Трейд з" + }, + "Recovery": { + "en": "Recovery", + "ru": "Восстановление", + "ua": "Відновлення" + }, + "ApiKey": { + "en": "Api key", + "ru": "Api ключ", + "ua": "Api ключ" + }, + "Market": { + "en": "Market", + "ru": "Маркет", + "ua": "Маркет" + } + }, + "CodeCopied": { + "ru": "Скопировано", + "en": "Copied", + "ua": "Скопійовано" + }, + "ContextMenus": { + "Mafile": { + "AddToGroup": { + "en": "Add to group", + "ru": "Добавить в группу", + "ua": "Додати в групу" + }, + "RemoveFromGroup": { + "en": "Remove from group", + "ru": "Удалить из группы", + "ua": "Видалити з групи" + } + }, + "Proxy": { + "Copy": { + "en": "Copy", + "ru": "Копировать", + "ua": "Копіювати" + }, + "CopyAddress": { + "en": "Copy address", + "ru": "Копировать адрес", + "ua": "Копіювати адресу" + } + } + } + }, + "SettingsDialog": { + "Title": { + "en": "Settings", + "ru": "Настройки", + "ua": "Налаштування" + }, + "BackgroundHint": { + "en": "Background", + "ru": "Фон", + "ua": "Фон" + }, + "BackgroundMode": { + "Default": { + "en": "Default", + "ru": "По умолчанию", + "ua": "За замовчуванням" + }, + "Custom": { + "en": "Custom", + "ru": "Свой", + "ua": "Свій" + }, + "NoBackground": { + "en": "No background", + "ru": "Без фона", + "ua": "Без фону" + } + }, + "DisableTimersOnSwitch": { + "en": "Disable timers on account switch", + "ru": "Отключать таймеры при смене аккаунта", + "ua": "Вимикати таймери при зміні аккаунта" + }, + "MinimizeToTray": { + "en": "Minimize to tray", + "ru": "Сворачивать в трей", + "ua": "Згортати в трей" + }, + "UseIndicator": { + "en": "Use indicator", + "ru": "Использовать индикатор", + "ua": "Використовувати індикатор" + }, + "UseCustomColor": { + "en": "Custom window color", + "ru": "Свой цвет окна", + "ua": "Свій колір вікна" + }, + "PasswordBox": { + "CurrentCryptPassword": { + "en": "Current encryption password", + "ru": "Текущий пароль шифрования", + "ua": "Поточний пароль шифрування" + }, + "Hint": { + "en": "Doesn't saved on disk", + "ru": "Не сохраняется в файл", + "ua": "Не зберігається у файлах" + } + }, + "LegacyMafileMode": { + "en": "Legacy mafile mode", + "ru": "Режим старых мафайлов", + "ua": "Режим старих мафайлів" + }, + "LegacyMafileModeHint": { + "en": "Use old mafile format for compatibility with old versions of Steam Desktop Authenticator", + "ru": "Использовать старый формат мафайлов для совместимости со старыми версиями Steam Desktop Authenticator", + "ua": "Використовувати старий формат мафайлів для сумісності зі старими версіями Steam Desktop Authenticator" + }, + "AllowAutoUpdate": { + "en": "Allow auto update", + "ru": "Разрешить автообновление", + "ua": "Дозволити автооновлення" + } + }, + "LoginAgainDialog": { + "Title": { + "en": "Login", + "ru": "Вход", + "ua": "Вхід" + }, + "LoginFor": { + "en": "Login for", + "ru": "Войти в", + "ua": "Увійти в" + }, + "LoginButton": { + "en": "Login", + "ru": "Войти", + "ua": "Увійти" + }, + "CancelButton": { + "en": "Cancel", + "ru": "Отмена", + "ua": "Відміна" + }, + "PasswordBox": { + "en": "Password", + "ru": "Пароль", + "ua": "Пароль" + }, + "SaveEncryptedPassword": { + "en": "Save encrypted password to mafile", + "ru": "Сохранить зашифрованный пароль в мафайл", + "ua": "Зберегти зашифрований пароль в мафайлі" + } + }, + "ProxyManagerDialog": { + "Title": { + "en": "Proxy manager", + "ru": "Менеджер прокси", + "ua": "Менеджер проксі" + }, + "DefaultProxy": { + "en": "Default proxy:", + "ru": "Прокси по умолчанию:", + "ua": "Проксі за замовчуванням:" + } + + }, + "WaitLoginDialog": { + "Text": { + "en": "Login in progress. Please wait", + "ru": "Производится логин. Ожидайте", + "ua": "Виконується вхід. Зачекайте" + } + }, + "LinkerDialog": { + "Title": { + "en": "Linker", + "ru": "Привязка", + "ua": "Прив'язка" + }, + "Proxy": { + "en": "Proxy", + "ru": "Прокси", + "ua": "Проксі" + }, + "Authorization": { + "en": "Authorization", + "ru": "Авторизация", + "ua": "Авторизація" + }, + "EmailCode": { + "en": "Email code", + "ru": "Код из письма", + "ua": "Код з листа" + }, + "PhoneNumber": { + "en": "Phone number", + "ru": "Номер телефона", + "ua": "Номер телефону" + }, + "EmailLink": { + "en": "Email link", + "ru": "Ссылка из письма", + "ua": "Посилання з листа" + }, + "SmsOrCode": { + "en": "Sms or code", + "ru": "Смс или код", + "ua": "Смс або код" + }, + "Completed": { + "en": "Completed", + "ru": "Завершено", + "ua": "Завершено" + }, + "Message": { + "en": "Message:", + "ru": "Сообщение:", + "ua": "Повідомлення:" + }, + "ProceedButton": { + "en": "Proceed", + "ru": "Продолжить", + "ua": "Продовжити" + } + }, + "SetEncryptedPasswordDialog": { + "DialogText": { + "en": "You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session. (Optional)", + "ru": "Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией. (Не обязательно)", + "ua": "Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією. (Не обов'язково)" + }, + "Ok": { + "en": "Ok", + "ru": "Ок", + "ua": "Ок" + }, + "Cancel": { + "en": "Cancel", + "ru": "Отмена", + "ua": "Відміна" + }, + "Password": { + "en": "Password", + "ru": "Пароль", + "ua": "Пароль" + } + }, + "CodeBehind": { + "MainVM": { + "SuccessfulLogin": { + "en": "Successful login", + "ru": "Успешный вход", + "ua": "Успішний вхід" + }, + "SessionRefreshed": { + "en": "Session refreshed", + "ru": "Сессия обновлена", + "ua": "Сесія оновлена" + }, + "MissingRCode": { + "en": "Missing RCode", + "ru": "Отсутствует RCode", + "ua": "Відсутній RCode" + }, + "ConfirmRemovingAuthenticator": { + "ru": "Вы точно уверены что хотите удалить аутентификатор?", + "en": "Are you sure you want to remove authenticator?", + "ua": "Ви точно впевнені що хочете видалити аутентифікатор?" + }, + "AuthenticatorRemoved": { + "ru": "Аутентификатор удален", + "en": "Authenticator removed", + "ua": "Аутентифікатор видалено" + }, + "AuthenticatorNotRemoved": { + "ru": "Что-то пошло не так", + "en": "Something went wrong", + "ua": "Щось пішло не так" + }, + "ConfirmLoginSuccess": { + "ru": "Вход осуществлен:", + "en": "Login success:", + "ua": "Вхід здійснено:" + }, + "ConfirmLoginFailedNoRequests": { + "ru": "Запросов на вход не обнаружено", + "en": "No login requests found", + "ua": "Запитів на вхід не виявлено" + }, + "ConfirmLoginFailedMoreThanOneRequest": { + "ru": "Обнаружено несколько запросов на вход. В целях безопасности операция отменена", + "en": "Multiple login requests found. For security reasons, the operation is canceled", + "ua": "Виявлено декілька запитів на вхід. З метою безпеки операція скасована" + }, + "ConfirmationError": { + "ru": "Ошибка подтверждения", + "en": "Confirmation error", + "ua": "Помилка підтвердження" + }, + "ConfirmMafileOverwrite": { + "ru": "Внимание, мафайл уже присутствует в папке mafiles.\r\nПерезаписать мафайлы, которые конфликтуют?", + "en": "Attention, mafile already exists in mafiles folder.\r\nOverwrite conflicting mafiles?", + "ua": "Увага, мафайл вже присутній у папці mafiles.\r\nПерезаписати мафайли, які конфліктують?" + }, + "MafileImportError": { + "ru": "Ошибка импорта мафайла", + "en": "Mafile import error", + "ua": "Помилка імпорту мафайла" + }, + "MissingSessionInMafile": { + "ru": ". У него отсутствует сессия. Добавьте этот файл отдельно от остальных", + "en": ". It has no session. Add this file separately from the others", + "ua": ". В нього відсутня сесія. Додайте цей файл окремо від інших" + }, + "Import": { + "ru": "Импорт", + "en": "Import", + "ua": "Імпорт" + }, + "ImportAdded": { + "ru": "добавлено:", + "en": "added:", + "ua": "додано:" + }, + "ImportSkipped": { + "ru": "пропущено:", + "en": "skipped:", + "ua": "пропущено:" + }, + "ImportErrors": { + "ru": "ошибки:", + "en": "errors:", + "ua": "помилки:" + }, + "RemoveMafileConfirmation": { + "ru": "Удалить мафайл? Это действие переместит мафайл в mafiles_removed", + "en": "Remove mafile? This action will move mafile to mafiles_removed", + "ua": "Видалити мафайл? Ця дія перемістить мафайл в mafiles_removed" + }, + "CantRemoveAlreadyExist": { + "ru": "Не удалось переместить мафайл, возможно в папке уже существует мафайл с таким именем.", + "en": "Can't move mafile, maybe mafile with same name already exists in mafiles_removed folder.", + "ua": "Не вдалося перемістити мафайл, можливо в папці вже існує мафайл з таким іменем." + }, + "CantRemoveMafile": { + "ru": "Не удалось удалить (переместить) мафайл:", + "en": "Can't remove mafile:", + "ua": "Не вдалося видалити (перемістити) мафайл:" + }, + "TimerConfirmed": { + "ru": "Авто: подтверждено ", + "en": "Auto: confirmed ", + "ua": "Авто: підтверджено " + }, + "TimerNotConfirmed": { + "ru": "Авто: подтверждение не сработало", + "en": "Auto: confirmation was unsuccessful", + "ua": "Авто: підтвердження не спрацювало" + }, + "TimerSessionError": { + "ru": "Авто: необходимо обновить сессию или перелогиниться", + "en": "Auto: need to refresh session or relogin", + "ua": "Авто: необхідно оновити сесію або перелогінитися" + }, + "TimerPrefix": { + "ru": "Авто: ", + "en": "Auto: ", + "ua": "Авто: " + }, + "TimerTooFast": { + "ru": "Слишком быстрый таймер.", + "en": "Too fast timer.", + "ua": "Занадто швидкий таймер." + } + }, + "ErrorTranslator": { + "Login": { + "CaptchaRequired": { + "en": "Captcha", + "ru": "Капча", + "ua": "Капча" + }, + "InvalidCredentials": { + "en": "Invalid password", + "ru": "Неверный пароль", + "ua": "Невірний пароль" + + }, + "EmailAuthRequired": { + "en": "Email code required", + "ru": "Требуется код с почты", + "ua": "Потрібен код з пошти" + }, + "InvalidEmailAuthCode": { + "en": "Invalid email code", + "ru": "Неверный код с почты", + "ua": "Невірний код з пошти" + }, + "InvalidTwoFactorCode": { + "en": "Invalid two factor code", + "ru": "Неверный двухфакторный код", + "ua": "Невірний двофакторний код" + }, + "UndefinedError": { + "en": "Undefined error", + "ru": "Неизвестная ошибка", + "ua": "Невідома помилка" + } + }, + "EResult": { + "InvalidPassword": { + "ru": "Неверный пароль", + "en": "Invalid password", + "ua": "Невірний пароль" + }, + "InvalidState": { + "ru": "Неверное состояние (11)", + "en": "Invalid state (11)", + "ua": "Невірний стан (11)" + + }, + "Timeout": { + "ru": "Таймаут (16)", + "en": "Timeout (16)", + "ua": "Таймаут (16)" + + }, + "Banned": { + "ru": "Забанен (17)", + "en": "Banned (17)", + "ua": "Забанений (17)" + }, + "AccountNotFound": { + "ru": "Аккаунт не найден (18)", + "en": "Account not found (18)", + "ua": "Аккаунт не знайдено (18)" + }, + "InvalidSteamID": { + "ru": "Неправильный SteamID (19)", + "en": "Invalid SteamID (19)", + "ua": "Неправильний SteamID (19)" + }, + "ServiceUnavailable": { + "ru": "Сервер не отвечает (20)", + "en": "Service unavailable (20)", + "ua": "Сервер не відповідає (20)" + }, + "Pending": { + "ru": "Ожидание (21)", + "en": "Pending (21)", + "ua": "Очікування (21)" + }, + "LimitExceeded": { + "ru": "Лимит превышен (25)", + "en": "Limit exceeded (25)", + "ua": "Ліміт перевищено (25)" + }, + "Expired": { + "ru": "Истекло (27)", + "en": "Expired (27)", + "ua": "Термін дії закінчився (27)" + }, + "Blocked": { + "ru": "Заблокировано (40)", + "en": "Blocked (40)", + "ua": "Заблоковано (40)" + }, + "AccountDisabled": { + "ru": "Аккаунт отключен (43)", + "en": "Account disabled (43)", + "ua": "Аккаунт відключено (43)" + + }, + "Suspended": { + "ru": "Отключено (Suspended 51)", + "en": "Suspended (51)", + "ua": "Відключено (Suspended 51)" + }, + "Cancelled": { + "ru": "Отмена (52)", + "en": "Cancelled (52)", + "ua": "Скасовано (52)" + + }, + "ExpiredLoginAuthCode": { + "ru": "Истекло время кода (71)", + "en": "Expired login auth code (71)", + "ua": "Термін дії коду закінчився (71)" + + }, + "AccountLockedDown": { + "ru": "Аккаунт заблокирован (КТ 73)", + "en": "Account locked down (73)", + "ua": "Аккаунт заблоковано (КТ 73)" + + }, + "UnexpectedError": { + "ru": "Неожиданная ошибка (79)", + "en": "Unexpected error (79)", + "ua": "Непередбачувана помилка (79)" + }, + "Disabled": { + "ru": "Отключено (80)", + "en": "Disabled (80)", + "ua": "Відключено (80)" + }, + "RegionLocked": { + "ru": "Регион заблокирован (83)", + "en": "Region locked (83)", + "ua": "Регіон заблоковано (83)" + }, + "RateLimitExceeded": { + "ru": "Слишком много запросов (84)", + "en": "Rate limit exceeded (84)", + "ua": "Забагато запитів (84)" + }, + "TwoFactorCodeMismatch": { + "ru": "2FA код не подошел (88)", + "en": "2FA code mismatch (88)", + "ua": "2FA код не підійшов (88)" + }, + "TwoFactorActivationCodeMismatch": { + "ru": "2FA код не прошел проверку (89)", + "en": "2FA activation code mismatch (89)", + "ua": "2FA код не пройшов перевірку (89)" + + }, + "TimeNotSynced": { + "ru": "Время не синхронизировано (93)", + "en": "Time not synced (93)", + "ua": "Час не синхронізовано (93)" + + }, + "SMSCodeFailed": { + "ru": "Ошибка SMS-кода (94)", + "en": "SMS code failed (94)", + "ua": "Помилка SMS-коду (94)" + }, + "AccountLimitExceeded": { + "ru": "Лимит аккаунта превышен (95)", + "en": "Account limit exceeded (95)", + "ua": "Ліміт аккаунта перевищено (95)" + + }, + "EmailSendFailure": { + "ru": "Не удалось отправить письмо (99)", + "en": "Email send failure (99)", + "ua": "Не вдалося відправити лист (99)" + + }, + "NeedCaptcha": { + "ru": "Капча (101)", + "en": "Need captcha (101)", + "ua": "Капча (101)" + + }, + "IPBanned": { + "ru": "IP заблокирован (105)", + "en": "IP banned (105)", + "ua": "IP заблоковано (105)" + + }, + "LimitedUserAccount": { + "ru": "Аккаунт с лимитом (112)", + "en": "Limited user account (112)", + "ua": "Аккаунт з лімітом (112)" + }, + "AccountDeleted": { + "ru": "Аккаунт удален (114)", + "en": "Account deleted (114)", + "ua": "Аккаунт видалено (114)" + + }, + "PhoneNumberIsVOIP": { + "ru": "Номера VOIP запрещены (127)", + "en": "Phone number is VOIP (127)", + "ua": "Номера VOIP заборонені (127)" + } + }, + "AuthenticatorLinkerError": { + "PhoneAlreadyAttached": { + "ru": "Телефон привязан, попробуйте через 20 минут", + "en": "Phone already attached, try again in 20 minutes", + "ua": "Телефон прив'язано, спробуйте через 20 хвилин" + }, + "InvalidPhoneNumber": { + "ru": "Неверный номер телефона", + "en": "Invalid phone number", + "ua": "Невірний номер телефону" + }, + "CantAttachPhone": { + "ru": "Не удается привязать телефон", + "en": "Can't attach phone", + "ua": "Не вдається прив'язати телефон" + }, + "CantConfirmAttachingEmail": { + "ru": "Не удалось подтвердить письмо", + "en": "Can't confirm attaching email", + "ua": "Не вдалося підтвердити лист" + }, + "CantSendSms": { + "ru": "Не удалось отправить SMS", + "en": "Can't send SMS", + "ua": "Не вдалося відправити SMS" + }, + "AuthenticatorPresent": { + "ru": "Аутентификатор привязан", + "en": "Authenticator present", + "ua": "Аутентифікатор прив'язано" + }, + "BadConfirmationCode": { + "ru": "Код не подошел", + "en": "Bad confirmation code", + "ua": "Код не підійшов" + }, + "UnableToGenerateCorrectCodes": { + "ru": "Не удается сгенерировать коды", + "en": "Unable to generate correct codes", + "ua": "Не вдається згенерувати коди" + }, + "InvalidStateWithStatus2": { + "ru": "Неверное состояние (InvalidState) со статусом 2. Попробуйте привязку с помощью СМС. Если СМС придет, но ошибка повторится. Нужно попробовать еще раз.", + "en": "Invalid state (InvalidState) with status 2. Try to attach with SMS. If SMS will come, but error will repeat. You need to try again.", + "ua": "Невірний стан (InvalidState) зі статусом 2. Спробуйте прив'язати з допомогою СМС. Якщо СМС прийде, але помилка повториться. Потрібно спробувати ще раз." + }, + "GeneralFailure": { + "ru": "General Failure", + "en": "General Failure", + "ua": "General Failure" + } + } + }, + "ExceptionHandler": { + "SessionExpiredException": { + "ru": "Сессия истекла. Попробуйте обновить ее через меню", + "en": "Session expired. Try to refresh it through menu", + "ua": "Сесія закінчилася. Спробуйте оновити її через меню" + }, + "SessionInvalidException": { + "ru": "Сессия невалидна. Нужно залогиниться заново", + "en": "Session invalid. Need to login again", + "ua": "Сесія невалідна. Потрібно залогінитися знову" + }, + "TaskCanceledException": { + "ru": "Произошла отмена операции", + "en": "Operation canceled", + "ua": "Операція скасована" + }, + "TimeoutException": { + "ru": "Таймаут подключения. Проверьте ваше соединение с Proxy или интернетом", + "en": "Connection timeout. Check your connection with Proxy or internet", + "ua": "Таймаут підключення. Перевірте ваше з'єднання з Proxy або інтернетом" + }, + "UnsupportedResponseException": { + "ru": "Получен неожиданный ответ от Steam. Результат сохранен в log.log. Свяжитесь с разработчиком", + "en": "Unexpected response from Steam. Result saved in log.log. Contact developer", + "ua": "Отримано неочікувану відповідь від Steam. Результат збережено в log.log. Зв'яжіться з розробником" + }, + "LoginException": { + "ru": "Ошибка входа: ", + "en": "Login error: ", + "ua": "Помилка входу: " + }, + "UnknownException": { + "ru": "Неизвестная ошибка: ", + "en": "Unknown error: ", + "ua": "Невідома помилка: " + } + + }, + "ProxyManagerVM": { + "WrongFormatSomeIdsMissing": { + "ru": "Неверный формат. Некоторые прокси не имеют ID", + "en": "Wrong format. Some proxies have no ID", + "ua": "Невірний формат. Деякі проксі не мають ID" + }, + "DuplicateId": { + "ru": "Прокси с ID {0} добавлен несколько раз", + "en": "Proxy with ID {0} added multiple times", + "ua": "Проксі з ID {0} додано декілька разів" + }, + "WrongFormatOnLine": { + "ru": "Прокси на строке {0} имеет неверный формат.", + "en": "Proxy on line {0} has wrong format.", + "ua": "Проксі на строці {0} має невірний формат." + }, + "WrongFormat": { + "ru": "Прокси имеет неверный формат.", + "en": "Proxy has wrong format.", + "ua": "Проксі має невірний формат." + } + }, + "LinkVM": { + "EnterLoginAndPassword": { + "ru": "Введите логин и пароль", + "en": "Enter login and password", + "ua": "Введіть логін і пароль" + }, + "CantLogin": { + "ru": "Не удалось залогиниться: ", + "en": "Can't login: ", + "ua": "Не вдалося залогінитися: " + }, + "MafileLinked": { + "ru": "Мафайл успешно привязан. RCode: {0}\r\nИмя файла: {1}.mafile", + "en": "Mafile successfully linked. RCode: {0}\r\nFile name: {1}.mafile", + "ua": "Мафайл успішно прив'язано. RCode: {0}\r\nІм'я файлу: {1}.mafile" + }, + "ErrorWithCode": { + "ru": "Ошибка с кодом: ", + "en": "Error with code: ", + "ua": "Помилка з кодом: " + }, + "UnknownError": { + "ru": "Неизвестная ошибка: ", + "en": "Unknown error: ", + "ua": "Невідома помилка: " + }, + "PleaseEnterCorrectPhone": { + "ru": "Пожалуйста, введите корректный номер телефона", + "en": "Please enter correct phone number", + "ua": "Будь ласка, введіть коректний номер телефону" + }, + "PleaseEnterCorrectCode": { + "ru": "Пожалуйста, введите корректный код", + "en": "Please enter correct code", + "ua": "Будь ласка, введіть коректний код" + }, + "EnterEmailCode": { + "ru": "Введите код из письма", + "en": "Enter code from email", + "ua": "Введіть код з листа" + }, + "ClickOnEmailLink": { + "ru": "На почту было отправлено письмо, нажмите по ссылке затем - 'Продолжить'", + "en": "Email was sent, click on link and then - 'Proceed'", + "ua": "На пошту було відправлено лист, натисніть по посиланню потім - 'Продовжити'" + }, + "PhoneHint": { + "ru": "На телефон {0} была отправлена СМС", + "en": "SMS was sent to phone {0}", + "ua": "На телефон {0} було відправлено СМС" + }, + "EnterPhoneNumber": { + "ru": "Введите номер телефона (без знака '+' и др. символов)", + "en": "Enter phone number (without '+' and other symbols)", + "ua": "Введіть номер телефону (без знака '+' та ін. символів)" + } + } + }, + "CantAlignTimeError": { + "ru": "Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log", + "en": "Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log", + "ua": "Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log" + } +} diff --git a/SteamLibForked/Account/MobileSessionData.cs b/SteamLibForked/Account/MobileSessionData.cs new file mode 100644 index 0000000..e68b6bd --- /dev/null +++ b/SteamLibForked/Account/MobileSessionData.cs @@ -0,0 +1,51 @@ +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace SteamLib.Account; + +//WARNING: Any changes here should be reflected in MafileSerializer.cs + +public sealed class MobileSessionData : SessionData, IMobileSessionData +{ + public SteamAuthToken? MobileToken { get; private set; } + + public SteamAuthToken? GetMobileToken() => MobileToken; + + + [MemberNotNull(nameof(MobileToken))] + public void SetMobileToken(SteamAuthToken token) + { + if (token.Type != SteamAccessTokenType.Mobile) + throw new ArgumentException("Token must be of type MobileAccess", nameof(token)) + { + Data = { { "ActualType", token.Type } } + }; + + MobileToken = token; + } + + [JsonConstructor] + public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, + SteamAuthToken? mobileToken, IDictionary? tokens) + : base(sessionId, steamId, refreshToken, tokens) + { + MobileToken = mobileToken; + } + + public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, + SteamAuthToken? mobileToken, IEnumerable? tokens) + : base(sessionId, steamId, refreshToken, tokens) + { + MobileToken = mobileToken; + } + + public override MobileSessionData Clone() => (MobileSessionData)((ISessionData)this).Clone(); + object ICloneable.Clone() + { + return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens); + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Account/SessionData.cs b/SteamLibForked/Account/SessionData.cs new file mode 100644 index 0000000..662e585 --- /dev/null +++ b/SteamLibForked/Account/SessionData.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Web.Converters; +using System.Collections.Concurrent; + +namespace SteamLib.Account; + +public class SessionData : ISessionData +{ + [JsonIgnore] + public bool? IsValid { get; set; } + public string SessionId { get; } + + [JsonConverter(typeof(SteamIdToSteam64Converter))] + public SteamId SteamId { get; } + public SteamAuthToken RefreshToken { get; } + public ConcurrentDictionary Tokens { get; } + + [JsonConstructor] + public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IDictionary? tokens) + { + SessionId = sessionId; + SteamId = steamId; + RefreshToken = refreshToken; + Tokens = new ConcurrentDictionary(tokens ?? new Dictionary()); + } + + public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IEnumerable? tokens) + { + SessionId = sessionId; + SteamId = steamId; + RefreshToken = refreshToken; + tokens ??= Array.Empty(); + Tokens = new ConcurrentDictionary(tokens.ToDictionary(t => t.Domain, t => t)); + } + + public virtual SteamAuthToken? GetToken(SteamDomain domain) + { + if (domain == SteamDomain.Undefined) + throw new ArgumentException("SteamDomain.Undefined is not allowed"); + if (Tokens.TryGetValue(domain, out var token)) + return token; + return null; + + } + + public void SetToken(SteamDomain domain, SteamAuthToken token) + { + Tokens[domain] = token; + } + + + + public virtual SessionData Clone() => (SessionData)((ICloneable)this).Clone(); + object ICloneable.Clone() + { + return new SessionData(SessionId, SteamId, RefreshToken, Tokens); + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Account/SteamAuthToken.cs b/SteamLibForked/Account/SteamAuthToken.cs new file mode 100644 index 0000000..2ea2507 --- /dev/null +++ b/SteamLibForked/Account/SteamAuthToken.cs @@ -0,0 +1,49 @@ +using AchiesUtilities.Models; +using AchiesUtilities.Newtonsoft.JSON.Converters.Special; +using Newtonsoft.Json; +using SteamLib.Authentication; +using SteamLib.Core.Enums; +using SteamLib.Web.Converters; + +namespace SteamLib.Account; + +public readonly struct SteamAuthToken +{ + public string Token { get; } + + [JsonConverter(typeof(SteamIdToSteam64Converter))] + public SteamId SteamId { get; } + + [JsonConverter(typeof(UnixTimeStampConverter))] + public UnixTimeStamp Expires { get; } + public SteamDomain Domain { get; init; } + public SteamAccessTokenType Type { get; } + + [JsonIgnore] + public bool IsExpired => Expires.Time < DateTime.UtcNow; + + [JsonIgnore] + public string SignedToken { get; } + + public SteamAuthToken(string token, long steamId, UnixTimeStamp expires, SteamDomain domain, SteamAccessTokenType type) + { + Token = token; + Expires = expires; + Domain = domain; + Type = type; + SteamId = SteamId.FromSteam64(steamId); + SignedToken = SteamTokenHelper.CombineJwtWithSteamId(SteamId.Steam64.Id, Token); + } + + [JsonConstructor] + public SteamAuthToken(string token, SteamId steamId, UnixTimeStamp expires, SteamDomain domain, SteamAccessTokenType type) + { + Token = token; + SteamId = steamId; + Expires = expires; + Domain = domain; + Type = type; + SignedToken = SteamTokenHelper.CombineJwtWithSteamId(SteamId.Steam64.Id, Token); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Account/SteamId.cs b/SteamLibForked/Account/SteamId.cs new file mode 100644 index 0000000..4985a58 --- /dev/null +++ b/SteamLibForked/Account/SteamId.cs @@ -0,0 +1,180 @@ +using SteamLib.Utility; + +namespace SteamLib.Account; + +public struct SteamId //TODO: validation in parse methods +{ + public SteamId64 Steam64 { get; } + public SteamId2 Steam2 { get; } + public SteamId3 Steam3 { get; } + public SteamId(SteamId64 steam64, char type = 'U', short universe = 0) + { + Steam64 = steam64; + Steam2 = steam64.ToSteam2(universe); + Steam3 = steam64.ToSteam3(type); + } + + public SteamId(SteamId2 steam2, char type = 'U') + { + Steam2 = steam2; + Steam64 = steam2.ToSteam64(); + Steam3 = steam2.ToSteam3(type); + } + + public SteamId(SteamId3 steam3, byte universe = 0) + { + Steam3 = steam3; + Steam64 = steam3.ToSteam64(); + Steam2 = steam3.ToSteam2(universe); + } + + public static SteamId FromSteam64(long steam64, char type = 'U', short universe = 0) => new(new SteamId64(steam64), type, universe); + public override string ToString() => Steam64.ToString(); + public static bool TryParse(string input, out SteamId result) => SteamIdParser.TryParse(input, out result); + + /// + public static SteamId Parse(string input) => SteamIdParser.Parse(input); + public static bool operator ==(SteamId left, SteamId right) => left.Equals(right); + public static bool operator !=(SteamId left, SteamId right) => !left.Equals(right); + public bool Equals(SteamId other) => Steam64.Equals(other.Steam64); + public override bool Equals(object? obj) => obj is SteamId other && Equals(other); + public override int GetHashCode() => Steam64.GetHashCode(); +} + +public struct SteamId64 +{ + public const long SEED = 76561197960265728L; + public long Id { get; } + public SteamId64(long id) + { + if (id < SEED) + throw new ArgumentOutOfRangeException(nameof(id),$"Invalid SteamID provided {id}"); + Id = id; + } + + public SteamId2 ToSteam2(short universe = 0) + { + var accountIdLowBit = (byte)(Id & 1); + var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF; + return new SteamId2((byte)universe, accountIdLowBit, accountIdHighBits); + } + + public SteamId3 ToSteam3(char type = 'U') + { + var accountIdLowBit = (byte)(Id & 1); + var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF; + return new SteamId3(accountIdLowBit + accountIdHighBits * 2, type); + } + + + public override string ToString() + { + return Id.ToString(); + } + + public ulong ToUlong() => (ulong)Id; + public long ToLong() => Id; + + public bool Equals(SteamId64 other) => Id == other.Id; + public override bool Equals(object? obj) => obj is SteamId64 other && Equals(other); + public override int GetHashCode() => Id.GetHashCode(); + public static bool operator ==(SteamId64 left, SteamId64 right) => left.Equals(right); + public static bool operator !=(SteamId64 left, SteamId64 right) => !left.Equals(right); + public static bool TryParse(string input, out SteamId64 result) => SteamIdParser.TryParse64(input, out result); + /// + public static SteamId64 Parse(string input) => SteamIdParser.Parse64(input); + + public static implicit operator long(SteamId64 steamId64) => steamId64.ToLong(); +} + +public struct SteamId2 +{ + + public byte Universe { get; } + public byte LowestBit { get; } + public int HighestBit { get; } + + + public SteamId2(byte lowestBit, int highestBit) + { + if(lowestBit > 1) + throw new ArgumentOutOfRangeException(nameof(lowestBit), $"Invalid SteamID2 lowestBit provided {lowestBit}. Max value is 1"); + + if(highestBit < 0) + throw new ArgumentOutOfRangeException(nameof(highestBit), $"Invalid SteamID2 highestBit provided {highestBit}"); + + LowestBit = lowestBit; + HighestBit = highestBit; + Universe = 0; + } + + public SteamId2(byte universe, byte lowestBit, int highestBit) + { + Universe = universe; + LowestBit = lowestBit; + HighestBit = highestBit; + } + + public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + LowestBit + HighestBit * 2); + + public SteamId3 ToSteam3(char type = 'U') => new SteamId3(HighestBit * 2 + LowestBit); + + public override string ToString() => $"STEAM_{Universe}:{LowestBit}:{HighestBit}"; + + public bool Equals(SteamId2 other) => Universe == other.Universe && LowestBit == other.LowestBit && HighestBit == other.HighestBit; + public override bool Equals(object? obj) => obj is SteamId2 other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Universe, LowestBit, HighestBit); + public static bool operator ==(SteamId2 left, SteamId2 right) => left.Equals(right); + public static bool operator !=(SteamId2 left, SteamId2 right) => !left.Equals(right); + public static bool TryParse(string input, out SteamId2 result) => SteamIdParser.TryParse2(input, out result); + /// + public static SteamId2 Parse(string input) => SteamIdParser.Parse2(input); +} + +public struct SteamId3 +{ + public char Type { get; } + public int Id { get; } + + public SteamId3(int id, char type = 'U') + { + if (id < 0) + throw new ArgumentOutOfRangeException(nameof(id), $"Invalid SteamID provided {id}"); + Type = type; + Id = id; + } + + public SteamId2 ToSteam2(byte universe = 0) + { + var bit = Id % 2; + var highestBits = Id / 2; + return new SteamId2(universe, (byte)bit, highestBits); + } + + + public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + Id); + public override string ToString() => $"[{Type}:1:{Id}]"; + + public string ToString(bool withBrackets) + { + if (withBrackets) + { + return ToString(); + } + else + { + return $"{Type}:1:{Id}"; + } + } + + public int ToInt() => Id; + public bool Equals(SteamId3 other) => Type == other.Type && Id == other.Id; + public override bool Equals(object? obj) => obj is SteamId3 other && Equals(other); + public readonly override int GetHashCode() => HashCode.Combine(Type, Id); + public static bool operator ==(SteamId3 left, SteamId3 right) => left.Equals(right); + public static bool operator !=(SteamId3 left, SteamId3 right) => !left.Equals(right); + public static bool TryParse(string input, out SteamId3 result) => SteamIdParser.TryParse3(input, out result); + + /// + public static SteamId3 Parse(string input) => SteamIdParser.Parse3(input); +} \ No newline at end of file diff --git a/SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs b/SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs new file mode 100644 index 0000000..3f933e7 --- /dev/null +++ b/SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs @@ -0,0 +1,305 @@ +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using SteamLib.Authentication; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Core.StatusCodes; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Services; +using SteamLib.SteamMobile; +using SteamLib.SteamMobile.AuthenticatorLinker; + +namespace SteamLib.Api.Mobile; + +[PublicAPI] +public static class SteamAuthenticatorLinkerApi +{ + private const string PHONE_REQ = SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax"; + + #region Global + + public static Task HasPhone(HttpClient client, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/AccountPhoneStatus/v1"; + + var reqUri = AddAccessToken(uri, accessToken); + return client.PostProto(reqUri, request: null); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task LinkRequest(HttpClient client, ISessionData data, string deviceId) + { + const string uri = SteamConstants.STEAM_API + "ITwoFactorService/AddAuthenticator/v1"; + data.EnsureValidated(); + var reqUri = AddAccessToken(uri, data.GetAccessToken()); + var req = new AddAuthenticator_Request + { + SteamId = (ulong)data.SteamId.Steam64.Id, + AuthenticatorType = 1, + DeviceIdentifier = deviceId, + Version = 2 + }; + + var resp = await client.PostProtoMsg(reqUri, req); + if (resp is {Result: EResult.InvalidState, ResponseMsg.Status: 2}) + { + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidStateWithStatus2); + } + + return resp.GetResponseEnsureSuccess(); + } + + public static async Task FinalizeLink(HttpClient client, string confirmationCode, ISessionData data, + byte[] sharedSecret, bool validateSmsCode) + { + const string uri = SteamConstants.STEAM_API + "ITwoFactorService/FinalizeAddAuthenticator/v1"; + data.EnsureValidated(); + var reqUri = AddAccessToken(uri, data.GetAccessToken()); + var time = await TimeAligner.GetSteamTimeAsync(); + if (validateSmsCode) + { + var validateSmsReq = new FinalizeAddAuthenticator_Request + { + SteamId = data.SteamId.Steam64.ToUlong(), + AuthenticatorTime = (ulong) time, + ConfirmationCode = confirmationCode, + ValidateConfirmationCode = true + }; + + var validateResp = await client.PostProto(reqUri, validateSmsReq); + if (validateResp.Success == false || validateResp.Status != 2) + { + if (validateResp.Status == 89) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.BadConfirmationCode); + throw new AuthenticatorLinkerException("Can't accept sms code. Status: " + validateResp.Status); + } + } + + var tries = 0; + + + while (tries < 30) + { + tries++; + time = await TimeAligner.GetSteamTimeAsync(); + var code = SteamGuardCodeGenerator.GenerateCode(sharedSecret, time); + + var req = new FinalizeAddAuthenticator_Request + { + SteamId = data.SteamId.Steam64.ToUlong(), + AuthenticatorCode = code, + AuthenticatorTime = (ulong)time, + ConfirmationCode = confirmationCode, + }; + + + var resp = await client.PostProto(reqUri, req); + + if (resp.Success && resp.WantMore == false) + { + return new LinkResult(); + } + + if (resp.Status == 89) + { + return new LinkResult(LinkError.BadConfirmationCode); + } + + + if (resp.Status == 88 && tries >= 30) + { + return new LinkResult(LinkError.UnableToGenerateCorrectCodes); + } + } + return new LinkResult(LinkError.GeneralFailure); + } + + public static async Task IsValidPhoneNumber(HttpClient client, long phoneNumber, string sessionId) + { + return await CheckPhoneNumber(client, phoneNumber, sessionId) == CheckPhoneResult.Valid; + } + public static async Task CheckPhoneNumber(HttpClient client, long phoneNumber, string sessionId) + { + var phone = '+' + phoneNumber.ToString(); + var data = new Dictionary + { + {"phoneNumber", phone}, + {"sessionID", sessionId} + }; + + var reqContent = new FormUrlEncodedContent(data); + var req = new HttpRequestMessage(HttpMethod.Post, SteamConstants.STEAM_STORE + "phone/validate"); + req.Content = reqContent; + req.Headers.Referrer = new Uri("https://store.steampowered.com/phone/add"); + var resp = await client.SendAsync(req); + var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var j = JObject.Parse(content); + + if (j["success"]!.Value() == false) + return CheckPhoneResult.GeneralFailure; + + if (j["is_voip"]!.Value()) + return CheckPhoneResult.VoIp; + + return j["is_valid"]!.Value() ? CheckPhoneResult.Valid : CheckPhoneResult.NotValid; + } + + public static async Task AttachPhone(HttpClient client, long phoneNumber, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/SetAccountPhoneNumber/v1"; + var phone = '+' + phoneNumber.ToString(); + var req = new SetAccountPhoneNumber_Request + { + PhoneNumber = phone + }; + + var reqUri = AddAccessToken(uri, accessToken); + var resp = + await client.PostProtoMsg(reqUri, req); + + return resp.Result == EResult.Pending; + } + public static async Task CheckEmailConfirmation(HttpClient client, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/IsAccountWaitingForEmailConfirmation/v1"; + + var reqUri = AddAccessToken(uri, accessToken); + + var i = 0; + while (i < 5) + { + i++; + var resp = await client.PostProto(reqUri, new EmptyMessage()); ; + + if (resp.IsWaiting == false) return true; + + await Task.Delay(resp.SecondsToWait * 1000); + } + + return false; + } + public static Task SendSmsCode(HttpClient client, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/SendPhoneVerificationCode/v1"; + + var reqUri = AddAccessToken(uri, accessToken); + return client.PostProto(reqUri, new SendPhoneVerificationCode_Request()); + } + public static async Task CheckSmsCode(HttpClient client, int smsCode, string sessionId) + { + var data = new Dictionary + { + {"op", "check_sms_code"}, + {"arg", smsCode.ToString()}, + {"checkfortos", "0"}, + {"skipvoip", "1"}, + {"sessionid", sessionId} + }; + + var content = new FormUrlEncodedContent(data); + var resp = await client.PostAsync(PHONE_REQ, content); + var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var success = SteamStatusCode.Translate(respContent, out _).Equals(SteamStatusCode.Ok); + + if (success) return true; + + await Task.Delay(5000); + var hasPhone = await HasPhone(client, sessionId); + return hasPhone.HasPhone; + } + private static FormUrlEncodedContent CreateAjaxContent(string op, string arg, string sessionId) + { + var data = new Dictionary + { + {"op",op}, + {"arg", arg}, + {"sessionid", sessionId} + }; + return new FormUrlEncodedContent(data); + } + + public static string GenerateDeviceId() => "android:" + Guid.NewGuid(); + private static string AddAccessToken(string uri, string accessToken) => uri + "?access_token=" + accessToken; + + #endregion + + #region ForLinker + + internal static Task LinkRequest(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return LinkRequest(linker.Client, linker.SessionData, linker.DeviceId ??= GenerateDeviceId()); + } + + internal static Task IsValidPhoneNumber(this SteamAuthenticatorLinker linker, long phoneNumber) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return IsValidPhoneNumber(linker.Client, phoneNumber, linker.SessionData.SessionId); + } + + internal static Task HasPhone(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return HasPhone(linker.Client, linker.SessionData.GetAccessToken()); + } + internal static Task AttachPhone(this SteamAuthenticatorLinker linker, long phoneNumber) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return AttachPhone(linker.Client, phoneNumber, linker.SessionData.GetAccessToken()); + } + internal static Task CheckEmailConfirmation(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return CheckEmailConfirmation(linker.Client, linker.SessionData.GetAccessToken()); + } + + internal static Task SendSmsCode(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return SendSmsCode(linker.Client, linker.SessionData.GetAccessToken()); + } + internal static Task CheckSmsCode(this SteamAuthenticatorLinker linker, int smsCode) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return CheckSmsCode(linker.Client, smsCode, linker.SessionData.SessionId); + } + + internal static Task FinalizeLink(this SteamAuthenticatorLinker linker, string confirmationCode, byte[] sharedSecret, bool validateSmsCode) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return FinalizeLink(linker.Client, confirmationCode, linker.SessionData, sharedSecret, validateSmsCode); + } + + + #endregion + private static string GetAccessToken(this ISessionData s) => s.GetToken(SteamDomain.Community)?.Token ?? throw new SessionInvalidException("Access token was null. MobileEndpoints requires valid AccessToken"); +} \ No newline at end of file diff --git a/SteamLibForked/Api/Mobile/SteamMobileApi.cs b/SteamLibForked/Api/Mobile/SteamMobileApi.cs new file mode 100644 index 0000000..d1cee0d --- /dev/null +++ b/SteamLibForked/Api/Mobile/SteamMobileApi.cs @@ -0,0 +1,89 @@ +using System.Net; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using SteamLib.Core; +using SteamLib.Exceptions; +using SteamLib.ProtoCore; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Services; + +namespace SteamLib.Api.Mobile; + + +[PublicAPI] +public static class SteamMobileApi +//TODO: cancellation token +//TODO: HealthMonitor +{ + + private const string GENERATE_ACCESS_TOKEN = + SteamConstants.STEAM_API + "IAuthenticationService/GenerateAccessTokenForApp/v1"; + + + /// + /// + /// + /// + /// + /// + /// Refreshed AccessToken + /// + public static async Task RefreshJwt(HttpClient client, string refreshToken, long steamId) + { + var req = new GenerateAccessTokenForApp_Request + { + RefreshToken = refreshToken, + SteamId = steamId, + TokenRenewalType = true + }; + + try + { + var resp = await client.PostProto(GENERATE_ACCESS_TOKEN, req); + return resp.AccessToken; + } + catch (EResultException ex) + when (ex.Result == EResult.AccessDenied) + { + throw new SessionInvalidException("RefreshToken is not accepted by Steam. You must login again and use new token"); + } + } + + public static async Task HasPhoneAttached(HttpClient client, string sessionId) + { + var data = new Dictionary + { + {"op", "has_phone"}, + {"arg", "null"}, + {"sessionid", sessionId} + + }; + var content = new FormUrlEncodedContent(data); + var resp = await client.PostAsync(SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax", content); + var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var json = JObject.Parse(respContent); + return json["has_phone"]!.Value(); + } + + + public static async Task RemoveAuthenticator(HttpClient client, string accessToken, string rCode) + { + var req = SteamConstants.STEAM_API + "ITwoFactorService/RemoveAuthenticator/v1?access_token=" + accessToken; + var reqData = new RemoveAuthenticator_Request + { + RevocationCode = rCode, + RevocationReason = 1, + SteamGuardScheme = 1 + }; + try + { + return await client.PostProto(req, reqData); + } + catch (HttpRequestException ex) + when (ex.StatusCode is HttpStatusCode.Unauthorized) + { + throw new SessionExpiredException("Session expired"); + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs b/SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs new file mode 100644 index 0000000..41b5ec8 --- /dev/null +++ b/SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs @@ -0,0 +1,96 @@ +using SteamLib.Core; +using SteamLib.Exceptions; +using SteamLib.SteamMobile.Confirmations; +using SteamLib.SteamMobile; +using System.Net; +using AchiesUtilities.Web.Extensions; +using SteamLib.Web.Scrappers.JSON; +using SteamLib.Core.StatusCodes; +using SteamLib.Utility; + +namespace SteamLib.Api.Mobile; + +public static class SteamMobileConfirmationsApi +{ + private const string CONF = SteamConstants.STEAM_COMMUNITY + "mobileconf"; + private const string CONF_OP = SteamConstants.STEAM_COMMUNITY + "mobileconf/ajaxop"; + private const string MULTI_CONF_OP = SteamConstants.STEAM_COMMUNITY + "mobileconf/multiajaxop"; + + public static async Task> GetConfirmations(HttpClient client, MobileData data, long steamId) + { + var nvc = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, "list"); + + var req = new Uri(CONF + "/getlist" + nvc.ToQueryString()); + var reqMsg = new HttpRequestMessage(HttpMethod.Get, req); + var resp = await client.SendAsync(reqMsg); + + var respStr = await resp.Content.ReadAsStringAsync(); + if (resp.StatusCode == HttpStatusCode.Redirect) + { + throw new SessionExpiredException("Mobile session expired"); + } + + resp.EnsureSuccessStatusCode(); + + + return HealthMonitor.LogOnException(respStr, MobileConfirmationScrapper.Scrap, typeof(SessionExpiredException)); + } + + public static async Task SendConfirmation(HttpClient client, Confirmation confirmation, long steamId, MobileData data, bool confirm) + { + var op = confirm ? "allow" : "cancel"; + + var query = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, op).ToList(); + + var id = confirmation.Id.ToString(); + var key = confirmation.Nonce.ToString(); + + query.Insert(0, new KeyValuePair("op", op)); + query.Add(new KeyValuePair("cid", id)); + query.Add(new KeyValuePair("ck", key)); + + var req = CONF_OP + query.ToQueryString(); + var resp = await client.GetStringAsync(req); + var successCode = HealthMonitor.LogOnException(resp, () => SteamStatusCode.Translate(resp, out _)); + + return successCode.Equals(SteamStatusCode.Ok); + + } + + + public static async Task SendMultipleConfirmations(HttpClient client, IEnumerable confirmations, + long steamId, MobileData data, bool confirm) + { + var op = confirm ? "allow" : "cancel"; + var query = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, op).ToList(); + query.Insert(0, new KeyValuePair("op", op)); + + foreach (var confirmation in confirmations) + { + query.Add(new KeyValuePair("cid[]", confirmation.Id.ToString())); + query.Add(new KeyValuePair("ck[]", confirmation.Nonce.ToString())); + } + + var content = new FormUrlEncodedContent(query); + var resp = await client.PostAsync(MULTI_CONF_OP, content); + var respStr = await resp.Content.ReadAsStringAsync(); + var successCode = HealthMonitor.LogOnException(respStr, () => SteamStatusCode.Translate(respStr, out _)); + return successCode.Equals(SteamStatusCode.Ok); + + } + + internal static IEnumerable> GetConfirmationKvp(long steamId, string deviceId, string identitySecret, string tag = "conf") + { + var time = TimeAligner.GetSteamTime(); + var hash = EncryptionHelper.GenerateConfirmationHash(time, identitySecret, tag); + return new KeyValuePair[] + { + new("p", deviceId), + new("a", steamId.ToString()), + new("k", hash), + new("t", time.ToString()), + new("m", "react"), + new("tag", tag) + }; + } +} \ No newline at end of file diff --git a/SteamLibForked/Api/SteamGlobalApi.cs b/SteamLibForked/Api/SteamGlobalApi.cs new file mode 100644 index 0000000..39f9421 --- /dev/null +++ b/SteamLibForked/Api/SteamGlobalApi.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; +using SteamLib.Authentication; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.StatusCodes; +using SteamLib.Exceptions; + +namespace SteamLib.Api; + +public static class SteamGlobalApi +{ + /// + /// Raw AccessToken value + /// + /// + /// + /// + /// + /// + public static async Task RefreshJwt(HttpClient client, SteamDomain domain, CancellationToken cancellationToken = default) + { + var domainUri = SteamDomains.GetDomain(domain); + var data = new Dictionary + { + {"redir", domainUri} + }; + var cont = new FormUrlEncodedContent(data); + var resp = await client.PostAsync("https://login.steampowered.com/jwt/ajaxrefresh", cont, cancellationToken); + var respStr = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(cancellationToken); + var jwtRefresh = JsonConvert.DeserializeObject(respStr); + if (jwtRefresh?.Success != true) + { + Exception? inner = null; + if (jwtRefresh?.Error != null) + { + var errorCode = jwtRefresh.Error.Value; + var translated = SteamStatusCode.Translate(errorCode, out _); + inner = new SteamStatusCodeException(translated, respStr); + } + throw new SessionInvalidException("AjaxRefresh was not successful. 'steamRefresh_steam' cookie is missing or refresh token is invalid", inner); + + } + + data = new Dictionary() + { + {"steamID", jwtRefresh.SteamId.ToString()}, + {"nonce", jwtRefresh.Nonce}, + {"redir", jwtRefresh.Redir}, + {"auth", jwtRefresh.Auth}, + }; + + cont = new FormUrlEncodedContent(data); + var update = await client.PostAsync(jwtRefresh.LoginUrl, cont, cancellationToken); + var updateResp = await update.Content.ReadAsStringAsync(cancellationToken); + if (updateResp != "{\"result\":1}") + { + throw new SessionInvalidException( + "AjaxRefresh (set-token) response was not successful. Response string stored in Exception.Data") + { + Data = {{"Response", updateResp}} + }; + } + return SteamTokenHelper.ExtractJwtFromSetCookiesHeader(update.Headers); + } + + + private class JwtRefreshJson + { + [JsonProperty("success")] public bool Success { get; set; } + [JsonProperty("login_url")] public string LoginUrl { get; set; } = string.Empty; + [JsonProperty("steamID")] public long SteamId { get; set; } + [JsonProperty("nonce")] public string Nonce { get; set; } = string.Empty; + [JsonProperty("redir")] public string Redir { get; set; } = string.Empty; + [JsonProperty("auth")] public string Auth { get; set; } = string.Empty; + [JsonProperty("error")] public int? Error { get; set; } + } +} + diff --git a/SteamLibForked/Authentication/AdmissionHelper.cs b/SteamLibForked/Authentication/AdmissionHelper.cs new file mode 100644 index 0000000..0408494 --- /dev/null +++ b/SteamLibForked/Authentication/AdmissionHelper.cs @@ -0,0 +1,176 @@ +using AchiesUtilities.Extensions; +using JetBrains.Annotations; +using SteamLib.Account; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using System.Net; + +namespace SteamLib.Authentication; + +[PublicAPI] +public static class AdmissionHelper +{ + public const string ACCESS_COOKIE_NAME = "steamLoginSecure"; + public const string REFRESH_COOKIE_NAME = "steamRefresh_steam"; + public const string LANGUAGE_COOKIE_NAME = "Steam_Language"; + + #region Main + + /// + /// Clear and set new session + /// + public static void SetSteamCookies(this CookieContainer container, ISessionData sessionData, string setLanguage = "english") + { + container.ClearSteamCookies(setLanguage); + + + AddRefreshToken(container, sessionData.RefreshToken); + + var community = SteamDomains.GetDomainUri(SteamDomain.Community); + container.Add(community, new Cookie("sessionid", sessionData.SessionId, "/")); + container.Add(community, new Cookie("Steam_Language", setLanguage, "/")); + TransferCommunityCookies(container); + foreach (var domain in SteamDomains.AllDomains) + { + var token = sessionData.GetToken(domain); + if (token == null) continue; + AddTokenCookie(container, token.Value); + } + } + + public static void SetDomainCookie(this CookieContainer container, SteamDomain domain, SteamAuthToken token) + { + var uri = SteamDomains.GetDomainUri(domain); + foreach (var cookie in container.GetCookies(uri).Where(c => c.Expired == false && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME))) + { + cookie.Expired = true; + } + + AddTokenCookie(container, token); + } + + + /// + /// Clear and set new session + /// + public static void SetSteamMobileCookies(this CookieContainer container, IMobileSessionData mobileSession, + string setLanguage = "english") + { + + container.ClearSteamCookies(setLanguage); + container.AddMinimalMobileCookies(); + + AddRefreshToken(container, mobileSession.RefreshToken); + + var community = SteamDomains.GetDomainUri(SteamDomain.Community); + container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString())); + container.Add(community, new Cookie("sessionid", mobileSession.SessionId)); + container.Add(community, new Cookie("Steam_Language", setLanguage)); + TransferCommunityCookies(container); + + foreach (var domain in SteamDomains.AllDomains) + { + var token = mobileSession.GetToken(domain); + if (token == null) continue; + AddTokenCookie(container, token.Value); + } + } + + + public static void AddMinimalMobileCookies(this CookieContainer container) + { + var community = SteamDomains.GetDomainUri(SteamDomain.Community); + container.Add(community, new Cookie("mobileClientVersion", "777777 3.6.1")); + container.Add(community, new Cookie("mobileClient", "android")); + } + + #endregion + + #region Clear + public static void ClearSteamCookies(this CookieContainer container, string setLanguage = "english") + { + var cookies = container.GetAllCookies().Where(IsSteamCookie).ToList(); + foreach (var cookie in cookies) + { + cookie.Expired = true; + } + container.Add(SteamDomains.GetDomainUri(SteamDomain.Community), new Cookie(LANGUAGE_COOKIE_NAME, setLanguage)); + TransferCommunityCookies(container); + } + + public static void ClearMobileSessionCookies(this CookieContainer container, string setLanguage = "english") + { + container.ClearSteamCookies(setLanguage); + container.AddMinimalMobileCookies(); + TransferCommunityCookies(container); + } + + + #endregion + + #region Helpers + public static void TransferCommunityCookies(CookieContainer container) + { + var cookies = container.GetAllCookies(); + + + foreach (Cookie cookie in cookies) + { + if (cookie.Domain.Contains("steamcommunity.com") == false || cookie.Expired || cookie.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)) continue; + + + container.Add(SteamDomains.GetDomainUri(SteamDomain.Store), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly }); + container.Add(SteamDomains.GetDomainUri(SteamDomain.Help), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly }); + container.Add(SteamDomains.GetDomainUri(SteamDomain.TV), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly }); + } + } + + public static void AddRefreshToken(CookieContainer container, SteamAuthToken token) + { + if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) + throw new ArgumentException($"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}", nameof(token)); + var refreshToken = token.SignedToken; + container.Add(SteamDomains.LoginSteamDomain, new Cookie(REFRESH_COOKIE_NAME, refreshToken) + { + Expires = token.Expires.ToLocalDateTime() + }); + } + + public static void AddTokenCookie(CookieContainer container, SteamAuthToken token) + { + if (token.Type is not SteamAccessTokenType.AccessToken) + throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}", nameof(token)); + + AddTokenCookie(container, token.Domain, token); + } + private static void AddTokenCookie(CookieContainer container, SteamDomain domain, SteamAuthToken token) + { + var domainUri = SteamDomains.GetDomainUri(token.Domain); + container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken) + { + HttpOnly = true, + Secure = true, + Expires = token.Expires.ToLocalDateTime() + }); + } + + + public static bool IsSteamCookie(Cookie cookie) => + cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") || cookie.Domain.Contains("steam.tv"); + + + public static string? GetSessionId(this CookieContainer container, string domain = "steamcommunity.com") + { + var cookies = container.GetAllCookies(); + return cookies + .FirstOrDefault(c => c.Name.Equals("sessionid", StringComparison.InvariantCultureIgnoreCase) + && c.Expired == false + && c.Domain.Contains(domain, StringComparison.InvariantCultureIgnoreCase))? + .Value; + } + + + #endregion + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginExecutor.cs b/SteamLibForked/Authentication/Default/LoginExecutor.cs new file mode 100644 index 0000000..01cef5c --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginExecutor.cs @@ -0,0 +1,108 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Authentication; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; + +namespace SteamLib.Login.Default; + +[Obsolete("Method removed from Steam")] +public class LoginExecutor +{ + public static ILoginConsumer NullConsumer { get; } = new NullLoginConsumer(); + public ILoginConsumer Caller { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ICaptchaResolver? CaptchaResolver { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + + private LoginExecutor(LoginExecutorOptions options) + { + Caller = options.Consumer; + HttpClient = options.HttpClient; + Logger = options.Logger; + EmailAuthProvider = options.EmailAuthProvider; + SteamGuardProvider = options.SteamGuardProvider; + CaptchaResolver = options.CaptchaResolver; + } + + + public static async Task DoLogin(LoginExecutorOptions options, string username, string password, CancellationToken cancellationToken = default) //TODO: logs + { + var executor = new LoginExecutor(options); + var client = executor.HttpClient; + + LoginStage loginStage = new GetRsaStage(username); + loginStage = await ((GetRsaStage)loginStage).Proceed(client, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + if (loginStage is ReadyToLoginStage rdyToLoginStage) + { + loginStage = await rdyToLoginStage.Proceed(client, password, string.Empty, cancellationToken); + } + cancellationToken.ThrowIfCancellationRequested(); + var twoFactorTry = 0; + var emailTry = 0; + while (loginStage.GetType() != typeof(LoginErrorStage) && loginStage is not LoginSuccessStage) + { + cancellationToken.ThrowIfCancellationRequested(); + switch (loginStage) + { + case CaptchaNeededStage captchaNeededStage: + { + if (executor.CaptchaResolver == null) throw new LoginException(LoginError.CaptchaRequired); + var captchaText = await executor.CaptchaResolver.Resolve(captchaNeededStage.CaptchaImage, client); + loginStage = await captchaNeededStage.Proceed(client, captchaText, cancellationToken); + break; + } + case EmailAuthStage emailAuthStage: + { + if (executor.EmailAuthProvider == null) throw new LoginException(LoginError.EmailAuthRequired); + if (emailTry > executor.EmailAuthProvider.MaxRetryCount) throw new LoginException(LoginError.InvalidEmailAuthCode); + var code = await executor.EmailAuthProvider.GetEmailAuthCode(executor.Caller); + loginStage = await emailAuthStage.Proceed(client, code, cancellationToken); + emailTry++; + break; + } + case TwoFactorStage twoFactorStage: + { + if (executor.SteamGuardProvider == null) throw new LoginException(LoginError.SteamGuardRequired); + if (twoFactorTry > executor.SteamGuardProvider.MaxRetryCount) throw new LoginException(LoginError.InvalidTwoFactorCode); + var twoFactor = await executor.SteamGuardProvider.GetSteamGuardCode(executor.Caller); + loginStage = await twoFactorStage.Proceed(client, twoFactor, cancellationToken); + twoFactorTry++; + break; + } + case ReadyToLoginStage readyToLoginStage: //When captcha proceeded, stage goes there + { + loginStage = await readyToLoginStage.Proceed(client, password, string.Empty, cancellationToken); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(loginStage)); + } + } + if (loginStage is LoginErrorStage error) + { + var content = await error.Response.Content.ReadAsStringAsync(cancellationToken); + if (content.Contains("The account name or password that you have entered is incorrect")) + { + throw new LoginException(LoginError.InvalidCredentials); + } + throw new LoginException(content); + } + + + if (loginStage is not LoginSuccessStage successStage) + { + throw new InvalidOperationException("Unexpected login stage at this point. " + loginStage.GetType()) + { + Data = { { "stage", loginStage } } + }; + } + + return successStage.TransferParameters; + + } + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginExecutorOptions.cs b/SteamLibForked/Authentication/Default/LoginExecutorOptions.cs new file mode 100644 index 0000000..a77393f --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginExecutorOptions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Core.Interfaces; + +namespace SteamLib.Login.Default; + + +public class LoginExecutorOptions +{ + public ILoginConsumer Consumer { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ICaptchaResolver? CaptchaResolver { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + public LoginExecutorOptions(ILoginConsumer consumer, HttpClient httpClient) + { + Consumer = consumer; + HttpClient = httpClient; + } + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginResultJson.cs b/SteamLibForked/Authentication/Default/LoginResultJson.cs new file mode 100644 index 0000000..bddae65 --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginResultJson.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace SteamLib.Login.Default; + +#pragma warning disable CS8618 +/// +/// Class to Deserialize the json response strings after the login/> +/// +internal class LoginResultJson +{ + [JsonProperty("success")] public bool Success { get; set; } + [JsonProperty("message")] public string Message { get; set; } + [JsonProperty("captcha_needed")] public bool CaptchaNeeded { get; set; } + [JsonProperty("captcha_gid")] public string CaptchaGid { get; set; } + [JsonProperty("emailauth_needed")] public bool EmailAuthNeeded { get; set; } + [JsonProperty("emailsteamid")] public string EmailSteamId { get; set; } + [JsonProperty("requires_twofactor")] public bool RequiresTwoFactor { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginStage.cs b/SteamLibForked/Authentication/Default/LoginStage.cs new file mode 100644 index 0000000..22844c0 --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginStage.cs @@ -0,0 +1,227 @@ +using AchiesUtilities.Web.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamLib.Authentication; +using SteamLib.Utility; + +namespace SteamLib.Login.Default; + +internal abstract class LoginStage +{ + protected Dictionary Data { get; } + protected LoginStage() + { + Data = new Dictionary(); + } + + protected LoginStage(Dictionary data) + { + Data = data; + } + + protected bool CheckIfLoginCompleted(LoginResultJson json) + { + return !json.CaptchaNeeded && !json.EmailAuthNeeded && !json.RequiresTwoFactor && json.Success; + } + protected async Task ConvertJson(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + var webResponseString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(webResponseString)!; + } + protected async Task CompleteLogin(HttpResponseMessage response) + { + var jsonStr = await response.Content.ReadAsStringAsync(); + var json = JObject.Parse(jsonStr); + var transferParameters = json["transfer_parameters"]!.ToObject()!; + return new LoginSuccessStage(transferParameters); + } + protected async Task LoginRequest(HttpClient client, CancellationToken cancellationToken = default) + { + return await client.PostAsync("https://steamcommunity.com/login/dologin", new FormUrlEncodedContent(Data), cancellationToken); + } +} +internal class LoginErrorStage : LoginStage +{ + public readonly string ErrorString; + public readonly int ErrorCode; + public readonly LoginStage Stage; + public readonly HttpResponseMessage Response; + public LoginErrorStage(string errorString, HttpResponseMessage responseMessage, LoginStage stage) + { + ErrorString = errorString; + Stage = stage; + ErrorCode = Utilities.GetSuccessCode(responseMessage.Content.ReadAsStringSync()); + Response = responseMessage; + } +} +internal class GetRsaStage : LoginStage +{ + public GetRsaStage(Dictionary data) : base(data) { } + public GetRsaStage(string userName) + { + Data["username"] = userName; + } + + + /// + /// + /// + /// + /// + /// if success + public async Task Proceed(HttpClient client, CancellationToken cancellationToken = default) + { + var content = new FormUrlEncodedContent(Data); + var response = await client.PostAsync("https://steamcommunity.com/login/getrsakey", content, cancellationToken); + var responseString = await response.Content.ReadAsStringAsync(cancellationToken); + var rsaJson = JsonConvert.DeserializeObject(responseString)!; + if (!rsaJson.success) + return new LoginErrorStage("Can't get RSA", response, this); + + Data["rsatimestamp"] = rsaJson.timestamp; + return new ReadyToLoginStage(Data, rsaJson.publickey_exp, rsaJson.publickey_mod); + } + +} +internal class ReadyToLoginStage : LoginStage +{ + private readonly string _publicKeyExp; + private readonly string _publicKeyMod; + internal ReadyToLoginStage(Dictionary data, string publicKeyExp, string publicKeyMod) : base(data) + { + _publicKeyExp = publicKeyExp; + _publicKeyMod = publicKeyMod; + } + + public async Task Proceed(HttpClient client, string password, string loginFriendlyName, CancellationToken cancellationToken = default) + { + var encryptedBase64Password = EncryptionHelper.ToBase64EncryptedPassword(_publicKeyExp, _publicKeyMod, password); + var unixTimestamp = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + Data["password"] = encryptedBase64Password; + Data["remember_login"] = "true"; + Data["loginfriendlyname"] = loginFriendlyName; + Data["donotcache"] = unixTimestamp + "000"; // Added three "0"'s because Steam has a weird unix timestamp interpretation. + + _ = await client.GetAsync("https://steamcommunity.com/", cancellationToken); + var webResponse = await LoginRequest(client, cancellationToken); + var result = await ConvertJson(webResponse); + + if (CheckIfLoginCompleted(result)) + { + return await CompleteLogin(webResponse); + } + + if (result.CaptchaNeeded) + { + return new CaptchaNeededStage(Data, result.CaptchaGid); + } + + if (result.EmailAuthNeeded) + { + return new EmailAuthStage(Data); + } + + if (result.RequiresTwoFactor) + { + return new TwoFactorStage(Data); + } + + return new LoginErrorStage("Can't proceed login.", webResponse, this); + } +} +internal class TwoFactorStage : LoginStage +{ + internal TwoFactorStage(Dictionary data) : base(data) { } + public async Task Proceed(HttpClient client, string twoFactorCode, CancellationToken cancellationToken = default) + { + Data["twofactorcode"] = twoFactorCode; + var webResponse = await LoginRequest(client, cancellationToken); + var result = await ConvertJson(webResponse); + + if (CheckIfLoginCompleted(result)) + { + return await CompleteLogin(webResponse); + } + + if (result.CaptchaNeeded) + { + return new CaptchaNeededStage(Data, result.CaptchaGid); + } + + if (result.RequiresTwoFactor) + { + return new TwoFactorStage(Data); + } + + return new LoginErrorStage("Can't proceed login. Bad TwoFactor code", webResponse, this); + } + +} +internal class EmailAuthStage : LoginStage +{ + internal EmailAuthStage(Dictionary data) : base(data) { } + + public async Task Proceed(HttpClient client, string emailCode, CancellationToken cancellationToken = default) + { + Data["emailauth"] = emailCode; + var webResponse = await LoginRequest(client, cancellationToken); + var result = await ConvertJson(webResponse); + + if (CheckIfLoginCompleted(result)) + { + return await CompleteLogin(webResponse); + } + + if (result.CaptchaNeeded) + { + return new CaptchaNeededStage(Data, result.CaptchaGid); + } + + if (result.EmailAuthNeeded) + { + return new EmailAuthStage(Data); + } + + + return new LoginErrorStage("Can't proceed login. Bad Email code", webResponse, this); + } +} +internal class CaptchaNeededStage : LoginStage +{ + internal Uri CaptchaImage { get; } + public CaptchaNeededStage(Dictionary data, string captchaGid) : base(data) + { + captchaGid = Uri.EscapeDataString(captchaGid); + Data["captchagid"] = captchaGid; + CaptchaImage = new Uri("https://steamcommunity.com/login/rendercaptcha?gid=" + captchaGid); + } + + /// + /// + /// + /// + /// + /// + /// or + public async Task Proceed(HttpClient client, string captchaText, CancellationToken cancellationToken = default) + { + Data["captcha_text"] = captchaText; + //When captcha required we need to do login from start + + var rsaStage = new GetRsaStage(Data); + var res = await rsaStage.Proceed(client, cancellationToken); + + return res; + } +} +internal class LoginSuccessStage : LoginStage +{ + public TransferParameters TransferParameters { get; } + public LoginSuccessStage(TransferParameters transferParameters) + { + TransferParameters = transferParameters; + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/NullLoginConsumer.cs b/SteamLibForked/Authentication/Default/NullLoginConsumer.cs new file mode 100644 index 0000000..0d23b9f --- /dev/null +++ b/SteamLibForked/Authentication/Default/NullLoginConsumer.cs @@ -0,0 +1,8 @@ +using SteamLib.Core.Interfaces; + +namespace SteamLib.Login.Default; + +internal class NullLoginConsumer : ILoginConsumer +{ + public string FriendlyName { get; } = "null"; +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/RsaKeyJson.cs b/SteamLibForked/Authentication/Default/RsaKeyJson.cs new file mode 100644 index 0000000..a22af43 --- /dev/null +++ b/SteamLibForked/Authentication/Default/RsaKeyJson.cs @@ -0,0 +1,16 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +#pragma warning disable CS8618 + +namespace SteamLib.Login.Default; + +/// +/// Class to Deserialize the json response strings of the getResKey request/> +/// +internal class RsaKeyJson +{ + public bool success { get; set; } + public string publickey_mod { get; set; } + public string publickey_exp { get; set; } + public string timestamp { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs b/SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs new file mode 100644 index 0000000..bfe479c --- /dev/null +++ b/SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace SteamLib.Authentication.LoginV2; + +class FinalizeLoginJson +{ + [JsonProperty("steamID")] + public ulong SteamId { get; set; } + + [JsonProperty("transfer_info")] + public List TransferInfo { get; set; } +} + +class TransferInfo +{ + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("params")] + public TransferInfoParams TransferInfoParams { get; set; } +} + + +class TransferInfoParams +{ + [JsonProperty("nonce")] + public string Nonce { get; set; } + + [JsonProperty("auth")] + public string Auth { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs b/SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs new file mode 100644 index 0000000..ee5c35f --- /dev/null +++ b/SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs @@ -0,0 +1,246 @@ +using AchiesUtilities.Web.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamLib.Account; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Core.StatusCodes; +using SteamLib.Exceptions; +using SteamLib.Login.Default; +using SteamLib.ProtoCore; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Services; +using SteamLib.Utility; +using SteamLib.Web; + +namespace SteamLib.Authentication.LoginV2; + +public class LoginV2Executor +{ + public static ILoginConsumer NullConsumer { get; } = new NullLoginConsumer(); + public ILoginConsumer Caller { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ICaptchaResolver? CaptchaResolver { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + public string WebsiteId { get; init; } + public DeviceDetails DeviceDetails { get; init; } + private LoginV2Executor(LoginV2ExecutorOptions options) + { + Caller = options.Consumer; + HttpClient = options.HttpClient; + Logger = options.Logger; + EmailAuthProvider = options.EmailAuthProvider; + SteamGuardProvider = options.SteamGuardProvider; + WebsiteId = options.GetWebsiteIdOrDefault(); + DeviceDetails = options.DeviceDetails ?? DeviceDetails.CreateDefault(); + } + + + /// + /// Do login on SteamCommunity.
+ /// Some functions requires proper SessionId. But contains only SteamCommunity related SessionId and some functions on other services may not work + ///
+ /// + /// + /// + /// + /// or depending on which token type is returned + /// + /// + /// + /// + public static async Task DoLogin(LoginV2ExecutorOptions options, string username, string password, CancellationToken cancellationToken = default) //TODO: logs + { + var executor = new LoginV2Executor(options); + var client = executor.HttpClient; + + var globalData = await SteamWebApi.GetMarketGlobalInfo(client, cancellationToken); + var sessionId = globalData.SessionId; + + var rsgMsg = new GetPasswordRSAPublicKey_Request + { + AccountName = username + }; + var rsaResp = await client.GetProto( + "https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1", rsgMsg, cancellationToken: cancellationToken); + + + var encodedPassword = EncryptionHelper.ToBase64EncryptedPassword(rsaResp.PublickKeyExp, rsaResp.PublickKeyMod, password); + + + var beginAuthMsg = new BeginAuthSessionViaCredentials_Request + { + DeviceFriendlyName = string.Empty, + AccountName = username, + EncryptedPassword = encodedPassword, + EncryptionTimestamp = rsaResp.Timestamp, + RememberLogin = true, + PlatformType = executor.DeviceDetails.PlatformType, + Persistence = 1, + WebsiteId = executor.WebsiteId, + DeviceDetails = executor.DeviceDetails, + }; + + BeginAuthSessionViaCredentials_Response beginAuthResp; + try + { + beginAuthResp = await client.PostProto( + "https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1", beginAuthMsg, cancellationToken: cancellationToken); + + } + catch (EResultException ex) + when (ex.Result == EResult.InvalidPassword) + { + throw new LoginException(LoginError.InvalidCredentials); + } + + + var clientId = beginAuthResp.ClientId; + var steamId = (long)beginAuthResp.Steamid; + + var conf = beginAuthResp.AllowedConfirmations.FirstOrDefault(c => + c.ConfirmationType is EAuthSessionGuardType.EmailCode or EAuthSessionGuardType.DeviceCode); + + conf ??= beginAuthResp.AllowedConfirmations.FirstOrDefault(); + + switch (conf?.ConfirmationType) + { + case EAuthSessionGuardType.None: + break; + case EAuthSessionGuardType.DeviceCode: + case EAuthSessionGuardType.EmailCode: + await UpdateWithCode(executor, clientId, (ulong)steamId, conf.ConfirmationType); + break; + case EAuthSessionGuardType.Unknown: + case EAuthSessionGuardType.DeviceConfirmation: + case EAuthSessionGuardType.EmailConfirmation: + case EAuthSessionGuardType.MachineToken: + case EAuthSessionGuardType.LegacyMachineAuth: + throw new NotSupportedException( + $"Auth confirmation type of {conf.ConfirmationType} is not implemented yet"); + default: + throw new ArgumentOutOfRangeException(nameof(conf.ConfirmationType), conf?.ConfirmationType, "Unknown confirmation type or null"); + } + + var pollSessionMsg = new PollAuthSessionStatus_Request + { + ClientId = clientId, + RequestId = beginAuthResp.RequestId, + + }; + + var pollResp = + await client.PostProto( + "https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1", pollSessionMsg, cancellationToken: cancellationToken); + + SteamAuthToken refreshToken; + try + { + refreshToken = SteamTokenHelper.Parse(pollResp.RefreshToken); + if (refreshToken.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) + throw new ArgumentException("Refresh token must be of type Refresh or MobileRefresh. No 'renew' audience found in JWT.", + nameof(pollResp.RefreshToken)); //Argument exception for less code + } + catch (ArgumentException ex) + { + throw new LoginException(null, + "Steam returned invalid refresh token or it's schema was unsupported. See inner exception for more details", + LoginError.UndefinedError, ex); + } + + var data = new Dictionary + { + {"nonce", pollResp.RefreshToken}, + {"sessionid", sessionId} + }; + + var finalize = await client.PostAsync("https://login.steampowered.com/jwt/finalizelogin", new FormUrlEncodedContent(data), cancellationToken); + var finalizeContent = await finalize.EnsureSuccessStatusCode().Content.ReadAsStringAsync(cancellationToken); + + var finalizeResp = + HealthMonitor.LogOnException(finalizeContent, () => JsonConvert.DeserializeObject(finalizeContent)!); + + + List tokens = new(); + foreach (var transferInfo in finalizeResp.TransferInfo) + { + var transferData = new Dictionary + { + {"nonce", transferInfo.TransferInfoParams.Nonce}, + {"auth", transferInfo.TransferInfoParams.Auth}, + {"steamID", steamId.ToString()} + }; + + + try + { + var req = new HttpRequestMessage(HttpMethod.Post, transferInfo.Url); + req.Content = new FormUrlEncodedContent(transferData); + req.Headers.Referrer = SteamDomains.GetDomainUri(SteamDomain.Store); + var transferResp = await client.SendAsync(req, cancellationToken); + var transferContent = await transferResp.ReadAsStringEnsureSuccessAsync(cancellationToken); + var status = JObject.Parse(transferContent); + var result = status["result"]?.Value(); + if(result != null) + SteamStatusCode.ValidateSuccessOrThrow(result.Value); //TODO: Fix steam.tv token transfer (result always 8). But who really cares.. + + var tokenStr = SteamTokenHelper.ExtractJwtFromSetCookiesHeader(transferResp.Headers); + var token = SteamTokenHelper.Parse(tokenStr); + tokens.Add(token); + } + catch (Exception ex) + { + executor.Logger?.Log(LogLevel.Warning, ex, "Can't transfer tokens for URI: {uri}", transferInfo.Url); + } + } + + var accessToken = SteamTokenHelper.Parse(pollResp.AccessToken); + + if (accessToken.Type == SteamAccessTokenType.Mobile) + { + return new MobileSessionData(sessionId, SteamId.FromSteam64(steamId), refreshToken, accessToken, tokens); + } + else + { + return new SessionData(sessionId, SteamId.FromSteam64(steamId), refreshToken, tokens); + } + + + + } + + private static async Task UpdateWithCode(LoginV2Executor executor, ulong clientId, ulong steamId, EAuthSessionGuardType guardType) + { + string? code; + if (guardType == EAuthSessionGuardType.DeviceCode) + { + if(executor.SteamGuardProvider != null) + code = await executor.SteamGuardProvider.GetSteamGuardCode(executor.Caller); + else + { + throw new LoginException(LoginError.SteamGuardRequired); + } + } + else + { + var t = executor.EmailAuthProvider?.GetEmailAuthCode(executor.Caller) ?? + throw new LoginException(LoginError.EmailAuthRequired); + code = await t; + } + var updateCodeMsg = new UpdateAuthSessionWithSteamGuardCode_Request + { + ClientId = clientId, + Code = code, + CodeType = guardType, + Steamid = steamId + }; + + await executor.HttpClient.PostProtoEnsureSuccess(new Uri("https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1"), updateCodeMsg); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs b/SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs new file mode 100644 index 0000000..a2e1c53 --- /dev/null +++ b/SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Core.Interfaces; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Services; + +namespace SteamLib.Authentication.LoginV2; + +public class LoginV2ExecutorOptions +{ + public ILoginConsumer Consumer { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + public DeviceDetails? DeviceDetails { get; init; } + + /// + /// Gets or sets the website id that the login will be performed for. + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". + /// Default value will be set to "Community" + /// + /// The website id. + public string? WebsiteId { get; init; } + public LoginV2ExecutorOptions(ILoginConsumer consumer, HttpClient httpClient) + { + Consumer = consumer; + HttpClient = httpClient; + } + + + + public string GetWebsiteIdOrDefault() + { + return WebsiteId ?? "Community"; + } + + + public static DeviceDetails GetMobileDefaultDevice() //FORTEST + { + return new DeviceDetails("Pixel 6 Pro", EAuthTokenPlatformType.MobileApp, -500, 528); + } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/SessionDataValidator.cs b/SteamLibForked/Authentication/SessionDataValidator.cs new file mode 100644 index 0000000..4e6cdf8 --- /dev/null +++ b/SteamLibForked/Authentication/SessionDataValidator.cs @@ -0,0 +1,96 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using SteamLib.Account; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; + +namespace SteamLib.Authentication; + +public static class SessionDataValidator +{ + [PublicAPI] + public static Dictionary> Validators { get; } = new(); + public static ValidateOptionsResult Validate(string? name, ISessionData data) + { + if(Validators.TryGetValue(data.GetType(), out var validator)) + return validator.Validate(name, data); + + return Validate(data); + + } + + public static ValidateOptionsResult Validate(string? name, T data) where T : ISessionData + { + if (Validators.TryGetValue(typeof(T), out var validator)) + return validator.Validate(name, data); + + return Validate(data); + } + + + private static ValidateOptionsResult Validate(ISessionData data) + { + if (data == null!) + { + return ValidateOptionsResult.Fail("SessionData cannot be null"); + } + + if (string.IsNullOrWhiteSpace(data.SessionId)) + { + return ValidateOptionsResult.Fail("SessionId cannot be null or empty"); + } + + if (data.SteamId.Steam64.Id < SteamId64.SEED) + { + return ValidateOptionsResult.Fail($"SteamId '{data.SteamId.Steam64}' is invalid."); + } + + if (string.IsNullOrWhiteSpace(data.RefreshToken.Token)) + { + return ValidateOptionsResult.Fail("RefreshToken cannot be null or empty"); + } + + if (data.RefreshToken.SteamId.Steam64.Id < SteamId64.SEED) + { + return ValidateOptionsResult.Fail($"SteamId in RefreshToken '{data.SteamId.Steam64}' is invalid."); + } + + + if (data is IMobileSessionData && data.RefreshToken.Type != SteamAccessTokenType.MobileRefresh) + { + return ValidateOptionsResult.Fail($"RefreshToken '{data.RefreshToken.Token}' is not of type MobileRefresh"); + } + else if (data.RefreshToken.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) + { + return ValidateOptionsResult.Fail($"RefreshToken '{data.RefreshToken.Token}' is not of type Refresh or MobileRefresh"); + } + + if (data.RefreshToken.Expires.ToLong() == 0L) + { + return ValidateOptionsResult.Fail($"RefreshToken '{data.RefreshToken.Token}' has invalid expiration date"); + } + + + + return ValidateOptionsResult.Success; + } + public static bool IsValid(this ISessionData data) + { + return Validate(null, data).Succeeded; + } + + public static void EnsureValidated(this ISessionData data) + { + if(data.IsValid == true) return; + if (data.IsValid == false) + throw new SessionInvalidException( + "SessionData was validated before and validation was not passed. Session is invalid"); + var validation = Validate(null, data); + data.IsValid = validation.Succeeded; + if (validation.Failed) + { + throw new SessionInvalidException("SessionData is invalid. " + validation.FailureMessage); + } + } + } diff --git a/SteamLibForked/Authentication/SteamTokenHelper.cs b/SteamLibForked/Authentication/SteamTokenHelper.cs new file mode 100644 index 0000000..63fb5db --- /dev/null +++ b/SteamLibForked/Authentication/SteamTokenHelper.cs @@ -0,0 +1,184 @@ +using AchiesUtilities.Models; +using Microsoft.IdentityModel.JsonWebTokens; +using SteamLib.Account; +using SteamLib.Core.Enums; +using System.Collections.ObjectModel; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; + +namespace SteamLib.Authentication; + +public static class SteamTokenHelper +{ + public static readonly Regex SteamTokenRegex = + new(@"^(?(?7\d{16,})(?:%7C%7C))?(?.+)$", RegexOptions.Compiled); + + public static readonly IReadOnlyDictionary AudDomains = + new ReadOnlyDictionary(new Dictionary + { + {"web:community", SteamDomain.Community}, + {"web:store", SteamDomain.Store}, + {"web:help", SteamDomain.Help}, + {"web:steamtv", SteamDomain.TV}, + {"web:checkout", SteamDomain.Checkout}, + }); + + + public static bool TryParse(string input, out SteamAuthToken result) + { + return Parse(input, out result, true); + } + + public static SteamAuthToken Parse(string input) + { + if (Parse(input, out var result, false)) + { + return result; + } + else + { + throw new ArgumentException("Provided Token is not valid JWT token", nameof(input)); + } + } + + private static bool Parse(string input, out SteamAuthToken result, bool trying) + { + result = default; + var match = SteamTokenRegex.Match(input); + if (match.Success == false) + { + if (trying) return false; + throw new ArgumentException("Provided Token is not valid SteamLoginSecure or SteamAccess token"); + } + + + var jwtStr = match.Groups["jwt"].Value; + JsonWebToken jwt; + try + { + jwt = new JsonWebToken(jwtStr); + } + catch (Exception e) + { + if (trying) return false; + throw new ArgumentException("Provided Token is not valid JWT token", nameof(input), e); + } + + var steamIdStr = jwt.Subject; + if (steamIdStr == null || long.TryParse(steamIdStr, out var steamId) == false || steamId < SteamId64.SEED) + { + if (trying) return false; + throw new ArgumentException( + $"Provided Token is not valid JWT token. Subject claim is null or not valid SteamId '{steamIdStr}'", + nameof(input)); + } + + SteamId id; + try + { + id = SteamId.FromSteam64(steamId); + } + catch (Exception ex) + { + if (trying) return false; + throw new ArgumentException( + $"Provided Token is not valid JWT token. Subject is claim not valid SteamId '{steamIdStr}'", + nameof(input), ex); + } + + var exp = jwt.ValidTo; + if (exp == DateTime.MinValue) + { + if (trying) + return false; + + throw new ArgumentException("Provided Token has no 'exp' claim.", nameof(input)); + } + + var expires = UnixTimeStamp.FromDateTime(exp); + var domain = SteamDomain.Undefined; + var audiences = jwt.Audiences.ToList(); + var tokenType = SteamAccessTokenType.Unknown; + if (jwt.Audiences.ToList().Count > 0) + { + var aud = audiences[0]; + if (AudDomains.TryGetValue(aud, out domain) == false && aud != "web") + { + if (trying) + { + result = new SteamAuthToken(jwtStr, id, expires, domain, tokenType); + return false; + } + + throw new ArgumentException($"Provided Token has invalid 'aud' claim. Value: {aud}", nameof(input)); + } + tokenType = SteamAccessTokenType.AccessToken; + + if (aud == "web") + { + + + var isMobile = audiences.Any(a => a.Equals("mobile")); + var isRefresh = audiences.Any(a => a.Equals("renew")); + if (isMobile ^ isRefresh) + { + tokenType = isMobile ? SteamAccessTokenType.Mobile : SteamAccessTokenType.Refresh; + } + else if (isMobile && isRefresh) + { + tokenType = SteamAccessTokenType.MobileRefresh; + } + else if (audiences.Count == 1) + { + tokenType = SteamAccessTokenType.Web; + } + else + { + if (trying) + { + result = new SteamAuthToken(jwtStr, id, expires, domain, SteamAccessTokenType.Unknown); + return false; + } + var auds = string.Join(", ", audiences); + throw new ArgumentException($"Provided Token has invalid 'aud' claim combination. Values: {auds}", nameof(input)); + } + + } + } + + + result = new SteamAuthToken(jwtStr, id, expires, domain, tokenType); + return true; + } + + public static string ExtractJwtFromSetCookiesHeader(HttpHeaders headers) + { + var setCookies = headers.GetValues("Set-Cookie"); + var steamLoginSecureHeader = setCookies.FirstOrDefault(x => x.StartsWith("steamLoginSecure=")); + if (steamLoginSecureHeader == null) throw new ArgumentException("Can't find steamLoginSecure cookie in Set-Cookie header"); + + var firstCookiePart = steamLoginSecureHeader + .ToCharArray() + .SkipWhile(ch => !ch.Equals('=')) + .Skip(1) + .TakeWhile(ch => !ch.Equals(';')) + .ToArray(); + + var steamLoginSecure = new string(firstCookiePart); + return ExtractJwtToken(steamLoginSecure); + } + + public static string CombineJwtWithSteamId(long steamId, string loginValue) => steamId + "%7C%7C" + loginValue; + internal static bool CheckIfProperLoginValue(string value) => value.Contains("%7C%7C"); + internal static string CombineLoginValueIfNeeded(long steamId, string loginValue) + { + return CheckIfProperLoginValue(loginValue) == false ? CombineJwtWithSteamId(steamId, loginValue) : loginValue; + } + + public static string ExtractJwtToken(string steamLoginSecure) + { + var match = SteamTokenRegex.Match(steamLoginSecure); + if (match.Success == false) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure"); + return match.Groups["jwt"].Value; + } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs b/SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs new file mode 100644 index 0000000..324b246 --- /dev/null +++ b/SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace SteamLib.Authentication; + +#pragma warning disable CS8618 +public class OAuthData +{ + [JsonProperty("steamid")] public long SteamId { get; set; } + [JsonProperty("oauth_token")] public string OAuthToken { get; set; } + [JsonProperty("webcookie")] public string WebCookie { get; set; } + [JsonProperty("wgtoken")] public string SteamLogin { get; set; } + [JsonProperty("wgtoken_secure")] public string SteamLoginSecure { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs b/SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs new file mode 100644 index 0000000..168905a --- /dev/null +++ b/SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace SteamLib.Authentication; + +#pragma warning disable CS8618 +public class TransferParameters +{ + [JsonProperty("steamid")] + public long SteamId { get; set; } + + [JsonProperty("token_secure")] + public string TokenSecure { get; set; } + + [JsonProperty("auth")] + public string Auth { get; set; } + + [JsonProperty("remember_login")] + public bool RememberLogin { get; set; } + + [JsonProperty("webcookie")] + public string WebCookie { get; set; } + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Constants.cs b/SteamLibForked/Core/Constants.cs new file mode 100644 index 0000000..0c6e8dd --- /dev/null +++ b/SteamLibForked/Core/Constants.cs @@ -0,0 +1,36 @@ +namespace SteamLib.Core; + +public static class SteamConstants +{ + #region Base + + public const string STEAM_STORE = "https://store.steampowered.com/"; + public const string STEAM_COMMUNITY = "https://steamcommunity.com/"; + public const string STEAM_API = "https://api.steampowered.com/"; + public const string STEAM_HELP = "https://help.steampowered.com/"; + public const string STEAM_TV = "https://steam.tv/"; + public const string STEAM_CHECKOUT = "https://checkout.steampowered.com/"; + public const string LOGIN_STEAM = "https://login.steampowered.com/"; + + + #endregion + + #region Endpoints + + public const string STEAM_MARKET = STEAM_COMMUNITY + "market/"; + public const string STORE_API = STEAM_STORE + "api/"; + + #endregion + + + #region Misc + + public const string ENGLISH = "english"; + + #endregion + + + + + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/Country.cs b/SteamLibForked/Core/Enums/Country.cs new file mode 100644 index 0000000..92037bf --- /dev/null +++ b/SteamLibForked/Core/Enums/Country.cs @@ -0,0 +1,758 @@ +// ReSharper disable All + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace SteamLib.Core.Enums; + + +public enum Country +{ + + Afghanistan, + Aland_Islands, + Albania, + Algeria, + American_Samoa, + Andorra, + Angola, + Anguilla, + Antigua_and_Barbuda, + Argentina, + Armenia, + Aruba, + Australia, + Austria, + Azerbaijan, + Bahamas, + Bahrain, + Bangladesh, + Barbados, + Belarus, + Belgium, + Belize, + Bhutan, + Bolivia, + Bosnia_and_Herzegovina, + Botswana, + Brazil, + Brunei_Darussalam, + Bulgaria, + Burundi, + Cambodia, + Cameroon, + Canada, + Cape_Verde, + Cayman_Islands, + Chad, + Chile, + China, + Colombia, + Congo, + Costa_Rica, + Cote_d_Ivoire, + Croatia, + Cyprus, + Czech_Republic, + Denmark, + Dominica, + Dominican_Republic, + Ecuador, + Egypt, + El_Salvador, + Estonia, + Eswatini, + Ethiopia, + Faroe_Islands, + Finland, + France, + French_Guiana, + French_Polynesia, + Gabon, + Georgia, + Germany, + Ghana, + Gibraltar, + Greece, + Greenland, + Grenada, + Guadeloupe, + Guam, + Guatemala, + Guernsey, + Guyana, + Haiti, + Honduras, + Hong_Kong, + Hungary, + Iceland, + India, + Indonesia, + Iran, + Iraq, + Ireland, + Isle_of_Man, + Israel, + Italy, + Jamaica, + Japan, + Jersey, + Jordan, + Kazakhstan, + Kenya, + Korea, + Kuwait, + Kyrgyzstan, + Laos, + Latvia, + Lebanon, + Libya, + Liechtenstein, + Lithuania, + Luxembourg, + Macau, + Macedonia, + Madagascar, + Malaysia, + Maldives, + Mali, + Malta, + Martinique, + Mauritius, + Mayotte, + Mexico, + Moldova, + Monaco, + Mongolia, + Montenegro, + Morocco, + Mozambique, + Myanmar, + Namibia, + Nepal, + Netherlands, + New_Zealand, + Nicaragua, + Nigeria, + Northern_Mariana_Islands, + Norway, + Oman, + Pakistan, + Palestinian_Territory, + Panama, + Papua_New_Guinea, + Paraguay, + Peru, + Philippines, + Poland, + Portugal, + Puerto_Rico, + Qatar, + Reunion, + Romania, + Russian_Federation, + Rwanda, + Saint_Kitts_and_Nevis, + Saint_Lucia, + Saint_Vincent_and_the_Grenadines, + Saudi_Arabia, + Senegal, + Serbia, + Seychelles, + Singapore, + Slovakia, + Slovenia, + South_Africa, + Spain, + Sri_Lanka, + Sudan, + Suriname, + Sweden, + Switzerland, + Taiwan, + Tajikistan, + Tanzania, + Thailand, + Togo, + Trinidad_and_Tobago, + Tunisia, + Turkey, + Turkmenistan, + Turks_and_Caicos_Islands, + Uganda, + Ukraine, + United_Arab_Emirates, + United_Kingdom, + United_States, + Uruguay, + Uzbekistan, + Venezuela, + Viet_Nam, + Virgin_Islands, + Yemen, + Zambia, + Zimbabwe, + +} +public static class CountryResolver +{ + public static IReadOnlyDictionary ResolveString { get; } = new Dictionary + { + { Country.Afghanistan , "Afghanistan" }, + { Country.Aland_Islands , "Aland Islands" }, + { Country.Albania , "Albania" }, + { Country.Algeria , "Algeria" }, + { Country.American_Samoa , "American Samoa" }, + { Country.Andorra , "Andorra" }, + { Country.Angola , "Angola" }, + { Country.Anguilla , "Anguilla" }, + { Country.Antigua_and_Barbuda , "Antigua and Barbuda" }, + { Country.Argentina , "Argentina" }, + { Country.Armenia , "Armenia" }, + { Country.Aruba , "Aruba" }, + { Country.Australia , "Australia" }, + { Country.Austria , "Austria" }, + { Country.Azerbaijan , "Azerbaijan" }, + { Country.Bahamas , "Bahamas" }, + { Country.Bahrain , "Bahrain" }, + { Country.Bangladesh , "Bangladesh" }, + { Country.Barbados , "Barbados" }, + { Country.Belarus , "Belarus" }, + { Country.Belgium , "Belgium" }, + { Country.Belize , "Belize" }, + { Country.Bhutan , "Bhutan" }, + { Country.Bolivia , "Bolivia" }, + { Country.Bosnia_and_Herzegovina , "Bosnia and Herzegovina" }, + { Country.Botswana , "Botswana" }, + { Country.Brazil , "Brazil" }, + { Country.Brunei_Darussalam , "Brunei Darussalam" }, + { Country.Bulgaria , "Bulgaria" }, + { Country.Burundi , "Burundi" }, + { Country.Cambodia , "Cambodia" }, + { Country.Cameroon , "Cameroon" }, + { Country.Canada , "Canada" }, + { Country.Cape_Verde , "Cape Verde" }, + { Country.Cayman_Islands , "Cayman Islands" }, + { Country.Chad , "Chad" }, + { Country.Chile , "Chile" }, + { Country.China , "China" }, + { Country.Colombia , "Colombia" }, + { Country.Congo , "Congo, the Democratic Republic of the" }, + { Country.Costa_Rica , "Costa Rica" }, + { Country.Cote_d_Ivoire , "Cote d'Ivoire" }, + { Country.Croatia , "Croatia" }, + { Country.Cyprus , "Cyprus" }, + { Country.Czech_Republic , "Czech Republic" }, + { Country.Denmark , "Denmark" }, + { Country.Dominica , "Dominica" }, + { Country.Dominican_Republic , "Dominican Republic" }, + { Country.Ecuador , "Ecuador" }, + { Country.Egypt , "Egypt" }, + { Country.El_Salvador , "El Salvador" }, + { Country.Estonia , "Estonia" }, + { Country.Eswatini , "Eswatini" }, + { Country.Ethiopia , "Ethiopia" }, + { Country.Faroe_Islands , "Faroe Islands" }, + { Country.Finland , "Finland" }, + { Country.France , "France" }, + { Country.French_Guiana , "French Guiana" }, + { Country.French_Polynesia , "French Polynesia" }, + { Country.Gabon , "Gabon" }, + { Country.Georgia , "Georgia" }, + { Country.Germany , "Germany" }, + { Country.Ghana , "Ghana" }, + { Country.Gibraltar , "Gibraltar" }, + { Country.Greece , "Greece" }, + { Country.Greenland , "Greenland" }, + { Country.Grenada , "Grenada" }, + { Country.Guadeloupe , "Guadeloupe" }, + { Country.Guam , "Guam" }, + { Country.Guatemala , "Guatemala" }, + { Country.Guernsey , "Guernsey" }, + { Country.Guyana , "Guyana" }, + { Country.Haiti , "Haiti" }, + { Country.Honduras , "Honduras" }, + { Country.Hong_Kong , "Hong Kong" }, + { Country.Hungary , "Hungary" }, + { Country.Iceland , "Iceland" }, + { Country.India , "India" }, + { Country.Indonesia , "Indonesia" }, + { Country.Iran , "Iran" }, + { Country.Iraq , "Iraq" }, + { Country.Ireland , "Ireland" }, + { Country.Isle_of_Man , "Isle of Man" }, + { Country.Israel , "Israel" }, + { Country.Italy , "Italy" }, + { Country.Jamaica , "Jamaica" }, + { Country.Japan , "Japan" }, + { Country.Jersey , "Jersey" }, + { Country.Jordan , "Jordan" }, + { Country.Kazakhstan , "Kazakhstan" }, + { Country.Kenya , "Kenya" }, + { Country.Korea , "Korea, Republic of" }, + { Country.Kuwait , "Kuwait" }, + { Country.Kyrgyzstan , "Kyrgyzstan" }, + { Country.Laos , "Lao People's Democratic Republic" }, + { Country.Latvia , "Latvia" }, + { Country.Lebanon , "Lebanon" }, + { Country.Libya , "Libya" }, + { Country.Liechtenstein , "Liechtenstein" }, + { Country.Lithuania , "Lithuania" }, + { Country.Luxembourg , "Luxembourg" }, + { Country.Macau , "Macau" }, + { Country.Macedonia , "Macedonia, the former Yugoslav Republic of" }, + { Country.Madagascar , "Madagascar" }, + { Country.Malaysia , "Malaysia" }, + { Country.Maldives , "Maldives" }, + { Country.Mali , "Mali" }, + { Country.Malta , "Malta" }, + { Country.Martinique , "Martinique" }, + { Country.Mauritius , "Mauritius" }, + { Country.Mayotte , "Mayotte" }, + { Country.Mexico , "Mexico" }, + { Country.Moldova , "Moldova, Republic of" }, + { Country.Monaco , "Monaco" }, + { Country.Mongolia , "Mongolia" }, + { Country.Montenegro , "Montenegro" }, + { Country.Morocco , "Morocco" }, + { Country.Mozambique , "Mozambique" }, + { Country.Myanmar , "Myanmar" }, + { Country.Namibia , "Namibia" }, + { Country.Nepal , "Nepal" }, + { Country.Netherlands , "Netherlands" }, + { Country.New_Zealand , "New Zealand" }, + { Country.Nicaragua , "Nicaragua" }, + { Country.Nigeria , "Nigeria" }, + { Country.Northern_Mariana_Islands , "Northern Mariana Islands" }, + { Country.Norway , "Norway" }, + { Country.Oman , "Oman" }, + { Country.Pakistan , "Pakistan" }, + { Country.Palestinian_Territory , "Palestinian Territory, Occupied" }, + { Country.Panama , "Panama" }, + { Country.Papua_New_Guinea , "Papua New Guinea" }, + { Country.Paraguay , "Paraguay" }, + { Country.Peru , "Peru" }, + { Country.Philippines , "Philippines" }, + { Country.Poland , "Poland" }, + { Country.Portugal , "Portugal" }, + { Country.Puerto_Rico , "Puerto Rico" }, + { Country.Qatar , "Qatar" }, + { Country.Reunion , "Reunion" }, + { Country.Romania , "Romania" }, + { Country.Russian_Federation , "Russian Federation" }, + { Country.Rwanda , "Rwanda" }, + { Country.Saint_Kitts_and_Nevis , "Saint Kitts and Nevis" }, + { Country.Saint_Lucia , "Saint Lucia" }, + { Country.Saint_Vincent_and_the_Grenadines , "Saint Vincent and the Grenadines" }, + { Country.Saudi_Arabia , "Saudi Arabia" }, + { Country.Senegal , "Senegal" }, + { Country.Serbia , "Serbia" }, + { Country.Seychelles , "Seychelles" }, + { Country.Singapore , "Singapore" }, + { Country.Slovakia , "Slovakia" }, + { Country.Slovenia , "Slovenia" }, + { Country.South_Africa , "South Africa" }, + { Country.Spain , "Spain" }, + { Country.Sri_Lanka , "Sri Lanka" }, + { Country.Sudan , "Sudan" }, + { Country.Suriname , "Suriname" }, + { Country.Sweden , "Sweden" }, + { Country.Switzerland , "Switzerland" }, + { Country.Taiwan , "Taiwan" }, + { Country.Tajikistan , "Tajikistan" }, + { Country.Tanzania , "Tanzania, United Republic of" }, + { Country.Thailand , "Thailand" }, + { Country.Togo , "Togo" }, + { Country.Trinidad_and_Tobago , "Trinidad and Tobago" }, + { Country.Tunisia , "Tunisia" }, + { Country.Turkey , "Turkey" }, + { Country.Turkmenistan , "Turkmenistan" }, + { Country.Turks_and_Caicos_Islands , "Turks and Caicos Islands" }, + { Country.Uganda , "Uganda" }, + { Country.Ukraine , "Ukraine" }, + { Country.United_Arab_Emirates , "United Arab Emirates" }, + { Country.United_Kingdom , "United Kingdom" }, + { Country.United_States , "United States" }, + { Country.Uruguay , "Uruguay" }, + { Country.Uzbekistan , "Uzbekistan" }, + { Country.Venezuela , "Venezuela" }, + { Country.Viet_Nam , "Viet Nam" }, + { Country.Virgin_Islands , "Virgin Islands, U.S." }, + { Country.Yemen , "Yemen" }, + { Country.Zambia , "Zambia" }, + { Country.Zimbabwe , "Zimbabwe" }, + }; + public static IReadOnlyDictionary ResolveCountry { get; } = new Dictionary() + { + {"Afghanistan", Country.Afghanistan}, + {"Aland Islands", Country.Aland_Islands}, + {"Albania", Country.Albania}, + {"Algeria", Country.Algeria}, + {"American Samoa", Country.American_Samoa}, + {"Andorra", Country.Andorra}, + {"Angola", Country.Angola}, + {"Anguilla", Country.Anguilla}, + {"Antigua and Barbuda", Country.Antigua_and_Barbuda}, + {"Argentina", Country.Argentina}, + {"Armenia", Country.Armenia}, + {"Aruba", Country.Aruba}, + {"Australia", Country.Australia}, + {"Austria", Country.Austria}, + {"Azerbaijan", Country.Azerbaijan}, + {"Bahamas", Country.Bahamas}, + {"Bahrain", Country.Bahrain}, + {"Bangladesh", Country.Bangladesh}, + {"Barbados", Country.Barbados}, + {"Belarus", Country.Belarus}, + {"Belgium", Country.Belgium}, + {"Belize", Country.Belize}, + {"Bhutan", Country.Bhutan}, + {"Bolivia", Country.Bolivia}, + {"Bosnia and Herzegovina", Country.Bosnia_and_Herzegovina}, + {"Botswana", Country.Botswana}, + {"Brazil", Country.Brazil}, + {"Brunei Darussalam", Country.Brunei_Darussalam}, + {"Bulgaria", Country.Bulgaria}, + {"Burundi", Country.Burundi}, + {"Cambodia", Country.Cambodia}, + {"Cameroon", Country.Cameroon}, + {"Canada", Country.Canada}, + {"Cape Verde", Country.Cape_Verde}, + {"Cayman Islands", Country.Cayman_Islands}, + {"Chad", Country.Chad}, + {"Chile", Country.Chile}, + {"China", Country.China}, + {"Colombia", Country.Colombia}, + {"Congo, the Democratic Republic of the", Country.Congo}, + {"Costa Rica", Country.Costa_Rica}, + {"Cote d'Ivoire", Country.Cote_d_Ivoire}, + {"Croatia", Country.Croatia}, + {"Cyprus", Country.Cyprus}, + {"Czech Republic", Country.Czech_Republic}, + {"Denmark", Country.Denmark}, + {"Dominica", Country.Dominica}, + {"Dominican Republic", Country.Dominican_Republic}, + {"Ecuador", Country.Ecuador}, + {"Egypt", Country.Egypt}, + {"El Salvador", Country.El_Salvador}, + {"Estonia", Country.Estonia}, + {"Eswatini", Country.Eswatini}, + {"Ethiopia", Country.Ethiopia}, + {"Faroe Islands", Country.Faroe_Islands}, + {"Finland", Country.Finland}, + {"France", Country.France}, + {"French Guiana", Country.French_Guiana}, + {"French Polynesia", Country.French_Polynesia}, + {"Gabon", Country.Gabon}, + {"Georgia", Country.Georgia}, + {"Germany", Country.Germany}, + {"Ghana", Country.Ghana}, + {"Gibraltar", Country.Gibraltar}, + {"Greece", Country.Greece}, + {"Greenland", Country.Greenland}, + {"Grenada", Country.Grenada}, + {"Guadeloupe", Country.Guadeloupe}, + {"Guam", Country.Guam}, + {"Guatemala", Country.Guatemala}, + {"Guernsey", Country.Guernsey}, + {"Guyana", Country.Guyana}, + {"Haiti", Country.Haiti}, + {"Honduras", Country.Honduras}, + {"Hong Kong", Country.Hong_Kong}, + {"Hungary", Country.Hungary}, + {"Iceland", Country.Iceland}, + {"India", Country.India}, + {"Indonesia", Country.Indonesia}, + {"Iran", Country.Iran}, + {"Iraq", Country.Iraq}, + {"Ireland", Country.Ireland}, + {"Isle of Man", Country.Isle_of_Man}, + {"Israel", Country.Israel}, + {"Italy", Country.Italy}, + {"Jamaica", Country.Jamaica}, + {"Japan", Country.Japan}, + {"Jersey", Country.Jersey}, + {"Jordan", Country.Jordan}, + {"Kazakhstan", Country.Kazakhstan}, + {"Kenya", Country.Kenya}, + {"Korea, Republic of", Country.Korea}, + {"Kuwait", Country.Kuwait}, + {"Kyrgyzstan", Country.Kyrgyzstan}, + {"Lao People's Democratic Republic", Country.Laos}, + {"Latvia", Country.Latvia}, + {"Lebanon", Country.Lebanon}, + {"Libya", Country.Libya}, + {"Liechtenstein", Country.Liechtenstein}, + {"Lithuania", Country.Lithuania}, + {"Luxembourg", Country.Luxembourg}, + {"Macau", Country.Macau}, + {"Macedonia, the former Yugoslav Republic of", Country.Macedonia}, + {"Madagascar", Country.Madagascar}, + {"Malaysia", Country.Malaysia}, + {"Maldives", Country.Maldives}, + {"Mali", Country.Mali}, + {"Malta", Country.Malta}, + {"Martinique", Country.Martinique}, + {"Mauritius", Country.Mauritius}, + {"Mayotte", Country.Mayotte}, + {"Mexico", Country.Mexico}, + {"Moldova, Republic of", Country.Moldova}, + {"Monaco", Country.Monaco}, + {"Mongolia", Country.Mongolia}, + {"Montenegro", Country.Montenegro}, + {"Morocco", Country.Morocco}, + {"Mozambique", Country.Mozambique}, + {"Myanmar", Country.Myanmar}, + {"Namibia", Country.Namibia}, + {"Nepal", Country.Nepal}, + {"Netherlands", Country.Netherlands}, + {"New Zealand", Country.New_Zealand}, + {"Nicaragua", Country.Nicaragua}, + {"Nigeria", Country.Nigeria}, + {"Northern Mariana Islands", Country.Northern_Mariana_Islands}, + {"Norway", Country.Norway}, + {"Oman", Country.Oman}, + {"Pakistan", Country.Pakistan}, + {"Palestinian Territory, Occupied", Country.Palestinian_Territory}, + {"Panama", Country.Panama}, + {"Papua New Guinea", Country.Papua_New_Guinea}, + {"Paraguay", Country.Paraguay}, + {"Peru", Country.Peru}, + {"Philippines", Country.Philippines}, + {"Poland", Country.Poland}, + {"Portugal", Country.Portugal}, + {"Puerto Rico", Country.Puerto_Rico}, + {"Qatar", Country.Qatar}, + {"Reunion", Country.Reunion}, + {"Romania", Country.Romania}, + {"Russian Federation", Country.Russian_Federation}, + {"Rwanda", Country.Rwanda}, + {"Saint Kitts and Nevis", Country.Saint_Kitts_and_Nevis}, + {"Saint Lucia", Country.Saint_Lucia}, + {"Saint Vincent and the Grenadines", Country.Saint_Vincent_and_the_Grenadines}, + {"Saudi Arabia", Country.Saudi_Arabia}, + {"Senegal", Country.Senegal}, + {"Serbia", Country.Serbia}, + {"Seychelles", Country.Seychelles}, + {"Singapore", Country.Singapore}, + {"Slovakia", Country.Slovakia}, + {"Slovenia", Country.Slovenia}, + {"South Africa", Country.South_Africa}, + {"Spain", Country.Spain}, + {"Sri Lanka", Country.Sri_Lanka}, + {"Sudan", Country.Sudan}, + {"Suriname", Country.Suriname}, + {"Sweden", Country.Sweden}, + {"Switzerland", Country.Switzerland}, + {"Taiwan", Country.Taiwan}, + {"Tajikistan", Country.Tajikistan}, + {"Tanzania, United Republic of", Country.Tanzania}, + {"Thailand", Country.Thailand}, + {"Togo", Country.Togo}, + {"Trinidad and Tobago", Country.Trinidad_and_Tobago}, + {"Tunisia", Country.Tunisia}, + {"Turkey", Country.Turkey}, + {"Turkmenistan", Country.Turkmenistan}, + {"Turks and Caicos Islands", Country.Turks_and_Caicos_Islands}, + {"Uganda", Country.Uganda}, + {"Ukraine", Country.Ukraine}, + {"United Arab Emirates", Country.United_Arab_Emirates}, + {"United Kingdom", Country.United_Kingdom}, + {"United States", Country.United_States}, + {"Uruguay", Country.Uruguay}, + {"Uzbekistan", Country.Uzbekistan}, + {"Venezuela", Country.Venezuela}, + {"Viet Nam", Country.Viet_Nam}, + {"Virgin Islands, U.S.", Country.Virgin_Islands}, + {"Yemen", Country.Yemen}, + {"Zambia", Country.Zambia}, + {"Zimbabwe", Country.Zimbabwe}, + }; + + public static IReadOnlyDictionary ResolveByCountryCode = new ReadOnlyDictionary( + new Dictionary() + { + {"US",Country.United_States}, + {"CA",Country.Canada}, + {"AF",Country.Afghanistan}, + {"AX",Country.Aland_Islands}, + {"AL",Country.Albania}, + {"DZ",Country.Algeria}, + {"AS",Country.American_Samoa}, + {"AD",Country.Andorra}, + {"AO",Country.Angola}, + {"AI",Country.Anguilla}, + {"AG",Country.Antigua_and_Barbuda}, + {"AR",Country.Argentina}, + {"AM",Country.Armenia}, + {"AW",Country.Aruba}, + {"AU",Country.Australia}, + {"AT",Country.Austria}, + {"AZ",Country.Azerbaijan}, + {"BS",Country.Bahamas}, + {"BH",Country.Bahrain}, + {"BD",Country.Bangladesh}, + {"BB",Country.Barbados}, + {"BY",Country.Belarus}, + {"BE",Country.Belgium}, + {"BZ",Country.Belize}, + {"BT",Country.Bhutan}, + {"BO",Country.Bolivia}, + {"BA",Country.Bosnia_and_Herzegovina}, + {"BW",Country.Botswana}, + {"BR",Country.Brazil}, + {"BN",Country.Brunei_Darussalam}, + {"BG",Country.Bulgaria}, + {"BI",Country.Burundi}, + {"KH",Country.Cambodia}, + {"CM",Country.Cameroon}, + {"CV",Country.Cape_Verde}, + {"KY",Country.Cayman_Islands}, + {"TD",Country.Chad}, + {"CL",Country.Chile}, + {"CN",Country.China}, + {"CO",Country.Colombia}, + {"CD",Country.Congo}, + {"CR",Country.Costa_Rica}, + {"CI", Country.Cote_d_Ivoire}, + {"HR",Country.Croatia}, + {"CY",Country.Cyprus}, + {"CZ",Country.Czech_Republic}, + {"DK",Country.Denmark}, + {"DM",Country.Dominica}, + {"DO",Country.Dominican_Republic}, + {"EC",Country.Ecuador}, + {"EG",Country.Egypt}, + {"SV",Country.El_Salvador}, + {"EE",Country.Estonia}, + {"ET",Country.Ethiopia}, + {"FO",Country.Faroe_Islands}, + {"FI",Country.Finland}, + {"FR",Country.France}, + {"GF",Country.French_Guiana}, + {"PF",Country.French_Polynesia}, + {"GA",Country.Gabon}, + {"GE",Country.Georgia}, + {"DE",Country.Germany}, + {"GH",Country.Ghana}, + {"GI",Country.Gibraltar}, + {"GR",Country.Greece}, + {"GL",Country.Greenland}, + {"GD",Country.Grenada}, + {"GP",Country.Guadeloupe}, + {"GU",Country.Guam}, + {"GT",Country.Guatemala}, + {"GG",Country.Guernsey}, + {"GY",Country.Guyana}, + {"HT",Country.Haiti}, + {"HN",Country.Honduras}, + {"HK",Country.Hong_Kong}, + {"HU",Country.Hungary}, + {"IS",Country.Iceland}, + {"IN",Country.India}, + {"ID",Country.Indonesia}, + {"IQ",Country.Iraq}, + {"IE",Country.Ireland}, + {"IR",Country.Iran}, + {"IM",Country.Isle_of_Man}, + {"IL",Country.Israel}, + {"IT",Country.Italy}, + {"JM",Country.Jamaica}, + {"JP",Country.Japan}, + {"JE",Country.Jersey}, + {"JO",Country.Jordan}, + {"KZ",Country.Kazakhstan}, + {"KE",Country.Kenya}, + {"KR",Country.Korea}, + {"KW",Country.Kuwait}, + {"KG",Country.Kyrgyzstan}, + {"LA",Country.Laos}, + {"LV",Country.Latvia}, + {"LB",Country.Lebanon}, + {"LY",Country.Libya}, + {"LI",Country.Liechtenstein}, + {"LT",Country.Lithuania}, + {"LU",Country.Luxembourg}, + {"MO",Country.Macau}, + {"MK",Country.Macedonia}, + {"MG",Country.Madagascar}, + {"MY",Country.Malaysia}, + {"MV",Country.Maldives}, + {"ML",Country.Mali}, + {"MT",Country.Malta}, + {"MQ",Country.Martinique}, + {"MU",Country.Mauritius}, + {"YT",Country.Mayotte}, + {"MX",Country.Mexico}, + {"MD",Country.Moldova}, + {"MC",Country.Monaco}, + {"MN",Country.Mongolia}, + {"ME",Country.Montenegro}, + {"MA",Country.Morocco}, + {"MZ",Country.Mozambique}, + {"MM",Country.Myanmar}, + {"NA",Country.Namibia}, + {"NP",Country.Nepal}, + {"NL",Country.Netherlands}, + {"NZ",Country.New_Zealand}, + {"NI",Country.Nicaragua}, + {"NG",Country.Nigeria}, + {"MP",Country.Northern_Mariana_Islands}, + {"NO",Country.Norway}, + {"OM",Country.Oman}, + {"PK",Country.Pakistan}, + {"PS",Country.Palestinian_Territory}, + {"PA",Country.Panama}, + {"PG",Country.Papua_New_Guinea}, + {"PY",Country.Paraguay}, + {"PE",Country.Peru}, + {"PH",Country.Philippines}, + {"PL",Country.Poland}, + {"PT",Country.Portugal}, + {"PR",Country.Puerto_Rico}, + {"QA",Country.Qatar}, + {"RE",Country.Reunion}, + {"RO",Country.Romania}, + {"RU",Country.Russian_Federation}, + {"RW",Country.Rwanda}, + {"LC",Country.Saint_Lucia}, + {"SA",Country.Saudi_Arabia}, + {"SN",Country.Senegal}, + {"RS",Country.Serbia}, + {"SC",Country.Seychelles}, + {"SG",Country.Singapore}, + {"SK",Country.Slovakia}, + {"SI",Country.Slovenia}, + {"ZA",Country.South_Africa}, + {"ES",Country.Spain}, + {"LK",Country.Sri_Lanka}, + {"KN",Country.Saint_Kitts_and_Nevis}, + {"PM", Country.Saint_Vincent_and_the_Grenadines}, + {"SD",Country.Sudan}, + {"SR",Country.Suriname}, + {"SE",Country.Sweden}, + {"CH",Country.Switzerland}, + {"TW",Country.Taiwan}, + {"TJ",Country.Tajikistan}, + {"TZ",Country.Tanzania}, + {"TH",Country.Thailand}, + {"TG",Country.Togo}, + {"TT",Country.Trinidad_and_Tobago}, + {"TN",Country.Tunisia}, + {"TR",Country.Turkey}, + {"TM",Country.Turkmenistan}, + {"TC",Country.Turks_and_Caicos_Islands}, + {"UG",Country.Uganda}, + {"UA",Country.Ukraine}, + {"GB",Country.United_Kingdom}, + {"VI", Country.Virgin_Islands}, + {"AE",Country.United_Arab_Emirates}, + {"UY",Country.Uruguay}, + {"UZ",Country.Uzbekistan}, + {"VE",Country.Venezuela}, + {"VN",Country.Viet_Nam}, + {"YE",Country.Yemen}, + {"ZM",Country.Zambia}, + {"ZW",Country.Zimbabwe}, + }); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/Currency.cs b/SteamLibForked/Core/Enums/Currency.cs new file mode 100644 index 0000000..714cc82 --- /dev/null +++ b/SteamLibForked/Core/Enums/Currency.cs @@ -0,0 +1,100 @@ +// ReSharper disable InconsistentNaming +namespace SteamLib.Core.Enums; + +public enum Currency +{ + None = 0, + USD = 1, + GBP = 2, + EUR = 3, + CHF = 4, + RUB = 5, + PLN = 6, + BRL = 7, + JPY = 8, + NOK = 9, + IDR = 10, + MYR = 11, + PHP = 12, + SGD = 13, + THB = 14, + VND = 15, + KRW = 16, + TRY = 17, + UAH = 18, + MXN = 19, + CAD = 20, + AUD = 21, + NZD = 22, + CNY = 23, + INR = 24, + CLP = 25, + PEN = 26, + COP = 27, + ZAR = 28, + HKD = 29, + TWD = 30, + SAR = 31, + AED = 32, + ARS = 34, + ILS = 35, + KZT = 37, + KWD = 38, + QAR = 39, + CRC = 40, + UYU = 41 +} +public static class CurrencyInfo +{ + public static int ToInt(this Currency currency) + { + return (int)currency; + } + + public static string ToIntString(this Currency currency) + { + return currency.ToInt().ToString(); + } + public static IReadOnlyDictionary CurrencySymbols { get; } = new Dictionary + { + { Currency.USD, "$" }, + { Currency.GBP, "£" }, + { Currency.EUR, "€" }, + { Currency.CHF, "CHF" }, + { Currency.RUB, "pуб." }, + { Currency.PLN, "zł" }, + { Currency.BRL, "R$" }, + { Currency.JPY, "¥" }, + { Currency.NOK, "kr" }, + { Currency.IDR, "Rp" }, + { Currency.MYR, "RM" }, + { Currency.PHP, "P" }, + { Currency.SGD, "S$" }, + { Currency.THB, "฿" }, + { Currency.VND, "₫" }, + { Currency.KRW, "₩" }, + { Currency.TRY, "TL" }, + { Currency.UAH, "₴" }, + { Currency.MXN, "Mex$" }, + { Currency.CAD, "CDN$" }, + { Currency.AUD, "A$" }, + { Currency.NZD, "NZ$" }, + { Currency.CNY, "¥" }, + { Currency.INR, "₹" }, + { Currency.CLP, "CLP$" }, + { Currency.PEN, "S/." }, + { Currency.COP, "COL$" }, + { Currency.ZAR, "R" }, + { Currency.HKD, "HK$" }, + { Currency.TWD, "NT$" }, + { Currency.SAR, "SR" }, + { Currency.AED, "AED" }, + { Currency.ARS, "ARS$" }, + { Currency.ILS, "₪" }, + { Currency.KZT, "₸" }, + { Currency.KWD, "KD" }, + { Currency.QAR, "QR" }, + { Currency.CRC, "₡" }, + { Currency.UYU, "$U" }, + }; +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/SteamAccessTokenType.cs b/SteamLibForked/Core/Enums/SteamAccessTokenType.cs new file mode 100644 index 0000000..12552d3 --- /dev/null +++ b/SteamLibForked/Core/Enums/SteamAccessTokenType.cs @@ -0,0 +1,28 @@ +using SteamLib.Api.Mobile; + +namespace SteamLib.Core.Enums; + +public enum SteamAccessTokenType +{ + Unknown = 0, + /// + /// Temporary token that returned after login + /// + Web, + /// + /// Access token that attached to domain + /// + AccessToken, + /// + /// Refresh token that can be used to refresh AccessToken + /// + Refresh, + /// + /// Mobile token that can be used to access mobile endpoints. Returned after login or by refreshing with + /// + Mobile, + /// + /// Refresh token that can be used to refresh Mobile token + /// + MobileRefresh, +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/SteamDomain.cs b/SteamLibForked/Core/Enums/SteamDomain.cs new file mode 100644 index 0000000..f20f267 --- /dev/null +++ b/SteamLibForked/Core/Enums/SteamDomain.cs @@ -0,0 +1,12 @@ +namespace SteamLib.Core.Enums; + +public enum SteamDomain +{ + Undefined = 0, + Community = 1, + Store = 2, + Help = 3, + TV = 4, + Checkout = 5, +} + diff --git a/SteamLibForked/Core/HealthMonitor.cs b/SteamLibForked/Core/HealthMonitor.cs new file mode 100644 index 0000000..8dd8d3f --- /dev/null +++ b/SteamLibForked/Core/HealthMonitor.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.Logging; + +namespace SteamLib.Core; + +public static class HealthMonitor +{ + public static ILogger? FatalLogger { get; set; } + internal static void LogUnexpected(string response, Exception ex) + { + FatalLogger?.LogCritical(ex, "Unexpected response detected:\n{response}", response); + } + + + internal static T LogOnException(string resp, Func del) + { + try + { + return del(); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del) + { + try + { + return del(resp); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + internal static T LogOnException(string resp, Func del, params Type[] exceptExceptions) + { + try + { + return del(); + } + catch (Exception ex) + when (exceptExceptions.Any(t => t == ex.GetType()) == false) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del, params Type[] exceptExceptions) + { + try + { + return del(resp); + } + catch (Exception ex) + when (exceptExceptions.Any(t => t == ex.GetType()) == false) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del, Func logPredicate) + { + try + { + return del(); + } + catch (Exception ex) + when (logPredicate(ex)) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del, Func logPredicate) + { + try + { + return del(resp); + } + catch (Exception ex) + when (logPredicate(ex)) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static void LogOnException(string resp, Action del) + { + try + { + del(); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + internal static void LogOnException(string resp, Action del, Func logPredicate) + { + try + { + del(); + } + catch (Exception ex) + when (logPredicate(ex)) + { + LogUnexpected(resp, ex); + throw; + } + } + + + internal static async Task LogOnExceptionAsync(string resp, Func> del) + { + try + { + return await del(); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + internal static async Task LogOnExceptionAsync(string resp, Func> del, params Type[] exceptExceptions) + { + try + { + return await del(); + } + catch (Exception ex) + when (exceptExceptions.Any(t => t == ex.GetType()) == false) + { + LogUnexpected(resp, ex); + throw; + } + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ICaptchaResolver.cs b/SteamLibForked/Core/Interfaces/ICaptchaResolver.cs new file mode 100644 index 0000000..a2d75d5 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ICaptchaResolver.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface ICaptchaResolver +{ + public Task Resolve(Uri imageUrl, HttpClient client); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs b/SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs new file mode 100644 index 0000000..82dacd5 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface ILoginConsumer +{ + public string FriendlyName { get; } +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs b/SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs new file mode 100644 index 0000000..6bd5eeb --- /dev/null +++ b/SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs @@ -0,0 +1,15 @@ +namespace SteamLib.Core.Interfaces; + +public enum EmailConfirmationType +{ + AttachPhoneAuthenticator +} +public interface IEmailProvider +{ + public int MaxRetryCount { get; } + public Task GetEmailAuthCode(ILoginConsumer caller); + public Task GetAddAuthenticatorCode(ILoginConsumer caller); + + public Task ConfirmEmailLink(ILoginConsumer caller, EmailConfirmationType confirmationType); + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs b/SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs new file mode 100644 index 0000000..ca58421 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface IPhoneNumberProvider +{ + public Task GetPhoneNumber(ILoginConsumer caller); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ISessionData.cs b/SteamLibForked/Core/Interfaces/ISessionData.cs new file mode 100644 index 0000000..2cbcdda --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ISessionData.cs @@ -0,0 +1,23 @@ +using SteamLib.Account; +using SteamLib.Core.Enums; + +namespace SteamLib.Core.Interfaces; + +//WARNING: Any changes here should be reflected in MafileSerializer.cs +public interface ISessionData : ICloneable +{ + internal bool? IsValid { get; set; } + public bool IsExpired => RefreshToken.IsExpired; + public string SessionId { get; } + public SteamId SteamId { get; } + public SteamAuthToken RefreshToken { get; } + + public SteamAuthToken? GetToken(SteamDomain domain); + public void SetToken(SteamDomain domain, SteamAuthToken token); +} + +public interface IMobileSessionData : ISessionData +{ + public SteamAuthToken? GetMobileToken(); + public void SetMobileToken(SteamAuthToken token); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs b/SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs new file mode 100644 index 0000000..d7810fe --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface ISmsCodeProvider +{ + public Task GetSmsCode(ILoginConsumer caller, long? phoneNumber, string? phoneHint); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs b/SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs new file mode 100644 index 0000000..dacbb88 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs @@ -0,0 +1,7 @@ +namespace SteamLib.Core.Interfaces; + +public interface ISteamGuardProvider +{ + public int MaxRetryCount { get; } + public ValueTask GetSteamGuardCode(ILoginConsumer caller); +} \ No newline at end of file diff --git a/SteamLibForked/Core/StatusCodes/SteamStatusCode.cs b/SteamLibForked/Core/StatusCodes/SteamStatusCode.cs new file mode 100644 index 0000000..73fa04d --- /dev/null +++ b/SteamLibForked/Core/StatusCodes/SteamStatusCode.cs @@ -0,0 +1,166 @@ +using SteamLib.Exceptions; +using SteamLib.Utility; +using SteamLib.Utility.Models; +using System.Collections.ObjectModel; + +namespace SteamLib.Core.StatusCodes; + +public partial class SteamStatusCode : Enumeration +{ + public static IReadOnlyDictionary StatusCodes { get; } + + protected SteamStatusCode(int id, string name) : base(id, name) { } + + static SteamStatusCode() + { + StatusCodes = new ReadOnlyDictionary(GetAll().ToDictionary(ssc => ssc.Id, ssc => ssc)); + } + + /// + /// Tries to translate status code. If no status code was found with will be thrown + /// + /// + /// + /// + /// if value is universal. if value is specific + /// + public static SteamStatusCode Translate(string response, out bool isUniversal) where T : SteamStatusCode + { + int statusCode; + try + { + statusCode = Utilities.GetSuccessCode(response); + } + catch + { + throw new SteamStatusCodeException(Undefined, response); + } + + return Translate(statusCode, out isUniversal); + } + + + + /// + /// Translate to specific or + /// + /// + /// + /// + /// if value is universal. if value is specific + /// + public static SteamStatusCode Translate(int statusCode, out bool isUniversal) where T : SteamStatusCode + { + if (statusCode < 2) + { + var status = StatusCodes[statusCode]; + isUniversal = true; + return status; + } + + var allSpecific = GetAll(); + var translated = allSpecific.FirstOrDefault(s => s.Id == statusCode); + + isUniversal = translated == null; + + if (translated != null) + { + return translated; + } + if (translated == null && StatusCodes.TryGetValue(statusCode, out var universal)) + { + return universal; + } + + return translated ?? new SteamStatusCode(statusCode, nameof(Unknown)); + } + + + public override bool Equals(object? obj) + { + if (obj is not SteamStatusCode translated) + return false; + + if (this.Id < 2) + { + return translated.Id == this.Id; + } + else + { + return base.Equals(translated); + } + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + + /// + /// + /// + /// + /// + public static void ValidateSuccessOrThrow(string response) where T : SteamStatusCode + { + var translated = Translate(response, out _); + if (translated.Id != 1) + throw new SteamStatusCodeException(translated, response); + } + + /// + /// Same as with generic parameter
+ /// (Used in case when steam status code is not defined for this operation) + ///
+ /// + /// + public static void ValidateSuccessOrThrow(string response) + { + ValidateSuccessOrThrow(response); + } + + /// + /// + /// + /// + /// + public static void ValidateSuccessOrThrow(int statusCode) where T : SteamStatusCode + { + if (statusCode == 1) return; + var translated = Translate(statusCode, out _); + throw new SteamStatusCodeException(translated, statusCode.ToString()); + } + + + /// + /// Same as with generic parameter
+ /// (Used in case when steam status code is not defined for this operation) + ///
+ /// + /// + public static void ValidateSuccessOrThrow(int statusCode) + { + ValidateSuccessOrThrow(statusCode); + } + + /// + /// Validates that status code is or specific + /// + /// + /// + /// + /// + public static SteamStatusCode TranslateOrThrow(string response, out bool isUniversal) where T : SteamStatusCode + { + var translated = Translate(response, out isUniversal); + if (translated.Id < 1) + throw new SteamStatusCodeException(translated, response); + return translated; + } + + public override string ToString() + { + return $"{Name} ({Id})"; + } +} \ No newline at end of file diff --git a/SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs b/SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs new file mode 100644 index 0000000..6f6b85b --- /dev/null +++ b/SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs @@ -0,0 +1,270 @@ +// ReSharper disable InconsistentNaming +namespace SteamLib.Core.StatusCodes; + +public partial class SteamStatusCode //Codes +{ + /// + /// Status code has no translation yet + /// + public static readonly SteamStatusCode Unknown = new(-1, nameof(Unknown)); + + /// + /// Response ended without status code even though it should have been there + /// + public static readonly SteamStatusCode Undefined = new(0, nameof(Undefined)); + + ///1 + public static readonly SteamStatusCode Ok = new(1, nameof(Ok)); + ///2 + public static readonly SteamStatusCode Fail = new(2, nameof(Fail)); + ///3 + public static readonly SteamStatusCode NoConnection = new(3, nameof(NoConnection)); + ///4 + public static readonly SteamStatusCode NoConnectionRetry = new(4, nameof(NoConnectionRetry)); + ///5 + public static readonly SteamStatusCode InvalidPassword = new(5, nameof(InvalidPassword)); + ///6 + public static readonly SteamStatusCode LoggedInElsewhere = new(6, nameof(LoggedInElsewhere)); + ///7 + public static readonly SteamStatusCode InvalidProtocolVer = new(7, nameof(InvalidProtocolVer)); + ///8 + public static readonly SteamStatusCode InvalidParam = new(8, nameof(InvalidParam)); + ///9 + public static readonly SteamStatusCode FileNotFound = new(9, nameof(FileNotFound)); + ///10 + public static readonly SteamStatusCode Busy = new(10, nameof(Busy)); + ///11 + public static readonly SteamStatusCode InvalidState = new(11, nameof(InvalidState)); + ///12 + public static readonly SteamStatusCode InvalidName = new(12, nameof(InvalidName)); + ///13 + public static readonly SteamStatusCode InvalidEmail = new(13, nameof(InvalidEmail)); + ///14 + public static readonly SteamStatusCode DuplicateName = new(14, nameof(DuplicateName)); + ///15 + public static readonly SteamStatusCode AccessDenied = new(15, nameof(AccessDenied)); + ///16 + public static readonly SteamStatusCode Timeout = new(16, nameof(Timeout)); + ///17 + public static readonly SteamStatusCode Banned = new(17, nameof(Banned)); + ///18 + public static readonly SteamStatusCode AccountNotFound = new(18, nameof(AccountNotFound)); + ///19 + public static readonly SteamStatusCode InvalidSteamID = new(19, nameof(InvalidSteamID)); + ///20 + public static readonly SteamStatusCode ServiceUnavailable = new(20, nameof(ServiceUnavailable)); + ///21 + public static readonly SteamStatusCode NotLoggedOn = new(21, nameof(NotLoggedOn)); + ///22 + public static readonly SteamStatusCode Pending = new(22, nameof(Pending)); + ///23 + public static readonly SteamStatusCode EncryptionFailure = new(23, nameof(EncryptionFailure)); + ///24 + public static readonly SteamStatusCode InsufficientPrivilege = new(24, nameof(InsufficientPrivilege)); + ///25 + public static readonly SteamStatusCode LimitExceeded = new(25, nameof(LimitExceeded)); + ///26 + public static readonly SteamStatusCode Revoked = new(26, nameof(Revoked)); + ///27 + public static readonly SteamStatusCode Expired = new(27, nameof(Expired)); + ///28 + public static readonly SteamStatusCode AlreadyRedeemed = new(28, nameof(AlreadyRedeemed)); + ///29 + public static readonly SteamStatusCode DuplicateRequest = new(29, nameof(DuplicateRequest)); + ///30 + public static readonly SteamStatusCode AlreadyOwned = new(30, nameof(AlreadyOwned)); + ///31 + public static readonly SteamStatusCode IPNotFound = new(31, nameof(IPNotFound)); + ///32 + public static readonly SteamStatusCode PersistFailed = new(32, nameof(PersistFailed)); + ///33 + public static readonly SteamStatusCode LockingFailed = new(33, nameof(LockingFailed)); + ///34 + public static readonly SteamStatusCode LogonSessionReplaced = new(34, nameof(LogonSessionReplaced)); + ///35 + public static readonly SteamStatusCode ConnectFailed = new(35, nameof(ConnectFailed)); + ///36 + public static readonly SteamStatusCode HandshakeFailed = new(36, nameof(HandshakeFailed)); + ///37 + public static readonly SteamStatusCode IOFailure = new(37, nameof(IOFailure)); + ///38 + public static readonly SteamStatusCode RemoteDisconnect = new(38, nameof(RemoteDisconnect)); + ///39 + public static readonly SteamStatusCode ShoppingCartNotFound = new(39, nameof(ShoppingCartNotFound)); + ///40 + public static readonly SteamStatusCode Blocked = new(40, nameof(Blocked)); + ///41 + public static readonly SteamStatusCode Ignored = new(41, nameof(Ignored)); + ///42 + public static readonly SteamStatusCode NoMatch = new(42, nameof(NoMatch)); + ///43 + public static readonly SteamStatusCode AccountDisabled = new(43, nameof(AccountDisabled)); + ///44 + public static readonly SteamStatusCode ServiceReadOnly = new(44, nameof(ServiceReadOnly)); + ///45 + public static readonly SteamStatusCode AccountNotFeatured = new(45, nameof(AccountNotFeatured)); + ///46 + public static readonly SteamStatusCode AdministratorOK = new(46, nameof(AdministratorOK)); + ///47 + public static readonly SteamStatusCode ContentVersion = new(47, nameof(ContentVersion)); + ///48 + public static readonly SteamStatusCode TryAnotherCM = new(48, nameof(TryAnotherCM)); + ///49 + public static readonly SteamStatusCode PasswordRequiredToKickSession = new(49, nameof(PasswordRequiredToKickSession)); + ///50 + public static readonly SteamStatusCode AlreadyLoggedInElsewhere = new(50, nameof(AlreadyLoggedInElsewhere)); + ///51 + public static readonly SteamStatusCode Suspended = new(51, nameof(Suspended)); + ///52 + public static readonly SteamStatusCode Cancelled = new(52, nameof(Cancelled)); + ///53 + public static readonly SteamStatusCode DataCorruption = new(53, nameof(DataCorruption)); + ///54 + public static readonly SteamStatusCode DiskFull = new(54, nameof(DiskFull)); + ///55 + public static readonly SteamStatusCode RemoteCallFailed = new(55, nameof(RemoteCallFailed)); + ///56 + public static readonly SteamStatusCode PasswordUnset = new(56, nameof(PasswordUnset)); + ///57 + public static readonly SteamStatusCode ExternalAccountUnlinked = new(57, nameof(ExternalAccountUnlinked)); + ///58 + public static readonly SteamStatusCode PSNTicketInvalid = new(58, nameof(PSNTicketInvalid)); + ///59 + public static readonly SteamStatusCode ExternalAccountAlreadyLinked = new(59, nameof(ExternalAccountAlreadyLinked)); + ///60 + public static readonly SteamStatusCode RemoteFileConflict = new(60, nameof(RemoteFileConflict)); + ///61 + public static readonly SteamStatusCode IllegalPassword = new(61, nameof(IllegalPassword)); + ///62 + public static readonly SteamStatusCode SameAsPreviousValue = new(62, nameof(SameAsPreviousValue)); + ///63 + public static readonly SteamStatusCode AccountLogonDenied = new(63, nameof(AccountLogonDenied)); + ///64 + public static readonly SteamStatusCode CannotUseOldPassword = new(64, nameof(CannotUseOldPassword)); + ///65 + public static readonly SteamStatusCode InvalidLoginAuthCode = new(65, nameof(InvalidLoginAuthCode)); + ///66 + public static readonly SteamStatusCode AccountLogonDeniedNoMail = new(66, nameof(AccountLogonDeniedNoMail)); + ///67 + public static readonly SteamStatusCode HardwareNotCapableOfIPT = new(67, nameof(HardwareNotCapableOfIPT)); + ///68 + public static readonly SteamStatusCode IPTInitError = new(68, nameof(IPTInitError)); + ///69 + public static readonly SteamStatusCode ParentalControlRestricted = new(69, nameof(ParentalControlRestricted)); + ///70 + public static readonly SteamStatusCode FacebookQueryError = new(70, nameof(FacebookQueryError)); + ///71 + public static readonly SteamStatusCode ExpiredLoginAuthCode = new(71, nameof(ExpiredLoginAuthCode)); + ///72 + public static readonly SteamStatusCode IPLoginRestrictionFailed = new(72, nameof(IPLoginRestrictionFailed)); + ///73 + public static readonly SteamStatusCode AccountLockedDown = new(73, nameof(AccountLockedDown)); + ///74 + public static readonly SteamStatusCode AccountLogonDeniedVerifiedEmailRequired = new(74, nameof(AccountLogonDeniedVerifiedEmailRequired)); + ///75 + public static readonly SteamStatusCode NoMatchingURL = new(75, nameof(NoMatchingURL)); + ///76 + public static readonly SteamStatusCode BadResponse = new(76, nameof(BadResponse)); + ///77 + public static readonly SteamStatusCode RequirePasswordReEntry = new(77, nameof(RequirePasswordReEntry)); + ///78 + public static readonly SteamStatusCode ValueOutOfRange = new(78, nameof(ValueOutOfRange)); + ///79 + public static readonly SteamStatusCode UnexpectedError = new(79, nameof(UnexpectedError)); + ///80 + public static readonly SteamStatusCode Disabled = new(80, nameof(Disabled)); + ///81 + public static readonly SteamStatusCode InvalidCEGSubmission = new(81, nameof(InvalidCEGSubmission)); + ///82 + public static readonly SteamStatusCode RestrictedDevice = new(82, nameof(RestrictedDevice)); + ///83 + public static readonly SteamStatusCode RegionLocked = new(83, nameof(RegionLocked)); + ///84 + public static readonly SteamStatusCode RateLimitExceeded = new(84, nameof(RateLimitExceeded)); + ///85 + public static readonly SteamStatusCode AccountLoginDeniedNeedTwoFactor = new(85, nameof(AccountLoginDeniedNeedTwoFactor)); + ///86 + public static readonly SteamStatusCode ItemDeleted = new(86, nameof(ItemDeleted)); + ///87 + public static readonly SteamStatusCode AccountLoginDeniedThrottle = new(87, nameof(AccountLoginDeniedThrottle)); + ///88 + public static readonly SteamStatusCode TwoFactorCodeMismatch = new(88, nameof(TwoFactorCodeMismatch)); + ///89 + public static readonly SteamStatusCode TwoFactorActivationCodeMismatch = new(89, nameof(TwoFactorActivationCodeMismatch)); + ///90 + public static readonly SteamStatusCode AccountAssociatedToMultiplePartners = new(90, nameof(AccountAssociatedToMultiplePartners)); + ///91 + public static readonly SteamStatusCode NotModified = new(91, nameof(NotModified)); + ///92 + public static readonly SteamStatusCode NoMobileDevice = new(92, nameof(NoMobileDevice)); + ///93 + public static readonly SteamStatusCode TimeNotSynced = new(93, nameof(TimeNotSynced)); + ///94 + public static readonly SteamStatusCode SmsCodeFailed = new(94, nameof(SmsCodeFailed)); + ///95 + public static readonly SteamStatusCode AccountLimitExceeded = new(95, nameof(AccountLimitExceeded)); + ///96 + public static readonly SteamStatusCode AccountActivityLimitExceeded = new(96, nameof(AccountActivityLimitExceeded)); + ///97 + public static readonly SteamStatusCode PhoneActivityLimitExceeded = new(97, nameof(PhoneActivityLimitExceeded)); + ///98 + public static readonly SteamStatusCode RefundToWallet = new(98, nameof(RefundToWallet)); + ///99 + public static readonly SteamStatusCode EmailSendFailure = new(99, nameof(EmailSendFailure)); + ///100 + public static readonly SteamStatusCode NotSettled = new(100, nameof(NotSettled)); + ///101 + public static readonly SteamStatusCode NeedCaptcha = new(101, nameof(NeedCaptcha)); + ///102 + public static readonly SteamStatusCode GSLTDenied = new(102, nameof(GSLTDenied)); + ///103 + public static readonly SteamStatusCode GSOwnerDenied = new(103, nameof(GSOwnerDenied)); + ///104 + public static readonly SteamStatusCode InvalidItemType = new(104, nameof(InvalidItemType)); + ///105 + public static readonly SteamStatusCode IPBanned = new(105, nameof(IPBanned)); + ///106 + public static readonly SteamStatusCode GSLTExpired = new(106, nameof(GSLTExpired)); + ///107 + public static readonly SteamStatusCode InsufficientFunds = new(107, nameof(InsufficientFunds)); + ///108 + public static readonly SteamStatusCode TooManyPending = new(108, nameof(TooManyPending)); + ///109 + public static readonly SteamStatusCode NoSiteLicensesFound = new(109, nameof(NoSiteLicensesFound)); + ///110 + public static readonly SteamStatusCode WGNetworkSendExceeded = new(110, nameof(WGNetworkSendExceeded)); + ///111 + public static readonly SteamStatusCode AccountNotFriends = new(111, nameof(AccountNotFriends)); + ///112 + public static readonly SteamStatusCode LimitedUserAccount = new(112, nameof(LimitedUserAccount)); + ///113 + public static readonly SteamStatusCode CantRemoveItem = new(113, nameof(CantRemoveItem)); + ///114 + public static readonly SteamStatusCode AccountDeleted = new(114, nameof(AccountDeleted)); + ///115 + public static readonly SteamStatusCode ExistingUserCancelledLicense = new(115, nameof(ExistingUserCancelledLicense)); + ///116 + public static readonly SteamStatusCode CommunityCooldown = new(116, nameof(CommunityCooldown)); + ///117 + public static readonly SteamStatusCode NoLauncherSpecified = new(117, nameof(NoLauncherSpecified)); + ///118 + public static readonly SteamStatusCode MustAgreeToSSA = new(118, nameof(MustAgreeToSSA)); + ///119 + public static readonly SteamStatusCode LauncherMigrated = new(119, nameof(LauncherMigrated)); + ///120 + public static readonly SteamStatusCode SteamRealmMismatch = new(120, nameof(SteamRealmMismatch)); + ///121 + public static readonly SteamStatusCode InvalidSignature = new(121, nameof(InvalidSignature)); + ///122 + public static readonly SteamStatusCode ParseFailure = new(122, nameof(ParseFailure)); + ///123 + public static readonly SteamStatusCode NoVerifiedPhone = new(123, nameof(NoVerifiedPhone)); + ///124 + public static readonly SteamStatusCode InsufficientBattery = new(124, nameof(InsufficientBattery)); + ///125 + public static readonly SteamStatusCode ChargerRequired = new(125, nameof(ChargerRequired)); + ///126 + public static readonly SteamStatusCode CachedCredentialInvalid = new(126, nameof(CachedCredentialInvalid)); + ///127 + public static readonly SteamStatusCode PhoneNumberIsVOIP = new(127, nameof(PhoneNumberIsVOIP)); +} \ No newline at end of file diff --git a/SteamLibForked/Core/SteamDomains.cs b/SteamLibForked/Core/SteamDomains.cs new file mode 100644 index 0000000..7eb8471 --- /dev/null +++ b/SteamLibForked/Core/SteamDomains.cs @@ -0,0 +1,74 @@ +using System.Collections.ObjectModel; +using SteamLib.Core.Enums; +using SteamLib.Exceptions; + +namespace SteamLib.Core; + +public static class SteamDomains +{ + public static readonly Uri LoginSteamDomain = new(SteamConstants.LOGIN_STEAM); + public static IReadOnlyDictionary Domains { get; } = new ReadOnlyDictionary( + new Dictionary + { + {SteamDomain.Community, SteamConstants.STEAM_COMMUNITY}, + {SteamDomain.Store, SteamConstants.STEAM_STORE}, + {SteamDomain.Help, SteamConstants.STEAM_HELP}, + {SteamDomain.TV, SteamConstants.STEAM_TV}, + {SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT} + }); + + public static IReadOnlyDictionary DomainUris { get; } + = new ReadOnlyDictionary( + Domains.ToDictionary(x => x.Key, x => new Uri(x.Value)) + ); + + + + public static SteamDomain[] AllDomains => new[] + { + SteamDomain.Community, + SteamDomain.Store, + SteamDomain.Help, + SteamDomain.TV, + SteamDomain.Checkout + }; + + public static Uri GetDomainUri(SteamDomain domain) + { + if (domain == SteamDomain.Undefined) + throw new ArgumentException("Invalid domain", nameof(domain)); + return DomainUris[domain]; + } + + public static SteamDomain GetDomain(string domain) + { + if (TryGetDomain(domain, out var result)) + { + return result; + } + else + { + throw UnknownSteamDomainException.Create(domain); + } + } + public static string GetDomain(SteamDomain domain) + { + if (domain == SteamDomain.Undefined) + throw new ArgumentException("Invalid domain", nameof(domain)); + return Domains[domain]; + } + + public static bool TryGetDomain(string domain, out SteamDomain result) + { + var uri = new Uri(domain); + var found = DomainUris.FirstOrDefault(x => x.Value.Host == uri.Host); + if (found.Value == default) + { + result = SteamDomain.Undefined; + return false; + } + + result = found.Key; + return true; + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/CantLoadConfirmationsException.cs b/SteamLibForked/Exceptions/CantLoadConfirmationsException.cs new file mode 100644 index 0000000..3b94a9c --- /dev/null +++ b/SteamLibForked/Exceptions/CantLoadConfirmationsException.cs @@ -0,0 +1,21 @@ +namespace SteamLib.Exceptions; + +[Serializable] +public class CantLoadConfirmationsException : Exception +{ + public LoadConfirmationsError Error { get; init; } + public CantLoadConfirmationsException() { } + public CantLoadConfirmationsException(string message) : base(message) { } + public CantLoadConfirmationsException(string message, Exception inner) : base(message, inner) { } + protected CantLoadConfirmationsException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} + + +public enum LoadConfirmationsError +{ + Unknown, + TryAgainLater, + NotSetupToReceiveConfirmations +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/General/UnsupportedResponseException.cs b/SteamLibForked/Exceptions/General/UnsupportedResponseException.cs new file mode 100644 index 0000000..5b60c82 --- /dev/null +++ b/SteamLibForked/Exceptions/General/UnsupportedResponseException.cs @@ -0,0 +1,21 @@ +namespace SteamLib.Exceptions.General; + +public class UnsupportedResponseException : Exception +{ + public string Response { get; } + + public UnsupportedResponseException(string response) : base("Request failed with unsupported response") + { + Response = response; + } + + public UnsupportedResponseException(string response, Exception? inner) : base("Request failed with unsupported response", inner) + { + Response = response; + } + + public UnsupportedResponseException(string response, string? message, Exception? inner = null) : base(message, inner) + { + Response = response; + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/LoginException.cs b/SteamLibForked/Exceptions/LoginException.cs new file mode 100644 index 0000000..c1fc5ae --- /dev/null +++ b/SteamLibForked/Exceptions/LoginException.cs @@ -0,0 +1,49 @@ +using SteamLib.Core.Interfaces; + +namespace SteamLib.Exceptions; + +public class LoginException : Exception +{ + public LoginError Error { get; } + public string? Response { get; } + public LoginException(LoginError error) + : base($"Login was unsuccessful. Error {error}") + { + Error = error; + } + + public LoginException(string response) + { + Error = LoginError.UndefinedError; + Response = response; + } + + public LoginException(string? response, string? message, LoginError? error, Exception? innerException) : base( + message, innerException) + { + Response = response; + Error = error ?? LoginError.UndefinedError; + } +} + +public enum LoginError +{ + + CaptchaRequired, + InvalidCredentials, + InvalidEmailAuthCode, + InvalidTwoFactorCode, + /// + /// SteamEmail authentication is required to login but no was provided + /// + EmailAuthRequired, + /// + /// SteamGuard is required to login but no was provided + /// + SteamGuardRequired, + /// + /// Some error occurred while trying to login. + /// + UndefinedError + +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs b/SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs new file mode 100644 index 0000000..1c695cf --- /dev/null +++ b/SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs @@ -0,0 +1,42 @@ +namespace SteamLib.Exceptions.Mobile; + + +public enum AuthenticatorLinkerError +{ + NoEmailProvider, + NoSmsCodeProvider, + PhoneAlreadyAttached, + InvalidPhoneNumber, + CantAttachPhone, + CantConfirmAttachingEmail, + CantSendSms, + AuthenticatorPresent, + BadConfirmationCode, + UnableToGenerateCorrectCodes, + InvalidStateWithStatus2, + GeneralFailure, + +} +public class AuthenticatorLinkerException : Exception +{ + public AuthenticatorLinkerError Error { get; } + public bool OnFinalization { get; init; } + public AuthenticatorLinkerException(AuthenticatorLinkerError error) : base($"Linking failed due to error: {error}") + { + Error = error; + } + + public AuthenticatorLinkerException(int code) + : base($"Linking failed due to error: {AuthenticatorLinkerError.GeneralFailure} ({code})") + { + Error = AuthenticatorLinkerError.GeneralFailure; + } + + public AuthenticatorLinkerException(string message) : base(message) + { + Error = AuthenticatorLinkerError.GeneralFailure; + } + + public AuthenticatorLinkerException(){} + +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs b/SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs new file mode 100644 index 0000000..0683e35 --- /dev/null +++ b/SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace SteamLib.Exceptions.Mobile; + +[Serializable] +public class BadMobileCookiesException : Exception +{ + // + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + // + + public BadMobileCookiesException() : base("You are using deafult HttpClient without mobile specific cookies. Login can't be proceeded with these cookies") + { + } + + public BadMobileCookiesException(string message) : base(message) + { + } + + public BadMobileCookiesException(string message, Exception inner) : base(message, inner) + { + } + + protected BadMobileCookiesException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/SessionExpiredException.cs b/SteamLibForked/Exceptions/SessionExpiredException.cs new file mode 100644 index 0000000..5a763c1 --- /dev/null +++ b/SteamLibForked/Exceptions/SessionExpiredException.cs @@ -0,0 +1,11 @@ +namespace SteamLib.Exceptions; +public class SessionExpiredException : SessionInvalidException +{ + public const string SESSION_EXPIRED_MSG = "Session expired and won't longer work. You must login to get new session"; + public SessionExpiredException() { } + public SessionExpiredException(string message) : base(message) { } + public SessionExpiredException(string message, Exception inner) : base(message, inner) { } + protected SessionExpiredException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} diff --git a/SteamLibForked/Exceptions/SessionInvalidException.cs b/SteamLibForked/Exceptions/SessionInvalidException.cs new file mode 100644 index 0000000..3cad890 --- /dev/null +++ b/SteamLibForked/Exceptions/SessionInvalidException.cs @@ -0,0 +1,12 @@ +namespace SteamLib.Exceptions; + +public class SessionInvalidException : Exception +{ + public const string SESSION_NULL_MSG = "Session was null. Before acting SteamClient must be logged in"; + public SessionInvalidException() { } + public SessionInvalidException(string message) : base(message) { } + public SessionInvalidException(string message, Exception? inner) : base(message, inner) { } + protected SessionInvalidException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} diff --git a/SteamLibForked/Exceptions/SteamStatusCodeException.cs b/SteamLibForked/Exceptions/SteamStatusCodeException.cs new file mode 100644 index 0000000..8f60cdf --- /dev/null +++ b/SteamLibForked/Exceptions/SteamStatusCodeException.cs @@ -0,0 +1,23 @@ +using SteamLib.Core.StatusCodes; + +namespace SteamLib.Exceptions; + +public class SteamStatusCodeException : Exception +{ + public SteamStatusCode StatusCode { get; } + public string? Response { get; } + + public SteamStatusCodeException(SteamStatusCode statusCode, string? response, Exception? innerException = null) + : base($"Steam return not successful status code {statusCode}", innerException) + { + StatusCode = statusCode; + Response = response; + } + + public SteamStatusCodeException(string message, SteamStatusCode statusCode, string? response = null, Exception? innerException = null) + : base(message, innerException) + { + StatusCode = statusCode; + Response = response; + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/UnknownSteamDomainException.cs b/SteamLibForked/Exceptions/UnknownSteamDomainException.cs new file mode 100644 index 0000000..b76d71f --- /dev/null +++ b/SteamLibForked/Exceptions/UnknownSteamDomainException.cs @@ -0,0 +1,25 @@ +namespace SteamLib.Exceptions; + +public class UnknownSteamDomainException : Exception +{ + public string? Domain { get; } + public UnknownSteamDomainException(string? domain) + { + Domain = domain; + } + + public UnknownSteamDomainException(string? domain, string? message) : base(message) + { + Domain = domain; + } + + public UnknownSteamDomainException(string? domain, string? message, Exception? inner) : base(message, inner) + { + Domain = domain; + } + + public static UnknownSteamDomainException Create(string? domain, Exception? inner = null) + { + return new(domain, $"Unknown Steam domain: {domain}", inner); + } +} \ No newline at end of file diff --git a/SteamLibForked/MobileSteamClient/MobileData.cs b/SteamLibForked/MobileSteamClient/MobileData.cs new file mode 100644 index 0000000..e0e6789 --- /dev/null +++ b/SteamLibForked/MobileSteamClient/MobileData.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using SteamLib.Account; + +namespace SteamLib; + +//WARNING: Any changes here should be reflected in MafileSerializer.cs + +public class MobileData +{ + [JsonRequired] public string SharedSecret { get; set; } = null!; + [JsonRequired] public string IdentitySecret { get; set; } = null!; + [JsonRequired] public string DeviceId { get; set; } = null!; + +} + +public class MobileDataExtended : MobileData +{ + public string? RevocationCode { get; set; } = null!; + public string AccountName { get; set; } = null!; + public MobileSessionData? SessionData { get; set; } + + + #region Unused + public long ServerTime { get; set; } //Unused + public ulong SerialNumber { get; set; } //Unused //greater than long must be ulong or string + public string Uri { get; set; } = null!;//Unused + public string TokenGid { get; set; } = null!;//Unused + public string Secret1 { get; set; } = null!; //Unused + + #endregion +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs b/SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs new file mode 100644 index 0000000..1bd2133 --- /dev/null +++ b/SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs @@ -0,0 +1,16 @@ +using ProtoBuf; + +namespace SteamLib.ProtoCore.Enums; + +[ProtoContract] +public enum EAuthSessionGuardType +{ + Unknown = 0, + None = 1, + EmailCode = 2, + DeviceCode = 3, + DeviceConfirmation = 4, + EmailConfirmation = 5, + MachineToken = 6, + LegacyMachineAuth = 7, +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs b/SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs new file mode 100644 index 0000000..b61c0f9 --- /dev/null +++ b/SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace SteamLib.ProtoCore.Enums; + +[ProtoContract] +public enum EAuthTokenPlatformType +{ + Unknown = 0, + SteamClient = 1, + WebBrowser = 2, + MobileApp = 3, +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Enums/EResult.cs b/SteamLibForked/ProtoCore/Enums/EResult.cs new file mode 100644 index 0000000..30d78a2 --- /dev/null +++ b/SteamLibForked/ProtoCore/Enums/EResult.cs @@ -0,0 +1,132 @@ +namespace SteamLib.ProtoCore.Enums; + +public enum EResult +{ + Invalid = 0, + OK = 1, + Fail = 2, + NoConnection = 3, + InvalidPassword = 5, + LoggedInElsewhere = 6, + InvalidProtocolVer = 7, + InvalidParam = 8, + FileNotFound = 9, + Busy = 10, + InvalidState = 11, + InvalidName = 12, + InvalidEmail = 13, + DuplicateName = 14, + AccessDenied = 15, + Timeout = 16, + Banned = 17, + AccountNotFound = 18, + InvalidSteamID = 19, + ServiceUnavailable = 20, + NotLoggedOn = 21, + Pending = 22, + EncryptionFailure = 23, + InsufficientPrivilege = 24, + LimitExceeded = 25, + Revoked = 26, + Expired = 27, + AlreadyRedeemed = 28, + DuplicateRequest = 29, + AlreadyOwned = 30, + IPNotFound = 31, + PersistFailed = 32, + LockingFailed = 33, + LogonSessionReplaced = 34, + ConnectFailed = 35, + HandshakeFailed = 36, + IOFailure = 37, + RemoteDisconnect = 38, + ShoppingCartNotFound = 39, + Blocked = 40, + Ignored = 41, + NoMatch = 42, + AccountDisabled = 43, + ServiceReadOnly = 44, + AccountNotFeatured = 45, + AdministratorOK = 46, + ContentVersion = 47, + TryAnotherCM = 48, + PasswordRequiredToKickSession = 49, + AlreadyLoggedInElsewhere = 50, + Suspended = 51, + Cancelled = 52, + DataCorruption = 53, + DiskFull = 54, + RemoteCallFailed = 55, + PasswordUnset = 56, + ExternalAccountUnlinked = 57, + PSNTicketInvalid = 58, + ExternalAccountAlreadyLinked = 59, + RemoteFileConflict = 60, + IllegalPassword = 61, + SameAsPreviousValue = 62, + AccountLogonDenied = 63, + CannotUseOldPassword = 64, + InvalidLoginAuthCode = 65, + AccountLogonDeniedNoMail = 66, + HardwareNotCapableOfIPT = 67, + IPTInitError = 68, + ParentalControlRestricted = 69, + FacebookQueryError = 70, + ExpiredLoginAuthCode = 71, + IPLoginRestrictionFailed = 72, + AccountLockedDown = 73, + AccountLogonDeniedVerifiedEmailRequired = 74, + NoMatchingURL = 75, + BadResponse = 76, + RequirePasswordReEntry = 77, + ValueOutOfRange = 78, + UnexpectedError = 79, + Disabled = 80, + InvalidCEGSubmission = 81, + RestrictedDevice = 82, + RegionLocked = 83, + RateLimitExceeded = 84, + AccountLoginDeniedNeedTwoFactor = 85, + ItemDeleted = 86, + AccountLoginDeniedThrottle = 87, + TwoFactorCodeMismatch = 88, + TwoFactorActivationCodeMismatch = 89, + AccountAssociatedToMultiplePartners = 90, + NotModified = 91, + NoMobileDevice = 92, + TimeNotSynced = 93, + SMSCodeFailed = 94, + AccountLimitExceeded = 95, + AccountActivityLimitExceeded = 96, + PhoneActivityLimitExceeded = 97, + RefundToWallet = 98, + EmailSendFailure = 99, + NotSettled = 100, + NeedCaptcha = 101, + GSLTDenied = 102, + GSOwnerDenied = 103, + InvalidItemType = 104, + IPBanned = 105, + GSLTExpired = 106, + InsufficientFunds = 107, + TooManyPending = 108, + NoSiteLicensesFound = 109, + WGNetworkSendExceeded = 110, + AccountNotFriends = 111, + LimitedUserAccount = 112, + CantRemoveItem = 113, + AccountDeleted = 114, + ExistingUserCancelledLicense = 115, + CommunityCooldown = 116, + NoLauncherSpecified = 117, + MustAgreeToSSA = 118, + LauncherMigrated = 119, + SteamRealmMismatch = 120, + InvalidSignature = 121, + ParseFailure = 122, + NoVerifiedPhone = 123, + InsufficientBattery = 124, + ChargerRequired = 125, + CachedCredentialInvalid = 126, + PhoneNumberIsVOIP = 127, +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Exceptions/EResutException.cs b/SteamLibForked/ProtoCore/Exceptions/EResutException.cs new file mode 100644 index 0000000..0e2276e --- /dev/null +++ b/SteamLibForked/ProtoCore/Exceptions/EResutException.cs @@ -0,0 +1,28 @@ +using SteamLib.ProtoCore.Enums; + +namespace SteamLib.ProtoCore.Exceptions; + +[Serializable] +public class EResultException : Exception +{ + public EResult Result { get; } + public EResultException() { } + public EResultException(EResult result) : base("EResult error: " + result) + { + Result = result; + } + + public EResultException(string message, EResult result, Exception inner) : base(message, inner) + { + Result = result; + } + + public EResultException(EResult result, Exception inner) : base("EResult error: " + result, inner) + { + Result = result; + } + + protected EResultException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} diff --git a/SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs b/SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs new file mode 100644 index 0000000..a340a89 --- /dev/null +++ b/SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace SteamLib.ProtoCore.Exceptions; + +[Serializable] +public class UnknownEResultException : Exception +{ + public int EResult { get; } + public UnknownEResultException() + {} + + public UnknownEResultException(int eResult) : base("Got unknown EResult: " + eResult) + { + EResult = eResult; + } + + protected UnknownEResultException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs b/SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs new file mode 100644 index 0000000..ce1a6ce --- /dev/null +++ b/SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs @@ -0,0 +1,6 @@ +namespace SteamLib.ProtoCore.Interfaces; + +public interface IProtoMsg +{ + +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/ProtoHelpers.cs b/SteamLibForked/ProtoCore/ProtoHelpers.cs new file mode 100644 index 0000000..fcb7b74 --- /dev/null +++ b/SteamLibForked/ProtoCore/ProtoHelpers.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using ProtoBuf; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore; + +public static class ProtoHelpers +{ + public static byte[] ProtoToBytes(IProtoMsg obj) + { + using var stream = new MemoryStream(); + Serializer.Serialize(stream, obj); + return stream.ToArray(); + } + + [return: NotNullIfNotNull("obj")] + public static string? ProtoToString(IProtoMsg? obj) + { + return obj == null ? null : Convert.ToBase64String(ProtoToBytes(obj)); + } + + public static EResult GetEResult(HttpResponseMessage response) + { + if (response.Headers.TryGetValues("x-eresult", out var val)) + { + var eResultInt = Convert.ToInt32(val.Single()); + + if (Enum.IsDefined(typeof(EResult), eResultInt) == false) + throw new UnknownEResultException(eResultInt); + + return (EResult)eResultInt; + } + + return EResult.Invalid; + } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs b/SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs new file mode 100644 index 0000000..83b0e8e --- /dev/null +++ b/SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs @@ -0,0 +1,229 @@ +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore; + +public static class ProtoHttpClientExtension +{ + #region Post + + #region ProtoResp + public static Task> PostProtoMsg(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, uri, HttpMethod.Post, str, cancellationToken); + } + public static Task> PostProtoMsg(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + } + + #endregion + + #region ProtoConverted + public static async Task PostProto(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, uri, HttpMethod.Post, str, cancellationToken); + return res.GetResponseEnsureSuccess(); + } + public static async Task PostProto(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + return res.GetResponseEnsureSuccess(); + } + + #endregion + + #region EResult + + public static Task PostProto(this HttpClient client, Uri uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, uri, HttpMethod.Post, str, cancellationToken); + } + + public static Task PostProto(this HttpClient client, string uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + } + + + public static async Task PostProtoEnsureSuccess(this HttpClient client, Uri uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, uri, HttpMethod.Post, str, cancellationToken); + res.EnsureSuccessEResult(); + } + + public static async Task PostProtoEnsureSuccess(this HttpClient client, string uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + res.EnsureSuccessEResult(); + } + + #endregion + + #endregion + + #region Get + + #region ProtoResp + public static Task> GetProtoMsg(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, uri, HttpMethod.Get, str, cancellationToken); + } + public static Task> GetProtoMsg(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + } + #endregion + + #region ProtoConverted + public static async Task GetProto(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, uri, HttpMethod.Get, str, cancellationToken); + res.Result.EnsureSuccessEResult(); + + return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null"); + } + public static async Task GetProto(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + res.Result.EnsureSuccessEResult(); + return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null"); + } + + #endregion + + #region EResult + public static Task GetProto(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, uri, HttpMethod.Get, str, cancellationToken); + } + + public static Task GetProto(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + } + public static async Task GetProtoEnsureSuccess(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, uri, HttpMethod.Get, str, cancellationToken); + res.EnsureSuccessEResult(); + } + + public static async Task GetProtoEnsureSuccess(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + res.EnsureSuccessEResult(); + } + #endregion + + #endregion + + + private static async Task> SendProtoWithResponse(this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken) where TProtoResponse : class, IProtoMsg + { + var resp = await SendProto(client, uri, method, protoMsg, cancellationToken); + return await ProtoResponse.FromHttpResponseAsync(resp, cancellationToken); + } + + private static async Task SendProtoWithEResult(HttpClient client, Uri uri, HttpMethod method, string? protoMsg, + CancellationToken cancellationToken) + { + var resp = await SendProto(client, uri, method, protoMsg, cancellationToken); + resp.EnsureSuccessStatusCode(); + return ProtoHelpers.GetEResult(resp); + } + + + public static Task SendProto(this HttpClient client, Uri uri, HttpMethod method, IProtoMsg? protoMsg, CancellationToken cancellationToken = default) + { + return client.SendProto(uri, method, ProtoHelpers.ProtoToString(protoMsg), cancellationToken); + } + + public static Task SendProto(this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken = default) + { + HttpContent? content = null; + + protoMsg ??= string.Empty; + + if (method.Equals(HttpMethod.Get)) + { + var uriStr = uri.ToString(); + var uriWithQuery = uriStr.Contains('?') + ? uriStr + "&input_protobuf_encoded=" + protoMsg + : uriStr + "?input_protobuf_encoded=" + protoMsg; + uri = new Uri(uriWithQuery); + } + else + { + var cont = new KeyValuePair[] + { + new("input_protobuf_encoded", protoMsg) + }; + content = new FormUrlEncodedContent(cont); + } + + var req = new HttpRequestMessage(method, uri) + { + Content = content + }; + + return client.SendAsync(req, cancellationToken); + } + public static void EnsureSuccessEResult(this EResult eResult) + { + if (eResult != EResult.OK) + { + throw new EResultException(eResult); + } + } + + /// + /// + /// + /// + /// + /// + /// + /// + public static T GetResponseEnsureSuccess(this ProtoResponse response) where T : class, IProtoMsg + { + if (response.Result != EResult.OK) + { + throw new EResultException(response.Result); + } + + return response.GetResponse(); + } +} diff --git a/SteamLibForked/ProtoCore/ProtoResponse.cs b/SteamLibForked/ProtoCore/ProtoResponse.cs new file mode 100644 index 0000000..f0aa86e --- /dev/null +++ b/SteamLibForked/ProtoCore/ProtoResponse.cs @@ -0,0 +1,102 @@ +using ProtoBuf; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore; + +public class ProtoResponse +{ + public EResult Result { get; } + public IProtoMsg? ProtoObject { get; protected set; } + + + protected ProtoResponse(EResult result, IProtoMsg? protoObject) + { + Result = result; + ProtoObject = protoObject; + } + + +} + +public class ProtoResponse : ProtoResponse + where TProtoResponse : IProtoMsg +{ + public TProtoResponse? ResponseMsg + { + get => ProtoObject == null ? default : (TProtoResponse)ProtoObject; + set => ProtoObject = value; + } + + public ProtoResponse(EResult result, TProtoResponse? protoObject) : base(result, protoObject) + { } + + public static ProtoResponse FromHttpResponse(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + EResult eResult = EResult.Invalid; + if (response.Headers.TryGetValues("x-eresult", out var val)) + { + var eResultInt = Convert.ToInt32(val.Single()); + eResult = (EResult)eResultInt; + } + + TProtoResponse? msg = default(TProtoResponse); + Stream? stream = null; + try + { + stream = response.Content.ReadAsStream(); + msg = Serializer.Deserialize(stream); + } + catch + { + //Ignored + } + finally + { + stream?.Dispose(); + } + return new ProtoResponse(eResult, msg); + } + + public static async Task> FromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + { + response.EnsureSuccessStatusCode(); + EResult eResult = EResult.Invalid; + if (response.Headers.TryGetValues("x-eresult", out var val)) + { + var eResultInt = Convert.ToInt32(val.Single()); + eResult = (EResult)eResultInt; + } + + TProtoResponse? msg = default(TProtoResponse); + Stream? stream = null; + try + { + stream = await response.Content.ReadAsStreamAsync(cancellationToken); + msg = Serializer.Deserialize(stream); + } + catch + { + //Ignored + } + finally { if (stream != null) await stream.DisposeAsync(); } + + + + + return new ProtoResponse(eResult, msg); + } + + /// + /// Ensures that is not and returns it otherwise throws . + /// + /// + /// + public TProtoResponse GetResponse() + { + if (ResponseMsg == null) + throw new NullReferenceException("ResponseMsg in response was null"); + return ResponseMsg; + } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Services/AuthenticationService.cs b/SteamLibForked/ProtoCore/Services/AuthenticationService.cs new file mode 100644 index 0000000..ca5cc35 --- /dev/null +++ b/SteamLibForked/ProtoCore/Services/AuthenticationService.cs @@ -0,0 +1,228 @@ +using System.Security.Cryptography; +using ProtoBuf; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Interfaces; +using SteamLib.Utility; +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +#pragma warning disable CS8618 + +namespace SteamLib.ProtoCore.Services; + +[ProtoContract] +public class GetPasswordRSAPublicKey_Request : IProtoMsg +{ + [ProtoMember(1)] public string AccountName { get; set; } +} + +[ProtoContract] +public class GetPasswordRSAPublicKey_Response : IProtoMsg +{ + [ProtoMember(1)] public string PublickKeyMod { get; set; } + [ProtoMember(2)] public string PublickKeyExp { get; set; } + [ProtoMember(3)] public ulong Timestamp { get; set; } +} + + +[ProtoContract] +public class BeginAuthSessionViaCredentials_Request : IProtoMsg +{ + [ProtoMember(1)] public string DeviceFriendlyName { get; set; } + [ProtoMember(2)] public string AccountName { get; set; } + [ProtoMember(3)] public string EncryptedPassword { get; set; } + [ProtoMember(4)] public ulong EncryptionTimestamp { get; set; } + [ProtoMember(5)] public bool RememberLogin { get; set; } + [ProtoMember(6)] public EAuthTokenPlatformType PlatformType { get; set; } + [ProtoMember(7)] public int Persistence { get; set; } + + /// + /// Gets or sets the website id that the login will be performed for. + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". + /// + /// The website id. + [ProtoMember(8)] public string WebsiteId { get; set; } + [ProtoMember(9)] public DeviceDetails DeviceDetails { get; set; } = new(); + [ProtoMember(10)] public string GuardData { get; set; } + [ProtoMember(11)] public uint Language { get; set; } + +} + +[ProtoContract] +public class DeviceDetails : IProtoMsg +{ + /// + /// User-Agent if platform is SteamCommunity + /// + [ProtoMember(1)] public string DeviceFriendlyName { get; } + [ProtoMember(2)] public EAuthTokenPlatformType PlatformType { get; } + [ProtoMember(3)] public int? OsType { get; } + [ProtoMember(4)] public uint? GamingDeviceType { get; } + + #region Members + public DeviceDetails() + { + } + + public DeviceDetails(string deviceFriendlyName, EAuthTokenPlatformType platformType, int? osType, uint? gamingDeviceType) + { + DeviceFriendlyName = deviceFriendlyName; + PlatformType = platformType; + OsType = osType; + GamingDeviceType = gamingDeviceType; + } + + public DeviceDetails(string deviceFriendlyName, EAuthTokenPlatformType platformType, EOSType? osType, uint? gamingDeviceType) + { + DeviceFriendlyName = deviceFriendlyName; + PlatformType = platformType; + OsType = (int?)osType; + GamingDeviceType = gamingDeviceType; + } + + public static DeviceDetails CreateDefault() + { + return new DeviceDetails("", EAuthTokenPlatformType.WebBrowser, (int?)null, null); + } + + public static DeviceDetails CreateCommunityDetails(string userAgent) + { + return new DeviceDetails(userAgent, EAuthTokenPlatformType.WebBrowser, (int?)null, null); + } + + public static DeviceDetails CreateMobileDetails(string deviceName) + { + return new DeviceDetails(deviceName, EAuthTokenPlatformType.MobileApp, EOSType.AndroidUnknown, 528); + } + + + #endregion +} + +[ProtoContract] +public class BeginAuthSessionViaCredentials_Response : IProtoMsg +{ + [ProtoMember(1)] public ulong ClientId { get; set; } + [ProtoMember(2)] public byte[] RequestId { get; set; } + [ProtoMember(3)] public float Interval { get; set; } + [ProtoMember(4)] public List AllowedConfirmations { get; } = new(); + [ProtoMember(5)] public ulong Steamid { get; set; } + [ProtoMember(6)] public string WeakToken { get; set; } + [ProtoMember(7)] public string AgreementSessionUrl { get; set; } + [ProtoMember(8)] public string ExtendedErrorMessage { get; set; } +} + +[ProtoContract] +public class AllowedConfirmationMsg : IProtoMsg +{ + [ProtoMember(1)] public EAuthSessionGuardType ConfirmationType { get; set; } + [ProtoMember(2)] public string AssociatedMessage { get; set; } +} + +[ProtoContract] +public class PollAuthSessionStatus_Request : IProtoMsg +{ + [ProtoMember(1)] public ulong ClientId { get; set; } + [ProtoMember(2)] public byte[] RequestId { get; set; } + [ProtoMember(3)] public ulong TokenToRevoke { get; set; } +} + +[ProtoContract] +public class PollAuthSessionStatus_Response : IProtoMsg +{ + [ProtoMember(1)] public ulong NewClientId { get; set; } + [ProtoMember(2)] public string NewChallengeUrl { get; set; } + [ProtoMember(3)] public string RefreshToken { get; set; } + [ProtoMember(4)] public string AccessToken { get; set; } + [ProtoMember(5)] public bool HadRemoteInteraction { get; set; } + [ProtoMember(6)] public string AccountName { get; set; } + [ProtoMember(7)] public string NewGuardData { get; set; } + [ProtoMember(8)] public string AgreementSessionUrl { get; set; } + +} + + + +[ProtoContract] +public class UpdateAuthSessionWithSteamGuardCode_Request : IProtoMsg +{ + [ProtoMember(1)] public ulong ClientId { get; set; } + [ProtoMember(2, DataFormat = DataFormat.FixedSize)] public ulong Steamid { get; set; } + [ProtoMember(3)] public string Code { get; set; } + [ProtoMember(4)] public EAuthSessionGuardType CodeType { get; set; } + +} + +[ProtoContract] +public class GetAuthSessionsForAccount_Response : IProtoMsg +{ + [ProtoMember(1)] public List ClientIds { get; set; } = new(); +} + +[ProtoContract] +public class GetAuthSessionInfo_Request : IProtoMsg +{ + [ProtoMember(1)] + public ulong ClientId { get; set; } +} + +[ProtoContract] +public class GetAuthSessionInfo_Response : IProtoMsg +{ + [ProtoMember(1)] public string IP { get; set; } + [ProtoMember(2)] public string GeoLoc { get; set; } + [ProtoMember(3)] public string City { get; set; } + [ProtoMember(4)] public string State { get; set; } + [ProtoMember(5)] public string Country { get; set; } + [ProtoMember(6)] public EAuthTokenPlatformType PlatformType { get; set; } + [ProtoMember(7)] public string DeviceFriendlyName { get; set; } + [ProtoMember(8)] public int Version { get; set; } + [ProtoMember(9)] public bool RequestorLocationMismatch { get; set; } + [ProtoMember(10)] public bool LoginHistory { get; set; } + [ProtoMember(11)] public bool HighUsageLogin { get; set; } + [ProtoMember(12)] public int RequestedPersistence { get; set; } +} + +[ProtoContract] +public class UpdateAuthSessionWithMobileConfirmation_Request : IProtoMsg +{ + [ProtoMember(1)] public int Version { get; set; } + [ProtoMember(2)] public ulong ClientId { get; set; } + [ProtoMember(3, DataFormat = DataFormat.FixedSize)] public ulong Steamid { get; set; } + [ProtoMember(4)] public byte[] Signature { get; set; } + [ProtoMember(5)] public bool Confirm { get; set; } + [ProtoMember(6)] public int Persistence { get; set; } + + + public void ComputeSignature(string privateKey) + { + byte[] signatureData = new byte[2 + 8 + 8]; + BitConverter.GetBytes(Version).CopyTo(signatureData, 0); + BitConverter.GetBytes(ClientId).CopyTo(signatureData, 2); + BitConverter.GetBytes(Steamid).CopyTo(signatureData, 10); + + using (var hmac = new HMACSHA256(Convert.FromBase64String(privateKey))) + { + Signature= hmac.ComputeHash(signatureData); + } + + } + +} + +[ProtoContract] +public class GenerateAccessTokenForApp_Request : IProtoMsg +{ + [ProtoMember(1)] + public string RefreshToken { get; set; } + [ProtoMember(2, DataFormat = DataFormat.FixedSize)] + public long SteamId { get; set; } + + [ProtoMember(3)] public bool TokenRenewalType { get; set; } = true; //enum: ETokenRenewalType +} + +[ProtoContract] +public class GenerateAccessTokenForApp_Response : IProtoMsg +{ + [ProtoMember(1)] + public string AccessToken { get; set; } +} diff --git a/SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs b/SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs new file mode 100644 index 0000000..d26121d --- /dev/null +++ b/SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs @@ -0,0 +1,138 @@ +using ProtoBuf; +using SteamLib.Account; +using SteamLib.ProtoCore.Interfaces; +// ReSharper disable InconsistentNaming + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +#pragma warning disable CS8618 + +namespace SteamLib.ProtoCore.Services; + +[ProtoContract] +public class AccountPhoneStatus_Response : IProtoMsg +{ + [ProtoMember(1)] public bool HasPhone { get; set; } +} + +[ProtoContract] +public class AddAuthenticator_Request : IProtoMsg +{ + [ProtoMember(1, DataFormat = DataFormat.FixedSize)] public ulong SteamId { get; set; } + [ProtoMember(4)] public int AuthenticatorType { get; set; } + [ProtoMember(5)] public string DeviceIdentifier { get; set; } + [ProtoMember(6)] public string SmsPhoneId { get; set; } + [ProtoMember(8)] public int Version { get; set; } +} + +[ProtoContract] +public class AddAuthenticator_Response : IProtoMsg +{ + [ProtoMember(1)] public byte[] SharedSecret { get; set; } + [ProtoMember(2)] public ulong SerialNumber { get; set; } + [ProtoMember(3)] public string RevocationCode { get; set; } + [ProtoMember(4)] public string Uri { get; set; } + [ProtoMember(5)] public long ServerTime { get; set; } + [ProtoMember(6)] public string AccountName { get; set; } + [ProtoMember(7)] public string TokenGid { get; set; } + [ProtoMember(8)] public byte[] IdentitySecret { get; set; } + [ProtoMember(9)] public byte[] Secret1 { get; set; } + [ProtoMember(10)] public int Status { get; set; } + [ProtoMember(11)] public string PhoneNumberHint { get; set; } + + /// + /// 2 - PhoneNumber + /// 3 - EmailCode + /// + [ProtoMember(12)] public int ConfirmType { get; set; } + + + public MobileDataExtended ToMobileDataExtended(string deviceId, MobileSessionData? sessionData) + { + return new MobileDataExtended + { + SharedSecret = Convert.ToBase64String(SharedSecret), + SerialNumber = SerialNumber, + RevocationCode = RevocationCode, + Uri = Uri, + ServerTime = ServerTime, + AccountName = AccountName, + TokenGid = TokenGid, + IdentitySecret = Convert.ToBase64String(IdentitySecret), + Secret1 = Convert.ToBase64String(Secret1), + SessionData = sessionData, + DeviceId = deviceId + }; + } +} + +[ProtoContract] +public class SetAccountPhoneNumber_Request : IProtoMsg +{ + [ProtoMember(1)] public string PhoneNumber { get; set; } + [ProtoMember(2)] public string CountryCode { get; set; } + +} + + +[ProtoContract] +public class SetAccountPhoneNumber_Response : IProtoMsg +{ + [ProtoMember(1)] public string EmailHint { get; set; } + [ProtoMember(2)] public string PhoneNumber { get; set; } + +} + + +[ProtoContract] +public class IsAccountWaitingForEmailConfirmation_Response : IProtoMsg +{ + [ProtoMember(1)] public bool IsWaiting { get; set; } + [ProtoMember(2)] public int SecondsToWait { get; set; } + +} + +[ProtoContract] +public class SendPhoneVerificationCode_Request : IProtoMsg +{ + [ProtoMember(1, IsRequired = true)] public int Language { get; set; } = 0; +} + + +[ProtoContract] +public class FinalizeAddAuthenticator_Request : IProtoMsg +{ + [ProtoMember(1, DataFormat = DataFormat.FixedSize)] public ulong SteamId { get; set; } + [ProtoMember(2)] public string AuthenticatorCode { get; set; } + [ProtoMember(3)] public ulong AuthenticatorTime { get; set; } + [ProtoMember(4)] public string ConfirmationCode { get; set; } + [ProtoMember(6)] public bool ValidateConfirmationCode { get; set; } +} + + + +[ProtoContract] +public class FinalizeAddAuthenticator_Response : IProtoMsg +{ + [ProtoMember(1)] public bool Success { get; set; } + + [ProtoMember(2)] public bool WantMore { get; set; } + [ProtoMember(3)] public ulong ServerTime { get; set; } + [ProtoMember(4)] public int Status { get; set; } +} + +[ProtoContract] +public class RemoveAuthenticator_Request : IProtoMsg +{ + [ProtoMember(2)] public string RevocationCode { get; set; } + [ProtoMember(5)] public int RevocationReason { get; set; } + [ProtoMember(6)] public int SteamGuardScheme { get; set; } + +} + +[ProtoContract] +public class RemoveAuthenticator_Response : IProtoMsg +{ + [ProtoMember(1)] public bool Success { get; set; } + [ProtoMember(5)] public int RevocationAttemptsRemaining { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Services/EmptyMessage.cs b/SteamLibForked/ProtoCore/Services/EmptyMessage.cs new file mode 100644 index 0000000..5dbc384 --- /dev/null +++ b/SteamLibForked/ProtoCore/Services/EmptyMessage.cs @@ -0,0 +1,10 @@ +using ProtoBuf; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore.Services; + +[ProtoContract] +public class EmptyMessage : IProtoMsg +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamLibForked.csproj b/SteamLibForked/SteamLibForked.csproj new file mode 100644 index 0000000..d73ab15 --- /dev/null +++ b/SteamLibForked/SteamLibForked.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs new file mode 100644 index 0000000..919dced --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs @@ -0,0 +1,9 @@ +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +public enum CheckPhoneResult +{ + Valid, + GeneralFailure, + VoIp, + NotValid +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs new file mode 100644 index 0000000..a68b29b --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Core.Interfaces; + +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +public class LinkOptions +{ + public ILoginConsumer LoginConsumer { get; } + public HttpClient HttpClient { get; } + public IEmailProvider? EmailProvider { get; } + public ISmsCodeProvider? SmsCodeProvider { get; } + public ICaptchaResolver? CaptchaResolver { get; } + public IPhoneNumberProvider? PhoneNumberProvider { get; } + public Action? BackupHandler { get; } + public ILogger? Logger { get; } + + public LinkOptions(HttpClient httpClient, ILoginConsumer loginConsumer, ISmsCodeProvider? smsCodeProvider, + ICaptchaResolver? captchaResolver, IEmailProvider? emailProvider, IPhoneNumberProvider? phoneNumberProvider, Action? backupHandler = null, + ILogger? logger = null) + { + EmailProvider = emailProvider; + SmsCodeProvider = smsCodeProvider; + CaptchaResolver = captchaResolver; + HttpClient = httpClient; + LoginConsumer = loginConsumer; + BackupHandler = backupHandler; + PhoneNumberProvider = phoneNumberProvider; + Logger = logger; + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs new file mode 100644 index 0000000..cf1196d --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +public class LinkResult +{ + [MemberNotNullWhen(false, nameof(Error))] + public bool Success { get; } + + public LinkError? Error { get; } + public int? Code { get; } + public LinkResult() + { + Success = true; + + } + + public LinkResult(LinkError error) + { + Success = false; + Error = error; + } + + public LinkResult(int code) + { + Success = false; + Code = code; + Error = LinkError.GeneralFailure; + } + +} + +public enum LinkError +{ + GeneralFailure, + BadConfirmationCode, + UnableToGenerateCorrectCodes, +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs new file mode 100644 index 0000000..fcb8282 --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs @@ -0,0 +1,162 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using SteamLib.Account; +using SteamLib.Api.Mobile; +using SteamLib.Authentication; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore.Enums; + +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +[PublicAPI] +public class SteamAuthenticatorLinker +{ + public ILoginConsumer Consumer { get; } + public LinkOptions Options { get; } + public ISessionData? SessionData { get; private set; } + internal HttpClient Client => Options.HttpClient; + internal string? DeviceId { get; set; } + private ILogger? Logger => Options.Logger; + public SteamAuthenticatorLinker(LinkOptions options) + { + Options = options; + Consumer = options.LoginConsumer; + } + + public async Task LinkAccount(MobileSessionData data) + { + Dictionary scopeData = new() + { + {"consumer", Consumer.FriendlyName} + }; + using var ctx = Options.Logger?.BeginScope(scopeData); + + SessionData = data; + + data.EnsureValidated(); + if (data.RefreshToken.IsExpired) + { + Logger?.LogError("Session expired"); + throw new SessionExpiredException(SessionExpiredException.SESSION_EXPIRED_MSG); + } + + var accessToken = data.GetMobileToken(); + if (accessToken == null) + throw new SessionInvalidException("SessionData must provide valid AccessToken to access mobile endpoints"); + + if (accessToken.Value.IsExpired || accessToken.Value.Type != SteamAccessTokenType.Mobile) + { + if (accessToken.Value.Type != SteamAccessTokenType.Mobile) + Logger?.LogWarning("Provided access token is not of type Mobile. Actual type: {actualType}", accessToken.Value.Type); + + var refreshed = await SteamMobileApi.RefreshJwt(Options.HttpClient, data.RefreshToken.Token, data.RefreshToken.SteamId.Steam64); + accessToken = SteamTokenHelper.Parse(refreshed); + data.SetMobileToken(accessToken.Value); + } + + if (accessToken.Value.Type != SteamAccessTokenType.Mobile) + throw new SessionInvalidException("SessionData must provide AccessToken of type Mobile to access mobile endpoints"); + + + var hasPhoneResp = await this.HasPhone(); + var hasPhone = hasPhoneResp.HasPhone; + + + if (hasPhone && Options.SmsCodeProvider == null) + { + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.PhoneAlreadyAttached); + } + + long? phoneNumber = null; + + if (hasPhone == false && Options.PhoneNumberProvider != null) + { + phoneNumber = await Options.PhoneNumberProvider.GetPhoneNumber(Consumer); + } + + if (hasPhone == false && phoneNumber != null) + { + Logger?.LogInformation("Attaching phone number {phoneNumber}", phoneNumber); + //if (await this.IsValidPhoneNumber(phoneNumber.Value) == false) + // throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidPhoneNumber); + + if (await this.AttachPhone(phoneNumber.Value) == false) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantAttachPhone); + + await Options.EmailProvider!.ConfirmEmailLink(Consumer, EmailConfirmationType.AttachPhoneAuthenticator); + + if (await this.CheckEmailConfirmation() == false) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantConfirmAttachingEmail); + + var sendSms = await this.SendSmsCode(); + if (sendSms != EResult.OK) + throw new AuthenticatorLinkerException($"Can't send SMS code: {sendSms} ({(int)sendSms})"); + Logger?.LogDebug("SMS code sent"); + } + + Logger?.LogInformation("Starting LinkRequest"); + var resp = await this.LinkRequest(); + + var mobileData = resp.ToMobileDataExtended(DeviceId!, data.Clone()); + Options.BackupHandler?.Invoke(mobileData); + + if (resp.Status == 29) + { + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.AuthenticatorPresent); + } + + if (resp.Status != 1) + { + throw new AuthenticatorLinkerException(resp.Status); + } + + + string code; + var isPhone = resp.ConfirmType is 1 or 2; + if (isPhone) + { + if (Options.SmsCodeProvider == null) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.NoSmsCodeProvider); + + var smsCode = await Options.SmsCodeProvider.GetSmsCode(Consumer, phoneNumber, resp.PhoneNumberHint); + code = smsCode.ToString(); + } + else if (resp.ConfirmType == 3) + { + if (Options.EmailProvider == null) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.NoEmailProvider); + + code = await Options.EmailProvider.GetAddAuthenticatorCode(Consumer); + } + else + { + throw new ArgumentOutOfRangeException(nameof(resp.ConfirmType), resp.ConfirmType, + $"ConfirmType {resp.ConfirmType} not supported"); + } + + Logger?.LogInformation("Finalizing link"); + var result = await this.FinalizeLink(code, resp.SharedSecret, isPhone); + + if (result.Success == false) + { + var error = result.Error switch + { + LinkError.GeneralFailure => throw new AuthenticatorLinkerException(result.Code!.Value) { OnFinalization = true }, + LinkError.BadConfirmationCode => AuthenticatorLinkerError.BadConfirmationCode, + LinkError.UnableToGenerateCorrectCodes => AuthenticatorLinkerError.UnableToGenerateCorrectCodes, + _ => throw new ArgumentOutOfRangeException(nameof(result.Error), result.Error, $"LinkError {result.Error} not supported") + }; + + throw new AuthenticatorLinkerException(error) { OnFinalization = true }; + } + + Logger?.LogInformation("Linking completed"); + return mobileData; + } + + + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs new file mode 100644 index 0000000..826b5f4 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs @@ -0,0 +1,8 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class AccountRecoveryConfirmation : Confirmation +{ + public AccountRecoveryConfirmation(long id, ulong nonce, long creator, string typeName) : base(id, nonce, 6, creator, ConfirmationType.AccountRecovery, typeName) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs new file mode 100644 index 0000000..6b904af --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs @@ -0,0 +1,44 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class Confirmation +{ + public long Id { get; } + + /// + /// The unique key used to act upon this confirmation. + /// + public ulong Nonce { get; } + + /// + /// The value of the data-type HTML attribute returned for this contribution. + /// + public int IntType; + + /// + /// Represents either the Trade Offer ID or market transaction ID that caused this confirmation to be created. + /// + public long CreatorId { get; } + + /// + /// The type of this confirmation. + /// + public readonly ConfirmationType ConfType; + + public string TypeName { get; init; } + public string HeadLine { get; init; } = string.Empty; + + public DateTime Time { get; set; } + + public virtual ConfirmationDetails? Details { get; } + + public Confirmation(long id, ulong nonce, int intType, long creatorId, ConfirmationType confType, string typeName) + { + Id = id; + Nonce = nonce; + IntType = intType; + CreatorId = creatorId; + ConfType = confType; + TypeName = typeName; + } + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs new file mode 100644 index 0000000..74433ef --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs @@ -0,0 +1,13 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class MarketConfirmation : Confirmation +{ + + public string ItemImageUri { get; init; } = string.Empty; + public string ItemName { get; init; } = string.Empty; + public string PriceString { get; init; } = string.Empty; + public MarketConfirmation(long id, ulong key, long creator, string typeName) : base(id, key, 3, creator, ConfirmationType.MarketSellTransaction, typeName) + { + + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs new file mode 100644 index 0000000..65354f1 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs @@ -0,0 +1,8 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class RegisterApiKeyConfirmation : Confirmation +{ + public RegisterApiKeyConfirmation(long id, ulong nonce, long creatorId, string typeName) : base(id, nonce, 8, creatorId, ConfirmationType.RegisterApiKey, typeName) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs new file mode 100644 index 0000000..7cce95a --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs @@ -0,0 +1,16 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class TradeConfirmation : Confirmation +{ + public string UserAvatarUri { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public bool IsReceiveNothing { get; set; } + public override TradeConfirmationDetails? Details { get; } + + public long TradeId => CreatorId; + + public TradeConfirmation(long id, ulong nonce, long creator, string typeName) : base(id, nonce, 1, creator, ConfirmationType.Trade, typeName) + { + + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs b/SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs new file mode 100644 index 0000000..903ee74 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs @@ -0,0 +1,11 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public enum ConfirmationType +{ + Unknown, + Trade = 2, + MarketSellTransaction = 3, + AccountRecovery = 6, + RegisterApiKey = 9 + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs new file mode 100644 index 0000000..a307751 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs @@ -0,0 +1,6 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class ConfirmationDetails +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs new file mode 100644 index 0000000..936e564 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs @@ -0,0 +1,6 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class MarketConfirmationDetails : ConfirmationDetails +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs new file mode 100644 index 0000000..92aeaa8 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs @@ -0,0 +1,6 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class TradeConfirmationDetails : ConfirmationDetails +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs b/SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs new file mode 100644 index 0000000..edd77b1 --- /dev/null +++ b/SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs @@ -0,0 +1,65 @@ +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Text; +using SteamLib.Core.Interfaces; + +namespace SteamLib.SteamMobile; + +public class SteamGuardCodeGenerator : ISteamGuardProvider +{ + private static readonly byte[] SteamGuardCodeTranslations = { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 }; + + public string SharedSecret { get; } + + int ISteamGuardProvider.MaxRetryCount => ProviderMaxRetryCount; + public int ProviderMaxRetryCount { get; set; } + + public SteamGuardCodeGenerator(string sharedSecret) + { + SharedSecret = sharedSecret; + } + + + public ValueTask GetSteamGuardCode(ILoginConsumer caller) => ValueTask.FromResult(GenerateCode()); + public string GenerateCode() => GenerateCode(SharedSecret); + + + public static string GenerateCode(string sharedSecret) => GenerateCode(sharedSecret, TimeAligner.GetSteamTime()); + public static string GenerateCode(string sharedSecret, long time) + { + var sharedSecretUnescaped = Regex.Unescape(sharedSecret); + var sharedSecretArray = Convert.FromBase64String(sharedSecretUnescaped); + return GenerateCode(sharedSecretArray, time); + } + + public static string GenerateCode(byte[] sharedSecret, long time) + { + var timeArray = new byte[8]; + + time /= 30L; + + for (var i = 8; i > 0; i--) + { + timeArray[i - 1] = (byte)time; + time >>= 8; + } + + HMACSHA1 hmacGenerator = new() + { + Key = sharedSecret + }; + var hashedData = hmacGenerator.ComputeHash(timeArray); + var codeArray = new byte[5]; + + var b = (byte)(hashedData[19] & 0xF); + var codePoint = (hashedData[b] & 0x7F) << 24 | (hashedData[b + 1] & 0xFF) << 16 | (hashedData[b + 2] & 0xFF) << 8 | (hashedData[b + 3] & 0xFF); + + for (var i = 0; i < 5; ++i) + { + codeArray[i] = SteamGuardCodeTranslations[codePoint % SteamGuardCodeTranslations.Length]; + codePoint /= SteamGuardCodeTranslations.Length; + } + + return Encoding.UTF8.GetString(codeArray); + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/TimeAligner.cs b/SteamLibForked/SteamMobile/TimeAligner.cs new file mode 100644 index 0000000..cee4720 --- /dev/null +++ b/SteamLibForked/SteamMobile/TimeAligner.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using AchiesUtilities.Models; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using SteamLib.Core; + +namespace SteamLib.SteamMobile; + + +//TODO: Refactor +[PublicAPI] +public static class TimeAligner +{ + private static bool _aligned; + private static int _timeDifference; + public static UnixTimeStamp UtcNow => UnixTimeStamp.FromDateTime(DateTime.UtcNow); + + private const string TIME_ALIGN_ENDPOINT = "ITwoFactorService/QueryTime/v0001"; + + + public static async ValueTask GetSteamTimeAsync() + { + if (!_aligned) + { + await AlignTimeAsync(); + } + return UtcNow.ToLong() + _timeDifference; + } + + public static long GetSteamTime() + { + if (!_aligned) + { + AlignTime(); + } + return UtcNow.ToLong() + _timeDifference; + } + + public static void AlignTime() + { + var client = new HttpClient(); + var sw = new Stopwatch(); + try + { + sw.Start(); + var req = new HttpRequestMessage(HttpMethod.Post, SteamConstants.STEAM_API + TIME_ALIGN_ENDPOINT); + var response = client.Send(req).EnsureSuccessStatusCode(); + sw.Stop(); + var stream = new StreamReader(response.Content.ReadAsStream()); + var respStr = stream.ReadToEnd(); + stream.Dispose(); + var j = JObject.Parse(respStr); + var time = j["response"]!["server_time"]!.Value(); + var now = UtcNow - sw.Elapsed; + _timeDifference = (int)(time - now.ToLong()); + _aligned = true; + } + finally + { + client.Dispose(); + } + } + + public static async Task AlignTimeAsync() + { + var client = new HttpClient(); + var sw = new Stopwatch(); + try + { + sw.Start(); + var response = await client.PostAsync(SteamConstants.STEAM_API + TIME_ALIGN_ENDPOINT, null); + sw.Stop(); + var respStr = await response.Content.ReadAsStringAsync(); + var j = JObject.Parse(respStr); + var time = j["response"]!["server_time"]!.Value(); + var now = UtcNow - sw.Elapsed; + _timeDifference = (int)(time - now.ToLong()); + _aligned = true; + + } + finally + { + sw.Stop(); + client.Dispose(); + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/EncryptionHelper.cs b/SteamLibForked/Utility/EncryptionHelper.cs new file mode 100644 index 0000000..8b0fdec --- /dev/null +++ b/SteamLibForked/Utility/EncryptionHelper.cs @@ -0,0 +1,74 @@ +using System.Security.Cryptography; +using System.Text; + +namespace SteamLib.Utility; + +public static class EncryptionHelper +{ + public static byte[] HexStringToByteArray(string hex) + { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + + public static string ToBase64EncryptedPassword(string keyExp, string keyMod, string password) + { + // RSA Encryption. + RSACryptoServiceProvider rsa = new(); + RSAParameters rsaParameters = new() + { + Exponent = HexStringToByteArray(keyExp), + Modulus = HexStringToByteArray(keyMod) + }; + rsa.ImportParameters(rsaParameters); + + // Encrypt the password and convert it. + byte[] bytePassword = Encoding.ASCII.GetBytes(password); + byte[] encodedPassword = rsa.Encrypt(bytePassword, false); + return Convert.ToBase64String(encodedPassword); + } + + public static string GenerateConfirmationHash(long time, string identitySecret, string tag = "conf") + { + var hashedBytes = GenerateConfirmationHashBytes(time, identitySecret, tag); + var encodedData = Convert.ToBase64String(hashedBytes, Base64FormattingOptions.None); + var hash = encodedData; + return hash; + } + public static byte[] GenerateConfirmationHashBytes(long time, string identitySecret, string tag = "conf") + { + byte[] decode = Convert.FromBase64String(identitySecret); + int n2; + if (tag.Length > 32) + { + n2 = 8 + 32; + } + else + { + n2 = 8 + tag.Length; + } + byte[] array = new byte[n2]; + int n3 = 8; + while (true) + { + int n4 = n3 - 1; + if (n3 <= 0) + { + break; + } + array[n4] = (byte)time; + time >>= 8; + n3 = n4; + } + Array.Copy(Encoding.UTF8.GetBytes(tag), 0, array, 8, n2 - 8); + + var hmacGenerator = new HMACSHA1 + { + Key = decode + }; + var hashedData = hmacGenerator.ComputeHash(array); + return hashedData; + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/EnvironmentUtility.cs b/SteamLibForked/Utility/EnvironmentUtility.cs new file mode 100644 index 0000000..a3111a2 --- /dev/null +++ b/SteamLibForked/Utility/EnvironmentUtility.cs @@ -0,0 +1,224 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace SteamLib.Utility; + +public static class EnvironmentUtility +{ + // ReSharper disable once InconsistentNaming + public static EOSType GetOSType() + { + var osVer = Environment.OSVersion; + var ver = osVer.Version; + + return osVer.Platform switch + { + PlatformID.Win32Windows => ver.Minor switch + { + 0 => EOSType.Win95, + 10 => EOSType.Win98, + 90 => EOSType.WinME, + _ => EOSType.WinUnknown, + }, + + PlatformID.Win32NT => ver.Major switch + { + 4 => EOSType.WinNT, + 5 => ver.Minor switch + { + 0 => EOSType.Win2000, + 1 => EOSType.WinXP, + // Assume nobody runs Windows XP Professional x64 Edition + // It's an edition of Windows Server 2003 anyway. + 2 => EOSType.Win2003, + _ => EOSType.WinUnknown, + }, + 6 => ver.Minor switch + { + 0 => EOSType.WinVista, // Also Server 2008 + 1 => EOSType.Windows7, // Also Server 2008 R2 + 2 => EOSType.Windows8, // Also Server 2012 + // Note: The OSVersion property reports the same version number (6.2.0.0) for both Windows 8 and Windows 8.1.- http://msdn.microsoft.com/en-us/library/system.environment.osversion(v=vs.110).aspx + // In practice, this will only get hit if the application targets Windows 8.1 in the app manifest. + // See http://msdn.microsoft.com/en-us/library/windows/desktop/dn481241(v=vs.85).aspx for more info. + 3 => EOSType.Windows81, // Also Server 2012 R2 + _ => EOSType.WinUnknown, + }, + 10 when ver.Build >= 22000 => EOSType.Win11, + 10 => EOSType.Windows10,// Also Server 2016, Server 2019, Server 2022 + _ => EOSType.WinUnknown, + }, + + // The specific minor versions only exist in Valve's enum for LTS versions + PlatformID.Unix when RuntimeInformation.IsOSPlatform(OSPlatform.Linux) => ver.Major switch + { + 2 => ver.Minor switch + { + 2 => EOSType.Linux22, + 4 => EOSType.Linux24, + 6 => EOSType.Linux26, + _ => EOSType.LinuxUnknown, + }, + 3 => ver.Minor switch + { + 2 => EOSType.Linux32, + 5 => EOSType.Linux35, + 6 => EOSType.Linux36, + 10 => EOSType.Linux310, + 16 => EOSType.Linux316, + 18 => EOSType.Linux318, + _ => EOSType.Linux3x, + }, + 4 => ver.Minor switch + { + 1 => EOSType.Linux41, + 4 => EOSType.Linux44, + 9 => EOSType.Linux49, + 14 => EOSType.Linux414, + 19 => EOSType.Linux419, + _ => EOSType.Linux4x, + }, + 5 => ver.Minor switch + { + 4 => EOSType.Linux54, + 10 => EOSType.Linux510, + _ => EOSType.Linux5x, + }, + 6 => EOSType.Linux6x, + 7 => EOSType.Linux7x, + _ => EOSType.LinuxUnknown, + }, + + PlatformID.Unix when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) => ver.Major switch + { + 11 => EOSType.MacOS107, // "Lion" + 12 => EOSType.MacOS108, // "Mountain Lion" + 13 => EOSType.MacOS109, // "Mavericks" + 14 => EOSType.MacOS1010, // "Yosemite" + 15 => EOSType.MacOS1011, // El Capitan + 16 => EOSType.MacOS1012, // Sierra + 17 => EOSType.Macos1013, // High Sierra + 18 => EOSType.Macos1014, // Mojave + 19 => EOSType.Macos1015, // Catalina + 20 => EOSType.MacOS11, // Big Sur + 21 => EOSType.MacOS12, // Monterey + _ => EOSType.MacOSUnknown, + }, + + _ => EOSType.Unknown, + }; + } +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum EOSType +{ + Unknown = -1, + Web = -700, + IOSUnknown = -600, + IOS1 = -599, + IOS2 = -598, + IOS3 = -597, + IOS4 = -596, + IOS5 = -595, + IOS6 = -594, + IOS6_1 = -593, + IOS7 = -592, + IOS7_1 = -591, + IOS8 = -590, + IOS8_1 = -589, + IOS8_2 = -588, + IOS8_3 = -587, + IOS8_4 = -586, + IOS9 = -585, + IOS9_1 = -584, + IOS9_2 = -583, + IOS9_3 = -582, + IOS10 = -581, + IOS10_1 = -580, + IOS10_2 = -579, + IOS10_3 = -578, + IOS11 = -577, + IOS11_1 = -576, + IOS11_2 = -575, + IOS11_3 = -574, + IOS11_4 = -573, + IOS12 = -572, + IOS12_1 = -571, + AndroidUnknown = -500, + Android6 = -499, + Android7 = -498, + Android8 = -497, + Android9 = -496, + UMQ = -400, + PS3 = -300, + MacOSUnknown = -102, + MacOS104 = -101, + MacOS105 = -100, + MacOS1058 = -99, + MacOS106 = -95, + MacOS1063 = -94, + MacOS1064_slgu = -93, + MacOS1067 = -92, + MacOS107 = -90, + MacOS108 = -89, + MacOS109 = -88, + MacOS1010 = -87, + MacOS1011 = -86, + MacOS1012 = -85, + Macos1013 = -84, + Macos1014 = -83, + Macos1015 = -82, + MacOS1016 = -81, + MacOS11 = -80, + MacOS111 = -79, + MacOS1017 = -78, + MacOS12 = -77, + MacOS13 = -76, + MacOSMax = -1, + LinuxUnknown = -203, + Linux22 = -202, + Linux24 = -201, + Linux26 = -200, + Linux32 = -199, + Linux35 = -198, + Linux36 = -197, + Linux310 = -196, + Linux316 = -195, + Linux318 = -194, + Linux3x = -193, + Linux4x = -192, + Linux41 = -191, + Linux44 = -190, + Linux49 = -189, + Linux414 = -188, + Linux419 = -187, + Linux5x = -186, + Linux54 = -185, + Linux6x = -184, + Linux7x = -183, + Linux510 = -182, + LinuxMax = -101, + WinUnknown = 0, + Win311 = 1, + Win95 = 2, + Win98 = 3, + WinME = 4, + WinNT = 5, + Win2000 = 6, + WinXP = 7, + Win2003 = 8, + WinVista = 9, + Windows7 = 10, + Win2008 = 11, + Win2012 = 12, + Windows8 = 13, + Windows81 = 14, + Win2012R2 = 15, + Windows10 = 16, + Win2016 = 17, + Win2019 = 18, + Win2022 = 19, + Win11 = 20, + WinMAX = 21, +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs b/SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs new file mode 100644 index 0000000..0bbca7a --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; + +namespace SteamLib.Utility.MaFiles; + +public enum DeserializedMafileSessionResult +{ + Missing, + Invalid, + Expired, + Valid, +} +public class DeserializedMafileData +{ + public int? Version { get; init; } + public bool IsExtended { get; init; } + public DeserializedMafileSessionResult SessionResult { get; init; } + public Dictionary? UnusedProperties { get; init; } = null; + public HashSet? MissingProperties { get; init; } = new(); + + + public bool IsActual => Version == MafileSerializer.MAFILE_VERSION; + public bool IsLegacy => Version == 0; + public bool HasUnusedProperties => UnusedProperties is { Count: > 0 }; + public bool HasMissingProperties => MissingProperties is { Count: > 0 }; + + internal static DeserializedMafileData Create(int? version = null, bool isExtended = false, Dictionary? properties = null, HashSet? missingProperties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Missing) + { + return new DeserializedMafileData + { + Version = version, + UnusedProperties = properties, + IsExtended = isExtended, + MissingProperties = missingProperties, + SessionResult = sessionResult + }; + } + + internal static DeserializedMafileData CreateActual(Dictionary? properties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Valid) + { + return new DeserializedMafileData + { + Version = MafileSerializer.MAFILE_VERSION, + UnusedProperties = properties, + IsExtended = true, + SessionResult = sessionResult + }; + } +} + + +/// +/// Representation class just for displaying deserialization result +/// +[PublicAPI] +public class DeserializedMafileResult +{ + public static readonly HashSet ImportantProperties = new() + { + nameof(MobileDataExtended.RevocationCode), + nameof(MobileDataExtended.AccountName), + }; + + public static readonly HashSet NotImportantProperties = new() + { + nameof(MobileDataExtended.ServerTime), + nameof(MobileDataExtended.SerialNumber), + nameof(MobileDataExtended.Uri), + nameof(MobileDataExtended.TokenGid), + nameof(MobileDataExtended.Secret1), + }; + + public int? Version { get; init; } + + public bool IsExtended { get; init; } + public DeserializedMafileSessionResult SessionResult { get; init; } + + public HashSet? MissingImportantProperties { get; init; } = new(); + public HashSet? MissingProperties { get; init; } = new(); + public Dictionary? UnusedProperties { get; init; } + + public bool IsActual => Version == MafileSerializer.MAFILE_VERSION; + public bool HasUnusedProperties => UnusedProperties is { Count: > 0 }; + public bool HasMissingProperties => MissingProperties is { Count: > 0 }; + public bool HasMissingImportantProperties => MissingImportantProperties is { Count: > 0 }; + public bool HasSession => SessionResult == DeserializedMafileSessionResult.Valid; + + + public static DeserializedMafileResult FromData(DeserializedMafileData data) + { + HashSet? missingImportantProperties = null; + HashSet? missingProperties = null; + if (data is { IsExtended: true, HasMissingProperties: true }) + { + var important = data.MissingProperties?.Intersect(ImportantProperties).ToList(); + if (important?.Count > 0) missingImportantProperties = important.ToHashSet(); + var notImportant = data.MissingProperties?.Intersect(NotImportantProperties).ToList(); + if (notImportant?.Count > 0) missingProperties = notImportant.ToHashSet(); + } + + return new DeserializedMafileResult + { + Version = data.Version, + IsExtended = data.IsExtended, + MissingImportantProperties = missingImportantProperties, + MissingProperties = missingProperties, + UnusedProperties = data.UnusedProperties?.ToDictionary(x => x.Key, x => JTokenToString(x.Value)), + SessionResult = data.SessionResult + }; + } + + private static string JTokenToString(JProperty property) + { + switch (property.Value.Type) + { + case JTokenType.None: + case JTokenType.Null: + return "null"; + case JTokenType.Object: + return "object"; + case JTokenType.Array: + return "array"; + case JTokenType.Integer: + return property.Value.Value().ToString(); + case JTokenType.Float: + return property.Value.Value().ToString(CultureInfo.InvariantCulture); + case JTokenType.String: + return property.Value.Value()!; + case JTokenType.Boolean: + return property.Value.Value().ToString(); + case JTokenType.Date: + return property.Value.Value().ToString(CultureInfo.InvariantCulture); + case JTokenType.Guid: + return property.Value.Value().ToString(); + case JTokenType.Uri: + return property.Value.Value()!.ToString(); + case JTokenType.TimeSpan: + return property.Value.Value().ToString(); + default: + return "unknown"; + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/LegacyMafile.cs b/SteamLibForked/Utility/MaFiles/LegacyMafile.cs new file mode 100644 index 0000000..b540b33 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/LegacyMafile.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using SteamLib.Web.Converters; + +namespace SteamLib.Utility.MaFiles; + +internal class LegacyMafile //TODO: move +{ + + [JsonProperty("shared_secret")] + [JsonRequired] + public string SharedSecret { get; set; } + + [JsonProperty("identity_secret")] + [JsonRequired] + public string IdentitySecret { get; set; } + + [JsonProperty("device_id")] + [JsonRequired] + public string DeviceId { get; set; } + + [JsonProperty("revocation_code")] + public string RevocationCode { get; set; } + + [JsonProperty("account_name")] + public string AccountName { get; set; } + + [JsonProperty("Session")] + public object? SessionData { get; set; } + [JsonProperty("server_time")] public long ServerTime { get; set; } //Unused + + [JsonProperty("serial_number")] + [JsonConverter(typeof(StringToLongIfNeededConverter))] + public string SerialNumber { get; set; } //Unused + [JsonProperty("uri")] public string Uri { get; set; } //Unused + [JsonProperty("token_gid")] public string TokenGid { get; set; } //Unused + [JsonProperty("secret_1")] public string Secret1 { get; set; } //Unused + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileCredits.cs b/SteamLibForked/Utility/MaFiles/MafileCredits.cs new file mode 100644 index 0000000..67d29d2 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileCredits.cs @@ -0,0 +1,17 @@ +namespace SteamLib.Utility.MaFiles; + +public class MafileCredits : IMafileCredits +{ + internal static readonly MafileCredits Instance = new(); + private const string ORIGINAL_AUTHOR = "Achies"; + private const string MOBILE_APP = "https://github.com/achiez/NebulaAuth"; + + public string OriginalAuthor => ORIGINAL_AUTHOR; + public string BestOpenSourceMobileApp => MOBILE_APP; + +} + +public interface IMafileCredits +{ + string OriginalAuthor { get; } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer.cs new file mode 100644 index 0000000..9f34bb8 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer.cs @@ -0,0 +1,136 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamLib.Account; + +namespace SteamLib.Utility.MaFiles; + +public static partial class MafileSerializer +{ + public const int MAFILE_VERSION = 2; + + public const string SIGNATURE_PROPERTY_NAME = "@SLSV"; + private static readonly HashSet ActualProperties = typeof(MobileDataExtended).GetProperties().Select(x => x.Name).ToHashSet(); + + + public static MobileData Deserialize(string json, out DeserializedMafileData mafileData) + { + + var j = JObject.Parse(json); + + var unusedProperties = j.Properties().ToDictionary(x => x.Name, x => x); + var versionToken = GetToken(j, SIGNATURE_PROPERTY_NAME); + unusedProperties.Remove(CREDITS_PROPERTY_NAME); + int? version = null; + if (versionToken is { Type: JTokenType.Integer }) + { + version = versionToken.Value(); + } + + unusedProperties.Remove(SIGNATURE_PROPERTY_NAME); + if (version == MAFILE_VERSION) + { + try + { + var unused = new Dictionary(); + foreach (var (key, prop) in unusedProperties) + { + if (ActualProperties.Contains(key)) + continue; + unused.Add(key, prop); + } + + var data = j.ToObject()!; + data.SessionData = Validate.ValidateMobileData(data, out var sessionResult); + + mafileData = DeserializedMafileData.CreateActual(unused, sessionResult); + return data; + + } + catch (Exception ex) + { + throw new JsonSerializationException("MafileSerializer found actual 'Version' signature but some error occurred during deserialization", ex); + } + } + + var sharedSecretToken = GetTokenOrThrow(j, nameof(MobileData.SharedSecret), unusedProperties, "sharedsecret", "shared_secret", "shared"); + var identitySecretToken = GetTokenOrThrow(j, nameof(MobileData.IdentitySecret), unusedProperties, "identitysecret", "identity_secret", "identity"); + var deviceIdToken = GetTokenOrThrow(j, nameof(MobileData.DeviceId), unusedProperties, "deviceid", "device_id", "device"); + + + var sharedSecret = GetBase64(nameof(MobileData.SharedSecret), sharedSecretToken); + var identitySecret = GetBase64(nameof(MobileData.IdentitySecret), identitySecretToken); + var deviceId = deviceIdToken.Value(); + Validate.NotNullOrEmpty(nameof(MobileData.DeviceId), deviceId); + + var accountNameToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.AccountName), "account_name", "accountname"); + var revocationCodeToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.RevocationCode), "revocation_code", "rcode", "r_code", "revocationcode"); + var sessionDataToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.SessionData), "session_data", "sessiondata", "session"); + + var accountName = accountNameToken?.Value(); + var revocationCode = revocationCodeToken?.Value(); + + + var minified + = string.IsNullOrWhiteSpace(accountName) + && string.IsNullOrWhiteSpace(revocationCode) + && (sessionDataToken == null || sessionDataToken.Type == JTokenType.Null); + + + if (minified) + { + mafileData = DeserializedMafileData.Create(version, false, unusedProperties); + return new MobileData + { + DeviceId = deviceId, + IdentitySecret = identitySecret, + SharedSecret = sharedSecret + }; + } + + + var missingProperties = new List(); + if (string.IsNullOrWhiteSpace(accountName)) missingProperties.Add(nameof(MobileDataExtended.AccountName)); + if (string.IsNullOrWhiteSpace(revocationCode)) missingProperties.Add(nameof(MobileDataExtended.RevocationCode)); + + var serverTime = GetLong(j, unusedProperties, nameof(MobileDataExtended.ServerTime), "server_time", "servertime"); + var serialNumber = GetULong(j, unusedProperties, nameof(MobileDataExtended.SerialNumber), "serial_number", "serialnumber"); + var uri = GetString(j,unusedProperties, nameof(MobileDataExtended.Uri), "url", "uri"); + var tokenGid = GetString(j, unusedProperties, nameof(MobileDataExtended.TokenGid), "token_gid", "tokengid"); + var secret1 = GetString(j, unusedProperties, nameof(MobileDataExtended.Secret1), "secret_1", "seecret1"); + + if(serverTime == null) missingProperties.Add(nameof(MobileDataExtended.ServerTime)); + if(serialNumber == null) missingProperties.Add(nameof(MobileDataExtended.SerialNumber)); + if(string.IsNullOrWhiteSpace(uri)) missingProperties.Add(nameof(MobileDataExtended.Uri)); + if(string.IsNullOrWhiteSpace(tokenGid)) missingProperties.Add(nameof(MobileDataExtended.TokenGid)); + if(string.IsNullOrWhiteSpace(secret1)) missingProperties.Add(nameof(MobileDataExtended.Secret1)); + + MobileSessionData? sessionData = null; + var sResult = DeserializedMafileSessionResult.Missing; + if (sessionDataToken is { Type: JTokenType.Object }) + { + sessionData = DeserializeMobileSessionData((JObject) sessionDataToken, out sResult); + } + + + mafileData = DeserializedMafileData.Create(version, true, unusedProperties, missingProperties.ToHashSet(), sResult); + return new MobileDataExtended + { + DeviceId = deviceId, + IdentitySecret = identitySecret, + SharedSecret = sharedSecret, + AccountName = accountName ?? string.Empty, + RevocationCode = revocationCode, + ServerTime = serverTime.GetValueOrDefault(), + SerialNumber = serialNumber.GetValueOrDefault(), + Uri = uri ?? string.Empty, + TokenGid = tokenGid ?? string.Empty, + Secret1 = secret1 ?? string.Empty, + SessionData = sessionData + }; + + } + + + + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs new file mode 100644 index 0000000..bdbac5f --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json.Linq; +using SteamLib.Account; +using SteamLib.Authentication; +using SteamLib.Core.Enums; + +namespace SteamLib.Utility.MaFiles; + +public partial class MafileSerializer //SessionData +{ + private static MobileSessionData? DeserializeMobileSessionData(JObject j, out DeserializedMafileSessionResult result) + { + result = DeserializedMafileSessionResult.Invalid; + var refreshTokenToken = GetToken(j, nameof(MobileSessionData.RefreshToken), "refreshtoken", "refresh_token", + "refresh", "OAuthToken"); + + + SteamAuthToken refreshToken; + if (refreshTokenToken == null || refreshTokenToken.Type == JTokenType.Null) return null; + if (refreshTokenToken.Type == JTokenType.String && SteamTokenHelper.TryParse(refreshTokenToken.Value()!, out var parsed)) + { + refreshToken = parsed; + } + else if (refreshTokenToken.Type == JTokenType.Object) + { + try + { + refreshToken = refreshTokenToken.ToObject(); + } + catch + { + return null; + } + } + else + { + return null; + } + + if (refreshToken.IsExpired || refreshToken.Type != SteamAccessTokenType.MobileRefresh) + { + result = DeserializedMafileSessionResult.Expired; + return null; + } + + var sessionId = GetString(j, "sessionid", "session_id", "session"); + var accessTokenToken = GetToken(j, "accesstoken", "access_token", "access"); + accessTokenToken ??= GetToken(j, "steamLoginSecure"); + + if (string.IsNullOrWhiteSpace(sessionId)) return null; + + SteamAuthToken? accessToken = null; + if (accessTokenToken == null || accessTokenToken.Type == JTokenType.Null) + { + accessToken = null; + } + if (accessTokenToken is { Type: JTokenType.Object }) + { + try + { + accessToken = refreshTokenToken.ToObject(); + } + catch + { + // ignored + } + } + else if (accessTokenToken is { Type: JTokenType.String } && + SteamTokenHelper.TryParse(accessTokenToken.Value()!, out var token) && token.Type == SteamAccessTokenType.Mobile) + { + accessToken = token; + } + + + var sessionData = new MobileSessionData(sessionId, refreshToken.SteamId, refreshToken, accessToken, new Dictionary()); + sessionData.IsValid = SessionDataValidator.Validate(null, sessionData).Succeeded; + if(sessionData.IsValid == false) + return null; + result = DeserializedMafileSessionResult.Valid; + return sessionData; + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs new file mode 100644 index 0000000..43486ff --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs @@ -0,0 +1,132 @@ +using Newtonsoft.Json.Linq; + +namespace SteamLib.Utility.MaFiles; + +public partial class MafileSerializer //Utility +{ + private static JToken? GetToken(JObject j, params string[] aliases) + { + foreach (var name in aliases) + { + if (j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) + { + return token; + } + + } + return null; + } + + private static JToken? GetToken(JObject j, Dictionary removeFrom, params string[] aliases) + { + foreach (var name in aliases) + { + if (!j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) continue; + var parent = token.Parent as JProperty; + removeFrom.Remove(parent!.Name); + return token; + + } + return null; + } + + private static JToken GetTokenOrThrow(JObject j, string propertyName, Dictionary removeFrom, params string[] aliases) + { + foreach (var name in aliases) + { + if (!j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) continue; + if (token.Type == JTokenType.Null) + { + throw new ArgumentException($"Required property {propertyName} is null"); + } + var parent = token.Parent as JProperty; + removeFrom.Remove(parent!.Name); + return token; + + } + + throw new ArgumentException($"Required property {propertyName} not found"); + } + + + private static string? GetString(JObject j, params string[] aliases) + { + var token = GetToken(j, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + + return token.Value(); + } + + private static string? GetString(JObject j, Dictionary removeFrom, + params string[] aliases) + { + var token = GetToken(j, removeFrom, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + + + return token.Value(); + + } + + private static long? GetLong(JObject j, Dictionary removeFrom, + params string[] aliases) + { + var token = GetToken(j, removeFrom, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.String when long.TryParse(token.Value()!, out var r) => r, + _ => null + }; + } + + private static ulong? GetULong(JObject j, Dictionary removeFrom, + params string[] aliases) + { + var token = GetToken(j, removeFrom, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.String when ulong.TryParse(token.Value()!, out var r) => r, + _ => null + }; + } + + private static string GetBase64(string propertyName, JToken token) + { + if (token.Type == JTokenType.Null) throw new ArgumentNullException(propertyName, $"{propertyName} is null"); + if (token.Type == JTokenType.String) + { + var result = token.Value()!; + Validate.IsValidBase64(propertyName, result); + return result; + } + else if (token.Type == JTokenType.Array) + { + try + { + var result = token.Value()!; + var base64 = Convert.ToBase64String(result); + return base64; + } + catch (Exception e) + { + throw new ArgumentException($"{propertyName}'s Array is not valid base64", e); + } + } + + throw new ArgumentOutOfRangeException(nameof(token.Type), + $"Not valid token type for base64 property '{propertyName}'. Type: {token.Type}"); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs new file mode 100644 index 0000000..7e7acfa --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json.Linq; +using SteamLib.Account; +using SteamLib.Authentication; +using SteamLib.Core.Interfaces; + +namespace SteamLib.Utility.MaFiles; + +public partial class MafileSerializer //Validate +{ + private static class Validate + { + public static void NotNull(string name, [System.Diagnostics.CodeAnalysis.NotNull] object? o) + { + if (o == null) + { + throw new ArgumentNullException(name, $"{name} is null"); + } + } + public static void NotNull(string name, [System.Diagnostics.CodeAnalysis.NotNull] JToken? o) + { + if (o == null || o.Type == JTokenType.Null) + { + throw new ArgumentNullException(name, $"{name} is null"); + } + } + + + public static void NotNullOrEmpty(string name, [System.Diagnostics.CodeAnalysis.NotNull] string? s) + { + if (string.IsNullOrWhiteSpace(s)) + { + throw new ArgumentException($"{name} is null or empty"); + } + } + + + + public static void IsValidBase64(string name, string base64) + { + var buffer = new Span(new byte[base64.Length]); + if(Convert.TryFromBase64String(base64, buffer, out _) == false) + throw new ArgumentException($"{name} is not valid base64 string"); + + } + + public static MobileSessionData? ValidateMobileData(MobileData data, out DeserializedMafileSessionResult sessionResult) + { + + NotNullOrEmpty(nameof(data.SharedSecret), data.SharedSecret); + NotNullOrEmpty(nameof(data.IdentitySecret), data.IdentitySecret); + NotNullOrEmpty(nameof(data.DeviceId), data.DeviceId); + IsValidBase64(nameof(data.SharedSecret), data.SharedSecret); + IsValidBase64(nameof(data.IdentitySecret), data.IdentitySecret); + + sessionResult = DeserializedMafileSessionResult.Missing; + if (data is MobileDataExtended d) + { + NotNullOrEmpty(nameof(MobileDataExtended.AccountName), d.AccountName); + NotNullOrEmpty(nameof(MobileDataExtended.RevocationCode), d.RevocationCode); + if (d.SessionData == null) return null; + + sessionResult = DeserializedMafileSessionResult.Invalid; + if (d.SessionData.RefreshToken.IsExpired) + { + sessionResult = DeserializedMafileSessionResult.Expired; + return null; + } + + d.SessionData.IsValid = SessionDataValidator.Validate(null, d.SessionData).Succeeded; + if (d.SessionData.IsValid == false) return null; + + sessionResult = DeserializedMafileSessionResult.Valid; + return d.SessionData; + } + return null; + } + + public static bool ValidateSessionData(ISessionData sessionData, out bool isOutdated) + { + + if (sessionData.RefreshToken.IsExpired) + { + isOutdated = true; + return false; + } + + isOutdated = false; + return SessionDataValidator.Validate(null, sessionData).Succeeded; + + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs new file mode 100644 index 0000000..89a2b84 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs @@ -0,0 +1,111 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Xml; +using Formatting = Newtonsoft.Json.Formatting; + +namespace SteamLib.Utility.MaFiles; + +public static partial class MafileSerializer //Write +{ + private const string CREDITS_PROPERTY_NAME = "Credits"; + public static string Serialize(MobileData mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null) + { + using var w = new StringWriter(); + using var write = new JsonTextWriter(w); + write.Formatting = formatting; + var j = JObject.FromObject(mobileData); + j.Add(SIGNATURE_PROPERTY_NAME, MAFILE_VERSION); + if (sign) + { + credits ??= MafileCredits.Instance; + var obj = JObject.FromObject(credits); + j.Add(CREDITS_PROPERTY_NAME, obj); + } + j.WriteTo(write); + return w.ToString(); + + + } + + public static async Task SerializeAsync(MobileData mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null) + { + await using var w = new StringWriter(); + await using var write = new JsonTextWriter(w); + write.Formatting = formatting; + var j = JObject.FromObject(mobileData); + j.Add(SIGNATURE_PROPERTY_NAME, j); + if (sign) + { + credits ??= MafileCredits.Instance; + var obj = JObject.FromObject(credits); + j.Add(CREDITS_PROPERTY_NAME, obj); + } + + + await j.WriteToAsync(write); + return w.ToString(); + + } + + public static string SerializeLegacy(MobileData mobileData, Formatting formatting, Dictionary? additionalProperties = null, bool sign = true, MafileCredits? credits = null) + { + var result = new LegacyMafile + { + SharedSecret = mobileData.SharedSecret, + IdentitySecret = mobileData.IdentitySecret, + DeviceId = mobileData.DeviceId, + + }; + + if (mobileData is MobileDataExtended ext) + { + result.RevocationCode = ext.RevocationCode ?? string.Empty; + result.AccountName = ext.AccountName; + result.SessionData = ext.SessionData == null + ? null + : new + { + AccesToken = ext.SessionData?.MobileToken?.Token, + steamLoginSecure = ext.SessionData?.MobileToken?.SignedToken, + RefreshToken = ext.SessionData?.RefreshToken.Token, + SteamID = ext.SessionData?.SteamId.Steam64.Id, + SessionID = ext.SessionData?.SessionId + }; + result.ServerTime = ext.ServerTime; + result.SerialNumber = ext.SerialNumber.ToString(); + result.Uri = ext.Uri; + result.TokenGid = ext.TokenGid; + result.Secret1 = ext.Secret1; + + } + + + using var w = new StringWriter(); + using var write = new JsonTextWriter(w); + write.Formatting = formatting; + var j = JObject.FromObject(result); + if (additionalProperties != null) + { + foreach (var (name, value) in additionalProperties) + { + JToken? jToken = null; + if (value != null) + { + jToken = JToken.FromObject(value); + } + + j.Add(name, jToken); + } + } + + if (sign) + { + credits ??= MafileCredits.Instance; + var obj = JObject.FromObject(credits); + j.Add(CREDITS_PROPERTY_NAME, obj); + } + j.WriteTo(write); + return w.ToString(); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/Models/Enumeration.cs b/SteamLibForked/Utility/Models/Enumeration.cs new file mode 100644 index 0000000..a5dbc9d --- /dev/null +++ b/SteamLibForked/Utility/Models/Enumeration.cs @@ -0,0 +1,44 @@ +using System.Reflection; + +namespace SteamLib.Utility.Models; + +public abstract class Enumeration(int id, string name) : IComparable +{ + public string Name { get; } = name; + public int Id { get; } = id; + + public override string ToString() => Name; + + //public static IEnumerable GetAll() where T : Enumeration => + // typeof(T).GetFields(BindingFlags.Public | + // BindingFlags.Static | + // BindingFlags.DeclaredOnly) + // .Select(f => f.GetValue(null)) + // .Cast(); + + public static IEnumerable GetAll() where T : Enumeration + { + var res = typeof(T).GetFields(BindingFlags.Public | + BindingFlags.Static | + BindingFlags.DeclaredOnly) + .Select(f => f.GetValue(null)) + .Cast(); + return res; + } + + public override bool Equals(object? obj) + { + if (obj is not Enumeration otherValue) + { + return false; + } + return GetType() == obj.GetType() && Id.Equals(otherValue.Id); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Id, GetType()); + } + + public int CompareTo(object? other) => Id.CompareTo(((Enumeration?)other)?.Id); +} \ No newline at end of file diff --git a/SteamLibForked/Utility/SteamIdParser.cs b/SteamLibForked/Utility/SteamIdParser.cs new file mode 100644 index 0000000..d0a622a --- /dev/null +++ b/SteamLibForked/Utility/SteamIdParser.cs @@ -0,0 +1,157 @@ +using System.Text.RegularExpressions; +using SteamLib.Account; + +namespace SteamLib.Utility; + +public static class SteamIdParser +{ + + public static readonly Regex Steam64Regex = new("^7656119([0-9]{10})$", RegexOptions.Compiled); + public static readonly Regex Steam2Regex = new("STEAM_(?[0-9]):(?[0-9]):(?[0-9]{1,10})", RegexOptions.Compiled); + public static readonly Regex Steam3Regex = new("^\\[?(?[a-zA-Z]):1:(?[0-9]{1,10})\\]$", RegexOptions.Compiled); + + + #region TryParse + public static bool TryParse(string input, out SteamId result) + { + if (TryParse64(input, out var steam64)) + { + result = new SteamId(steam64); + return true; + } + + if (TryParse2(input, out var steam2)) + { + result = new SteamId(steam2); + return true; + } + + if (TryParse3(input, out var steam3)) + { + result = new SteamId(steam3); + return true; + } + + + result = new SteamId(new SteamId2(0, 0)); + return false; + } + + public static bool TryParse64(string input, out SteamId64 result) + { + var match64 = Steam64Regex.Match(input); + if (match64.Success) + { + result = new SteamId64(long.Parse(match64.Value)); + return true; + } + + result = default; + return false; + } + + public static bool TryParse2(string input, out SteamId2 result) + { + var match2 = Steam2Regex.Match(input); + if (match2.Success) + { + var universe = byte.Parse(match2.Groups["universe"].Value); + var lowestBit = byte.Parse(match2.Groups["lowestBit"].Value); + var highestBits = int.Parse(match2.Groups["highestBits"].Value); + result = new SteamId2(universe, lowestBit, highestBits); + return true; + } + + result = default; + return false; + } + + public static bool TryParse3(string input, out SteamId3 result) + { + var match3 = Steam3Regex.Match(input); + if (match3.Success) + { + var type = match3.Groups["type"].Value[0]; + var id = int.Parse(match3.Groups["id"].Value); + result = new SteamId3(id, type); + return true; + } + + + result = default; + return false; + } + #endregion + + #region Parse + + public static SteamId Parse(string input) + { + if (TryParse64(input, out var steam64)) + { + return new SteamId(steam64); + } + + if (TryParse2(input, out var steam2)) + { + return new SteamId(steam2); + + } + + if (TryParse3(input, out var steam3)) + { + return new SteamId(steam3); + } + + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId64, SteamId2 or SteamId3."); + } + + public static SteamId64 Parse64(string input) + { + var match64 = Steam64Regex.Match(input); + if (match64.Success) + { + return new SteamId64(long.Parse(match64.Value)); + } + else + { + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId64."); + } + + } + + public static SteamId2 Parse2(string input) + { + var match2 = Steam2Regex.Match(input); + if (match2.Success) + { + var universe = byte.Parse(match2.Groups["universe"].Value); + var lowestBit = byte.Parse(match2.Groups["lowestBit"].Value); + var highestBits = int.Parse(match2.Groups["highestBits"].Value); + return new SteamId2(universe, lowestBit, highestBits); + } + else + { + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId2."); + } + } + + public static SteamId3 Parse3(string input) + { + var match3 = Steam3Regex.Match(input); + if (match3.Success) + { + var type = match3.Groups["type"].Value[0]; + var id = int.Parse(match3.Groups["id"].Value); + return new SteamId3(id, type); + + } + else + { + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId3."); + } + } + + #endregion + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/Utilities.cs b/SteamLibForked/Utility/Utilities.cs new file mode 100644 index 0000000..76c7046 --- /dev/null +++ b/SteamLibForked/Utility/Utilities.cs @@ -0,0 +1,28 @@ +using System.Text.RegularExpressions; + +namespace SteamLib.Utility; + +public static class Utilities //TODO: refactor +{ + public static int GetSuccessCode(string response) + { + Regex regexInt = new("\"success\":(\\d*)"); + var matchInt = regexInt.Match(response); + + if (!string.IsNullOrEmpty(matchInt.Groups[1].Value)) + return Convert.ToInt32(matchInt.Groups[1].Value); + + Regex regexBool = new("\"success\":(\\w*)"); + var matchBool = regexBool.Match(response); + + if (!string.IsNullOrEmpty(matchBool.Groups[1].Value)) + return bool.Parse(matchBool.Groups[1].Value) ? 1 : 0; + return 0; + } + + public static FormUrlEncodedContent ToFormContent(this IDictionary dic) => new(dic); //TODO: убрать и зарефакторить + + public static int DivideByDecimal(this int i, decimal value) => (int) (i / value); + public static int DivideByDouble(this int i, double value) => (int)(i / value); + +} \ No newline at end of file diff --git a/SteamLibForked/Web/ClientBuilder.cs b/SteamLibForked/Web/ClientBuilder.cs new file mode 100644 index 0000000..65505d9 --- /dev/null +++ b/SteamLibForked/Web/ClientBuilder.cs @@ -0,0 +1,51 @@ +using System.Net; +using System.Net.Http.Headers; +using AchiesUtilities.Web.Models; +using SteamLib.Authentication; +using SteamLib.Core.Interfaces; + +namespace SteamLib.Web; + +public static class ClientBuilder +{ + public static HttpClientHandlerPair BuildMobileClient(IWebProxy? proxy, IMobileSessionData? sessionData, bool disposeHandler = true) + { + sessionData?.EnsureValidated(); + var handler = new HttpClientHandler(); + var client = new HttpClient(handler, disposeHandler); + + client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/javascript, text/html, application/xml, text/xml, */*"); + client.DefaultRequestHeaders.UserAgent.ParseAdd("okhttp/3.12.12"); + + if (proxy != null) + { + handler.Proxy = proxy; + } + + var container = handler.CookieContainer; + if (sessionData == null) + { + container.ClearMobileSessionCookies(); + } + else + { + container.SetSteamMobileCookies(sessionData); + } + + ConfigureCommon(handler, client); + return new HttpClientHandlerPair(client, handler); + + } + + + private static void ConfigureCommon(HttpClientHandler handler, HttpClient client) + { + ConfigureCommonClient(client); + handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; + } + private static void ConfigureCommonClient(HttpClient client) + { + client.Timeout = TimeSpan.FromSeconds(50); + client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US")); + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Converters/SteamIdConverters.cs b/SteamLibForked/Web/Converters/SteamIdConverters.cs new file mode 100644 index 0000000..a0ada56 --- /dev/null +++ b/SteamLibForked/Web/Converters/SteamIdConverters.cs @@ -0,0 +1,106 @@ +using Newtonsoft.Json; +using SteamLib.Account; + +namespace SteamLib.Web.Converters; + +public class SteamIdToSteam64Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId value, JsonSerializer serializer) + { + writer.WriteValue(value.Steam64.ToLong()); + } + + public override SteamId ReadJson(JsonReader reader, Type objectType, SteamId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.Value is long l) + { + return SteamId.FromSteam64(l); + } + + var str = (string) reader.Value!; + return new SteamId(SteamId64.Parse(str)); + } +} + +public class SteamIdToSteam2Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId value, JsonSerializer serializer) + { + writer.WriteValue(value.Steam2.ToString()); + } + + public override SteamId ReadJson(JsonReader reader, Type objectType, SteamId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + + var str = (string)reader.Value!; + return new SteamId(SteamId2.Parse(str)); + } +} + + +public class SteamIdToSteam3Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId value, JsonSerializer serializer) + { + writer.WriteValue(value.Steam3.ToString()); + } + + public override SteamId ReadJson(JsonReader reader, Type objectType, SteamId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + + var str = (string)reader.Value!; + return new SteamId(SteamId3.Parse(str)); + } +} + +public class Steam64ToLongConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId64 value, JsonSerializer serializer) + { + writer.WriteValue(value.ToLong()); + } + + public override SteamId64 ReadJson(JsonReader reader, Type objectType, SteamId64 existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.Value is long l) + { + return new SteamId64(l); + } + var str = (string)reader.Value!; + return SteamId64.Parse(str); + } +} + +public class SteamId2ToStringConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId2 value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override SteamId2 ReadJson(JsonReader reader, Type objectType, SteamId2 existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var str = (string)reader.Value!; + return SteamId2.Parse(str); + } +} + +public class SteamId3ToStringConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId3 value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override SteamId3 ReadJson(JsonReader reader, Type objectType, SteamId3 existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var str = (string)reader.Value!; + return SteamId3.Parse(str); + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs b/SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs new file mode 100644 index 0000000..24ff267 --- /dev/null +++ b/SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace SteamLib.Web.Converters; + +public class StringToLongIfNeededConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, string? value, JsonSerializer serializer) + { + writer.WriteValue(value); + } + + public override string ReadJson(JsonReader reader, Type objectType, string? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value?.ToString() ?? "null"; + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/Account/UserWalletSchema.cs b/SteamLibForked/Web/Models/Account/UserWalletSchema.cs new file mode 100644 index 0000000..9a65fcd --- /dev/null +++ b/SteamLibForked/Web/Models/Account/UserWalletSchema.cs @@ -0,0 +1,15 @@ +using AchiesUtilities.Newtonsoft.JSON.Converters.Common; +using Newtonsoft.Json; + +namespace SteamLib.Web.Models.Account; + +#pragma warning disable CS8618 +public class UserWalletSchema +{ + [JsonProperty("amount")] + [JsonConverter(typeof(IntToStringConverter))] + public int Amount { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/Account/WalletSchema.cs b/SteamLibForked/Web/Models/Account/WalletSchema.cs new file mode 100644 index 0000000..1134134 --- /dev/null +++ b/SteamLibForked/Web/Models/Account/WalletSchema.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +#pragma warning disable CS8618 + +namespace SteamLib.Web.Models.Account; + +public class WalletInfoSchema +{ + [JsonProperty("success")] + public int Success { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("country_code")] + public string CountryCode { get; set; } + + [JsonProperty("alternate_min_amount")] + public bool AlternateMinAmount { get; set; } + + [JsonProperty("amounts")] + public List Amounts { get; set; } + + [JsonProperty("related_trans_type")] + public bool RelatedTransType { get; set; } + + [JsonProperty("related_trainsid")] + public bool RelatedTrainsId { get; set; } + + [JsonProperty("user_wallet")] + public UserWalletSchema UserWalletSchema { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs b/SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs new file mode 100644 index 0000000..92556e9 --- /dev/null +++ b/SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +#pragma warning disable CS8618 + +namespace SteamLib.Web.Models.GlobalMarketInfo; + +[PublicAPI] +public class GlobalInfoModel +{ + [MemberNotNullWhen(true, nameof(SteamId))] + public bool IsLoggedIn { get; set; } + + /// + /// if is or wallet doesn't exist + /// + public MarketWalletSchema? WalletInfo { get; set; } + public bool RequiresBillingInfo { get; set; } + public bool HasBillingStates { get; set; } + public string CountryCode { get; set; } + public string Language { get; set; } + public long? SteamId { get; set; } + public string SessionId { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs b/SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs new file mode 100644 index 0000000..f11a5f9 --- /dev/null +++ b/SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs @@ -0,0 +1,61 @@ +using AchiesUtilities.Newtonsoft.JSON.Converters.Common; +using Newtonsoft.Json; +using SteamLib.Core.Enums; + +#pragma warning disable CS8618 + +namespace SteamLib.Web.Models.GlobalMarketInfo; + +public class MarketWalletSchema +{ + [JsonProperty("wallet_currency")] + public Currency WalletCurrency { get; set; } + + [JsonProperty("wallet_country")] + public string WalletCountry { get; set; } + + [JsonProperty("wallet_state")] + public string WalletState { get; set; } + + [JsonProperty("wallet_fee")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletFee { get; set; } + + [JsonProperty("wallet_fee_minimum")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletFeeMinimum { get; set; } + + [JsonProperty("wallet_fee_percent")] + [JsonConverter(typeof(StringToDoubleConverter))] + public double WalletFeePercent { get; set; } + + [JsonProperty("wallet_publisher_fee_percent_default")] + [JsonConverter(typeof(StringToDoubleConverter))] + public double WalletPublisherFeePercentDefault { get; set; } + + [JsonProperty("wallet_fee_base")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletFeeBase { get; set; } + + [JsonProperty("wallet_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletBalance { get; set; } + + [JsonProperty("wallet_delayed_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletDelayedBalance { get; set; } + + [JsonProperty("wallet_max_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletMaxBalance { get; set; } + + [JsonProperty("wallet_trade_max_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletTradeMaxBalance { get; set; } + + [JsonProperty("success")] + public int Success { get; set; } + + [JsonProperty("rwgrsn")] + public int Rwgrsn { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/Mobile/Confirmations.cs b/SteamLibForked/Web/Models/Mobile/Confirmations.cs new file mode 100644 index 0000000..6ea4788 --- /dev/null +++ b/SteamLibForked/Web/Models/Mobile/Confirmations.cs @@ -0,0 +1,41 @@ +using AchiesUtilities.Models; +using Newtonsoft.Json; +using SteamLib.SteamMobile.Confirmations; + +namespace SteamLib.Web.Models.Mobile; + +public class ConfirmationsJson +{ + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("needauth")] + public bool NeedAuth { get; set; } + + [JsonProperty("message")] + public string? Message { get; set; } + + + [JsonProperty("detail")] + public string? Detail { get; set; } + [JsonProperty("conf")] public List Conf { get; set; } = new(); + +} + + +public class ConfirmationJson +{ + [JsonProperty("type")] public ConfirmationType Type { get; set; } + [JsonProperty("type_name")] public string TypeName { get; set; } = string.Empty; + [JsonProperty("id")] public long Id { get; set; } + [JsonProperty("creator_id")] public long CreatorId { get; set; } + [JsonProperty("nonce")] public ulong Nonce { get; set; } + [JsonProperty("creation_time")] public UnixTimeStamp CreationTime { get; set; } + [JsonProperty("cancel")] public string Cancel { get; set; } = string.Empty; + [JsonProperty("accept")] public string Confirm { get; set; } = string.Empty; + [JsonProperty("icon")] public Uri? Icon { get; set; } + [JsonProperty("multi")] public bool Multi { get; set; } + [JsonProperty("headline")] public string Headline { get; set; } = string.Empty; + [JsonProperty("summary")] public List Summary { get; set; } = null!; + [JsonProperty("warn")] public List Warn { get; set; } = null!; +} diff --git a/SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs b/SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs new file mode 100644 index 0000000..216b324 --- /dev/null +++ b/SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs @@ -0,0 +1,80 @@ +using System.Text.RegularExpressions; +using HtmlAgilityPack; +using Newtonsoft.Json; +using SteamLib.Web.Models.GlobalMarketInfo; + +namespace SteamLib.Web.Scrappers.HTML; + +public static class MarketGlobalInfoScrapper +{ + private static readonly Regex LoggedInRegex = new("var g_bLoggedIn = (.+);", RegexOptions.Compiled); + private static readonly Regex ReqBillingInfoRegex = new("var g_bRequiresBillingInfo = (.+);", RegexOptions.Compiled); + private static readonly Regex CountryCodeRegex = new("var g_strCountryCode = \"(.+)\";", RegexOptions.Compiled); + private static readonly Regex HasBillingStates = new("var g_bHasBillingStates = (.+);", RegexOptions.Compiled); + private static readonly Regex LanguageRegex = new("var g_strLanguage = \"(.+)\";", RegexOptions.Compiled); + private static readonly Regex WalletInfoRegex = new("var g_rgWalletInfo = (.+);", RegexOptions.Compiled); + + private static readonly Regex SessionIdRegex = new("g_sessionID = \"(.+)\";", RegexOptions.Compiled); + private static readonly Regex SteamIdRegex = new("g_steamID = \"(.+)\";", RegexOptions.Compiled); + + public static GlobalInfoModel Scrap(string html) + { + var document = new HtmlDocument(); + document.LoadHtml(html); + + var scriptNodes = document.DocumentNode.SelectNodes("//*[@id=\"responsive_page_template_content\"]/script"); + var index = scriptNodes.Count > 2 ? 1 : 0; //If account is limited or market unavailable elements will be displaced + + var scriptNode = scriptNodes[index]; + var script = scriptNode.InnerText!; + + var logged = GetBool(script, LoggedInRegex); + var hasBillingStates = GetBool(script, HasBillingStates); + var reqBillingInfo = GetBool(script, ReqBillingInfoRegex); + + var country = CountryCodeRegex.Match(html).Groups[1].Value; + var language = LanguageRegex.Match(html).Groups[1].Value; + + + var walletInfoStr = WalletInfoRegex.Match(html).Groups[1].Value; + MarketWalletSchema? wallet = null; + + var sessionScriptNode = document.DocumentNode.SelectSingleNode($"//div[@class='responsive_page_content']/script"); + var sessionScript = sessionScriptNode.InnerText!; + + var sessionId = SessionIdRegex.Match(sessionScript).Groups[1].Value; + long? steamId = null; + + + + if (logged) + { + wallet = JsonConvert.DeserializeObject(walletInfoStr); + var steamIdStr = SteamIdRegex.Match(sessionScript).Groups[1].Value; + steamId = long.Parse(new string(steamIdStr.Where(char.IsDigit).ToArray())); + } + + + + return new GlobalInfoModel + { + CountryCode = country, + HasBillingStates = hasBillingStates, + IsLoggedIn = logged, + Language = language, + RequiresBillingInfo = reqBillingInfo, + WalletInfo = wallet, + SessionId = sessionId, + SteamId = steamId + }; + + + + } + private static bool GetBool(string script, Regex regex) + { + var value = regex.Match(script).Groups[1].Value; + return bool.Parse(value); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs b/SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs new file mode 100644 index 0000000..2b4f2fd --- /dev/null +++ b/SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs @@ -0,0 +1,137 @@ +using Newtonsoft.Json; +using SteamLib.Exceptions; +using SteamLib.Exceptions.General; +using SteamLib.SteamMobile.Confirmations; +using SteamLib.Web.Models.Mobile; + +namespace SteamLib.Web.Scrappers.JSON; + +public static class MobileConfirmationScrapper +{ + public static Dictionary ErrorMessages { get; } = new() + { + {"Oh nooooooes!", LoadConfirmationsError.TryAgainLater}, + {"You are not set up to receive mobile confirmations", LoadConfirmationsError.NotSetupToReceiveConfirmations} + }; + + public static List Scrap(string response) + { + ConfirmationsJson conf; + try + { + conf = JsonConvert.DeserializeObject(response, StaticJson.DefaultSettings)!; + } + catch (Exception ex) + { + throw new UnsupportedResponseException(response, ex); + } + + if (conf.NeedAuth) + { + throw new SessionExpiredException(); + } + + if (conf is {Success: false, Message: not null}) + { + var error = LoadConfirmationsError.Unknown; + if (ErrorMessages.TryGetValue(conf.Message, out var e)) + { + error = e; + } + + throw new CantLoadConfirmationsException(conf.Message) + { + Error = error + }; + + } + + if (conf.Success == false) + { + throw new UnsupportedResponseException(response); + } + + + var result = new List(); + if (conf.Conf.Count == 0) return result; + + + foreach (var confirmationJson in conf.Conf) + { + result.Add(GetConcrete(confirmationJson)); + } + + return result; + } + + + private static Confirmation GetConcrete(ConfirmationJson json) + { + return json.Type switch + { + ConfirmationType.Trade => GetTradeConfirmation(json), + ConfirmationType.AccountRecovery => GetAccountRecoveryConfirmation(json), + ConfirmationType.MarketSellTransaction => GetMarketConfirmation(json), + ConfirmationType.RegisterApiKey => GetRegisterApiKeyConfirmation(json), + _ => new Confirmation(json.Id, json.Nonce, (int)json.Type, json.CreatorId, json.Type, json.TypeName) + { + Time = json.CreationTime.ToLocalDateTime() + }, + }; + } + + private static TradeConfirmation GetTradeConfirmation(ConfirmationJson json) + { + + var avatarUri = json.Icon; + + var userName = json.Headline; + + + var receiveNodeText = string.Empty; + if (json.Summary.Count > 0) + { + receiveNodeText = json.Summary[1]; + } + + var receiveNothing = receiveNodeText == "You will receive nothing"; + + + return new TradeConfirmation(json.Id, json.Nonce, json.CreatorId, json.TypeName) + { + UserAvatarUri = avatarUri!.ToString(), + UserName = userName, + IsReceiveNothing = receiveNothing, + Time = json.CreationTime.ToLocalDateTime() + }; + } + + private static AccountRecoveryConfirmation GetAccountRecoveryConfirmation(ConfirmationJson confirmation) + { + return new AccountRecoveryConfirmation(confirmation.Id, confirmation.Nonce, confirmation.CreatorId, confirmation.TypeName) + { + Time = confirmation.CreationTime.ToLocalDateTime() + }; + } + private static RegisterApiKeyConfirmation GetRegisterApiKeyConfirmation(ConfirmationJson confirmation) + { + return new RegisterApiKeyConfirmation(confirmation.Id, confirmation.Nonce, confirmation.CreatorId, confirmation.TypeName) + { + Time = confirmation.CreationTime.ToLocalDateTime() + }; + } + + private static MarketConfirmation GetMarketConfirmation(ConfirmationJson confirmation) //TODO: test this + { + + var itemName = confirmation.Headline; + var itemImageUri = confirmation.Icon?.ToString() ?? string.Empty; + return new MarketConfirmation(confirmation.Id, confirmation.Nonce, confirmation.CreatorId, confirmation.TypeName) + { + Time = confirmation.CreationTime.ToLocalDateTime(), + ItemImageUri = itemImageUri, + ItemName = itemName, + PriceString = string.Empty //TODO: + }; + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Scrappers/JSON/StaticJson.cs b/SteamLibForked/Web/Scrappers/JSON/StaticJson.cs new file mode 100644 index 0000000..4571186 --- /dev/null +++ b/SteamLibForked/Web/Scrappers/JSON/StaticJson.cs @@ -0,0 +1,18 @@ +using AchiesUtilities.Newtonsoft.JSON; +using Newtonsoft.Json; + +namespace SteamLib.Web.Scrappers.JSON; + +public static class StaticJson //TODO: remove? +{ + public static JsonSerializerSettings DefaultSettings { get; } + + + static StaticJson() + { + DefaultSettings = new JsonSerializerSettings + { + Converters = StaticConverters.AllDefaultConverters + }; + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs b/SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs new file mode 100644 index 0000000..6f0a404 --- /dev/null +++ b/SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs @@ -0,0 +1,28 @@ +using SteamLib.Core; +using SteamLib.Exceptions.General; +using SteamLib.Web.Models.GlobalMarketInfo; +using SteamLib.Web.Scrappers.HTML; + +namespace SteamLib.Web; + +public static partial class SteamWebApi +{ + /// + /// Login is not required + /// + /// + /// + /// + public static async Task GetMarketGlobalInfo(HttpClient client, CancellationToken cancellationToken = default) + { + var resp = await client.GetStringAsync(SteamConstants.STEAM_MARKET, cancellationToken); + try + { + return MarketGlobalInfoScrapper.Scrap(resp); + } + catch (Exception ex) + { + throw new UnsupportedResponseException(resp, ex); + } + } +} \ No newline at end of file