Compare commits

...

41 Commits

Author SHA1 Message Date
ChsBuffer
9a3a1e3664 Fix #589 GetProcessByUsedTcpPort multiple process 2021-03-22 22:01:03 +08:00
AmazingDM
3f4a31dac8 Update UpdateChecker.cs 2021-03-22 21:40:26 +08:00
AmazingDM
7c0088cc7f optimization 2021-03-22 21:23:11 +08:00
ChsBuffer
1a28d791b7 Merge pull request #585 from NetchX/dependabot/nuget/Vanara.PInvoke.IpHlpApi-3.3.6
Bump Vanara.PInvoke.IpHlpApi from 3.3.5 to 3.3.6
2021-03-22 11:18:42 +08:00
ChsBuffer
05df786ab6 Merge pull request #586 from NetchX/dependabot/nuget/Vanara.PInvoke.User32-3.3.6
Bump Vanara.PInvoke.User32 from 3.3.5 to 3.3.6
2021-03-22 11:18:33 +08:00
ChsBuffer
1ffbae6135 Feat: Show Release Note when confirm 2021-03-22 11:09:16 +08:00
dependabot[bot]
8ca9d6d9be Bump Vanara.PInvoke.User32 from 3.3.5 to 3.3.6
Bumps [Vanara.PInvoke.User32](https://github.com/dahall/vanara) from 3.3.5 to 3.3.6.
- [Release notes](https://github.com/dahall/vanara/releases)
- [Commits](https://github.com/dahall/vanara/compare/v3.3.5...v3.3.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-21 23:10:21 +00:00
dependabot[bot]
4f1ae20b9b Bump Vanara.PInvoke.IpHlpApi from 3.3.5 to 3.3.6
Bumps [Vanara.PInvoke.IpHlpApi](https://github.com/dahall/vanara) from 3.3.5 to 3.3.6.
- [Release notes](https://github.com/dahall/vanara/releases)
- [Commits](https://github.com/dahall/vanara/compare/v3.3.5...v3.3.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-21 23:10:16 +00:00
ChsBuffer
15a1db3b21 Feat: Backup configuration file before update 2021-03-22 01:59:49 +08:00
ChsBuffer
54243a80e7 Update SuffixVersion 2021-03-22 01:43:06 +08:00
ChsBuffer
947bf2b3ca Bump version to 1.8.3-Beta1 2021-03-21 22:53:00 +08:00
ChsBuffer
2a165c79df Start Profile, Refactor Save LastSelectedServer/Mode 2021-03-21 22:44:15 +08:00
ChsBuffer
55280df299 Update Load configuration 2021-03-21 22:00:16 +08:00
ChsBuffer
af48e7119e Auto reload modes, Lazy load mode rules 2021-03-21 04:54:33 +08:00
ChsBuffer
a1b978a22c Remove WindowsJobAPI 2021-03-21 03:38:34 +08:00
ChsBuffer
d08a9d5bfd Refactor Start Port Check Kill Process 2021-03-21 03:38:25 +08:00
ChsBuffer
c69c40750a Fix a typo 2021-03-21 01:48:44 +08:00
ChsBuffer
77376502b7 Update CI 2021-03-21 00:05:20 +08:00
ChsBuffer
fdfc3f11eb Update NFController 2021-03-20 21:38:24 +08:00
AmazingDM
0d956efac6 update binaries 2021-03-20 20:36:37 +08:00
AmazingDM
3f9709167d update NFController
optimization rdr
2021-03-20 20:35:56 +08:00
ChsBuffer
425e468f78 Revert "Update Netch.csproj to be compatible with dotnet cli"
This reverts commit 77f2b761fc.
2021-03-20 18:33:04 +08:00
AmazingDM
a485a4647c fix error set TYPE_FILTERCHILDPROC 2021-03-20 17:56:45 +08:00
ChsBuffer
0b484face4 Update Edit Process Mode Form 2021-03-20 04:33:35 +08:00
ChsBuffer
e0b5b0e49c Restore Edit Process Scan Button And Update Select Button 2021-03-20 03:41:37 +08:00
ChsBuffer
f519850ffc Trim 2021-03-20 00:50:06 +08:00
ChsBuffer
4513a68e73 Update Nuget packages
Bump MSTest.TestAdapter from 2.2.1 to 2.2.3
Bump MSTest.TestFramework from 2.2.1 to 2.2.3
2021-03-19 03:58:57 +08:00
ChsBuffer
95de42e778 Update SS/SSR Parameter 2021-03-19 03:56:54 +08:00
ChsBuffer
18168c3a4e Add Nullable.Extended.Analyzer 2021-03-19 03:50:08 +08:00
ChsBuffer
77f2b761fc Update Netch.csproj to be compatible with dotnet cli 2021-03-19 03:07:07 +08:00
ChsBuffer
9bd02ec122 Update Debug Logging 2021-03-19 03:05:57 +08:00
ChsBuffer
cfb4a5b3f6 The Debug configuration will make the build attach to the console and write the application log to standard output 2021-03-19 00:58:34 +08:00
ChsBuffer
6178045f15 Fix Import v2rayN format sharelink got exception when property type Number 2021-03-16 16:43:46 +08:00
ChsBuffer
4773de99e5 Fix typo 2021-03-16 16:41:35 +08:00
ChsBuffer
eb713db867 Refactor: generate SS/SSR start arguments 2021-03-16 15:55:46 +08:00
ChsBuffer
15f4895c0f Update UnitTest 2021-03-16 15:24:30 +08:00
ChsBuffer
afbda60dfb format NFController 2021-03-16 15:19:25 +08:00
ChsBuffer
f51229f2c8 Fix: SS/SSR password not allow empty and Update Model 2021-03-16 15:19:12 +08:00
ChsBuffer
26f9ae3958 Update Server Model value type 2021-03-15 22:38:33 +08:00
ChsBuffer
dfc680f0b7 Update .gitignore 2021-03-15 22:10:46 +08:00
ChsBuffer
5e56556534 Remove unused resources 2021-03-15 22:03:47 +08:00
50 changed files with 1022 additions and 755 deletions

30
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Netch CI
on:
push:
branches-ignore:
dependabot/**
pull_request:
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.0.2
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Build Solution
shell: pwsh
run: .\BUILD.ps1
- name: Upload Artifact
continue-on-error: true
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/upload-artifact@v2
with:
name: Netch
path: Netch\bin\x64\Release

View File

@@ -1,5 +1,8 @@
name: Netch CI
on: [push, pull_request]
name: Netch Release
on:
push:
tags:
- '*.*'
jobs:
build:
name: Build
@@ -17,14 +20,6 @@ jobs:
shell: pwsh
run: .\BUILD.ps1
- name: Upload Artifact
continue-on-error: true
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/upload-artifact@v2
with:
name: Netch
path: Netch\bin\x64\Release
- name: Package
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
shell: pwsh
@@ -39,10 +34,8 @@ jobs:
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
with:
name: ${{ env.GITHUB_TAG_NAME }}
prerelease: true
prerelease: ${{ contains(github.ref, '-') }}
draft: false
files: |
C:\builtfiles\Netch.7z
@@ -55,4 +48,4 @@ jobs:
## 校验和
| 文件名 | SHA256 |
| :- | :- |
| Netch.7z | ${{ env.Netch_SHA256 }} |
| Netch.7z | ${{ env.Netch_SHA256 }} |

7
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/.vs
/packages
.vs/
.idea/
/*.user
*/bin/
*/obj/
*.csproj.user

3
Netch/.gitignore vendored
View File

@@ -1,3 +0,0 @@
/bin
/obj
/Netch.csproj.user

View File

@@ -24,7 +24,6 @@ namespace Netch.Controllers
public void Start(in Mode mode)
{
PrivoxyController.Start(MainController.Server!);
Global.Job.AddProcess(PrivoxyController.Instance!);
string? pacUrl = null;
if (MainController.Server is Socks5 or Trojan && mode.BypassChina || (Global.Settings.AlwaysStartPACServer ?? false))

View File

@@ -1,11 +1,10 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Netch.Models;
using Netch.Servers.Socks5;
using Netch.Utils;
using static Netch.Utils.PortHelper;
namespace Netch.Controllers
{
@@ -105,20 +104,11 @@ namespace Netch.Controllers
{
controller = ServerHelper.GetUtilByTypeName(server.Type).GetController();
if (controller is Guard instanceController)
Utils.Utils.KillProcessByName(instanceController.MainFile);
PortCheck(controller.Socks5LocalPort(), "Socks5");
TryReleaseTcpPort(controller.Socks5LocalPort(), "Socks5");
Global.MainForm.StatusText(i18N.TranslateFormat("Starting {0}", controller.Name));
controller.Start(in server, mode);
if (controller is Guard {Instance: { }} guard)
Task.Run(() =>
{
Thread.Sleep(1000);
Global.Job.AddProcess(guard.Instance!);
});
if (server is Socks5 socks5)
{
@@ -133,19 +123,17 @@ namespace Netch.Controllers
private static void StartMode(Mode mode)
{
ModeController = ModeHelper.GetModeControllerByType(mode.Type, out var port, out var portName, out var portType);
ModeController = ModeHelper.GetModeControllerByType(mode.Type, out var port, out var portName);
if (ModeController == null)
return;
if (port != null)
PortCheck((ushort) port, portName, portType);
TryReleaseTcpPort((ushort) port, portName);
Global.MainForm.StatusText(i18N.TranslateFormat("Starting {0}", ModeController.Name));
ModeController.Start(mode);
if (ModeController is Guard {Instance: { }} guard)
Global.Job.AddProcess(guard.Instance!);
}
public static async Task StopAsync()
@@ -189,7 +177,7 @@ namespace Netch.Controllers
{
try
{
CheckPort(port, portType);
PortHelper.CheckPort(port, portType);
}
catch (PortInUseException)
{
@@ -200,6 +188,26 @@ namespace Netch.Controllers
throw new MessageException(i18N.TranslateFormat("The {0} port is reserved by system.", $"{portName} ({port})"));
}
}
public static void TryReleaseTcpPort(ushort port, string portName)
{
foreach (var p in PortHelper.GetProcessByUsedTcpPort(port))
{
if (p.MainModule!.FileName.StartsWith(Global.NetchDir))
{
p.Kill();
p.WaitForExit();
}
else
{
throw new MessageException(i18N.TranslateFormat("The {0} port is used by {1}.",
$"{portName} ({port})",
$"({p.Id}){p.MainModule.FileName}"));
}
}
PortCheck(port, portName, PortType.TCP);
}
}
public class MessageException : Exception

View File

@@ -17,57 +17,30 @@ namespace Netch.Controllers
{
private static readonly ServiceController NFService = new("netfilter2");
private static readonly string BinDriver;
private const string BinDriver = "bin\\nfdriver.sys";
private static readonly string SystemDriver = $"{Environment.SystemDirectory}\\drivers\\netfilter2.sys";
static NFController()
{
string fileName;
switch ($"{Environment.OSVersion.Version.Major}.{Environment.OSVersion.Version.Minor}")
{
case "10.0":
case "6.3":
case "6.2":
case "6.1":
case "6.0":
fileName = "nfdriver.sys";
break;
default:
throw new MessageException($"不支持的系统版本:{Environment.OSVersion.Version}");
}
BinDriver = "bin\\" + fileName;
}
public string Name { get; } = "Redirector";
public void Start(in Mode mode)
{
CheckDriver();
#region aio_dial
aio_dial((int) NameList.TYPE_FILTERLOOPBACK, "false");
aio_dial((int) NameList.TYPE_TCPLISN, Global.Settings.RedirectorTCPPort.ToString());
aio_dial((int)NameList.TYPE_FILTERLOOPBACK, "false");
aio_dial((int)NameList.TYPE_TCPLISN, Global.Settings.RedirectorTCPPort.ToString());
// Server
aio_dial((int) NameList.TYPE_FILTERUDP, (Global.Settings.ProcessProxyProtocol != PortType.TCP).ToString().ToLower());
aio_dial((int) NameList.TYPE_FILTERTCP, (Global.Settings.ProcessProxyProtocol != PortType.UDP).ToString().ToLower());
dial_Server(Global.Settings.ProcessProxyProtocol);
aio_dial((int)NameList.TYPE_FILTERUDP, (Global.Settings.ProcessProxyProtocol != PortType.TCP).ToString().ToLower());
aio_dial((int)NameList.TYPE_FILTERTCP, (Global.Settings.ProcessProxyProtocol != PortType.UDP).ToString().ToLower());
SetServer(Global.Settings.ProcessProxyProtocol);
// Mode Rule
dial_Name(mode);
if (!CheckRule(mode.FullRule, out var list))
throw new MessageException($"\"{string.Join("", list.Select(s => s + "\n"))}\" does not conform to C++ regular expression syntax");
SetName(mode);
#endregion
if (Global.Settings.RedirectDNS)
aio_dial((int)NameList.TYPE_REDIRCTOR_DNS, Global.Settings.RedirectDNSAddr.ToString());
if (Global.Settings.RedirectICMP)
aio_dial((int)NameList.TYPE_REDIRCTOR_ICMP, Global.Settings.RedirectICMPAddr.ToString());
aio_dial((int)NameList.TYPE_FILTERCHILDPROC, Global.Settings.ChildProcessHandle.ToString());
// Features
aio_dial((int) NameList.TYPE_REDIRCTOR_DNS, Global.Settings.RedirectDNS ? Global.Settings.RedirectDNSAddr : "");
aio_dial((int) NameList.TYPE_REDIRCTOR_ICMP, Global.Settings.RedirectICMP ? Global.Settings.RedirectICMPAddr : "");
aio_dial((int) NameList.TYPE_FILTERCHILDPROC, Global.Settings.ChildProcessHandle.ToString().ToLower());
if (!aio_init())
throw new MessageException("Redirector Start failed, run Netch with \"-console\" argument");
@@ -78,83 +51,54 @@ namespace Netch.Controllers
aio_free();
}
/// <summary>
/// </summary>
/// <param name="rules"></param>
/// <param name="incompatibleRule"></param>
/// <returns>No Problem true</returns>
public static bool CheckRule(IEnumerable<string> rules, out IEnumerable<string> incompatibleRule)
{
incompatibleRule = rules.Where(r => !CheckCppRegex(r, false));
aio_dial((int)NameList.TYPE_CLRNAME, "");
return !incompatibleRule.Any();
}
#region CheckRule
/// <summary>
/// </summary>
/// <param name="r"></param>
/// <param name="clear"></param>
/// <returns>No Problem true</returns>
public static bool CheckCppRegex(string r, bool clear = true)
private static bool CheckCppRegex(string r, bool clear = true)
{
try
{
if (r.StartsWith("!"))
return aio_dial((int)NameList.TYPE_ADDNAME, r.Substring(1));
return aio_dial((int) NameList.TYPE_ADDNAME, r.Substring(1));
return aio_dial((int)NameList.TYPE_ADDNAME, r);
return aio_dial((int) NameList.TYPE_ADDNAME, r);
}
finally
{
if (clear)
aio_dial((int)NameList.TYPE_CLRNAME, "");
aio_dial((int) NameList.TYPE_CLRNAME, "");
}
}
private static void CheckDriver()
/// <summary>
/// </summary>
/// <param name="rules"></param>
/// <param name="results"></param>
/// <returns>No Problem true</returns>
public static bool CheckRules(IEnumerable<string> rules, out IEnumerable<string> results)
{
var binFileVersion = Utils.Utils.GetFileVersion(BinDriver);
var systemFileVersion = Utils.Utils.GetFileVersion(SystemDriver);
Logging.Info("内置驱动版本: " + binFileVersion);
Logging.Info("系统驱动版本: " + systemFileVersion);
if (!File.Exists(SystemDriver))
{
InstallDriver();
return;
}
var reinstallFlag = false;
if (Version.TryParse(binFileVersion, out var binResult) && Version.TryParse(systemFileVersion, out var systemResult))
{
if (binResult.CompareTo(systemResult) > 0)
// Bin greater than Installed
reinstallFlag = true;
else if (systemResult.Major != binResult.Major)
// Installed greater than Bin but Major Version Difference (has breaking changes), do downgrade
reinstallFlag = true;
}
else
{
if (!systemFileVersion.Equals(binFileVersion))
reinstallFlag = true;
}
if (!reinstallFlag)
return;
Logging.Info("更新驱动");
UninstallDriver();
InstallDriver();
results = rules.Where(r => !CheckCppRegex(r, false));
aio_dial((int) NameList.TYPE_CLRNAME, "");
return !results.Any();
}
private void SetServer(in PortType portType)
public static string GenerateInvalidRulesMessage(IEnumerable<string> rules)
{
return $"{string.Join("\n", rules)}\nAbove rules does not conform to C++ regular expression syntax";
}
#endregion
private void dial_Server(in PortType portType)
{
if (portType == PortType.Both)
{
SetServer(PortType.TCP);
SetServer(PortType.UDP);
dial_Server(PortType.TCP);
dial_Server(PortType.UDP);
return;
}
@@ -177,105 +121,97 @@ namespace Netch.Controllers
if (server is Socks5 socks5)
{
aio_dial((int)NameList.TYPE_TCPTYPE + offset, "Socks5");
aio_dial((int)NameList.TYPE_TCPHOST + offset, $"{socks5.AutoResolveHostname()}:{socks5.Port}");
aio_dial((int)NameList.TYPE_TCPUSER + offset, socks5.Username ?? string.Empty);
aio_dial((int)NameList.TYPE_TCPPASS + offset, socks5.Password ?? string.Empty);
aio_dial((int)NameList.TYPE_TCPMETH + offset, string.Empty);
aio_dial((int) NameList.TYPE_TCPTYPE + offset, "Socks5");
aio_dial((int) NameList.TYPE_TCPHOST + offset, $"{socks5.AutoResolveHostname()}:{socks5.Port}");
aio_dial((int) NameList.TYPE_TCPUSER + offset, socks5.Username ?? string.Empty);
aio_dial((int) NameList.TYPE_TCPPASS + offset, socks5.Password ?? string.Empty);
aio_dial((int) NameList.TYPE_TCPMETH + offset, string.Empty);
}
else if (server is Shadowsocks shadowsocks && !shadowsocks.HasPlugin() && Global.Settings.RedirectorSS)
{
aio_dial((int)NameList.TYPE_TCPTYPE + offset, "Shadowsocks");
aio_dial((int)NameList.TYPE_TCPHOST + offset, $"{shadowsocks.AutoResolveHostname()}:{shadowsocks.Port}");
aio_dial((int)NameList.TYPE_TCPMETH + offset, shadowsocks.EncryptMethod ?? string.Empty);
aio_dial((int)NameList.TYPE_TCPPASS + offset, shadowsocks.Password ?? string.Empty);
aio_dial((int) NameList.TYPE_TCPTYPE + offset, "Shadowsocks");
aio_dial((int) NameList.TYPE_TCPHOST + offset, $"{shadowsocks.AutoResolveHostname()}:{shadowsocks.Port}");
aio_dial((int) NameList.TYPE_TCPMETH + offset, shadowsocks.EncryptMethod);
aio_dial((int) NameList.TYPE_TCPPASS + offset, shadowsocks.Password);
}
else
{
aio_dial((int)NameList.TYPE_TCPTYPE + offset, "Socks5");
aio_dial((int)NameList.TYPE_TCPHOST + offset, $"127.0.0.1:{controller.Socks5LocalPort()}");
aio_dial((int)NameList.TYPE_TCPUSER + offset, string.Empty);
aio_dial((int)NameList.TYPE_TCPPASS + offset, string.Empty);
aio_dial((int)NameList.TYPE_TCPMETH + offset, string.Empty);
aio_dial((int) NameList.TYPE_TCPTYPE + offset, "Socks5");
aio_dial((int) NameList.TYPE_TCPHOST + offset, $"127.0.0.1:{controller.Socks5LocalPort()}");
aio_dial((int) NameList.TYPE_TCPUSER + offset, string.Empty);
aio_dial((int) NameList.TYPE_TCPPASS + offset, string.Empty);
aio_dial((int) NameList.TYPE_TCPMETH + offset, string.Empty);
}
}
private void SetName(Mode mode)
private void dial_Name(Mode mode)
{
aio_dial((int)NameList.TYPE_CLRNAME, "");
foreach (var rule in mode.FullRule)
aio_dial((int) NameList.TYPE_CLRNAME, "");
var list = new List<string>();
foreach (var s in mode.FullRule)
{
if (rule.StartsWith("!"))
if (s.StartsWith("!"))
{
aio_dial((int)NameList.TYPE_BYPNAME, rule.Substring(1));
if (!aio_dial((int) NameList.TYPE_BYPNAME, s.Substring(1)))
list.Add(s);
continue;
}
aio_dial((int)NameList.TYPE_ADDNAME, rule);
if (!aio_dial((int) NameList.TYPE_ADDNAME, s))
list.Add(s);
}
aio_dial((int)NameList.TYPE_ADDNAME, @"NTT\.exe");
aio_dial((int)NameList.TYPE_BYPNAME, "^" + Global.NetchDir.ToRegexString() + @"((?!NTT\.exe).)*$");
if (list.Any())
throw new MessageException(GenerateInvalidRulesMessage(list));
aio_dial((int) NameList.TYPE_ADDNAME, @"NTT\.exe");
aio_dial((int) NameList.TYPE_BYPNAME, "^" + Global.NetchDir.ToRegexString() + @"((?!NTT\.exe).)*$");
}
#region NativeMethods
#region DriverUtil
private const int UdpNameListOffset = (int)NameList.TYPE_UDPTYPE - (int)NameList.TYPE_TCPTYPE;
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern bool aio_dial(int name, [MarshalAs(UnmanagedType.LPWStr)] string value);
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern bool aio_init();
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern bool aio_free();
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern ulong aio_getUP();
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern ulong aio_getDL();
public enum NameList
private static void CheckDriver()
{
//bool
TYPE_FILTERLOOPBACK,
TYPE_FILTERTCP,
TYPE_FILTERUDP,
TYPE_FILTERIP,
TYPE_FILTERCHILDPROC,//子进程捕获
var binFileVersion = Utils.Utils.GetFileVersion(BinDriver);
var systemFileVersion = Utils.Utils.GetFileVersion(SystemDriver);
TYPE_TCPLISN,
TYPE_TCPTYPE,
TYPE_TCPHOST,
TYPE_TCPUSER,
TYPE_TCPPASS,
TYPE_TCPMETH,
Logging.Info("内置驱动版本: " + binFileVersion);
Logging.Info("系统驱动版本: " + systemFileVersion);
TYPE_UDPTYPE,
TYPE_UDPHOST,
TYPE_UDPUSER,
TYPE_UDPPASS,
TYPE_UDPMETH,
if (!File.Exists(SystemDriver))
{
// Install
InstallDriver();
return;
}
TYPE_ADDNAME,
TYPE_ADDFIP,
var reinstall = false;
if (Version.TryParse(binFileVersion, out var binResult) && Version.TryParse(systemFileVersion, out var systemResult))
{
if (binResult.CompareTo(systemResult) > 0)
// Update
reinstall = true;
else if (systemResult.Major != binResult.Major)
// Downgrade when Major version different (may have breaking changes)
reinstall = true;
}
else
{
// Parse File versionName to Version failed
if (!systemFileVersion.Equals(binFileVersion))
// versionNames are different, Reinstall
reinstall = true;
}
TYPE_BYPNAME,
if (!reinstall)
return;
TYPE_CLRNAME,
TYPE_CLRFIP,
//str addr x.x.x.x only ipv4
TYPE_REDIRCTOR_DNS,
TYPE_REDIRCTOR_ICMP
Logging.Info("更新驱动");
UninstallDriver();
InstallDriver();
}
#endregion
#region Utils
/// <summary>
/// 安装 NF 驱动
/// </summary>
@@ -341,5 +277,61 @@ namespace Netch.Controllers
}
#endregion
#region NativeMethods
private const int UdpNameListOffset = (int) NameList.TYPE_UDPTYPE - (int) NameList.TYPE_TCPTYPE;
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern bool aio_dial(int name, [MarshalAs(UnmanagedType.LPWStr)] string value);
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern bool aio_init();
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern bool aio_free();
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern ulong aio_getUP();
[DllImport("Redirector.bin", CallingConvention = CallingConvention.Cdecl)]
private static extern ulong aio_getDL();
public enum NameList
{
//bool
TYPE_FILTERLOOPBACK,
TYPE_FILTERTCP,
TYPE_FILTERUDP,
TYPE_FILTERIP,
TYPE_FILTERCHILDPROC, //子进程捕获
TYPE_TCPLISN,
TYPE_TCPTYPE,
TYPE_TCPHOST,
TYPE_TCPUSER,
TYPE_TCPPASS,
TYPE_TCPMETH,
TYPE_UDPTYPE,
TYPE_UDPHOST,
TYPE_UDPUSER,
TYPE_UDPPASS,
TYPE_UDPMETH,
TYPE_ADDNAME,
TYPE_ADDFIP,
TYPE_BYPNAME,
TYPE_CLRNAME,
TYPE_CLRFIP,
//str addr x.x.x.x only ipv4
TYPE_REDIRCTOR_DNS,
TYPE_REDIRCTOR_ICMP
}
#endregion
}
}

View File

@@ -47,7 +47,7 @@ namespace Netch.Controllers
Global.MainForm.BeginInvoke(new Action(() =>
{
if (!_form!.IsDisposed)
_form!.richTextBox1.AppendText(line + "\n");
_form.richTextBox1.AppendText(line + "\n");
}));
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -18,8 +19,8 @@ namespace Netch.Controllers
public const string Name = @"Netch";
public const string Copyright = @"Copyright © 2019 - 2021";
public const string AssemblyVersion = @"1.8.2";
private const string Suffix = @"";
public const string AssemblyVersion = @"1.8.3";
private const string Suffix = @"Beta2";
public static readonly string Version = $"{AssemblyVersion}{(string.IsNullOrEmpty(Suffix) ? "" : $"-{Suffix}")}";
@@ -89,5 +90,19 @@ namespace Netch.Controllers
fileName = match.Groups["filename"].Value;
sha256 = match.Groups["sha256"].Value;
}
public static string GetLatestReleaseContent()
{
var sb = new StringBuilder();
foreach (string l in LatestRelease.body.GetLines(false).SkipWhile(l => l.FirstOrDefault() != '#'))
{
if (l.Contains("校验和"))
break;
sb.AppendLine(l);
}
return sb.ToString();
}
}
}
}

View File

@@ -423,7 +423,7 @@
this.ModeComboBox.Size = new System.Drawing.Size(546, 24);
this.ModeComboBox.TabIndex = 2;
this.ModeComboBox.DrawItem += new System.Windows.Forms.DrawItemEventHandler(this.ComboBox_DrawItem);
this.ModeComboBox.SelectedIndexChanged += new System.EventHandler(this.ModeComboBox_SelectedIndexChanged);
this.ModeComboBox.SelectionChangeCommitted += new System.EventHandler(this.ModeComboBox_SelectionChangeCommitted);
//
// ServerComboBox
//
@@ -438,7 +438,7 @@
this.ServerComboBox.Size = new System.Drawing.Size(546, 24);
this.ServerComboBox.TabIndex = 1;
this.ServerComboBox.DrawItem += new System.Windows.Forms.DrawItemEventHandler(this.ComboBox_DrawItem);
this.ServerComboBox.SelectedIndexChanged += new System.EventHandler(this.ServerComboBox_SelectedIndexChanged);
this.ServerComboBox.SelectionChangeCommitted += new System.EventHandler(this.ServerComboBox_SelectionChangeCommitted);
//
// tableLayoutPanel2
//

View File

@@ -30,7 +30,6 @@ namespace Netch.Forms
private readonly Dictionary<string, object> _mainFormText = new();
private bool _comboBoxInitialized;
private bool _textRecorded;
public MainForm()
@@ -85,6 +84,8 @@ namespace Netch.Forms
private void MainForm_Load(object sender, EventArgs e)
{
Netch.TimePoint("MainForm ctor (Pre MainForm Load)");
// 计算 ComboBox绘制 目标宽度
RecordSize();
@@ -93,7 +94,6 @@ namespace Netch.Forms
ModeHelper.Load();
LoadModes();
_comboBoxInitialized = true;
// 加载翻译
TranslateControls();
@@ -121,6 +121,8 @@ namespace Netch.Forms
if (Global.Settings.StartWhenOpened)
ControlButton_Click(null, null);
});
Netch.TimePoint("Post Form Load", false);
}
private void RecordSize()
@@ -577,7 +579,8 @@ namespace Netch.Forms
return;
}
if (MessageBoxX.Show(i18N.Translate("Download and install now?"), confirm: true) != DialogResult.OK)
if (MessageBoxX.Show(i18N.Translate($"Download and install now?\n\n{UpdateChecker.GetLatestReleaseContent()}"), confirm: true) !=
DialogResult.OK)
return;
NotifyTip(i18N.Translate("Start downloading new version"));
@@ -735,13 +738,9 @@ namespace Netch.Forms
private void LoadServers()
{
var comboBoxInitialized = _comboBoxInitialized;
_comboBoxInitialized = false;
ServerComboBox.Items.Clear();
ServerComboBox.Items.AddRange(Global.Settings.Server.ToArray());
SelectLastServer();
_comboBoxInitialized = comboBoxInitialized;
}
public void SelectLastServer()
@@ -756,11 +755,8 @@ namespace Netch.Forms
// 如果当前 ServerComboBox 中没元素,不做处理
}
private void ServerComboBox_SelectedIndexChanged(object sender, EventArgs o)
private void ServerComboBox_SelectionChangeCommitted(object sender, EventArgs o)
{
if (!_comboBoxInitialized)
return;
Global.Settings.ServerComboBoxSelectedIndex = ServerComboBox.SelectedIndex;
}
@@ -859,14 +855,10 @@ namespace Netch.Forms
public void LoadModes()
{
var comboBoxInitialized = _comboBoxInitialized;
_comboBoxInitialized = false;
ModeComboBox.Items.Clear();
ModeComboBox.Items.AddRange(Global.Modes.ToArray());
ModeComboBox.Items.AddRange(Global.Modes.Cast<object>().ToArray());
ModeComboBox.Tag = null;
SelectLastMode();
_comboBoxInitialized = comboBoxInitialized;
}
public void SelectLastMode()
@@ -881,11 +873,8 @@ namespace Netch.Forms
// 如果当前 ModeComboBox 中没元素,不做处理
}
private void ModeComboBox_SelectedIndexChanged(object sender, EventArgs o)
private void ModeComboBox_SelectionChangeCommitted(object sender, EventArgs o)
{
if (!_comboBoxInitialized)
return;
try
{
Global.Settings.ModeComboBoxSelectedIndex = Global.Modes.IndexOf((Models.Mode) ModeComboBox.SelectedItem);

View File

@@ -31,25 +31,21 @@ namespace Netch.Forms.Mode
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.ConfigurationGroupBox = new System.Windows.Forms.GroupBox();
this.RemarkLabel = new System.Windows.Forms.Label();
this.RemarkTextBox = new System.Windows.Forms.TextBox();
this.FilenameLabel = new System.Windows.Forms.Label();
this.FilenameTextBox = new System.Windows.Forms.TextBox();
this.containerControl1 = new System.Windows.Forms.ContainerControl();
this.RuleListBox = new System.Windows.Forms.ListBox();
this.RuleRichTextBox = new System.Windows.Forms.RichTextBox();
this.ProcessGroupBox = new System.Windows.Forms.GroupBox();
this.ProcessNameTextBox = new System.Windows.Forms.TextBox();
this.AddButton = new System.Windows.Forms.Button();
this.SelectButton = new System.Windows.Forms.Button();
this.contextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components);
this.DeleteToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.ScanButton = new System.Windows.Forms.Button();
this.ValidationButton = new System.Windows.Forms.Button();
this.ControlButton = new System.Windows.Forms.Button();
this.ConfigurationGroupBox.SuspendLayout();
this.containerControl1.SuspendLayout();
this.ProcessGroupBox.SuspendLayout();
this.contextMenuStrip.SuspendLayout();
this.SuspendLayout();
//
// ConfigurationGroupBox
@@ -60,11 +56,12 @@ namespace Netch.Forms.Mode
this.ConfigurationGroupBox.Controls.Add(this.FilenameTextBox);
this.ConfigurationGroupBox.Controls.Add(this.containerControl1);
this.ConfigurationGroupBox.Controls.Add(this.ProcessGroupBox);
this.ConfigurationGroupBox.Controls.Add(this.SelectButton);
this.ConfigurationGroupBox.Location = new System.Drawing.Point(12, 12);
this.ConfigurationGroupBox.Controls.Add(this.ControlButton);
this.ConfigurationGroupBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.ConfigurationGroupBox.Location = new System.Drawing.Point(12, 5);
this.ConfigurationGroupBox.Name = "ConfigurationGroupBox";
this.ConfigurationGroupBox.Size = new System.Drawing.Size(340, 344);
this.ConfigurationGroupBox.TabIndex = 1;
this.ConfigurationGroupBox.Size = new System.Drawing.Size(431, 378);
this.ConfigurationGroupBox.TabIndex = 0;
this.ConfigurationGroupBox.TabStop = false;
this.ConfigurationGroupBox.Text = "Configuration";
//
@@ -81,7 +78,7 @@ namespace Netch.Forms.Mode
//
this.RemarkTextBox.Location = new System.Drawing.Point(84, 22);
this.RemarkTextBox.Name = "RemarkTextBox";
this.RemarkTextBox.Size = new System.Drawing.Size(250, 23);
this.RemarkTextBox.Size = new System.Drawing.Size(341, 23);
this.RemarkTextBox.TabIndex = 1;
this.RemarkTextBox.TextChanged += new System.EventHandler(this.RemarkTextBox_TextChanged);
//
@@ -99,86 +96,76 @@ namespace Netch.Forms.Mode
this.FilenameTextBox.Location = new System.Drawing.Point(84, 52);
this.FilenameTextBox.Name = "FilenameTextBox";
this.FilenameTextBox.ReadOnly = true;
this.FilenameTextBox.Size = new System.Drawing.Size(250, 23);
this.FilenameTextBox.Size = new System.Drawing.Size(341, 23);
this.FilenameTextBox.TabIndex = 3;
//
// containerControl1
//
this.containerControl1.Controls.Add(this.RuleListBox);
this.containerControl1.Controls.Add(this.RuleRichTextBox);
this.containerControl1.Location = new System.Drawing.Point(6, 81);
this.containerControl1.Name = "containerControl1";
this.containerControl1.Size = new System.Drawing.Size(328, 176);
this.containerControl1.TabIndex = 5;
this.containerControl1.Size = new System.Drawing.Size(419, 221);
this.containerControl1.TabIndex = 4;
this.containerControl1.Text = "containerControl1";
//
// RuleListBox
// RuleRichTextBox
//
this.RuleListBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.RuleListBox.FormattingEnabled = true;
this.RuleListBox.ItemHeight = 17;
this.RuleListBox.Location = new System.Drawing.Point(0, 0);
this.RuleListBox.Name = "RuleListBox";
this.RuleListBox.Size = new System.Drawing.Size(328, 176);
this.RuleListBox.TabIndex = 0;
this.RuleListBox.MouseUp += new System.Windows.Forms.MouseEventHandler(this.RuleListBox_MouseUp);
this.RuleRichTextBox.DetectUrls = false;
this.RuleRichTextBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.RuleRichTextBox.Location = new System.Drawing.Point(0, 0);
this.RuleRichTextBox.Name = "RuleRichTextBox";
this.RuleRichTextBox.Size = new System.Drawing.Size(419, 221);
this.RuleRichTextBox.TabIndex = 0;
this.RuleRichTextBox.Text = "";
this.RuleRichTextBox.WordWrap = false;
//
// ProcessGroupBox
//
this.ProcessGroupBox.Controls.Add(this.ProcessNameTextBox);
this.ProcessGroupBox.Controls.Add(this.AddButton);
this.ProcessGroupBox.Location = new System.Drawing.Point(6, 263);
this.ProcessGroupBox.Controls.Add(this.SelectButton);
this.ProcessGroupBox.Controls.Add(this.ScanButton);
this.ProcessGroupBox.Controls.Add(this.ValidationButton);
this.ProcessGroupBox.Location = new System.Drawing.Point(6, 295);
this.ProcessGroupBox.Name = "ProcessGroupBox";
this.ProcessGroupBox.Size = new System.Drawing.Size(328, 46);
this.ProcessGroupBox.TabIndex = 6;
this.ProcessGroupBox.Size = new System.Drawing.Size(419, 44);
this.ProcessGroupBox.TabIndex = 5;
this.ProcessGroupBox.TabStop = false;
//
// ProcessNameTextBox
//
this.ProcessNameTextBox.Location = new System.Drawing.Point(6, 15);
this.ProcessNameTextBox.Name = "ProcessNameTextBox";
this.ProcessNameTextBox.Size = new System.Drawing.Size(222, 23);
this.ProcessNameTextBox.TabIndex = 0;
//
// AddButton
//
this.AddButton.Location = new System.Drawing.Point(247, 15);
this.AddButton.Name = "AddButton";
this.AddButton.Size = new System.Drawing.Size(75, 23);
this.AddButton.TabIndex = 1;
this.AddButton.Text = "Add";
this.AddButton.UseVisualStyleBackColor = true;
this.AddButton.Click += new System.EventHandler(this.AddButton_Click);
//
// SelectButton
//
this.SelectButton.Location = new System.Drawing.Point(6, 315);
this.SelectButton.Location = new System.Drawing.Point(6, 13);
this.SelectButton.Name = "SelectButton";
this.SelectButton.Size = new System.Drawing.Size(75, 23);
this.SelectButton.TabIndex = 7;
this.SelectButton.TabIndex = 0;
this.SelectButton.Text = "Select";
this.SelectButton.UseVisualStyleBackColor = true;
this.SelectButton.Click += new System.EventHandler(this.SelectButton_Click);
//
// contextMenuStrip
// ScanButton
//
this.contextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.DeleteToolStripMenuItem});
this.contextMenuStrip.Name = "contextMenuStrip";
this.contextMenuStrip.Size = new System.Drawing.Size(114, 26);
this.ScanButton.Location = new System.Drawing.Point(87, 13);
this.ScanButton.Name = "ScanButton";
this.ScanButton.Size = new System.Drawing.Size(75, 23);
this.ScanButton.TabIndex = 1;
this.ScanButton.Text = "Scan";
this.ScanButton.UseVisualStyleBackColor = true;
this.ScanButton.Click += new System.EventHandler(this.ScanButton_Click);
//
// DeleteToolStripMenuItem
// ValidationButton
//
this.DeleteToolStripMenuItem.Name = "DeleteToolStripMenuItem";
this.DeleteToolStripMenuItem.Size = new System.Drawing.Size(113, 22);
this.DeleteToolStripMenuItem.Text = "Delete";
this.DeleteToolStripMenuItem.Click += new System.EventHandler(this.deleteRule_Click);
this.ValidationButton.Location = new System.Drawing.Point(338, 13);
this.ValidationButton.Name = "ValidationButton";
this.ValidationButton.Size = new System.Drawing.Size(75, 23);
this.ValidationButton.TabIndex = 2;
this.ValidationButton.Text = "Validation";
this.ValidationButton.UseVisualStyleBackColor = true;
this.ValidationButton.Click += new System.EventHandler(this.ValidationButton_Click);
//
// ControlButton
//
this.ControlButton.Location = new System.Drawing.Point(277, 362);
this.ControlButton.Location = new System.Drawing.Point(344, 345);
this.ControlButton.Name = "ControlButton";
this.ControlButton.Size = new System.Drawing.Size(75, 23);
this.ControlButton.TabIndex = 2;
this.ControlButton.TabIndex = 6;
this.ControlButton.Text = "Save";
this.ControlButton.UseVisualStyleBackColor = true;
this.ControlButton.Click += new System.EventHandler(this.ControlButton_Click);
@@ -187,14 +174,14 @@ namespace Netch.Forms.Mode
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(364, 397);
this.ClientSize = new System.Drawing.Size(455, 388);
this.Controls.Add(this.ConfigurationGroupBox);
this.Controls.Add(this.ControlButton);
this.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.MaximizeBox = false;
this.Name = "Process";
this.Padding = new System.Windows.Forms.Padding(12, 5, 12, 5);
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Create Process Mode";
this.Load += new System.EventHandler(this.ModeForm_Load);
@@ -202,27 +189,23 @@ namespace Netch.Forms.Mode
this.ConfigurationGroupBox.PerformLayout();
this.containerControl1.ResumeLayout(false);
this.ProcessGroupBox.ResumeLayout(false);
this.ProcessGroupBox.PerformLayout();
this.contextMenuStrip.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Button ScanButton;
private System.Windows.Forms.ContainerControl containerControl1;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip;
private System.Windows.Forms.ToolStripMenuItem DeleteToolStripMenuItem;
public System.Windows.Forms.GroupBox ConfigurationGroupBox;
private System.Windows.Forms.Label RemarkLabel;
private System.Windows.Forms.GroupBox ProcessGroupBox;
private System.Windows.Forms.ListBox RuleListBox;
private System.Windows.Forms.TextBox RemarkTextBox;
private System.Windows.Forms.TextBox ProcessNameTextBox;
private System.Windows.Forms.Button AddButton;
private System.Windows.Forms.Button SelectButton;
public System.Windows.Forms.Button ControlButton;
private System.Windows.Forms.Label FilenameLabel;
private System.Windows.Forms.TextBox FilenameTextBox;
private RichTextBox RuleRichTextBox;
private Button ValidationButton;
}
}

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.WindowsAPICodePack.Dialogs;
using Netch.Controllers;
using Netch.Models;
using Netch.Properties;
using Netch.Utils;
@@ -33,10 +35,24 @@ namespace Netch.Forms.Mode
_mode = mode;
}
/// <summary>
/// 是否被编辑过
/// </summary>
public bool Edited { get; private set; }
#region Model
public IEnumerable<string> Rules => RuleRichTextBox.Lines;
private void RuleAdd(string value)
{
RuleRichTextBox.AppendText($"{value}\n");
}
private void RuleAddRange(IEnumerable<string> value)
{
foreach (string s in value)
{
RuleAdd(s);
}
}
#endregion
public void ModeForm_Load(object sender, EventArgs e)
{
@@ -47,60 +63,10 @@ namespace Netch.Forms.Mode
RemarkTextBox.TextChanged -= RemarkTextBox_TextChanged;
RemarkTextBox.Text = _mode.Remark;
FilenameTextBox.Text = _mode.RelativePath;
RuleListBox.Items.AddRange(_mode.Rule.Cast<object>().ToArray());
RuleAddRange(_mode.Rule);
}
i18N.TranslateForm(this);
i18N.Translate(contextMenuStrip);
}
/// <summary>
/// listBox右键菜单
/// </summary>
private void RuleListBox_MouseUp(object sender, MouseEventArgs e)
{
RuleListBox.SelectedIndex = RuleListBox.IndexFromPoint(e.X, e.Y);
if (RuleListBox.SelectedIndex == -1)
return;
if (e.Button == MouseButtons.Right)
contextMenuStrip.Show(RuleListBox, e.Location);
}
private void deleteRule_Click(object sender, EventArgs e)
{
if (RuleListBox.SelectedIndex == -1)
return;
RuleListBox.Items.RemoveAt(RuleListBox.SelectedIndex);
Edited = true;
}
private async void AddButton_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
if (string.IsNullOrWhiteSpace(ProcessNameTextBox.Text))
{
MessageBoxX.Show(i18N.Translate("rule can not be empty"));
return;
}
if (!NFController.CheckCppRegex(ProcessNameTextBox.Text))
{
MessageBoxX.Show("Rule does not conform to C++ regular expression syntax");
return;
}
var process = ProcessNameTextBox.Text;
if (!RuleListBox.Items.Contains(process))
RuleListBox.Items.Add(process);
Edited = true;
RuleListBox.SelectedIndex = RuleListBox.Items.IndexOf(process);
ProcessNameTextBox.Text = string.Empty;
});
}
private void SelectButton_Click(object sender, EventArgs e)
@@ -108,7 +74,7 @@ namespace Netch.Forms.Mode
var dialog = new CommonOpenFileDialog
{
IsFolderPicker = true,
Multiselect = false,
Multiselect = true,
Title = i18N.Translate("Select a folder"),
AddToMostRecentlyUsedList = false,
EnsurePathExists = true,
@@ -117,17 +83,20 @@ namespace Netch.Forms.Mode
if (dialog.ShowDialog(Handle) == CommonFileDialogResult.Ok)
{
var path = dialog.FileName;
if (!path.EndsWith(@"\"))
path += @"\";
foreach (string p in dialog.FileNames)
{
string path = p;
if (!path.EndsWith(@"\"))
path += @"\";
RuleListBox.Items.Add(path.ToRegexString());
RuleAdd($"^{path.ToRegexString()}");
}
}
}
public void ControlButton_Click(object sender, EventArgs e)
{
if (RuleListBox.Items.Count == 0)
if (!RuleRichTextBox.Lines.Any())
{
MessageBoxX.Show(i18N.Translate("Unable to add empty rule"));
return;
@@ -149,11 +118,9 @@ namespace Netch.Forms.Mode
{
_mode.Remark = RemarkTextBox.Text;
_mode.Rule.Clear();
_mode.Rule.AddRange(RuleListBox.Items.Cast<string>());
_mode.Rule.AddRange(RuleRichTextBox.Lines);
_mode.WriteFile();
Global.MainForm.LoadModes();
Edited = false;
MessageBoxX.Show(i18N.Translate("Mode updated successfully"));
}
else
@@ -173,10 +140,9 @@ namespace Netch.Forms.Mode
Remark = RemarkTextBox.Text
};
mode.Rule.AddRange(RuleListBox.Items.Cast<string>());
mode.Rule.AddRange(RuleRichTextBox.Lines);
mode.WriteFile();
ModeHelper.Add(mode);
MessageBoxX.Show(i18N.Translate("Mode added successfully"));
}
@@ -190,5 +156,57 @@ namespace Netch.Forms.Mode
FilenameTextBox.Text = FilenameTextBox.Text = ModeEditorUtils.GetCustomModeRelativePath(RemarkTextBox.Text);
}));
}
private void ScanButton_Click(object sender, EventArgs e)
{
var dialog = new CommonOpenFileDialog
{
IsFolderPicker = true,
Multiselect = false,
Title = i18N.Translate("Select a folder"),
AddToMostRecentlyUsedList = false,
EnsurePathExists = true,
NavigateToShortcut = true
};
if (dialog.ShowDialog(Handle) == CommonFileDialogResult.Ok)
{
var path = dialog.FileName;
var list = new List<string>();
const uint maxCount = 50;
try
{
ScanDirectory(path, list);
}
catch
{
MessageBoxX.Show(i18N.Translate($"The number of executable files in the \"{path}\" directory is greater than {maxCount}"),
LogLevel.WARNING);
return;
}
RuleAddRange(list);
}
}
private void ScanDirectory(string directory, List<string> list, uint maxCount = 30)
{
foreach (string dir in Directory.GetDirectories(directory))
ScanDirectory(dir, list, maxCount);
list.AddRange(Directory.GetFiles(directory).Select(Path.GetFileName).Where(s => s.EndsWith(".exe")).Select(s => s.ToRegexString()));
if (maxCount != 0 && list.Count > maxCount)
throw new Exception("The number of results is greater than maxCount");
}
private void ValidationButton_Click(object sender, EventArgs e)
{
if (NFController.CheckRules(Rules, out var results))
MessageBoxX.Show(NFController.GenerateInvalidRulesMessage(results), LogLevel.WARNING);
else
MessageBoxX.Show("Fine");
}
}
}

View File

@@ -83,7 +83,6 @@ namespace Netch.Forms.Mode
_mode.Type = (int) comboBox1.SelectedValue;
_mode.WriteFile();
Global.MainForm.LoadModes();
MessageBoxX.Show(i18N.Translate("Mode updated successfully"));
}
else
@@ -105,7 +104,6 @@ namespace Netch.Forms.Mode
mode.Rule.AddRange(richTextBox1.Lines);
mode.WriteFile();
ModeHelper.Add(mode);
MessageBoxX.Show(i18N.Translate("Mode added successfully"));
}

View File

@@ -4,7 +4,6 @@ using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Windows.Forms;
using WindowsJobAPI;
using Netch.Controllers;
using Netch.Forms;
using Netch.Models;
@@ -34,12 +33,6 @@ namespace Netch
public static Mutex Mutex => LazyMutex.Value;
#if DEBUG
public static bool Testing = false;
#else
public const bool Testing = false;
#endif
/// <summary>
/// 用于读取和写入的配置
/// </summary>
@@ -50,11 +43,6 @@ namespace Netch
/// </summary>
public static readonly List<Mode> Modes = new();
/// <summary>
/// Windows Job API
/// </summary>
public static readonly JobObject Job = new();
public static class Flags
{
public static readonly bool IsWindows10Upper = Environment.OSVersion.Version.Major >= 10;

View File

@@ -1,54 +1,28 @@
using System;
using System.Text;
using System.Linq;
namespace Netch.Models.GitHubRelease
{
[Serializable]
public struct SuffixVersion : ICloneable, IComparable, IComparable<SuffixVersion>, IEquatable<SuffixVersion>
public struct SuffixVersion : IComparable, IComparable<SuffixVersion>
{
public int Major { get; }
public Version Version { get; }
public int Minor { get; }
public string Suffix { get; }
public int Patch { get; }
public string PreRelease { get; }
public int Build { get; }
public SuffixVersion(int major, int minor, int patch, string preRelease, int build)
public SuffixVersion(Version version, string suffix)
{
Major = major;
Minor = minor;
Patch = patch;
PreRelease = preRelease;
Build = build;
}
public SuffixVersion(Version version, string preRelease, int build)
{
Major = version.Major;
Minor = version.Minor;
Patch = version.Build;
PreRelease = preRelease;
Build = build;
Version = version;
Suffix = suffix;
}
public static SuffixVersion Parse(string input)
{
var splitStr = input.Split('-');
var dotNetVersion = Version.Parse(splitStr[0]);
var preRelease = new StringBuilder();
var build = 0;
var split = input.Split('-');
var dotNetVersion = Version.Parse(split[0]);
var preRelease = split.ElementAtOrDefault(1) ?? string.Empty;
if (splitStr.Length > 1)
foreach (var c in splitStr[1])
if (int.TryParse(c.ToString(), out var n))
build = build * 10 + n;
else
preRelease.Append(c);
return new SuffixVersion(dotNetVersion, preRelease.ToString(), build);
return new SuffixVersion(dotNetVersion, preRelease);
}
public static bool TryParse(string input, out SuffixVersion result)
@@ -65,17 +39,12 @@ namespace Netch.Models.GitHubRelease
}
}
public object Clone()
{
return new SuffixVersion(Major, Major, Patch, PreRelease, Build);
}
public int CompareTo(object obj)
{
if (obj is SuffixVersion version)
return CompareTo(version);
if (obj is not SuffixVersion version)
throw new ArgumentOutOfRangeException();
return -1;
return CompareTo(version);
}
/// <summary>
@@ -86,57 +55,27 @@ namespace Netch.Models.GitHubRelease
/// </returns>
public int CompareTo(SuffixVersion other)
{
var majorComparison = Major.CompareTo(other.Major);
if (majorComparison != 0)
return majorComparison;
var versionComparison = Version.CompareTo(other.Version);
if (versionComparison != 0)
return versionComparison;
var minorComparison = Minor.CompareTo(other.Minor);
if (minorComparison != 0)
return minorComparison;
if (Suffix == string.Empty)
return other.Suffix == string.Empty ? 0 : 1;
var patchComparison = Patch.CompareTo(other.Patch);
if (patchComparison != 0)
return patchComparison;
if (PreRelease == string.Empty)
return other.PreRelease == string.Empty ? 0 : 1;
if (other.PreRelease == string.Empty)
if (other.Suffix == string.Empty)
return -1;
var suffixComparison = string.Compare(PreRelease, other.PreRelease, StringComparison.Ordinal);
if (suffixComparison != 0)
return suffixComparison;
return Build.CompareTo(other.Build);
}
public bool Equals(SuffixVersion other)
{
return Major == other.Major && Minor == other.Minor && Patch == other.Patch && PreRelease == other.PreRelease && Build == other.Build;
}
public override bool Equals(object obj)
{
return obj is SuffixVersion other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = Major;
hashCode = (hashCode * 397) ^ Minor;
hashCode = (hashCode * 397) ^ Patch;
hashCode = (hashCode * 397) ^ (PreRelease != null ? PreRelease.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ Build;
return hashCode;
}
var suffixComparison = string.Compare(Suffix, other.Suffix, StringComparison.OrdinalIgnoreCase);
return suffixComparison;
}
public override string ToString()
{
return $"{Major}.{Minor}.{Patch}{(string.IsNullOrEmpty(PreRelease) ? "" : "-")}{PreRelease}{(Build == 0 ? "" : Build.ToString())}";
var s = Version.ToString();
if (Suffix != string.Empty)
s += $"-{Suffix}";
return s;
}
}
}

View File

@@ -10,12 +10,41 @@ namespace Netch.Models
{
public class Mode
{
public readonly string? FullName;
private readonly Lazy<List<string>> _lazyRule;
public string? FullName { get; private set; }
public Mode(string? fullName = default)
{
_lazyRule = new Lazy<List<string>>(ReadRules);
if (fullName == null)
return;
FullName = fullName;
if (!File.Exists(FullName))
return;
var text = File.ReadLines(FullName).First();
// load head
if (text.First() != '#')
throw new Exception($"mode {FullName} head not found at Line 0");
var split = text.Substring(1).SplitTrimEntries(',');
Remark = split[0];
var typeResult = int.TryParse(split.ElementAtOrDefault(1), out var type);
Type = typeResult ? type : 0;
// TODO throw NotSupportedModeTypeException
var bypassChinaResult = int.TryParse(split.ElementAtOrDefault(2), out var bypassChina);
BypassChina = this.ClientRouting() && bypassChinaResult && bypassChina == 1;
}
/// <summary>
/// 规则
/// </summary>
public readonly List<string> Rule = new();
public List<string> Rule => _lazyRule.Value;
/// <summary>
/// 绕过中国0. 不绕过 1. 绕过)
@@ -45,15 +74,6 @@ namespace Netch.Models
/// </summary>
public int Type { get; set; } = 0;
public Mode(string fullName)
{
FullName = fullName;
}
public Mode()
{
}
/// <summary>
/// 文件相对路径(必须是存在的文件)
/// </summary>
@@ -79,7 +99,7 @@ namespace Netch.Models
relativePath.Replace(">", "");
relativePath.Replace(".h", ".txt");
var mode = Global.Modes.FirstOrDefault(m => m!.FullName != null && m.RelativePath!.Equals(relativePath.ToString()));
var mode = Global.Modes.FirstOrDefault(m => m.FullName != null && m.RelativePath!.Equals(relativePath.ToString()));
if (mode == null)
throw new MessageException($"{relativePath} file included in {Remark} not found");
@@ -105,6 +125,14 @@ namespace Netch.Models
}
}
private List<string> ReadRules()
{
if (FullName == null || !File.Exists(FullName))
return new List<string>();
return File.ReadLines(FullName!).Skip(1).ToList();
}
public void WriteFile(string? fullName = null)
{
if (fullName != null)

View File

@@ -0,0 +1,121 @@
using System;
using System.Linq;
using System.Reflection;
using Netch.Utils;
namespace Netch.Models
{
public abstract class ParameterBase
{
// null value par
private readonly bool _full;
protected readonly string ParametersSeparate = " ";
protected readonly string Separate = " ";
protected readonly string VerbPrefix = "-";
protected readonly string FullPrefix = "--";
protected ParameterBase()
{
_full = !GetType().IsDefined(typeof(VerbAttribute));
}
public override string ToString()
{
var parameters = GetType().GetProperties().Select(PropToParameter).Where(s => s != null).Cast<string>();
return string.Join(ParametersSeparate, parameters).Trim();
}
private string? PropToParameter(PropertyInfo p)
{
// prefix
bool full;
if (p.IsDefined(typeof(VerbAttribute)))
full = false;
else if (p.IsDefined(typeof(FullAttribute)))
full = true;
else
full = _full;
var prefix = full ? FullPrefix : VerbPrefix;
// key
var key = p.GetCustomAttribute<RealNameAttribute>()?.Name ?? p.Name;
// build
var value = p.GetValue(this);
switch (value)
{
case bool b:
return b ? $"{prefix}{key}" : null;
default:
if ((value?.ToString() ?? null).IsNullOrWhiteSpace())
return p.IsDefined(typeof(OptionalAttribute)) ? null : throw new RequiredArgumentValueInvalidException(p.Name, this, null);
if (p.IsDefined(typeof(QuoteAttribute)))
value = $"\"{value}\"";
return $"{prefix}{key}{Separate}{value}";
}
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class VerbAttribute : Attribute
{
// Don't use verb and full both on one class or property
// if you did, will take verb
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class FullAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Property)]
public class OptionalAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Property)]
public class QuoteAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Property)]
public class RealNameAttribute : Attribute
{
public string Name { get; }
public RealNameAttribute(string name)
{
Name = name;
}
}
[Serializable]
public class RequiredArgumentValueInvalidException : Exception
{
public string? ArgumentName { get; }
public object? ArgumentObject { get; }
private readonly string? _message;
private const string DefaultMessage = "{0}'s Argument \"{1}\" value invalid. A required argument's value can't be null or empty.";
public override string Message => _message ?? string.Format(DefaultMessage, ArgumentObject!.GetType(), ArgumentName);
public RequiredArgumentValueInvalidException()
{
_message = "Some Argument value invalid. A required argument value's can't be null or empty.";
}
public RequiredArgumentValueInvalidException(string argumentName, object argumentObject, string? message)
{
ArgumentName = argumentName;
ArgumentObject = argumentObject;
_message = message;
}
}
}

View File

@@ -16,15 +16,41 @@ namespace Netch
{
public static class Netch
{
private static readonly Stopwatch Stopwatch = new();
public static void StartStopwatch(string name)
{
if (Stopwatch.IsRunning)
throw new Exception();
Stopwatch.Start();
Console.WriteLine($"Start {name} Stopwatch");
}
public static void TimePoint(string name, bool restart = true)
{
if (!Stopwatch.IsRunning)
throw new Exception();
Stopwatch.Stop();
Console.WriteLine($"{name} Stopwatch: {Stopwatch.ElapsedMilliseconds}");
if (restart)
Stopwatch.Restart();
}
/// <summary>
/// 应用程序的主入口点
/// </summary>
[STAThread]
public static void Main(string[] args)
{
#if DEBUG
AttachConsole();
#else
if (args.Contains("-console"))
if (!NativeMethods.AttachConsole(-1))
NativeMethods.AllocConsole();
AttachConsole();
#endif
StartStopwatch("Netch");
// 设置当前目录
Directory.SetCurrentDirectory(Global.NetchDir);
@@ -40,9 +66,11 @@ namespace Netch
if (!Directory.Exists(item))
Directory.CreateDirectory(item);
TimePoint("Clean Old, Create Directory");
// 加载配置
Configuration.Load();
TimePoint("Load Configuration");
// 检查是否已经运行
if (!Global.Mutex.WaitOne(0, false))
{
@@ -76,6 +104,8 @@ namespace Netch
Logging.Info($"版本: {UpdateChecker.Owner}/{UpdateChecker.Repo}@{UpdateChecker.Version}");
Task.Run(() => { Logging.Info($"主程序 SHA256: {Utils.Utils.SHA256CheckSum(Global.NetchExecutable)}"); });
TimePoint("Get Info, Pre-Form");
// 绑定错误捕获
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
Application.ThreadException += Application_OnException;
@@ -85,6 +115,12 @@ namespace Netch
Application.Run(Global.MainForm);
}
private static void AttachConsole()
{
if (!NativeMethods.AttachConsole(-1))
NativeMethods.AllocConsole();
}
public static void Application_OnException(object sender, ThreadExceptionEventArgs e)
{
Logging.Error(e.Exception.ToString());

View File

@@ -14,6 +14,9 @@
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<!-- <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>-->
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
@@ -43,15 +46,18 @@
<PackageReference Include="MaxMind.GeoIP2" Version="4.0.1" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.66" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Nullable.Extended.Analyzer" Version="1.2.4089">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Include="System.Reflection.Metadata" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.1" />
<PackageReference Include="TaskScheduler" Version="2.9.1" />
<PackageReference Include="Vanara.PInvoke.IpHlpApi" Version="3.3.5" />
<PackageReference Include="Vanara.PInvoke.IpHlpApi" Version="3.3.6" />
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.4" />
<PackageReference Include="Vanara.PInvoke.User32" Version="3.3.5" />
<PackageReference Include="Vanara.PInvoke.User32" Version="3.3.6" />
<PackageReference Include="WindowsFirewallHelper" Version="2.0.4.70-beta2" />
<PackageReference Include="WindowsJobAPI" Version="5.0.1" />
<PackageReference Include="WindowsProxy" Version="5.0.3" />
</ItemGroup>

View File

@@ -1,10 +1,10 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
@@ -13,13 +13,13 @@ namespace Netch.Properties {
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -33,7 +33,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +47,8 @@ namespace Netch.Properties {
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
@@ -61,7 +61,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// 查找 System.Byte[] 类型的本地化资源。
/// </summary>
internal static byte[] _7za {
get {
@@ -71,17 +71,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] abp_js {
get {
object obj = ResourceManager.GetObject("abp_js", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// 查找 System.Drawing.Bitmap 类型的本地化资源。
/// </summary>
internal static System.Drawing.Bitmap CopyLink {
get {
@@ -91,17 +81,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] defaultTUNTAP {
get {
object obj = ResourceManager.GetObject("defaultTUNTAP", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// 查找 System.Drawing.Bitmap 类型的本地化资源。
/// </summary>
internal static System.Drawing.Bitmap delete {
get {
@@ -111,7 +91,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// 查找 System.Drawing.Bitmap 类型的本地化资源。
/// </summary>
internal static System.Drawing.Bitmap edit {
get {
@@ -121,7 +101,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// 查找类似于 (图标) 的 System.Drawing.Icon 类型的本地化资源。
/// </summary>
internal static System.Drawing.Icon icon {
get {
@@ -131,7 +111,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// 查找 System.Drawing.Bitmap 类型的本地化资源。
/// </summary>
internal static System.Drawing.Bitmap Netch {
get {
@@ -141,7 +121,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// 查找 System.Drawing.Bitmap 类型的本地化资源。
/// </summary>
internal static System.Drawing.Bitmap speed {
get {
@@ -151,7 +131,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// 查找 System.Drawing.Bitmap 类型的本地化资源。
/// </summary>
internal static System.Drawing.Bitmap Sponsor {
get {
@@ -161,7 +141,7 @@ namespace Netch.Properties {
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// 查找 System.Byte[] 类型的本地化资源。
/// </summary>
internal static byte[] zh_CN {
get {

View File

@@ -118,10 +118,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="defaultTUNTAP" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\defaultTUNTAP;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="zh_CN" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zh-CN;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089</value>
@@ -150,10 +146,6 @@
<value>..\Resources\CopyLink.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="abp_js" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\abp.js.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="7za" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\7za.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089</value>

Binary file not shown.

View File

@@ -1,6 +0,0 @@
[Generic]
Address = 10.0.236.10
Netmask = 255.255.255.0
Gateway = 10.0.236.1
DNS = 1.1.1.1
UseCustomDNS = False

View File

@@ -1,4 +1,5 @@
using Netch.Forms;
using Netch.Utils;
namespace Netch.Servers.Shadowsocks.Form
{
@@ -8,7 +9,7 @@ namespace Netch.Servers.Shadowsocks.Form
{
server ??= new Shadowsocks();
Server = server;
CreateTextBox("Password", "Password", s => true, s => server.Password = s, server.Password);
CreateTextBox("Password", "Password", s => !s.IsNullOrWhiteSpace(), s => server.Password = s, server.Password);
CreateComboBox("EncryptMethod", "Encrypt Method", SSGlobal.EncryptMethods, s => server.EncryptMethod = s, server.EncryptMethod);
CreateTextBox("Plugin", "Plugin", s => true, s => server.Plugin = s, server.Plugin);
CreateTextBox("PluginsOption", "Plugin Options", s => true, s => server.PluginOption = s, server.PluginOption);

View File

@@ -24,26 +24,60 @@ namespace Netch.Servers.Shadowsocks
{
var server = (Shadowsocks) s;
#region Argument
var argument = new StringBuilder();
argument.Append($"-s {server.AutoResolveHostname()} " + $"-p {server.Port} " + $"-b {this.LocalAddress()} " +
$"-l {this.Socks5LocalPort()} " + $"-m {server.EncryptMethod} " + $"-k \"{server.Password}\" " + "-u");
if (!string.IsNullOrWhiteSpace(server.Plugin) && !string.IsNullOrWhiteSpace(server.PluginOption))
argument.Append($" --plugin {server.Plugin}" + $" --plugin-opts \"{server.PluginOption}\"");
var command = new SSParameter
{
s = server.AutoResolveHostname(),
p = server.Port,
b = this.LocalAddress(),
l = this.Socks5LocalPort(),
m = server.EncryptMethod,
k = server.Password,
u = true,
plugin = server.Plugin,
plugin_opts = server.PluginOption
};
if (mode.BypassChina)
argument.Append($" --acl \"{Path.GetFullPath(File.Exists(Global.UserACL) ? Global.UserACL : Global.BuiltinACL)}\"");
command.acl = $"{Path.GetFullPath(File.Exists(Global.UserACL) ? Global.UserACL : Global.BuiltinACL)}";
#endregion
StartInstanceAuto(command.ToString());
}
StartInstanceAuto(argument.ToString());
[Verb]
private class SSParameter : ParameterBase
{
public string? s { get; set; }
public ushort? p { get; set; }
public string? b { get; set; }
public ushort? l { get; set; }
public string? m { get; set; }
public string? k { get; set; }
public bool u { get; set; }
[Full]
[Optional]
public string? plugin { get; set; }
[Full]
[Optional]
[RealName("plugin-opts")]
public string? plugin_opts { get; set; }
[Full]
[Quote]
[Optional]
public string? acl { get; set; }
}
public override void Stop()
{
StopInstance();
StopInstance();
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Netch.Servers.Shadowsocks
/// <summary>
/// 密码
/// </summary>
public string? Password { get; set; }
public string Password { get; set; } = string.Empty;
/// <summary>
/// 插件

View File

@@ -1,4 +1,5 @@
using Netch.Forms;
using Netch.Utils;
namespace Netch.Servers.ShadowsocksR.Form
{
@@ -8,7 +9,7 @@ namespace Netch.Servers.ShadowsocksR.Form
{
server ??= new ShadowsocksR();
Server = server;
CreateTextBox("Password", "Password", s => true, s => server.Password = s, server.Password);
CreateTextBox("Password", "Password", s => !s.IsNullOrWhiteSpace(), s => server.Password = s, server.Password);
CreateComboBox("EncryptMethod", "Encrypt Method", SSRGlobal.EncryptMethods, s => server.EncryptMethod = s, server.EncryptMethod);
CreateComboBox("Protocol", "Protocol", SSRGlobal.Protocols, s => server.Protocol = s, server.Protocol);
CreateTextBox("ProtocolParam", "Protocol Param", s => true, s => server.ProtocolParam = s, server.ProtocolParam);

View File

@@ -24,31 +24,64 @@ namespace Netch.Servers.ShadowsocksR
{
var server = (ShadowsocksR) s;
#region Argument
var argument = new StringBuilder();
argument.Append($"-s {server.AutoResolveHostname()} -p {server.Port} -k \"{server.Password}\" -m {server.EncryptMethod} -t 120");
if (!string.IsNullOrEmpty(server.Protocol))
var command = new SSRParameter
{
argument.Append($" -O {server.Protocol}");
if (!string.IsNullOrEmpty(server.ProtocolParam))
argument.Append($" -G \"{server.ProtocolParam}\"");
}
s = server.AutoResolveHostname(),
p = server.Port,
k = server.Password,
m = server.EncryptMethod,
t = "120",
O = server.Protocol,
G = server.ProtocolParam,
o = server.OBFS,
g = server.OBFSParam,
b = this.LocalAddress(),
l = this.Socks5LocalPort(),
u = true
};
if (!string.IsNullOrEmpty(server.OBFS))
{
argument.Append($" -o {server.OBFS}");
if (!string.IsNullOrEmpty(server.OBFSParam))
argument.Append($" -g \"{server.OBFSParam}\"");
}
argument.Append($" -b {this.LocalAddress()} -l {this.Socks5LocalPort()} -u");
if (mode.BypassChina)
argument.Append($" --acl \"{Path.GetFullPath(File.Exists(Global.UserACL) ? Global.UserACL : Global.BuiltinACL)}\"");
command.acl = $"{Path.GetFullPath(File.Exists(Global.UserACL) ? Global.UserACL : Global.BuiltinACL)}";
#endregion
StartInstanceAuto(command.ToString());
}
StartInstanceAuto(argument.ToString());
[Verb]
class SSRParameter : ParameterBase
{
public string? s { get; set; }
public ushort? p { get; set; }
[Quote]
public string? k { get; set; }
public string? m { get; set; }
public string? t { get; set; }
[Optional]
public string? O { get; set; }
[Optional]
public string? G { get; set; }
[Optional]
public string? o { get; set; }
[Optional]
public string? g { get; set; }
public string? b { get; set; }
public ushort? l { get; set; }
public bool u { get; set; }
[Full]
[Quote]
[Optional]
public string? acl { get; set; }
}
public override void Stop()

View File

@@ -7,26 +7,16 @@ namespace Netch.Servers.ShadowsocksR
{
public override string Type { get; } = "SSR";
/// <summary>
/// 加密方式
/// </summary>
public string EncryptMethod { get; set; } = SSRGlobal.EncryptMethods[0];
/// <summary>
/// 混淆
/// </summary>
public string OBFS { get; set; } = SSRGlobal.OBFSs[0];
/// <summary>
/// 混淆参数
/// </summary>
public string? OBFSParam { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; } = string.Empty;
/// <summary>
/// 加密方式
/// </summary>
public string EncryptMethod { get; set; } = SSRGlobal.EncryptMethods[0];
/// <summary>
/// 协议
/// </summary>
@@ -36,6 +26,16 @@ namespace Netch.Servers.ShadowsocksR
/// 协议参数
/// </summary>
public string? ProtocolParam { get; set; }
/// <summary>
/// 混淆
/// </summary>
public string OBFS { get; set; } = SSRGlobal.OBFSs[0];
/// <summary>
/// 混淆参数
/// </summary>
public string? OBFSParam { get; set; }
}
public class SSRGlobal

View File

@@ -13,7 +13,7 @@
/// <summary>
/// 额外 ID
/// </summary>
public string aid { get; set; } = string.Empty;
public int aid { get; set; }
/// <summary>
/// 伪装域名HTTPWS
@@ -38,7 +38,7 @@
/// <summary>
/// 端口
/// </summary>
public string port { get; set; } = string.Empty;
public ushort port { get; set; }
/// <summary>
/// 备注
@@ -58,6 +58,6 @@
/// <summary>
/// 链接版本
/// </summary>
public string v { get; set; } = string.Empty;
public int v { get; set; } = 2;
}
}

View File

@@ -77,9 +77,7 @@ namespace Netch.Servers.V2ray
var parameter = new Dictionary<string, string>();
// protocol-specific fields
parameter.Add("type", server.TransferProtocol);
if (server.EncryptMethod == "none")
// VLESS outbounds[].settings.encryption当前可选值只有 none
parameter.Add("encryption", server.EncryptMethod);
parameter.Add("encryption", server.EncryptMethod);
// transport-specific fields
switch (server.TransferProtocol)

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Netch.Controllers;
using Netch.Models;
using Netch.Servers.V2ray;
@@ -43,12 +44,12 @@ namespace Netch.Servers.VMess
var vmessJson = JsonSerializer.Serialize(new V2rayNSharing
{
v = "2",
v = 2,
ps = server.Remark,
add = server.Hostname,
port = server.Port.ToString(),
port = server.Port,
id = server.UserID,
aid = server.AlterID.ToString(),
aid = server.AlterID,
net = server.TransferProtocol,
type = server.FakeType,
host = server.Host,
@@ -85,13 +86,14 @@ namespace Netch.Servers.VMess
return V2rayUtils.ParseVUri(text);
}
V2rayNSharing vmess = JsonSerializer.Deserialize<V2rayNSharing>(s)!;
V2rayNSharing vmess = JsonSerializer.Deserialize<V2rayNSharing>(s,
new JsonSerializerOptions {NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString})!;
data.Remark = vmess.ps;
data.Hostname = vmess.add;
data.Port = ushort.Parse(vmess.port);
data.Port = vmess.port;
data.UserID = vmess.id;
data.AlterID = int.Parse(vmess.aid);
data.AlterID = vmess.aid;
data.TransferProtocol = vmess.net;
data.FakeType = vmess.type;

View File

@@ -95,6 +95,17 @@ namespace Netch.Updater
private void ApplyUpdate()
{
// Pre Update
try
{
// Backup Configuration file
File.Copy(Configuration.SettingFileFullName, Configuration.SettingFileFullName + ".bak", true);
}
catch (Exception)
{
// ignored
}
// extract Update file to {tempDirectory}\extract
var extractPath = Path.Combine(_tempDirectory, "extract");
int exitCode;
@@ -126,7 +137,7 @@ namespace Netch.Updater
if (extendedKeepDirectories.Any(p => file.StartsWith(p)))
continue;
if (Path.GetFileName(file) is ModeHelper.DISABLE_MODE_DIRECTORY_FILENAME)
if (Path.GetFileName(file) is ModeHelper.DisableModeDirectoryFileName)
continue;
filesToDelete.Add(file);

View File

@@ -12,12 +12,10 @@ namespace Netch.Utils
/// <summary>
/// 数据目录
/// </summary>
public const string DATA_DIR = "data";
public static string DataDirectoryFullName => Path.Combine(Global.NetchDir, "data");
public static string SettingFileFullName => $"{DataDirectoryFullName}\\settings.json";
/// <summary>
/// 设置
/// </summary>
public static readonly string SETTINGS_JSON = $"{DATA_DIR}\\settings.json";
private static readonly JsonSerializerOptions JsonSerializerOptions = Global.NewDefaultJsonSerializerOptions;
static Configuration()
@@ -31,45 +29,39 @@ namespace Netch.Utils
/// </summary>
public static void Load()
{
if (File.Exists(SETTINGS_JSON))
if (File.Exists(SettingFileFullName))
{
Global.Settings = ParseSetting(File.ReadAllText(SETTINGS_JSON));
try
{
using var fileStream = File.OpenRead(SettingFileFullName);
var settings = JsonSerializer.DeserializeAsync<Setting>(fileStream, JsonSerializerOptions).Result!;
CheckSetting(settings);
Global.Settings = settings;
}
catch (Exception e)
{
Logging.Error(e.ToString());
Utils.Open(Logging.LogFile);
Environment.Exit(-1);
Global.Settings = null!;
}
}
else
{
// 弹出提示
i18N.Load("System");
// 创建 data 文件夹并保存默认设置
// 保存默认设置
Save();
}
}
public static Setting ParseSetting(string text)
private static void CheckSetting(Setting settings)
{
try
{
var settings = JsonSerializer.Deserialize<Setting>(text, JsonSerializerOptions)!;
settings.Profiles.RemoveAll(p => p.ServerRemark == string.Empty || p.ModeRemark == string.Empty);
#region Check Profile
settings.Profiles.RemoveAll(p => p.ServerRemark == string.Empty || p.ModeRemark == string.Empty);
if (settings.Profiles.Any(p => settings.Profiles.Any(p1 => p1 != p && p1.Index == p.Index)))
for (var i = 0; i < settings.Profiles.Count; i++)
settings.Profiles[i].Index = i;
#endregion
return settings;
}
catch (Exception e)
{
Logging.Error(e.ToString());
Utils.Open(Logging.LogFile);
Environment.Exit(-1);
return null!;
}
if (settings.Profiles.Any(p => settings.Profiles.Any(p1 => p1 != p && p1.Index == p.Index)))
for (var i = 0; i < settings.Profiles.Count; i++)
settings.Profiles[i].Index = i;
}
/// <summary>
@@ -77,10 +69,10 @@ namespace Netch.Utils
/// </summary>
public static void Save()
{
if (!Directory.Exists(DATA_DIR))
Directory.CreateDirectory(DATA_DIR);
if (!Directory.Exists(DataDirectoryFullName))
Directory.CreateDirectory(DataDirectoryFullName);
File.WriteAllBytes(SETTINGS_JSON, JsonSerializer.SerializeToUtf8Bytes(Global.Settings, JsonSerializerOptions));
File.WriteAllBytes(SettingFileFullName, JsonSerializer.SerializeToUtf8Bytes(Global.Settings, JsonSerializerOptions));
}
}
}

View File

@@ -40,14 +40,23 @@ namespace Netch.Utils
private static void Write(string text, LogLevel logLevel)
{
var contents = $@"[{DateTime.Now}][{logLevel.ToString()}] {text}{Global.EOF}";
if (Global.Testing)
#if DEBUG
switch (logLevel)
{
Console.WriteLine(contents);
return;
case LogLevel.INFO:
case LogLevel.WARNING:
Console.Write(contents);
break;
case LogLevel.ERROR:
Console.Error.Write(contents);
break;
default:
throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
}
#else
lock (FileLock)
File.AppendAllText(LogFile, contents);
#endif
}
}
}

View File

@@ -10,19 +10,45 @@ namespace Netch.Utils
{
public static class ModeHelper
{
private const string MODE_DIR = "mode";
public const string DISABLE_MODE_DIRECTORY_FILENAME = "disabled";
public const string DisableModeDirectoryFileName = "disabled";
public static readonly string ModeDirectory = Path.Combine(Global.NetchDir, $"{MODE_DIR}\\");
public static string ModeDirectoryFullName => Path.Combine(Global.NetchDir, "mode");
private static readonly FileSystemWatcher FileSystemWatcher;
static ModeHelper()
{
FileSystemWatcher = new FileSystemWatcher(ModeDirectoryFullName)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName,
IncludeSubdirectories = true,
EnableRaisingEvents = true
};
FileSystemWatcher.Changed += OnModeChanged;
FileSystemWatcher.Created += OnModeChanged;
FileSystemWatcher.Deleted += OnModeChanged;
FileSystemWatcher.Renamed += OnModeChanged;
}
private static void OnModeChanged(object sender, FileSystemEventArgs e)
{
Load();
Global.MainForm.LoadModes();
}
public static string GetRelativePath(string fullName)
{
return fullName.Substring(ModeDirectory.Length);
var length = ModeDirectoryFullName.Length;
if (!ModeDirectoryFullName.EndsWith("\\"))
length++;
return fullName.Substring(length);
}
public static string GetFullPath(string relativeName)
{
return Path.Combine(ModeDirectory, relativeName);
return Path.Combine(ModeDirectoryFullName, relativeName);
}
/// <summary>
@@ -31,7 +57,7 @@ namespace Netch.Utils
public static void Load()
{
Global.Modes.Clear();
LoadModeDirectory(ModeDirectory);
LoadModeDirectory(ModeDirectoryFullName);
Sort();
}
@@ -44,11 +70,18 @@ namespace Netch.Utils
LoadModeDirectory(directory);
// skip Directory with a disabled file in
if (File.Exists(Path.Combine(modeDirectory, DISABLE_MODE_DIRECTORY_FILENAME)))
if (File.Exists(Path.Combine(modeDirectory, DisableModeDirectoryFileName)))
return;
foreach (var file in Directory.GetFiles(modeDirectory).Where(f => f.EndsWith(".txt")))
LoadModeFile(file);
try
{
Global.Modes.Add(new Mode(file));
}
catch (Exception)
{
// ignored
}
}
catch
{
@@ -56,71 +89,17 @@ namespace Netch.Utils
}
}
private static void LoadModeFile(string fullName)
{
var mode = new Mode(fullName);
var content = File.ReadAllLines(fullName);
if (content.Length == 0)
return;
for (var i = 0; i < content.Length; i++)
{
var text = content[i].Trim();
if (i == 0)
{
if (text.First() != '#')
return;
try
{
var splited = text.Substring(1).SplitTrimEntries(',');
mode.Remark = splited[0];
var typeResult = int.TryParse(splited.ElementAtOrDefault(1), out var type);
mode.Type = typeResult ? type : 0;
var bypassChinaResult = int.TryParse(splited.ElementAtOrDefault(2), out var bypassChina);
mode.BypassChina = mode.ClientRouting() && bypassChinaResult && bypassChina == 1;
}
catch
{
return;
}
}
else
{
mode.Rule.Add(text);
}
}
Global.Modes.Add(mode);
}
private static void Sort()
{
Global.Modes.Sort((a, b) => string.Compare(a.Remark, b.Remark, StringComparison.Ordinal));
}
public static void Add(Mode mode)
{
Global.Modes.Add(mode);
Sort();
Global.MainForm.LoadModes();
}
public static void Delete(Mode mode)
{
if (mode.FullName == null)
throw new ArgumentException("FullName");
throw new ArgumentException(nameof(mode.FullName));
if (File.Exists(mode.FullName))
File.Delete(mode.FullName);
Global.Modes.Remove(mode);
Global.MainForm.LoadModes();
File.Delete(mode.FullName);
}
public static bool SkipServerController(Server server, Mode mode)
@@ -137,17 +116,15 @@ namespace Netch.Utils
};
}
public static IModeController? GetModeControllerByType(int type, out ushort? port, out string portName, out PortType portType)
public static IModeController? GetModeControllerByType(int type, out ushort? port, out string portName)
{
port = null;
portName = string.Empty;
portType = PortType.Both;
switch (type)
{
case 0:
port = Global.Settings.RedirectorTCPPort;
portName = "Redirector TCP";
portType = PortType.TCP;
return new NFController();
case 1:
case 2:
@@ -156,7 +133,6 @@ namespace Netch.Utils
case 5:
port = Global.Settings.HTTPLocalPort;
portName = "HTTP";
portType = PortType.TCP;
StatusPortInfoText.HttpPort = (ushort) port;
return new HTTPController();
case 4:

View File

@@ -4,6 +4,8 @@ using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using Netch.Models;
using static Vanara.PInvoke.IpHlpApi;
using static Vanara.PInvoke.Ws2_32;
namespace Netch.Utils
{
@@ -26,6 +28,16 @@ namespace Netch.Utils
}
}
public static IEnumerable<Process> GetProcessByUsedTcpPort(ushort port)
{
if (port == 0)
throw new ArgumentOutOfRangeException();
var row = GetTcpTable2().Where(r => ntohs((ushort) r.dwLocalPort) == port);
return row.Select(r => Process.GetProcessById((int) r.dwOwningPid));
}
private static void GetReservedPortRange(PortType portType, ref List<Range> targetList)
{
var process = new Process

View File

@@ -20,6 +20,7 @@ namespace Netch.Utils
}
catch
{
// Unsupported Server Type
return JsonSerializer.Deserialize<Server>(jsonElement.GetRawText(), new JsonSerializerOptions())!;
}
}

View File

@@ -21,8 +21,6 @@ namespace Netch.Utils
{
public static bool Open(string path)
{
if (Global.Testing)
return true;
try
{
Process.Start(new ProcessStartInfo
@@ -116,30 +114,11 @@ namespace Netch.Utils
}
}
public static void KillProcessByName(string name)
{
try
{
foreach (var p in Process.GetProcessesByName(name))
if (p.MainModule != null && p.MainModule.FileName.StartsWith(Global.NetchDir))
p.Kill();
}
catch (Win32Exception e)
{
Logging.Error($"结束进程 {name} 错误:" + e.Message);
}
catch (Exception)
{
// ignored
}
}
public static string GetFileVersion(string file)
{
return File.Exists(file) ? FileVersionInfo.GetVersionInfo(file).FileVersion : string.Empty;
}
public static void DrawCenterComboBox(object sender, DrawItemEventArgs e)
{
if (sender is ComboBox cbx)

View File

@@ -1,3 +0,0 @@
/bin
/obj
/SearchComboBox.csproj.user

3
UnitTest/.gitignore vendored
View File

@@ -1,3 +0,0 @@
/bin
/obj
/Netch.csproj.user

View File

@@ -6,10 +6,10 @@ using Netch.Utils;
namespace UnitTest
{
[TestClass]
public class FunctionTest : TestBase
public class Function : TestBase
{
[TestMethod]
public void TestLoadI18N()
public void LoadLanguage()
{
void TestLoad(string t)
{

98
UnitTest/ParameterTest.cs Normal file
View File

@@ -0,0 +1,98 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Netch.Models;
namespace UnitTest
{
[TestClass]
public class ParameterTest
{
[Verb]
private class VerbAndRealName : ParameterBase
{
[RealName("v")]
public string v1 { get; } = "a";
[RealName("v")]
public string v2 { get; } = "b";
}
private class Full : ParameterBase
{
[RealName("f")]
public string f1 { get; } = "a";
[RealName("f")]
public string f2 { get; } = "b";
}
private class FullWithVerb : ParameterBase
{
public string f { get; } = "a";
[Verb]
public string v { get; } = "b";
}
[Verb]
private class VerbWithFull : ParameterBase
{
public string v { get; } = "a";
[Full]
public string f { get; } = "b";
}
private class QuoteValue : ParameterBase
{
public static string pathValue = @"C:\Programe Files\Damn thats space";
[Quote]
public string path { get; set; } = pathValue;
}
private class FlagAndOptional : ParameterBase
{
public bool a { get; set; } = true;
public bool b { get; set; } = false;
[Optional]
public string c { get; set; } = string.Empty;
[Optional]
public string? d { get; set; } = null;
}
private class RequiredEmpty : ParameterBase
{
public string? udp { get; set; } = string.Empty;
}
private class RequiredNull : ParameterBase
{
public string? udp { get; set; } = null;
}
private class Number : ParameterBase
{
public ushort a { get; set; } = 1;
public int b { get; set; } = 1;
}
[TestMethod]
public void Test()
{
Assert.AreEqual(new VerbAndRealName().ToString(), "-v a -v b");
Assert.AreEqual(new Full().ToString(), "--f a --f b");
Assert.AreEqual(new FullWithVerb().ToString(), "--f a -v b");
Assert.AreEqual(new VerbWithFull().ToString(), "-v a --f b");
Assert.AreEqual(new QuoteValue().ToString(), $"--path \"{QuoteValue.pathValue}\"");
Assert.AreEqual(new FlagAndOptional().ToString(), "--a");
Assert.ThrowsException<RequiredArgumentValueInvalidException>(() => { _ = new RequiredEmpty().ToString(); });
Assert.ThrowsException<RequiredArgumentValueInvalidException>(() => { _ = new RequiredNull().ToString(); });
Assert.AreEqual(new Number().ToString(), "--a 1 --b 1");
}
}
}

View File

@@ -10,10 +10,10 @@ using Netch.Utils;
namespace UnitTest
{
[TestClass]
public class TestParseShareLink : TestBase
public class ParseShareLink : TestBase
{
[TestMethod]
public void TestServerFromSSR()
public void ParseSSR()
{
const string normalCase =
"ssr://MTI3LjAuMC4xOjEyMzQ6YXV0aF9hZXMxMjhfbWQ1OmFlcy0xMjgtY2ZiOnRsczEuMl90aWNrZXRfYXV0aDpZV0ZoWW1KaS8_b2Jmc3BhcmFtPVluSmxZV3QzWVRFeExtMXZaUQ";

View File

@@ -0,0 +1,26 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Netch.Models.GitHubRelease;
namespace UnitTest
{
[TestClass]
public class SuffixVersionTest
{
[TestMethod]
public void Test()
{
var rel = SuffixVersion.Parse("1.0.0");
var a1 = SuffixVersion.Parse("1.0.0-Alpha1");
var a3 = SuffixVersion.Parse("1.0.0-aLpHa3");
var b2 = SuffixVersion.Parse("1.0.0-betA2");
Assert.AreEqual(rel.ToString(), "1.0.0");
Assert.AreEqual(a1.ToString(), "1.0.0-Alpha1");
Assert.IsTrue(rel.CompareTo(a1) > 0);
Assert.IsTrue(rel.CompareTo(b2) > 0);
Assert.IsTrue(b2.CompareTo(a1) > 0);
Assert.IsTrue(b2.CompareTo(a3) > 0);
}
}
}

View File

@@ -1,14 +1,6 @@
using Netch;
namespace UnitTest
namespace UnitTest
{
public class TestBase
{
protected TestBase()
{
#if DEBUG
Global.Testing = true;
#endif
}
}
}

View File

@@ -6,6 +6,7 @@
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -23,8 +24,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
<PackageReference Include="System.Text.Json" Version="5.0.1" />
</ItemGroup>

2
modes

Submodule modes updated: 42375ef724...72ad96d018