diff --git a/src/NebulaAuth/Converters/Converters.xaml b/src/NebulaAuth/Converters/Converters.xaml
index 97888f1..8ef5bd7 100644
--- a/src/NebulaAuth/Converters/Converters.xaml
+++ b/src/NebulaAuth/Converters/Converters.xaml
@@ -15,6 +15,7 @@
+
diff --git a/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs b/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs
new file mode 100644
index 0000000..f39d5b4
--- /dev/null
+++ b/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NebulaAuth.Model;
+
+namespace NebulaAuth.Converters;
+
+///
+/// Converts a proxy ID (int) to the number of accounts assigned to it.
+/// Returns empty string when count is zero so the badge is invisible.
+///
+public class ProxyAccountCountConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not int proxyId) return string.Empty;
+ var count = ProxyAssignmentCache.GetAccountCount(proxyId);
+ return count > 0 ? count.ToString() : string.Empty;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
\ No newline at end of file
diff --git a/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json b/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json
index 9c61462..2464707 100644
--- a/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json
+++ b/src/NebulaAuth/Localization/dialogs.mafilemover.loc.json
@@ -42,14 +42,14 @@
"kk": "RCode: {0}\r\nФайл атауы: {1}.mafile"
},
"GuardIsNotActive": {
- "ru": "Steam Guard установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
- "en": "Steam Guard is set on this account. To link mafile to the account, use the 'Link' window",
- "zh": "此账户已启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。",
- "uk": "Steam Guard встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'",
- "fr": "Steam Guard est configuré sur ce compte. Pour lier mafile au compte, utilisez la fenêtre 'Lien'",
- "es": "Steam Guard está activado en esta cuenta. Para vincular el mafile a la cuenta, usa la ventana 'Vincular'",
- "tr": "Steam Guard bu hesapta zaten etkin. Mafile'ı hesaba bağlamak için 'Bağla' penceresini kullanın",
- "kk": "Бұл аккаунтта Steam Guard орнатылған. Mafile-ды аккаунтқа байланыстыру үшін 'Байланыстыру' терезесін пайдаланыңыз"
+ "ru": "Steam Guard НЕ установлен на данном аккаунте. Чтобы привязать mafile к аккаунту, воспользуйтесь окном \"Привязать\"",
+ "en": "Steam Guard is NOT set on this account. To link mafile to the account, use the 'Link' window",
+ "zh": "此账户尚未启用令牌。要将 mafile 绑定到此账户,请使用“绑定”窗口。",
+ "uk": "Steam Guard НЕ встановлено на цьому акаунті. Щоб прив'язати mafile до акаунту, скористайтеся вікном 'Прив'язати'",
+ "fr": "Steam Guard n'est PAS configuré sur ce compte. Pour lier le mafile au compte, utilisez la fenêtre 'Lier'",
+ "es": "Steam Guard NO está activado en esta cuenta. Para vincular el mafile a la cuenta, usa la ventana 'Vincular'",
+ "tr": "Steam Guard bu hesapta etkin DEĞİL. Mafile'ı hesaba bağlamak için 'Bağla' penceresini kullanın",
+ "kk": "Бұл аккаунтта Steam Guard орнатылмаған. Mafile-ды аккаунтқа байланыстыру үшін 'Байланыстыру' терезесін пайдаланыңыз"
},
"SeemsNoPhoneNumber": {
"en": "The account has no phone number linked, returned Fail code (2)",
diff --git a/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json b/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json
index 07193f8..04a7baf 100644
--- a/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json
+++ b/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json
@@ -40,6 +40,26 @@
"es": "El proxy tiene un formato incorrecto",
"tr": "Proxy yanlış formatta",
"kk": "Прокси қате форматта"
+ },
+ "AssignmentSuccess": {
+ "ru": "Прокси назначены: {0}",
+ "en": "Assigned: {0}",
+ "zh": "已分配:{0}",
+ "uk": "Проксі призначено: {0}",
+ "fr": "Attribués: {0}",
+ "es": "Asignados: {0}",
+ "tr": "Atandı: {0}",
+ "kk": "Тағайындалды: {0}"
+ },
+ "AssignmentPartialSuccess": {
+ "ru": "Назначено: {0}, не найдено: {1}, ошибки: {2}",
+ "en": "Assigned: {0}, not found: {1}, errors: {2}",
+ "zh": "已分配:{0},未找到:{1},错误:{2}",
+ "uk": "Призначено: {0}, не знайдено: {1}, помилки: {2}",
+ "fr": "Attribués: {0}, non trouvés: {1}, erreurs: {2}",
+ "es": "Asignados: {0}, no encontrados: {1}, errores: {2}",
+ "tr": "Atandı: {0}, bulunamadı: {1}, hatalar: {2}",
+ "kk": "Тағайындалды: {0}, табылмады: {1}, қателер: {2}"
}
}
},
@@ -93,6 +113,96 @@
"es": "Mostrar credenciales",
"tr": "Kimlik bilgilerini göster",
"kk": "Тіркелгі деректерін көрсету"
+ },
+ "TabManager": {
+ "en": "Manager",
+ "ru": "Менеджер",
+ "zh": "管理",
+ "uk": "Менеджер",
+ "fr": "Gestionnaire",
+ "es": "Administrador",
+ "tr": "Yönetici",
+ "kk": "Менеджер"
+ },
+ "TabAssignment": {
+ "en": "Assignment",
+ "ru": "Назначение",
+ "zh": "分配",
+ "uk": "Призначення",
+ "fr": "Attribution",
+ "es": "Asignación",
+ "tr": "Atama",
+ "kk": "Тағайындау"
+ },
+ "TotalAccounts": {
+ "en": "Accounts",
+ "ru": "Аккаунтов",
+ "zh": "账户",
+ "uk": "Акаунтів",
+ "fr": "Comptes",
+ "es": "Cuentas",
+ "tr": "Hesaplar",
+ "kk": "Аккаунттар"
+ },
+ "TotalProxies": {
+ "en": "Proxies",
+ "ru": "Прокси",
+ "zh": "代理",
+ "uk": "Проксі",
+ "fr": "Proxies",
+ "es": "Proxies",
+ "tr": "Proxy'ler",
+ "kk": "Проксилер"
+ },
+ "AssignedProxies": {
+ "en": "Assigned",
+ "ru": "Назначено",
+ "zh": "已分配",
+ "uk": "Призначено",
+ "fr": "Attribués",
+ "es": "Asignados",
+ "tr": "Atandı",
+ "kk": "Тағайындалды"
+ },
+ "UnspecifiedBehavior": {
+ "en": "Unspecified proxy:",
+ "ru": "Не указанный прокси:",
+ "zh": "未指定代理:",
+ "uk": "Не вказаний проксі:",
+ "fr": "Proxy non spécifié:",
+ "es": "Proxy no especificado:",
+ "tr": "Belirtilmemiş proxy:",
+ "kk": "Көрсетілмеген прокси:"
+ },
+ "UnspecifiedRandom": {
+ "en": "Assign random",
+ "ru": "Назначить случайный",
+ "zh": "随机分配",
+ "uk": "Призначити випадковий",
+ "fr": "Attribuer aléatoirement",
+ "es": "Asignar aleatorio",
+ "tr": "Rastgele ata",
+ "kk": "Кездейсоқ тағайындау"
+ },
+ "UnspecifiedRemove": {
+ "en": "Remove proxy",
+ "ru": "Удалить прокси",
+ "zh": "删除代理",
+ "uk": "Видалити проксі",
+ "fr": "Supprimer le proxy",
+ "es": "Eliminar proxy",
+ "tr": "Proxy'yi kaldır",
+ "kk": "Проксиді жою"
+ },
+ "ApplyAssignment": {
+ "en": "Apply",
+ "ru": "Применить",
+ "zh": "应用",
+ "uk": "Застосувати",
+ "fr": "Appliquer",
+ "es": "Aplicar",
+ "tr": "Uygula",
+ "kk": "Қолдану"
}
}
}
\ No newline at end of file
diff --git a/src/NebulaAuth/Localization/mainwindow.loc.json b/src/NebulaAuth/Localization/mainwindow.loc.json
index 13bcaec..8a54a68 100644
--- a/src/NebulaAuth/Localization/mainwindow.loc.json
+++ b/src/NebulaAuth/Localization/mainwindow.loc.json
@@ -894,6 +894,16 @@
"es": "Desvincular proxy",
"tr": "Proxy bağlantısını kaldır",
"kk": "Проксиді ажырату"
+ },
+ "AssignFreeProxy": {
+ "en": "Assign free proxy",
+ "ru": "Назначить свободный прокси",
+ "zh": "分配空闲代理",
+ "uk": "Призначити вільний проксі",
+ "fr": "Attribuer un proxy libre",
+ "es": "Asignar proxy libre",
+ "tr": "Serbest proxy ata",
+ "kk": "Бос проксиді тағайындау"
}
},
"Proxy": {
diff --git a/src/NebulaAuth/MainWindow.xaml b/src/NebulaAuth/MainWindow.xaml
index 6eb2983..47654ed 100644
--- a/src/NebulaAuth/MainWindow.xaml
+++ b/src/NebulaAuth/MainWindow.xaml
@@ -420,6 +420,9 @@
+
diff --git a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs
index 6395256..23555e6 100644
--- a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs
+++ b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs
@@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies();
- ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
+ ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData);
}
diff --git a/src/NebulaAuth/Model/MaClient.cs b/src/NebulaAuth/Model/MaClient.cs
index 115f5e3..74598a2 100644
--- a/src/NebulaAuth/Model/MaClient.cs
+++ b/src/NebulaAuth/Model/MaClient.cs
@@ -38,7 +38,7 @@ public static class MaClient
{
ClientHandler.CookieContainer.ClearAllCookies();
if (account == null) return;
- ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
+ ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData);
Proxy.SetData(account.Proxy?.Data);
}
diff --git a/src/NebulaAuth/Model/ProxyAssignmentCache.cs b/src/NebulaAuth/Model/ProxyAssignmentCache.cs
new file mode 100644
index 0000000..0a02f32
--- /dev/null
+++ b/src/NebulaAuth/Model/ProxyAssignmentCache.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AchiesUtilities.Collections;
+using AchiesUtilities.Web.Proxy;
+using NebulaAuth.Model.Entities;
+
+namespace NebulaAuth.Model;
+
+///
+/// In-memory cache tracking which proxy IDs are currently assigned to accounts.
+/// Provides fast lookup for free proxy selection without iterating all mafiles.
+/// Initialized at startup and kept in sync whenever proxy assignments change.
+///
+public static class ProxyAssignmentCache
+{
+ private static readonly Dictionary> _proxyToAccounts = new();
+
+ public static void Initialize(IEnumerable mafiles)
+ {
+ _proxyToAccounts.Clear();
+ foreach (var maf in mafiles)
+ {
+ if (maf.Proxy != null)
+ AddInternal(maf.Proxy.Id, maf.AccountName);
+ }
+ }
+
+ ///
+ /// Call whenever a proxy assignment on a mafile changes and is persisted to disk.
+ ///
+ public static void UpdateAssignment(string? accountName, int? oldProxyId, int? newProxyId)
+ {
+ if (accountName == null) return;
+ if (oldProxyId.HasValue) RemoveInternal(oldProxyId.Value, accountName);
+ if (newProxyId.HasValue) AddInternal(newProxyId.Value, accountName);
+ }
+
+ public static bool IsProxyFree(int proxyId)
+ {
+ return !_proxyToAccounts.TryGetValue(proxyId, out var set) || set.Count == 0;
+ }
+
+ public static int GetAccountCount(int proxyId)
+ {
+ return _proxyToAccounts.TryGetValue(proxyId, out var set) ? set.Count : 0;
+ }
+
+ ///
+ /// Returns a random free proxy (not the default proxy), or null if none available.
+ ///
+ public static KeyValuePair? GetRandomFreeProxy(
+ ObservableDictionary proxies,
+ int? excludeDefaultProxyId)
+ {
+ var free = proxies
+ .Where(kvp => kvp.Key != excludeDefaultProxyId && IsProxyFree(kvp.Key))
+ .ToList();
+ if (free.Count == 0) return null;
+ return free[Random.Shared.Next(free.Count)];
+ }
+
+ private static void AddInternal(int proxyId, string accountName)
+ {
+ if (!_proxyToAccounts.TryGetValue(proxyId, out var set))
+ {
+ set = new HashSet(StringComparer.OrdinalIgnoreCase);
+ _proxyToAccounts[proxyId] = set;
+ }
+
+ set.Add(accountName);
+ }
+
+ private static void RemoveInternal(int proxyId, string accountName)
+ {
+ if (_proxyToAccounts.TryGetValue(proxyId, out var set))
+ set.Remove(accountName);
+ }
+}
\ No newline at end of file
diff --git a/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs b/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs
index b644462..fe40011 100644
--- a/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs
+++ b/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs
@@ -30,7 +30,7 @@ public partial class SessionHandler //API
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
mafile.SetSessionData(mafile.SessionData);
await Storage.UpdateMafileAsync(mafile);
- chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
+ chp.Handler.CookieContainer.SetSteamMobileCookies(mafile.SessionData);
}
public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword)
diff --git a/src/NebulaAuth/Model/Shell.cs b/src/NebulaAuth/Model/Shell.cs
index 7a74a5d..f2ce3e5 100644
--- a/src/NebulaAuth/Model/Shell.cs
+++ b/src/NebulaAuth/Model/Shell.cs
@@ -45,6 +45,7 @@ public static class Shell
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
+ ProxyAssignmentCache.Initialize(Storage.MaFiles);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
diff --git a/src/NebulaAuth/Theme/Brushes.xaml b/src/NebulaAuth/Theme/Brushes.xaml
index a75f635..5c13e96 100644
--- a/src/NebulaAuth/Theme/Brushes.xaml
+++ b/src/NebulaAuth/Theme/Brushes.xaml
@@ -89,4 +89,7 @@
Color="{StaticResource SecondaryColor}" />
+
+
\ No newline at end of file
diff --git a/src/NebulaAuth/View/ProxyManagerView.xaml b/src/NebulaAuth/View/ProxyManagerView.xaml
index 6eea83f..6dfadf9 100644
--- a/src/NebulaAuth/View/ProxyManagerView.xaml
+++ b/src/NebulaAuth/View/ProxyManagerView.xaml
@@ -20,12 +20,12 @@
FontSize="16">
-
-
+
+
@@ -45,90 +45,41 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
-
+
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/NebulaAuth/ViewModel/MainVM.cs b/src/NebulaAuth/ViewModel/MainVM.cs
index 9390467..a9e5232 100644
--- a/src/NebulaAuth/ViewModel/MainVM.cs
+++ b/src/NebulaAuth/ViewModel/MainVM.cs
@@ -42,7 +42,7 @@ public partial class MainVM : ObservableObject
new MaProxy(kvp.Key, kvp.Value)));
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups();
- UpdateManager.CheckForUpdates(false);
+ UpdateManager.CheckForUpdates();
}
[RelayCommand]
diff --git a/src/NebulaAuth/ViewModel/MainVM_File.cs b/src/NebulaAuth/ViewModel/MainVM_File.cs
index 506e016..8825736 100644
--- a/src/NebulaAuth/ViewModel/MainVM_File.cs
+++ b/src/NebulaAuth/ViewModel/MainVM_File.cs
@@ -26,6 +26,11 @@ namespace NebulaAuth.ViewModel;
public partial class MainVM //File //TODO: Refactor
{
+ private record MafileReadResult(
+ Mafile? Mafile,
+ bool SdaPasswordPrompted,
+ SDAEncryptionHelper.Context? SdaContext);
+
public Settings Settings => Settings.Instance;
diff --git a/src/NebulaAuth/ViewModel/MainVM_Proxy.cs b/src/NebulaAuth/ViewModel/MainVM_Proxy.cs
index f7fca2d..795a555 100644
--- a/src/NebulaAuth/ViewModel/MainVM_Proxy.cs
+++ b/src/NebulaAuth/ViewModel/MainVM_Proxy.cs
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
+using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -38,9 +39,10 @@ public partial class MainVM
}
if (!system && SelectedMafile != null)
-
{
+ var oldProxyId = SelectedMafile.Proxy?.Id;
SelectedMafile.Proxy = SelectedProxy;
+ ProxyAssignmentCache.UpdateAssignment(SelectedMafile.AccountName, oldProxyId, SelectedProxy?.Id);
Storage.UpdateMafile(SelectedMafile);
}
@@ -74,7 +76,9 @@ public partial class MainVM
{
mafile ??= SelectedMafile;
if (mafile?.Proxy == null) return;
+ var oldProxyId = mafile.Proxy.Id;
mafile.Proxy = null;
+ ProxyAssignmentCache.UpdateAssignment(mafile.AccountName, oldProxyId, null);
if (mafile == SelectedMafile)
SetProxy(null, false); //Not system, triggered by user
}
@@ -85,6 +89,38 @@ public partial class MainVM
return mafile is {Proxy: not null};
}
+ [RelayCommand(CanExecute = nameof(AssignFreeProxyCanExecute))]
+ private void AssignFreeProxy(Mafile? mafile)
+ {
+ mafile ??= SelectedMafile;
+ if (mafile == null) return;
+
+ int? defaultProxyId = null;
+ if (MaClient.DefaultProxy != null)
+ {
+ var defKvp = ProxyStorage.Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy));
+ if (defKvp.Value != null)
+ defaultProxyId = defKvp.Key;
+ }
+
+ var freeProxy = ProxyAssignmentCache.GetRandomFreeProxy(ProxyStorage.Proxies, defaultProxyId);
+ if (freeProxy == null) return;
+
+ var newProxy = new MaProxy(freeProxy.Value.Key, freeProxy.Value.Value);
+ var oldProxyId = mafile.Proxy?.Id;
+ mafile.Proxy = newProxy;
+ ProxyAssignmentCache.UpdateAssignment(mafile.AccountName, oldProxyId, newProxy.Id);
+ Storage.UpdateMafile(mafile);
+
+ if (mafile == SelectedMafile)
+ SetProxy(newProxy, true); // system=true: update UI only, assignment already saved
+ }
+
+ private bool AssignFreeProxyCanExecute(Mafile? mafile)
+ {
+ return ProxyStorage.Proxies.Count > 0;
+ }
+
private void CheckProxyExist()
{
if (SelectedProxy == null)
diff --git a/src/NebulaAuth/ViewModel/Other/ProxyManagerVM.Assignment.cs b/src/NebulaAuth/ViewModel/Other/ProxyManagerVM.Assignment.cs
new file mode 100644
index 0000000..7b88370
--- /dev/null
+++ b/src/NebulaAuth/ViewModel/Other/ProxyManagerVM.Assignment.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AchiesUtilities.Web.Proxy;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using NebulaAuth.Model;
+using NebulaAuth.Model.Entities;
+using NebulaAuth.Model.Mafiles;
+using SteamLibForked.Models.SteamIds;
+
+namespace NebulaAuth.ViewModel.Other;
+
+public partial class ProxyManagerVM
+{
+ public int TotalAccounts => Storage.MaFiles.Count;
+ public int TotalProxies => ProxyStorage.Proxies.Count;
+ public int AssignedProxies => Storage.MaFiles.Count(m => m.Proxy != null);
+
+ [ObservableProperty] private string? _assignmentInput;
+ [ObservableProperty] private string? _assignmentTip;
+
+ ///
+ /// true = assign a random free proxy to unspecified accounts; false = remove proxy from unspecified accounts.
+ ///
+ [ObservableProperty] private bool _unspecifiedAssignRandom = true;
+
+ public void RefreshAssignmentCounters()
+ {
+ OnPropertyChanged(nameof(TotalAccounts));
+ OnPropertyChanged(nameof(TotalProxies));
+ OnPropertyChanged(nameof(AssignedProxies));
+ }
+
+ [RelayCommand]
+ private async Task ApplyAssignment()
+ {
+ AssignmentTip = null;
+ var input = AssignmentInput;
+ if (string.IsNullOrWhiteSpace(input)) return;
+
+ var lines = input.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
+ var mafs = Storage.MaFiles.ToList();
+ var defaultProxyId = GetDefaultProxyId();
+
+ var success = 0;
+ var errors = 0;
+ var notFound = 0;
+
+ foreach (var rawLine in lines)
+ {
+ var line = rawLine.Trim();
+ if (string.IsNullOrWhiteSpace(line)) continue;
+
+ try
+ {
+ string login;
+ // resolvedProxy: null means unspecified (apply toggle behaviour)
+ MaProxy? resolvedProxy;
+
+ var colonIdx = line.IndexOf(':');
+ if (colonIdx >= 0)
+ {
+ login = line[..colonIdx].Trim();
+ var proxyPart = line[(colonIdx + 1)..].Trim();
+
+ if (string.IsNullOrEmpty(proxyPart))
+ {
+ resolvedProxy = null; // unspecified
+ }
+ else if (int.TryParse(proxyPart, out var parsedId))
+ {
+ // Mode: login:ID — proxy ID
+ if (!ProxyStorage.Proxies.TryGetValue(parsedId, out var byId))
+ {
+ errors++;
+ continue;
+ }
+
+ resolvedProxy = new MaProxy(parsedId, byId);
+ }
+ else if (ProxyStorage.DefaultScheme.TryParse(proxyPart, out var parsedProxy))
+ {
+ // Mode: login:proxy_string — find existing or add
+ resolvedProxy = FindOrAddProxy(parsedProxy);
+ }
+ else
+ {
+ errors++;
+ continue;
+ }
+ }
+ else
+ {
+ login = line;
+ resolvedProxy = null; // unspecified
+ }
+
+ if (string.IsNullOrWhiteSpace(login))
+ {
+ errors++;
+ continue;
+ }
+
+ var maf = FindMafile(mafs, login);
+ if (maf == null)
+ {
+ notFound++;
+ continue;
+ }
+
+ var newProxy = resolvedProxy ?? (UnspecifiedAssignRandom
+ ? BuildRandomFreeProxy(defaultProxyId)
+ : null);
+
+ var oldProxyId = maf.Proxy?.Id;
+ maf.Proxy = newProxy;
+ ProxyAssignmentCache.UpdateAssignment(maf.AccountName, oldProxyId, newProxy?.Id);
+ await Storage.UpdateMafileAsync(maf);
+ success++;
+ }
+ catch
+ {
+ errors++;
+ }
+ }
+
+ AnyChanges = true;
+ RefreshAssignmentCounters();
+ AssignmentTip = BuildAssignmentTip(success, notFound, errors);
+ }
+
+ [RelayCommand]
+ private void ClearAssignmentTip()
+ {
+ AssignmentTip = null;
+ }
+
+ ///
+ /// Finds a proxy equal to in storage.
+ /// If not found, adds it and returns the new entry.
+ ///
+ private static MaProxy FindOrAddProxy(ProxyData proxyData)
+ {
+ var existing = ProxyStorage.Proxies.FirstOrDefault(kvp => kvp.Value.Equals(proxyData));
+ if (existing.Value != null)
+ return new MaProxy(existing.Key, existing.Value);
+
+ ProxyStorage.SetProxy(null, proxyData);
+
+ // Fetch the entry we just added (SetProxy guarantees it was stored)
+ var added = ProxyStorage.Proxies.First(kvp => kvp.Value.Equals(proxyData));
+ return new MaProxy(added.Key, added.Value);
+ }
+
+ private static MaProxy? BuildRandomFreeProxy(int? defaultProxyId)
+ {
+ var free = ProxyAssignmentCache.GetRandomFreeProxy(ProxyStorage.Proxies, defaultProxyId);
+ return free.HasValue ? new MaProxy(free.Value.Key, free.Value.Value) : null;
+ }
+
+ private static int? GetDefaultProxyId()
+ {
+ if (MaClient.DefaultProxy == null) return null;
+ var defKvp = ProxyStorage.Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy));
+ return defKvp.Value != null ? defKvp.Key : null;
+ }
+
+ private static Mafile? FindMafile(List mafs, string login)
+ {
+ SteamId64? steamId = null;
+ if (SteamId64.TryParse(login, out var id64))
+ steamId = id64;
+ if (steamId != null)
+ return mafs.FirstOrDefault(m => m.SteamId == steamId || m.AccountName == login);
+ return mafs.FirstOrDefault(m =>
+ string.Equals(m.AccountName, login, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static string BuildAssignmentTip(int success, int notFound, int errors)
+ {
+ if (success > 0 && errors == 0 && notFound == 0)
+ return string.Format(GetLocalizationOrDefault("AssignmentSuccess"), success);
+ return string.Format(GetLocalizationOrDefault("AssignmentPartialSuccess"), success, notFound, errors);
+ }
+}
\ No newline at end of file
diff --git a/src/SteamLibForked/Authentication/AdmissionHelper.cs b/src/SteamLibForked/Authentication/AdmissionHelper.cs
index 30ab7b2..57ece1c 100644
--- a/src/SteamLibForked/Authentication/AdmissionHelper.cs
+++ b/src/SteamLibForked/Authentication/AdmissionHelper.cs
@@ -54,33 +54,20 @@ public static class AdmissionHelper
return;
}
-
- AddRefreshToken(container, sessionData.RefreshToken);
+ container.SetSteamRefreshToken(sessionData.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/"));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/"));
TransferCommunityCookies(container);
- foreach (var domain in SteamDomains.AuthDomains)
+ foreach (var domain in SteamDomains.WebDomains)
{
var token = sessionData.GetToken(domain);
if (token == null) continue;
- AddTokenCookie(container, token.Value);
+ container.SetSteamAccessToken(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 && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
- {
- cookie.Expired = true;
- }
-
- AddTokenCookie(container, token);
- }
-
///
/// Clear and set new session
///
@@ -95,70 +82,22 @@ public static class AdmissionHelper
return;
}
- AddRefreshToken(container, mobileSession.RefreshToken);
+ container.SetSteamRefreshToken(mobileSession.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString()));
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
TransferCommunityCookies(container);
- foreach (var domain in SteamDomains.AuthDomains)
+ foreach (var domain in SteamDomains.WebDomains)
{
var token = mobileSession.GetToken(domain);
if (token == null) continue;
- AddTokenCookie(container, token.Value);
+ var domainUri = SteamDomains.GetDomainUri(domain);
+ container.SetSteamAccessTokenUnsafe(token.Value, domainUri);
}
}
- ///
- /// Clear and set new session. Not recommended. Uses for domain
- /// instead of its own cookie. It's okay to use it only for confirmations. But
- /// Market, Trading and other pages won't be authorized
- ///
- public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container,
- IMobileSessionData? mobileSession,
- string setLanguage = "english")
- {
- container.ClearSteamCookies(setLanguage);
- container.AddMinimalMobileCookies();
-
- if (mobileSession == null)
- {
- TransferCommunityCookies(container);
- return;
- }
-
-
- 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(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
- container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
- TransferCommunityCookies(container);
-
- var domainCookieSet = false;
- foreach (var domain in SteamDomains.AllDomains)
- {
- var token = mobileSession.GetToken(domain);
- if (token == null || token.Value.IsExpired) continue;
- if (domain == SteamDomain.Community)
- domainCookieSet = true;
- AddTokenCookie(container, token.Value);
- }
-
- var mobileToken = mobileSession.GetMobileToken();
- if (!domainCookieSet && mobileToken is {IsExpired: false})
- {
- var domain = SteamDomains.GetDomainUri(SteamDomain.Community);
- container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken)
- {
- HttpOnly = true,
- Secure = true,
- Expires = mobileToken.Value.Expires.ToLocalDateTime()
- });
- }
- }
public static void AddMinimalMobileCookies(this CookieContainer container)
{
@@ -229,27 +168,93 @@ public static class AdmissionHelper
}
}
- public static void AddRefreshToken(CookieContainer container, SteamAuthToken token)
+ ///
+ /// Sets a Steam refresh token as a cookie to the specified cookie container.
+ ///
+ ///
+ /// The added cookie will have its expiration set according to the token's expiration time. This
+ /// method is typically used to enable authenticated requests to Steam services that require a refresh
+ /// token.
+ ///
+ /// The cookie container to which the Steam refresh token cookie will be added. Cannot be null.
+ ///
+ /// The Steam authentication token to add as a refresh cookie. Must be of type Refresh or
+ /// MobileRefresh.
+ ///
+ /// Thrown if the token is not of type Refresh or MobileRefresh.
+ public static void SetSteamRefreshToken(this 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(SteamLoginUri, new Cookie(REFRESH_COOKIE_NAME, refreshToken)
+ SetSteamRefreshTokenUnsafe(container, token, SteamLoginUri);
+ }
+
+ ///
+ /// Sets a Steam refresh token as a cookie to the specified cookie container for the given domain.
+ ///
+ ///
+ /// This method does not perform validation on the input parameters. Callers must ensure that the
+ /// provided values are valid and appropriate for use.
+ ///
+ /// The cookie container to which the refresh token cookie will be added. Cannot be null.
+ ///
+ /// The Steam authentication token containing the signed token value and expiration information. Cannot
+ /// be null.
+ ///
+ /// The URI of the domain for which the refresh token cookie should be set. Cannot be null.
+ public static void SetSteamRefreshTokenUnsafe(CookieContainer container, SteamAuthToken token, Uri domainUri)
+ {
+ container.Add(domainUri, new Cookie(REFRESH_COOKIE_NAME, token.SignedToken)
{
Expires = token.Expires.ToLocalDateTime()
});
}
- public static void AddTokenCookie(CookieContainer container, SteamAuthToken token)
+ ///
+ /// Sets a Steam access token to the specified cookie container for use with Steam web requests.
+ ///
+ ///
+ /// This method is intended for standard web access tokens bound to a specific Steam web domain.
+ /// Mobile tokens are intentionally rejected, since their audiences are capability-based rather
+ /// than domain-based and may grant access to multiple web domains.
+ ///
+ /// The cookie container to which the Steam access token will be added. Cannot be null.
+ ///
+ /// The Steam access token to add. Must be of type AccessToken and associated with a valid Steam
+ /// domain.
+ ///
+ /// Thrown if the token is not of type AccessToken.
+ public static void SetSteamAccessToken(this CookieContainer container, SteamAuthToken token)
{
+ if (token.Type == SteamAccessTokenType.Mobile)
+ throw new ArgumentException(
+ $"Mobile access tokens cannot be added using this method. Use SetSteamAccessTokenUnsafe instead. Provided token has type: {token.Type}",
+ nameof(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));
- var domain = SteamDomains.GetDomainUri(token.Domain);
- container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken)
+ var domainUri = SteamDomains.GetDomainUri(token.Domain);
+ container.SetSteamAccessTokenUnsafe(token, domainUri);
+ }
+
+
+ ///
+ /// Sets a Steam access token as a secure, HTTP-only cookie to the specified cookie container for the given domain.
+ ///
+ ///
+ /// This method does not perform validation on the input parameters and should only be used when
+ /// input values are trusted. The added cookie is marked as secure and HTTP-only, and its expiration is set
+ /// according to the token's expiration time.
+ ///
+ /// The cookie container to which the Steam access token cookie will be added. Cannot be null.
+ /// The Steam access token to add as a cookie. Must contain a valid signed token and expiration.
+ /// The URI of the domain for which the cookie will be set. Cannot be null.
+ public static void SetSteamAccessTokenUnsafe(this CookieContainer container, SteamAuthToken token, Uri domainUri)
+ {
+ container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken)
{
HttpOnly = true,
Secure = true,
@@ -257,7 +262,6 @@ public static class AdmissionHelper
});
}
-
public static bool IsSteamCookie(Cookie cookie)
{
return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") ||
diff --git a/src/SteamLibForked/Core/SteamDomains.cs b/src/SteamLibForked/Core/SteamDomains.cs
index a74737c..6f9e83d 100644
--- a/src/SteamLibForked/Core/SteamDomains.cs
+++ b/src/SteamLibForked/Core/SteamDomains.cs
@@ -1,4 +1,4 @@
-using System.Collections.ObjectModel;
+using System.Collections.Immutable;
using SteamLibForked.Models.Core;
namespace SteamLib.Core;
@@ -6,44 +6,38 @@ namespace SteamLib.Core;
public static class SteamDomains
{
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},
- {SteamDomain.Login, SteamConstants.STEAM_LOGIN},
- {SteamDomain.API, SteamConstants.STEAM_API}
- });
+ 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},
+ {SteamDomain.Login, SteamConstants.STEAM_LOGIN},
+ {SteamDomain.API, SteamConstants.STEAM_API}
+ }.ToImmutableDictionary();
public static IReadOnlyDictionary DomainUris { get; }
- = new ReadOnlyDictionary(
- Domains.ToDictionary(x => x.Key, x => new Uri(x.Value))
- );
+ = Domains
+ .ToDictionary(x => x.Key, x => new Uri(x.Value))
+ .ToImmutableDictionary();
- public static IEnumerable AllDomains { get; } =
- [
- SteamDomain.Community,
- SteamDomain.Store,
- SteamDomain.Help,
- SteamDomain.TV,
- SteamDomain.Checkout,
- SteamDomain.Login,
- SteamDomain.API
- ];
-
- public static IEnumerable AuthDomains { get; } =
- [
- SteamDomain.Community,
- SteamDomain.Store,
- SteamDomain.Help,
- SteamDomain.TV,
- SteamDomain.Checkout
- ];
+ ///
+ /// All known public Steam domains.
+ ///
+ public static IEnumerable AllDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
+ SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout, SteamDomain.Login, SteamDomain.API);
+ ///
+ /// Steam web domains that participate in the standard login authorization flow.
+ ///
+ /// These domains use the web:* audience format and receive
+ /// authentication cookies/tokens during session initialization.
+ ///
+ ///
+ public static IReadOnlySet WebDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
+ SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout);
public static Uri GetDomainUri(SteamDomain domain)
{
diff --git a/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs b/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs
index 5c228fc..e6417b6 100644
--- a/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs
+++ b/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
+using SteamLib.Core;
using SteamLibForked.Abstractions;
using SteamLibForked.Models.Core;
@@ -7,7 +8,7 @@ namespace SteamLibForked.Models.Session;
//WARNING: Any changes here should be reflected in MafileSerializer.cs
-public sealed class MobileSessionData : SessionData, IMobileSessionData
+public class MobileSessionData : SessionData, IMobileSessionData
{
public SteamAuthToken? MobileToken { get; private set; }
@@ -31,9 +32,23 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
return MobileToken;
}
+ public override SteamAuthToken? GetToken(SteamDomain domain)
+ {
+ var isWeb = SteamDomains.WebDomains.Contains(domain);
+ if (isWeb)
+ {
+ // Mobile-issued tokens usually contain the "web" audience,
+ // so we assume they can also be used for all web:* domains.
+ // See: SteamAuthToken 'TODO' for more details
+ return MobileToken ?? base.GetToken(domain);
+ }
+
+ return base.GetToken(domain);
+ }
+
[MemberNotNull(nameof(MobileToken))]
- public void SetMobileToken(SteamAuthToken token)
+ public virtual void SetMobileToken(SteamAuthToken token)
{
if (token.Type != SteamAccessTokenType.Mobile)
throw new ArgumentException("Token must be of type MobileAccess", nameof(token))
@@ -44,13 +59,13 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
MobileToken = token;
}
- public override MobileSessionData Clone()
- {
- return (MobileSessionData) ((ISessionData) this).Clone();
- }
-
object ICloneable.Clone()
{
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
}
+
+ public override MobileSessionData Clone()
+ {
+ return (MobileSessionData) ((ISessionData) this).Clone();
+ }
}
\ No newline at end of file
diff --git a/src/SteamLibForked/Utility/MafileSerialization/LegacyMafile.cs b/src/SteamLibForked/Utility/MafileSerialization/LegacyMafile.cs
index 331fc4d..46e93b2 100644
--- a/src/SteamLibForked/Utility/MafileSerialization/LegacyMafile.cs
+++ b/src/SteamLibForked/Utility/MafileSerialization/LegacyMafile.cs
@@ -4,14 +4,11 @@ namespace SteamLib.Utility.MafileSerialization;
internal class LegacyMafile
{
- [JsonProperty("shared_secret")]
- public string SharedSecret { get; set; } = null!;
+ [JsonProperty("shared_secret")] public string SharedSecret { get; set; } = null!;
- [JsonProperty("identity_secret")]
- public string IdentitySecret { get; set; } = null!;
+ [JsonProperty("identity_secret")] public string IdentitySecret { get; set; } = null!;
- [JsonProperty("device_id")]
- public string DeviceId { get; set; } = null!;
+ [JsonProperty("device_id")] public string DeviceId { get; set; } = null!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!;
[JsonProperty("account_name")] public string AccountName { get; set; } = null!;
diff --git a/src/SteamLibForked/Web/ClientBuilder.cs b/src/SteamLibForked/Web/ClientBuilder.cs
index 9118d7a..28640fd 100644
--- a/src/SteamLibForked/Web/ClientBuilder.cs
+++ b/src/SteamLibForked/Web/ClientBuilder.cs
@@ -31,7 +31,7 @@ public static class ClientBuilder
}
else
{
- container.SetSteamMobileCookiesWithMobileToken(sessionData);
+ container.SetSteamMobileCookies(sessionData);
}
//Nebula tweak: