mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-29 07:27:36 +08:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd7276f6a | ||
|
|
62fb8038be | ||
|
|
c3279e78c2 | ||
|
|
7c8f8448af | ||
|
|
66fdfb728e | ||
|
|
9261344a3c | ||
|
|
95190efa5c | ||
|
|
dd71972adc | ||
|
|
6c1e6d40e9 | ||
|
|
187c7f3b58 | ||
|
|
4e3c86d009 | ||
|
|
ed5cc929d3 | ||
|
|
213b2c2644 | ||
|
|
e1adf07789 | ||
|
|
83f0634863 | ||
|
|
f08122769f | ||
|
|
49fdeadfd2 | ||
|
|
bca80245bb | ||
|
|
0f6c127ad3 | ||
|
|
c087a02303 | ||
|
|
6c84a876ff | ||
|
|
df27068329 | ||
|
|
f10df31edd | ||
|
|
902919cf98 | ||
|
|
a6aec5ad62 | ||
|
|
2fd651fd25 | ||
|
|
67615028e1 | ||
|
|
1eca159e23 | ||
|
|
d1d9ca82df | ||
|
|
88bd2dbfe9 | ||
|
|
ae2b2970db |
@@ -68,19 +68,19 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Build BD.WTTS.Client.AppHost
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.AppHost\BD.WTTS.Client.AppHost.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.AppHost\BD.WTTS.Client.AppHost.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
|
||||
|
||||
- name: Build BD.WTTS.Client.Plugins.Accelerator.ReverseProxy
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy\BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
|
||||
|
||||
- name: Build BD.WTTS.Client.Avalonia.App
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.App\BD.WTTS.Client.Avalonia.App.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.App\BD.WTTS.Client.Avalonia.App.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
|
||||
|
||||
- name: Build BD.WTTS.Client.Avalonia.Designer.HostApp
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.Designer.HostApp\BD.WTTS.Client.Avalonia.Designer.HostApp.csproj --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
|
||||
run: dotnet build -c Release src\BD.WTTS.Client.Avalonia.Designer.HostApp\BD.WTTS.Client.Avalonia.Designer.HostApp.csproj --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
|
||||
|
||||
- name: Test
|
||||
run: dotnet test src/BD.WTTS.UnitTest/BD.WTTS.UnitTest.csproj -c Release -p:GeneratePackageOnBuild=false --nologo -v q --property:WarningLevel=1;DebugType=pdbonly
|
||||
run: dotnet test src/BD.WTTS.UnitTest/BD.WTTS.UnitTest.csproj -c Release -p:GeneratePackageOnBuild=false --nologo -v q --property:WarningLevel=1 --property:DebugType=pdbonly
|
||||
|
||||
# release:
|
||||
# name: Release
|
||||
|
||||
+1
-1
Submodule ref/DirectoryPackages updated: b6b85044cc...a2fed28cac
+1
-1
Submodule ref/SteamClient updated: fddb907049...3cf39ef2c8
Submodule ref/WTTS.MicroServices.ClientSDK updated: 5f3111baad...e9ee12cce9
+1
-1
Submodule ref/WinAuth updated: f6ad4f84ca...a73491b482
@@ -3,14 +3,6 @@ namespace BD.WTTS;
|
||||
/// <summary>
|
||||
/// 提供用来获取应用程序信息(如版本号、说明、加载的程序集等)的属性。
|
||||
/// </summary>
|
||||
[Mobius3(
|
||||
"""
|
||||
Mobius.AssemblyInfo
|
||||
AssemblyInfo.cs
|
||||
AssemblyInfo.Version.cs
|
||||
AssemblyInfo.Version.NativeHost.cs
|
||||
AssemblyInfo.Version.Setup.Windows.cs
|
||||
""")]
|
||||
public static partial class AssemblyInfo
|
||||
{
|
||||
const string Version2 = "3.0";
|
||||
@@ -29,7 +21,7 @@ public static partial class AssemblyInfo
|
||||
/// <summary>
|
||||
/// RC 版本号,范围 0~9,只增不减
|
||||
/// </summary>
|
||||
const string ver_for_rc = "8";
|
||||
const string ver_for_rc = "9";
|
||||
|
||||
public const string FileVersion = $"{Version2}.{ver_for_rc}0{ver_for_preview}.0";
|
||||
|
||||
@@ -154,20 +146,4 @@ false
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
sealed class Mobius3Attribute : Attribute
|
||||
{
|
||||
public Mobius3Attribute()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Mobius3Attribute(string str)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Obsolete { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Mobius Properties\XmlnsDefinition.cs
|
||||
#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
|
||||
// C# 10 定义全局 using
|
||||
|
||||
|
||||
+2505
-166
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
<Identity
|
||||
Name="4651ED44255E.47979655102CE"
|
||||
Publisher="CN=A90E406B-B2D3-4A23-B061-0FA1D65C4F66"
|
||||
Version="3.0.999.0" />
|
||||
Version="3.0.207.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Watt Toolkit</DisplayName>
|
||||
@@ -66,6 +66,15 @@
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Extensions>
|
||||
<desktop2:Extension Category="windows.firewallRules">
|
||||
<desktop2:FirewallRules Executable="Steam++\modules\Accelerator\Steam++.Accelerator.exe">
|
||||
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all"/>
|
||||
<desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all"/>
|
||||
</desktop2:FirewallRules>
|
||||
</desktop2:Extension>
|
||||
</Extensions>
|
||||
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
|
||||
@@ -13,6 +13,7 @@ public static class WindowTransparencyLevelExtensions
|
||||
WindowBackgroundMaterial.Blur => WindowTransparencyLevel.Blur,
|
||||
WindowBackgroundMaterial.AcrylicBlur => WindowTransparencyLevel.AcrylicBlur,
|
||||
WindowBackgroundMaterial.Mica => WindowTransparencyLevel.Mica,
|
||||
WindowBackgroundMaterial.Transparent => WindowTransparencyLevel.Transparent,
|
||||
_ => WindowTransparencyLevel.None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,28 +7,12 @@ namespace BD.WTTS.UI;
|
||||
|
||||
partial class App
|
||||
{
|
||||
[Mobius(
|
||||
"""
|
||||
App.DefaultActualTheme
|
||||
""")]
|
||||
const AppTheme _DefaultActualTheme = AppTheme.FollowingSystem;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.DefaultActualTheme
|
||||
""")]
|
||||
AppTheme IApplication.DefaultActualTheme => _DefaultActualTheme;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.mTheme
|
||||
""")]
|
||||
AppTheme mTheme = _DefaultActualTheme;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.Theme
|
||||
""")]
|
||||
public AppTheme Theme
|
||||
{
|
||||
get => mTheme;
|
||||
@@ -66,10 +50,6 @@ App.Theme
|
||||
}
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.SetThemeNotChangeValue
|
||||
""")]
|
||||
public void SetThemeNotChangeValue(AppTheme value)
|
||||
{
|
||||
var mode = value switch
|
||||
@@ -91,10 +71,6 @@ App.SetThemeNotChangeValue
|
||||
}
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.SetThemeAccent
|
||||
""")]
|
||||
public static void SetThemeAccent(string? colorHex)
|
||||
{
|
||||
if (colorHex == null)
|
||||
|
||||
@@ -9,34 +9,14 @@ namespace BD.WTTS.UI;
|
||||
|
||||
partial class App
|
||||
{
|
||||
[Mobius(
|
||||
"""
|
||||
App.TrayMenus
|
||||
""")]
|
||||
public Dictionary<string, TrayMenuItem> TrayMenus { get; } = new Dictionary<string, TrayMenuItem>();
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.TrayIcons
|
||||
""")]
|
||||
public TrayIcons TrayIcons { get; init; } = new TrayIcons();
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.trayIconMenus
|
||||
""")]
|
||||
readonly Dictionary<string, ICommand> trayIconMenus = new();
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.TrayIconMenus
|
||||
""")]
|
||||
public IReadOnlyDictionary<string, ICommand> TrayIconMenus => trayIconMenus;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.InitTrayIcon
|
||||
""")]
|
||||
void InitTrayIcon()
|
||||
{
|
||||
if (ApplicationLifetime is not
|
||||
@@ -83,14 +63,50 @@ App.InitTrayIcon
|
||||
#else
|
||||
s.HasTrayIcon ? ShutdownMode.OnExplicitShutdown : ShutdownMode.OnMainWindowClose;
|
||||
#endif
|
||||
|
||||
#if MACOS
|
||||
if (Current!.TryGetFeature<IActivatableLifetime>() is { } activatableLifetime)
|
||||
{
|
||||
activatableLifetime.Activated += ActivatableLifetimeOnActivated;
|
||||
// macOS 中 右键隐藏 会触发目前不需要使用
|
||||
//activatableLifetime.Deactivated += ActivatableLifetimeOnDeactivated;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.UpdateMenuItems
|
||||
""")]
|
||||
#if MACOS
|
||||
/// <summary>
|
||||
/// 从 macOS Dock 栏恢复窗口显示
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void ActivatableLifetimeOnActivated(object? sender, ActivatedEventArgs e)
|
||||
{
|
||||
switch (e.Kind)
|
||||
{
|
||||
case ActivationKind.Background:
|
||||
case ActivationKind.Reopen:
|
||||
RestoreMainWindow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// macOS 中 右键隐藏 会触发目前不需要使用
|
||||
///// </summary>
|
||||
///// <param name="sender"></param>
|
||||
///// <param name="e"></param>
|
||||
//private void ActivatableLifetimeOnDeactivated(object? sender, ActivatedEventArgs e)
|
||||
//{
|
||||
// switch (e.Kind)
|
||||
// {
|
||||
// case ActivationKind.Background:
|
||||
// Console.WriteLine($"ActivatableLifetimeOnDeactivated {e.Kind}");
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
#endif
|
||||
|
||||
public void UpdateMenuItems()
|
||||
{
|
||||
MainThread2.BeginInvokeOnMainThread(() =>
|
||||
@@ -190,10 +206,6 @@ App.UpdateMenuItems
|
||||
});
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.UpdateMenuItems
|
||||
""")]
|
||||
public void UpdateMenuItems(string menuKey, TrayMenuItem trayMenuItem)
|
||||
{
|
||||
if (TrayMenus != null)
|
||||
|
||||
@@ -14,6 +14,8 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel>, IV
|
||||
public static readonly StyledProperty<bool> IsSaveWindowSizeProperty = AvaloniaProperty
|
||||
.Register<ReactiveAppWindow<TViewModel>, bool>(nameof(IsSaveWindowSize), true);
|
||||
|
||||
private bool _isInitialize;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReactiveAppWindow{TViewModel}"/> class.
|
||||
/// </summary>
|
||||
@@ -127,27 +129,32 @@ public class ReactiveAppWindow<TViewModel> : AppWindow, IViewFor<TViewModel>, IV
|
||||
{
|
||||
var primaryScreenBounds = screen.Bounds;
|
||||
|
||||
if (CanResize && !IsVisible)
|
||||
if (CanResize && !_isInitialize && !IsVisible)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
var scalingOffset = 31 - ((screen.Scaling - 1) / 0.5); // appwindow偏移量
|
||||
|
||||
if (SizePosition.Width > 0 &&
|
||||
primaryScreenBounds.Width >= SizePosition.Width)
|
||||
Width = SizePosition.Width;
|
||||
|
||||
if (SizePosition.Height > 0 &&
|
||||
primaryScreenBounds.Height >= SizePosition.Height)
|
||||
Height = SizePosition.Height;
|
||||
Height = SizePosition.Height - scalingOffset;
|
||||
|
||||
if (SizePosition.X > 0 && SizePosition.Y > 0)
|
||||
{
|
||||
var leftTopPoint = new PixelPoint(SizePosition.X, SizePosition.Y);
|
||||
var offsetY = (int)Math.Round(scalingOffset * screen.Scaling);
|
||||
//var offsetY = scalingOffset;
|
||||
var leftTopPoint = new PixelPoint(SizePosition.X, SizePosition.Y + offsetY);
|
||||
var rightBottomPoint = new PixelPoint(SizePosition.X + (int)Width, SizePosition.Y + (int)Height);
|
||||
if (primaryScreenBounds.Contains(leftTopPoint) &&
|
||||
primaryScreenBounds.Contains(rightBottomPoint))
|
||||
{
|
||||
Position = leftTopPoint;
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
}
|
||||
}
|
||||
_isInitialize = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,13 +74,18 @@
|
||||
MaxWidth="1080"
|
||||
Spacing="20">
|
||||
<spp:Snackbar
|
||||
Title="{Binding Path=Res.Notice_Tray_Title, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
IsVisible="{spp:OnPlatform !GA}"
|
||||
Message="当前使用的程序版本为测试版本,可能会有各种各样不可言明的 Bug,遇到问题请理性反馈,谢谢。"
|
||||
Severity="Warning" />
|
||||
<spp:Snackbar
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
IsVisible="{spp:OnPlatform !GA}"
|
||||
Message="如果您不喜欢当前主页为默认启动页面可以在【设置-界面设置-启动时默认打开页面】处更改" />
|
||||
<spp:Snackbar
|
||||
Title="{Binding Path=Res.Notice_Tray_Title, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
VerticalAlignment="Top"
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
<TextBlock Text="{Binding Path=User.NickName, Mode=OneWay, Source={x:Static s:UserService.Current}}" />
|
||||
<Border
|
||||
Padding="5,1"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="2"
|
||||
IsVisible="{Binding Path=User.UserType, Mode=OneWay, Source={x:Static s:UserService.Current}, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:UserType.Sponsor}}">
|
||||
|
||||
@@ -341,6 +341,7 @@
|
||||
<ComboBox SelectedItem="{Binding Source={x:Static ms:UISettings.WindowBackgroundMaterial}, Path=Value, Mode=TwoWay}">
|
||||
<ComboBox.Items>
|
||||
<spp:WindowBackgroundMaterial>None</spp:WindowBackgroundMaterial>
|
||||
<spp:WindowBackgroundMaterial>Transparent</spp:WindowBackgroundMaterial>
|
||||
<spp:WindowBackgroundMaterial>Blur</spp:WindowBackgroundMaterial>
|
||||
<spp:WindowBackgroundMaterial>AcrylicBlur</spp:WindowBackgroundMaterial>
|
||||
<spp:WindowBackgroundMaterial>Mica</spp:WindowBackgroundMaterial>
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed partial class MainWindow : ReactiveAppWindow<MainWindowViewModel>
|
||||
pageType = page.PageType;
|
||||
}
|
||||
}
|
||||
INavigationService.Instance.Navigate(pageType ?? typeof(PluginStorePage));
|
||||
INavigationService.Instance.Navigate(pageType ?? typeof(HomePage));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace BD;
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
public sealed class MobiusAttribute : Attribute
|
||||
{
|
||||
public MobiusAttribute()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MobiusAttribute(string str)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Obsolete { get; set; }
|
||||
}
|
||||
@@ -6,7 +6,6 @@ namespace BD.WTTS.Services;
|
||||
/// <summary>
|
||||
/// 子进程的 IPC 服务实现
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
public interface IPCSubProcessService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace BD;
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
public sealed class MobiusAttribute : Attribute
|
||||
{
|
||||
public MobiusAttribute()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MobiusAttribute(string str)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Obsolete { get; set; }
|
||||
}
|
||||
+2
-2
@@ -12,10 +12,10 @@ sealed class LifetimeHttpHandler : DelegatingHandler
|
||||
|
||||
public LifeTimeKey LifeTimeKey { get; }
|
||||
|
||||
public LifetimeHttpHandler(IDomainResolver domainResolver, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
|
||||
public LifetimeHttpHandler(IDomainResolver domainResolver, IWebProxy webProxy, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
|
||||
{
|
||||
LifeTimeKey = lifeTimeKey;
|
||||
InnerHandler = new ReverseProxyHttpClientHandler(lifeTimeKey.DomainConfig, domainResolver);
|
||||
InnerHandler = new ReverseProxyHttpClientHandler(lifeTimeKey.DomainConfig, domainResolver, webProxy);
|
||||
timer = new Timer(OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public class ReverseProxyHttpClient : HttpMessageInvoker
|
||||
// = new(new ProductHeaderValue(Constants.HARDCODED_APP_NAME_NEW + "_" + DateTimeOffset.UtcNow.Ticks, ThisAssembly.Version));
|
||||
|
||||
public ReverseProxyHttpClient(IDomainConfig domainConfig, IDomainResolver domainResolver)
|
||||
: this(new ReverseProxyHttpClientHandler(domainConfig, domainResolver), disposeHandler: true)
|
||||
: this(new ReverseProxyHttpClientHandler(domainConfig, domainResolver, HttpNoProxy.Instance), disposeHandler: true)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+195
-1
@@ -7,12 +7,14 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
|
||||
{
|
||||
readonly IDomainConfig domainConfig;
|
||||
readonly IDomainResolver domainResolver;
|
||||
readonly IWebProxy webProxy;
|
||||
readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
|
||||
|
||||
public ReverseProxyHttpClientHandler(IDomainConfig domainConfig, IDomainResolver domainResolver)
|
||||
public ReverseProxyHttpClientHandler(IDomainConfig domainConfig, IDomainResolver domainResolver, IWebProxy webProxy)
|
||||
{
|
||||
this.domainConfig = domainConfig;
|
||||
this.domainResolver = domainResolver;
|
||||
this.webProxy = webProxy;
|
||||
InnerHandler = CreateSocketsHttpHandler();
|
||||
}
|
||||
|
||||
@@ -53,12 +55,199 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
|
||||
{
|
||||
Proxy = HttpNoProxy.Instance,
|
||||
UseProxy = false,
|
||||
PreAuthenticate = false,
|
||||
UseCookies = false,
|
||||
AllowAutoRedirect = false,
|
||||
AutomaticDecompression = DecompressionMethods.None,
|
||||
ConnectCallback = ConnectCallback,
|
||||
EnableMultipleHttp2Connections = true,
|
||||
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8,
|
||||
ResponseHeaderEncodingSelector = (_, _) => Encoding.UTF8,
|
||||
};
|
||||
|
||||
private async ValueTask<Stream> ConnectThroughProxyAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.InitialRequestMessage.RequestUri == null)
|
||||
{
|
||||
throw new InvalidOperationException("Request URI is null");
|
||||
}
|
||||
var proxyUri = webProxy.GetProxy(context.InitialRequestMessage.RequestUri);
|
||||
if (proxyUri == null)
|
||||
{
|
||||
throw new InvalidOperationException("Proxy URI is null");
|
||||
}
|
||||
|
||||
var proxyEndPoint = new DnsEndPoint(proxyUri.Host, proxyUri.Port);
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
await socket.ConnectAsync(proxyEndPoint, cancellationToken);
|
||||
|
||||
var stream = new NetworkStream(socket, ownsSocket: true);
|
||||
|
||||
// 确定代理类型
|
||||
var proxyType = DetermineProxyType(proxyUri);
|
||||
|
||||
switch (proxyType)
|
||||
{
|
||||
case ExternalProxyType.Http:
|
||||
await ConnectHttpProxy(stream, context, cancellationToken);
|
||||
break;
|
||||
case ExternalProxyType.Socks4:
|
||||
await ConnectSocks4Proxy(stream, context, cancellationToken);
|
||||
break;
|
||||
case ExternalProxyType.Socks5:
|
||||
await ConnectSocks5Proxy(stream, context, cancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
if (context.InitialRequestMessage.RequestUri.Scheme == "https" || context.InitialRequestMessage.RequestUri.Port == 443)
|
||||
{
|
||||
var requestContext = context.InitialRequestMessage.GetRequestContext();
|
||||
var tlsSniValue = requestContext.TlsSniValue;
|
||||
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
|
||||
{
|
||||
TargetHost = tlsSniValue.Value,
|
||||
RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true,
|
||||
}, cancellationToken);
|
||||
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private ExternalProxyType DetermineProxyType(Uri proxyUri)
|
||||
{
|
||||
return proxyUri.Scheme.ToLower() switch
|
||||
{
|
||||
"socks4" => ExternalProxyType.Socks4,
|
||||
"socks5" => ExternalProxyType.Socks5,
|
||||
_ => ExternalProxyType.Http
|
||||
};
|
||||
|
||||
throw new NotImplementedException("Proxy type determination needs to be implemented");
|
||||
}
|
||||
|
||||
private async Task ConnectHttpProxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var credentials = webProxy.Credentials as NetworkCredential;
|
||||
var authHeader = credentials != null
|
||||
? $"Proxy-Authorization: Basic {Convert.ToBase64String(Encoding.ASCII.GetBytes($"{credentials.UserName}:{credentials.Password}"))}\r\n"
|
||||
: string.Empty;
|
||||
|
||||
var connectRequest = $"CONNECT {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port} HTTP/1.1\r\n" +
|
||||
$"Host: {context.DnsEndPoint.Host}:{context.DnsEndPoint.Port}\r\n" +
|
||||
$"{authHeader}\r\n";
|
||||
|
||||
var connectBytes = Encoding.ASCII.GetBytes(connectRequest);
|
||||
await stream.WriteAsync(connectBytes, cancellationToken);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
var bytesRead = await stream.ReadAsync(buffer, cancellationToken);
|
||||
var response = Encoding.ASCII.GetString(buffer, 0, bytesRead);
|
||||
|
||||
if (!response.StartsWith("HTTP/1.1 200"))
|
||||
{
|
||||
throw new Exception($"Proxy connection failed: {response}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectSocks4Proxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var credentials = webProxy.Credentials as NetworkCredential;
|
||||
var userId = credentials?.UserName ?? string.Empty;
|
||||
|
||||
var destinationAddress = await GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken).FirstAsync();
|
||||
var portBytes = BitConverter.GetBytes((ushort)context.DnsEndPoint.Port);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(portBytes);
|
||||
|
||||
var request = new byte[] { 0x04, 0x01 }
|
||||
.Concat(portBytes)
|
||||
.Concat(destinationAddress.Address.GetAddressBytes())
|
||||
.Concat(Encoding.ASCII.GetBytes(userId))
|
||||
.Concat(new byte[] { 0x00 })
|
||||
.ToArray();
|
||||
|
||||
await stream.WriteAsync(request, cancellationToken);
|
||||
|
||||
var response = new byte[8];
|
||||
await stream.ReadAsync(response, cancellationToken);
|
||||
|
||||
if (response[1] != 0x5A)
|
||||
{
|
||||
throw new Exception($"SOCKS4 proxy connection failed: {response[1]}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectSocks5Proxy(NetworkStream stream, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var credentials = webProxy.Credentials as NetworkCredential;
|
||||
|
||||
// SOCKS5 握手
|
||||
byte[] authMethod = credentials != null ? new byte[] { 0x02 } : new byte[] { 0x00 };
|
||||
await stream.WriteAsync(new byte[] { 0x05, 0x01 }.Concat(authMethod).ToArray(), cancellationToken);
|
||||
|
||||
var response = new byte[2];
|
||||
await stream.ReadAsync(response, cancellationToken);
|
||||
|
||||
if (response[0] != 0x05)
|
||||
{
|
||||
throw new Exception("SOCKS5 handshake failed");
|
||||
}
|
||||
|
||||
// 如果需要身份验证
|
||||
if (response[1] == 0x02 && credentials != null)
|
||||
{
|
||||
var userBytes = Encoding.ASCII.GetBytes(credentials.UserName);
|
||||
var passBytes = Encoding.ASCII.GetBytes(credentials.Password);
|
||||
var authRequest = new byte[] { 0x01, (byte)userBytes.Length }
|
||||
.Concat(userBytes)
|
||||
.Concat(new byte[] { (byte)passBytes.Length })
|
||||
.Concat(passBytes)
|
||||
.ToArray();
|
||||
|
||||
await stream.WriteAsync(authRequest, cancellationToken);
|
||||
|
||||
var authResponse = new byte[2];
|
||||
await stream.ReadAsync(authResponse, cancellationToken);
|
||||
|
||||
if (authResponse[1] != 0x00)
|
||||
{
|
||||
throw new Exception("SOCKS5 authentication failed");
|
||||
}
|
||||
}
|
||||
|
||||
// 发送连接请求
|
||||
byte[] addressBytes;
|
||||
byte addressType;
|
||||
if (IPAddress.TryParse(context.DnsEndPoint.Host, out IPAddress? ipAddress))
|
||||
{
|
||||
addressBytes = ipAddress.GetAddressBytes();
|
||||
addressType = (byte)(ipAddress.AddressFamily == AddressFamily.InterNetwork ? 1 : 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
addressBytes = Encoding.ASCII.GetBytes(context.DnsEndPoint.Host);
|
||||
addressType = 3; // 域名
|
||||
}
|
||||
|
||||
var request = new byte[] { 0x05, 0x01, 0x00, addressType }
|
||||
.Concat(addressType == 3 ? new[] { (byte)addressBytes.Length } : Array.Empty<byte>())
|
||||
.Concat(addressBytes)
|
||||
.Concat(BitConverter.GetBytes((ushort)context.DnsEndPoint.Port).Reverse())
|
||||
.ToArray();
|
||||
|
||||
await stream.WriteAsync(request, cancellationToken);
|
||||
|
||||
var connectResponse = new byte[10];
|
||||
await stream.ReadAsync(connectResponse, cancellationToken);
|
||||
|
||||
if (connectResponse[1] != 0x00)
|
||||
{
|
||||
throw new Exception($"SOCKS5 connection failed: {connectResponse[1]}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接回调
|
||||
/// </summary>
|
||||
@@ -67,6 +256,11 @@ sealed class ReverseProxyHttpClientHandler : DelegatingHandler
|
||||
/// <returns></returns>
|
||||
async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (webProxy != null && !HttpNoProxy.IsNoProxy(webProxy))
|
||||
{
|
||||
return await ConnectThroughProxyAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
var innerExceptions = new List<Exception>();
|
||||
var ipEndPoints = GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
|
||||
|
||||
|
||||
+20
-23
@@ -1,3 +1,4 @@
|
||||
using System.Net;
|
||||
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
@@ -140,11 +141,6 @@ sealed class HttpLocalRequestMiddleware
|
||||
return null;
|
||||
}
|
||||
|
||||
StringValues cookie = context.Request.Headers["cookie-steamTool"];
|
||||
if (StringValues.IsNullOrEmpty(cookie))
|
||||
cookie = context.Request.Headers["Cookie"];
|
||||
StringValues referer = context.Request.Headers["Referer-steamTool"];
|
||||
|
||||
context.Response.Headers.AccessControlAllowOrigin = context.Request.Headers.Origin.Count == 0 ? "*" : context.Request.Headers.Origin;
|
||||
context.Response.Headers.AccessControlAllowHeaders = "*";
|
||||
context.Response.Headers.AccessControlAllowMethods = "*";
|
||||
@@ -166,29 +162,30 @@ sealed class HttpLocalRequestMiddleware
|
||||
Method = methodObj,
|
||||
Content = hasReqContent ? new StreamContent(context.Request.Body) : null,
|
||||
};
|
||||
req.Headers.UserAgent.ParseAdd(context.Request.Headers.UserAgent);
|
||||
if (!StringValues.IsNullOrEmpty(cookie))
|
||||
foreach (var item in context.Request.Headers)
|
||||
{
|
||||
CookieHttpClient.CookieContainer.Add(new Cookie
|
||||
{
|
||||
CommentUri = requestUri,
|
||||
Domain = requestUri.Host,
|
||||
Value = cookie,
|
||||
});
|
||||
// req.Headers.Add("Cookie", cookie);
|
||||
}
|
||||
if (!StringValues.IsNullOrEmpty(referer))
|
||||
{
|
||||
try
|
||||
{
|
||||
var refererUri = new Uri(referer.ToString());
|
||||
req.Headers.Referrer = refererUri;
|
||||
}
|
||||
catch
|
||||
var headers = item.Key.ToLower();
|
||||
if (headers.EndsWith("-steamtool"))
|
||||
{
|
||||
|
||||
if (headers.Equals("cookie", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
CookieHttpClient.CookieContainer.Add(new Cookie
|
||||
{
|
||||
CommentUri = requestUri,
|
||||
Domain = requestUri.Host,
|
||||
Value = item.Value
|
||||
});
|
||||
}
|
||||
if (headers.Equals("referer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var refererUri = new Uri(item.Value.ToString());
|
||||
req.Headers.Referrer = refererUri;
|
||||
}
|
||||
req.Headers.TryAddWithoutValidation(item.Key.TrimEnd("-steamtool"), (IEnumerable<string?>)item.Value);
|
||||
}
|
||||
}
|
||||
req.Headers.UserAgent.ParseAdd(context.Request.Headers.UserAgent);
|
||||
if (hasReqContent)
|
||||
{
|
||||
req.Content!.Headers.ContentType = MediaTypeHeaderValue.Parse(context.Request.ContentType.ThrowIsNull());
|
||||
|
||||
+24
-1
@@ -1,5 +1,6 @@
|
||||
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.HttpServer/HttpReverseProxyMiddleware.cs
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Yarp.ReverseProxy.Forwarder;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
@@ -52,7 +53,29 @@ sealed partial class HttpReverseProxyMiddleware
|
||||
|
||||
if (TryGetDomainConfig(url, out var domainConfig) == false)
|
||||
{
|
||||
await next(context);
|
||||
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateHttpClient("GlobalProxy", defaultDomainConfig);
|
||||
var destinationPrefix = GetDestinationPrefix(context.Request.Scheme, context.Request.Host, null);
|
||||
var forwarderRequestConfig = new ForwarderRequestConfig()
|
||||
{
|
||||
Version = context.Request.Protocol switch
|
||||
{
|
||||
var protocol when protocol.StartsWith("HTTP/2") => System.Net.HttpVersion.Version20,
|
||||
var protocol when protocol.StartsWith("HTTP/3") => System.Net.HttpVersion.Version30,
|
||||
_ => System.Net.HttpVersion.Version11,
|
||||
},
|
||||
};
|
||||
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, forwarderRequestConfig, HttpTransformer.Empty);
|
||||
if (error != ForwarderError.None)
|
||||
{
|
||||
await HandleErrorAsync(context, error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+15
-2
@@ -1,11 +1,14 @@
|
||||
// https://github.com/dotnetcore/FastGithub/blob/2.1.4/FastGithub.Http/HttpClientFactory.cs
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
using System.Net;
|
||||
|
||||
namespace BD.WTTS.Services.Implementation;
|
||||
|
||||
sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
|
||||
{
|
||||
readonly IDomainResolver domainResolver;
|
||||
readonly IWebProxy webProxy;
|
||||
|
||||
/// <summary>
|
||||
/// 首次生命周期
|
||||
@@ -27,9 +30,19 @@ sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
|
||||
/// </summary>
|
||||
readonly ConcurrentDictionary<LifeTimeKey, Lazy<LifetimeHttpHandler>> httpHandlerLazyCache = new();
|
||||
|
||||
public ReverseProxyHttpClientFactory(IDomainResolver domainResolver)
|
||||
public ReverseProxyHttpClientFactory(IDomainResolver domainResolver, IReverseProxyConfig reverseProxyConfig)
|
||||
{
|
||||
this.domainResolver = domainResolver;
|
||||
if (reverseProxyConfig.Service.TwoLevelAgentEnable)
|
||||
{
|
||||
this.webProxy = new WebProxy($"{reverseProxyConfig.Service.TwoLevelAgentProxyType}://{reverseProxyConfig.Service.TwoLevelAgentIp}:{reverseProxyConfig.Service.TwoLevelAgentPortId}", true, null, null);
|
||||
if (!string.IsNullOrEmpty(reverseProxyConfig.Service.TwoLevelAgentUserName) || !string.IsNullOrEmpty(reverseProxyConfig.Service.TwoLevelAgentPassword))
|
||||
{
|
||||
webProxy.Credentials = new NetworkCredential(reverseProxyConfig.Service.TwoLevelAgentUserName, reverseProxyConfig.Service.TwoLevelAgentPassword);
|
||||
}
|
||||
}
|
||||
else
|
||||
this.webProxy = HttpNoProxy.Instance;
|
||||
}
|
||||
|
||||
public ReverseProxyHttpClient CreateHttpClient(string domain, IDomainConfig domainConfig)
|
||||
@@ -43,7 +56,7 @@ sealed class ReverseProxyHttpClientFactory : IReverseProxyHttpClientFactory
|
||||
}
|
||||
|
||||
LifetimeHttpHandler CreateLifetimeHttpHandler(LifeTimeKey lifeTimeKey, TimeSpan lifeTime)
|
||||
=> new(domainResolver, lifeTimeKey, lifeTime, OnLifetimeHttpHandlerDeactivate);
|
||||
=> new(domainResolver, webProxy, lifeTimeKey, lifeTime, OnLifetimeHttpHandlerDeactivate);
|
||||
|
||||
void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler)
|
||||
{
|
||||
|
||||
+6
-3
@@ -164,10 +164,13 @@ abstract class ReverseProxyServiceImpl : IReverseProxySettings
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reverseProxySettings.ProxyIp))
|
||||
return StartProxyResultCode.Exception;
|
||||
var inUsePort = SocketHelper.IsUsePort(IPAddress.Parse(reverseProxySettings.ProxyIp!), 443);
|
||||
if (inUsePort)
|
||||
if (reverseProxySettings.ProxyMode == ProxyMode.Hosts)
|
||||
{
|
||||
return StartProxyResultCode.BindPortError;
|
||||
var inUsePort = SocketHelper.IsUsePort(IPAddress.Parse(reverseProxySettings.ProxyIp!), 443);
|
||||
if (inUsePort)
|
||||
{
|
||||
return StartProxyResultCode.BindPortError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -80,17 +80,19 @@ sealed partial class YarpReverseProxyServiceImpl : ReverseProxyServiceImpl, IRev
|
||||
builder.WebHost.UseKestrel(options =>
|
||||
{
|
||||
options.AddServerHeader = false;
|
||||
options.RequestHeaderEncodingSelector = _ => Encoding.UTF8;
|
||||
options.ResponseHeaderEncodingSelector = _ => Encoding.UTF8;
|
||||
options.NoLimit();
|
||||
#if WINDOWS
|
||||
#if !NET7_0_OR_GREATER
|
||||
if (OperatingSystem2.IsWindows7())
|
||||
{
|
||||
//https://github.com/dotnet/aspnetcore/issues/22563
|
||||
options.ConfigureHttpsDefaults(httpsOptions =>
|
||||
{
|
||||
httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
|
||||
});
|
||||
}
|
||||
if (OperatingSystem2.IsWindows7())
|
||||
{
|
||||
//https://github.com/dotnet/aspnetcore/issues/22563
|
||||
options.ConfigureHttpsDefaults(httpsOptions =>
|
||||
{
|
||||
httpsOptions.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
//options.ListenSshReverseProxy();
|
||||
//options.ListenGitReverseProxy();
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ public partial interface IDnsAnalysisService // 公共常量定义
|
||||
const string DNS_Ali_DohAddres2 = "https://223.6.6.6/dns-query";
|
||||
const string DNS_Ali_DohAddres3 = "https://223.5.5.5/dns-query";
|
||||
|
||||
const string Dnspod_DohAddres = "https://1.12.12.12";
|
||||
const string Dnspod_DohAddres2 = "https://doh.pub";
|
||||
const string Dnspod_DohAddres = "https://1.12.12.12/dns-query";
|
||||
const string Dnspod_DohAddres2 = "https://doh.pub/dns-query";
|
||||
const string Dnspod_DohAddres3 = "https://120.53.53.53/dns-query";
|
||||
|
||||
const string Google_DohAddres = "https://dns.google";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 网络环境检查步骤
|
||||
/// </summary>
|
||||
public enum NetworkEnvCheckStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Hosts文件检查
|
||||
/// </summary>
|
||||
HostsFileCheck = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 网卡检查
|
||||
/// </summary>
|
||||
NetworkInterfaceCheck = 2,
|
||||
|
||||
/// <summary>
|
||||
/// LSP检查
|
||||
/// </summary>
|
||||
LSPCheck = 3
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
public enum TransportProtocol
|
||||
{
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace BD.WTTS.Services.Implementation;
|
||||
|
||||
public class NetworkEnvCheckService
|
||||
{
|
||||
const string HOST_FILE_PATH =
|
||||
#if WINDOWS
|
||||
@"C:\Windows\System32\drivers\etc\hosts";
|
||||
#else
|
||||
"/etc/hosts";
|
||||
#endif
|
||||
|
||||
public async Task CheckNetworkEnvAsync(IProgress<(NetworkEnvCheckStep Step, bool HasError, string Msg)> progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Parallel.ForAsync(
|
||||
(int)NetworkEnvCheckStep.HostsFileCheck,
|
||||
(int)NetworkEnvCheckStep.LSPCheck + 1,
|
||||
cancellationToken,
|
||||
async (step, ct) =>
|
||||
{
|
||||
(bool pass, string msg) = step switch
|
||||
{
|
||||
(int)NetworkEnvCheckStep.HostsFileCheck => await CheckHostsAsync(),
|
||||
(int)NetworkEnvCheckStep.NetworkInterfaceCheck => CheckNetworkInterfaces(),
|
||||
(int)NetworkEnvCheckStep.LSPCheck =>
|
||||
#if WINDOWS
|
||||
CheckLSP(HandleWSAProtocolInfo),
|
||||
#else
|
||||
(true, string.Empty),
|
||||
#endif
|
||||
_ => (true, string.Empty)
|
||||
};
|
||||
|
||||
progress.Report(((NetworkEnvCheckStep)step, pass, msg));
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Task<(bool Pass, string Msg)> CheckHostsAsync()
|
||||
{
|
||||
bool hostFileExist = File.Exists(HOST_FILE_PATH);
|
||||
|
||||
if (!hostFileExist)
|
||||
return Task.FromResult((false, "未找到 hosts 文件"));
|
||||
|
||||
return Task.FromResult((hostFileExist, string.Empty));
|
||||
}
|
||||
|
||||
private static (bool Pass, string ErrorMsg) CheckNetworkInterfaces(NetworkInterface[]? interfaces = null)
|
||||
{
|
||||
interfaces ??= NetworkInterface.GetAllNetworkInterfaces();
|
||||
|
||||
if (interfaces == null || interfaces.Length == 0)
|
||||
return (false, "未获取到网络适配器信息!");
|
||||
|
||||
// 如何判断虚拟网卡?
|
||||
|
||||
var useableNetworkInterfaces =
|
||||
interfaces.Where(x =>
|
||||
!x.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase) &&
|
||||
(x.NetworkInterfaceType == NetworkInterfaceType.Ethernet || x.NetworkInterfaceType == NetworkInterfaceType.Wireless80211));
|
||||
|
||||
if (!useableNetworkInterfaces.Any())
|
||||
return (false, "未获取到可用的以太网,无线网络适配器");
|
||||
|
||||
var checkResults =
|
||||
useableNetworkInterfaces.Select(x => new
|
||||
{
|
||||
Source = x,
|
||||
Result = CheckNetworkInterface(x)
|
||||
});
|
||||
|
||||
if (checkResults.Any(x => x.Result.Pass))
|
||||
return (true, string.Empty);
|
||||
|
||||
var firstError = checkResults.First(x => !x.Result.Pass);
|
||||
|
||||
return (firstError.Result.Pass, $"{firstError.Source.Name}|{firstError.Source.Description} {firstError.Result.ErrorMsg}");
|
||||
}
|
||||
|
||||
private static (bool Pass, string ErrorMsg) CheckNetworkInterface(NetworkInterface @interface)
|
||||
{
|
||||
var name = @interface.Name;
|
||||
var desc = @interface.Description;
|
||||
|
||||
var ipProps = @interface.GetIPProperties();
|
||||
|
||||
if (@interface.OperationalStatus != OperationalStatus.Up)
|
||||
return (false, "未启用网卡");
|
||||
|
||||
if (ipProps == null)
|
||||
return (false, $"获取网络信息失败!");
|
||||
|
||||
bool hasIPv4 = ipProps.UnicastAddresses
|
||||
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
|
||||
|
||||
bool hasIPv6 = ipProps.UnicastAddresses
|
||||
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetworkV6);
|
||||
|
||||
if (!hasIPv4 && !hasIPv6)
|
||||
return (false, "网络适配器未启用TCP/IP服务");
|
||||
|
||||
// 如何判断虚拟网卡?
|
||||
|
||||
var dns = ipProps.DnsAddresses;
|
||||
|
||||
#if WINDOWS
|
||||
var isDHCPEnable = ipProps.GetIPv4Properties().IsDhcpEnabled;
|
||||
#endif
|
||||
return (true, string.Empty);
|
||||
}
|
||||
|
||||
private static (bool Pass, string ErrorMsg) CheckLSP(CheckLSPPredicate predicate)
|
||||
{
|
||||
int bufferSize = 0;
|
||||
int error = 0;
|
||||
|
||||
// 获取 buffer 的大小
|
||||
_ = WSCEnumProtocols(null, IntPtr.Zero, ref bufferSize, ref error);
|
||||
|
||||
IntPtr protocolBuffer = Marshal.AllocHGlobal(bufferSize);
|
||||
|
||||
try
|
||||
{
|
||||
int result = WSCEnumProtocols(null, protocolBuffer, ref bufferSize, ref error);
|
||||
if (result == -1)
|
||||
{
|
||||
return (false, "获取LSP信息失败!");
|
||||
}
|
||||
|
||||
int structSize = Marshal.SizeOf(typeof(WSAPROTOCOL_INFO));
|
||||
int count = bufferSize / structSize;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
IntPtr item = new(protocolBuffer + (i * structSize));
|
||||
|
||||
WSAPROTOCOL_INFO protocolInfo = Marshal.PtrToStructure<WSAPROTOCOL_INFO>(item);
|
||||
|
||||
#if DEBUG
|
||||
// Console.WriteLine($"Protocol: {protocolInfo.szProtocol}");
|
||||
// Console.WriteLine($"Address Family: {protocolInfo.iAddressFamily}");
|
||||
// Console.WriteLine($"Socket Type: {protocolInfo.iSocketType}");
|
||||
// Console.WriteLine($"Protocol: {protocolInfo.iProtocol}");
|
||||
// Console.WriteLine($"Provider ID: {protocolInfo.ProviderId}");
|
||||
// Console.WriteLine();
|
||||
#endif
|
||||
var handle = predicate(in protocolInfo);
|
||||
|
||||
if (!handle)
|
||||
return (false, "LSP响应异常!");
|
||||
}
|
||||
|
||||
return (true, string.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(protocolBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
#region P/Invoke
|
||||
|
||||
/// <summary>
|
||||
/// 处理判断 协议信息是否有效
|
||||
/// </summary>
|
||||
/// <param name="info"></param>
|
||||
/// <returns></returns>
|
||||
static bool HandleWSAProtocolInfo(in WSAPROTOCOL_INFO info)
|
||||
{
|
||||
const int MAX = 255;
|
||||
|
||||
StringBuilder pathBuilder = new StringBuilder(MAX);
|
||||
int pathLength = MAX;
|
||||
|
||||
Guid queryProviderId = info.ProviderId;
|
||||
|
||||
int result = WSCGetProviderPath(ref queryProviderId, pathBuilder, ref pathLength, out int errorCode);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
// 获取失败
|
||||
}
|
||||
|
||||
// path 可能包含系统环境变量例如 %SystemRoot%
|
||||
// 是否需要解析
|
||||
string path = pathBuilder.ToString();
|
||||
|
||||
Console.WriteLine("path:" + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
[DllImport("Ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
static extern int WSCEnumProtocols(
|
||||
int[]? lpiProtocols,
|
||||
IntPtr lpProtocolBuffer,
|
||||
ref int lpdwBufferLength,
|
||||
ref int lpErrno);
|
||||
|
||||
[DllImport("Ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
static extern int WSCGetProviderPath(
|
||||
[In] ref Guid providerId,
|
||||
[Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder providerDllPath,
|
||||
[In, Out] ref int providerDllPathLength,
|
||||
[Out] out int lpErrno
|
||||
);
|
||||
|
||||
private delegate bool CheckLSPPredicate(in WSAPROTOCOL_INFO protocolInfo);
|
||||
|
||||
/// <summary>
|
||||
/// WSAPROTOCOL_INFO 结构用于存储或检索给定协议的完整信息。
|
||||
/// <see cref="https://learn.microsoft.com/zh-cn/windows/win32/api/winsock2/ns-winsock2-wsaprotocol_infoa"/>
|
||||
/// </summary>
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "<挂起>")]
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct WSAPROTOCOL_INFO
|
||||
{
|
||||
/// <summary>
|
||||
/// 描述协议提供的服务的位掩码
|
||||
/// </summary>
|
||||
public uint dwServiceFlags1;
|
||||
|
||||
/// <summary>
|
||||
/// 保留给其他协议属性定义。
|
||||
/// </summary>
|
||||
public uint dwServiceFlags2;
|
||||
|
||||
/// <summary>
|
||||
/// 保留给其他协议属性定义。
|
||||
/// </summary>
|
||||
public uint dwServiceFlags3;
|
||||
|
||||
/// <summary>
|
||||
/// 保留给其他协议属性定义。
|
||||
/// </summary>
|
||||
public uint dwServiceFlags4;
|
||||
|
||||
/// <summary>
|
||||
/// 一组标志,提供有关如何在 Winsock 目录中表示此协议的信息
|
||||
/// </summary>
|
||||
public uint dwProviderFlags;
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供商供应商) 分配给提供程序的 GUID
|
||||
/// </summary>
|
||||
public Guid ProviderId;
|
||||
|
||||
/// <summary>
|
||||
/// 由 WS2_32.DLL 为每个WSAPROTOCOL_INFO结构分配的唯 一 标识符。
|
||||
/// </summary>
|
||||
public uint dwCatalogEntryId;
|
||||
|
||||
public WSAPROTOCOLCHAIN ProtocolChain;
|
||||
|
||||
/// <summary>
|
||||
/// 协议版本标识符。
|
||||
/// </summary>
|
||||
public int iVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 要作为地址系列参数传递给 套接字 或 WSASocket 函数的
|
||||
/// </summary>
|
||||
public int iAddressFamily;
|
||||
|
||||
/// <summary>
|
||||
/// 最大地址长度(以字节为单位)。
|
||||
/// </summary>
|
||||
public int iMaxSockAddr;
|
||||
|
||||
/// <summary>
|
||||
/// 最小地址长度(以字节为单位)。
|
||||
/// </summary>
|
||||
public int iMinSockAddr;
|
||||
|
||||
public int iSocketType;
|
||||
public int iProtocol;
|
||||
public int iProtocolMaxOffset;
|
||||
|
||||
/// <summary>
|
||||
/// 目前,这些值是 BIGENDIAN 和 LITTLEENDIAN) (清单常量,分别指示值为 0 和 1 的 big-endian 或 little-endian
|
||||
/// </summary>
|
||||
public int iNetworkByteOrder;
|
||||
|
||||
public int iSecurityScheme;
|
||||
|
||||
/// <summary>
|
||||
/// 协议支持的最大消息大小(以字节为单位)
|
||||
/// </summary>
|
||||
public uint dwMessageSize;
|
||||
|
||||
public uint dwProviderReserved;
|
||||
|
||||
/// <summary>
|
||||
/// 一个字符数组,其中包含标识协议的可读名称,例如“MSAFD Tcpip [UDP/IP]”。 允许的最大字符数为 WSAPROTOCOL_LEN,定义为 255。
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string szProtocol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WSAPROTOCOLCHAIN 结构包含构成协议链的目录条目标识符的计数列表。
|
||||
/// <see cref="https://learn.microsoft.com/zh-cn/windows/win32/api/winsock2/ns-winsock2-wsaprotocolchain"/>
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WSAPROTOCOLCHAIN
|
||||
{
|
||||
/// <summary>
|
||||
/// 链的长度(以字节为单位)
|
||||
/// 将 ChainLen 设置为零表示分层协议
|
||||
/// 将 ChainLen 设置为 1 指示基本协议
|
||||
/// 将 ChainLen 设置为大于 1 表示协议链
|
||||
/// ChainEntries[MAX_PROTOCOL_CHAIN]
|
||||
/// </summary>
|
||||
public int ChainLen;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
|
||||
public uint[] ChainEntries;
|
||||
}
|
||||
|
||||
#endregion P/Invoke
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
using STUN.Client;
|
||||
using STUN.StunResult;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace BD.WTTS.Services.Implementation;
|
||||
|
||||
internal partial class NetworkTestService : INetworkTestService
|
||||
{
|
||||
#region 常量
|
||||
|
||||
// 默认测试地址
|
||||
const string DEFAULT_TESTSERVER_HOSTNAME = "stun.syncthing.net";
|
||||
|
||||
const int DEFAULT_PORT = 3478;
|
||||
const int DEFAULT_TLSPORT = 5349;
|
||||
|
||||
// 默认端口号
|
||||
const int DEFAULT_TESTSTUN3489_PORT = DEFAULT_PORT;
|
||||
|
||||
// 默认端口号
|
||||
const int DEFAULT_TESTSTUN5389_PORT = DEFAULT_PORT;
|
||||
|
||||
#endregion 常量
|
||||
|
||||
public NetworkTestService()
|
||||
{
|
||||
}
|
||||
|
||||
#region Ping
|
||||
|
||||
/// <summary>
|
||||
/// 将 Internet 控制消息协议 (ICMP) 回显请求消息发送到远程计算机,并等待来自该计算机的 ICMP 回送答复消息。 有关 ICMP 消息的详细说明,请参阅 RFC 792,可从 https://www.ietf.org获取。
|
||||
/// </summary>
|
||||
/// <param name="testHostNameOrAddress"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<PingReply> TestPingAsync(string testHostNameOrAddress, TimeSpan timeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using Ping p = new Ping();
|
||||
|
||||
var reply = await p.SendPingAsync(
|
||||
testHostNameOrAddress,
|
||||
timeout,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
#endregion Ping
|
||||
|
||||
#region Open/Upload/Download Speed Test
|
||||
|
||||
public async Task<(bool success, long? delayMs)> TestOpenUrlAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var testClient = httpClientFunc?.Invoke() ?? new HttpClient();
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await testClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
return (true, watch.ElapsedMilliseconds);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "测试打开 URL 异常");
|
||||
return (false, default);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<(bool success, double? rate)> TestUploadSpeedAsync(
|
||||
string uploadServerUrl,
|
||||
byte[] uploadBytes,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using HttpClient testClient = new HttpClient();
|
||||
|
||||
return await TestUploadSpeedCoreAsync(
|
||||
() => testClient,
|
||||
BuildUploadContent,
|
||||
uploadServerUrl,
|
||||
uploadBytes,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
static MultipartFormDataContent BuildUploadContent(byte[] data)
|
||||
{
|
||||
MultipartFormDataContent content = new MultipartFormDataContent();
|
||||
|
||||
content.Add(new ByteArrayContent(data));
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool success, double? rate)> TestDownloadSpeedAsync(string downloadUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using HttpClient testClient = new HttpClient();
|
||||
|
||||
return await TestDownloadSpeedCoreAsync(downloadUrl, () => testClient, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试上传速度
|
||||
/// </summary>
|
||||
/// <param name="httpClientFunc">自定义用于发送 http 请求的 client </param>
|
||||
/// <param name="uploadContentFunc">自定义上传内容类型</param>
|
||||
/// <param name="uploadServerUrl"></param>
|
||||
/// <param name="uploadBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
static async Task<(bool success, double? rate)> TestUploadSpeedCoreAsync(
|
||||
Func<HttpClient> httpClientFunc,
|
||||
Func<byte[], HttpContent> uploadContentFunc,
|
||||
string uploadServerUrl,
|
||||
byte[] uploadBytes,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uploadServerUrl))
|
||||
return (false, null);
|
||||
|
||||
var uploadContent = uploadContentFunc(uploadBytes);
|
||||
try
|
||||
{
|
||||
var httpClient = httpClientFunc();
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
var resp = await httpClient.PostAsync(uploadServerUrl, uploadContent, cancellationToken);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
watch.Stop();
|
||||
|
||||
return (true, GetRate(uploadBytes.Length, watch.Elapsed.TotalSeconds));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "测试上传异常");
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试下载速度
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="httpClientFunc"></param>
|
||||
/// <returns></returns>
|
||||
static async Task<(bool success, double? rate)> TestDownloadSpeedCoreAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpClient = httpClientFunc?.Invoke() ?? new HttpClient();
|
||||
|
||||
try
|
||||
{
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
var resp = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
var bytesArray = await resp.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
return (true, GetRate(bytesArray.Length, watch.Elapsed.TotalSeconds));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "测试下载异常");
|
||||
return (false, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (httpClientFunc == null)
|
||||
httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
static double GetRate(long byteArrayLength, double seconds)
|
||||
{
|
||||
if (seconds == 0)
|
||||
throw new ArgumentException("时间不能小于0", nameof(seconds));
|
||||
|
||||
long bits = byteArrayLength * 8;
|
||||
double megabits = bits / 1_000_000.0; // 将比特转换为兆比特
|
||||
|
||||
return megabits / seconds; // 返回速率,单位是 Mbps
|
||||
}
|
||||
|
||||
#endregion Upload/Download Speed Test
|
||||
|
||||
#region STUN 测试
|
||||
|
||||
#region RFC3489
|
||||
|
||||
StunClient3489? _stunClient;
|
||||
|
||||
/// <summary>
|
||||
/// 测试 STUN 客户端 基于 RFC3489
|
||||
/// </summary>
|
||||
/// <param name="testServerHostName">测试服务地址</param>
|
||||
/// <param name="testServerPort">测试服务端口</param>
|
||||
/// <param name="localIPEndPoint">本机 IP </param>
|
||||
/// <param name="force">强制刷新</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<ClassicStunResult?> TestStunClient3489Async(
|
||||
string? testServerHostName = DEFAULT_TESTSERVER_HOSTNAME,
|
||||
int? testServerPort = DEFAULT_TESTSTUN3489_PORT,
|
||||
IPEndPoint? localIPEndPoint = null,
|
||||
bool force = false,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
testServerHostName ??= DEFAULT_TESTSERVER_HOSTNAME;
|
||||
testServerPort ??= DEFAULT_TESTSTUN3489_PORT;
|
||||
|
||||
if (_stunClient == null || force)
|
||||
_stunClient = await GetStunClient3489Async(testServerHostName, testServerPort.Value, localIPEndPoint);
|
||||
|
||||
try
|
||||
{
|
||||
await _stunClient.QueryAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "TestStunClient3489Async 异常");
|
||||
return null;
|
||||
}
|
||||
|
||||
return _stunClient.State;
|
||||
}
|
||||
|
||||
private async ValueTask<StunClient3489> GetStunClient3489Async(string serverHostName, int port, IPEndPoint? localIPEndPoint = null)
|
||||
{
|
||||
IPEndPoint serverEndpoint = await GetServerIPEndPointAsync(serverHostName, port);
|
||||
|
||||
IPEndPoint localEndpoint = localIPEndPoint ?? new IPEndPoint(IPAddress.Any, default);
|
||||
|
||||
var client = new StunClient3489(
|
||||
serverEndpoint,
|
||||
localEndpoint
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private async Task<IPEndPoint> GetServerIPEndPointAsync(string hostNameOrAddress, int port)
|
||||
{
|
||||
var serverIps = await Dns.GetHostAddressesAsync(hostNameOrAddress);
|
||||
|
||||
IPEndPoint serverEndpoint = new IPEndPoint(serverIps.First(), port);
|
||||
|
||||
return serverEndpoint;
|
||||
}
|
||||
|
||||
#endregion RFC3489
|
||||
|
||||
#region RFC5389
|
||||
|
||||
/// <summary>
|
||||
/// 测试 STUN 客户端 基于 RFC5389
|
||||
/// </summary>
|
||||
/// <param name="protocol"></param>
|
||||
/// <param name="testServerHostName"></param>
|
||||
/// <param name="testServerPort"></param>
|
||||
/// <param name="localIPEndPoint"></param>
|
||||
/// <param name="force"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<StunResult5389?> TestStunClient5389Async(
|
||||
TransportProtocol protocol,
|
||||
string? testServerHostName = DEFAULT_TESTSERVER_HOSTNAME,
|
||||
int? testServerPort = DEFAULT_TESTSTUN5389_PORT,
|
||||
IPEndPoint? localIPEndPoint = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
testServerHostName ??= DEFAULT_TESTSERVER_HOSTNAME;
|
||||
testServerPort ??= DEFAULT_TESTSTUN5389_PORT;
|
||||
|
||||
IStunClient5389 client = await GetStunClient5389Async(protocol, testServerHostName, testServerPort.Value, localIPEndPoint);
|
||||
|
||||
if (client == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
await client.QueryAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), ex, "TestStunClient5389Async 异常");
|
||||
return null;
|
||||
}
|
||||
|
||||
return client.State;
|
||||
}
|
||||
|
||||
private async ValueTask<IStunClient5389> GetStunClient5389Async(TransportProtocol protocol, string serverHostName, int port, IPEndPoint? localIPEndPoint = null)
|
||||
{
|
||||
IPEndPoint serverEndpoint = await GetServerIPEndPointAsync(serverHostName, port);
|
||||
|
||||
IPEndPoint localEndpoint = localIPEndPoint ?? new IPEndPoint(IPAddress.Any, default);
|
||||
|
||||
IStunClient5389 client = protocol == TransportProtocol.Tcp
|
||||
? new StunClient5389TCP(serverEndpoint, localEndpoint)
|
||||
: new StunClient5389UDP(serverEndpoint, localEndpoint);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
#endregion RFC5389
|
||||
|
||||
#endregion STUN 测试
|
||||
|
||||
#region DNS 查询
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS UDP
|
||||
/// </summary>
|
||||
/// <param name="testDomain">要测试的域名</param>
|
||||
/// <param name="dnsServerIp">DNS 服务地址</param>
|
||||
/// <param name="dnsServerPort">DNS 服务端口</param>
|
||||
/// <param name="dnsRecordType"> dns 测试类型 </param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(long DelayMs, IPAddress[] Address)> TestDNSAsync(
|
||||
string testDomain,
|
||||
string dnsServerIp,
|
||||
int dnsServerPort,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using UdpClient udpClient = new UdpClient(dnsServerIp, dnsServerPort);
|
||||
|
||||
var query = new DnsQueryUdpRequest(testDomain, dnsRecordType);
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
await udpClient.SendAsync(query, cancellationToken);
|
||||
|
||||
var receivedData = await udpClient.ReceiveAsync(cancellationToken);
|
||||
|
||||
watch.Stop();
|
||||
|
||||
var dnsQueryResponse = new DnsQueryUdpResponse(receivedData.Buffer);
|
||||
|
||||
return (watch.ElapsedMilliseconds, dnsQueryResponse.GetAddresses().ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS Over Https
|
||||
/// </summary>
|
||||
/// <param name="testDomain"></param>
|
||||
/// <param name="dohServer"></param>
|
||||
/// <param name="dnsRecordType"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(long DelayMs, IPAddress[] Address)> TestDNSOverHttpsAsync(
|
||||
string testDomain,
|
||||
string dohServer,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
|
||||
string queryUrl = $"{dohServer}?name={testDomain}&type={dnsRecordType}";
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
HttpResponseMessage? resp = null;
|
||||
try
|
||||
{
|
||||
resp = await client.GetAsync(queryUrl, HttpCompletionOption.ResponseContentRead, cancellationToken);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
finally
|
||||
{
|
||||
watch.Stop();
|
||||
}
|
||||
|
||||
var queryResp = await resp.Content.ReadFromJsonAsync<DnsQueryJsonResponse>();
|
||||
|
||||
return (watch.ElapsedMilliseconds, queryResp?.GetAddresses().ToArray() ?? Array.Empty<IPAddress>());
|
||||
}
|
||||
|
||||
#region DNS 解析定义
|
||||
|
||||
internal interface IDnsQueryResponse
|
||||
{
|
||||
List<DnsQueryAnswerRecord> Answers { get; }
|
||||
|
||||
IEnumerable<IPAddress> GetAddresses();
|
||||
}
|
||||
|
||||
internal record DnsQueryUdpResponse : DnsQueryUdpRequest, IDnsQueryResponse
|
||||
{
|
||||
public ushort AnswerCount { get; init; } // (ushort)((_origin[6] << 8) | _origin[7]);
|
||||
|
||||
public ushort AuthorityCount { get; init; } // (ushort)((_origin[8] << 8) | _origin[9]);
|
||||
|
||||
public ushort AdditionalCount { get; init; } // (ushort)((_origin[10] << 8) | _origin[11]);
|
||||
|
||||
public List<DnsQueryAnswerRecord> Answers { get; init; }
|
||||
|
||||
private readonly byte[] _origin;
|
||||
|
||||
public DnsQueryUdpResponse(byte[] origin) : base(origin)
|
||||
{
|
||||
_origin = origin;
|
||||
|
||||
AnswerCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
AuthorityCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
AdditionalCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
|
||||
// 跳过查询问题部分
|
||||
for (int i = 0; i < QuestionCount; i++)
|
||||
{
|
||||
Position = SkipName(origin, Position);
|
||||
Position += 4; // 跳过类型
|
||||
}
|
||||
|
||||
Answers = new List<DnsQueryAnswerRecord>();
|
||||
|
||||
Position = ParseRecords(origin, Position, AnswerCount, Answers);
|
||||
Position = ParseRecords(origin, Position, AuthorityCount, Answers);
|
||||
Position = ParseRecords(origin, Position, AdditionalCount, Answers);
|
||||
}
|
||||
|
||||
private static int ParseRecords(byte[] response, int position, int recordCount, List<DnsQueryAnswerRecord> answers)
|
||||
{
|
||||
for (int i = 0; i < recordCount; i++)
|
||||
{
|
||||
position = ParseName(response, position, out var parsedName);
|
||||
|
||||
int @type = (response[position++] << 8) | response[position++];
|
||||
int @class = (response[position++] << 8) | response[position++];
|
||||
long ttl = (response[position++] << 24) | (response[position++] << 16) | (response[position++] << 8) | response[position++];
|
||||
int dataLength = (response[position++] << 8) | response[position++];
|
||||
|
||||
if (@type == (int)DnsQueryAnswerRecord.DnsRecordType.A && dataLength == 4) // Type A (IPv4)
|
||||
{
|
||||
var ipAddress = new byte[4];
|
||||
Array.Copy(response, position, ipAddress, 0, 4);
|
||||
|
||||
answers.Add(
|
||||
new DnsQueryAnswerRecord.A(
|
||||
parsedName!,
|
||||
(DnsQueryAnswerRecord.DnsRecordType)@type,
|
||||
ttl,
|
||||
new IPAddress(ipAddress)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (@type == (int)DnsQueryAnswerRecord.DnsRecordType.AAAA && dataLength == 16) // Type AAAA (IPv6)
|
||||
{
|
||||
var ipAddress = new byte[16];
|
||||
Array.Copy(response, position, ipAddress, 0, 16);
|
||||
|
||||
answers.Add(
|
||||
new DnsQueryAnswerRecord.AAAA(
|
||||
parsedName!,
|
||||
(DnsQueryAnswerRecord.DnsRecordType)@type,
|
||||
ttl,
|
||||
new IPAddress(ipAddress)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.CNAME)
|
||||
{
|
||||
ParseName(response, position, out var parsedCName);
|
||||
|
||||
if (string.IsNullOrEmpty(parsedCName))
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), $"解析 DNS CName字段异常 name :{parsedName}, data length :{dataLength}");
|
||||
continue;
|
||||
}
|
||||
|
||||
answers.Add(
|
||||
new DnsQueryAnswerRecord.CName(
|
||||
parsedName!,
|
||||
(DnsQueryAnswerRecord.DnsRecordType)@type,
|
||||
ttl,
|
||||
parsedCName!
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error(nameof(NetworkTestService), $"未支持的 DNS响应记录 type :{@type} ,class :{@class} name :{parsedName}, data length :{dataLength}");
|
||||
continue;
|
||||
}
|
||||
|
||||
position += dataLength;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
private static int ParseName(byte[] response, int position, out string? name)
|
||||
{
|
||||
StringBuilder nameBuilder = new StringBuilder();
|
||||
int originalPosition = position;
|
||||
bool isPointer = false;
|
||||
|
||||
// 0表示结束
|
||||
while (response[position] != 0)
|
||||
{
|
||||
// dns 协议 名称可能是 指针 或者 直接表示
|
||||
// 如果大于等于 192 表示是指针开始
|
||||
if (response[position] >= 192)
|
||||
{
|
||||
if (!isPointer)
|
||||
{
|
||||
originalPosition = position + 2;
|
||||
isPointer = true;
|
||||
}
|
||||
// 因为 00111111 可以忽略前两位 用 0x3F 掩码获取指针
|
||||
int pointer = ((response[position] & 0x3F) << 8) | response[position + 1];
|
||||
position = pointer;
|
||||
}
|
||||
else
|
||||
{
|
||||
int length = response[position];
|
||||
position++;
|
||||
nameBuilder.Append(Encoding.ASCII.GetString(response, position, length));
|
||||
position += length;
|
||||
if (response[position] != 0)
|
||||
{
|
||||
nameBuilder.Append('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPointer)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
else
|
||||
{
|
||||
position = originalPosition;
|
||||
}
|
||||
|
||||
name = nameBuilder.ToString();
|
||||
return position;
|
||||
}
|
||||
|
||||
private static int SkipName(byte[] response, int position)
|
||||
{
|
||||
if (response[position] >= 192)
|
||||
{
|
||||
return position + 2; // Name is a pointer
|
||||
}
|
||||
|
||||
while (response[position] != 0)
|
||||
{
|
||||
position += response[position] + 1;
|
||||
}
|
||||
return position + 1;
|
||||
}
|
||||
|
||||
public IEnumerable<IPAddress> GetAddresses()
|
||||
{
|
||||
foreach (var answer in Answers)
|
||||
{
|
||||
if (answer is DnsQueryAnswerRecord.A a)
|
||||
{
|
||||
yield return a.Data;
|
||||
}
|
||||
else if (answer is DnsQueryAnswerRecord.AAAA aaaa)
|
||||
{
|
||||
yield return aaaa.Data;
|
||||
}
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
internal record DnsQueryUdpRequest
|
||||
{
|
||||
public ushort QueryId { get; init; } // (ushort)((_origin[0] << 8) | _origin[1]);
|
||||
|
||||
public ushort Flags { get; init; } //(ushort)((_origin[2] << 8) | _origin[3]);
|
||||
|
||||
public ushort QuestionCount { get; init; } // (ushort)((_origin[4] << 8) | _origin[5]);
|
||||
|
||||
public int Length => _origin.Length;
|
||||
|
||||
protected int Position { get; set; }
|
||||
|
||||
private readonly byte[] _origin;
|
||||
|
||||
public DnsQueryUdpRequest(byte[] origin)
|
||||
{
|
||||
_origin = origin;
|
||||
|
||||
Position = default;
|
||||
QueryId = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
Flags = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
QuestionCount = (ushort)((origin[Position++] << 8) | origin[Position++]);
|
||||
}
|
||||
|
||||
public DnsQueryUdpRequest(string domainName, DnsQueryAnswerRecord.DnsRecordType type = DnsQueryAnswerRecord.DnsRecordType.A)
|
||||
: this(CreateDnsQuery(domainName, type))
|
||||
{
|
||||
}
|
||||
|
||||
public static byte[] CreateDnsQuery(string domainName, DnsQueryAnswerRecord.DnsRecordType type = DnsQueryAnswerRecord.DnsRecordType.A)
|
||||
{
|
||||
// 12字节报文头部
|
||||
const int DNS_QUERY_HEADER_BYTECOUNT = 12;
|
||||
// 2字节终止符长度
|
||||
const int DNS_QUERY_END_BYTECOUNT = 2;
|
||||
// 4字节查询类型和查询类字段
|
||||
const int DNS_QUERY_QUERYTYPE_BYTECOUNT = 4;
|
||||
|
||||
// dns queryId 范围是一个16位字段,16位无符号的范围就是0-65535
|
||||
var queryId = (ushort)Random.Shared.Next(0, 65536);
|
||||
|
||||
byte[] dnsQuery = new byte[
|
||||
DNS_QUERY_HEADER_BYTECOUNT
|
||||
+ domainName.Length
|
||||
+ DNS_QUERY_END_BYTECOUNT
|
||||
+ DNS_QUERY_QUERYTYPE_BYTECOUNT
|
||||
];
|
||||
|
||||
BuildQueryHeader(queryId, dnsQuery);
|
||||
BuildBody(domainName, dnsQuery);
|
||||
|
||||
return dnsQuery;
|
||||
|
||||
void BuildQueryHeader(ushort queryId, byte[] query)
|
||||
{
|
||||
// 查询 ID 是一个 16 位(2 字节)的字段。
|
||||
// 为了将这个 16 位的整数值分配到字节数组 dnsQuery 的两个字节中,
|
||||
// 我们需要将其高 8 位和低 8 位分别存储在数组的第一个和第二个字节中
|
||||
query[0] = (byte)(queryId >> 8);
|
||||
query[1] = (byte)(queryId & 0xFF);
|
||||
|
||||
// 标志字段(Flags) 2 - 3 包含各种控制标志
|
||||
// 如查询/响应(QR)、操作码(Opcode)、授权回答(AA)、截断(TC)、递归期望(RD)、递归可用(RA)等
|
||||
query[2] = 0x01;
|
||||
|
||||
// 问题计数(QDCOUNT)4 - 5 表示查询问题部分的数量
|
||||
query[5] = 0x01;
|
||||
|
||||
// 其他查询报文中通常为 0
|
||||
// 回答记录计数(ANCOUNT),授权记录计数(NSCOUNT),附加记录计数(ARCOUNT)
|
||||
}
|
||||
|
||||
void BuildBody(string domainName, byte[] query)
|
||||
{
|
||||
// 获取域名组成部分 因为后面要根据部分长度和内容填充结果
|
||||
string[] domainParts = domainName.Split('.');
|
||||
|
||||
int currentPosition = 12;
|
||||
|
||||
// 根据 dns 查询报文 每一部分开始都要填充这一部分的长度
|
||||
foreach (var domainPart in domainParts)
|
||||
{
|
||||
// 标志当前 part 的长度
|
||||
query[currentPosition++] = (byte)domainPart.Length;
|
||||
|
||||
// 获取填充的字节
|
||||
var fillBytes = Encoding.ASCII.GetBytes(domainPart);
|
||||
|
||||
// 移动到当前查询体中
|
||||
fillBytes.CopyTo(query, currentPosition);
|
||||
|
||||
// 移动当前填充开始位置
|
||||
currentPosition += fillBytes.Length;
|
||||
}
|
||||
// 0 表示域名结束
|
||||
query[currentPosition++] = 0x00;
|
||||
|
||||
// 填充 查询类型和查询类字段
|
||||
query[currentPosition++] = 0x00;
|
||||
query[currentPosition++] = (byte)type; // 表示查询类型为 A 记录(IPv4 地址)。
|
||||
query[currentPosition++] = 0x00;
|
||||
query[currentPosition++] = 0x01; // 表示查询类为 IN(Internet 类)。
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator byte[](DnsQueryUdpRequest data)
|
||||
{
|
||||
return data._origin;
|
||||
}
|
||||
|
||||
public static implicit operator ReadOnlyMemory<byte>(DnsQueryUdpRequest data)
|
||||
{
|
||||
return data._origin;
|
||||
}
|
||||
}
|
||||
|
||||
internal record DnsQueryJsonResponse : IDnsQueryResponse
|
||||
{
|
||||
[S_JsonProperty("Status")]
|
||||
public int Status { get; init; }
|
||||
|
||||
[S_JsonProperty("TC")]
|
||||
public bool TC { get; init; }
|
||||
|
||||
[S_JsonProperty("RD")]
|
||||
public bool RD { get; init; }
|
||||
|
||||
[S_JsonProperty("RA")]
|
||||
public bool RA { get; init; }
|
||||
|
||||
[S_JsonProperty("AD")]
|
||||
public bool AD { get; init; }
|
||||
|
||||
[S_JsonProperty("CD")]
|
||||
public bool CD { get; init; }
|
||||
|
||||
[S_JsonProperty("Answer")]
|
||||
public List<DnsQueryAnswerRecord> Answers { get; init; }
|
||||
|
||||
public DnsQueryJsonResponse(
|
||||
int status,
|
||||
bool tc,
|
||||
bool rd,
|
||||
bool ra,
|
||||
bool ad,
|
||||
bool cd,
|
||||
List<DnsQueryAnswerRecord>? answers
|
||||
)
|
||||
{
|
||||
Status = status;
|
||||
TC = tc;
|
||||
RD = rd;
|
||||
RA = ra;
|
||||
AD = ad;
|
||||
CD = cd;
|
||||
Answers = answers?.ToList() ?? new();
|
||||
}
|
||||
|
||||
public IEnumerable<IPAddress> GetAddresses()
|
||||
{
|
||||
return Answers.Aggregate(Enumerable.Empty<IPAddress>(), (acc, x) =>
|
||||
{
|
||||
if (x is DnsQueryAnswerRecord.A ipv4)
|
||||
{
|
||||
return acc.Append(ipv4.Data);
|
||||
}
|
||||
else if (x is DnsQueryAnswerRecord.AAAA ipv6)
|
||||
{
|
||||
return acc.Append(ipv6.Data);
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DnsQueryAnswerRecordConverter : System.Text.Json.Serialization.JsonConverter<DnsQueryAnswerRecord>
|
||||
{
|
||||
public override DnsQueryAnswerRecord? Read(ref Utf8JsonReader reader, Type typeToConvert, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
if (typeToConvert == typeof(DnsQueryAnswerRecord))
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
var json = JsonDocument.ParseValue(ref reader);
|
||||
|
||||
if (json.RootElement.TryGetProperty("type", out var typeElement))
|
||||
{
|
||||
var type = typeElement.GetInt32();
|
||||
|
||||
if (type == (int)DnsQueryAnswerRecord.DnsRecordType.A)
|
||||
{
|
||||
return json.RootElement.Deserialize<DnsQueryAnswerRecord.A>();
|
||||
}
|
||||
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.AAAA)
|
||||
{
|
||||
return json.RootElement.Deserialize<DnsQueryAnswerRecord.AAAA>();
|
||||
}
|
||||
else if (type == (int)DnsQueryAnswerRecord.DnsRecordType.CNAME)
|
||||
{
|
||||
return json.RootElement.Deserialize<DnsQueryAnswerRecord.CName>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DnsQueryAnswerRecord value, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[System.Text.Json.Serialization.JsonConverter(typeof(DnsQueryAnswerRecordConverter))]
|
||||
[JsonDerivedType(typeof(DnsQueryAnswerRecord.A))]
|
||||
[JsonDerivedType(typeof(DnsQueryAnswerRecord.AAAA))]
|
||||
[JsonDerivedType(typeof(DnsQueryAnswerRecord.CName))]
|
||||
internal record DnsQueryAnswerRecord
|
||||
{
|
||||
[S_JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[S_JsonProperty("type")]
|
||||
public DnsRecordType Type { get; set; }
|
||||
|
||||
[S_JsonProperty("TTL")]
|
||||
public long Ttl { get; set; }
|
||||
|
||||
public DnsQueryAnswerRecord(
|
||||
string name,
|
||||
DnsRecordType type,
|
||||
long ttl
|
||||
)
|
||||
{
|
||||
Name = name;
|
||||
Type = @type;
|
||||
Ttl = ttl;
|
||||
//Data = data;
|
||||
//DataLength = data.Length;
|
||||
}
|
||||
|
||||
#region DnsQueryAnswerRecord 子类派生
|
||||
|
||||
internal record A : DnsQueryAnswerRecord
|
||||
{
|
||||
public A(string name, DnsRecordType type, long ttl, IPAddress data) : base(name, type, ttl)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[S_JsonProperty("data")]
|
||||
[SystemTextJsonConverter(typeof(IPAddressConverter))]
|
||||
public IPAddress Data { get; set; }
|
||||
}
|
||||
|
||||
internal record AAAA : DnsQueryAnswerRecord
|
||||
{
|
||||
public AAAA(string name, DnsRecordType type, long ttl, IPAddress data) : base(name, type, ttl)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[S_JsonProperty("data")]
|
||||
[SystemTextJsonConverter(typeof(IPAddressConverter))]
|
||||
public IPAddress Data { get; set; }
|
||||
}
|
||||
|
||||
internal record CName : DnsQueryAnswerRecord
|
||||
{
|
||||
public CName(string name, DnsRecordType type, long ttl, string data) : base(name, type, ttl)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
[S_JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
#endregion DnsQueryAnswerRecord 子类派生
|
||||
|
||||
class IPAddressConverter : System.Text.Json.Serialization.JsonConverter<IPAddress>
|
||||
{
|
||||
public override IPAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
IPAddress? result = null;
|
||||
|
||||
// 目前只解析字符串
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
string? ipString = reader.GetString();
|
||||
result = !string.IsNullOrWhiteSpace(ipString) ? IPAddress.Parse(ipString) : default;
|
||||
}
|
||||
// byte 数组支持
|
||||
else if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
List<byte> bytes = new List<byte>();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
bytes.Add((byte)reader.GetInt32());
|
||||
}
|
||||
else if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
if (byte.TryParse(reader.GetString(), out byte byteValue))
|
||||
{
|
||||
bytes.Add(byteValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = bytes.Count == 4 || bytes.Count == 16 ? new IPAddress(bytes.ToArray()) : default;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IPAddress value, SystemTextJsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal enum DnsRecordClass : ushort
|
||||
{
|
||||
Internet = 1, // Internet(IN)
|
||||
Chaos = 3, // Chaos(CH)
|
||||
Hesiod = 4, // Hesiod(HS)
|
||||
QCLASSNONE = 254, // QCLASS NONE
|
||||
QCLASSANY = 255, // QCLASS * (ANY)
|
||||
}
|
||||
|
||||
// 域名系统记录类型 (省略了有些未使用的类型)
|
||||
internal enum DnsRecordType : byte
|
||||
{
|
||||
A = 1, // 指定域名对应的IPv4地址
|
||||
AAAA = 28, // 指定域名对应的IPv6地址
|
||||
CNAME = 5, // 别名记录,
|
||||
MX = 15, // 邮件交换记录,
|
||||
NS = 2, // 指定该域名由哪个DNS服务器来进行解析
|
||||
TXT = 16, // 主机名或域名的说明
|
||||
SOA = 6, // 起始授权机构
|
||||
PTR = 12, // 反向IP查询
|
||||
ANY = 255, // 所有DNS记录类型
|
||||
}
|
||||
}
|
||||
|
||||
#endregion DNS 解析定义
|
||||
|
||||
#endregion DNS 查询
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using STUN.StunResult;
|
||||
using System.Net.NetworkInformation;
|
||||
using static BD.WTTS.Services.Implementation.NetworkTestService;
|
||||
|
||||
namespace BD.WTTS.Services;
|
||||
|
||||
internal interface INetworkTestService : IHttpRequestTestService, IStunTestService
|
||||
{
|
||||
/// <summary>
|
||||
/// Ping
|
||||
/// </summary>
|
||||
/// <param name="testHostNameOrAddress"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<PingReply> TestPingAsync(string testHostNameOrAddress, TimeSpan timeout, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS UDP
|
||||
/// </summary>
|
||||
/// <param name="testDomain"></param>
|
||||
/// <param name="dnsServerIp"></param>
|
||||
/// <param name="dnsServerPort"></param>
|
||||
/// <param name="dnsRecordType"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(long DelayMs, IPAddress[] Address)> TestDNSAsync(
|
||||
string testDomain,
|
||||
string dnsServerIp,
|
||||
int dnsServerPort,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 测试 DNS Over Https
|
||||
/// </summary>
|
||||
/// <param name="testDomain"></param>
|
||||
/// <param name="dohServer"></param>
|
||||
/// <param name="dnsRecordType"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(long DelayMs, IPAddress[] Address)> TestDNSOverHttpsAsync(
|
||||
string testDomain,
|
||||
string dohServer,
|
||||
DnsQueryAnswerRecord.DnsRecordType dnsRecordType = DnsQueryAnswerRecord.DnsRecordType.A,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
||||
internal interface IHttpRequestTestService
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试打开网址
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="httpClientFunc"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(bool success, long? delayMs)> TestOpenUrlAsync(string url, Func<HttpClient>? httpClientFunc = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 测试上传速度
|
||||
/// </summary>
|
||||
/// <param name="uploadServerUrl"></param>
|
||||
/// <param name="uploadBytes"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(bool success, double? rate)> TestUploadSpeedAsync(
|
||||
string uploadServerUrl,
|
||||
byte[] uploadBytes,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 测试下载速度
|
||||
/// </summary>
|
||||
/// <param name="downloadUrl"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<(bool success, double? rate)> TestDownloadSpeedAsync(string downloadUrl, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal interface IStunTestService
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 STUN Client RFC3489
|
||||
/// </summary>
|
||||
/// <param name="testServerHostName">STUN 服务地址</param>
|
||||
/// <param name="testServerPort">STUN 服务端口</param>
|
||||
/// <param name="localIPEndPoint">本机IP</param>
|
||||
/// <param name="force">刷新测试 Client</param>
|
||||
/// <remarks></remarks>
|
||||
/// <returns></returns>
|
||||
Task<ClassicStunResult?> TestStunClient3489Async(
|
||||
string? testServerHostName = default,
|
||||
int? testServerPort = default,
|
||||
IPEndPoint? localIPEndPoint = default,
|
||||
bool force = false,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 测试 STUN Client RFC5389
|
||||
/// </summary>
|
||||
/// <param name="protocol">使用的传输协议 Tcp,Udp</param>
|
||||
/// <param name="testServerHostName">STUN 服务地址</param>
|
||||
/// <param name="testServerPort">STUN 服务端口</param>
|
||||
/// <param name="localIPEndPoint">本机IP</param>
|
||||
/// <param name="force"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<StunResult5389?> TestStunClient5389Async(
|
||||
TransportProtocol protocol,
|
||||
string? testServerHostName = default,
|
||||
int? testServerPort = default,
|
||||
IPEndPoint? localIPEndPoint = null,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
@@ -206,7 +206,7 @@ partial class ProxyService
|
||||
}
|
||||
}
|
||||
|
||||
string? proxyToken = proxyDomains.Any_Nullable(s => s.ProxyType == ProxyType.ServerAccelerate) ?
|
||||
string? proxyToken = proxyDomains.Any_Nullable(s => s.ProxyType == ProxyType.ServerAccelerate || s.Items.Any_Nullable(x => x.ProxyType == ProxyType.ServerAccelerate)) ?
|
||||
await TryRequestServerSideProxyToken() : null;
|
||||
|
||||
ReverseProxySettings reverseProxySettings = new(proxyDomains, scripts,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
using Avalonia.Data;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace BD.WTTS.Services;
|
||||
|
||||
@@ -65,12 +67,12 @@ public sealed partial class ProxyService
|
||||
.ToObservableChangeSet()
|
||||
.AutoRefresh(x => x.ObservableItems)
|
||||
.TransformMany(t => t.ObservableItems ?? new ObservableCollection<AccelerateProjectDTO>())
|
||||
.AutoRefresh(x => x.Checked)
|
||||
.WhenPropertyChanged(x => x.Checked, false)
|
||||
.AutoRefresh(x => x.ThreeStateEnable)
|
||||
.WhenPropertyChanged(x => x.ThreeStateEnable, false)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
IsChangeSupportProxyServicesStatus = true;
|
||||
ProxySettings.SupportProxyServicesStatus.Value = EnableProxyDomains?.Select(k => k.Id.ToString()).ToImmutableHashSet();
|
||||
ProxySettings.SupportProxyServicesStatus.Value = GetAccelerateEnableAllIds(EnableProxyDomains).ToImmutableHashSet();
|
||||
}));
|
||||
|
||||
this.WhenAnyValue(v => v.ProxyScripts)
|
||||
@@ -152,10 +154,10 @@ public sealed partial class ProxyService
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<AccelerateProjectDTO>? GetEnableProxyDomains()
|
||||
public IEnumerable<AccelerateProjectDTO> GetEnableProxyDomains()
|
||||
{
|
||||
if (!ProxyDomains.Items.Any_Nullable())
|
||||
return null;
|
||||
return [];
|
||||
var data = ProxyDomains.Items
|
||||
.Where(x => x.Items != null)
|
||||
.SelectMany(s => s.Items!.Where(w => w.ThreeStateEnable != false));
|
||||
@@ -163,7 +165,7 @@ public sealed partial class ProxyService
|
||||
return data;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AccelerateProjectDTO>? EnableProxyDomains => GetEnableProxyDomains()?.ToImmutableArray();
|
||||
public IReadOnlyCollection<AccelerateProjectDTO>? EnableProxyDomains => GetEnableProxyDomains().ToImmutableArray();
|
||||
|
||||
//static IEnumerable<AccelerateProjectDTO>? GetProxyDomainsItems(AccelerateProjectDTO accelerates)
|
||||
//{
|
||||
@@ -403,22 +405,8 @@ public sealed partial class ProxyService
|
||||
{
|
||||
var items = ProxyDomains.Items!.SelectMany(s => s.Items!);
|
||||
var enableItems = ProxySettings.SupportProxyServicesStatus.Value;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (enableItems.Contains(item.Id.ToString()))
|
||||
{
|
||||
item.ThreeStateEnable = true;
|
||||
}
|
||||
}
|
||||
RestoreAccelerateEnableAllIds(items, enableItems);
|
||||
}
|
||||
|
||||
//if (IsLoadImage && ProxyDomains.Items.Any_Nullable())
|
||||
//{
|
||||
// foreach (var item in ProxyDomains.Items)
|
||||
// {
|
||||
// item.ImageStream = ImageChannelType.AccelerateGroup.GetImageAsync(ImageUrlHelper.GetImageApiUrlById(item.ImageId));
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
public static bool IsChangeSupportProxyServicesStatus { get; set; }
|
||||
@@ -462,6 +450,29 @@ public sealed partial class ProxyService
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetAccelerateEnableAllIds(IEnumerable<AccelerateProjectDTO>? nodes)
|
||||
{
|
||||
if (nodes == null)
|
||||
return Enumerable.Empty<string>();
|
||||
return nodes.Where(s => s.ThreeStateEnable == true).SelectMany(node => new[] { node.Id.ToString() }.Concat(GetAccelerateEnableAllIds(node.Items)));
|
||||
}
|
||||
|
||||
private void RestoreAccelerateEnableAllIds(IEnumerable<AccelerateProjectDTO> nodes, IReadOnlyCollection<string> enableItems)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Items.Any_Nullable())
|
||||
{
|
||||
RestoreAccelerateEnableAllIds(node.Items, enableItems);
|
||||
continue;
|
||||
}
|
||||
if (enableItems.Contains(node.Id.ToString()))
|
||||
{
|
||||
node.ThreeStateEnable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer? timer;
|
||||
|
||||
public void StartTimer()
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ public sealed class ProxySettingsWindowViewModel : WindowViewModel
|
||||
Title = DisplayName;
|
||||
}
|
||||
|
||||
public IEnumerable<ExternalProxyType> ProxyTypes { get; }
|
||||
= Enum2.GetAll<ExternalProxyType>();
|
||||
//public IEnumerable<ExternalProxyType> ProxyTypes { get; }
|
||||
// = Enum2.GetAll<ExternalProxyType>();
|
||||
|
||||
public IEnumerable<string> ProxyDNSs { get; } = new[]
|
||||
{
|
||||
|
||||
@@ -531,6 +531,23 @@
|
||||
<StackPanel Margin="10">
|
||||
<DockPanel>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<Border
|
||||
Padding="5,0"
|
||||
HorizontalAlignment="Left"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding ProxyType, Converter={StaticResource EnumEqualValueConverter}, ConverterParameter={x:Static enum:ProxyType.ServerAccelerate}}"
|
||||
ToolTip.Tip="Beta 测试加速项,不稳定且随时可能调整">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0.0" Color="#43cbff" />
|
||||
<GradientStop Offset="1.0" Color="#9708cc" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Text="Beta" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
<Separator Classes="Separator" />
|
||||
</StackPanel>
|
||||
@@ -737,7 +754,7 @@
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<Button Theme="{StaticResource TransparentButton}">
|
||||
<Button Theme="{StaticResource TransparentButton}" ToolTip.ShowDelay="0">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock>
|
||||
<Run FontSize="16" Text="Q:为什么加速后流量一直显示 0KB ?" />
|
||||
@@ -750,7 +767,7 @@
|
||||
<Run Text="A:可以尝试执行以下操作进行修复,关闭加速,点击加速设置-移除证书再安装证书,重开加速。或者加速设置-代理设置-尝试更换doh地址或者关闭doh使用dns等方案。" />
|
||||
</TextBlock>
|
||||
</ToolTip.Tip>
|
||||
<ui:FontIcon FontSize="22" Glyph="" />
|
||||
<ui:FontIcon FontSize="20" Glyph="" />
|
||||
</Button>
|
||||
<TextBlock Text="{x:Static spp:Strings.CommunityFix_ProxyMode}" />
|
||||
<TextBlock Text="{Binding Source={x:Static ms:ProxySettings.ProxyMode}, Path=Value, Mode=OneWay, Converter={StaticResource EnumLocalizationNameConverter}}" />
|
||||
|
||||
@@ -35,17 +35,22 @@
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.ProxyMasterDns}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
<!--<DockPanel>
|
||||
<Label Content="监听端口"></Label>
|
||||
<NumericUpDown Value="{Binding Source={x:Static ms:ProxySettings.HostProxyPortId},Path=Value, Mode=TwoWay}"
|
||||
FormatString="0"
|
||||
Maximum="65535"/>
|
||||
</DockPanel>-->
|
||||
|
||||
<ui:SettingsExpanderItem Description="{Binding Path=Res.Settings_Proxy_SystemProxyPortId_Tip, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_SystemProxyPortId, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<NumericUpDown
|
||||
MaxWidth="200"
|
||||
FormatString="0"
|
||||
Maximum="65535"
|
||||
Value="{Binding Source={x:Static ms:ProxySettings.SystemProxyPortId}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<!--<DockPanel>
|
||||
<TextBlock Text="IPV6支持:" />
|
||||
<TextBlock Text="IPV6地址" ></TextBlock>
|
||||
<Button Content="{Binding Path=Res.Refresh,Mode=OneWay,Source={x:Static resx:R.Current}}"></Button>
|
||||
<Button Content="{Binding Path=Res.Refresh,Mode=OneWay,Source={x:Static s:ResourceService.Current}}"></Button>
|
||||
</DockPanel>-->
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.CommunityFix_AutoRunProxy, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
@@ -89,12 +94,72 @@
|
||||
<Button Content="选择证书"/>
|
||||
</DockPanel>-->
|
||||
|
||||
<ui:SettingsExpanderItem Description="*该选项勾选后会导致程序的加速完全不生效,请确认是仅需要脚本功能后再启用">
|
||||
<ui:SettingsExpanderItem Description="*该选项勾选后会导致程序的加速完全不生效,请确认是配合二级代理实现仅需要脚本功能后再启用">
|
||||
<TextBlock Text="{Binding Path=Res.CommunityFix_ProxyOnlyOnScript, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ToggleSwitch Classes="LeftContent" IsChecked="{Binding Source={x:Static ms:ProxySettings.OnlyEnableProxyScript}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
|
||||
<ui:SettingsExpander
|
||||
Description="*改动后需重启加速才能生效"
|
||||
Header="{Binding Path=Res.Settings_Proxy_TwoLevelAgent, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding Source={x:Static ms:ProxySettings.TwoLevelAgentEnable}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_ProxyType, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FAComboBox MinWidth="200" SelectedItem="{Binding Source={x:Static ms:ProxySettings.TwoLevelAgentProxyType}, Path=Value, Mode=TwoWay}">
|
||||
<ui:FAComboBox.Items>
|
||||
<spp:ExternalProxyType>Http</spp:ExternalProxyType>
|
||||
<spp:ExternalProxyType>Socks4</spp:ExternalProxyType>
|
||||
<spp:ExternalProxyType>Socks5</spp:ExternalProxyType>
|
||||
</ui:FAComboBox.Items>
|
||||
</ui:FAComboBox>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_IPAddress, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<TextBox MinWidth="200" Text="{Binding Source={x:Static ms:ProxySettings.TwoLevelAgentIp}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_Port, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<NumericUpDown
|
||||
MinWidth="200"
|
||||
FormatString="0"
|
||||
Maximum="65535"
|
||||
Minimum="80"
|
||||
Value="{Binding Source={x:Static ms:ProxySettings.TwoLevelAgentPortId}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_UserName, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<TextBox MinWidth="200" Text="{Binding Source={x:Static ms:ProxySettings.TwoLevelAgentUserName}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Text="{Binding Path=Res.Settings_Proxy_Password, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<TextBox
|
||||
MinWidth="200"
|
||||
Classes="revealPasswordButton"
|
||||
PasswordChar="{StaticResource PasswordChar}"
|
||||
Text="{Binding Source={x:Static ms:ProxySettings.TwoLevelAgentPassword}, Path=Value, Mode=TwoWay}" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
|
||||
|
||||
protected sealed override string? AuthorOriginalString => null;
|
||||
|
||||
public sealed override string Description => moduleName;
|
||||
public sealed override string Description => moduleName + " 控制台功能实现";
|
||||
|
||||
public sealed override object? Icon => Resources.asf;
|
||||
|
||||
|
||||
@@ -475,6 +475,27 @@ public static class AuthenticatorHelper
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public static async Task<IEnumerable<IFileResult>?> MultipleSelectFolderPath(string? fileExtension = null)
|
||||
{
|
||||
var options = new PickOptions();
|
||||
if (!string.IsNullOrEmpty(fileExtension))
|
||||
{
|
||||
AvaloniaFilePickerFileTypeFilter fileTypes = new AvaloniaFilePickerFileTypeFilter.Item[] {
|
||||
new($"{fileExtension} Files") {
|
||||
Patterns = new[] { $"*{fileExtension}", },
|
||||
//MimeTypes
|
||||
//AppleUniformTypeIdentifiers =
|
||||
},
|
||||
};
|
||||
options.FileTypes = fileTypes;
|
||||
}
|
||||
var filePaths = await FilePicker2.PickMultipleAsync(options);
|
||||
|
||||
if (!filePaths.Any_Nullable())
|
||||
Toast.Show(ToastIcon.Warning, Strings.FilePathNotExist);
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
public static async Task<bool> VerifyMaxValue()
|
||||
{
|
||||
var auths = await AuthenticatorHelper.GetAllSourceAuthenticatorAsync();
|
||||
|
||||
+31
-24
@@ -1,5 +1,6 @@
|
||||
using BD.WTTS.UI.Views.Pages;
|
||||
using WinAuth;
|
||||
using static SteamKit2.DepotManifest;
|
||||
using SJsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
@@ -16,6 +17,8 @@ public class AuthenticatorImportPageViewModel : ViewModelBase
|
||||
{
|
||||
AuthenticatorImportMethods = new List<AuthenticatorImportMethod>
|
||||
{
|
||||
new AuthenticatorImportMethod(Strings.LocalAuth_Import.Format(Strings.LocalAuth_JoinSteamAuthenticator), Strings.LocalAuth_JoinSteamAuthenticatorDesc,
|
||||
PageType: typeof(JoinSteamAuthenticatorPage)),
|
||||
new AuthenticatorImportMethod(Strings.Auth_SteamLoginImport, Strings.Steam_UserLoginTip,
|
||||
PageType: typeof(SteamLoginImportPage)),
|
||||
new AuthenticatorImportMethod(Strings.LocalAuth_Import.Format(Strings.SteamGuard), Strings.LocalAuth_SteamGuardImport,
|
||||
@@ -26,8 +29,6 @@ public class AuthenticatorImportPageViewModel : ViewModelBase
|
||||
if (await AuthenticatorHelper.VerifyMaxValue())
|
||||
await ImportFromSdaFile();
|
||||
})),
|
||||
new AuthenticatorImportMethod(Strings.LocalAuth_Import.Format(Strings.LocalAuth_JoinSteamAuthenticator), Strings.LocalAuth_JoinSteamAuthenticatorDesc,
|
||||
PageType: typeof(JoinSteamAuthenticatorPage)),
|
||||
new AuthenticatorImportMethod(Strings.LocalAuth_Import.Format(Strings.WattToolKitV2), Strings.LocalAuth_WattToolKitV2Import,
|
||||
Command: ReactiveCommand.Create(async () =>
|
||||
{
|
||||
@@ -68,34 +69,40 @@ public class AuthenticatorImportPageViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = await AuthenticatorHelper.SelectFolderPath(FileEx.maFile);
|
||||
var filePaths = await AuthenticatorHelper.MultipleSelectFolderPath(FileEx.maFile);
|
||||
|
||||
if (string.IsNullOrEmpty(filePath)) return;
|
||||
if (!filePaths.Any_Nullable())
|
||||
return;
|
||||
|
||||
var text = await File.ReadAllTextAsync(filePath);
|
||||
|
||||
var sdaFileModel = SJsonSerializer.Deserialize(text, ImportFileModelJsonContext.Default.SdaFileModel);
|
||||
sdaFileModel.ThrowIsNull();
|
||||
var steamDataModel = new SdaFileConvertToSteamDataModel(sdaFileModel);
|
||||
|
||||
SteamAuthenticator steamAuthenticator = new()
|
||||
foreach (var file in filePaths)
|
||||
{
|
||||
DeviceId = sdaFileModel.DeviceId,
|
||||
Serial = sdaFileModel.SerialNumber,
|
||||
SecretKey = Convert.FromBase64String(sdaFileModel.SharedSecret),
|
||||
SteamData = SJsonSerializer.Serialize(steamDataModel, ImportFileModelJsonContext.Default.SdaFileConvertToSteamDataModel),
|
||||
};
|
||||
if (!File.Exists(file.FullPath)) return;
|
||||
var text = await File.ReadAllTextAsync(file.FullPath);
|
||||
|
||||
var authDto =
|
||||
new AuthenticatorDTO()
|
||||
var sdaFileModel = SJsonSerializer.Deserialize(text, ImportFileModelJsonContext.Default.SdaFileModel);
|
||||
sdaFileModel.ThrowIsNull();
|
||||
var steamDataModel = new SdaFileConvertToSteamDataModel(sdaFileModel);
|
||||
|
||||
SteamAuthenticator steamAuthenticator = new()
|
||||
{
|
||||
Name = $"(Steam){steamAuthenticator.AccountName}",
|
||||
Value = steamAuthenticator,
|
||||
Created = DateTimeOffset.Now,
|
||||
DeviceId = sdaFileModel.DeviceId,
|
||||
Serial = sdaFileModel.SerialNumber,
|
||||
SecretKey = Convert.FromBase64String(sdaFileModel.SharedSecret),
|
||||
SteamData = SJsonSerializer.Serialize(steamDataModel, ImportFileModelJsonContext.Default.SdaFileConvertToSteamDataModel),
|
||||
};
|
||||
if (await AuthenticatorHelper.SaveAuthenticator(authDto))
|
||||
{
|
||||
Toast.Show(ToastIcon.Success, Strings.ModelContent_ImportSuccessful_.Format(authDto.Name));
|
||||
|
||||
var authDto =
|
||||
new AuthenticatorDTO()
|
||||
{
|
||||
Name = $"(Steam){steamAuthenticator.AccountName}",
|
||||
Value = steamAuthenticator,
|
||||
Created = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
if (await AuthenticatorHelper.SaveAuthenticator(authDto))
|
||||
{
|
||||
Toast.Show(ToastIcon.Success, Strings.ModelContent_ImportSuccessful_.Format(authDto.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using AppResources = BD.WTTS.Client.Resources.Strings;
|
||||
|
||||
using BD.SteamClient.Models;
|
||||
using BD.SteamClient.Services;
|
||||
using WinAuth;
|
||||
using AppResources = BD.WTTS.Client.Resources.Strings;
|
||||
using WSteamClient = WinAuth.SteamClient;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
@@ -73,11 +72,61 @@ public sealed partial class SteamTradePageViewModel : WindowViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
IsLogged = true;
|
||||
IsLogged = await CheckAndRefreshToken();
|
||||
|
||||
await GetConfirmations(_steamClient);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CheckAndRefreshToken()
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
// 不具备检查条件 没有保持的会话信息
|
||||
if (_steamClient == null)
|
||||
return false;
|
||||
|
||||
if (_steamClient.Session == null || _steamAuthenticator == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// token 依旧有效 直接返回
|
||||
if (!_steamClient.Session.IsAccessTokenExpired())
|
||||
return true;
|
||||
|
||||
// 是否有 refresh token
|
||||
string? refreshToken = _steamClient.Session?.RefreshToken;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refreshToken))
|
||||
return false;
|
||||
|
||||
var session = _steamClient.Session;
|
||||
|
||||
// 刷新 access token (有效时间同样为一天)
|
||||
await session!.RefreshAccessToken(_steamClient);
|
||||
|
||||
// 使用原先获取的具体派生类型 修改数据
|
||||
_steamAuthenticator.SessionData = session.ToString();
|
||||
|
||||
// 重置当前 client
|
||||
if (!ResetCurrentClient())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 通过原始令牌引用保存信息
|
||||
await AuthenticatorHelper.SaveAuthenticator(_authenticatorDto)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Refresh()
|
||||
{
|
||||
if (_steamClient?.IsLoggedIn() == true)
|
||||
@@ -161,6 +210,30 @@ public sealed partial class SteamTradePageViewModel : WindowViewModel
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
bool ResetCurrentClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_steamAuthenticator == null)
|
||||
return false;
|
||||
|
||||
_steamClient?.Dispose();
|
||||
|
||||
// 重新生成 client
|
||||
_steamClient = _steamAuthenticator
|
||||
.GetClient(ResourceService.GetCurrentCultureSteamLanguageName());
|
||||
|
||||
if (_steamClient == null)
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async Task<IEnumerable<SteamTradeConfirmationModel>?> GetConfirmations(WSteamClient steamClient)
|
||||
{
|
||||
IsLoading = true;
|
||||
@@ -314,7 +387,6 @@ public sealed partial class SteamTradePageViewModel : WindowViewModel
|
||||
// Content = Strings.LocalAuth_AuthTrade_MessageBoxTip_.Format(statusText)
|
||||
// }, statusText, isCancelButton: true, isDialog: false))
|
||||
//{
|
||||
|
||||
Toast.Show(ToastIcon.Info, Strings.LocalAuth_AuthTrade_ConfirmTip_.Format(statusText));
|
||||
|
||||
if (!await ChangeTradeStatus(status, selectedList)) return;
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\Views\CsgoVacRepairPage.axaml.cs">
|
||||
<DependentUpon>CsgoVacRepairPage.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UI\Views\GameToolsPage.axaml.cs">
|
||||
<DependentUpon>GameToolsPage.axaml</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
|
||||
public sealed partial class CsgoVacRepairPageViewModel : ViewModelBase
|
||||
{
|
||||
string _OutputString = string.Empty;
|
||||
|
||||
public string OutputString
|
||||
{
|
||||
get => _OutputString;
|
||||
set => this.RaiseAndSetIfChanged(ref _OutputString, value);
|
||||
}
|
||||
|
||||
[Reactive]
|
||||
public bool Repairing { get; set; } = false;
|
||||
|
||||
#if WINDOWS
|
||||
|
||||
string BatPath = Path.Combine(Plugin.Instance.AppDataDirectory, "BAT", $"CSGOVAC_REPAIR{FileEx.BAT}");
|
||||
|
||||
public void Repairs_Click()
|
||||
{
|
||||
OutputString = string.Empty;
|
||||
Task2.InBackground(async () =>
|
||||
{
|
||||
await ExcuteBat();
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ExcuteBat()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(BatPath))
|
||||
{
|
||||
GenerateFixScript(BatPath);
|
||||
}
|
||||
Repairing = true;
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo =
|
||||
{
|
||||
FileName = BatPath,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
Arguments = $"\"{Registry.GetValue(@"HKEY_CURRENT_USER\SOFTWARE\Valve\Steam", "SteamPath", null)}/bin\""
|
||||
}
|
||||
};
|
||||
|
||||
process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
OutHandle(e.Data);
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
OutHandle(e.Data);
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Repairing = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void GenerateFixScript(string path)
|
||||
{
|
||||
var dirPath = Path.GetDirectoryName(path);
|
||||
if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath!);
|
||||
|
||||
using var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
|
||||
stream.Write("""
|
||||
@echo off
|
||||
chcp 65001
|
||||
:steam
|
||||
echo info - Checking if Steam is launched.
|
||||
tasklist | find /I "Steam.exe"
|
||||
if errorlevel 1 goto closedstatus
|
||||
if not errorlevel 1 goto killsteam
|
||||
:killsteam
|
||||
taskkill /F /IM Steam.exe
|
||||
goto steaminstall
|
||||
:closedstatus
|
||||
echo info - Not Started
|
||||
goto steaminstall
|
||||
sc config Netman start= AUTO
|
||||
sc start Netman
|
||||
sc config RasMan start= AUTO
|
||||
sc start RasMan
|
||||
sc config TapiSrv start= AUTO
|
||||
sc start TapiSrv
|
||||
sc config MpsSvc start= AUTO
|
||||
sc start MpsSvc
|
||||
netsh advfirewall set allprofiles state on
|
||||
goto steam
|
||||
:steaminstall
|
||||
echo info - ※^>^>^> 执行 Steam Services 修复中......
|
||||
cd /d %1
|
||||
steamservice /install
|
||||
ping -n 2 127.0.0.1>nul
|
||||
echo
|
||||
steamservice /repair
|
||||
echo info - ※ 开启数据执行保护
|
||||
bcdedit /debug off
|
||||
bcdedit.exe/set {current} nx alwayson
|
||||
echo info - ※ 执行完毕
|
||||
sc config "Steam Client Service" start= AUTO
|
||||
sc start "Steam Client Service"
|
||||
"""u8);
|
||||
stream.Flush();
|
||||
stream.SetLength(stream.Position);
|
||||
}
|
||||
|
||||
void OutHandle(string? msg)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(msg))
|
||||
{
|
||||
OutputString += $"{msg}\n";
|
||||
|
||||
if (msg.Contains("Add firewall exception failed for steamservice.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OutputString += "info - ※ 修复 Steam Services 失败\n";
|
||||
OutputString += "info - ※ 请检查您的防火墙设置(关闭 \"不允许例外\" 选项)再次尝试\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -123,28 +123,59 @@
|
||||
Text="{Binding Path=Res.GameRelated_Borderless_MoveMouseTip, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</WrapPanel>
|
||||
|
||||
<StackPanel
|
||||
<Grid
|
||||
HorizontalAlignment="Center"
|
||||
ColumnDefinitions="Auto,*"
|
||||
IsVisible="{ReflectionBinding !!SelectWindow}"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock TextWrapping="WrapWithOverflow">
|
||||
<Run Text="{Binding Path=Res.GameRelated_Borderless_Title, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<Run Text="{ReflectionBinding SelectWindow.Title, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="WrapWithOverflow">
|
||||
<Run Text="{Binding Path=Res.GameRelated_Borderless_ClassName, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<Run Text="{ReflectionBinding SelectWindow.ClassName, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="WrapWithOverflow">
|
||||
<Run Text="{Binding Path=Res.GameRelated_Borderless_Process, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<Run Text="{ReflectionBinding SelectWindow.Name, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="WrapWithOverflow">
|
||||
<Run Text="{Binding Path=Res.GameRelated_Borderless_Path, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<Run Text="{ReflectionBinding SelectWindow.Path, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
RowDefinitions="Auto,Auto,Auto,Auto">
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding Path=Res.GameRelated_Borderless_Title, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Margin="0,0,0,10"
|
||||
IsReadOnly="True"
|
||||
Text="{ReflectionBinding SelectWindow.Title,
|
||||
Mode=OneWay}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path=Res.GameRelated_Borderless_Process, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,0,10"
|
||||
IsReadOnly="True"
|
||||
Text="{ReflectionBinding SelectWindow.Title,
|
||||
Mode=OneWay}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path=Res.GameRelated_Borderless_ClassName, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<TextBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,0,10"
|
||||
IsReadOnly="True"
|
||||
Text="{ReflectionBinding SelectWindow.Name,
|
||||
Mode=OneWay}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path=Res.GameRelated_Borderless_Path, Mode=OneWay, Source={x:Static s:ResourceService.Current}, StringFormat={}{0}:}" />
|
||||
<TextBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,0,10"
|
||||
IsReadOnly="True"
|
||||
Text="{ReflectionBinding SelectWindow.Path,
|
||||
Mode=OneWay}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
</Grid>
|
||||
|
||||
<WrapPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<WrapPanel.Styles>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<spp:PageBase
|
||||
x:Class="BD.WTTS.UI.Views.Pages.CsgoVacRepairPage"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:i="using:Avalonia.Xaml.Interactivity"
|
||||
xmlns:ia="using:Avalonia.Xaml.Interactions.Core"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:s="https://steampp.net/services"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
Title="CS:GO VAC 修复"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="spp:CsgoVacRepairPageViewModel"
|
||||
Description="该功能可以为玩家修复 CS:GO VAC 问题"
|
||||
Subtitle="{Binding Author, Source={x:Static s:Plugin.Instance}, Mode=OneWay, Converter={StaticResource StringFormatConverter}, ConverterParameter=Plugin_Author}"
|
||||
mc:Ignorable="d">
|
||||
<spp:PageBase.PreviewImage>
|
||||
<ui:ImageIconSource Source="avares://BD.WTTS.Client.Plugins.GameTools/UI/Assets/CounterStrike.png" />
|
||||
</spp:PageBase.PreviewImage>
|
||||
<spp:PageBase.ActionContent>
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<Button
|
||||
Padding="8,4"
|
||||
DockPanel.Dock="Right"
|
||||
IsVisible="{Binding !Repairing}"
|
||||
Command="{ReflectionBinding Repairs_Click}"
|
||||
Theme="{StaticResource TransparentButton}">
|
||||
<Panel>
|
||||
<Viewbox
|
||||
Width="18"
|
||||
Height="18"
|
||||
HorizontalAlignment="Left">
|
||||
<ui:FontIcon Glyph=""/>
|
||||
</Viewbox>
|
||||
<TextBlock
|
||||
Margin="28,4,0,4"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Text="修复" />
|
||||
</Panel>
|
||||
</Button>
|
||||
<Button
|
||||
Padding="8,4"
|
||||
DockPanel.Dock="Right"
|
||||
IsVisible="{Binding Repairing}"
|
||||
Theme="{StaticResource TransparentButton}">
|
||||
<Panel>
|
||||
<Viewbox
|
||||
Width="18"
|
||||
Height="18"
|
||||
HorizontalAlignment="Left">
|
||||
<ui:FontIcon Glyph=""/>
|
||||
</Viewbox>
|
||||
<TextBlock
|
||||
Margin="28,4,0,4"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Text="修复中" />
|
||||
</Panel>
|
||||
</Button>
|
||||
<DropDownButton
|
||||
Padding="8"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Content="{StaticResource More}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="20"
|
||||
Theme="{StaticResource TransparentButton}">
|
||||
<DropDownButton.Flyout>
|
||||
<ui:FAMenuFlyout Placement="BottomEdgeAlignedRight">
|
||||
<!--<ui:MenuFlyoutItem x:Name="PluginSettingButton" Text="{Binding Path=Res.Settings_Plugin, Mode=OneWay,Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:MenuFlyoutSeparator />-->
|
||||
<ui:MenuFlyoutItem IsEnabled="False">
|
||||
<ui:MenuFlyoutItem.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1}">
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.Plugin_Version"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Version"
|
||||
Source="{x:Static s:Plugin.Instance}" />
|
||||
</MultiBinding>
|
||||
</ui:MenuFlyoutItem.Text>
|
||||
</ui:MenuFlyoutItem>
|
||||
<!--<ui:MenuFlyoutItem Text="{Binding Path=Res.ViewInStore, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:MenuFlyoutItem Text="{Binding Path=Res.UseHelp, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<ui:MenuFlyoutItem Text="{Binding Path=Res.AboutThisPlugin, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />-->
|
||||
</ui:FAMenuFlyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
</StackPanel>
|
||||
</spp:PageBase.ActionContent>
|
||||
<Panel>
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<TextBox
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Classes="readonlybox"
|
||||
Text="{Binding OutputString}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<!--<TextBox
|
||||
Name="CommandTextBox"
|
||||
Grid.Row="1"
|
||||
Watermark="{Binding Path=Res.Debug_InstructionPlaceholder, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />-->
|
||||
</Grid>
|
||||
</Border>
|
||||
</Panel>
|
||||
</spp:PageBase>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Pages;
|
||||
|
||||
public partial class CsgoVacRepairPage : PageBase<CsgoVacRepairPageViewModel>
|
||||
{
|
||||
public CsgoVacRepairPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext ??= new CsgoVacRepairPageViewModel();
|
||||
}
|
||||
}
|
||||
@@ -134,8 +134,10 @@
|
||||
<Button
|
||||
Click="GameToolsPage_Click"
|
||||
IsEnabled="{spp:OnPlatform Windows}"
|
||||
Tag="{x:Null}"
|
||||
Theme="{StaticResource NavButtonStyle}">
|
||||
<Button.Tag>
|
||||
<x:Type TypeName="spp:CsgoVacRepairPage" />
|
||||
</Button.Tag>
|
||||
<Grid RowDefinitions="3*,*">
|
||||
<Border
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -157,7 +159,7 @@
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
|
||||
|
||||
<Border spp:Animations.EnableAnimations="True">
|
||||
<Button
|
||||
Click="GameToolsPage_Click"
|
||||
|
||||
@@ -2,6 +2,7 @@ using BD.SteamClient.Models;
|
||||
using BD.SteamClient.Models.Idle;
|
||||
using BD.SteamClient.Services;
|
||||
using BD.SteamClient.Constants;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
|
||||
@@ -129,13 +130,7 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
}
|
||||
else
|
||||
{
|
||||
RunState = false;
|
||||
StopIdle();
|
||||
RunOrStopAutoNext(false);
|
||||
RunOrStopAutoCardDropCheck(false);
|
||||
IdleTime = default;
|
||||
ResetCurrentIdle();
|
||||
ChangeRunTxt();
|
||||
StopIdleRun();
|
||||
}
|
||||
|
||||
RunLoaingState = false;
|
||||
@@ -147,6 +142,17 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
private void StopIdleRun()
|
||||
{
|
||||
RunState = false;
|
||||
StopIdle();
|
||||
RunOrStopAutoNext(false);
|
||||
RunOrStopAutoCardDropCheck(false);
|
||||
IdleTime = default;
|
||||
ResetCurrentIdle();
|
||||
ChangeRunTxt();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动切换下一个游戏
|
||||
/// </summary>
|
||||
@@ -307,17 +313,26 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var isTokenAccess = await Ioc.Get<ISteamAccountService>().CheckAccessTokenValidation(SteamLoginState.AccessToken!);
|
||||
var isTokenAccess = Ioc.Get<ISteamAccountService>().IsAccessTokenValid(SteamLoginState.AccessToken!);
|
||||
if (isTokenAccess)
|
||||
{
|
||||
await RunLoadBadges();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ISecureStorage.Instance.RemoveAsync(ISteamSessionService.CurrentSteamUserKey);
|
||||
SteamLoginState = new();
|
||||
await LoginSteam();
|
||||
await RunLoadBadges();
|
||||
var new_accessToken = await Ioc.Get<ISteamAccountService>().RefreshAccessToken(SteamLoginState.SteamId, SteamLoginState.RefreshToken!);
|
||||
if (new_accessToken is not null) // 刷新Token
|
||||
{
|
||||
await RefreshAccessTokenAsync(new_accessToken);
|
||||
await RunLoadBadges();
|
||||
}
|
||||
else // 重新登录
|
||||
{
|
||||
await ISecureStorage.Instance.RemoveAsync(ISteamSessionService.CurrentSteamUserKey);
|
||||
SteamLoginState = new();
|
||||
await LoginSteam();
|
||||
await RunLoadBadges();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -377,17 +392,19 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
IdleGameList.Clear();
|
||||
var badges = Badges.Where(w => w.CardsRemaining != 0);
|
||||
var apps = SteamIdleSettings.IdleSequentital.Value switch
|
||||
var apps = (SteamIdleSettings.IdleSequentital.Value switch
|
||||
{
|
||||
IdleSequentital.LeastCards => badges.OrderBy(o => o.CardsRemaining).Select(s => new IdleApp(s)),
|
||||
IdleSequentital.Mostcards => badges.OrderByDescending(o => o.CardsRemaining).Select(s => new IdleApp(s)),
|
||||
IdleSequentital.Mostvalue => badges.OrderByDescending(o => o.RegularAvgPrice).Select(s => new IdleApp(s)),
|
||||
_ => badges.Select(s => new IdleApp(s)),
|
||||
};
|
||||
foreach (var app in apps)
|
||||
IdleGameList.Add(app);
|
||||
}).ToImmutableList();
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
IdleGameList.Clear();
|
||||
EnumerableExtensions.AddRange(IdleGameList, apps);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -428,10 +445,11 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
{
|
||||
var multi = IdleGameList.Where(z => z.Badge.HoursPlayed >= SteamIdleSettings.MinRunTime.Value).ToList();
|
||||
|
||||
if (multi.Count > 1 || (multi.Count == 1 && IdleGameList.Count == 1))
|
||||
var isLastSingle = multi.Count == 1 && IdleGameList.Count == 1; // 是否只剩最后一个游戏
|
||||
if (multi.Count > 1 || isLastSingle)
|
||||
{
|
||||
idleApp = VerifyIsNext(multi, isNext);
|
||||
PauseAutoNext(false);
|
||||
PauseAutoNext(isLastSingle);
|
||||
StartSoloIdle(idleApp);
|
||||
}
|
||||
else
|
||||
@@ -777,6 +795,23 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
Toast.Show(ToastIcon.Success, message);
|
||||
INotificationService.Instance.Notify(message, NotificationType.Message);
|
||||
}
|
||||
|
||||
private async Task RefreshAccessTokenAsync(string new_accessToken)
|
||||
{
|
||||
SteamLoginState.AccessToken = new_accessToken;
|
||||
SteamSession session = new SteamSession()
|
||||
{
|
||||
SteamId = SteamLoginState.SteamId.ToString(),
|
||||
AccessToken = SteamLoginState.AccessToken,
|
||||
RefreshToken = SteamLoginState.RefreshToken
|
||||
};
|
||||
session.GenerateSetCookie();
|
||||
var sessionService = Ioc.Get<ISteamSessionService>();
|
||||
sessionService.AddOrSetSeesion(session);
|
||||
|
||||
if (LoginViewModel is null || LoginViewModel.RemenberLogin)
|
||||
await sessionService.SaveSession(session);
|
||||
}
|
||||
#endregion
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace BD;
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
public sealed class MobiusAttribute : Attribute
|
||||
{
|
||||
public MobiusAttribute()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MobiusAttribute(string str)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Obsolete { get; set; }
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace BD;
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
public sealed class MobiusAttribute : Attribute
|
||||
{
|
||||
public MobiusAttribute()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MobiusAttribute(string str)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Obsolete { get; set; }
|
||||
}
|
||||
@@ -10,8 +10,6 @@ partial interface IApplication
|
||||
/// </summary>
|
||||
/// <param name="iconKey"></param>
|
||||
/// <returns></returns>
|
||||
[Mobius(Obsolete = true)]
|
||||
[Obsolete]
|
||||
string? GetIconSourceByIconKey(string? iconKey) => string.IsNullOrWhiteSpace(iconKey) ? null :
|
||||
#if WINDOWS || MACCATALYST || MACOS || LINUX
|
||||
$"avares://BD.WTTS.Client.Avalonia.App/UI/Assets/Icons/{iconKey}.ico";
|
||||
@@ -24,8 +22,6 @@ partial interface IApplication
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns></returns>
|
||||
[Mobius(Obsolete = true)]
|
||||
[Obsolete]
|
||||
string GetIconKeyByTypeName(string typeName)
|
||||
{
|
||||
#if WINDOWS || MACCATALYST || MACOS || LINUX
|
||||
|
||||
@@ -12,12 +12,6 @@ partial interface IApplication
|
||||
/// <param name="msgToast"></param>
|
||||
/// <param name="showToast"></param>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
App // ICommand
|
||||
async void CopyToClipboardCommandCore(object? text)
|
||||
ICommand CopyToClipboardCommand
|
||||
""")]
|
||||
static async Task CopyToClipboardAsync(string? text, string? msgToast = null, bool showToast = true)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
|
||||
@@ -3,65 +3,37 @@ namespace BD.WTTS;
|
||||
|
||||
partial interface IApplication : IDisposableHolder
|
||||
{
|
||||
[Mobius(
|
||||
"""
|
||||
App.CompositeDisposable
|
||||
""")]
|
||||
new CompositeDisposable CompositeDisposable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 切换当前桌面应用的主题而不改变设置值
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
[Mobius(
|
||||
"""
|
||||
App.SetThemeNotChangeValue
|
||||
""")]
|
||||
void SetThemeNotChangeValue(AppTheme value) { }
|
||||
|
||||
/// <summary>
|
||||
/// 退出整个程序
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.Shutdown
|
||||
""")]
|
||||
void Shutdown() { }
|
||||
|
||||
/// <summary>
|
||||
/// 主窗口恢复显示
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.RestoreMainWindow
|
||||
""")]
|
||||
void RestoreMainWindow() { }
|
||||
|
||||
/// <summary>
|
||||
/// 主窗口置顶一次
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.RestoreMainWindow
|
||||
""")]
|
||||
void SetTopmostOneTime() { }
|
||||
|
||||
/// <summary>
|
||||
/// 托盘菜单
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.TrayIconMenus
|
||||
""")]
|
||||
IReadOnlyDictionary<string, ICommand> TrayIconMenus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否有活动窗口
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
App.HasActiveWindow
|
||||
""")]
|
||||
bool HasActiveWindow();
|
||||
}
|
||||
@@ -5,16 +5,8 @@ partial interface IApplication
|
||||
{
|
||||
#region Logger
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
const LogLevel DefaultLoggerMinLevel = IPCSubProcessFileSystem.DefaultLoggerMinLevel;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
public static NLogLevel DefaultNLoggerMinLevel => IPCSubProcessFileSystem.DefaultNLoggerMinLevel;
|
||||
|
||||
/// <summary>
|
||||
@@ -22,24 +14,12 @@ Mobius.Helpers.LogInitHelper
|
||||
/// </summary>
|
||||
/// <param name="logLevel">level to be converted.</param>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static NLogLevel ConvertLogLevel(LogLevel logLevel) => IPCSubProcessFileSystem.ConvertLogLevel(logLevel);
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void SetNLoggerMinLevel(LogLevel logLevel) => SetNLoggerMinLevel(ConvertLogLevel(logLevel));
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void SetNLoggerMinLevel(NLogLevel logLevel)
|
||||
{
|
||||
@@ -47,7 +27,6 @@ Mobius.Helpers.LogInitHelper
|
||||
NInternalLogger.LogLevel = logLevel;
|
||||
}
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void TrySetLoggerMinLevel(LogLevel logLevel)
|
||||
{
|
||||
@@ -62,19 +41,11 @@ Mobius.Helpers.LogInitHelper
|
||||
SetNLoggerMinLevel(ConvertLogLevel(logLevel));
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
private static LoggerFilterOptions? _LoggerFilterOptions;
|
||||
|
||||
/// <summary>
|
||||
/// 日志过滤选项
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
static LoggerFilterOptions? LoggerFilterOptions
|
||||
{
|
||||
get
|
||||
@@ -88,10 +59,6 @@ Mobius.Helpers.LogInitHelper
|
||||
/// <summary>
|
||||
/// 配置 NLog 提供程序,仅初始化时调用
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper.GetMobiusLoggingBuilder
|
||||
""")]
|
||||
public static Action<ILoggingBuilder> ConfigureLogging(LogLevel minLevel = DefaultLoggerMinLevel)
|
||||
{
|
||||
return (ILoggingBuilder builder) =>
|
||||
@@ -125,10 +92,6 @@ Mobius.Helpers.LogInitHelper.GetMobiusLoggingBuilder
|
||||
/// <summary>
|
||||
/// 日志最低等级
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper
|
||||
""")]
|
||||
public static LogLevel LoggerMinLevel
|
||||
{
|
||||
get
|
||||
@@ -156,7 +119,6 @@ Mobius.Helpers.LogInitHelper
|
||||
/// <summary>
|
||||
/// 启用日志
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
public static bool EnableLogger
|
||||
{
|
||||
get => LoggerMinLevel > LogLevel.None;
|
||||
@@ -173,10 +135,6 @@ Mobius.Helpers.LogInitHelper
|
||||
/// <summary>
|
||||
/// 日志存放文件夹路径
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
MobiusHost.LogDirPath
|
||||
""")]
|
||||
static string LogDirPath { get; internal set; } = string.Empty;
|
||||
|
||||
#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
|
||||
@@ -184,7 +142,6 @@ MobiusHost.LogDirPath
|
||||
/// <summary>
|
||||
/// 日志存放文件夹路径(ASF)
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
static string LogDirPathASF { get; set; } = string.Empty;
|
||||
|
||||
#endif
|
||||
@@ -197,7 +154,6 @@ MobiusHost.LogDirPath
|
||||
static bool LogUnderCache => true;
|
||||
#endif
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
static Action? ASFInitCoreLoggers { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -3,10 +3,6 @@ namespace BD.WTTS;
|
||||
|
||||
partial interface IApplication
|
||||
{
|
||||
[Mobius(
|
||||
"""
|
||||
MobiusHost.EnableDevTools
|
||||
""")]
|
||||
public static bool EnableDevtools { get; set; } =
|
||||
#if DEBUG
|
||||
true
|
||||
@@ -18,18 +14,10 @@ MobiusHost.EnableDevTools
|
||||
/// <summary>
|
||||
/// 禁用 GPU 硬件加速
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
MobiusHost.DisableGPU
|
||||
""")]
|
||||
public static bool DisableGPU { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用 native OpenGL(仅 Windows)
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
MobiusHost.UseWgl
|
||||
""")]
|
||||
public static bool UseWgl { get; set; }
|
||||
}
|
||||
@@ -20,19 +20,11 @@ partial interface IApplication
|
||||
}
|
||||
#endif
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
AppConstants.ProgramName
|
||||
""")]
|
||||
private static string? _ProgramName;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前主程序文件名,例如 steam++.exe
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
AppConstants.ProgramName
|
||||
""")]
|
||||
static string ProgramName
|
||||
{
|
||||
get
|
||||
@@ -42,10 +34,6 @@ AppConstants.ProgramName
|
||||
}
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
AppConstants.ProgramName
|
||||
""")]
|
||||
private static string GetProgramName()
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
|
||||
@@ -6,19 +6,11 @@ partial interface IApplication
|
||||
/// <summary>
|
||||
/// 获取或设置当前应用的主题,例如跟随系统,浅色,深色
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.Theme
|
||||
""")]
|
||||
AppTheme Theme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前应用的实际主题
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.ActualTheme
|
||||
""")]
|
||||
AppTheme ActualTheme => Theme switch
|
||||
{
|
||||
AppTheme.FollowingSystem => GetActualThemeByFollowingSystem(),
|
||||
@@ -30,13 +22,8 @@ App.ActualTheme
|
||||
/// <summary>
|
||||
/// 获取当前应用的默认主题
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
App.DefaultActualTheme
|
||||
""")]
|
||||
protected AppTheme DefaultActualTheme { get; }
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static AppTheme GetAppThemeByIsLightOrDarkTheme(bool isLightOrDarkTheme) => isLightOrDarkTheme ? AppTheme.Light : AppTheme.Dark;
|
||||
|
||||
@@ -44,10 +31,6 @@ App.DefaultActualTheme
|
||||
/// 获取当前应用主题跟随系统时的实际主题
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
App.GetActualThemeByFollowingSystem
|
||||
""")]
|
||||
protected AppTheme GetActualThemeByFollowingSystem()
|
||||
{
|
||||
var dps = IPlatformService.Instance;
|
||||
|
||||
@@ -2,10 +2,6 @@ namespace BD.WTTS;
|
||||
|
||||
public partial interface IApplication
|
||||
{
|
||||
[Mobius(
|
||||
"""
|
||||
App.TrayMenus
|
||||
""")]
|
||||
Dictionary<string, TrayMenuItem>? TrayMenus { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -13,9 +9,5 @@ App.TrayMenus
|
||||
/// </summary>
|
||||
/// <param name="menuKey"></param>
|
||||
/// <param name="trayMenuItem"></param>
|
||||
[Mobius(
|
||||
"""
|
||||
App.UpdateMenuItems
|
||||
""")]
|
||||
void UpdateMenuItems(string menuKey, TrayMenuItem trayMenuItem);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ namespace BD.WTTS;
|
||||
/// </summary>
|
||||
public partial interface IApplication
|
||||
{
|
||||
[Mobius(
|
||||
"""
|
||||
App // ICommand
|
||||
async void CopyToClipboardCommandCore(object? text)
|
||||
ICommand CopyToClipboardCommand
|
||||
""")]
|
||||
static IApplication Instance => Ioc.Get<IApplication>();
|
||||
|
||||
/// <summary>
|
||||
@@ -19,7 +13,6 @@ ICommand CopyToClipboardCommand
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Mobius(Obsolete = true)]
|
||||
static bool IsDesktop()
|
||||
#if WINDOWS || MACOS || LINUX
|
||||
=> true;
|
||||
@@ -31,7 +24,6 @@ ICommand CopyToClipboardCommand
|
||||
=> OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || (OperatingSystem.IsLinux() && !OperatingSystem.IsAndroid());
|
||||
#endif
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
[Obsolete("use IsDesktop", true)]
|
||||
static bool IsDesktopPlatform => IsDesktop();
|
||||
|
||||
@@ -39,7 +31,6 @@ ICommand CopyToClipboardCommand
|
||||
/// 获取当前平台 UI Host
|
||||
/// <para>reference to the ViewController (if using Xamarin.iOS), Activity (if using Xamarin.Android) IWin32Window or IntPtr (if using .Net Framework).</para>
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
object CurrentPlatformUIHost { get; }
|
||||
|
||||
#if WINDOWS
|
||||
@@ -61,7 +52,6 @@ ICommand CopyToClipboardCommand
|
||||
});
|
||||
#endif
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
DeploymentMode DeploymentMode =>
|
||||
#if WINDOWS
|
||||
_DeploymentMode.Value;
|
||||
@@ -70,16 +60,8 @@ ICommand CopyToClipboardCommand
|
||||
#endif
|
||||
|
||||
/// <inheritdoc cref="IPlatformService.SetBootAutoStart(bool, string)"/>
|
||||
[Mobius(
|
||||
"""
|
||||
SystemBootHelper.SetBootAutoStart
|
||||
""")]
|
||||
static void SetBootAutoStart(bool isAutoStart) => IPlatformService.Instance.SetBootAutoStart(isAutoStart, Constants.HARDCODED_APP_NAME);
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
ISppWebApiService.clientPlatform
|
||||
""")]
|
||||
private static readonly Lazy<ClientPlatform> clientPlatform = new(() =>
|
||||
{
|
||||
var platform = DeviceInfo2.Platform();
|
||||
@@ -163,29 +145,13 @@ ISppWebApiService.clientPlatform
|
||||
return default;
|
||||
});
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.GetApplicationIcon
|
||||
""")]
|
||||
static byte[] Login_512 => Properties.Resources.AppLogo_512;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
ISppWebApiService.clientPlatform
|
||||
""")]
|
||||
static ClientPlatform ClientPlatform => clientPlatform.Value;
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
System.Drawing.Size? GetScreenSize() => null;
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
bool? IsAnyWindowNotMinimized() => null;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App // ICommand
|
||||
async void OpenBrowserCommandCore(object? url)
|
||||
ICommand OpenBrowserCommand
|
||||
""")]
|
||||
async void OpenBrowserCommandCore(object? url) => await Browser2.OpenAsync(url?.ToString());
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace BD.WTTS;
|
||||
/// 指定应用此特性的类需要指定的授权
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
|
||||
[Mobius(Obsolete = true)]
|
||||
[Obsolete]
|
||||
public sealed class AuthorizeAttribute : Attribute
|
||||
{
|
||||
public static bool HasAuthorize(object obj)
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="Net.Codecrete.QrCodeGenerator" />
|
||||
<PackageReference Include="Fleck" />
|
||||
<PackageReference Include="Stun.Net" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<!--<PackageReference Include="Portable.BouncyCastle" />-->
|
||||
<PackageReference Include="ReactiveUI.Fody" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS.Columns;
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
public interface IReadOnlyName
|
||||
{
|
||||
string? Name { get; }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS.Columns;
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
[Obsolete]
|
||||
public interface IReadOnlyStaticDisplayName
|
||||
{
|
||||
static abstract string? DisplayName { get; }
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
CertificateConstants
|
||||
""")]
|
||||
public static class CertificateConstants
|
||||
{
|
||||
public const int CertificateValidDays = 300;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Constants.UrlConstants
|
||||
""")]
|
||||
[Obsolete("use Constants.Urls/Constants", true)]
|
||||
public static class UrlConstants
|
||||
{
|
||||
|
||||
@@ -4,19 +4,11 @@ namespace BD.WTTS.Diagnostics;
|
||||
/// 启动耗时跟踪
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
[Mobius(
|
||||
"""
|
||||
App.WriteElapsedMilliseconds
|
||||
""", Obsolete = true)]
|
||||
public static class StartWatchTrace
|
||||
{
|
||||
static Stopwatch? sw;
|
||||
static StringBuilder? sb;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.WriteElapsedMilliseconds
|
||||
""")]
|
||||
public static void Record(string? mark = null, bool dispose = false)
|
||||
{
|
||||
if (sw != null)
|
||||
@@ -49,12 +41,7 @@ App.WriteElapsedMilliseconds
|
||||
}
|
||||
}
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
App.Elapsed
|
||||
""")]
|
||||
public static new string ToString() => sb?.ToString() ?? string.Empty;
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
public static long ElapsedMilliseconds => sw == null ? 0L : sw.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ namespace BD.WTTS.Entities;
|
||||
/// <summary>
|
||||
/// 通知记录
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
[SQLiteTable("A765AD32")]
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class Notification : IEntity<Guid>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace BD.WTTS.Entities;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Entities.Http.RequestCache
|
||||
""")]
|
||||
[SQLiteTable(TableName)]
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class RequestCache : IEntity<string>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace BD.WTTS.Entities;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Entities.Identity.User
|
||||
""")]
|
||||
[SQLiteTable("D5428AED")]
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class User : IEntity<Guid>
|
||||
|
||||
@@ -3,10 +3,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 应用程序终结点路由
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.App.AppEndPoint
|
||||
""", Obsolete = true)]
|
||||
public enum AppEndPoint : byte
|
||||
{
|
||||
LoginOrRegister = 1,
|
||||
|
||||
@@ -3,7 +3,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 指定用于检索应用程序所使用的文件夹目录路径的枚举常数
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
public enum AppFolder : byte
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,7 +24,6 @@ public enum AppFolder : byte
|
||||
/// <summary>
|
||||
/// Enum 扩展 <see cref="AppFolder"/>
|
||||
/// </summary>
|
||||
[Mobius(Obsolete = true)]
|
||||
public static class AppFolderEnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -4,10 +4,6 @@ namespace BD.WTTS.Enums;
|
||||
/// 应用服务级别,较低的级别注入较少的服务,适用于其他轻量进程业务启动
|
||||
/// </summary>
|
||||
[Flags]
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.App.AppServicesLevel
|
||||
""")]
|
||||
public enum AppServicesLevel : uint
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -5,10 +5,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 应用程序主题
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.App.AppTheme
|
||||
""")]
|
||||
public enum AppTheme : byte
|
||||
{
|
||||
/// <summary>
|
||||
@@ -38,7 +34,6 @@ public enum AppTheme : byte
|
||||
Custom,
|
||||
}
|
||||
|
||||
[Mobius(Obsolete = true)]
|
||||
public static partial class AppThemeEnumExtensions
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
@@ -3,10 +3,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 应用程序 Web 代理模式
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.App.AppWebProxyMode
|
||||
""")]
|
||||
public enum AppWebProxyMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.ExitCode
|
||||
// src/ExitCode.cs
|
||||
""")]
|
||||
public enum CommandExitCode
|
||||
{
|
||||
#region 100 ~ 5xx HttpStatusCode
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.Text.EncodingType
|
||||
""")]
|
||||
public enum EncodingType : byte
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -23,10 +23,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetDescription(NotificationChannelType)"/>中添加渠道描述</para>
|
||||
/// <para>在<see cref="NotificationType_Channel_EnumExtensions.GetImportanceLevel(NotificationChannelType)"/>中添加渠道重要性级别</para>
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.PlatformNotification.PlatformNotificationChannelType
|
||||
""")]
|
||||
public enum NotificationChannelType
|
||||
{
|
||||
/// <inheritdoc cref="NotificationType.NewVersion"/>
|
||||
@@ -49,10 +45,6 @@ public enum NotificationChannelType
|
||||
/// <summary>
|
||||
/// Enum 扩展 <see cref="NotificationType"/> AND <see cref="NotificationChannelType"/>
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.PlatformNotification.PlatformNotificationType_Channel_EnumExtensions
|
||||
""")]
|
||||
public static partial class NotificationType_Channel_EnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,10 +3,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 退出操作系统方式
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.App.OSExitMode
|
||||
""")]
|
||||
public enum OSExitMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace BD.WTTS.Enums;
|
||||
/// 表示操作结果的枚举
|
||||
/// </summary>
|
||||
[Description("操作结果的枚举")]
|
||||
[Mobius(Obsolete = true)]
|
||||
public enum OperationResultType
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,10 +3,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 文本阅读器提供商
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.Text.TextReaderProvider
|
||||
""")]
|
||||
public enum TextReaderProvider
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.UI.GridLayoutType
|
||||
""")]
|
||||
public enum GridLayoutType
|
||||
{
|
||||
List = 0,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.UI.NavigationTransitionEffect
|
||||
""")]
|
||||
public enum NavigationTransitionEffect
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -4,10 +4,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 资源图标
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.UI.ResIcon
|
||||
""")]
|
||||
public enum ResIcon
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -4,10 +4,6 @@ namespace BD.WTTS.Enums;
|
||||
/// <summary>
|
||||
/// 指定是否可以调整窗口大小,并在可调整大小的情况下指定如何调整窗口大小。由 SetResizeMode 使用
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.UI.ResizeMode
|
||||
""")]
|
||||
public enum ResizeMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.UI.WindowBackgroundMaterial
|
||||
""")]
|
||||
public enum WindowBackgroundMaterial : byte
|
||||
{
|
||||
None = 0,
|
||||
|
||||
Transparent = 1,
|
||||
|
||||
Blur = 2,
|
||||
|
||||
AcrylicBlur = 3,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS.Enums;
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.Enums.UI.XamlMediaStretch
|
||||
""")]
|
||||
public enum XamlMediaStretch : byte
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -3,18 +3,10 @@ namespace BD.WTTS;
|
||||
/// <summary>
|
||||
/// <see cref="ApiRspImpl"/> 的扩展函数
|
||||
/// </summary>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.ApiRspImplHandleUIExtensions
|
||||
""", Obsolete = true)]
|
||||
public static class ApiRspImplHandleUIExtensions
|
||||
{
|
||||
const string TAG = "HandleUI";
|
||||
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.ApiRspImplHandleUIExtensions
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void LogError(this ApiRspBase apiRsp, string message)
|
||||
{
|
||||
@@ -30,10 +22,6 @@ Mobius.ApiRspImplHandleUIExtensions
|
||||
/// <param name="errorAppendText"></param>
|
||||
/// <param name="errorFormat"></param>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.ApiRspImplHandleUIExtensions
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HandleUI(this ApiRspBase apiRsp, ToastIcon icon = ToastIcon.Error, string? errorAppendText = null, string? errorFormat = null)
|
||||
{
|
||||
@@ -64,10 +52,6 @@ Mobius.ApiRspImplHandleUIExtensions
|
||||
/// <param name="errorAppendText"></param>
|
||||
/// <param name="errorFormat"></param>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.ApiRspImplHandleUIExtensions
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HandleUI<TContent>(this ApiRsp<TContent> apiRsp,
|
||||
[NotNullWhen(true)] out TContent? content, ToastIcon icon = ToastIcon.Error, string? errorAppendText = null, string? errorFormat = null)
|
||||
@@ -123,10 +107,6 @@ Mobius.ApiRspImplHandleUIExtensions
|
||||
/// <param name="errorAppendText"></param>
|
||||
/// <param name="errorFormat"></param>
|
||||
/// <returns></returns>
|
||||
[Mobius(
|
||||
"""
|
||||
Mobius.ApiRspImplHandleUIExtensions
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HandleUIAllowNull<TContent>(this ApiRsp<TContent> apiRsp,
|
||||
out TContent? content, ToastIcon icon = ToastIcon.Error, string? errorAppendText = null, string? errorFormat = null)
|
||||
|
||||
@@ -3,15 +3,12 @@ using System.Reactive.Disposables;
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace System;
|
||||
|
||||
[BD.Mobius(Obsolete = true)]
|
||||
public static partial class CompositeDisposableExtensions
|
||||
{
|
||||
[BD.Mobius(Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Add(this CompositeDisposable @this, Action dispose)
|
||||
=> @this.Add(Disposable.Create(dispose));
|
||||
|
||||
[BD.Mobius(Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Add(this CompositeDisposable @this, Func<ValueTask> dispose)
|
||||
=> @this.Add(new AsyncDisposable(dispose));
|
||||
|
||||
@@ -7,10 +7,6 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static partial class ServiceCollectionExtensions
|
||||
{
|
||||
[BD.Mobius(
|
||||
"""
|
||||
Mobius.Helpers.*
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddPlatformService(
|
||||
this IServiceCollection services,
|
||||
|
||||
-4
@@ -4,10 +4,6 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static partial class ServiceCollectionExtensions
|
||||
{
|
||||
[BD.Mobius(
|
||||
"""
|
||||
Mobius.Helpers.WinScheduledTaskHelper
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddScheduledTaskService(this IServiceCollection services)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace System;
|
||||
public static class ProcessExtensions
|
||||
{
|
||||
/// <inheritdoc cref="IPlatformService.GetCommandLineArgs(Process)"/>
|
||||
[BD.Mobius(Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string GetCommandLineArgs(this Process process)
|
||||
{
|
||||
|
||||
-4
@@ -8,10 +8,6 @@ public static partial class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
[BD.Mobius(
|
||||
"""
|
||||
BD.Common8.Security.Extensions.AddSecurityService
|
||||
""")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddSecurityService(this IServiceCollection services)
|
||||
{
|
||||
|
||||
-4
@@ -10,10 +10,6 @@ public static partial class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
[BD.Mobius(
|
||||
"""
|
||||
Mobius.Helpers.LogInitHelper.AddMobiusLogging
|
||||
""")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddGeneralLogging(this IServiceCollection services)
|
||||
{
|
||||
|
||||
-4
@@ -9,10 +9,6 @@ public static partial class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
[BD.Mobius(
|
||||
"""
|
||||
services.AddSingleton<IHostsFileService, HostsFileServiceImpl>();
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddHostsFileService(this IServiceCollection services)
|
||||
{
|
||||
|
||||
-5
@@ -3,11 +3,6 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static partial class ServiceCollectionExtensions
|
||||
{
|
||||
[BD.Mobius(
|
||||
"""
|
||||
services.AddSingleton<IUserRepository, UserRepository>();
|
||||
services.AddSingleton<IRequestCacheRepository, RequestCacheRepository>();
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||
{
|
||||
|
||||
-4
@@ -3,10 +3,6 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static partial class ServiceCollectionExtensions
|
||||
{
|
||||
[BD.Mobius(
|
||||
"""
|
||||
AddFrontendToast
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddStartupToastIntercept(this IServiceCollection services)
|
||||
{
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ public static partial class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
[BD.Mobius(Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection AddViewModelManager(this IServiceCollection services)
|
||||
{
|
||||
|
||||
-4
@@ -8,10 +8,6 @@ public static partial class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
[BD.Mobius(
|
||||
"""
|
||||
services.AddSingleton<IHttpPlatformHelperService, ClientHttpPlatformHelperServiceImpl>();
|
||||
""", Obsolete = true)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceCollection TryAddClientHttpPlatformHelperService(this IServiceCollection services)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user