├── .gitattributes ├── .gitignore ├── Directory.Build.props ├── Directory.Packages.props ├── FrpGUI.Avalonia.Browser ├── FrpGUI.Avalonia.Browser.csproj ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ └── launchSettings.json ├── runtimeconfig.template.json ├── uiconfig.json ├── web.config └── wwwroot │ ├── app.css │ ├── favicon.ico │ ├── index.html │ ├── main.js │ └── utils.js ├── FrpGUI.Avalonia.Desktop ├── FrpGUI.Avalonia.Desktop.csproj ├── Program.cs └── app.manifest ├── FrpGUI.Avalonia ├── App.axaml ├── App.axaml.cs ├── Assets │ ├── WRYH.ttf │ ├── WRYH_Bold.ttf │ └── icon.ico ├── Brushes.axaml ├── Converters │ ├── NullableValueConverter.cs │ ├── ProcessStatus2BrushConverter.cs │ └── RuleParameterEnableConverter.cs ├── DataProviders │ ├── HttpRequester.cs │ ├── IDataProvider.cs │ ├── LocalDataProvider.cs │ └── WebDataProvider.cs ├── FrpGUI.Avalonia.csproj ├── JsInterop.cs ├── LocalAppLifetimeService.cs ├── LocalLogger.cs ├── Models │ └── FrpAvaloniaSourceGenerationContext.cs ├── RunningMode.cs ├── UIConfig.cs ├── ViewModels │ ├── FrpConfigViewModel.cs │ ├── FrpStatusInfo.cs │ ├── LogInfo.cs │ ├── LogViewModel.cs │ ├── MainViewModel.cs │ ├── RuleViewModel.cs │ ├── SettingViewModel.cs │ └── ViewModelBase.cs └── Views │ ├── ClientPanel.axaml │ ├── ClientPanel.axaml.cs │ ├── ConfigPanelBase.cs │ ├── ControlBar.axaml │ ├── ControlBar.axaml.cs │ ├── LogPanel.axaml │ ├── LogPanel.axaml.cs │ ├── MainView.axaml │ ├── MainView.axaml.cs │ ├── MainWindow.axaml │ ├── MainWindow.axaml.cs │ ├── ProgressRingOverlay.axaml │ ├── ProgressRingOverlay.axaml.cs │ ├── RuleDialog.axaml │ ├── RuleDialog.axaml.cs │ ├── ServerPanel.axaml │ ├── ServerPanel.axaml.cs │ ├── SettingsDialog.axaml │ └── SettingsDialog.axaml.cs ├── FrpGUI.Core ├── Enums │ ├── NetType.cs │ ├── ProcessStatus.cs │ └── TokenVerification.cs ├── FrpGUI.Core.csproj ├── Models │ ├── ClientConfig.cs │ ├── FrpConfigBase.cs │ ├── FrpConfigJsonConverter.cs │ ├── IFrpProcess.cs │ ├── IToFrpConfig.cs │ ├── LogEntity.cs │ ├── ProcessInfo.cs │ ├── Rule.cs │ └── ServerConfig.cs └── StatusBasedException.cs ├── FrpGUI.Service ├── Configs │ ├── AppConfig.cs │ ├── AppConfigBase.cs │ └── AppConfigSourceGenerationContext.cs ├── FrpGUI.Service.csproj ├── Models │ ├── FrpProcess.cs │ └── FrpProcessCollection.cs └── Services │ ├── AppLifetimeService.cs │ ├── Logger.cs │ └── ProcessService.cs ├── FrpGUI.SimpleWeb └── index.html ├── FrpGUI.Tests ├── FrpGUI.Tests.csproj ├── GlobalUsings.cs └── UnitTest.cs ├── FrpGUI.WPF ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── FrpGUI.WPF.csproj ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Panel │ ├── AddRulePanel.xaml │ ├── AddRulePanel.xaml.cs │ ├── ClientPanel.xaml │ ├── ClientPanel.xaml.cs │ ├── PanelBase.cs │ ├── RuleParameterEnableConverter.cs │ ├── ServerPanel.xaml │ └── ServerPanel.xaml.cs ├── app.manifest ├── icon.ico └── icon.svg ├── FrpGUI.WebAPI ├── Controllers │ ├── ConfigController.cs │ ├── FrpControllerBase.cs │ ├── LogController.cs │ ├── NeedTokenAttribute.cs │ ├── ProcessController.cs │ └── TokenController.cs ├── CreateWindowsService.bat ├── DeleteWindowsService.bat ├── FrpGUI.Service.http ├── FrpGUI.WebAPI.csproj ├── FrpGUIActionFilter.cs ├── Logger.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ └── WebConfigService.cs ├── WebAppLifetimeService.cs ├── appsettings.Development.json └── appsettings.json ├── FrpGUI.sln ├── README.md ├── build.ps1 ├── clean.ps1 ├── config.json └── img ├── browser.png ├── linux.png ├── structure.png ├── swagger.png ├── windows.png └── 软件架构图.vsdx /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3.0.0 5 | 6 | 7 | 11.2.6 8 | 9 | 2.3.0 10 | 11 | 12 | T 13 | 14 | 15 | 16 | 17 | 18 | 19 | CS0168,CS0169;MSB3539 20 | 21 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/FrpGUI.Avalonia.Browser.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0-browser 4 | Exe 5 | disable 6 | latest 7 | ../Generation/bin 8 | false 9 | true 10 | false 11 | false 12 | true 13 | $(AppVersion) 14 | $(Temp)\$(SolutionName)\$(Configuration)\$(AssemblyName) 15 | $(Temp)\$(SolutionName)\obj\$(Configuration)\$(AssemblyName) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Always 39 | 40 | 41 | Always 42 | 43 | 44 | Always 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Browser; 3 | using FrpGUI.Avalonia; 4 | using FrpGUI.Avalonia.Models; 5 | using System; 6 | using System.IO; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Runtime.InteropServices.JavaScript; 10 | using System.Runtime.Versioning; 11 | using System.Text.Json; 12 | using System.Threading.Tasks; 13 | 14 | [assembly: SupportedOSPlatform("browser")] 15 | 16 | internal sealed partial class Program 17 | { 18 | public static AppBuilder BuildAvaloniaApp() 19 | { 20 | return AppBuilder.Configure(); 21 | } 22 | 23 | private static async Task Main(string[] args) 24 | { 25 | await JSHost.ImportAsync("utils.js", "../utils.js"); 26 | try 27 | { 28 | await ProcessDefaultUIConfigAsync(); 29 | } 30 | catch (Exception ex) 31 | { 32 | JsInterop.Alert("获取默认UI配置失败:" + ex.Message); 33 | } 34 | await BuildAvaloniaApp().StartBrowserAppAsync("out"); 35 | } 36 | 37 | private static async Task ProcessDefaultUIConfigAsync() 38 | { 39 | UIConfig config = new UIConfig(); 40 | string url = $"{JsInterop.GetCurrentUrl().TrimEnd('/')}/{Path.GetFileName(config.ConfigPath)}"; 41 | 42 | HttpClient httpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) }; 43 | var response = await httpClient.GetAsync(url); 44 | if (response.StatusCode == HttpStatusCode.NotFound) 45 | { 46 | return; 47 | } 48 | var s = await response.Content.ReadAsStreamAsync(); 49 | UIConfig.DefaultConfig = JsonSerializer.Deserialize(s, FrpAvaloniaSourceGenerationContext.Default.UIConfig); 50 | } 51 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.Versioning.SupportedOSPlatform("browser")] -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "FrpGUI.Browser": { 4 | "commandName": "Project", 5 | "launchBrowser": false, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:7169", 10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "wasmHostProperties": { 3 | "perHostConfig": [ 4 | { 5 | "name": "browser", 6 | "host": "browser" 7 | } 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/uiconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "RunningMode": 1, 3 | "ServerAddress": "http://localhost:5113", 4 | "ServerToken": "" 5 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | /* HTML styles for the splash screen */ 2 | .avalonia-splash { 3 | position: absolute; 4 | height: 100%; 5 | width: 100%; 6 | background: white; 7 | font-family: 'Outfit', sans-serif; 8 | justify-content: center; 9 | align-items: center; 10 | display: flex; 11 | pointer-events: none; 12 | } 13 | 14 | /* Light theme styles */ 15 | @media (prefers-color-scheme: light) { 16 | .avalonia-splash { 17 | background: white; 18 | } 19 | 20 | .avalonia-splash h2 { 21 | color: #1b2a4e; 22 | } 23 | 24 | .avalonia-splash a { 25 | color: #0D6EFD; 26 | } 27 | } 28 | 29 | @media (prefers-color-scheme: dark) { 30 | .avalonia-splash { 31 | background: #1b2a4e; 32 | } 33 | 34 | .avalonia-splash h2 { 35 | color: white; 36 | } 37 | 38 | .avalonia-splash a { 39 | color: white; 40 | } 41 | } 42 | 43 | .avalonia-splash h2 { 44 | font-weight: 400; 45 | font-size: 1.5rem; 46 | } 47 | 48 | .avalonia-splash a { 49 | text-decoration: none; 50 | font-size: 2.5rem; 51 | display: block; 52 | } 53 | 54 | .avalonia-splash.splash-close { 55 | transition: opacity 200ms, display 200ms; 56 | display: none; 57 | opacity: 0; 58 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-shake/FrpGUI/17ad5ab9a92941bde1625b861b75f16ce6c858c2/FrpGUI.Avalonia.Browser/wwwroot/favicon.ico -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FrpGUI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 |

18 | FrpGUI 19 |

20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/wwwroot/main.js: -------------------------------------------------------------------------------- 1 | import { dotnet } from './_framework/dotnet.js' 2 | 3 | const is_browser = typeof window != "undefined"; 4 | if (!is_browser) throw new Error(`Expected to be running in a browser`); 5 | 6 | const dotnetRuntime = await dotnet 7 | .withDiagnosticTracing(false) 8 | .withApplicationArgumentsFromQuery() 9 | .create(); 10 | 11 | const config = dotnetRuntime.getConfig(); 12 | 13 | await dotnetRuntime.runMain(config.mainAssemblyName, [globalThis.location.href]); -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Browser/wwwroot/utils.js: -------------------------------------------------------------------------------- 1 | export function setLocalStorage(key, value) { 2 | localStorage.setItem(key, value); 3 | } 4 | 5 | export function getLocalStorage(key) { 6 | return localStorage.getItem(key); 7 | } 8 | 9 | export function showAlert(message) { 10 | alert(message); 11 | } 12 | 13 | export function reload() { 14 | location.reload(true) 15 | } 16 | 17 | export function getCurrentUrl() { 18 | return window.location.href 19 | } 20 | export function openUrl() { 21 | return window.open("url", "_blank") 22 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Desktop/FrpGUI.Avalonia.Desktop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | 6 | net8.0 7 | disable 8 | true 9 | app.manifest 10 | ../Generation/bin 11 | zh-CN 12 | false 13 | $(Temp)\$(SolutionName)\$(Configuration)\$(AssemblyName) 14 | $(Temp)\$(SolutionName)\obj\$(Configuration)\$(AssemblyName) 15 | ../FrpGUI.Avalonia/Assets/icon.ico 16 | $(AppVersion) 17 | true 18 | partial 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Desktop/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Serilog; 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace FrpGUI.Avalonia.Desktop; 8 | 9 | 10 | class Program 11 | { 12 | public static AppBuilder BuildAvaloniaApp() 13 | => AppBuilder.Configure() 14 | .LogToTrace() 15 | .UsePlatformDetect(); 16 | //.UseDesktopWebView(); 17 | 18 | [STAThread] 19 | public static void Main(string[] args) 20 | { 21 | Log.Logger = new LoggerConfiguration() 22 | .MinimumLevel.Debug() 23 | .WriteTo.File("logs/logs.txt", rollingInterval: RollingInterval.Day) 24 | .CreateLogger(); 25 | Log.Information("程序启动"); 26 | #if !DEBUG 27 | TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; 28 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 29 | 30 | try 31 | { 32 | #endif 33 | BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); 34 | #if !DEBUG 35 | } 36 | catch (Exception ex) 37 | { 38 | Log.Fatal(ex, "未捕获的主线程错误"); 39 | } 40 | finally 41 | { 42 | Log.CloseAndFlush(); 43 | } 44 | #endif 45 | } 46 | 47 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 48 | { 49 | Log.Fatal(e.ExceptionObject as Exception, "未捕获的AppDomain异常"); 50 | } 51 | 52 | private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) 53 | { 54 | Log.Fatal(e.Exception, "未捕获的TaskScheduler异常"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia.Desktop/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/App.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 63 | 64 | 65 | 66 | 67 | 71 | --> 72 | 73 | 74 | 75 | 76 | 77 | 78 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Assets/WRYH.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-shake/FrpGUI/17ad5ab9a92941bde1625b861b75f16ce6c858c2/FrpGUI.Avalonia/Assets/WRYH.ttf -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Assets/WRYH_Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-shake/FrpGUI/17ad5ab9a92941bde1625b861b75f16ce6c858c2/FrpGUI.Avalonia/Assets/WRYH_Bold.ttf -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-shake/FrpGUI/17ad5ab9a92941bde1625b861b75f16ce6c858c2/FrpGUI.Avalonia/Assets/icon.ico -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Brushes.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 42 | 46 | 50 | 54 | 58 | 59 | 60 | 61 | 64 | 67 | 70 | 73 | 74 | 78 | 82 | 86 | 90 | 91 | 92 | 93 | 96 | 99 | 102 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Converters/NullableValueConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Data; 3 | using Avalonia.Data.Converters; 4 | using System; 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | 8 | namespace FrpGUI.Avalonia.Converters 9 | { 10 | public class NullableValueConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | return value?.ToString(); 15 | } 16 | 17 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 18 | { 19 | if (Nullable.GetUnderlyingType(targetType) == null) 20 | { 21 | throw new ArgumentException("目标类型不可为空", nameof(targetType)); 22 | } 23 | if (value == null) 24 | { 25 | return null; 26 | } 27 | if (value is not string str) 28 | { 29 | throw new ArgumentException("值不是字符串", nameof(value)); 30 | } 31 | if (string.IsNullOrWhiteSpace(str)) 32 | { 33 | return null; 34 | } 35 | Type underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; 36 | try 37 | { 38 | return underlyingType switch 39 | { 40 | // 基本数值类型 41 | Type t when t == typeof(int) => int.Parse(str, culture), 42 | Type t when t == typeof(double) => double.Parse(str, culture), 43 | Type t when t == typeof(float) => float.Parse(str, culture), 44 | Type t when t == typeof(decimal) => decimal.Parse(str, culture), 45 | Type t when t == typeof(long) => long.Parse(str, culture), 46 | Type t when t == typeof(short) => short.Parse(str, culture), 47 | Type t when t == typeof(byte) => byte.Parse(str, culture), 48 | Type t when t == typeof(sbyte) => sbyte.Parse(str, culture), 49 | Type t when t == typeof(uint) => uint.Parse(str, culture), 50 | Type t when t == typeof(ulong) => ulong.Parse(str, culture), 51 | Type t when t == typeof(ushort) => ushort.Parse(str, culture), 52 | 53 | // 布尔类型 54 | Type t when t == typeof(bool) => bool.Parse(str), 55 | 56 | // 日期时间 57 | Type t when t == typeof(DateTime) => DateTime.Parse(str, culture), 58 | Type t when t == typeof(DateTimeOffset) => DateTimeOffset.Parse(str, culture), 59 | Type t when t == typeof(TimeSpan) => TimeSpan.Parse(str, culture), 60 | 61 | // 其他类型(如 Guid) 62 | Type t when t == typeof(Guid) => Guid.Parse(str), 63 | 64 | // 枚举类型(AOT 需要确保枚举被编译) 65 | Type t when t.IsEnum => Enum.Parse(t, str, ignoreCase: true), 66 | 67 | // 默认情况(可能不支持) 68 | _ => throw new NotSupportedException($"不支持转换到类型 {underlyingType.Name}") 69 | }; 70 | } 71 | catch (FormatException ex) 72 | { 73 | //return new BindingNotification(new ArgumentException($"无法将值 '{str}' 转换为类型 {underlyingType.Name}", nameof(targetType)),BindingErrorType.DataValidationError) 74 | throw new ArgumentException($"无法将值 '{str}' 转换为类型 {underlyingType.Name}", nameof(targetType)); 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Converters/ProcessStatus2BrushConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using Avalonia.Media; 3 | using FrpGUI.Enums; 4 | using System; 5 | using System.Globalization; 6 | 7 | namespace FrpGUI.Avalonia.Converters 8 | { 9 | public class ProcessStatus2BrushConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | if (value == null) 14 | { 15 | return null; 16 | } 17 | ProcessStatus status = (ProcessStatus)value; 18 | if (status == ProcessStatus.Stopped) 19 | { 20 | return Brushes.Red; 21 | } 22 | return Brushes.Green; 23 | } 24 | 25 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 26 | { 27 | throw new NotImplementedException(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Converters/RuleParameterEnableConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | 3 | using FrpGUI.Enums; 4 | using FrpGUI.Models; 5 | using System; 6 | using System.Globalization; 7 | 8 | namespace FrpGUI.Avalonia.Converters 9 | { 10 | public class RuleParameterEnableConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | NetType type = (NetType)value; 15 | return (parameter as string) switch 16 | { 17 | nameof(Rule.Domains) => type is NetType.HTTP or NetType.HTTPS, 18 | nameof(Rule.StcpKey) => type is NetType.STCP or NetType.STCP_Visitor, 19 | nameof(Rule.StcpServerName) => type is NetType.STCP_Visitor, 20 | nameof(Rule.RemotePort) => type is NetType.TCP or NetType.UDP, 21 | _ => throw new ArgumentException(), 22 | }; 23 | } 24 | 25 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 26 | { 27 | throw new NotImplementedException(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/DataProviders/HttpRequester.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization.Metadata; 9 | using System.Threading.Tasks; 10 | 11 | namespace FrpGUI.Avalonia.DataProviders 12 | { 13 | public class HttpRequester(UIConfig config) 14 | { 15 | private const string AuthorizationKey = "Authorization"; 16 | private readonly HttpClient httpClient = new HttpClient(); 17 | public string Token { get; private set; } 18 | protected string BaseApiUrl => config.ServerAddress; 19 | 20 | public void Dispose() 21 | { 22 | httpClient.Dispose(); 23 | } 24 | 25 | public async Task GetAsync(string endpoint) 26 | { 27 | WriteAuthorizationHeader(); 28 | var response = await httpClient.GetAsync($"{BaseApiUrl}/{endpoint}"); 29 | 30 | if (response.IsSuccessStatusCode) 31 | { 32 | return response.Content; 33 | } 34 | if (response.StatusCode == System.Net.HttpStatusCode.NotFound) 35 | { 36 | return null; 37 | } 38 | await ProcessError(response); 39 | throw new Exception(); 40 | } 41 | 42 | public Task GetObjectAsync(string endpoint, JsonTypeInfo jsonTypeInfo, params (string Key, string Value)[] query) where T : class 43 | { 44 | var querys = query.Select(p => $"{p.Key}={p.Value}"); 45 | return GetObjectAsync(endpoint + "?" + string.Join('&', querys), jsonTypeInfo); 46 | } 47 | 48 | public async Task GetObjectAsync(string endpoint, JsonTypeInfo jsonTypeInfo) 49 | { 50 | using var responseStream = await (await GetAsync(endpoint)).ReadAsStreamAsync(); 51 | return await JsonSerializer.DeserializeAsync(responseStream, jsonTypeInfo); 52 | } 53 | 54 | public async Task PostAsync(string endpoint, object data, JsonTypeInfo jsonTypeInfo) 55 | { 56 | WriteAuthorizationHeader(); 57 | ArgumentNullException.ThrowIfNullOrWhiteSpace(endpoint); 58 | ArgumentNullException.ThrowIfNull(data); 59 | ArgumentNullException.ThrowIfNull(jsonTypeInfo); 60 | var jsonContent = data == null ? null : new StringContent(JsonSerializer.Serialize(data, jsonTypeInfo), Encoding.UTF8, "application/json"); 61 | var response = await httpClient.PostAsync($"{BaseApiUrl}/{endpoint}", jsonContent); 62 | await ProcessError(response); 63 | } 64 | 65 | public async Task PostAsync(string endpoint) 66 | { 67 | WriteAuthorizationHeader(); 68 | var response = await httpClient.PostAsync($"{BaseApiUrl}/{endpoint}", null); 69 | await ProcessError(response); 70 | } 71 | 72 | public async Task PostAsync(string endpoint, JsonTypeInfo jsonResultTypeInfo, object data, JsonTypeInfo jsonDataTypeInfo) 73 | { 74 | WriteAuthorizationHeader(); 75 | ArgumentNullException.ThrowIfNullOrWhiteSpace(endpoint); 76 | ArgumentNullException.ThrowIfNull(data); 77 | ArgumentNullException.ThrowIfNull(jsonResultTypeInfo); 78 | ArgumentNullException.ThrowIfNull(jsonDataTypeInfo); 79 | var jsonContent = data == null ? null : new StringContent(JsonSerializer.Serialize(data, jsonResultTypeInfo), Encoding.UTF8, "application/json"); 80 | var response = await httpClient.PostAsync($"{BaseApiUrl}/{endpoint}", jsonContent); 81 | 82 | await ProcessError(response); 83 | if (response.Content.Headers.ContentLength == 0) 84 | { 85 | return default; 86 | } 87 | return await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), jsonResultTypeInfo); 88 | } 89 | 90 | public async Task PostAsync(string endpoint, JsonTypeInfo jsonResultTypeInfo) 91 | { 92 | WriteAuthorizationHeader(); 93 | ArgumentNullException.ThrowIfNullOrWhiteSpace(endpoint); 94 | ArgumentNullException.ThrowIfNull(jsonResultTypeInfo); 95 | var response = await httpClient.PostAsync($"{BaseApiUrl}/{endpoint}", null); 96 | 97 | await ProcessError(response); 98 | if (response.Content.Headers.ContentLength == 0) 99 | { 100 | return default; 101 | } 102 | return await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), jsonResultTypeInfo); 103 | } 104 | 105 | public void WriteAuthorizationHeader() 106 | { 107 | if (string.IsNullOrWhiteSpace(config.ServerToken)) 108 | { 109 | return; 110 | } 111 | if (httpClient.DefaultRequestHeaders.TryGetValues(AuthorizationKey, out IEnumerable values)) 112 | { 113 | var count = values.Count(); 114 | if (count >= 1) 115 | { 116 | if (values.First() == config.ServerToken) 117 | { 118 | return; 119 | } 120 | httpClient.DefaultRequestHeaders.Remove(AuthorizationKey); 121 | httpClient.DefaultRequestHeaders.Add(AuthorizationKey, config.ServerToken); 122 | } 123 | else 124 | { 125 | throw new Exception(); 126 | } 127 | } 128 | else 129 | { 130 | httpClient.DefaultRequestHeaders.Add(AuthorizationKey, config.ServerToken); 131 | } 132 | } 133 | protected static async Task ProcessError(HttpResponseMessage response) 134 | { 135 | if (!response.IsSuccessStatusCode) 136 | { 137 | if (response == null) 138 | { 139 | throw new Exception($"API请求失败({(int)response.StatusCode}{response.StatusCode})"); 140 | } 141 | var message = await response.Content.ReadAsStringAsync(); 142 | message = message.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)[0]; 143 | if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) 144 | { 145 | throw new Exception($"服务器处理错误(500):{Environment.NewLine}{message}"); 146 | } 147 | throw new Exception($"API请求失败({(int)response.StatusCode}{response.StatusCode}):{Environment.NewLine}{message}"); 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/DataProviders/IDataProvider.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.ViewModels; 2 | using FrpGUI.Enums; 3 | using FrpGUI.Models; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace FrpGUI.Avalonia.DataProviders 9 | { 10 | public interface IDataProvider 11 | { 12 | Task AddClientAsync(); 13 | 14 | Task AddServerAsync(); 15 | 16 | Task DeleteFrpConfigAsync(string id); 17 | Task> GetConfigsAsync(); 18 | 19 | Task GetFrpStatusAsync(string id); 20 | 21 | Task> GetFrpStatusesAsync(); 22 | 23 | Task> GetLogsAsync(DateTime timeAfter); 24 | 25 | Task> GetSystemProcesses(); 26 | 27 | Task KillProcess(int id); 28 | 29 | Task ModifyConfigAsync(FrpConfigBase config); 30 | 31 | Task RestartFrpAsync(string id); 32 | 33 | Task SetTokenAsync(string oldToken, string newToken); 34 | 35 | Task StartFrpAsync(string id); 36 | 37 | Task StopFrpAsync(string id); 38 | 39 | Task VerifyTokenAsync(); 40 | } 41 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/DataProviders/LocalDataProvider.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.ViewModels; 2 | using FrpGUI.Configs; 3 | using FrpGUI.Enums; 4 | using FrpGUI.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace FrpGUI.Avalonia.DataProviders 11 | { 12 | public class LocalDataProvider : IDataProvider 13 | { 14 | private readonly AppConfig configs; 15 | private readonly LocalLogger logger; 16 | private readonly FrpProcessCollection processes; 17 | 18 | public LocalDataProvider(AppConfig configs, FrpProcessCollection processes) 19 | { 20 | this.configs = configs; 21 | this.processes = processes; 22 | } 23 | 24 | public Task AddClientAsync() 25 | { 26 | ClientConfig client = new ClientConfig(); 27 | configs.FrpConfigs.Add(client); 28 | configs.Save(); 29 | return Task.FromResult(client); 30 | } 31 | 32 | public Task AddServerAsync() 33 | { 34 | ServerConfig server = new ServerConfig(); 35 | configs.FrpConfigs.Add(server); 36 | configs.Save(); 37 | 38 | return Task.FromResult(server); 39 | } 40 | 41 | public Task DeleteFrpConfigAsync(string id) 42 | { 43 | return processes.RemoveFrpAsync(id); 44 | } 45 | 46 | public Task> GetConfigsAsync() 47 | { 48 | return Task.FromResult(configs.FrpConfigs); 49 | } 50 | 51 | public Task GetFrpStatusAsync(string id) 52 | { 53 | return Task.FromResult(new FrpStatusInfo(processes.GetOrCreateProcess(id))); 54 | } 55 | 56 | public Task> GetFrpStatusesAsync() 57 | { 58 | return Task.FromResult(processes.GetAll().Select(p => new FrpStatusInfo(p)).ToList()); 59 | } 60 | 61 | public Task> GetLogsAsync(DateTime timeAfter) 62 | { 63 | throw new NotSupportedException(); 64 | } 65 | 66 | public Task> GetSystemProcesses() 67 | { 68 | return Task.FromResult(ProcessInfo.GetFrpProcesses()); 69 | } 70 | 71 | public Task KillProcess(int id) 72 | { 73 | ProcessInfo.KillProcess(id); 74 | return Task.CompletedTask; 75 | } 76 | 77 | public Task ModifyConfigAsync(FrpConfigBase config) 78 | { 79 | var p = processes.GetOrCreateProcess(config.ID); 80 | if (p.Config.GetType() != config.GetType()) 81 | { 82 | throw new ArgumentException("提供的配置与已有配置类型不同"); 83 | } 84 | config.Adapt(p.Config); 85 | configs.Save(); 86 | return Task.CompletedTask; 87 | } 88 | 89 | public Task RestartFrpAsync(string id) 90 | { 91 | configs.Save(); 92 | return processes.GetOrCreateProcess(id).RestartAsync(); 93 | } 94 | 95 | public Task SetTokenAsync(string oldToken, string newToken) 96 | { 97 | throw new NotImplementedException(); 98 | } 99 | 100 | public Task StartFrpAsync(string id) 101 | { 102 | configs.Save(); 103 | return processes.GetOrCreateProcess(id).StartAsync(); 104 | } 105 | 106 | public Task StopFrpAsync(string id) 107 | { 108 | return processes.GetOrCreateProcess(id).StopAsync(); 109 | } 110 | 111 | public Task VerifyTokenAsync() 112 | { 113 | throw new NotSupportedException(); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/DataProviders/WebDataProvider.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.Models; 2 | using FrpGUI.Avalonia.ViewModels; 3 | using FrpGUI.Enums; 4 | using FrpGUI.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace FrpGUI.Avalonia.DataProviders 13 | { 14 | public class WebDataProvider : HttpRequester, IDataProvider 15 | { 16 | private const string AddClientEndpoint = "Config/FrpConfigs/Add/Client"; 17 | private const string AddServerEndpoint = "Config/FrpConfigs/Add/Server"; 18 | private const string DeleteFrpConfigsEndpoint = "Config/FrpConfigs/Delete"; 19 | private const string ConfigsEndpoint = "Config/Configs"; 20 | private const string FrpStatusEndpoint = "Process/Status"; 21 | private const string KillProcessEndpoint = "Process/Kill"; 22 | private const string LogsEndpoint = "Log/List"; 23 | private const string ModifyConfigEndpoint = "Config/FrpConfigs/Modify"; 24 | private const string RestartFrpEndpoint = "Process/Restart"; 25 | private const string StartFrpEndpoint = "Process/Start"; 26 | private const string StopFrpEndpoint = "Process/Stop"; 27 | private const string SystemProcessesEndpoint = "Process/All"; 28 | private const string TokenEndpoint = "Token"; 29 | private readonly UIConfig config; 30 | private readonly LocalLogger logger; 31 | private PeriodicTimer timer; 32 | 33 | private List<(string Name, Func task)> timerTasks = new List<(string Name, Func task)>(); 34 | 35 | public WebDataProvider(UIConfig config, LocalLogger logger) : base(config) 36 | { 37 | this.config = config; 38 | this.logger = logger; 39 | StartTimer(); 40 | } 41 | 42 | public Task AddClientAsync() 43 | { 44 | return PostAsync(AddClientEndpoint, JContext.ClientConfig); 45 | } 46 | 47 | public Task AddServerAsync() 48 | { 49 | return PostAsync(AddServerEndpoint, JContext.ServerConfig); 50 | } 51 | 52 | public void AddTimerTask(string name, Func task) 53 | { 54 | ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); 55 | ArgumentNullException.ThrowIfNull(task, nameof(task)); 56 | timerTasks.Add((name, task)); 57 | } 58 | 59 | public Task DeleteFrpConfigAsync(string id) 60 | { 61 | return PostAsync($"{DeleteFrpConfigsEndpoint}/{id}"); 62 | } 63 | 64 | public Task> GetConfigsAsync() 65 | { 66 | return GetObjectAsync(ConfigsEndpoint, JContext.ListFrpConfigBase); 67 | } 68 | 69 | public Task GetFrpStatusAsync(string id) 70 | { 71 | return PostAsync($"{FrpStatusEndpoint}/{id}", JContext.FrpStatusInfo); 72 | } 73 | 74 | public async Task> GetFrpStatusesAsync() 75 | { 76 | var result = await GetObjectAsync(FrpStatusEndpoint, JContext.ListFrpStatusInfo); 77 | return result;//.Select(p => new FrpStatusInfo(p)).ToList(); 78 | } 79 | 80 | private FrpAvaloniaSourceGenerationContext JContext => FrpAvaloniaSourceGenerationContext.Get(); 81 | 82 | public Task> GetLogsAsync(DateTime timeAfter) 83 | { 84 | return GetObjectAsync(LogsEndpoint, JContext.ListLogEntity, ("timeAfter", timeAfter.ToString("yyyy-MM-ddTHH:mm:ss.fffffff"))); 85 | } 86 | 87 | public Task> GetSystemProcesses() 88 | { 89 | return GetObjectAsync(SystemProcessesEndpoint, JContext.ListProcessInfo); 90 | } 91 | 92 | public Task KillProcess(int id) 93 | { 94 | return PostAsync($"{KillProcessEndpoint}/{id}"); 95 | } 96 | 97 | public Task ModifyConfigAsync(FrpConfigBase config) 98 | { 99 | switch (config) 100 | { 101 | case ClientConfig c: 102 | return PostAsync(ModifyConfigEndpoint, config, JContext.ClientConfig); 103 | case ServerConfig s: 104 | return PostAsync(ModifyConfigEndpoint, config, JContext.ServerConfig); 105 | default: 106 | throw new ArgumentOutOfRangeException(); 107 | } 108 | } 109 | 110 | public Task RestartFrpAsync(string id) 111 | { 112 | return PostAsync($"{RestartFrpEndpoint}/{id}"); 113 | } 114 | 115 | public Task SetTokenAsync(string oldToken, string newToken) 116 | { 117 | return PostAsync($"{TokenEndpoint}?oldToken={WebUtility.UrlEncode(oldToken ?? "")}&newToken={WebUtility.UrlEncode(newToken)}", JContext.TokenVerification); 118 | } 119 | 120 | public Task StartFrpAsync(string id) 121 | { 122 | return PostAsync($"{StartFrpEndpoint}/{id}"); 123 | } 124 | 125 | public Task StopFrpAsync(string id) 126 | { 127 | return PostAsync($"{StopFrpEndpoint}/{id}"); 128 | } 129 | 130 | public Task VerifyTokenAsync() 131 | { 132 | return GetObjectAsync(TokenEndpoint, JContext.TokenVerification); 133 | } 134 | 135 | private async void StartTimer() 136 | { 137 | var timer = new PeriodicTimer(TimeSpan.FromSeconds(2)); 138 | while (await timer.WaitForNextTickAsync()) 139 | { 140 | foreach (var (name, task) in timerTasks.ToList()) 141 | { 142 | try 143 | { 144 | await task.Invoke(); 145 | } 146 | catch (Exception ex) 147 | { 148 | logger.Error($"执行定时任务“{name}”失败", null, ex); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/FrpGUI.Avalonia.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | disable 5 | latest 6 | ../Generation/bin 7 | false 8 | $(Temp)\$(SolutionName)\$(Configuration)\$(AssemblyName) 9 | $(Temp)\$(SolutionName)\obj\$(Configuration)\$(AssemblyName) 10 | true 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ..\..\FzLib\Publish\Release\net8.0\FzLib.dll 44 | 45 | 46 | ..\..\FzLib\Publish\Release\net8.0\FzLib.Avalonia.dll 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Designer 65 | 66 | 67 | 68 | 69 | 70 | LogPanel.axaml 71 | 72 | 73 | ControlBar.axaml 74 | 75 | 76 | ProgressRingOverlay.axaml 77 | 78 | 79 | SettingsDialog.axaml 80 | 81 | 82 | Code 83 | RuleDialog.axaml 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/JsInterop.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices.JavaScript; 2 | using System.Runtime.Versioning; 3 | 4 | namespace FrpGUI.Avalonia; 5 | 6 | [SupportedOSPlatform("browser")] 7 | public partial class JsInterop 8 | { 9 | [JSImport("showAlert", "utils.js")] 10 | public static partial string Alert(string message); 11 | 12 | [JSImport("getCurrentUrl", "utils.js")] 13 | public static partial string GetCurrentUrl(); 14 | 15 | [JSImport("getLocalStorage", "utils.js")] 16 | public static partial string GetLocalStorage(string key); 17 | 18 | [JSImport("openUrl", "utils.js")] 19 | public static partial void OpenUrl(string url); 20 | 21 | [JSImport("reload", "utils.js")] 22 | public static partial void Reload(); 23 | 24 | [JSImport("setLocalStorage", "utils.js")] 25 | public static partial void SetLocalStorage(string key, string value); 26 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/LocalAppLifetimeService.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using FrpGUI.Configs; 4 | using FrpGUI.Models; 5 | using FrpGUI.Services; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace FrpGUI.Avalonia; 10 | 11 | public partial class App 12 | { 13 | public class LocalAppLifetimeService(AppConfig config, UIConfig uiconfig, LoggerBase logger, FrpProcessCollection processes) 14 | : AppLifetimeService(config, logger, processes) 15 | { 16 | public override Task StopAsync(CancellationToken cancellationToken) 17 | { 18 | uiconfig.Save(); 19 | return base.StopAsync(cancellationToken); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/LocalLogger.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Models; 2 | using FrpGUI.Services; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | 7 | namespace FrpGUI.Avalonia 8 | { 9 | public class LocalLogger : LoggerBase 10 | { 11 | public ConcurrentBag savedLogs = new ConcurrentBag(); 12 | 13 | public event EventHandler NewLog; 14 | 15 | public bool SaveLogs { get; set; } = true; 16 | 17 | public LogEntity[] GetSavedLogs() => [.. savedLogs]; 18 | 19 | protected override void AddLog(LogEntity logEntity) 20 | { 21 | NewLog?.Invoke(this, new NewLogEventArgs(logEntity)); 22 | if (SaveLogs) 23 | { 24 | savedLogs.Add(logEntity); 25 | } 26 | } 27 | 28 | public class NewLogEventArgs : EventArgs 29 | { 30 | public NewLogEventArgs(LogEntity log) 31 | { 32 | ArgumentNullException.ThrowIfNull(log); 33 | Log = log; 34 | } 35 | 36 | public LogEntity Log { get; } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Models/FrpAvaloniaSourceGenerationContext.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.ViewModels; 2 | using FrpGUI.Configs; 3 | using FrpGUI.Enums; 4 | using FrpGUI.Models; 5 | using System.Collections.Generic; 6 | using System.Text.Encodings.Web; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using System.Text.Unicode; 10 | 11 | namespace FrpGUI.Avalonia.Models; 12 | 13 | [JsonSourceGenerationOptions(WriteIndented = true, PropertyNameCaseInsensitive = true)] 14 | [JsonSerializable(typeof(FrpStatusInfo))] 15 | [JsonSerializable(typeof(FrpProcess))] 16 | [JsonSerializable(typeof(UIConfig))] 17 | [JsonSerializable(typeof(LogEntity))] 18 | [JsonSerializable(typeof(TokenVerification))] 19 | [JsonSerializable(typeof(List))] 20 | [JsonSerializable(typeof(List))] 21 | [JsonSerializable(typeof(List))] 22 | [JsonSerializable(typeof(List))] 23 | [JsonSerializable(typeof(List))] 24 | [JsonSerializable(typeof(List))] 25 | [JsonSerializable(typeof(List))] 26 | public partial class FrpAvaloniaSourceGenerationContext : JsonSerializerContext 27 | { 28 | public static FrpAvaloniaSourceGenerationContext Get() 29 | { 30 | return new FrpAvaloniaSourceGenerationContext(new JsonSerializerOptions() 31 | { 32 | WriteIndented = true, 33 | PropertyNameCaseInsensitive = true, 34 | Converters = { new FrpConfigJsonConverter() } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/RunningMode.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace FrpGUI.Avalonia; 4 | 5 | public enum RunningMode 6 | { 7 | [Description("单机模式")] 8 | Singleton, 9 | 10 | [Description("服务模式")] 11 | Service 12 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/UIConfig.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.Models; 2 | using FrpGUI.Configs; 3 | using FzLib; 4 | using System; 5 | using System.ComponentModel; 6 | using System.IO; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using CommunityToolkit.Mvvm.ComponentModel; 10 | using System.Text.Json.Serialization.Metadata; 11 | 12 | namespace FrpGUI.Avalonia; 13 | 14 | public class UIConfig : AppConfigBase, INotifyPropertyChanged 15 | { 16 | private RunningMode runningMode; 17 | 18 | private bool showTrayIcon; 19 | 20 | public UIConfig() : base() 21 | { 22 | } 23 | 24 | public event PropertyChangedEventHandler PropertyChanged; 25 | 26 | public static UIConfig DefaultConfig { get; set; } 27 | 28 | [JsonIgnore] 29 | public override string ConfigPath => Path.Combine(AppContext.BaseDirectory, "uiconfig.json"); 30 | 31 | public RunningMode RunningMode 32 | { 33 | get => runningMode; 34 | set 35 | { 36 | runningMode = value; 37 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RunningMode))); 38 | } 39 | } 40 | public string ServerAddress { get; set; } = "http://localhost:5113"; 41 | 42 | public string ServerToken { get; set; } = ""; 43 | 44 | public bool ShowTrayIcon 45 | { 46 | get => showTrayIcon; 47 | set 48 | { 49 | showTrayIcon = value; 50 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowTrayIcon))); 51 | } 52 | } 53 | 54 | private static JsonTypeInfo JsonTypeInfo { get; } = FrpAvaloniaSourceGenerationContext.Get().UIConfig; 55 | 56 | public static UIConfig Get() 57 | { 58 | return Get(JsonTypeInfo); 59 | } 60 | 61 | public void Save() 62 | { 63 | if (OperatingSystem.IsBrowser()) 64 | { 65 | var json = JsonSerializer.Serialize(this, JsonTypeInfo); 66 | JsInterop.SetLocalStorage("config", json); 67 | } 68 | else 69 | { 70 | Save(JsonTypeInfo); 71 | } 72 | } 73 | 74 | protected override T GetImpl(JsonTypeInfo jsonTypeInfo) 75 | { 76 | if (OperatingSystem.IsBrowser()) 77 | { 78 | try 79 | { 80 | var json = JsInterop.GetLocalStorage("config"); 81 | if (string.IsNullOrEmpty(json)) 82 | { 83 | if (DefaultConfig != null) 84 | { 85 | return DefaultConfig as T; //优先级2:默认配置。由于HttpClient不支持同步,所以DefaultConfig在Browser项目中进行了赋值 86 | } 87 | 88 | return new UIConfig() as T; //优先级3:新配置 89 | } 90 | 91 | //优先级1:LocalStorage配置 92 | return JsonSerializer.Deserialize(JsInterop.GetLocalStorage("config"), JsonTypeInfo) as T; 93 | } 94 | catch (Exception ex) 95 | { 96 | JsInterop.Alert("读取配置文件错误:" + ex.ToString()); 97 | throw; 98 | } 99 | } 100 | else 101 | { 102 | return base.GetImpl(jsonTypeInfo); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/FrpConfigViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using CommunityToolkit.Mvvm.Input; 3 | using FrpGUI.Avalonia.DataProviders; 4 | using FrpGUI.Avalonia.Views; 5 | 6 | using FrpGUI.Models; 7 | using FzLib.Avalonia.Messages; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Collections.ObjectModel; 11 | using System.Threading.Tasks; 12 | 13 | namespace FrpGUI.Avalonia.ViewModels; 14 | 15 | public partial class FrpConfigViewModel(IDataProvider provider, IServiceProvider services) : ViewModelBase(provider) 16 | { 17 | [ObservableProperty] 18 | private IFrpProcess frp; 19 | 20 | [ObservableProperty] 21 | private ObservableCollection rules; 22 | 23 | public async Task AddRuleAsync() 24 | { 25 | var dialog = services.GetRequiredService(); 26 | var message = SendMessage(new DialogHostMessage(dialog)); 27 | var result = await message.Task; 28 | if (result is Rule newRule) 29 | { 30 | Rules.Add(newRule); 31 | } 32 | } 33 | 34 | public void LoadConfig(IFrpProcess frp) 35 | { 36 | Frp = frp; 37 | if (frp?.Config is ClientConfig cc) 38 | { 39 | Rules = new ObservableCollection(cc.Rules); 40 | Rules.CollectionChanged += (s, e) => cc.Rules = [.. Rules]; 41 | } 42 | } 43 | 44 | [RelayCommand] 45 | private void DisableRule(Rule rule) 46 | { 47 | rule.Enable = false; 48 | } 49 | 50 | [RelayCommand] 51 | private void EnableRule(Rule rule) 52 | { 53 | rule.Enable = true; 54 | } 55 | 56 | [RelayCommand] 57 | private async Task ModifyRuleAsync(Rule rule) 58 | { 59 | var dialog = services.GetRequiredService(); 60 | dialog.SetRule(rule); 61 | var message = SendMessage(new DialogHostMessage(dialog)); 62 | var result = await message.Task; 63 | if (result is Rule newRule) 64 | { 65 | Rules[Rules.IndexOf(rule)] = newRule; 66 | } 67 | } 68 | 69 | [RelayCommand] 70 | private void RemoveRule(Rule rule) 71 | { 72 | Rules.Remove(rule); 73 | } 74 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/FrpStatusInfo.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | using FrpGUI.Enums; 4 | using FrpGUI.Models; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace FrpGUI.Avalonia.ViewModels; 9 | 10 | public partial class FrpStatusInfo : ObservableObject, IFrpProcess 11 | { 12 | private FrpConfigBase config; 13 | 14 | private ProcessStatus processStatus; 15 | 16 | public FrpStatusInfo() 17 | { 18 | } 19 | 20 | public FrpStatusInfo(FrpConfigBase config) 21 | { 22 | Config = config; 23 | } 24 | 25 | public FrpStatusInfo(IFrpProcess fp) 26 | { 27 | Config = fp.Config; 28 | ProcessStatus = fp.ProcessStatus; 29 | fp.StatusChanged += (s, e) => 30 | { 31 | ProcessStatus = (s as IFrpProcess).ProcessStatus; 32 | StatusChanged?.Invoke(s, e); 33 | }; 34 | } 35 | 36 | public event EventHandler StatusChanged; 37 | 38 | public FrpConfigBase Config 39 | { 40 | get => config; 41 | set => SetProperty(ref config, value, nameof(Config)); 42 | } 43 | 44 | public ProcessStatus ProcessStatus 45 | { 46 | get => processStatus; 47 | set => SetProperty(ref processStatus, value, nameof(ProcessStatus)); 48 | } 49 | 50 | public Task RestartAsync() 51 | { 52 | throw new NotImplementedException(); 53 | } 54 | 55 | public Task StartAsync() 56 | { 57 | throw new NotImplementedException(); 58 | } 59 | 60 | public Task StopAsync() 61 | { 62 | throw new NotImplementedException(); 63 | } 64 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/LogInfo.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using FrpGUI.Models; 4 | using System; 5 | using System.Diagnostics; 6 | 7 | namespace FrpGUI.Avalonia.ViewModels; 8 | 9 | [DebuggerDisplay("{Message}")] 10 | public partial class LogInfo(LogEntity e) : ObservableObject 11 | { 12 | [ObservableProperty] 13 | [NotifyPropertyChangedFor(nameof(HasUpdated))] 14 | public int updateTimes; 15 | 16 | [ObservableProperty] 17 | private bool fromFrp = e.FromFrp; 18 | 19 | [ObservableProperty] 20 | private string instanceName = e.InstanceName; 21 | 22 | [ObservableProperty] 23 | private string message = e.Message; 24 | 25 | [ObservableProperty] 26 | private DateTime time = e.Time; 27 | 28 | [ObservableProperty] 29 | private char type = e.Type; 30 | 31 | [ObservableProperty] 32 | private IBrush typeBrush; 33 | 34 | public bool HasUpdated => UpdateTimes > 0; 35 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/LogViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using CommunityToolkit.Mvvm.Input; 4 | using FrpGUI.Avalonia.DataProviders; 5 | using FrpGUI.Models; 6 | using FzLib.Avalonia.Messages; 7 | using System; 8 | using System.Collections.ObjectModel; 9 | 10 | namespace FrpGUI.Avalonia.ViewModels; 11 | 12 | public partial class LogViewModel : ViewModelBase 13 | { 14 | private readonly UIConfig config; 15 | private readonly LocalLogger logger; 16 | 17 | [ObservableProperty] 18 | private LogInfo selectedLog; 19 | 20 | public LogViewModel(IDataProvider provider, UIConfig config, LocalLogger logger) : base(provider) 21 | { 22 | this.config = config; 23 | this.logger = logger; 24 | StartTimer(); 25 | } 26 | 27 | public ObservableCollection Logs { get; } = new ObservableCollection(); 28 | 29 | public void AddLog(LogEntity e) 30 | { 31 | try 32 | { 33 | //不知道为什么,加了内置浏览器后,Logs会报莫名其妙的错误,有时候Logs最后一个是null, 34 | //但是CollectionChanged里也不触发。所以加了最后一个null的判断以及try-catch 35 | while (Logs.Count > 0 && Logs[^1] == null) 36 | { 37 | Logs.RemoveAt(Logs.Count - 1); 38 | } 39 | IBrush brush = Brushes.Transparent; 40 | if (e.Type == 'W') 41 | { 42 | brush = Brushes.Orange; 43 | } 44 | else if (e.Type == 'E') 45 | { 46 | brush = Brushes.Red; 47 | } 48 | 49 | if (Logs.Count >= 2) 50 | { 51 | for (int i = 1; i <= 2; i++) 52 | { 53 | if (Logs[^i].Message == e.Message) 54 | { 55 | Logs[^i].UpdateTimes++; 56 | return; 57 | } 58 | } 59 | } 60 | var log = new LogInfo(e) 61 | { 62 | TypeBrush = brush, 63 | }; 64 | 65 | Logs.Add(log); 66 | SelectedLog = log; 67 | } 68 | catch (Exception ex) 69 | { 70 | 71 | } 72 | } 73 | 74 | [RelayCommand] 75 | private void CopyLog(LogInfo log) 76 | { 77 | SendMessage(new GetClipboardMessage()).Clipboard.SetTextAsync(log.Message); 78 | } 79 | 80 | private void StartTimer() 81 | { 82 | if (DataProvider is WebDataProvider webDataProvider) 83 | { 84 | DateTime lastRequestTime = DateTime.MinValue; 85 | webDataProvider.AddTimerTask("获取日志", async () => 86 | { 87 | var logs = await DataProvider.GetLogsAsync(lastRequestTime); 88 | if (logs.Count > 0) 89 | { 90 | lastRequestTime = logs[^1].Time; 91 | foreach (var log in logs) 92 | { 93 | AddLog(log); 94 | } 95 | } 96 | }); 97 | } 98 | logger.NewLog += (s, e) => AddLog(e.Log); 99 | logger.SaveLogs = false; 100 | foreach (var log in logger.GetSavedLogs()) 101 | { 102 | AddLog(log); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/RuleViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using FrpGUI.Avalonia.DataProviders; 3 | using FrpGUI.Enums; 4 | using FrpGUI.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace FrpGUI.Avalonia.ViewModels; 10 | 11 | public partial class RuleViewModel(IDataProvider provider) : ViewModelBase(provider) 12 | { 13 | [ObservableProperty] 14 | public Rule rule = new Rule(); 15 | 16 | [ObservableProperty] 17 | public string errorMessage; 18 | 19 | private ushort[] GetPorts(string port) 20 | { 21 | if (ushort.TryParse(port, out ushort result)) 22 | { 23 | return [result]; 24 | } 25 | HashSet ports = new HashSet(); 26 | foreach (var part in port.Split(',')) 27 | { 28 | if (ushort.TryParse(part, out ushort r)) 29 | { 30 | Add(r); 31 | } 32 | var range = part.Split('-'); 33 | if (range.Length != 2) 34 | { 35 | T("范围数量错误"); 36 | } 37 | if (ushort.TryParse(range[0], out ushort from)) 38 | { 39 | if (ushort.TryParse(range[1], out ushort to)) 40 | { 41 | if (from >= to) 42 | { 43 | T("范围起始大于结束"); 44 | } 45 | for (ushort i = from; i <= to; i++) 46 | { 47 | Add(i); 48 | } 49 | } 50 | else 51 | { 52 | T("范围解析错误"); 53 | } 54 | } 55 | else 56 | { 57 | T("范围解析错误"); 58 | } 59 | } 60 | return ports.ToArray(); 61 | void Add(ushort p) 62 | { 63 | if (ports.Contains(p)) 64 | { 65 | if (!ports.Add(p)) 66 | { 67 | T("端口号重复:" + p); 68 | } 69 | } 70 | } 71 | } 72 | 73 | private void T(string message) 74 | { 75 | throw new ArgumentException(message); 76 | } 77 | 78 | public bool Check() 79 | { 80 | try 81 | { 82 | if (Rule.Name.Length == 0) T("名称为空"); 83 | if (Rule.Name.Length > 10) T("名称长度不可超过10"); 84 | if (Rule.LocalPort.Length == 0) T("本地端口为空"); 85 | if (Rule.LocalAddress.Length == 0) T("本地地址为空"); 86 | 87 | ushort[] localPort = null; 88 | ushort[] remotePort = null; 89 | switch (Rule.Type) 90 | { 91 | case NetType.TCP: 92 | case NetType.UDP: 93 | if (Rule.RemotePort.Length == 0) T("远程端口为空"); 94 | try 95 | { 96 | localPort = GetPorts(Rule.LocalPort); 97 | } 98 | catch (FormatException ex) 99 | { 100 | T("本地端口" + ex.Message); 101 | } 102 | try 103 | { 104 | remotePort = GetPorts(Rule.RemotePort); 105 | } 106 | catch (FormatException ex) 107 | { 108 | T("远程端口" + ex.Message); 109 | } 110 | if (localPort.Length != remotePort.Length) 111 | { 112 | T("本地端口和远程端口数量不同"); 113 | } 114 | break; 115 | 116 | case NetType.HTTP: 117 | case NetType.HTTPS: 118 | case NetType.STCP: 119 | case NetType.STCP_Visitor: 120 | break; 121 | } 122 | if (Rule.Type is NetType.STCP or NetType.STCP_Visitor && string.IsNullOrWhiteSpace(Rule.StcpKey)) T("STCP密钥为空"); 123 | if (Rule.Type is NetType.STCP_Visitor && string.IsNullOrWhiteSpace(Rule.StcpServerName)) T("STCP服务名为空"); 124 | 125 | //暂不考虑端口不对应 126 | 127 | return true; 128 | } 129 | catch (Exception ex) 130 | { 131 | ErrorMessage = ex.Message; 132 | return false; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/SettingViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using CommunityToolkit.Mvvm.Input; 3 | using FrpGUI.Avalonia.DataProviders; 4 | using FrpGUI.Models; 5 | using FzLib.Avalonia.Messages; 6 | 7 | using System; 8 | using System.Collections.ObjectModel; 9 | using System.Diagnostics; 10 | using System.Threading.Tasks; 11 | using FzLib.Program.Startup; 12 | using Microsoft.Extensions.DependencyInjection; 13 | 14 | namespace FrpGUI.Avalonia.ViewModels 15 | { 16 | public partial class SettingViewModel : ViewModelBase 17 | { 18 | [ObservableProperty] 19 | private string newToken; 20 | 21 | [ObservableProperty] 22 | private string oldToken; 23 | 24 | [ObservableProperty] 25 | private ObservableCollection processes; 26 | 27 | [ObservableProperty] 28 | private string serverAddress; 29 | 30 | [ObservableProperty] 31 | private bool startup; 32 | 33 | [ObservableProperty] 34 | private string token; 35 | public SettingViewModel(IDataProvider provider, UIConfig config) : base(provider) 36 | { 37 | if (!OperatingSystem.IsBrowser()) 38 | { 39 | startup = App.Services.GetRequiredService().IsStartupEnabled(); 40 | } 41 | Config = config; 42 | ServerAddress = config.ServerAddress; 43 | FillProcesses(); 44 | Config.PropertyChanged += (s, e) => 45 | { 46 | if (e.PropertyName == nameof(Config.RunningMode) && Config.RunningMode == RunningMode.Service) 47 | { 48 | Startup = false; 49 | } 50 | }; 51 | } 52 | 53 | public UIConfig Config { get; } 54 | 55 | private async void FillProcesses() 56 | { 57 | try 58 | { 59 | Processes = new ObservableCollection(await DataProvider.GetSystemProcesses()); 60 | } 61 | catch (Exception ex) 62 | { } 63 | } 64 | 65 | [RelayCommand] 66 | private async Task KillProcessAsync(ProcessInfo p) 67 | { 68 | Debug.Assert(p != null); 69 | try 70 | { 71 | await DataProvider.KillProcess(p.Id); 72 | Processes.Remove(p); 73 | } 74 | catch (Exception ex) 75 | { 76 | await SendMessage(new CommonDialogMessage() 77 | { 78 | Type = CommonDialogMessage.CommonDialogType.Error, 79 | Title = "结束进程失败", 80 | Exception = ex, 81 | }).Task; 82 | } 83 | } 84 | 85 | partial void OnStartupChanged(bool value) 86 | { 87 | if (!OperatingSystem.IsBrowser()) 88 | { 89 | var startupManager = App.Services.GetRequiredService(); 90 | 91 | if (value) 92 | { 93 | startupManager.EnableStartup("s"); 94 | Config.ShowTrayIcon = true; 95 | } 96 | else 97 | { 98 | startupManager.DisableStartup(); 99 | } 100 | } 101 | } 102 | [RelayCommand] 103 | private async Task RestartAsync() 104 | { 105 | Config.ServerAddress = ServerAddress; 106 | if (!string.IsNullOrEmpty(Token)) 107 | { 108 | Config.ServerToken = Token; 109 | } 110 | Config.Save(); 111 | 112 | if (OperatingSystem.IsBrowser()) 113 | { 114 | JsInterop.Reload(); 115 | } 116 | else 117 | { 118 | string exePath = Environment.ProcessPath; 119 | Process.Start(new ProcessStartInfo(exePath) 120 | { 121 | UseShellExecute = true 122 | }); 123 | await (App.Current as App).ShutdownAsync(); 124 | } 125 | } 126 | 127 | [RelayCommand] 128 | private async Task SetTokenAsync() 129 | { 130 | try 131 | { 132 | Config.ServerAddress = ServerAddress; 133 | await DataProvider.SetTokenAsync(OldToken, NewToken); 134 | Config.ServerToken = NewToken; 135 | Config.Save(); 136 | await SendMessage(new CommonDialogMessage() 137 | { 138 | Type = CommonDialogMessage.CommonDialogType.Ok, 139 | Title = "修改密码", 140 | Message = "修改密码成功" 141 | }).Task; 142 | } 143 | catch (Exception ex) 144 | { 145 | await ShowErrorAsync(ex, "修改密码失败"); 146 | } 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using CommunityToolkit.Mvvm.Messaging; 3 | using FrpGUI.Avalonia.DataProviders; 4 | using FzLib.Avalonia.Messages; 5 | using System; 6 | using System.Threading.Tasks; 7 | using static CommunityToolkit.Mvvm.Messaging.IMessengerExtensions; 8 | using static FzLib.Avalonia.Messages.CommonDialogMessage; 9 | 10 | namespace FrpGUI.Avalonia.ViewModels; 11 | 12 | public class ViewModelBase : ObservableObject 13 | { 14 | public ViewModelBase(IDataProvider provider) 15 | { 16 | DataProvider = provider; 17 | } 18 | 19 | protected IDataProvider DataProvider { get; } 20 | 21 | protected TMessage SendMessage(TMessage message) where TMessage : class 22 | { 23 | return WeakReferenceMessenger.Default.Send(message); 24 | } 25 | 26 | protected Task ShowErrorAsync(Exception ex, string title) 27 | { 28 | return SendMessage(new CommonDialogMessage() 29 | { 30 | Type = CommonDialogType.Error, 31 | Title = title, 32 | Exception = ex 33 | }).Task; 34 | } 35 | 36 | protected Task ShowErrorAsync(string message, string title) 37 | { 38 | return SendMessage(new CommonDialogMessage() 39 | { 40 | Type = CommonDialogType.Error, 41 | Title = title, 42 | Message = message 43 | }).Task; 44 | } 45 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ClientPanel.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace FrpGUI.Avalonia.Views; 4 | 5 | public partial class ClientPanel : ConfigPanelBase 6 | { 7 | public ClientPanel() 8 | { 9 | InitializeComponent(); 10 | } 11 | 12 | private void PanelBase_SizeChanged(object sender, SizeChangedEventArgs e) 13 | { 14 | Resources["RuleWidth"] = lstRules.Bounds.Width switch 15 | { 16 | < 840 => lstRules.Bounds.Width - 0, 17 | _ => 420d 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ConfigPanelBase.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace FrpGUI.Avalonia.Views 4 | { 5 | public class ConfigPanelBase : UserControl 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ControlBar.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using FzLib.Avalonia.Controls; 4 | 5 | namespace FrpGUI.Avalonia.Views; 6 | 7 | public partial class ControlBar : UserControl 8 | { 9 | //private MainViewModel viewModel; 10 | 11 | public ControlBar() 12 | { 13 | InitializeComponent(); 14 | } 15 | 16 | protected override void OnLoaded(RoutedEventArgs e) 17 | { 18 | base.OnLoaded(e); 19 | if (TopLevel.GetTopLevel(this) is Window) 20 | { 21 | new WindowDragHelper(thumb).EnableDrag(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/LogPanel.axaml: -------------------------------------------------------------------------------- 1 | 13 | 21 | 22 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 56 | 60 | 61 | 69 | 77 | 87 | 88 | 89 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/LogPanel.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Markup.Xaml; 3 | using FrpGUI.Avalonia.ViewModels; 4 | 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FrpGUI.Avalonia.Views; 8 | 9 | public partial class LogPanel : UserControl 10 | { 11 | public LogPanel() 12 | { 13 | DataContext = App.Services.GetRequiredService(); 14 | InitializeComponent(); 15 | } 16 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/MainView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Interactivity; 4 | using CommunityToolkit.Mvvm.Messaging; 5 | using FrpGUI.Avalonia.ViewModels; 6 | 7 | using FrpGUI.Models; 8 | using FzLib.Avalonia.Controls; 9 | using FzLib.Avalonia.Dialogs; 10 | using FzLib.Avalonia.Messages; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using System; 13 | 14 | namespace FrpGUI.Avalonia.Views; 15 | 16 | public partial class MainView : UserControl 17 | { 18 | public MainView() 19 | { 20 | DataContext = App.Services.GetRequiredService(); 21 | InitializeComponent(); 22 | RegisterMessages(); 23 | } 24 | 25 | protected override void OnLoaded(RoutedEventArgs e) 26 | { 27 | base.OnLoaded(e); 28 | if (TopLevel.GetTopLevel(this) is Window) 29 | { 30 | //new WindowDragHelper(controlBar).EnableDrag(); 31 | new WindowDragHelper(tbkLogo).EnableDrag(); 32 | } 33 | } 34 | 35 | private void RegisterDialogHostMessage() 36 | { 37 | WeakReferenceMessenger.Default.Register(this, async delegate (object _, DialogHostMessage m) 38 | { 39 | try 40 | { 41 | m.SetResult(await m.Dialog.ShowDialog(DialogContainerType.WindowPreferred, TopLevel.GetTopLevel(this))); 42 | } 43 | catch (Exception exception) 44 | { 45 | m.SetException(exception); 46 | } 47 | }); 48 | } 49 | private void RegisterMessages() 50 | { 51 | this.RegisterCommonDialogMessage(); 52 | RegisterDialogHostMessage(); 53 | this.RegisterGetClipboardMessage(); 54 | this.RegisterGetStorageProviderMessage(); 55 | this.RegisterInputDialogMessage(); 56 | } 57 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using FrpGUI.Avalonia.ViewModels; 4 | using FrpGUI.Enums; 5 | using FrpGUI.Models; 6 | using FzLib.Avalonia.Controls; 7 | using FzLib.Avalonia.Dialogs; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Diagnostics; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace FrpGUI.Avalonia.Views; 15 | 16 | public partial class MainWindow : ExtendedWindow 17 | { 18 | private readonly UIConfig config=App.Services.GetRequiredService(); 19 | 20 | private readonly FrpProcessCollection processes= App.Services.GetService(); 21 | 22 | private bool forceClose = false; 23 | 24 | public MainWindow() 25 | { 26 | InitializeComponent(); 27 | if (OperatingSystem.IsWindows()) 28 | { 29 | grid.Children.Add(new WindowButtons()); 30 | } 31 | 32 | } 33 | 34 | public async Task TryCloseAsync() 35 | { 36 | if (config.RunningMode == RunningMode.Service) 37 | { 38 | forceClose = true; 39 | Close(); 40 | } 41 | 42 | if (processes != null && processes.Any(p => p.Value.ProcessStatus == ProcessStatus.Running)) 43 | { 44 | if (!IsVisible) 45 | { 46 | Show(); 47 | } 48 | var runningFrps = processes.Where(p => p.Value.ProcessStatus == ProcessStatus.Running).ToList(); 49 | if (await this.ShowYesNoDialogAsync("退出", $"存在{runningFrps.Count}个正在运行的frp进程,是否退出?") == true) 50 | { 51 | foreach (var frp in runningFrps) 52 | { 53 | await frp.Value.StopAsync(); 54 | } 55 | forceClose = true; 56 | Close(); 57 | } 58 | } 59 | else 60 | { 61 | forceClose = true; 62 | Close(); 63 | } 64 | } 65 | 66 | protected override async void OnClosing(WindowClosingEventArgs e) 67 | { 68 | if (forceClose) 69 | { 70 | return; 71 | } 72 | if (config.ShowTrayIcon) 73 | { 74 | e.Cancel = true; 75 | Hide(); 76 | } 77 | else 78 | { 79 | await TryCloseAsync(); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ProgressRingOverlay.axaml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ProgressRingOverlay.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | 4 | namespace FrpGUI.Avalonia.Views; 5 | 6 | public partial class ProgressRingOverlay : UserControl 7 | { 8 | public static readonly StyledProperty IsActiveProperty = 9 | AvaloniaProperty.Register(nameof(IsActive)); 10 | 11 | public ProgressRingOverlay() 12 | { 13 | InitializeComponent(); 14 | } 15 | 16 | public bool IsActive 17 | { 18 | get => GetValue(IsActiveProperty); 19 | set => SetValue(IsActiveProperty, value); 20 | } 21 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/RuleDialog.axaml: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 56 | 57 | 58 | 61 | 62 | 63 | 66 | 67 | 68 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 82 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/RuleDialog.axaml.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.ViewModels; 2 | 3 | using FrpGUI.Models; 4 | using FzLib.Avalonia.Dialogs; 5 | 6 | namespace FrpGUI.Avalonia.Views; 7 | 8 | public partial class RuleDialog : DialogHost 9 | { 10 | public RuleDialog() 11 | { 12 | InitializeComponent(); 13 | } 14 | 15 | public void SetRule(Rule rule) 16 | { 17 | (DataContext as RuleViewModel).Rule = rule.Clone() as Rule; 18 | } 19 | 20 | protected override void OnCloseButtonClick() 21 | { 22 | Close(); 23 | } 24 | 25 | protected override void OnPrimaryButtonClick() 26 | { 27 | var vm = DataContext as RuleViewModel; 28 | if (vm.Check()) 29 | { 30 | Close(vm.Rule); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ServerPanel.axaml: -------------------------------------------------------------------------------- 1 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/ServerPanel.axaml.cs: -------------------------------------------------------------------------------- 1 | namespace FrpGUI.Avalonia.Views; 2 | 3 | public partial class ServerPanel : ConfigPanelBase 4 | { 5 | public ServerPanel() 6 | { 7 | InitializeComponent(); 8 | } 9 | } -------------------------------------------------------------------------------- /FrpGUI.Avalonia/Views/SettingsDialog.axaml.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Avalonia.ViewModels; 2 | 3 | using FzLib.Avalonia.Dialogs; 4 | 5 | namespace FrpGUI.Avalonia.Views; 6 | 7 | public partial class SettingsDialog : DialogHost 8 | { 9 | public SettingsDialog() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | protected override void OnCloseButtonClick() 15 | { 16 | Close(); 17 | } 18 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Enums/NetType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace FrpGUI.Enums 4 | { 5 | public enum NetType 6 | { 7 | TCP, 8 | UDP, 9 | HTTP, 10 | HTTPS, 11 | STCP, 12 | 13 | [Description("STCP访问者")] 14 | STCP_Visitor, 15 | 16 | //XTCP 17 | } 18 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Enums/ProcessStatus.cs: -------------------------------------------------------------------------------- 1 | namespace FrpGUI.Enums 2 | { 3 | public enum ProcessStatus 4 | { 5 | Stopped, 6 | Running, 7 | Busy 8 | } 9 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Enums/TokenVerification.cs: -------------------------------------------------------------------------------- 1 | namespace FrpGUI.Enums 2 | { 3 | public enum TokenVerification 4 | { 5 | OK, 6 | NotEqual, 7 | NeedSet 8 | } 9 | } -------------------------------------------------------------------------------- /FrpGUI.Core/FrpGUI.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | disable 7 | ../Generation/bin 8 | false 9 | $(Temp)\$(SolutionName)\$(Configuration)\$(AssemblyName) 10 | $(Temp)\$(SolutionName)\obj\$(Configuration)\$(AssemblyName) 11 | FzLib 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FrpGUI.Core/Models/ClientConfig.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using System.Text; 3 | 4 | namespace FrpGUI.Models 5 | { 6 | public partial class ClientConfig : FrpConfigBase 7 | { 8 | [ObservableProperty] 9 | private bool enableTls; 10 | 11 | [ObservableProperty] 12 | private bool loginFailExit = false; 13 | 14 | [ObservableProperty] 15 | private short poolCount = 50; 16 | 17 | [ObservableProperty] 18 | private List rules = new List(); 19 | 20 | [ObservableProperty] 21 | private string serverAddress; 22 | 23 | [ObservableProperty] 24 | private ushort serverPort = 7000; 25 | 26 | public ClientConfig() 27 | { 28 | Name = "客户端"; 29 | } 30 | 31 | public override char Type { get; } = 'c'; 32 | 33 | public override object Clone() 34 | { 35 | var newItem = base.Clone() as ClientConfig; 36 | newItem.Rules = Rules.Select(p => p.Clone() as Rule).ToList(); 37 | return newItem; 38 | } 39 | 40 | public override string ToToml() 41 | { 42 | StringBuilder str = new StringBuilder(); 43 | str.Append("serverAddr = ").Append('"').Append(ServerAddress).Append('"').AppendLine(); 44 | str.Append("serverPort = ").Append(ServerPort).AppendLine(); 45 | str.Append("loginFailExit = ").Append(LoginFailExit.ToString().ToLower()).AppendLine(); 46 | 47 | str.Append("webServer.addr = ").Append('"').Append(DashBoardAddress).Append('"').AppendLine(); 48 | str.Append("webServer.port = ").Append(DashBoardPort).AppendLine(); 49 | str.Append("webServer.user = ").Append('"').Append(DashBoardUsername).Append('"').AppendLine(); 50 | str.Append("webServer.password = ").Append('"').Append(DashBoardPassword).Append('"').AppendLine(); 51 | if (!string.IsNullOrWhiteSpace(Token)) 52 | { 53 | str.Append("auth.token = ").Append('"').Append(Token).Append('"').AppendLine(); 54 | } 55 | 56 | str.Append("transport.tls.enable = ").Append(EnableTls.ToString().ToLower()).AppendLine(); 57 | str.Append("transport.poolCount = ").Append(PoolCount).AppendLine(); 58 | 59 | str.AppendLine(); 60 | foreach (var rule in Rules.Where(p => p.Enable && !string.IsNullOrEmpty(p.Name))) 61 | { 62 | str.Append(rule.ToToml()).AppendLine(); 63 | } 64 | str.AppendLine(); 65 | return str.ToString(); 66 | } 67 | 68 | public override void Adapt(FrpConfigBase config) 69 | { 70 | base.Adapt(config); 71 | if (config is not ClientConfig clientConfig) 72 | { 73 | throw new ArgumentException("必须为" + nameof(ClientConfig)); 74 | } 75 | clientConfig.EnableTls = EnableTls; 76 | clientConfig.LoginFailExit = LoginFailExit; 77 | clientConfig.PoolCount = PoolCount; 78 | clientConfig.ServerAddress = ServerAddress; 79 | clientConfig.ServerPort = ServerPort; 80 | clientConfig.Rules = Rules.Select(rule => rule.Clone() as Rule).ToList(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/FrpConfigBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace FrpGUI.Models 5 | { 6 | [JsonDerivedType(typeof(ClientConfig))] 7 | [JsonDerivedType(typeof(ServerConfig))] 8 | public abstract partial class FrpConfigBase : ObservableObject, IToFrpConfig, ICloneable 9 | { 10 | [ObservableProperty] 11 | private bool autoStart; 12 | 13 | [ObservableProperty] 14 | private string dashBoardAddress = "localhost"; 15 | 16 | [ObservableProperty] 17 | private string dashBoardPassword = "admin"; 18 | 19 | [ObservableProperty] 20 | private ushort dashBoardPort = 7500; 21 | 22 | [ObservableProperty] 23 | private string dashBoardUsername = "admin"; 24 | 25 | [ObservableProperty] 26 | private string name; 27 | 28 | [ObservableProperty] 29 | private string token = ""; 30 | 31 | public FrpConfigBase() 32 | { 33 | } 34 | 35 | public string ID { get; set; } = Guid.NewGuid().ToString(); 36 | 37 | public abstract char Type { get; } 38 | 39 | public virtual object Clone() 40 | { 41 | var newItem = MemberwiseClone() as FrpConfigBase; 42 | return newItem; 43 | } 44 | 45 | public abstract string ToToml(); 46 | 47 | public virtual void Adapt(FrpConfigBase config) 48 | { 49 | config.AutoStart = AutoStart; 50 | config.DashBoardPassword = DashBoardPassword; 51 | config.DashBoardPort = DashBoardPort; 52 | config.DashBoardUsername = DashBoardUsername; 53 | config.DashBoardAddress = DashBoardAddress; 54 | config.Name = Name; 55 | config.Token = Token; 56 | config.ID = ID; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/FrpConfigJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using System.Text.Json; 3 | 4 | namespace FrpGUI.Models 5 | { 6 | public class FrpConfigJsonConverter : JsonConverter 7 | { 8 | public override FrpConfigBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | using JsonDocument doc = JsonDocument.ParseValue(ref reader); 11 | if ((doc.RootElement.TryGetProperty("Type", out JsonElement typeElement) || doc.RootElement.TryGetProperty("type", out typeElement)) 12 | && typeElement.ValueKind == JsonValueKind.String) 13 | { 14 | char typeChar = typeElement.GetString()[0]; 15 | return typeChar switch 16 | { 17 | 'c' => JsonSerializer.Deserialize(doc.RootElement.GetRawText(), options), 18 | 's' => JsonSerializer.Deserialize(doc.RootElement.GetRawText(), options), 19 | _ => throw new NotImplementedException(), 20 | }; 21 | } 22 | //老版本的JSON配置文件没有Type属性 23 | else if (doc.RootElement.TryGetProperty("Rules", out JsonElement rulesElement) || doc.RootElement.TryGetProperty("rules", out rulesElement)) 24 | { 25 | return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), options); 26 | } 27 | else 28 | { 29 | return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), options); 30 | } 31 | } 32 | 33 | public override void Write(Utf8JsonWriter writer, FrpConfigBase value, JsonSerializerOptions options) 34 | { 35 | JsonSerializer.Serialize(writer, value, value.GetType(), options); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/IFrpProcess.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Enums; 2 | 3 | namespace FrpGUI.Models 4 | { 5 | public interface IFrpProcess 6 | { 7 | public FrpConfigBase Config { get; } 8 | 9 | public ProcessStatus ProcessStatus { get; set; } 10 | 11 | public Task StartAsync(); 12 | 13 | public Task StopAsync(); 14 | 15 | public Task RestartAsync(); 16 | 17 | public event EventHandler StatusChanged; 18 | } 19 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/IToFrpConfig.cs: -------------------------------------------------------------------------------- 1 | namespace FrpGUI.Models 2 | { 3 | public interface IToFrpConfig 4 | { 5 | public string ToToml(); 6 | } 7 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/LogEntity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Diagnostics; 3 | 4 | namespace FrpGUI.Models 5 | { 6 | public class LogEntity 7 | { 8 | public LogEntity() 9 | { 10 | } 11 | 12 | public LogEntity(string message, char type, FrpConfigBase config, bool fromFrp, Exception exception) : this() 13 | { 14 | Message = message; 15 | InstanceName = config?.Name; 16 | InstanceId = config?.ID; 17 | Type = type; 18 | FromFrp = fromFrp; 19 | Exception = exception?.ToString(); 20 | } 21 | 22 | [Key] 23 | public int Id { get; set; } 24 | 25 | public DateTime Time { get; set; } = DateTime.Now; 26 | public string Message { get; set; } 27 | public string InstanceName { get; set; } 28 | public string InstanceId { get; set; } 29 | public char Type { get; set; } 30 | public bool FromFrp { get; set; } 31 | public string Exception { get; set; } 32 | public DateTime ProcessStartTime { get; set; } = CurrentProcessStartTime; 33 | 34 | public static DateTime CurrentProcessStartTime { get; } 35 | 36 | static LogEntity() 37 | { 38 | //wasm不支持Process 39 | try 40 | { 41 | CurrentProcessStartTime = Process.GetCurrentProcess().StartTime; 42 | } 43 | catch 44 | { 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/ProcessInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace FrpGUI.Models 4 | { 5 | public class ProcessInfo 6 | { 7 | public int Id { get; set; } 8 | public string ProcessName { get; set; } 9 | public DateTime StartTime { get; set; } 10 | public string FileName { get; set; } 11 | 12 | public static List GetFrpProcesses() 13 | { 14 | return Process.GetProcesses() 15 | .Where(p => p.ProcessName is "frps" or "frpc") 16 | .Select(p => new ProcessInfo() 17 | { 18 | Id = p.Id, 19 | ProcessName = p.ProcessName, 20 | StartTime = p.StartTime, 21 | FileName = p.MainModule.FileName 22 | }) 23 | .ToList(); 24 | } 25 | 26 | public static void KillProcess(int id) 27 | { 28 | Process process; 29 | try 30 | { 31 | process = Process.GetProcessById(id); 32 | } 33 | catch (ArgumentException) 34 | { 35 | throw new StatusBasedException($"不存在ID为{id}的进程",System.Net.HttpStatusCode.NotFound); 36 | } 37 | if (process.ProcessName is not ("frps" or "frpc")) 38 | { 39 | throw new StatusBasedException("指定的进程不是Frp进程", System.Net.HttpStatusCode.Forbidden); 40 | } 41 | 42 | process.Kill(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/Rule.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using FrpGUI.Enums; 3 | using System.Text; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace FrpGUI.Models 7 | { 8 | public partial class Rule : ObservableObject, IToFrpConfig, ICloneable 9 | { 10 | [ObservableProperty] 11 | private int bandwidthLimitKB = 1024; 12 | 13 | [ObservableProperty] 14 | private bool enableBandwidthLimit; 15 | 16 | [ObservableProperty] 17 | private bool compression; 18 | 19 | [ObservableProperty] 20 | private string domains; 21 | 22 | [ObservableProperty] 23 | private bool enable = true; 24 | 25 | [ObservableProperty] 26 | private bool encryption; 27 | 28 | [ObservableProperty] 29 | private string localAddress = "localhost"; 30 | 31 | [ObservableProperty] 32 | private string localPort = ""; 33 | 34 | [ObservableProperty] 35 | private string name = ""; 36 | 37 | [ObservableProperty] 38 | private string remotePort = ""; 39 | 40 | [ObservableProperty] 41 | [property: JsonPropertyName("STCPKey")] 42 | private string stcpKey; 43 | 44 | [ObservableProperty] 45 | [property: JsonPropertyName("STCPServerName")] 46 | private string stcpServerName; 47 | 48 | [ObservableProperty] 49 | [NotifyPropertyChangedFor(nameof(Domains), nameof(StcpKey), nameof(StcpServerName))] 50 | private NetType type = NetType.TCP; 51 | 52 | public object Clone() 53 | { 54 | return MemberwiseClone(); 55 | } 56 | 57 | public string ToToml() 58 | { 59 | StringBuilder str = new StringBuilder(); 60 | 61 | str.AppendLine(Type == NetType.STCP_Visitor ? "[[visitors]]" : "[[proxies]]"); 62 | str.Append("name = ").Append('"').Append(Name).Append('"').AppendLine(); 63 | if (Type is NetType.STCP_Visitor) 64 | { 65 | str.Append("type = \"stcp\"").AppendLine(); 66 | } 67 | else 68 | { 69 | str.Append("type = ").Append('"').Append(Type.ToString().ToLower()).Append('"').AppendLine(); 70 | } 71 | if (Encryption) 72 | { 73 | str.AppendLine("transport.useEncryption = true"); 74 | } 75 | if (Compression) 76 | { 77 | str.AppendLine("transport.useCompression = true"); 78 | } 79 | if (EnableBandwidthLimit && BandwidthLimitKB > 0) 80 | { 81 | str.Append("transport.bandwidthLimit = \"").Append(BandwidthLimitKB).Append("KB\"").AppendLine(); 82 | } 83 | 84 | switch (Type) 85 | { 86 | case NetType.HTTP or NetType.HTTPS: 87 | str.Append("customDomains = [").Append('"').Append(Domains).Append('"').Append(']').AppendLine(); 88 | break; 89 | 90 | case NetType.TCP or NetType.UDP: 91 | str.Append("remotePort = ").Append(RemotePort).AppendLine(); 92 | break; 93 | } 94 | 95 | if (Type == NetType.STCP || Type == NetType.STCP_Visitor) 96 | { 97 | str.Append("secretKey = ").Append('"').Append(StcpKey).Append('"').AppendLine(); 98 | } 99 | 100 | if (Type == NetType.STCP_Visitor) 101 | { 102 | str.Append("serverName = ").Append('"').Append(StcpServerName).Append('"').AppendLine(); 103 | str.Append("bindAddr = ").Append('"').Append(LocalAddress).Append('"').AppendLine(); 104 | str.Append("bindPort = ").Append(LocalPort).AppendLine(); 105 | } 106 | else 107 | { 108 | str.Append("localIP = ").Append('"').Append(LocalAddress).Append('"').AppendLine(); 109 | str.Append("localPort = ").Append(LocalPort).AppendLine(); 110 | } 111 | 112 | return str.ToString(); 113 | } 114 | 115 | partial void OnTypeChanged(NetType value) 116 | { 117 | switch (value) 118 | { 119 | case NetType.TCP or NetType.UDP: 120 | StcpKey = null; 121 | StcpServerName = null; 122 | Domains = null; 123 | break; 124 | 125 | case NetType.HTTP or NetType.HTTPS: 126 | StcpKey = null; 127 | StcpServerName = null; 128 | RemotePort = null; 129 | break; 130 | 131 | case NetType.STCP: 132 | RemotePort = null; 133 | Domains = null; 134 | StcpServerName = null; 135 | break; 136 | 137 | case NetType.STCP_Visitor: 138 | RemotePort = null; 139 | Domains = null; 140 | break; 141 | } 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /FrpGUI.Core/Models/ServerConfig.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using System.Text; 3 | 4 | namespace FrpGUI.Models 5 | { 6 | public partial class ServerConfig : FrpConfigBase 7 | { 8 | [ObservableProperty] 9 | private ushort? httpPort; 10 | 11 | [ObservableProperty] 12 | private ushort? httpsPort; 13 | 14 | [ObservableProperty] 15 | private short maxPoolCount = 100; 16 | 17 | [ObservableProperty] 18 | private ushort port = 7000; 19 | 20 | [ObservableProperty] 21 | private bool tlsOnly; 22 | 23 | public ServerConfig() 24 | { 25 | Name = "服务端"; 26 | } 27 | 28 | public override char Type { get; } = 's'; 29 | 30 | public override string ToToml() 31 | { 32 | StringBuilder str = new StringBuilder(); 33 | str.Append("bindPort = ").Append(Port).AppendLine(); 34 | str.Append("webServer.addr = ").Append('"').Append(DashBoardAddress).Append('"').AppendLine(); 35 | str.Append("webServer.port = ").Append(DashBoardPort).AppendLine(); 36 | str.Append("webServer.user = ").Append('"').Append(DashBoardUsername).Append('"').AppendLine(); 37 | str.Append("webServer.password = ").Append('"').Append(DashBoardPassword).Append('"').AppendLine(); 38 | 39 | if (HttpPort.HasValue && HttpPort.Value > 0) 40 | { 41 | str.Append("vhostHTTPPort = ").Append(HttpPort.Value).AppendLine(); 42 | } 43 | if (HttpsPort.HasValue && HttpsPort.Value > 0) 44 | { 45 | str.Append("vhostHTTPSPort = ").Append(HttpsPort.Value).AppendLine(); 46 | } 47 | if (!string.IsNullOrWhiteSpace(Token)) 48 | { 49 | str.Append("auth.token = ").Append('"').Append(Token).Append('"').AppendLine(); 50 | } 51 | 52 | str.Append("transport.tls.force = ").Append(TlsOnly.ToString().ToLower()).AppendLine(); 53 | str.Append("transport.maxPoolCount = ").Append(MaxPoolCount).AppendLine(); 54 | 55 | return str.ToString(); 56 | } 57 | 58 | public override void Adapt(FrpConfigBase config) 59 | { 60 | base.Adapt(config); // 先复制基类属性 61 | if (config is not ServerConfig serverConfig) 62 | { 63 | throw new ArgumentException("必须为" + nameof(ServerConfig)); 64 | } 65 | // 复制 ServerConfig 特有的属性 66 | serverConfig.HttpPort = HttpPort; 67 | serverConfig.HttpsPort = HttpsPort; 68 | serverConfig.MaxPoolCount = MaxPoolCount; 69 | serverConfig.Port = Port; 70 | serverConfig.TlsOnly = TlsOnly; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /FrpGUI.Core/StatusBasedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FrpGUI; 4 | 5 | public class StatusBasedException : Exception 6 | { 7 | public StatusBasedException(string message, HttpStatusCode statusCode) : base(message) 8 | { 9 | StatusCode = statusCode; 10 | } 11 | 12 | public StatusBasedException(string message, HttpStatusCode statusCode, Exception innerException) : base(message, innerException) 13 | { 14 | StatusCode = statusCode; 15 | } 16 | 17 | public HttpStatusCode StatusCode { get; set; } 18 | } -------------------------------------------------------------------------------- /FrpGUI.Service/Configs/AppConfig.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Models; 2 | using System.Diagnostics; 3 | using System.Text.Json; 4 | using System.Text.Json.Nodes; 5 | using System.Text.Json.Serialization; 6 | using System.Text.Json.Serialization.Metadata; 7 | 8 | namespace FrpGUI.Configs 9 | { 10 | public class AppConfig : AppConfigBase 11 | { 12 | private static readonly string ConfigPathS = Path.Combine(AppContext.BaseDirectory, "config.json"); 13 | public override string ConfigPath { get; } = ConfigPathS; 14 | public List FrpConfigs { get; set; } = new List(); 15 | public string Token { get; set; } 16 | private JsonTypeInfo JsonTypeInfo { get; } = AppConfigSourceGenerationContext.Get().AppConfig; 17 | 18 | public static AppConfig Get() 19 | { 20 | //MigrateConfig20250407(); 21 | return Get(AppConfigSourceGenerationContext.Get().AppConfig); 22 | } 23 | 24 | public void Save() 25 | { 26 | Save(JsonTypeInfo); 27 | } 28 | 29 | protected override void OnLoaded() 30 | { 31 | if (FrpConfigs.Count == 0) 32 | { 33 | FrpConfigs.Add(new ServerConfig()); 34 | FrpConfigs.Add(new ClientConfig()); 35 | } 36 | } 37 | 38 | //private static void MigrateConfig20250407() 39 | //{ 40 | // //将2025年4月6日之前版本的配置(Servers和Clients合并)迁移至后面的版本 41 | // if (!File.Exists(ConfigPathS)) 42 | // { 43 | // return; 44 | // } 45 | // try 46 | // { 47 | // var json = JsonNode.Parse(File.ReadAllText(ConfigPathS))?.AsObject(); 48 | // if (json == null) 49 | // { 50 | // return; 51 | // } 52 | // if (!json.ContainsKey("FrpConfigs"))//不是旧版 53 | // { 54 | // return; 55 | // } 56 | // var oldConfigs = json["FrpConfigs"].AsArray(); 57 | // var servers = new JsonArray(); 58 | // json.Add(nameof(Servers), servers); 59 | // var clients = new JsonArray(); 60 | // json.Add(nameof(Clients), clients); 61 | // foreach (JsonObject c in oldConfigs) 62 | // { 63 | // if (c[nameof(FrpConfigBase.Type)]?.GetValue() == "c" || c.ContainsKey(nameof(ClientConfig.Rules))) 64 | // { 65 | // clients.Add(c.DeepClone()); 66 | // } 67 | // else 68 | // { 69 | // servers.Add(c.DeepClone()); 70 | // } 71 | // } 72 | // File.WriteAllText(ConfigPathS, json.ToJsonString(new JsonSerializerOptions 73 | // { 74 | // WriteIndented = true, 75 | // PropertyNameCaseInsensitive = true 76 | // })); 77 | // } 78 | // catch(Exception ex) 79 | // { 80 | // Debug.Assert(false); 81 | // } 82 | //} 83 | } 84 | } -------------------------------------------------------------------------------- /FrpGUI.Service/Configs/AppConfigBase.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using System.Text.Json.Serialization.Metadata; 4 | 5 | namespace FrpGUI.Configs 6 | { 7 | public abstract class AppConfigBase 8 | { 9 | public abstract string ConfigPath { get; } 10 | 11 | public static T Get(JsonTypeInfo jsonTypeInfo) where T : AppConfigBase, new() 12 | { 13 | T config = new T(); 14 | 15 | if (OperatingSystem.IsBrowser() 16 | || File.Exists(config.ConfigPath)) 17 | { 18 | try 19 | { 20 | config = config.GetImpl(jsonTypeInfo); 21 | } 22 | catch (Exception ex) 23 | { 24 | config = new T(); 25 | } 26 | } 27 | config.OnLoaded(); 28 | return config; 29 | } 30 | 31 | protected virtual T GetImpl(JsonTypeInfo jsonTypeInfo) where T : AppConfigBase 32 | { 33 | return JsonSerializer.Deserialize(File.ReadAllText(ConfigPath), jsonTypeInfo); 34 | } 35 | 36 | public void Save(JsonTypeInfo jsonTypeInfo) 37 | { 38 | var bytes = JsonSerializer.SerializeToUtf8Bytes(this, jsonTypeInfo); 39 | File.WriteAllBytes(ConfigPath, bytes); 40 | } 41 | 42 | protected virtual void OnLoaded() 43 | { 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /FrpGUI.Service/Configs/AppConfigSourceGenerationContext.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Models; 2 | using System.Text.Encodings.Web; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using System.Text.Unicode; 6 | 7 | namespace FrpGUI.Configs 8 | { 9 | [JsonSourceGenerationOptions(WriteIndented = true, PropertyNameCaseInsensitive = true)] 10 | [JsonSerializable(typeof(AppConfig))] 11 | internal partial class AppConfigSourceGenerationContext : JsonSerializerContext 12 | { 13 | public static AppConfigSourceGenerationContext Get() 14 | { 15 | return new AppConfigSourceGenerationContext(new JsonSerializerOptions() 16 | { 17 | WriteIndented = true, 18 | PropertyNameCaseInsensitive = true, 19 | Converters = { new FrpConfigJsonConverter() } 20 | }); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /FrpGUI.Service/FrpGUI.Service.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | disable 7 | ../Generation/bin 8 | false 9 | $(Temp)\$(SolutionName)\$(Configuration)\$(AssemblyName) 10 | $(Temp)\$(SolutionName)\obj\$(Configuration)\$(AssemblyName) 11 | FzLib 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ..\..\FzLib\Publish\Release\net8.0\FzLib.dll 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /FrpGUI.Service/Models/FrpProcess.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Enums; 2 | using FrpGUI.Services; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace FrpGUI.Models; 6 | 7 | public class FrpProcess : IFrpProcess 8 | { 9 | private readonly LoggerBase logger; 10 | 11 | public FrpProcess() 12 | { } 13 | 14 | public FrpProcess(FrpConfigBase config, LoggerBase logger) 15 | { 16 | Config = config; 17 | this.logger = logger; 18 | Process = new ProcessService(Config, logger); 19 | Process.Exited += Process_Exited; 20 | } 21 | 22 | public event EventHandler StatusChanged; 23 | 24 | public FrpConfigBase Config { get; } 25 | 26 | [JsonIgnore] 27 | public ProcessService Process { get; protected set; } 28 | 29 | public ProcessStatus ProcessStatus { get; set; } 30 | 31 | public void ChangeStatus(ProcessStatus status) 32 | { 33 | logger.Info("进程状态改变:" + status.ToString(), Config); 34 | ProcessStatus = status; 35 | StatusChanged?.Invoke(this, new EventArgs()); 36 | } 37 | 38 | public async Task RestartAsync() 39 | { 40 | if (ProcessStatus == ProcessStatus.Stopped) 41 | { 42 | throw new Exception("进程未在运行"); 43 | } 44 | ChangeStatus(ProcessStatus.Busy); 45 | try 46 | { 47 | await Process.RestartAsync(); 48 | } 49 | catch (Exception ex) 50 | { 51 | ChangeStatus(ProcessStatus.Stopped); 52 | throw; 53 | } 54 | ChangeStatus(ProcessStatus.Running); 55 | } 56 | 57 | public async Task StartAsync() 58 | { 59 | if (ProcessStatus == ProcessStatus.Running) 60 | { 61 | throw new Exception("进程已在运行"); 62 | } 63 | ChangeStatus(ProcessStatus.Busy); 64 | try 65 | { 66 | await Task.Run(Process.Start).ConfigureAwait(false); 67 | ChangeStatus(ProcessStatus.Running); 68 | } 69 | catch (Exception ex) 70 | { 71 | ChangeStatus(ProcessStatus.Stopped); 72 | throw; 73 | } 74 | } 75 | 76 | public async Task StopAsync() 77 | { 78 | if (ProcessStatus == ProcessStatus.Stopped) 79 | { 80 | throw new Exception("进程未在运行"); 81 | } 82 | ChangeStatus(ProcessStatus.Busy); 83 | await Process.StopAsync(); 84 | ChangeStatus(ProcessStatus.Stopped); 85 | } 86 | 87 | private void Process_Exited(object sender, EventArgs e) 88 | { 89 | ChangeStatus(ProcessStatus.Stopped); 90 | } 91 | } -------------------------------------------------------------------------------- /FrpGUI.Service/Models/FrpProcessCollection.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Configs; 2 | using FrpGUI.Enums; 3 | using FrpGUI.Services; 4 | 5 | namespace FrpGUI.Models; 6 | 7 | public class FrpProcessCollection(AppConfig config, LoggerBase logger) : Dictionary 8 | { 9 | public IFrpProcess GetOrCreateProcess(string id) 10 | { 11 | if (TryGetValue(id, out FrpProcess process)) 12 | { 13 | return process; 14 | } 15 | var frp = GetFrpConfig(id); 16 | process = new FrpProcess(frp, logger); 17 | Add(id, process); 18 | return process; 19 | } 20 | 21 | protected FrpConfigBase GetFrpConfig(string id) 22 | { 23 | var client = config.FrpConfigs.FirstOrDefault(p => p.ID == id); 24 | return client ?? throw new ArgumentException($"找不到ID为{id}的配置"); 25 | } 26 | 27 | public IList GetAll() 28 | { 29 | List list = new List(); 30 | foreach (var item in config.FrpConfigs) 31 | { 32 | list.Add(GetOrCreateProcess(item.ID)); 33 | } 34 | return list; 35 | } 36 | 37 | public async Task RemoveFrpAsync(string id) 38 | { 39 | var frp = GetOrCreateProcess(id); 40 | if (frp.ProcessStatus == ProcessStatus.Running) 41 | { 42 | await frp.StopAsync(); 43 | } 44 | config.FrpConfigs.Remove(frp.Config); 45 | Remove(frp.Config.ID); 46 | config.Save(); 47 | return frp.Config; 48 | } 49 | } -------------------------------------------------------------------------------- /FrpGUI.Service/Services/AppLifetimeService.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI; 2 | using FrpGUI.Configs; 3 | using FrpGUI.Models; 4 | using Microsoft.Extensions.Hosting; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace FrpGUI.Services 8 | { 9 | public class AppLifetimeService(AppConfig config, LoggerBase logger, FrpProcessCollection processes) : IHostedService 10 | { 11 | protected AppConfig Config { get; } = config; 12 | protected LoggerBase Logger { get; } = logger; 13 | protected FrpProcessCollection Processes { get; } = processes; 14 | 15 | public virtual async Task StartAsync(CancellationToken cancellationToken) 16 | { 17 | foreach (var fp in Processes.GetAll()) 18 | { 19 | if (fp.Config.AutoStart) 20 | { 21 | Logger.Info($"自动启动:{fp.Config.Name}"); 22 | try 23 | { 24 | await Processes.GetOrCreateProcess(fp.Config.ID).StartAsync().ConfigureAwait(false); 25 | } 26 | catch (Exception ex) 27 | { 28 | Logger.Error($"自动启动{fp.Config.Name}失败", fp.Config, ex); 29 | } 30 | } 31 | } 32 | } 33 | 34 | public virtual async Task StopAsync(CancellationToken cancellationToken) 35 | { 36 | Config.Save(); 37 | foreach (FrpProcess fp in Processes.Values) 38 | { 39 | if (fp.ProcessStatus == FrpGUI.Enums.ProcessStatus.Running) 40 | { 41 | Logger.Info($"应用正在退出,正在停止:{fp.Config.Name}"); 42 | try 43 | { 44 | await fp.StopAsync(); 45 | } 46 | catch (Exception ex) 47 | { 48 | Logger.Error($"停止{fp.Config.Name}失败", fp.Config, ex); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /FrpGUI.Service/Services/Logger.cs: -------------------------------------------------------------------------------- 1 | using FrpGUI.Models; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace FrpGUI.Services 5 | { 6 | public abstract class LoggerBase 7 | { 8 | private readonly string[] errorMessages = [ 9 | "error", 10 | "unknown", 11 | "Only one usage of each socket address (protocol/network address/port) is normally permitted.", 12 | ]; 13 | 14 | private readonly Regex rFrpLog = new Regex(@"(?