Merge pull request #22 from achiez/feat/proxy-assignment

feat(proxy): add bulk assignment and proxy usage stats
This commit is contained in:
Achies
2026-06-19 19:00:47 +03:00
committed by GitHub
21 changed files with 836 additions and 259 deletions
@@ -15,6 +15,7 @@
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" /> <converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
<converters:BoolToValueConverter x:Key="BoolToValueConverter" /> <converters:BoolToValueConverter x:Key="BoolToValueConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" /> <converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
<converters:ProxyAccountCountConverter x:Key="ProxyAccountCountConverter" />
<!-- Background converters--> <!-- Background converters-->
<background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" /> <background:BackgroundImageVisibleConverter x:Key="BackgroundImageVisibleConverter" />
<background:BackgroundSourceConverter x:Key="BackgroundSourceConverter" /> <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", "es": "El proxy tiene un formato incorrecto",
"tr": "Proxy yanlış formatta", "tr": "Proxy yanlış formatta",
"kk": "Прокси қате форматта" "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", "es": "Mostrar credenciales",
"tr": "Kimlik bilgilerini göster", "tr": "Kimlik bilgilerini göster",
"kk": "Тіркелгі деректерін көрсету" "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", "es": "Desvincular proxy",
"tr": "Proxy bağlantısını kaldır", "tr": "Proxy bağlantısını kaldır",
"kk": "Проксиді ажырату" "kk": "Проксиді ажырату"
},
"AssignFreeProxy": {
"en": "Assign free proxy",
"ru": "Назначить свободный прокси",
"zh": "分配空闲代理",
"uk": "Призначити вільний проксі",
"fr": "Attribuer un proxy libre",
"es": "Asignar proxy libre",
"tr": "Serbest proxy ata",
"kk": "Бос проксиді тағайындау"
} }
}, },
"Proxy": { "Proxy": {
+3
View File
@@ -420,6 +420,9 @@
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}" <MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
Command="{Binding RemoveProxyCommand}" Command="{Binding RemoveProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" /> 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> </ContextMenu>
</FrameworkElement.ContextMenu> </FrameworkElement.ContextMenu>
@@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
SetStatus(newStatus); SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies(); 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(); ClientHandler.CookieContainer.ClearAllCookies();
if (account == null) return; if (account == null) return;
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData); ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData);
Proxy.SetData(account.Proxy?.Data); 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 //Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
mafile.SetSessionData(mafile.SessionData); mafile.SetSessionData(mafile.SessionData);
await Storage.UpdateMafileAsync(mafile); 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) 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; var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads); await Storage.Initialize(threads);
ProxyAssignmentCache.Initialize(Storage.MaFiles);
MAACStorage.Initialize(); MAACStorage.Initialize();
MafileExporterStorage.Initialize(); MafileExporterStorage.Initialize();
+3
View File
@@ -89,4 +89,7 @@
Color="{StaticResource SecondaryColor}" /> Color="{StaticResource SecondaryColor}" />
<SolidColorBrush x:Key="MaterialDesign.Brush.Secondary.Dark.Foreground" <SolidColorBrush x:Key="MaterialDesign.Brush.Secondary.Dark.Foreground"
Color="{StaticResource BaseContentColor}" /> Color="{StaticResource BaseContentColor}" />
<SolidColorBrush x:Key="MaterialDesign.Brush.RadioButton.Outline"
Color="{StaticResource BaseContentColor}" />
</ResourceDictionary> </ResourceDictionary>
+136 -24
View File
@@ -20,12 +20,12 @@
FontSize="16"> FontSize="16">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Title -->
<Grid Margin="10,10,10,5"> <Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@@ -45,7 +45,23 @@
</Button> </Button>
</Grid> </Grid>
<Separator Grid.Row="1" /> <Separator Grid.Row="1" />
<Grid Grid.Row="2">
<!-- Tabs -->
<TabControl Grid.Row="2"
md:ColorZoneAssist.Background="{DynamicResource Base100Brush}"
Style="{StaticResource MaterialDesignUniformTabControl}">
<!-- Tab 1: Proxy Manager -->
<TabItem Header="{Tr ProxyManagerDialog.TabManager}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Default proxy + display options -->
<Grid Grid.Row="0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
<RowDefinition /> <RowDefinition />
@@ -57,7 +73,8 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Stretch"> <TextBlock HorizontalAlignment="Stretch">
<Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" /> <Run Text="{Tr ProxyManagerDialog.DefaultProxy, IsDynamic=False}" />
<Run Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" /> <Run
Text="{Binding DefaultProxy.Key, StringFormat='&#x0a;0:', FallbackValue='-', Mode=OneWay}" />
<Run> <Run>
<Run.Text> <Run.Text>
<MultiBinding Mode="OneWay" <MultiBinding Mode="OneWay"
@@ -71,19 +88,20 @@
</Run.Text> </Run.Text>
</Run> </Run>
</TextBlock> </TextBlock>
<Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}"> <Button Grid.Column="1" Command="{Binding RemoveDefaultCommand}">
<md:PackIcon Kind="ClearBox" Width="20" Height="20" /> <md:PackIcon Kind="ClearBox" Width="20" Height="20" />
</Button> </Button>
</Grid> </Grid>
<StackPanel Grid.Row="1" Margin="15,0,15,15"> <StackPanel Grid.Row="1" Margin="15,0,15,15">
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyProtocol}" IsChecked="{Binding DisplayProtocol}" /> <CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyProtocol}"
IsChecked="{Binding DisplayProtocol}" />
<CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyCredentials}" <CheckBox Content="{Tr ProxyManagerDialog.DisplayProxyCredentials}"
IsChecked="{Binding DisplayCredentials}" /> IsChecked="{Binding DisplayCredentials}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<md:Card Grid.Row="3" Margin="10">
<!-- Proxy list -->
<md:Card Grid.Row="1" Margin="10">
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14" <ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" FontSize="14"
SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}"> SelectedValue="{Binding SelectedProxy}" ItemsSource="{Binding Proxies}">
<ListBox.InputBindings> <ListBox.InputBindings>
@@ -110,17 +128,15 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style> </Style>
</ListBox.ItemContainerStyle> </ListBox.ItemContainerStyle>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center"> <TextBlock VerticalAlignment="Center">
<Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " /> <Run Text="{Binding Key, Mode=OneWay}" /><Run Text=": " />
@@ -136,23 +152,28 @@
</MultiBinding> </MultiBinding>
</Run.Text> </Run.Text>
</Run> </Run>
</TextBlock> </TextBlock>
<Button Style="{StaticResource MaterialDesignIconButton}" Padding="0" Width="24"
Height="24" md:RippleAssist.IsDisabled="True" Grid.Column="1" <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" HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DataContext.SetDefaultCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" Command="{Binding DataContext.SetDefaultCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"> CommandParameter="{Binding}">
<md:PackIcon Height="16" Width="16" Kind="Heart" /> <md:PackIcon Height="16" Width="16" Kind="Heart" />
</Button> </Button>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</md:Card> </md:Card>
<Grid Grid.Row="4" Margin="5,0,15,0">
<!-- Add / Remove area -->
<Grid Grid.Row="2" Margin="5,0,15,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
<RowDefinition /> <RowDefinition />
@@ -166,14 +187,12 @@
Text="{Binding ErrorText}" Text="{Binding ErrorText}"
CloseCommand="{Binding ClearErrorCommand}" CloseCommand="{Binding ClearErrorCommand}"
ShowCloseButton="True" /> ShowCloseButton="True" />
<Grid Grid.Row="1"> <Grid Grid.Row="1">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox Text="{Binding AddProxyField}" <TextBox Text="{Binding AddProxyField}"
KeyDown="ProxyInput_KeyDown" KeyDown="ProxyInput_KeyDown"
FontSize="12" FontSize="12"
@@ -185,17 +204,110 @@
md:HintAssist.IsFloating="False" 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} 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" <Button x:Name="AddProxyBtn" ToolTip="CTRL+ENTER" IsDefault="True" Grid.Column="1"
Command="{Binding AddProxyCommand}"> Height="90" Command="{Binding AddProxyCommand}">
<md:PackIcon Kind="Add" /> <md:PackIcon Kind="Add" />
</Button> </Button>
<Button Grid.Column="2" Height="90" Command="{Binding RemoveProxyCommand}" CommandParameter="{x:Null}"> <Button Grid.Column="2" Height="90" Command="{Binding RemoveProxyCommand}"
CommandParameter="{x:Null}">
<md:PackIcon Kind="Trash" /> <md:PackIcon Kind="Trash" />
</Button> </Button>
</Grid> </Grid>
</Grid>
</Grid>
</TabItem>
<!-- 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> </Grid>
</TabItem>
</TabControl>
</Grid> </Grid>
</UserControl> </UserControl>
+1 -1
View File
@@ -42,7 +42,7 @@ public partial class MainVM : ObservableObject
new MaProxy(kvp.Key, kvp.Value))); new MaProxy(kvp.Key, kvp.Value)));
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged; Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups(); QueryGroups();
UpdateManager.CheckForUpdates(false); UpdateManager.CheckForUpdates();
} }
[RelayCommand] [RelayCommand]
+5 -5
View File
@@ -25,6 +25,11 @@ namespace NebulaAuth.ViewModel;
public partial class MainVM //File //TODO: Refactor public partial class MainVM //File //TODO: Refactor
{ {
private record MafileReadResult(
Mafile? Mafile,
bool SdaPasswordPrompted,
SDAEncryptionHelper.Context? SdaContext);
public Settings Settings => Settings.Instance; public Settings Settings => Settings.Instance;
@@ -347,9 +352,4 @@ public partial class MainVM //File //TODO: Refactor
if (mafile is not Mafile maf) return false; if (mafile is not Mafile maf) return false;
return maf.Password != null && PHandler.IsPasswordSet; 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.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -38,9 +39,10 @@ public partial class MainVM
} }
if (!system && SelectedMafile != null) if (!system && SelectedMafile != null)
{ {
var oldProxyId = SelectedMafile.Proxy?.Id;
SelectedMafile.Proxy = SelectedProxy; SelectedMafile.Proxy = SelectedProxy;
ProxyAssignmentCache.UpdateAssignment(SelectedMafile.AccountName, oldProxyId, SelectedProxy?.Id);
Storage.UpdateMafile(SelectedMafile); Storage.UpdateMafile(SelectedMafile);
} }
@@ -74,7 +76,9 @@ public partial class MainVM
{ {
mafile ??= SelectedMafile; mafile ??= SelectedMafile;
if (mafile?.Proxy == null) return; if (mafile?.Proxy == null) return;
var oldProxyId = mafile.Proxy.Id;
mafile.Proxy = null; mafile.Proxy = null;
ProxyAssignmentCache.UpdateAssignment(mafile.AccountName, oldProxyId, null);
if (mafile == SelectedMafile) if (mafile == SelectedMafile)
SetProxy(null, false); //Not system, triggered by user SetProxy(null, false); //Not system, triggered by user
} }
@@ -85,6 +89,38 @@ public partial class MainVM
return mafile is {Proxy: not null}; 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() private void CheckProxyExist()
{ {
if (SelectedProxy == null) 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; return;
} }
container.SetSteamRefreshToken(sessionData.RefreshToken);
AddRefreshToken(container, sessionData.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community); var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/")); container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/"));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/")); container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/"));
TransferCommunityCookies(container); TransferCommunityCookies(container);
foreach (var domain in SteamDomains.AuthDomains) foreach (var domain in SteamDomains.WebDomains)
{ {
var token = sessionData.GetToken(domain); var token = sessionData.GetToken(domain);
if (token == null) continue; 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> /// <summary>
/// Clear and set new session /// Clear and set new session
/// </summary> /// </summary>
@@ -95,70 +82,22 @@ public static class AdmissionHelper
return; return;
} }
AddRefreshToken(container, mobileSession.RefreshToken); container.SetSteamRefreshToken(mobileSession.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community); var community = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString())); 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(SESSION_ID_COOKIE_NAME, mobileSession.SessionId));
container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage)); container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage));
TransferCommunityCookies(container); TransferCommunityCookies(container);
foreach (var domain in SteamDomains.AuthDomains) foreach (var domain in SteamDomains.WebDomains)
{ {
var token = mobileSession.GetToken(domain); var token = mobileSession.GetToken(domain);
if (token == null) continue; 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) 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)) if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh))
throw new ArgumentException( throw new ArgumentException(
$"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}", $"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}",
nameof(token)); nameof(token));
var refreshToken = token.SignedToken; SetSteamRefreshTokenUnsafe(container, token, SteamLoginUri);
container.Add(SteamLoginUri, new Cookie(REFRESH_COOKIE_NAME, refreshToken) }
/// <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() 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) if (token.Type is not SteamAccessTokenType.AccessToken)
throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}", throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}",
nameof(token)); nameof(token));
var domain = SteamDomains.GetDomainUri(token.Domain); var domainUri = SteamDomains.GetDomainUri(token.Domain);
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken) 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, HttpOnly = true,
Secure = true, Secure = true,
@@ -257,7 +262,6 @@ public static class AdmissionHelper
}); });
} }
public static bool IsSteamCookie(Cookie cookie) public static bool IsSteamCookie(Cookie cookie)
{ {
return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") || return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") ||
+19 -25
View File
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel; using System.Collections.Immutable;
using SteamLibForked.Models.Core; using SteamLibForked.Models.Core;
namespace SteamLib.Core; namespace SteamLib.Core;
@@ -6,7 +6,6 @@ namespace SteamLib.Core;
public static class SteamDomains public static class SteamDomains
{ {
public static IReadOnlyDictionary<SteamDomain, string> Domains { get; } = public static IReadOnlyDictionary<SteamDomain, string> Domains { get; } =
new ReadOnlyDictionary<SteamDomain, string>(
new Dictionary<SteamDomain, string> new Dictionary<SteamDomain, string>
{ {
{SteamDomain.Community, SteamConstants.STEAM_COMMUNITY}, {SteamDomain.Community, SteamConstants.STEAM_COMMUNITY},
@@ -16,34 +15,29 @@ public static class SteamDomains
{SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT}, {SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT},
{SteamDomain.Login, SteamConstants.STEAM_LOGIN}, {SteamDomain.Login, SteamConstants.STEAM_LOGIN},
{SteamDomain.API, SteamConstants.STEAM_API} {SteamDomain.API, SteamConstants.STEAM_API}
}); }.ToImmutableDictionary();
public static IReadOnlyDictionary<SteamDomain, Uri> DomainUris { get; } public static IReadOnlyDictionary<SteamDomain, Uri> DomainUris { get; }
= new ReadOnlyDictionary<SteamDomain, Uri>( = Domains
Domains.ToDictionary(x => x.Key, x => new Uri(x.Value)) .ToDictionary(x => x.Key, x => new Uri(x.Value))
); .ToImmutableDictionary();
public static IEnumerable<SteamDomain> AllDomains { get; } = /// <summary>
[ /// All known public Steam domains.
SteamDomain.Community, /// </summary>
SteamDomain.Store, public static IEnumerable<SteamDomain> AllDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community,
SteamDomain.Help, SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout, SteamDomain.Login, SteamDomain.API);
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>
/// 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) public static Uri GetDomainUri(SteamDomain domain)
{ {
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json; using Newtonsoft.Json;
using SteamLib.Core;
using SteamLibForked.Abstractions; using SteamLibForked.Abstractions;
using SteamLibForked.Models.Core; using SteamLibForked.Models.Core;
@@ -7,7 +8,7 @@ namespace SteamLibForked.Models.Session;
//WARNING: Any changes here should be reflected in MafileSerializer.cs //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; } public SteamAuthToken? MobileToken { get; private set; }
@@ -31,9 +32,23 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
return MobileToken; 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))] [MemberNotNull(nameof(MobileToken))]
public void SetMobileToken(SteamAuthToken token) public virtual void SetMobileToken(SteamAuthToken token)
{ {
if (token.Type != SteamAccessTokenType.Mobile) if (token.Type != SteamAccessTokenType.Mobile)
throw new ArgumentException("Token must be of type MobileAccess", nameof(token)) throw new ArgumentException("Token must be of type MobileAccess", nameof(token))
@@ -44,13 +59,13 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
MobileToken = token; MobileToken = token;
} }
public override MobileSessionData Clone()
{
return (MobileSessionData) ((ISessionData) this).Clone();
}
object ICloneable.Clone() object ICloneable.Clone()
{ {
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens); 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 internal class LegacyMafile
{ {
[JsonProperty("shared_secret")] [JsonProperty("shared_secret")] public string SharedSecret { get; set; } = null!;
public string SharedSecret { get; set; } = null!;
[JsonProperty("identity_secret")] [JsonProperty("identity_secret")] public string IdentitySecret { get; set; } = null!;
public string IdentitySecret { get; set; } = null!;
[JsonProperty("device_id")] [JsonProperty("device_id")] public string DeviceId { get; set; } = null!;
public string DeviceId { get; set; } = null!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!; [JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!;
[JsonProperty("account_name")] public string AccountName { 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 else
{ {
container.SetSteamMobileCookiesWithMobileToken(sessionData); container.SetSteamMobileCookies(sessionData);
} }
//Nebula tweak: //Nebula tweak: