Compare commits

..

3 Commits

Author SHA1 Message Date
achiez b5d3b4a8af feat(proxy): add bulk assignment and proxy usage stats
- Implement bulk proxy assignment for accounts with flexible input (login:proxy, login:ID, or login)
- Add UI for mass assignment with counters and behavior selection for unspecified proxies
- Show assigned account count badges in proxy list for better visibility
- Enable quick assignment of a free proxy to an account
- Ensure fast and consistent proxy assignment state via in-memory cache
- Perform ReSharper cleanup
2026-05-14 16:44:14 +03:00
achiez 30cf049f23 refactor(auth): redesign Steam audience and web domain authorization model
- Rename AuthorizedDomains to WebDomains
- Introduce explicit Steam audience constants and web audience mappings
- Improve Steam token/domain resolution and cookie installation flow
- Allow mobile-issued tokens with "web" audience to be used for web domains
- Align authorization logic closer to actual Steam audience behavior
- Rename cookie APIs from Add* to Set* where appropriate
2026-05-12 16:36:37 +03:00
achiez ce55005d44 chore: remove outdated README files and update main README with new features and documentation link 2026-05-04 19:37:37 +03:00
26 changed files with 854 additions and 457 deletions
-74
View File
@@ -1,74 +0,0 @@
# NebulaAuth
## Описание
<h3 align="center" style="margin-bottom:0">
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Скачать последнюю версию</a>
</h3>
<h3 align="center">NebulaAuth — это приложение для эмуляции действий из мобильного приложения Steam. Которая заменяет ваш смартфон при работе в Steam.</h3>
<h4 align="center"><a href="https://t.me/nebulaauth">Официальная группа в Telegram</a></h4>
## Основные преимущества
- **Локализация на трёх языках**: английском, русском и украинском.
- **Полная функциональность Steam Desktop Authenticator**, переосмысление [старого приложения](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Поддержка прокси** во всех процессах работы с аккаунтом.
- **Группировка мафайлов** для продвинутого контроля.
- **Автоматическое подтверждение трейдов/действий на ТП** для экономии времени.
- **Массовый импорт мафайлов** с помощью Drag'n'Drop или CTRL+V для удобства.
- **Настройка внешнего вида** для персонализации интерфейса.
- **Возможность подтвердить вход в учетную запись без ввода кода** для облегчения доступа.
- **Автообновление** программы для использования новейших функций.
- **Автоматический повторный вход в случае проблем с сессией** для непрерывной работы.
- **Интуитивно понятный интерфейс** с подсказками и удобствами
- **Постоянная поддержка** кода приложения и других функций.
## Установка
1. Если приложение не запускается, необходимо установить [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
2. [Скачать программу из релизов этого репозитория на Github](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest)
* *Для сохранности ваших данных скачивайте приложение только отсюда*
4. Распакуйте ZIP-файл в любую папку.
5. Запустите файл **NebulaAuth.exe**.
## Использование
![gh-main-window-rus](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/6a84f414-0e24-40a4-8adb-f1923fbe8719)
1. Панель управления.
- управление файлами и настройки
- управление аккаунтом (вход, привязка, отвязка)
- группировка
- выбор прокси
- индикатор с подсказкой об используемом прокси (горит либо желтым, либо красным, при наведении отобразит дополнительную информацию)
- таймеры для автоматического подтверждения трейдов/продаж на торговой площадке
- интервал таймера подтверждений (в секундах)
2. Список ваших аккаунтов
3. Код подтверждения входа (нажмите, чтобы скопировать)
4. Главное окно подтверждений
5. Поиск по логину или SteamID (7xxxxxxxxxxxxx)
6. Подтвердить вход на другогом устройстве.
7. Гиперссылка на официальную страницу приложения с указанием авторства.
## Настройки
![gh-settings-rus](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/33246ed1-1e3d-4310-88c5-085e5c50bc6b)
1. Режим фона. Используйте, если вы хотите отключить фон или установить собственный (поместите файл «Background.png» в папку приложения)
2. Язык приложения
3. Отключить таймеры при переключении между аккаунтами
4. Скрывать в трей при сворачивании
5. Индикатор с цветом. Маленький кружочек на значке панели задач с произвольным цветом. Полезно при использовании нескольких окон.
6. Пользовательский цвет приложения.
7. Текущий пароль шифрования. Если установлено, вы можете сохранять зашифрованные пароли в mafile, чтобы облегчить повторный вход в систему при проблемах с сессией. (Не рекомендуется)
8. Режим устаревших файлов. Режим совместимости Mafile с другими клиентами (SDA и т.д.). Если установлено, приложение будет сохранять файлы в старом стандартном формате (по умолчанию: включено).
9. Разрешить автообновление без подтверждения
## [Лицензия](/LICENSE.md)
Коммерческое использование запрещено. При распространении измененного кода необходимо указывать оригинальное авторство.
-71
View File
@@ -1,71 +0,0 @@
# NebulaAuth
## Опис
<h3 align="center" style="margin-bottom:0">
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Завантажити останню версію</a>
</h3>
<h3 align="center">NebulaAuth - це програма для емуляції дій з мобільного додатку Steam. Яка замінює ваш смартфон під час роботи в Steam.</h3>
<h4 align="center"><a href="https://t.me/nebulaauth">Офіційна група в Telegram</a></h4>
## Основні переваги
- **Локалізація трьома мовами**: англійською, російською та українською.
- **Повна функціональність Steam Desktop Authenticator** переосмислення [старої програми](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Підтримка проксі** у всіх процесах роботи з обліковим записом.
- **Угруповання мафайлів** для просунутого контролю.
- **Автоматичне підтвердження трейдів/дій на маркеті** для економії часу.
- **Масовий імпорт мафайлів** за допомогою Drag'n'Drop або CTRL+V для зручності.
- **Налаштування зовнішнього вигляду** для персоналізації інтерфейсу.
- **Можливість підтвердити вхід до облікового запису без введення коду** для полегшення доступу.
- **Автооновлення** програми для використання новітніх функцій.
- **Автоматичний повторний вхід у разі проблем із сесією** для безперервної роботи.
- **Інтуїтивно зрозумілий інтерфейс** з підказками та зручностями
- **Постійна підтримка** коду програми та інших функцій.
## Монтаж
1. Якщо програма не запускається, необхідно встановити [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
2. [Завантажити програму з релізів цього репозиторію на Github](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest)
* *Для збереження ваших даних завантажуйте програму тільки звідси*
4. Розпакуйте ZIP-файл у будь-яку папку.
5. Запустіть файл **NebulaAuth.exe**.
## Використання
![gh-main-window-ua](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/bf61ac61-b21c-4589-9b5a-751a6d983120)
1. Панель керування.
- керування файлами та налаштування
- керування акаунтом (вхід, прив'язка, відв'язування)
- угруповання
- вибір проксі
- індикатор з підказкою про проксі (світиться або жовтим, або червоним, при наведенні відобразить додаткову інформацію)
- таймери для автоматичного підтвердження трейдів/продажів на маркеті
- інтервал таймера підтверджень (у секундах)
2. Список ваших облікових записів
3. Код підтвердження входу (натисніть, щоб скопіювати)
4. Головне вікно підтвердження
5. Пошук за логіном або SteamID (7xxxxxxxxxxxxx)
6. Підтвердити вхід на іншому пристрою.
7. Гіперпосилання на офіційну сторінку додатку із зазначенням авторства.
## Налаштування
![gh-settings-ua](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/3f57e58d-d647-42a1-a4c9-ca53ab7b38a3)
1. Режим фону. Використовуйте, якщо ви хочете вимкнути фон або встановити власний (помістіть файл "Background.png" у папку програми)
2. Мова локалізації
3. Вимкнути таймери під час перемикання між обліковими записами
4. Приховувати у трей при згортанні
5. Індикатор із кольором. Маленький кружечок на піктограмі панелі завдань з довільним кольором. Корисно при використанні кількох вікон.
6. Власний колір програми.
7. Поточний пароль шифрування. Якщо встановлено, ви можете зберігати зашифровані паролі в mafile, щоб полегшити повторний вхід до системи у разі проблеми з сесією. (Не рекомендується)
8. Режим застарілих файлів. Режим сумісності Mafile з іншими клієнтами (SDA тощо). Якщо встановлено, програма зберігатиме файли у старому стандартному форматі (за замовчуванням: увімкнено).
9. Дозволити автооновлення без підтвердження
## [Ліцензія](/LICENSE.md)
Комерційне використання заборонено. У разі поширення зміненого коду необхідно вказувати оригінальне авторство.
+18 -45
View File
@@ -1,12 +1,5 @@
# NebulaAuth
* [Русский](README-RU.md)
* [Українська](README-UA.md)
## Description
<h3 align="center" style="margin-bottom:0">
<a href="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/latest">Download latest version</a>
</h3>
@@ -19,24 +12,33 @@ NebulaAuth is an application for emulating actions from the Steam Mobile App, re
<sub>NebulaAuth is an independent project and is not affiliated with Valve or Steam.</sub>
</p>
<h4 align="center"><a href="https://t.me/nebulaauth">Official Telegram Group</a></h4>
<h4 align="center"><a href="https://achiefy-project.gitbook.io/nebulaauth">Documentation</a></h4>
## Main advantages
- **Localization in three languages**: English, Russian and Ukrainian.
<p align="center">
<img src="misc/main_window.png" width="600"/>
</p>
- **Localization in six languages**: English, Russian, Ukrainian, Spanish, Turkish and Kazakh.
- **Full functionality of Steam Desktop Authenticator** reimagining [old app](https://github.com/Jessecar96/SteamDesktopAuthenticator)
- **Proxy support** in all account work processes.
- **Mafile grouping** for improved management.
- **Automatic confirmations of trades/market actions** to save time.
- **Bulk import of .mafiles** via Drag'n'Drop or CTRL+V for convenience.
- **Bulk import of .mafiles** via Drag'n'Drop or Ctrl+V, including SDA-encrypted mafiles with automatic manifest detection.
- **Design customization** to personalize the interface.
- **Ability to confirm account login without entering a code** for easier access.
- **Auto-update** program to use the latest features.
- **Auto-update** with SHA256 checksum verification, changelog viewer and flexible update options.
- **Automatic relogin in case of problems with the session** for continuous operation.
- **Intuitive interface** with tips and conveniences
- **Continious support** of application code and other features.
- **Intuitive interface** with tips and conveniences.
- **Continuous support** of application code and other features.
## Documentation
You can find the documentation for the application [here](https://achiefy-project.gitbook.io/nebulaauth). Currently, only the Russian version is available, but the English version will be released soon.
## Installation
1. If the application does not start, you need to install [.NET Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
@@ -45,40 +47,11 @@ NebulaAuth is an application for emulating actions from the Steam Mobile App, re
4. Unpack the .zip file to any folder
5. Run the file **NebulaAuth.exe**
## Usage
![gh-main-window-eng](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/15c0e870-1766-43a5-9e8c-2f34d5436beb)
1. Control panel.
- file management and settings
- account management (login, linking, unlinking)
- grouping
- proxy selection
- an indicator with a hint about the proxy used (lit either yellow or red, when hovered it will display additional information)
- timers for automatic confirmation of trade offers/sale offers on the marketplace
- confirmation timer interval (in seconds)
2. List of your accounts
3. Login confirmation code (click to copy)
4. Main confirmation window
5. Search by login or SteamID (7xxxxxxxxxxxxx)
6. Confirm login on another device
7. Hyperlink to the official application page with attribution
## Project Ownership
## Settings
![gh-settings-eng](https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/assets/106531132/cd704495-d2df-45a7-a73e-40c19410eb72)
NebulaAuth is the original project by Achiefy™.
1. Background mode. Use it if you want to disable default or set custom background of application (put file 'Background.png' to your application folder)
2. Localization language
3. Disable timers when switching between accounts
4. Hide to tray on minimize
5. Indicator with color. Small ellipse on your task-bar icon with custom color. Useful when using multiple windows
6. Custom background color of application
7. Current encryption password. If set you can save encrypted passwords to mafile to help re-login on session troubles. (Not recommended)
8. Legacy mafile mode. Mafile compability mode for another applications (SDA and etc). If checked application will save mafiles with old standart format (Default: checked)
9. Allow auto-update without confirmation
Please keep attribution and do not present modified versions as official.
## [License](/LICENSE.md)
Commercial use prohibited. When redistributing modified code, you must indicate the original authorship.
AGPL-3.0 — see [LICENSE](/LICENSE).
Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.5.6.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.5.6/NebulaAuth.1.5.6.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.5.6.html</changelog>
<mandatory>false</mandatory>
</item>
@@ -15,6 +15,7 @@
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<converters:ProxyAccountCountConverter x:Key="ProxyAccountCountConverter" />
<!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter" />
@@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
using NebulaAuth.Model;
namespace NebulaAuth.Converters;
/// <summary>
/// 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.
/// </summary>
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();
}
}
@@ -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": "Қолдану"
}
}
}
@@ -904,6 +904,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": {
+3
View File
@@ -420,6 +420,9 @@
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
Command="{Binding RemoveProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.AssignFreeProxy}"
Command="{Binding AssignFreeProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
@@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies();
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData);
}
+1 -1
View File
@@ -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);
}
@@ -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;
/// <summary>
/// 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.
/// </summary>
public static class ProxyAssignmentCache
{
private static readonly Dictionary<int, HashSet<string>> _proxyToAccounts = new();
public static void Initialize(IEnumerable<Mafile> mafiles)
{
_proxyToAccounts.Clear();
foreach (var maf in mafiles)
{
if (maf.Proxy != null)
AddInternal(maf.Proxy.Id, maf.AccountName);
}
}
/// <summary>
/// Call whenever a proxy assignment on a mafile changes and is persisted to disk.
/// </summary>
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;
}
/// <summary>
/// Returns a random free proxy (not the default proxy), or null if none available.
/// </summary>
public static KeyValuePair<int, ProxyData>? GetRandomFreeProxy(
ObservableDictionary<int, ProxyData> 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<string>(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);
}
}
@@ -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)
+1
View File
@@ -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();
+3
View File
@@ -89,4 +89,7 @@
Color="{StaticResource SecondaryColor}" />
<SolidColorBrush x:Key="MaterialDesign.Brush.Secondary.Dark.Foreground"
Color="{StaticResource BaseContentColor}" />
<SolidColorBrush x:Key="MaterialDesign.Brush.RadioButton.Outline"
Color="{StaticResource BaseContentColor}" />
</ResourceDictionary>
+238 -126
View File
@@ -20,12 +20,12 @@
FontSize="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Title -->
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -45,90 +45,41 @@
</Button>
</Grid>
<Separator Grid.Row="1" />
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Stretch">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="DefaultProxy.Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}">
<md:PackIcon Kind="ClearBox" Width="20" Height="20" />
</Button>
<!-- Tabs -->
<TabControl Grid.Row="2"
md:ColorZoneAssist.Background="{DynamicResource Base100Brush}"
Style="{StaticResource MaterialDesignUniformTabControl}">
</Grid>
<StackPanel Grid.Row="1" Margin="15,0,15,15">
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyProtocol}" IsChecked="{Binding DisplayProtocol}" />
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyCredentials}"
IsChecked="{Binding DisplayCredentials}" />
</StackPanel>
</Grid>
<md:Card Grid.Row="3" Margin="10">
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14"
SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
<ListBox.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DataContext.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</ListBox.InputBindings>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="Tag"
Value="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=DataContext}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.Copy}"
Command="{Binding PlacementTarget.Tag.CopyProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.CopyAddress}"
Command="{Binding PlacementTarget.Tag.CopyProxyAddressCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr Common.Delete}"
Command="{Binding PlacementTarget.Tag.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!-- Tab 1: Proxy Manager -->
<TabItem Header="{Tr ProxyManagerDialog.TabManager}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<!-- Default proxy + display options -->
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">
<Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " />
<TextBlock HorizontalAlignment="Stretch">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run
Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="Value" />
<Binding Path="DefaultProxy.Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
@@ -136,66 +87,227 @@
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Button Style="{StaticResource MaterialDesignIconButton}" Padding="0" Width="24"
Height="24" md:RippleAssist.IsDisabled="True" Grid.Column="1"
HorizontalAlignment="Right"
Command="{Binding DataContext.SetDefaultCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<md:PackIcon Height="16" Width="16" Kind="Heart" />
<Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}">
<md:PackIcon Kind="ClearBox" Width="20" Height="20" />
</Button>
</Grid>
<StackPanel Grid.Row="1" Margin="15,0,15,15">
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyProtocol}"
IsChecked="{Binding DisplayProtocol}" />
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyCredentials}"
IsChecked="{Binding DisplayCredentials}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</md:Card>
<Grid Grid.Row="4" Margin="5,0,15,0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<nebulaAuth:HintBox
Visibility="{Binding ErrorText, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="15,0,15,0"
Grid.Row="0"
Severity="Error"
FontSize="14"
Text="{Binding ErrorText}"
CloseCommand="{Binding ClearErrorCommand}"
ShowCloseButton="True" />
<!-- Proxy list -->
<md:Card Grid.Row="1" Margin="10">
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14"
SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
<ListBox.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DataContext.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</ListBox.InputBindings>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="Tag"
Value="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=DataContext}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.Copy}"
Command="{Binding PlacementTarget.Tag.CopyProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Proxy.CopyAddress}"
Command="{Binding PlacementTarget.Tag.CopyProxyAddressCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
<MenuItem Header="{Tr Common.Delete}"
Command="{Binding PlacementTarget.Tag.RemoveProxyCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding Value}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">
<Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " />
<Run>
<Run.Text>
<MultiBinding Mode="OneWay"
Converter="{StaticResource ProxyDataTextMultiConverter}">
<Binding Path="Value" />
<Binding Path="DataContext.DisplayProtocol"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext.DisplayCredentials"
RelativeSource="{RelativeSource AncestorType=UserControl}" />
</MultiBinding>
</Run.Text>
</Run>
</TextBlock>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" VerticalAlignment="Center" FontSize="10" Margin="0,0,2,0"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}"
Text="{Binding Key, Converter={StaticResource ProxyAccountCountConverter}}" />
<Button Style="{StaticResource MaterialDesignIconButton}" Padding="0"
Width="24"
Height="24" md:RippleAssist.IsDisabled="True" Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DataContext.SetDefaultCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<md:PackIcon Height="16" Width="16" Kind="Heart" />
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</md:Card>
<TextBox Text="{Binding AddProxyField}"
KeyDown="ProxyInput_KeyDown"
FontSize="12"
md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True"
MaxHeight="100" Style="{StaticResource MaterialDesignOutlinedTextBox}"
Margin="15"
Height="90"
md:HintAssist.IsFloating="False"
md:HintAssist.Hint="IP:PORT&#x0a;IP:PORT:USER:PASS&#x0a;PROTOCOL://IP:PORT:USER:PASS&#x0a;IP:PORT:USER:PASS{ID}
<!-- Add / Remove area -->
<Grid Grid.Row="2" Margin="5,0,15,0">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<nebulaAuth:HintBox
Visibility="{Binding ErrorText, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="15,0,15,0"
Grid.Row="0"
Severity="Error"
FontSize="14"
Text="{Binding ErrorText}"
CloseCommand="{Binding ClearErrorCommand}"
ShowCloseButton="True" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding AddProxyField}"
KeyDown="ProxyInput_KeyDown"
FontSize="12"
md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True"
MaxHeight="100" Style="{StaticResource MaterialDesignOutlinedTextBox}"
Margin="15"
Height="90"
md:HintAssist.IsFloating="False"
md:HintAssist.Hint="IP:PORT&#x0a;IP:PORT:USER:PASS&#x0a;PROTOCOL://IP:PORT:USER:PASS&#x0a;IP:PORT:USER:PASS{ID}
" />
<Button x:Name="AddProxyBtn" ToolTip="CTRL+ENTER" IsDefault="True" Grid.Column="1" Height="90"
Command="{Binding AddProxyCommand}">
<md:PackIcon Kind="Add" />
</Button>
<Button Grid.Column="2" Height="90" Command="{Binding RemoveProxyCommand}" CommandParameter="{x:Null}">
<md:PackIcon Kind="Trash" />
</Button>
</Grid>
<Button x:Name="AddProxyBtn" ToolTip="CTRL+ENTER" IsDefault="True" Grid.Column="1"
Height="90" Command="{Binding AddProxyCommand}">
<md:PackIcon Kind="Add" />
</Button>
<Button Grid.Column="2" Height="90" Command="{Binding RemoveProxyCommand}"
CommandParameter="{x:Null}">
<md:PackIcon Kind="Trash" />
</Button>
</Grid>
</Grid>
</Grid>
</TabItem>
</Grid>
<!-- Tab 2: Bulk Assignment -->
<TabItem Header="{Tr ProxyManagerDialog.TabAssignment}">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Stats counters -->
<UniformGrid Grid.Row="0" Columns="3" Margin="0,8,0,12">
<Border Margin="0,0,4,0" Padding="10,8" CornerRadius="8"
Background="{DynamicResource MaterialDesign.Brush.Card.Background}">
<StackPanel HorizontalAlignment="Center">
<TextBlock FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding TotalAccounts, Mode=OneWay}" />
<TextBlock FontSize="11" HorizontalAlignment="Center"
Text="{Tr ProxyManagerDialog.TotalAccounts}"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
</StackPanel>
</Border>
<Border Margin="4,0,4,0" Padding="10,8" CornerRadius="8"
Background="{DynamicResource MaterialDesign.Brush.Card.Background}">
<StackPanel HorizontalAlignment="Center">
<TextBlock FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding TotalProxies, Mode=OneWay}" />
<TextBlock FontSize="11" HorizontalAlignment="Center"
Text="{Tr ProxyManagerDialog.TotalProxies}"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
</StackPanel>
</Border>
<Border Margin="4,0,0,0" Padding="10,8" CornerRadius="8"
Background="{DynamicResource MaterialDesign.Brush.Card.Background}">
<StackPanel HorizontalAlignment="Center">
<TextBlock FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding AssignedProxies, Mode=OneWay}" />
<TextBlock FontSize="11" HorizontalAlignment="Center"
Text="{Tr ProxyManagerDialog.AssignedProxies}"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
</StackPanel>
</Border>
</UniformGrid>
<!-- Toggle: behaviour for unspecified proxy -->
<StackPanel Grid.Row="1" Margin="0,0,0,10">
<TextBlock Text="{Tr ProxyManagerDialog.UnspecifiedBehavior}" FontSize="13"
Margin="0,0,0,4"
Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
<StackPanel Orientation="Horizontal">
<RadioButton Content="{Tr ProxyManagerDialog.UnspecifiedRandom}"
IsChecked="{Binding UnspecifiedAssignRandom}"
Margin="0,0,24,0" />
<RadioButton Content="{Tr ProxyManagerDialog.UnspecifiedRemove}"
IsChecked="{Binding UnspecifiedAssignRandom, Converter={StaticResource ReverseBooleanConverter}}" />
</StackPanel>
</StackPanel>
<!-- Input -->
<TextBox Grid.Row="2"
Text="{Binding AssignmentInput, UpdateSourceTrigger=PropertyChanged}"
FontSize="12"
md:TextFieldAssist.HasClearButton="True"
AcceptsReturn="True"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
VerticalScrollBarVisibility="Auto"
md:HintAssist.IsFloating="False"
VerticalContentAlignment="Top"
Cursor="IBeam"
md:HintAssist.Hint="login:IP:PORT&#x0a;login:IP:PORT:USER:PASS&#x0a;login:socks5://USER:PASS@IP:PORT&#x0a;login:ID&#x0a;login" />
<!-- Result HintBox -->
<nebulaAuth:HintBox Grid.Row="3"
Visibility="{Binding AssignmentTip, Converter={StaticResource NullableToVisibilityConverter}}"
Margin="0,8,0,0"
Severity="Info"
FontSize="14"
Text="{Binding AssignmentTip}"
CloseCommand="{Binding ClearAssignmentTipCommand}"
ShowCloseButton="True" />
<!-- Apply button -->
<Button Grid.Row="4" Margin="0,10,0,0" HorizontalAlignment="Right"
Command="{Binding ApplyAssignmentCommand}"
Content="{Tr ProxyManagerDialog.ApplyAssignment}" />
</Grid>
</TabItem>
</TabControl>
</Grid>
</UserControl>
+1 -1
View File
@@ -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]
+5 -5
View File
@@ -25,6 +25,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;
@@ -347,9 +352,4 @@ public partial class MainVM //File //TODO: Refactor
if (mafile is not Mafile maf) return false;
return maf.Password != null && PHandler.IsPasswordSet;
}
private record MafileReadResult(
Mafile? Mafile,
bool SdaPasswordPrompted,
SDAEncryptionHelper.Context? SdaContext);
}
+37 -1
View File
@@ -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)
@@ -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;
/// <summary>
/// true = assign a random free proxy to unspecified accounts; false = remove proxy from unspecified accounts.
/// </summary>
[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;
}
/// <summary>
/// Finds a proxy equal to <paramref name="proxyData" /> in storage.
/// If not found, adds it and returns the new entry.
/// </summary>
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<Mafile> 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);
}
}
@@ -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);
}
/// <summary>
/// Clear and set new session
/// </summary>
@@ -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);
}
}
/// <summary>
/// Clear and set new session. Not recommended. Uses <see cref="IMobileSessionData.GetMobileToken()" /> for domain
/// <see cref="SteamDomain.Community" /> instead of its own cookie. It's okay to use it only for confirmations. But
/// Market, Trading and other pages won't be authorized
/// </summary>
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)
/// <summary>
/// Sets a Steam refresh token as a cookie to the specified cookie container.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="container">The cookie container to which the Steam refresh token cookie will be added. Cannot be null.</param>
/// <param name="token">
/// The Steam authentication token to add as a refresh cookie. Must be of type Refresh or
/// MobileRefresh.
/// </param>
/// <exception cref="ArgumentException">Thrown if the token is not of type Refresh or MobileRefresh.</exception>
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);
}
/// <summary>
/// Sets a Steam refresh token as a cookie to the specified cookie container for the given domain.
/// </summary>
/// <remarks>
/// This method does not perform validation on the input parameters. Callers must ensure that the
/// provided values are valid and appropriate for use.
/// </remarks>
/// <param name="container">The cookie container to which the refresh token cookie will be added. Cannot be null.</param>
/// <param name="token">
/// The Steam authentication token containing the signed token value and expiration information. Cannot
/// be null.
/// </param>
/// <param name="domainUri">The URI of the domain for which the refresh token cookie should be set. Cannot be null.</param>
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)
/// <summary>
/// Sets a Steam access token to the specified cookie container for use with Steam web requests.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="container">The cookie container to which the Steam access token will be added. Cannot be null.</param>
/// <param name="token">
/// The Steam access token to add. Must be of type AccessToken and associated with a valid Steam
/// domain.
/// </param>
/// <exception cref="ArgumentException">Thrown if the token is not of type AccessToken.</exception>
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);
}
/// <summary>
/// Sets a Steam access token as a secure, HTTP-only cookie to the specified cookie container for the given domain.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="container">The cookie container to which the Steam access token cookie will be added. Cannot be null.</param>
/// <param name="token">The Steam access token to add as a cookie. Must contain a valid signed token and expiration.</param>
/// <param name="domainUri">The URI of the domain for which the cookie will be set. Cannot be null.</param>
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") ||
+28 -34
View File
@@ -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<SteamDomain, string> Domains { get; } =
new ReadOnlyDictionary<SteamDomain, string>(
new Dictionary<SteamDomain, string>
{
{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, string>
{
{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<SteamDomain, Uri> DomainUris { get; }
= new ReadOnlyDictionary<SteamDomain, Uri>(
Domains.ToDictionary(x => x.Key, x => new Uri(x.Value))
);
= Domains
.ToDictionary(x => x.Key, x => new Uri(x.Value))
.ToImmutableDictionary();
public static IEnumerable<SteamDomain> AllDomains { get; } =
[
SteamDomain.Community,
SteamDomain.Store,
SteamDomain.Help,
SteamDomain.TV,
SteamDomain.Checkout,
SteamDomain.Login,
SteamDomain.API
];
public static IEnumerable<SteamDomain> AuthDomains { get; } =
[
SteamDomain.Community,
SteamDomain.Store,
SteamDomain.Help,
SteamDomain.TV,
SteamDomain.Checkout
];
/// <summary>
/// All known public Steam domains.
/// </summary>
public static IEnumerable<SteamDomain> AllDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout, SteamDomain.Login, SteamDomain.API);
/// <summary>
/// Steam web domains that participate in the standard login authorization flow.
/// <para>
/// These domains use the <c>web:*</c> audience format and receive
/// authentication cookies/tokens during session initialization.
/// </para>
/// </summary>
public static IReadOnlySet<SteamDomain> WebDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout);
public static Uri GetDomainUri(SteamDomain domain)
{
@@ -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();
}
}
@@ -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!;
+1 -1
View File
@@ -31,7 +31,7 @@ public static class ClientBuilder
}
else
{
container.SetSteamMobileCookiesWithMobileToken(sessionData);
container.SetSteamMobileCookies(sessionData);
}
//Nebula tweak: