From ddb23b83bd3beda11389455ba6926ac9ce3d481e Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 3 Oct 2024 14:29:09 +0800 Subject: Rename secret tool. --- tools/Crupest.SecretTool/.gitignore | 7 + tools/Crupest.SecretTool/Crupest.SecretTool.sln | 30 ++ .../Crupest.SecretTool/.gitignore | 1 + .../Crupest.SecretTool/Config.cs | 95 +++++++ .../Crupest.SecretTool/Controller.cs | 113 ++++++++ .../Crupest.SecretTool/Crupest.SecretTool.csproj | 25 ++ .../Crupest.SecretTool/FileWatcher.cs | 26 ++ .../Crupest.SecretTool/GeoDataManager.cs | 311 +++++++++++++++++++++ .../Crupest.SecretTool/HostMatchConfig.cs | 123 ++++++++ .../Crupest.SecretTool/Program.cs | 94 +++++++ .../PublishProfiles/FolderProfile.pubxml | 13 + .../Crupest.SecretTool/Crupest.SecretTool/Proxy.cs | 58 ++++ .../Crupest.SecretTool/Routing.cs | 97 +++++++ .../Crupest.SecretTool/StaticHosts.cs | 40 +++ .../Crupest.SecretTool/SurgeConfigGenerator.cs | 60 ++++ .../Crupest.SecretTool/Template.cs | 231 +++++++++++++++ .../Crupest.SecretTool/ToolConfig.cs | 154 ++++++++++ .../Crupest.SecretTool/V4ConfigJsonObjects.cs | 25 ++ .../Crupest.SecretTool/V5ConfigJsonObjects.cs | 31 ++ .../Crupest.SecretTool/config.json.template | 63 +++++ .../Crupest.SecretTool/config.v5.json.template | 55 ++++ .../Crupest.SecretTool/hosts.txt | 2 + .../Crupest.SecretTool/proxy.txt | 45 +++ tools/Crupest.SecretTool/build-secret.bash | 41 +++ tools/Crupest.SecretTool/tools/cru-proxy-edit | 12 + tools/Crupest.SecretTool/tools/cru-proxy-log | 13 + .../tools/crupest-secret-tool.service | 8 + .../tools/life.crupest.secret-tool.plist | 18 ++ tools/Crupest.V2ray/.gitignore | 7 - tools/Crupest.V2ray/Crupest.V2ray/.gitignore | 1 - tools/Crupest.V2ray/Crupest.V2ray/Config.cs | 91 ------ .../Crupest.V2ray/Crupest.V2ray.csproj | 25 -- tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs | 26 -- .../Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs | 307 -------------------- tools/Crupest.V2ray/Crupest.V2ray/Program.cs | 94 ------- .../PublishProfiles/FolderProfile.pubxml | 13 - tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs | 14 - .../Crupest.V2ray/SurgeConfigGenerator.cs | 60 ---- tools/Crupest.V2ray/Crupest.V2ray/Template.cs | 231 --------------- tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs | 153 ---------- .../Crupest.V2ray/Crupest.V2ray/V2rayController.cs | 113 -------- .../Crupest.V2ray/V2rayHostMacherConfig.cs | 109 -------- tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs | 42 --- tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs | 58 ---- tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs | 87 ------ .../Crupest.V2ray/V2rayV4ConfigJsonObjects.cs | 25 -- .../Crupest.V2ray/V2rayV5ConfigJsonObjects.cs | 31 -- .../Crupest.V2ray/V2rayV5StaticHostRule.cs | 122 -------- .../Crupest.V2ray/config.json.template | 63 ----- .../Crupest.V2ray/config.v5.json.template | 55 ---- tools/Crupest.V2ray/Crupest.V2ray/hosts.txt | 2 - tools/Crupest.V2ray/Crupest.V2ray/proxy.txt | 45 --- tools/Crupest.V2ray/CrupestV2ray.sln | 33 --- tools/Crupest.V2ray/build-secret.bash | 41 --- tools/Crupest.V2ray/tools/cru-proxy-edit | 12 - tools/Crupest.V2ray/tools/cru-proxy-log | 13 - tools/Crupest.V2ray/tools/crupest-v2ray.service | 8 - tools/Crupest.V2ray/tools/life.crupest.v2ray.plist | 18 -- 58 files changed, 1791 insertions(+), 1899 deletions(-) create mode 100644 tools/Crupest.SecretTool/.gitignore create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool.sln create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/.gitignore create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Config.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Controller.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/Template.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/config.json.template create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/hosts.txt create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt create mode 100755 tools/Crupest.SecretTool/build-secret.bash create mode 100755 tools/Crupest.SecretTool/tools/cru-proxy-edit create mode 100755 tools/Crupest.SecretTool/tools/cru-proxy-log create mode 100644 tools/Crupest.SecretTool/tools/crupest-secret-tool.service create mode 100644 tools/Crupest.SecretTool/tools/life.crupest.secret-tool.plist delete mode 100644 tools/Crupest.V2ray/.gitignore delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/.gitignore delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/Config.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/Program.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/SurgeConfigGenerator.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/Template.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/config.json.template delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/hosts.txt delete mode 100644 tools/Crupest.V2ray/Crupest.V2ray/proxy.txt delete mode 100644 tools/Crupest.V2ray/CrupestV2ray.sln delete mode 100755 tools/Crupest.V2ray/build-secret.bash delete mode 100755 tools/Crupest.V2ray/tools/cru-proxy-edit delete mode 100755 tools/Crupest.V2ray/tools/cru-proxy-log delete mode 100644 tools/Crupest.V2ray/tools/crupest-v2ray.service delete mode 100644 tools/Crupest.V2ray/tools/life.crupest.v2ray.plist (limited to 'tools') diff --git a/tools/Crupest.SecretTool/.gitignore b/tools/Crupest.SecretTool/.gitignore new file mode 100644 index 0000000..ac4d8a4 --- /dev/null +++ b/tools/Crupest.SecretTool/.gitignore @@ -0,0 +1,7 @@ +.vs +bin +obj +*.pubxml.user +*.csproj.user + +publish diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool.sln b/tools/Crupest.SecretTool/Crupest.SecretTool.sln new file mode 100644 index 0000000..fde4347 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34024.191 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4C2CE80-CDF8-4B08-8912-D1F0F14196AD}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crupest.SecretTool", "Crupest.SecretTool\Crupest.SecretTool.csproj", "{D6335AE4-FD22-49CD-9624-37371F3B4F82}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6335AE4-FD22-49CD-9624-37371F3B4F82}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1E8FD9C-9157-4F4E-8265-4B37F30EEC5E} + EndGlobalSection +EndGlobal diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/.gitignore b/tools/Crupest.SecretTool/Crupest.SecretTool/.gitignore new file mode 100644 index 0000000..c936492 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/.gitignore @@ -0,0 +1 @@ +vmess.txt diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Config.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Config.cs new file mode 100644 index 0000000..ff58551 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Config.cs @@ -0,0 +1,95 @@ +namespace Crupest.SecretTool; + +public record ConfigItem(string Value, int LineNumber); + +public class DictionaryConfig(string configString, List? requiredKeys = null) +{ + private static Dictionary Parse(string configString, List? requiredKeys = null) + { + var config = new Dictionary(); + var lines = configString.Split('\n'); + int lineNumber = 1; + + foreach (var line in lines) + { + var l = line; + var beginOfComment = l.IndexOf('#'); + if (beginOfComment >= 0) + { + l = line[..beginOfComment]; + } + l = l.Trim(); + if (!string.IsNullOrEmpty(l)) + { + var equalIndex = l.IndexOf('='); + if (equalIndex == -1) + { + throw new FormatException($"No '=' found in line {lineNumber}."); + } + + config.Add(l[..equalIndex].Trim(), new ConfigItem(l[(equalIndex + 1)..].Trim(), lineNumber)); + } + + lineNumber++; + } + + if (requiredKeys is not null) + { + foreach (var key in requiredKeys) + { + if (!config.ContainsKey(key)) + { + throw new FormatException($"Required key '{key}' not found in config."); + } + } + } + + return config; + } + + public string ConfigString { get; } = configString; + public List? RequiredKeys { get; } = requiredKeys; + public Dictionary Config { get; } = Parse(configString); + public ConfigItem GetItemCaseInsensitive(string key) + { + foreach (var (originalKey, value) in Config) + { + if (string.Equals(originalKey, key, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + throw new KeyNotFoundException($"Key '{key}' not found in config case-insensitively."); + } +} + +public class ListConfig(string configString) +{ + private static List Parse(string configString) + { + var config = new List(); + var lines = configString.Split('\n'); + int lineNumber = 1; + + foreach (var line in lines) + { + var l = line; + var beginOfComment = l.IndexOf('#'); + if (beginOfComment >= 0) + { + l = line[..beginOfComment]; + } + l = l.Trim(); + if (!string.IsNullOrEmpty(l)) + { + config.Add(new ConfigItem(l, lineNumber)); + } + lineNumber++; + } + + return config; + } + + public string ConfigString { get; } = configString; + public List Config { get; } = Parse(configString); +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Controller.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Controller.cs new file mode 100644 index 0000000..0803b01 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Controller.cs @@ -0,0 +1,113 @@ +using System.Diagnostics; + +namespace Crupest.SecretTool; + +public class Controller(string executablePath, string configPath, string? assetPath) +{ + public const string ToolAssetEnvironmentVariableName = "v2ray.location.asset"; + + public static string? FindExecutable(string contentDir, out bool isLocal, string? executableName = null) + { + isLocal = false; + executableName ??= "v2ray"; + + if (OperatingSystem.IsWindows()) + { + executableName += ".exe"; + } + + var localToolPath = Path.Combine(contentDir, executableName); + if (File.Exists(localToolPath)) + { + isLocal = true; + return localToolPath; + } + + var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator); + if (paths is not null) + { + foreach (var p in paths) + { + var toolPath = Path.Combine(p, executableName); + if (File.Exists(toolPath)) + { + return toolPath; + } + } + } + + return null; + } + + public string ExecutablePath { get; } = executablePath; + public string ConfigPath { get; } = configPath; + public string? AssetPath { get; } = assetPath; + public Process? CurrentProcess { get; private set; } + + private Process CreateProcess() + { + var process = new Process(); + + var startInfo = new ProcessStartInfo + { + FileName = ExecutablePath, + }; + startInfo.ArgumentList.Add("run"); + startInfo.ArgumentList.Add("-c"); + startInfo.ArgumentList.Add(ConfigPath); + if (AssetPath is not null) + { + startInfo.EnvironmentVariables[ToolAssetEnvironmentVariableName] = AssetPath; + } + + process.StartInfo = startInfo; + process.OutputDataReceived += (_, args) => + { + Console.Out.Write(args.Data); + }; + process.ErrorDataReceived += (_, args) => + { + Console.Error.WriteLine(args.Data); + }; + + return process; + } + + public void Stop() + { + if (CurrentProcess is not null) + { + CurrentProcess.Kill(); + CurrentProcess.Dispose(); + CurrentProcess = null; + Console.WriteLine("V2ray stopped."); + } + } + + public void Start(bool stopOld = false) + { + if (stopOld) Stop(); + + if (CurrentProcess is null) + { + CurrentProcess = CreateProcess(); + CurrentProcess.EnableRaisingEvents = true; + CurrentProcess.Exited += (_, _) => + { + if (CurrentProcess.ExitCode != 0) + { + const string message = "V2ray exited with error."; + Console.Error.WriteLine(message); + throw new Exception(message); + } + }; + CurrentProcess.Start(); + Console.WriteLine("V2ray started."); + } + } + + public void Restart() + { + Start(true); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj b/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj new file mode 100644 index 0000000..0812e4c --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs new file mode 100644 index 0000000..193874f --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/FileWatcher.cs @@ -0,0 +1,26 @@ +namespace Crupest.SecretTool; + +public class FileWatcher(string directory, List fileNames) +{ + public string Directory { get; set; } = directory; + public List FileNames { get; set; } = fileNames; + + public delegate void OnChangedHandler(); + public event OnChangedHandler? OnChanged; + + public void Run() + { + var sourceWatcher = new FileSystemWatcher(Directory); + foreach (var fileName in FileNames) + { + sourceWatcher.Filters.Add(fileName); + } + sourceWatcher.NotifyFilter = NotifyFilters.LastWrite; + + while (true) + { + var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed); + OnChanged?.Invoke(); + } + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs new file mode 100644 index 0000000..2d9e2eb --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs @@ -0,0 +1,311 @@ +using System.IO.Compression; + +namespace Crupest.SecretTool; + +public interface IGeoSiteEntry +{ + bool IsInclude { get; } + string Value { get; } +} + +public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSiteEntry +{ + public bool IsInclude => true; +} + +public record GeoSiteRuleEntry(HostMatchKind Kind, string Value, List Attributes, string ContainingSite) : IGeoSiteEntry +{ + public bool IsInclude => false; +} + +public record GeoSite(string Name, List Entries) +{ + public static GeoSite Parse(string name, string str) + { + List entries = []; + var listConfig = new ListConfig(str); + foreach (var item in listConfig.Config) + { + var (value, line) = item; + + if (value.StartsWith("include:")) + { + var include = value["include:".Length..].Trim(); + if (include.Length == 0 || include.Contains(' ')) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid include value."); + } + entries.Add(new GeoSiteIncludeEntry(include, name)); + continue; + } + + var segments = value.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 2) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. More than one ':'."); + } + + HostMatchKind kind; + if (segments.Length == 2) + { + kind = segments[0] switch + { + "domain" => kind = HostMatchKind.DomainSuffix, + "full" => kind = HostMatchKind.DomainFull, + "keyword" => kind = HostMatchKind.DomainKeyword, + "regexp" => kind = HostMatchKind.DomainRegex, + _ => throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Unknown matcher.") + }; + } + else + { + kind = HostMatchKind.DomainSuffix; + } + + var domainSegments = segments[^1].Split('@', StringSplitOptions.TrimEntries); + var domain = domainSegments[0]; + if (kind != HostMatchKind.DomainRegex && Uri.CheckHostName(domain) != UriHostNameType.Dns) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid domain."); + } + + List attributes = []; + foreach (var s in domainSegments[1..]) + { + if (s.Length == 0) + { + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Empty attribute value."); + } + attributes.Add(s); + } + + entries.Add(new GeoSiteRuleEntry(kind, domain, attributes, name)); + } + return new GeoSite(name, entries); + } +} + +public class GeoSiteData(string directory) +{ + private static List Parse(string directory) + { + var sites = new List(); + foreach (var path in Directory.GetFileSystemEntries(directory)) + { + var content = File.ReadAllText(path); + sites.Add(GeoSite.Parse(Path.GetFileName(path), content)); + } + return sites; + } + + public string DataDirectory { get; } = directory; + + public List Sites { get; } = Parse(directory); + + public GeoSite? GetSite(string name) + { + return Sites.Where(s => s.Name == name).FirstOrDefault(); + } + + public List GetEntriesRecursive(List sites, + List? onlyMatcherKinds = null, List? onlyAttributes = null) + { + List entries = []; + HashSet visited = []; + HashSet? kinds = onlyMatcherKinds?.ToHashSet(); + + void Visit(string site) + { + if (visited.Contains(site)) + { + return; + } + + visited.Add(site); + var siteData = GetSite(site); + if (siteData == null) + { + return; + } + foreach (var entry in siteData.Entries) + { + if (entry is GeoSiteIncludeEntry includeEntry) + { + Visit(includeEntry.Value); + } + else if (entry is GeoSiteRuleEntry geoSiteRuleEntry) + { + if (kinds != null && !kinds.Contains(geoSiteRuleEntry.Kind)) + { + continue; + } + + if (onlyAttributes != null && !geoSiteRuleEntry.Attributes.Intersect(onlyAttributes).Any()) + { + continue; + } + + entries.Add(geoSiteRuleEntry); + } + } + } + + foreach (var s in sites) + { + Visit(s); + } + + return entries; + } +} + +public class GeoDataManager +{ + public const string GeoSiteFileName = "geosite.dat"; + public const string GeoIpFileName = "geoip.dat"; + public const string GeoIpCnFileName = "geoip-only-cn-private.dat"; + + public static class ToolGithub + { + public const string Organization = "v2fly"; + public const string GeoSiteRepository = "domain-list-community"; + public const string GeoIpRepository = "geoip"; + public const string GeoSiteReleaseFilename = "dlc.dat"; + public const string GeoIpReleaseFilename = "geoip.dat"; + public const string GeoIpCnReleaseFilename = "geoip-only-cn-private.dat"; + } + + public static GeoDataManager Instance { get; } = new GeoDataManager(); + + public record GeoDataAsset(string Name, string FileName, string GithubUser, string GithubRepo, string GithubReleaseFileName); + + public GeoDataManager() + { + Assets = + [ + new("geosite", GeoSiteFileName, ToolGithub.Organization, ToolGithub.GeoSiteRepository, ToolGithub.GeoSiteRepository), + new("geoip", GeoIpFileName, ToolGithub.Organization, ToolGithub.GeoIpRepository, ToolGithub.GeoIpReleaseFilename), + new("geoip-cn", GeoIpCnFileName, ToolGithub.Organization, ToolGithub.GeoIpRepository, ToolGithub.GeoIpCnReleaseFilename), + ]; + } + + public List Assets { get; set; } + + public GeoSiteData? GeoSiteData { get; set; } + + public GeoSiteData GetOrCreateGeoSiteData(bool clean, bool silent) + { + if (GeoSiteData is not null) { return GeoSiteData; } + GeoSiteData = DownloadAndGenerateGeoSiteData(clean, silent); + return GeoSiteData; + } + + private static string GetReleaseFileUrl(string user, string repo, string fileName) + { + return $"https://github.com/{user}/{repo}/releases/latest/download/{fileName}"; + } + + private static void GithubDownloadRelease(HttpClient httpClient, string user, string repo, string fileName, string outputPath) + { + using var responseStream = httpClient.GetStreamAsync(GetReleaseFileUrl(user, repo, fileName)).Result; + using var outputFileStream = File.OpenWrite(outputPath); + responseStream.CopyTo(outputFileStream); + } + + public bool HasAllAssets(string directory, out List missing) + { + missing = []; + foreach (var asset in Assets) + { + var assetPath = Path.Combine(directory, asset.FileName); + if (!File.Exists(assetPath)) + { + missing.Add(asset.Name); + } + } + return missing.Count == 0; + } + + public void Download(string outputDir, bool silent) + { + using var httpClient = new HttpClient(); + + foreach (var asset in Assets) + { + if (!silent) + { + Console.WriteLine($"Downloading {asset.Name}..."); + } + GithubDownloadRelease(httpClient, asset.GithubUser, asset.GithubRepo, asset.GithubReleaseFileName, Path.Combine(outputDir, asset.FileName)); + if (!silent) + { + Console.WriteLine($"Downloaded {asset.Name}!"); + } + } + } + + private static string GetGithubRepositoryArchiveUrl(string user, string repo) + { + return $"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"; + } + + private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath, bool silent) + { + var url = GetGithubRepositoryArchiveUrl(user, repo); + if (!silent) { Console.WriteLine($"Begin to download data from {url} to {outputPath}."); } + using var responseStream = httpClient.GetStreamAsync(url).Result; + using var outputFileStream = File.OpenWrite(outputPath); + responseStream.CopyTo(outputFileStream); + if (!silent) { Console.WriteLine("Succeeded to download."); } + } + + private static void Unzip(string zipPath, string outputPath) + { + using var zip = ZipFile.OpenRead(zipPath) ?? throw new Exception($"Failed to open zip file {zipPath}"); + zip.ExtractToDirectory(outputPath); + } + + private static string DownloadAndExtractGeoDataRepository(bool cleanTempDirIfFailed, bool silent, out string tempDirectoryPath) + { + tempDirectoryPath = ""; + const string zipFileName = "v2ray-geosite-master.zip"; + using var httpClient = new HttpClient(); + var tempDirectory = Directory.CreateTempSubdirectory(Program.Name); + tempDirectoryPath = tempDirectory.FullName; + try + { + var archivePath = Path.Combine(tempDirectoryPath, zipFileName); + var extractPath = Path.Combine(tempDirectoryPath, "repo"); + GithubDownloadRepository(httpClient, ToolGithub.Organization, ToolGithub.GeoSiteRepository, archivePath, silent); + if (!silent) { Console.WriteLine($"Extract geo data to {extractPath}."); } + Directory.CreateDirectory(extractPath); + Unzip(archivePath, extractPath); + if (!silent) { Console.WriteLine($"Extraction done."); } + return Path.Join(extractPath, "domain-list-community-master"); + } + catch (Exception) + { + if (cleanTempDirIfFailed) + { + Directory.Delete(tempDirectoryPath, true); + } + throw; + } + } + + private static GeoSiteData DownloadAndGenerateGeoSiteData(bool clean, bool silent) + { + var repoDirectory = DownloadAndExtractGeoDataRepository(clean, silent, out var tempDirectoryPath); + try + { + return new GeoSiteData(Path.Join(repoDirectory, "data")); + } + finally + { + if (clean) + { + Directory.Delete(tempDirectoryPath, true); + } + } + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs new file mode 100644 index 0000000..5cc0c3d --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs @@ -0,0 +1,123 @@ +namespace Crupest.SecretTool; + +public enum HostMatchKind +{ + DomainFull, + DomainSuffix, + DomainKeyword, + DomainRegex, + Ip, + GeoSite, + GeoIp, +} + +public static class HostMatchKindExtensions +{ + public static bool IsDomain(this HostMatchKind kind) + { + return kind.IsNonRegexDomain() || kind == HostMatchKind.DomainRegex; + } + + public static bool IsNonRegexDomain(this HostMatchKind kind) + { + return kind is HostMatchKind.DomainFull or HostMatchKind.DomainSuffix or HostMatchKind.DomainKeyword; + } + + + public static List DomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword, HostMatchKind.DomainRegex]; + + public static List NonRegexDomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword]; +} + +public record HostMatchConfigItem(HostMatchKind Kind, string MatchString, List Values); + +public class HostMatchConfig(string configString, List allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1) +{ + private static List Parse(string configString, List allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1) + { + var items = new ListConfig(configString).Config; + var result = new List(); + + foreach (var item in items) + { + var lineNumber = item.LineNumber; + var line = item.Value; + var hasExplicitMatchKind = false; + var segments = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); + + foreach (var matchKind in Enum.GetValues()) + { + var matchKindName = Enum.GetName(matchKind) ?? throw new Exception("No such match kind."); + hasExplicitMatchKind = true; + if (segments[0] == matchKindName) + { + if (segments.Count < 2) + { + throw new FormatException($"Explicit match item needs a value in line {lineNumber}."); + } + if (allowedMatchKinds.Contains(matchKind)) + { + if (matchKind.IsNonRegexDomain() && Uri.CheckHostName(matchKindName) != UriHostNameType.Dns) + { + throw new FormatException($"Invalid domain format in line {lineNumber}."); + } + + var components = segments[2..].ToList(); + if (minComponentCount > 0 && components.Count < minComponentCount) + { + throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required."); + } + if (maxComponentCount >= 0 && components.Count > maxComponentCount) + { + throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed."); + } + result.Add(new HostMatchConfigItem(matchKind, segments[1], components)); + } + else + { + throw new FormatException($"Match kind {matchKindName} is not allowed at line {lineNumber}."); + } + } + } + + if (!hasExplicitMatchKind) + { + if (minComponentCount > 0 && segments.Count - 1 < minComponentCount) + { + throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required."); + } + if (maxComponentCount >= 0 && segments.Count - 1 > maxComponentCount) + { + throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed."); + } + result.Add(new HostMatchConfigItem(HostMatchKind.DomainSuffix, segments[0], segments.Count == 1 ? [] : segments[1..])); + } + } + return result; + } + + public string ConfigString { get; } = configString; + public List AllowedMatchKinds { get; } = allowedMatchKinds; + public int MinComponentCount { get; } = minComponentCount; + public int MaxComponentCount { get; } = maxComponentCount; + public List Items { get; } = Parse(configString, allowedMatchKinds, minComponentCount, maxComponentCount); +} + +public class HostMatchConfigFile +{ + public HostMatchConfigFile(string path, List allowedMatchKinds, int minComponentCount = -1, int maxComponentCount = -1) + { + Path = path; + FileContent = File.ReadAllText(path); + Config = new HostMatchConfig(FileContent, allowedMatchKinds, minComponentCount, maxComponentCount); ; + } + + public string Path { get; } + public string FileContent { get; } + public HostMatchConfig Config { get; } +} + +public class ProxyFile(string path) : + HostMatchConfigFile(path, [.. Enum.GetValues()], maxComponentCount: 0) +{ +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs new file mode 100644 index 0000000..afbcde9 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs @@ -0,0 +1,94 @@ +using System.Reflection; + +namespace Crupest.SecretTool; + +public static class Program +{ + public static string Name { get; } = typeof(Program).Namespace ?? throw new Exception("Can't get the name of Crupest.SecretTool."); + + public static string CrupestSecretToolDirectory { get; } = + Environment.GetEnvironmentVariable("CRUPEST_V2RAY_DIR") ?? + Path.GetFullPath(Path.GetDirectoryName( + Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of Crupest.SecretTool.")); + + private const string ConfigOutputFileName = "config.json"; + private const string SurgeRuleSetChinaOutputFileName = "ChinaRuleSet.txt"; + private const string SurgeRuleSetGlobalOutputFileName = "GlobalRuleSet.txt"; + + public static void RunToolAndWatchConfigChange() + { + var executablePath = Controller.FindExecutable(CrupestSecretToolDirectory, out var isLocal) ?? + throw new Exception("Can't find v2ray executable either in Crupest.SecretTool directory or in PATH."); + + string? assetsPath; + if (isLocal) + { + assetsPath = CrupestSecretToolDirectory; + var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestSecretToolDirectory, out var missing); + if (!assetsComplete) + { + throw new Exception($"Missing assets: {string.Join(", ", missing)} in {CrupestSecretToolDirectory}. This v2ray is local. So only use assets in Crupest.SecretTool directory."); + } + } + else + { + assetsPath = CrupestSecretToolDirectory; + var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestSecretToolDirectory, out var missing); + if (!assetsComplete) + { + Console.WriteLine($"Missing assets: {string.Join(", ", missing)} in {CrupestSecretToolDirectory}. This v2ray is global. So fallback to its own assets."); + assetsPath = null; + } + } + + var controller = new Controller(executablePath, Path.Combine(CrupestSecretToolDirectory, ConfigOutputFileName), assetsPath); + var configFileWatcher = new FileWatcher(CrupestSecretToolDirectory, ToolConfig.ConfigFileNames); + + ToolConfig.FromDirectoryAndWriteToFile(CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, ConfigOutputFileName)); + controller.Start(); + + configFileWatcher.OnChanged += () => + { + ToolConfig.FromDirectoryAndWriteToFile(CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, ConfigOutputFileName)); + controller.Restart(); + }; + + configFileWatcher.Run(); + } + + public static void Main(string[] args) + { + if (args.Length != 0) + { + if (args.Length != 1) + { + throw new Exception("Invalid command line arguments."); + } + var verb = args[0].ToLower(); + if (verb == "download-geodata" || verb == "dg") + { + GeoDataManager.Instance.Download(CrupestSecretToolDirectory, false); + return; + } + else if (verb == "generate-surge-rule-set" || verb == "gs") + { + SurgeConfigGenerator.GenerateTo( + Path.Join(CrupestSecretToolDirectory, "proxy.txt"), + Path.Join(CrupestSecretToolDirectory, SurgeRuleSetChinaOutputFileName), + Path.Join(CrupestSecretToolDirectory, SurgeRuleSetGlobalOutputFileName), + true, false + ); + return; + } + else if (verb == "generate" || verb == "g") + { + var config = ToolConfig.FromDirectory(CrupestSecretToolDirectory); + Console.Out.WriteLine(config.ToJsonStringV4()); + return; + } + throw new Exception("Invalid command line arguments."); + } + + RunToolAndWatchConfigChange(); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml b/tools/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..5fca454 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + \ No newline at end of file diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs new file mode 100644 index 0000000..638edb6 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs @@ -0,0 +1,58 @@ +namespace Crupest.SecretTool; + +public abstract class Proxy(string tag) : IV4ConfigObject +{ + public string Tag { get; set; } = tag; + + public abstract V4ConfigJsonObjects.Outbound ToJsonObjectV4(); + + object IV4ConfigObject.ToJsonObjectV4() + { + return ToJsonObjectV4(); + } +} + +public class HttpProxy(string host, int port, string tag) : Proxy(tag) +{ + public string Host { get; set; } = host; + public int Port { get; set; } = port; + + public override V4ConfigJsonObjects.Outbound ToJsonObjectV4() + { + return new V4ConfigJsonObjects.Outbound(Tag, "http", + new V4ConfigJsonObjects.HttpOutboundSettings([new V4ConfigJsonObjects.HttpOutboundServer(Host, Port, [])]), + null + ); + } +} + + +public class VmessProxy(string host, int port, string userId, string path, string tag) : Proxy(tag) +{ + public string Host { get; set; } = host; + public int Port { get; set; } = port; + public string Path { get; set; } = path; + public string UserId { get; set; } = userId; + + public override V4ConfigJsonObjects.Outbound ToJsonObjectV4() + { + return new V4ConfigJsonObjects.Outbound(Tag, "vmess", + new V4ConfigJsonObjects.VmessOutboundSettings( + [new V4ConfigJsonObjects.VnextServer(Host, Port, [new V4ConfigJsonObjects.VnextServerUser(UserId, 0, "auto", 0)])]), + new V4ConfigJsonObjects.WsStreamSettings("ws", "tls", new V4ConfigJsonObjects.WsSettings(Path, new() { ["Host"] = Host })) + ); + } + + public static VmessProxy CreateFromConfigString(string configString, string tag) + { + var config = new DictionaryConfig(configString, ["host", "port", "userid", "path"]); + var portString = config.GetItemCaseInsensitive("port").Value; + if (!int.TryParse(portString, out var port) || port <= 0) + { + throw new FormatException($"Invalid port number: {portString}: not an integer or is a invalid number."); + } + return new VmessProxy(config.GetItemCaseInsensitive("host").Value, port, + config.GetItemCaseInsensitive("userid").Value, config.GetItemCaseInsensitive("path").Value, tag + ); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs new file mode 100644 index 0000000..dbced0e --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs @@ -0,0 +1,97 @@ +namespace Crupest.SecretTool; + +public record RoutingRule(HostMatchKind MatchKind, string MatchString, string OutboundTag) : IV4ConfigObject +{ + public string ToolConfigString => MatchKind switch + { + HostMatchKind.DomainFull => $"full:{MatchString}", + HostMatchKind.DomainSuffix => $"domain:{MatchString}", + HostMatchKind.DomainKeyword => MatchString, + HostMatchKind.DomainRegex => $"regexp:{MatchString}", + HostMatchKind.Ip => MatchString, + HostMatchKind.GeoSite => $"geosite:{MatchString}", + HostMatchKind.GeoIp => $"geoip:{MatchString}", + _ => throw new ArgumentException("Invalid matcher kind.") + }; + + public static Dictionary> GroupByOutboundTag(List rules) + => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary(); + + public static Dictionary> GroupByMatchKind(List rules) + => rules.GroupBy(r => r.MatchKind).Select(g => (g.Key, g.ToList())).ToDictionary(); + + public static List> GroupByOutboundTagAndMatcherKind(List rules) + => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatchKind(groupByTag).Values).ToList(); + + public static V4ConfigJsonObjects.RoutingRule ListToJsonObject(List rules) + { + if (rules.Count == 0) + { + throw new ArgumentException("Rule list is empty."); + } + + var matchKind = rules[0].MatchKind; + var outboundTag = rules[0].OutboundTag; + + if (rules.Any(r => r.OutboundTag != outboundTag) || rules.Any(r => r.MatchKind != matchKind)) + { + throw new ArgumentException("Rules must have the same matcher kind and outbound tag."); + } + + List toolConfigList = rules.Select(r => r.ToolConfigString).ToList(); + + return new V4ConfigJsonObjects.RoutingRule(OutboundTag: outboundTag, + Ip: (matchKind is HostMatchKind.Ip or HostMatchKind.GeoIp) ? toolConfigList : null, + Domains: (matchKind.IsDomain() || matchKind == HostMatchKind.GeoSite) ? toolConfigList : null + ); + } + + public RoutingRule CloneGeositeWithCnAttribute(string outboundTag) + { + if (MatchKind is not HostMatchKind.GeoSite) + { + throw new ArgumentException("Matcher kind must be GeoSite."); + } + + return new RoutingRule(HostMatchKind.GeoSite, $"{MatchString}@cn", outboundTag); + } + + public V4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]); + + object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); +} + +public record Routing(List Rules, bool DirectGeositeCn = true, string DomainStrategy = "IpOnDemand") : IV4ConfigObject +{ + public List CreateGeositeCnDirectRules() + { + return Rules.Where(r => r.MatchKind is HostMatchKind.GeoSite) + .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList(); + } + + public V4ConfigJsonObjects.Routing ToJsonObjectV4(bool directGeositeCn = true) + { + List ruleJsonObjects = []; + + if (directGeositeCn) + { + ruleJsonObjects.Add(RoutingRule.ListToJsonObject(CreateGeositeCnDirectRules())); + } + + ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(RoutingRule.ListToJsonObject)); + + return new V4ConfigJsonObjects.Routing(ruleJsonObjects); + } + + object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); + + public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag, bool directGeositeCn) + { + + return new Routing( + proxyFile.Config.Items.Select( + i => new RoutingRule(i.Kind, i.MatchString, outboundTag)).ToList(), + directGeositeCn + ); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs new file mode 100644 index 0000000..b112e1c --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/StaticHosts.cs @@ -0,0 +1,40 @@ +namespace Crupest.SecretTool; + +public record StaticHostRule(HostMatchKind MatchKind, string MatchString, List ResolveResult) +{ + public string AddressString() + { + return MatchKind switch + { + HostMatchKind.DomainFull => MatchString, + HostMatchKind.DomainSuffix => $"domain:{MatchString}", + HostMatchKind.DomainKeyword => $"keyword:{MatchString}", + HostMatchKind.DomainRegex => $"regexp:{MatchString}", + _ => throw new ArgumentOutOfRangeException($"Match kind {MatchKind} is not allowed in static host rule."), + }; + } + + public object ResolveResultToJsonObject() + { + return ResolveResult.Count == 1 ? ResolveResult[0] : ResolveResult; + } +} + +public class StaticHosts(List rules) : IV4ConfigObject +{ + public List Rules { get; } = rules; + + public Dictionary ToJsonObjectV4() => + Rules.ToDictionary(rule => rule.AddressString(), rule => rule.ResolveResultToJsonObject()); + + object IV4ConfigObject.ToJsonObjectV4() + { + return ToJsonObjectV4(); + } + + public static StaticHosts CreateFromHostMatchConfigString(string configString) + { + var config = new HostMatchConfig(configString, HostMatchKindExtensions.DomainMatchKinds, minComponentCount: 1); + return new StaticHosts(config.Items.Select(i => new StaticHostRule(i.Kind, i.MatchString, [.. i.Values])).ToList()); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs new file mode 100644 index 0000000..451db3e --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs @@ -0,0 +1,60 @@ +namespace Crupest.SecretTool; + +public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoSiteData) +{ + public ProxyFile ProxyFile { get; } = proxyFile; + public GeoSiteData GeoSiteData { get; } = geoSiteData; + + private static string ToSurgeRuleString(HostMatchKind kind, string value) + { + var ruleType = kind switch + { + HostMatchKind.DomainFull => "DOMAIN", + HostMatchKind.DomainSuffix => "DOMAIN-SUFFIX", + HostMatchKind.DomainKeyword => "DOMAIN-KEYWORD", + HostMatchKind.DomainRegex => "URL-REGEX", + _ => throw new Exception("Unacceptable matcher kind for Surge rule.") + }; + + return $"{ruleType},{value}"; + } + + private static List DomainMatcherKinds { get; } = [ + HostMatchKind.DomainFull, HostMatchKind.DomainKeyword, + HostMatchKind.DomainRegex, HostMatchKind.DomainSuffix, + ]; + + public string GenerateChinaRuleSet() + { + var geoSites = ProxyFile.Config.Items.Where(i => i.Kind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList(); + var cnRules = GeoSiteData.GetEntriesRecursive(geoSites, DomainMatcherKinds, ["cn"]).ToList(); + return string.Join('\n', cnRules.Select(r => ToSurgeRuleString(r.Kind, r.Value))); + } + + public string GenerateGlobalRuleSet() + { + var geoSites = ProxyFile.Config.Items.Where(i => i.Kind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList(); + var nonCnRules = GeoSiteData.GetEntriesRecursive(geoSites, DomainMatcherKinds).Where(e => !e.Attributes.Contains("cn")).ToList(); + var domainRules = ProxyFile.Config.Items.Where(i => DomainMatcherKinds.Contains(i.Kind)).ToList(); + return string.Join('\n', [ + ..nonCnRules.Select(r => ToSurgeRuleString(r.Kind, r.Value)), + ..domainRules.Select(r => ToSurgeRuleString(r.Kind, r.MatchString)) + ]); + } + + public static SurgeConfigGenerator Create(string proxyFilePath, bool clean, bool silent) + { + var proxyFile = new ProxyFile(proxyFilePath); + var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); + return new SurgeConfigGenerator(proxyFile, geoSiteData); + } + + public static void GenerateTo(string proxyFilePath, string cnPath, string globalPath, bool clean, bool silent) + { + var generator = Create(proxyFilePath, clean, silent); + File.WriteAllText(cnPath, generator.GenerateChinaRuleSet()); + if (!silent) Console.WriteLine($"China rule set written to {cnPath}."); + File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet()); + if (!silent) Console.WriteLine($"Global rule set written to {globalPath}."); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Template.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Template.cs new file mode 100644 index 0000000..1fe91b1 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Template.cs @@ -0,0 +1,231 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Crupest.SecretTool; + +public class Template +{ + private enum ParseState + { + Text, + Dollar, + LeftBracket, + VariableName, + VariableNameFinish, + } + + private interface ITemplateNode + { + string Render(Dictionary values); + } + + private class TextNode(string text) : ITemplateNode + { + + public string Text { get; } = text; + + public string Render(Dictionary values) + { + return Text; + } + } + + private class VariableNode(string variableName) : ITemplateNode + { + public string VariableName { get; } = variableName; + + public string Render(Dictionary values) + { + return values.GetValueOrDefault(VariableName) ?? ""; + } + } + + public Template(string templateString) + { + TemplateString = templateString; + Nodes = Parse(templateString); + VariableNames = Nodes.OfType().Select(node => node.VariableName).ToList(); + } + + private static List Parse(string templateString) + { + int lineNumber = 1; + int columnNumber = 0; + List nodes = []; + ParseState state = ParseState.Text; + StringBuilder stringBuilder = new(); + + string GetPosition() => $"line {lineNumber} column{columnNumber}"; + + [DoesNotReturn] + void ReportInvalidState(string message) + { + throw new Exception($"Invalid state at {GetPosition()}: {message}"); + } + + [DoesNotReturn] + void ReportInvalidCharacter(char c) + { + throw new FormatException($"Unexpected '{c}' at {GetPosition()}."); + } + + void FinishText() + { + if (state != ParseState.Text) + { + ReportInvalidState($"Can't call FinishText here."); + } + + if (stringBuilder.Length > 0) + { + nodes.Add(new TextNode(stringBuilder.ToString())); + stringBuilder.Clear(); + } + } + + foreach (var c in templateString) + { + if (c == '\n') + { + lineNumber++; + columnNumber = 0; + } + + columnNumber++; + + switch (c) + { + case '$': + if (state == ParseState.Text) + { + FinishText(); + state = ParseState.Dollar; + } + else if (state == ParseState.Dollar) + { + if (stringBuilder.Length > 0) + { + throw new Exception($"Invalid state at {GetPosition()}: when we meet the second '$', text builder should be empty."); + } + stringBuilder.Append(c); + state = ParseState.Text; + } + else + { + throw new FormatException($"Unexpected '$' at {GetPosition()}."); + } + break; + case '{': + if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.Dollar) + { + state = ParseState.LeftBracket; + } + else + { + throw new Exception($"Unexpected '{{' at {GetPosition()}."); + } + break; + case '}': + if (state == ParseState.Text) + { + stringBuilder.Append(c); + state = ParseState.Text; + } + else if (state == ParseState.VariableName || state == ParseState.VariableNameFinish) + { + nodes.Add(new VariableNode(stringBuilder.ToString())); + stringBuilder.Clear(); + state = ParseState.Text; + } + else + { + ReportInvalidCharacter(c); + } + break; + default: + if (state == ParseState.Dollar) + { + ReportInvalidCharacter(c); + } + + if (char.IsWhiteSpace(c)) + { + if (state == ParseState.LeftBracket || state == ParseState.VariableNameFinish) + { + continue; + } + else if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.VariableName) + { + state = ParseState.VariableNameFinish; + } + else + { + ReportInvalidCharacter(c); + } + } + else + { + if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.LeftBracket || state == ParseState.VariableName) + { + stringBuilder.Append(c); + state = ParseState.VariableName; + } + else + { + ReportInvalidCharacter(c); + } + } + break; + } + } + + if (state == ParseState.Text) + { + FinishText(); + } + else + { + throw new FormatException("Unexpected end of template string."); + } + + return nodes; + } + + public string TemplateString { get; } + private List Nodes { get; set; } + public List VariableNames { get; } + + public string Generate(Dictionary values, bool allowMissingVariable = false) + { + StringBuilder stringBuilder = new(); + foreach (var node in Nodes) + { + if (node is TextNode textNode) + { + stringBuilder.Append(textNode.Text); + } + else if (node is VariableNode variableNode) + { + var hasValue = values.TryGetValue(variableNode.VariableName, out var value); + if (!hasValue && !allowMissingVariable) + { + throw new Exception($"Variable '{variableNode.VariableName}' is not set."); + } + stringBuilder.Append(hasValue ? value : string.Empty); + } + } + return stringBuilder.ToString(); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs new file mode 100644 index 0000000..4fe9a40 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs @@ -0,0 +1,154 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Crupest.SecretTool; + +public interface IV4ConfigObject +{ + object ToJsonObjectV4(); +} + +public class ToolConfig(Template template, List proxies, Routing router, StaticHosts? hosts) +{ + private class JsonInterfaceConverter : JsonConverter + { + public override Interface Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write( + Utf8JsonWriter writer, + Interface value, + JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, typeof(object), options); + } + } + + + public const string ConfigTemplateFileName = "config.json.template"; + public const string VmessConfigFileName = "vmess.txt"; + public const string ProxyConfigFileName = "proxy.txt"; + public const string HostsConfigFileName = "hosts.txt"; + + public static List RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName]; + public static List ConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, HostsConfigFileName]; + + private const string ProxyAnchor = "PROXY_ANCHOR"; + private const string RoutingAnchor = "ROUTING_ANCHOR"; + private const string HostsAnchor = "HOSTS_ANCHOR"; + + public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V2RAY_GEOSITE_USE_CN"; + + private static bool UseCnGeoSite => Environment.GetEnvironmentVariable(AddCnAttributeToGeositeEnvironmentVariable) switch + { + "0" or "false" or "off" or "disable" => false, + _ => true + }; + + public Template Template { get; set; } = template; + public List Proxies { get; set; } = proxies; + public Routing Routing { get; set; } = router; + public StaticHosts Hosts { get; set; } = hosts is null ? new StaticHosts([]) : hosts; + + public string ToJsonStringV4(bool pretty = true) + { + var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }); + // TODO: Make interface converter generic. + jsonOptions.Converters.Add(new JsonInterfaceConverter()); + jsonOptions.Converters.Add(new JsonInterfaceConverter()); + + var templateValues = new Dictionary + { + [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))), + [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(), jsonOptions), + [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions), + }; + + var configString = Template.Generate(templateValues); + + if (pretty) + { + var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions) + { + WriteIndented = true, + }; + return JsonSerializer.Serialize(JsonSerializer.Deserialize(configString, jsonOptionsPretty), jsonOptionsPretty); + } + else + { + return configString; + } + } + + public static ToolConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string? hostsPath) + { + foreach (var path in new List([templatePath, vmessPath, proxyPath])) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Required config file not found: {path}."); + } + } + + ProxyFile proxyFile = new(proxyPath); + string templateString, vmessString; + string? hostsString; + + string file = ""; + try + { + file = templatePath; + templateString = File.ReadAllText(templatePath); + file = vmessPath; + vmessString = File.ReadAllText(vmessPath); + hostsString = hostsPath is not null ? File.ReadAllText(hostsPath) : null; + } + catch (Exception e) + { + throw new Exception($"Error reading config file {file}.", e); + } + + try + { + file = templatePath; + var template = new Template(templateString); + file = vmessPath; + var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy"); + file = proxyPath; + var routing = Routing.FromProxyFile(proxyFile, "proxy", UseCnGeoSite); + file = hostsPath ?? ""; + var hosts = hostsString is not null ? StaticHosts.CreateFromHostMatchConfigString(hostsString) : null; + return new ToolConfig(template, [vmess], routing, hosts); + } + catch (Exception e) + { + throw new Exception($"Error parsing config file {file}.", e); + } + } + + public static ToolConfig FromDirectory(string directory) + { + return FromFiles( + Path.Join(directory, ConfigTemplateFileName), + Path.Join(directory, VmessConfigFileName), + Path.Join(directory, ProxyConfigFileName), + Path.Join(directory, HostsConfigFileName) + ); + } + + public static void FromDirectoryAndWriteToFile(string directory, string outputPath) + { + var config = FromDirectory(directory); + File.WriteAllText(outputPath, config.ToJsonStringV4()); + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs new file mode 100644 index 0000000..3e81dbb --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/V4ConfigJsonObjects.cs @@ -0,0 +1,25 @@ +namespace Crupest.SecretTool; + +public static class V4ConfigJsonObjects +{ + public interface IObject; + public interface IOutboundSettings : IObject; + public interface IOutboundStreamSettings : IObject; + + public record WsSettings(string Path, Dictionary Headers) : IObject; + public record WsStreamSettings(string Network, string Security, WsSettings WsSettings) : IOutboundStreamSettings; + public record VnextServerUser(string Id, int AlterId, string Security, int Level) : IObject; + public record VnextServer(string Address, int Port, List Users) : IObject; + public record VmessOutboundSettings(List Vnext) : IOutboundSettings; + public record HttpOutboundUser(string User, string Pass) : IObject; + public record HttpOutboundServer(string Address, int Port, List Users) : IObject; + public record HttpOutboundSettings(List Servers) : IOutboundSettings; + public record Outbound(string Tag, string Protocol, IOutboundSettings Settings, + IOutboundStreamSettings? StreamSettings) : IObject; + + public record RoutingRule(string DomainMatcher = "mph", string Type = "field", List? Domains = null, List? Ip = null, + string? Port = null, string? SourcePort = null, string? Network = null, List? Source = null, + List? User = null, List? InboundTag = null, List? Protocol = null, string? Attrs = null, + string? OutboundTag = null, string? BalancerTag = null) : IObject; + public record Routing(List Rules, string DomainStrategy = "IpOnDemand", string DomainMatcher = "mph") : IObject; +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs new file mode 100644 index 0000000..a50e9be --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/V5ConfigJsonObjects.cs @@ -0,0 +1,31 @@ +namespace Crupest.SecretTool; + +public static class V5ConfigJsonObjects +{ + public record OutboundObject(string Protocol, object Settings, string Tag, object? StreamSettings) + { + public static OutboundObject VmessViaWs(string tag, string address, int port, string uuid, string path) + { + return new OutboundObject("vmess", new VmessSettings(address, port, uuid), tag, StreamSettingsObject.Ws(path)); + } + + public static OutboundObject Http(string tag, string address, int port) + { + return new OutboundObject("http", new HttpSettingsObject(address, port), tag, null); + } + } + + public record WsSettingsObject(string Path, Dictionary Headers); + + public record StreamSettingsObject(string Transport, object TransportSettings, string Security, object SecuritySettings) + { + public static StreamSettingsObject Ws(string path) + { + return new StreamSettingsObject("ws", new WsSettingsObject(path, new()), "tls", new()); + } + } + + public record VmessSettings(string Address, int Port, string Uuid); + + public record HttpSettingsObject(string Address, int Port); +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/config.json.template b/tools/Crupest.SecretTool/Crupest.SecretTool/config.json.template new file mode 100644 index 0000000..424e996 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/config.json.template @@ -0,0 +1,63 @@ +{ + "log": { + "loglevel": "warning" + }, + "inbounds": [ + { + "port": 2081, + "listen": "127.0.0.1", + "tag": "socks-inbound", + "protocol": "socks", + "settings": { + "auth": "noauth" + } + }, + { + "port": 2080, + "listen": "127.0.0.1", + "tag": "http-inbound", + "protocol": "http", + "settings": { + "auth": "noauth" + } + } + ], + "outbounds": [ + { + "protocol": "freedom", + "settings": {}, + "tag": "direct" + }, + { + "protocol": "blackhole", + "settings": {}, + "tag": "blocked" + }, + ${PROXY_ANCHOR} + ], + "routing": ${ROUTING_ANCHOR}, + "dns": { + "hosts": ${HOSTS_ANCHOR}, + "servers": [ + "https://doh.pub/dns-query", + "1.1.1.1", + "8.8.8.8", + "localhost" + ] + }, + "policy": { + "levels": { + "0": { + "uplinkOnly": 0, + "downlinkOnly": 0 + } + }, + "system": { + "statsInboundUplink": false, + "statsInboundDownlink": false, + "statsOutboundUplink": false, + "statsOutboundDownlink": false + } + }, + "other": {} +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template b/tools/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template new file mode 100644 index 0000000..01ccf7a --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/config.v5.json.template @@ -0,0 +1,55 @@ +{ + "log": { + "access": { + "type": "Console", + "level": "Info" + } + }, + "dns": { + "nameServer": [{ + "address": "https://doh.pub/dns-query" + }, { + "address": "1.1.1.1" + }, { + "address": "8.8.8.8" + }, { + "address": "localhost" + }], + "staticHosts": ${HOSTS_ANCHOR} + }, + "inbounds": [{ + { + "protocol": "socks", + "port": 2081, + "listen": "127.0.0.1", + "tag": "socks-inbound", + "settings": { + "auth": "noauth" + } + }, + { + "protocol": "http", + "port": 2080, + "listen": "127.0.0.1", + "tag": "http-inbound", + "settings": { + "auth": "noauth" + } + } + }], + "outbounds": [ + { + "protocol": "freedom", + "settings": {}, + "tag": "direct" + }, + { + "protocol": "blackhole", + "settings": {}, + "tag": "blocked" + }, + ${PROXY_ANCHOR} + ], + "router": ${ROUTER_ANCHOR} +} + diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/hosts.txt b/tools/Crupest.SecretTool/Crupest.SecretTool/hosts.txt new file mode 100644 index 0000000..88d5015 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/hosts.txt @@ -0,0 +1,2 @@ +cdn.jsdelivr.net cdn.jsdelivr.net.cdn.cloudflare.net + diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt b/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt new file mode 100644 index 0000000..6273e35 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt @@ -0,0 +1,45 @@ +GeoSite github +GeoSite google +GeoSite youtube +GeoSite twitter +GeoSite facebook +GeoSite discord +GeoSite reddit +GeoSite twitch +GeoSite onedrive +GeoSite quora +GeoSite telegram +GeoSite imgur +GeoSite stackexchange + +GeoSite duckduckgo +GeoSite wikimedia +GeoSite gitbook +GeoSite gitlab +GeoSite sourceforge +GeoSite creativecommons +GeoSite archive +GeoSite matrix +GeoSite tor + +GeoSite python +GeoSite ruby +GeoSite rust +GeoSite nodejs +GeoSite npmjs +GeoSite qt +GeoSite docker +GeoSite v2ray +GeoSite homebrew + +GeoSite azure +GeoSite jsdelivr +GeoSite fastly +GeoSite heroku +GeoSite bootstrap +GeoSite vercel + +GeoSite ieee +GeoSite sci-hub +GeoSite libgen +GeoSite z-library diff --git a/tools/Crupest.SecretTool/build-secret.bash b/tools/Crupest.SecretTool/build-secret.bash new file mode 100755 index 0000000..8878049 --- /dev/null +++ b/tools/Crupest.SecretTool/build-secret.bash @@ -0,0 +1,41 @@ +#! /usr/bin/env bash + +set -e + +function print_argument_error_message_and_exit() { + argument_error_message="You must specify exactly one argument, the build target (win-x64 | linux-x64 | osx-x64)." + echo "$argument_error_message" + exit 1 +} + + + +if [[ $# != 1 ]]; then + print_argument_error_message_and_exit +fi + +case "$1" in + win-x64 | linux-x64 | osx-x64) + echo "Build target: $1" + ;; + *) + print_argument_error_message_and_exit + ;; +esac + +secret_dir=$(realpath "$(dirname "$0")") + +echo "Secret dir: ${secret_dir}" + +echo "Check dotnet..." +dotnet --version + +echo "Enter \"secret\" dir..." +pushd "$secret_dir" + +echo "Begin to build..." +dotnet publish Crupest.SecretTool -c Release -o "$secret_dir/publish" --sc -r "$1" + +popd + +echo "Finish!" diff --git a/tools/Crupest.SecretTool/tools/cru-proxy-edit b/tools/Crupest.SecretTool/tools/cru-proxy-edit new file mode 100755 index 0000000..51a33e1 --- /dev/null +++ b/tools/Crupest.SecretTool/tools/cru-proxy-edit @@ -0,0 +1,12 @@ +#! /usr/bin/env bash + +set -e + +p="$HOME/codes/crupest/tools/Crupest.SecretTool/publish/proxy.txt" + +if [[ ! -f "$p" ]]; then + echo "File $p does not exist!" >&2 + exit 1 +fi + +exec vim "$p" diff --git a/tools/Crupest.SecretTool/tools/cru-proxy-log b/tools/Crupest.SecretTool/tools/cru-proxy-log new file mode 100755 index 0000000..6ec6ee1 --- /dev/null +++ b/tools/Crupest.SecretTool/tools/cru-proxy-log @@ -0,0 +1,13 @@ +#! /usr/bin/env bash + +set -e + +if [[ -e /proc ]]; then + # I don't believe your system is Linux but there is no /proc. + exec journalctl --user -u crupest-secret-tool "$@" +elif [[ "$(uname)" == "Darwin" ]]; then + exec less "$HOME/.local/state/Crupest.SecretTool/log" +else + echo "Not supported on systems other than macOS and Linux now." >&2 + exit 1 +fi diff --git a/tools/Crupest.SecretTool/tools/crupest-secret-tool.service b/tools/Crupest.SecretTool/tools/crupest-secret-tool.service new file mode 100644 index 0000000..df6d172 --- /dev/null +++ b/tools/Crupest.SecretTool/tools/crupest-secret-tool.service @@ -0,0 +1,8 @@ +[Unit] +Description=crupest v2ray service + +[Service] +ExecStart=%h/.local/bin/Crupest.SecretTool + +[Install] +WantedBy=default.target diff --git a/tools/Crupest.SecretTool/tools/life.crupest.secret-tool.plist b/tools/Crupest.SecretTool/tools/life.crupest.secret-tool.plist new file mode 100644 index 0000000..bdfe490 --- /dev/null +++ b/tools/Crupest.SecretTool/tools/life.crupest.secret-tool.plist @@ -0,0 +1,18 @@ + + + + + Label + life.crupest.secret-tool + ProgramArguments + + /Users/crupest/.local/bin/Crupest.SecretTool + + KeepAlive + + StandardOutPath + /Users/crupest/.local/state/Crupest.SecretTool/log + StandardErrorPath + /Users/crupest/.local/state/Crupest.SecretTool/error + + diff --git a/tools/Crupest.V2ray/.gitignore b/tools/Crupest.V2ray/.gitignore deleted file mode 100644 index ac4d8a4..0000000 --- a/tools/Crupest.V2ray/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.vs -bin -obj -*.pubxml.user -*.csproj.user - -publish diff --git a/tools/Crupest.V2ray/Crupest.V2ray/.gitignore b/tools/Crupest.V2ray/Crupest.V2ray/.gitignore deleted file mode 100644 index c936492..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/.gitignore +++ /dev/null @@ -1 +0,0 @@ -vmess.txt diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Config.cs b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs deleted file mode 100644 index 2ef18f0..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/Config.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace Crupest.V2ray; - -public record ConfigItem(string Value, int LineNumber); - -public class DictionaryConfig(string configString, List? requiredKeys = null) -{ - private static Dictionary Parse(string configString, List? requiredKeys = null) - { - var config = new Dictionary(); - var lines = configString.Split('\n'); - int lineNumber = 1; - - foreach (var line in lines) - { - var trimmedLine = line.Trim(); - if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith('#')) - { - lineNumber++; - continue; - } - - var equalIndex = trimmedLine.IndexOf('='); - if (equalIndex == -1) - { - throw new FormatException($"No '=' found in line {lineNumber}."); - } - - config.Add(trimmedLine[..equalIndex].Trim(), new ConfigItem(trimmedLine[(equalIndex + 1)..].Trim(), lineNumber)); - lineNumber++; - } - - if (requiredKeys is not null) - { - foreach (var key in requiredKeys) - { - if (!config.ContainsKey(key)) - { - throw new FormatException($"Required key '{key}' not found in config."); - } - } - } - - return config; - } - - public string ConfigString { get; } = configString; - public List? RequiredKeys { get; } = requiredKeys; - public Dictionary Config { get; } = Parse(configString); - public ConfigItem GetItemCaseInsensitive(string key) - { - foreach (var (originalKey, value) in Config) - { - if (string.Equals(originalKey, key, StringComparison.OrdinalIgnoreCase)) - { - return value; - } - } - throw new KeyNotFoundException($"Key '{key}' not found in config case-insensitively."); - } -} - -public class ListConfig(string configString) -{ - private static List Parse(string configString) - { - var config = new List(); - var lines = configString.Split('\n'); - int lineNumber = 1; - - foreach (var line in lines) - { - var l = line; - var beginOfComment = l.IndexOf('#'); - if (beginOfComment >= 0) - { - l = line[..beginOfComment]; - } - l = l.Trim(); - if (!string.IsNullOrEmpty(l)) - { - config.Add(new ConfigItem(l, lineNumber)); - } - lineNumber++; - } - - return config; - } - - public string ConfigString { get; } = configString; - public List Config { get; } = Parse(configString); -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj b/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj deleted file mode 100644 index 0812e4c..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs deleted file mode 100644 index 547adeb..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Crupest.V2ray; - -public class FileWatcher(string directory, List fileNames) -{ - public string Directory { get; set; } = directory; - public List FileNames { get; set; } = fileNames; - - public delegate void OnChangedHandler(); - public event OnChangedHandler? OnChanged; - - public void Run() - { - var sourceWatcher = new FileSystemWatcher(Directory); - foreach (var fileName in FileNames) - { - sourceWatcher.Filters.Add(fileName); - } - sourceWatcher.NotifyFilter = NotifyFilters.LastWrite; - - while (true) - { - var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed); - OnChanged?.Invoke(); - } - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs deleted file mode 100644 index 3dce3f6..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System.IO.Compression; - -namespace Crupest.V2ray; - -public interface IGeoSiteEntry -{ - bool IsInclude { get; } - string Value { get; } -} - -public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSiteEntry -{ - public bool IsInclude => true; -} - -public record GeoSiteRuleEntry(V2rayHostMatcherKind Kind, string Value, List Attributes, string ContainingSite) : IGeoSiteEntry -{ - public bool IsInclude => false; -} - -public record GeoSite(string Name, List Entries) -{ - public static GeoSite Parse(string name, string str) - { - List entries = []; - var listConfig = new ListConfig(str); - foreach (var item in listConfig.Config) - { - var (value, line) = item; - - if (value.StartsWith("include:")) - { - var include = value["include:".Length..].Trim(); - if (include.Length == 0 || include.Contains(' ')) - { - throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid include value."); - } - entries.Add(new GeoSiteIncludeEntry(include, name)); - continue; - } - - var segments = value.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (segments.Length > 2) - { - throw new FormatException($"Invalid geo site rule '{name}' in line {line}. More than one ':'."); - } - - V2rayHostMatcherKind kind; - if (segments.Length == 2) - { - kind = segments[0] switch - { - "domain" => kind = V2rayHostMatcherKind.DomainSuffix, - "full" => kind = V2rayHostMatcherKind.DomainFull, - "keyword" => kind = V2rayHostMatcherKind.DomainKeyword, - "regexp" => kind = V2rayHostMatcherKind.DomainRegex, - _ => throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Unknown matcher.") - }; - } - else - { - kind = V2rayHostMatcherKind.DomainSuffix; - } - - var domainSegments = segments[^1].Split('@', StringSplitOptions.TrimEntries); - var domain = domainSegments[0]; - if (kind != V2rayHostMatcherKind.DomainRegex && Uri.CheckHostName(domain) != UriHostNameType.Dns) - { - throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid domain."); - } - - List attributes = []; - foreach (var s in domainSegments[1..]) - { - if (s.Length == 0) - { - throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Empty attribute value."); - } - attributes.Add(s); - } - - entries.Add(new GeoSiteRuleEntry(kind, domain, attributes, name)); - } - return new GeoSite(name, entries); - } -} - -public class GeoSiteData(string directory) -{ - private static List Parse(string directory) - { - var sites = new List(); - foreach (var path in Directory.GetFileSystemEntries(directory)) - { - var content = File.ReadAllText(path); - sites.Add(GeoSite.Parse(Path.GetFileName(path), content)); - } - return sites; - } - - public string DataDirectory { get; } = directory; - - public List Sites { get; } = Parse(directory); - - public GeoSite? GetSite(string name) - { - return Sites.Where(s => s.Name == name).FirstOrDefault(); - } - - public List GetEntriesRecursive(List sites, - List? onlyMatcherKinds = null, List? onlyAttributes = null) - { - List entries = []; - HashSet visited = []; - HashSet? kinds = onlyMatcherKinds?.ToHashSet(); - - void Visit(string site) - { - if (visited.Contains(site)) - { - return; - } - - visited.Add(site); - var siteData = GetSite(site); - if (siteData == null) - { - return; - } - foreach (var entry in siteData.Entries) - { - if (entry is GeoSiteIncludeEntry includeEntry) - { - Visit(includeEntry.Value); - } - else if (entry is GeoSiteRuleEntry geoSiteRuleEntry) - { - if (kinds != null && !kinds.Contains(geoSiteRuleEntry.Kind)) - { - continue; - } - - if (onlyAttributes != null && !geoSiteRuleEntry.Attributes.Intersect(onlyAttributes).Any()) - { - continue; - } - - entries.Add(geoSiteRuleEntry); - } - } - } - - foreach (var s in sites) - { - Visit(s); - } - - return entries; - } -} - -public class GeoDataManager -{ - public const string GeoSiteFileName = "geosite.dat"; - public const string GeoIpFileName = "geoip.dat"; - public const string GeoIpCnFileName = "geoip-only-cn-private.dat"; - public const string V2rayGithubOrganization = "v2fly"; - public const string V2rayGeoSiteGithubRepository = "domain-list-community"; - public const string V2rayGeoIpGithubRepository = "geoip"; - public const string V2rayGeoSiteCnGithubReleaseFilename = "dlc.dat"; - public const string V2rayGeoIpGithubReleaseFilename = "geoip.dat"; - public const string V2rayGeoIpCnGithubReleaseFilename = "geoip-only-cn-private.dat"; - - public static GeoDataManager Instance { get; } = new GeoDataManager(); - - public record GeoDataAsset(string Name, string FileName, string GithubUser, string GithubRepo, string GithubReleaseFileName); - - public GeoDataManager() - { - Assets = - [ - new("geosite", GeoSiteFileName, V2rayGithubOrganization, V2rayGeoSiteGithubRepository, V2rayGeoSiteGithubRepository), - new("geoip", GeoIpFileName, V2rayGithubOrganization, V2rayGeoIpGithubRepository, V2rayGeoIpGithubReleaseFilename), - new("geoip-cn", GeoIpCnFileName, V2rayGithubOrganization, V2rayGeoIpGithubRepository, V2rayGeoIpCnGithubReleaseFilename), - ]; - } - - public List Assets { get; set; } - - public GeoSiteData? GeoSiteData { get; set; } - - public GeoSiteData GetOrCreateGeoSiteData(bool clean, bool silent) - { - if (GeoSiteData is not null) { return GeoSiteData; } - GeoSiteData = DownloadAndGenerateGeoSiteData(clean, silent); - return GeoSiteData; - } - - private static string GetReleaseFileUrl(string user, string repo, string fileName) - { - return $"https://github.com/{user}/{repo}/releases/latest/download/{fileName}"; - } - - private static void GithubDownloadRelease(HttpClient httpClient, string user, string repo, string fileName, string outputPath) - { - using var responseStream = httpClient.GetStreamAsync(GetReleaseFileUrl(user, repo, fileName)).Result; - using var outputFileStream = File.OpenWrite(outputPath); - responseStream.CopyTo(outputFileStream); - } - - public bool HasAllAssets(string directory, out List missing) - { - missing = []; - foreach (var asset in Assets) - { - var assetPath = Path.Combine(directory, asset.FileName); - if (!File.Exists(assetPath)) - { - missing.Add(asset.Name); - } - } - return missing.Count == 0; - } - - public void Download(string outputDir, bool silent) - { - using var httpClient = new HttpClient(); - - foreach (var asset in Assets) - { - if (!silent) - { - Console.WriteLine($"Downloading {asset.Name}..."); - } - GithubDownloadRelease(httpClient, asset.GithubUser, asset.GithubRepo, asset.GithubReleaseFileName, Path.Combine(outputDir, asset.FileName)); - if (!silent) - { - Console.WriteLine($"Downloaded {asset.Name}!"); - } - } - } - - private static string GetGithubRepositoryArchiveUrl(string user, string repo) - { - return $"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"; - } - - private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath, bool silent) - { - var url = GetGithubRepositoryArchiveUrl(user, repo); - if (!silent) { Console.WriteLine($"Begin to download data from {url} to {outputPath}."); } - using var responseStream = httpClient.GetStreamAsync(url).Result; - using var outputFileStream = File.OpenWrite(outputPath); - responseStream.CopyTo(outputFileStream); - if (!silent) { Console.WriteLine("Succeeded to download."); } - } - - private static void Unzip(string zipPath, string outputPath) - { - using var zip = ZipFile.OpenRead(zipPath) ?? throw new Exception($"Failed to open zip file {zipPath}"); - zip.ExtractToDirectory(outputPath); - } - - private static string DownloadAndExtractGeoDataRepository(bool cleanTempDirIfFailed, bool silent, out string tempDirectoryPath) - { - tempDirectoryPath = ""; - const string zipFileName = "v2ray-geosite-master.zip"; - using var httpClient = new HttpClient(); - var tempDirectory = Directory.CreateTempSubdirectory(Program.Name); - tempDirectoryPath = tempDirectory.FullName; - try - { - var archivePath = Path.Combine(tempDirectoryPath, zipFileName); - var extractPath = Path.Combine(tempDirectoryPath, "repo"); - GithubDownloadRepository(httpClient, V2rayGithubOrganization, V2rayGeoSiteGithubRepository, archivePath, silent); - if (!silent) { Console.WriteLine($"Extract geo data to {extractPath}."); } - Directory.CreateDirectory(extractPath); - Unzip(archivePath, extractPath); - if (!silent) { Console.WriteLine($"Extraction done."); } - return Path.Join(extractPath, "domain-list-community-master"); - } - catch (Exception) - { - if (cleanTempDirIfFailed) - { - Directory.Delete(tempDirectoryPath, true); - } - throw; - } - } - - private static GeoSiteData DownloadAndGenerateGeoSiteData(bool clean, bool silent) - { - var repoDirectory = DownloadAndExtractGeoDataRepository(clean, silent, out var tempDirectoryPath); - try - { - return new GeoSiteData(Path.Join(repoDirectory, "data")); - } - finally - { - if (clean) - { - Directory.Delete(tempDirectoryPath, true); - } - } - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs deleted file mode 100644 index 0e98861..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Reflection; - -namespace Crupest.V2ray; - -public static class Program -{ - public static string Name { get; } = typeof(Program).Namespace ?? throw new Exception("Can't get the name of Crupest.V2ray."); - - public static string CrupestV2rayDirectory { get; } = - Environment.GetEnvironmentVariable("CRUPEST_V2RAY_DIR") ?? - Path.GetFullPath(Path.GetDirectoryName( - Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of Crupest.V2ray.")); - - private const string ConfigOutputFileName = "config.json"; - private const string SurgeRuleSetChinaOutputFileName = "ChinaRuleSet.txt"; - private const string SurgeRuleSetGlobalOutputFileName = "GlobalRuleSet.txt"; - - public static void RunV2rayAndWatchConfigChange() - { - var v2rayPath = V2rayController.FindExecutable(CrupestV2rayDirectory, out var isLocal) ?? - throw new Exception("Can't find v2ray executable either in Crupest.V2ray directory or in PATH."); - - string? assetsPath; - if (isLocal) - { - assetsPath = CrupestV2rayDirectory; - var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestV2rayDirectory, out var missing); - if (!assetsComplete) - { - throw new Exception($"Missing assets: {string.Join(", ", missing)} in {CrupestV2rayDirectory}. This v2ray is local. So only use assets in Crupest.V2ray directory."); - } - } - else - { - assetsPath = CrupestV2rayDirectory; - var assetsComplete = GeoDataManager.Instance.HasAllAssets(CrupestV2rayDirectory, out var missing); - if (!assetsComplete) - { - Console.WriteLine($"Missing assets: {string.Join(", ", missing)} in {CrupestV2rayDirectory}. This v2ray is global. So fallback to its own assets."); - assetsPath = null; - } - } - - var v2rayController = new V2rayController(v2rayPath, Path.Combine(CrupestV2rayDirectory, ConfigOutputFileName), assetsPath); - var configFileWatcher = new FileWatcher(CrupestV2rayDirectory, V2rayConfig.ConfigFileNames); - - V2rayConfig.FromDirectoryAndWriteToFile(CrupestV2rayDirectory, Path.Join(CrupestV2rayDirectory, ConfigOutputFileName)); - v2rayController.Start(); - - configFileWatcher.OnChanged += () => - { - V2rayConfig.FromDirectoryAndWriteToFile(CrupestV2rayDirectory, Path.Join(CrupestV2rayDirectory, ConfigOutputFileName)); - v2rayController.Restart(); - }; - - configFileWatcher.Run(); - } - - public static void Main(string[] args) - { - if (args.Length != 0) - { - if (args.Length != 1) - { - throw new Exception("Invalid command line arguments."); - } - var verb = args[0].ToLower(); - if (verb == "download-geodata" || verb == "dg") - { - GeoDataManager.Instance.Download(CrupestV2rayDirectory, false); - return; - } - else if (verb == "generate-surge-rule-set" || verb == "gs") - { - SurgeConfigGenerator.GenerateTo( - Path.Join(CrupestV2rayDirectory, "proxy.txt"), - Path.Join(CrupestV2rayDirectory, SurgeRuleSetChinaOutputFileName), - Path.Join(CrupestV2rayDirectory, SurgeRuleSetGlobalOutputFileName), - true, false - ); - return; - } - else if (verb == "generate" || verb == "g") - { - var config = V2rayConfig.FromDirectory(CrupestV2rayDirectory); - Console.Out.WriteLine(config.ToJsonStringV4()); - return; - } - throw new Exception("Invalid command line arguments."); - } - - RunV2rayAndWatchConfigChange(); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml b/tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml deleted file mode 100644 index bbdd2ad..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/Properties/PublishProfiles/FolderProfile.pubxml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Release - Any CPU - bin\Release\net7.0\publish\ - FileSystem - <_TargetId>Folder - - \ No newline at end of file diff --git a/tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs b/tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs deleted file mode 100644 index ca5ca56..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Crupest.V2ray; - -public class ProxyFile(string path) : - HostMatcherConfigFile(path, [.. Enum.GetValues()], maxComponentCount: 0) -{ - public V2rayRouting ToV2rayRouting(string outboundTag, bool directGeositeCn) - { - return new V2rayRouting( - MatcherConfig.Items.Select( - i => new V2rayRoutingRule(i.Kind, i.Matcher, outboundTag)).ToList(), - directGeositeCn - ); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/SurgeConfigGenerator.cs b/tools/Crupest.V2ray/Crupest.V2ray/SurgeConfigGenerator.cs deleted file mode 100644 index bd52234..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/SurgeConfigGenerator.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace Crupest.V2ray; - -public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoSiteData) -{ - public ProxyFile ProxyFile { get; } = proxyFile; - public GeoSiteData GeoSiteData { get; } = geoSiteData; - - private static string ToSurgeRuleString(V2rayHostMatcherKind kind, string value) - { - var ruleType = kind switch - { - V2rayHostMatcherKind.DomainFull => "DOMAIN", - V2rayHostMatcherKind.DomainSuffix => "DOMAIN-SUFFIX", - V2rayHostMatcherKind.DomainKeyword => "DOMAIN-KEYWORD", - V2rayHostMatcherKind.DomainRegex => "URL-REGEX", - _ => throw new Exception("Unacceptable matcher kind for Surge rule.") - }; - - return $"{ruleType},{value}"; - } - - private static List DomainMatcherKinds { get; } = [ - V2rayHostMatcherKind.DomainFull, V2rayHostMatcherKind.DomainKeyword, - V2rayHostMatcherKind.DomainRegex, V2rayHostMatcherKind.DomainSuffix, - ]; - - public string GenerateChinaRuleSet() - { - var geoSites = ProxyFile.MatcherConfig.Items.Where(i => i.Kind == V2rayHostMatcherKind.GeoSite).Select(i => i.Matcher).ToList(); - var cnRules = GeoSiteData.GetEntriesRecursive(geoSites, DomainMatcherKinds, ["cn"]).ToList(); - return string.Join('\n', cnRules.Select(r => ToSurgeRuleString(r.Kind, r.Value))); - } - - public string GenerateGlobalRuleSet() - { - var geoSites = ProxyFile.MatcherConfig.Items.Where(i => i.Kind == V2rayHostMatcherKind.GeoSite).Select(i => i.Matcher).ToList(); - var nonCnRules = GeoSiteData.GetEntriesRecursive(geoSites, DomainMatcherKinds).Where(e => !e.Attributes.Contains("cn")).ToList(); - var domainRules = ProxyFile.MatcherConfig.Items.Where(i => DomainMatcherKinds.Contains(i.Kind)).ToList(); - return string.Join('\n', [ - ..nonCnRules.Select(r => ToSurgeRuleString(r.Kind, r.Value)), - ..domainRules.Select(r => ToSurgeRuleString(r.Kind, r.Matcher)) - ]); - } - - public static SurgeConfigGenerator Create(string proxyFilePath, bool clean, bool silent) - { - var proxyFile = new ProxyFile(proxyFilePath); - var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); - return new SurgeConfigGenerator(proxyFile, geoSiteData); - } - - public static void GenerateTo(string proxyFilePath, string cnPath, string globalPath, bool clean, bool silent) - { - var generator = Create(proxyFilePath, clean, silent); - File.WriteAllText(cnPath, generator.GenerateChinaRuleSet()); - if (!silent) Console.WriteLine($"China rule set written to {cnPath}."); - File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet()); - if (!silent) Console.WriteLine($"Global rule set written to {globalPath}."); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Template.cs b/tools/Crupest.V2ray/Crupest.V2ray/Template.cs deleted file mode 100644 index 9c137b0..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/Template.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Crupest.V2ray; - -public class Template -{ - private enum ParseState - { - Text, - Dollar, - LeftBracket, - VariableName, - VariableNameFinish, - } - - private interface ITemplateNode - { - string Render(Dictionary values); - } - - private class TextNode(string text) : ITemplateNode - { - - public string Text { get; } = text; - - public string Render(Dictionary values) - { - return Text; - } - } - - private class VariableNode(string variableName) : ITemplateNode - { - public string VariableName { get; } = variableName; - - public string Render(Dictionary values) - { - return values.GetValueOrDefault(VariableName) ?? ""; - } - } - - public Template(string templateString) - { - TemplateString = templateString; - Nodes = Parse(templateString); - VariableNames = Nodes.OfType().Select(node => node.VariableName).ToList(); - } - - private static List Parse(string templateString) - { - int lineNumber = 1; - int columnNumber = 0; - List nodes = []; - ParseState state = ParseState.Text; - StringBuilder stringBuilder = new(); - - string GetPosition() => $"line {lineNumber} column{columnNumber}"; - - [DoesNotReturn] - void ReportInvalidState(string message) - { - throw new Exception($"Invalid state at {GetPosition()}: {message}"); - } - - [DoesNotReturn] - void ReportInvalidCharacter(char c) - { - throw new FormatException($"Unexpected '{c}' at {GetPosition()}."); - } - - void FinishText() - { - if (state != ParseState.Text) - { - ReportInvalidState($"Can't call FinishText here."); - } - - if (stringBuilder.Length > 0) - { - nodes.Add(new TextNode(stringBuilder.ToString())); - stringBuilder.Clear(); - } - } - - foreach (var c in templateString) - { - if (c == '\n') - { - lineNumber++; - columnNumber = 0; - } - - columnNumber++; - - switch (c) - { - case '$': - if (state == ParseState.Text) - { - FinishText(); - state = ParseState.Dollar; - } - else if (state == ParseState.Dollar) - { - if (stringBuilder.Length > 0) - { - throw new Exception($"Invalid state at {GetPosition()}: when we meet the second '$', text builder should be empty."); - } - stringBuilder.Append(c); - state = ParseState.Text; - } - else - { - throw new FormatException($"Unexpected '$' at {GetPosition()}."); - } - break; - case '{': - if (state == ParseState.Text) - { - stringBuilder.Append(c); - } - else if (state == ParseState.Dollar) - { - state = ParseState.LeftBracket; - } - else - { - throw new Exception($"Unexpected '{{' at {GetPosition()}."); - } - break; - case '}': - if (state == ParseState.Text) - { - stringBuilder.Append(c); - state = ParseState.Text; - } - else if (state == ParseState.VariableName || state == ParseState.VariableNameFinish) - { - nodes.Add(new VariableNode(stringBuilder.ToString())); - stringBuilder.Clear(); - state = ParseState.Text; - } - else - { - ReportInvalidCharacter(c); - } - break; - default: - if (state == ParseState.Dollar) - { - ReportInvalidCharacter(c); - } - - if (char.IsWhiteSpace(c)) - { - if (state == ParseState.LeftBracket || state == ParseState.VariableNameFinish) - { - continue; - } - else if (state == ParseState.Text) - { - stringBuilder.Append(c); - } - else if (state == ParseState.VariableName) - { - state = ParseState.VariableNameFinish; - } - else - { - ReportInvalidCharacter(c); - } - } - else - { - if (state == ParseState.Text) - { - stringBuilder.Append(c); - } - else if (state == ParseState.LeftBracket || state == ParseState.VariableName) - { - stringBuilder.Append(c); - state = ParseState.VariableName; - } - else - { - ReportInvalidCharacter(c); - } - } - break; - } - } - - if (state == ParseState.Text) - { - FinishText(); - } - else - { - throw new FormatException("Unexpected end of template string."); - } - - return nodes; - } - - public string TemplateString { get; } - private List Nodes { get; set; } - public List VariableNames { get; } - - public string Generate(Dictionary values, bool allowMissingVariable = false) - { - StringBuilder stringBuilder = new(); - foreach (var node in Nodes) - { - if (node is TextNode textNode) - { - stringBuilder.Append(textNode.Text); - } - else if (node is VariableNode variableNode) - { - var hasValue = values.TryGetValue(variableNode.VariableName, out var value); - if (!hasValue && !allowMissingVariable) - { - throw new Exception($"Variable '{variableNode.VariableName}' is not set."); - } - stringBuilder.Append(hasValue ? value : string.Empty); - } - } - return stringBuilder.ToString(); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs deleted file mode 100644 index e81a6cb..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Crupest.V2ray; - -public interface IV2rayV4ConfigObject -{ - object ToJsonObjectV4(); -} - -public class V2rayConfig(Template template, List proxies, V2rayRouting router, V2rayHosts? hosts) -{ - private class JsonInterfaceConverter : JsonConverter - { - public override Interface Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override void Write( - Utf8JsonWriter writer, - Interface value, - JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, typeof(object), options); - } - } - - - public const string ConfigTemplateFileName = "config.json.template"; - public const string VmessConfigFileName = "vmess.txt"; - public const string ProxyConfigFileName = "proxy.txt"; - public const string HostsConfigFileName = "hosts.txt"; - - public static List RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName]; - public static List ConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName, HostsConfigFileName]; - - private const string ProxyAnchor = "PROXY_ANCHOR"; - private const string RoutingAnchor = "ROUTING_ANCHOR"; - private const string HostsAnchor = "HOSTS_ANCHOR"; - - public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V@RAY_GEOSITE_USE_CN"; - - private static bool UseCnGeoSite => Environment.GetEnvironmentVariable(AddCnAttributeToGeositeEnvironmentVariable) switch - { - "0" or "false" or "off" or "disable" => false, - _ => true - }; - - public Template Template { get; set; } = template; - public List Proxies { get; set; } = proxies; - public V2rayRouting Routing { get; set; } = router; - public V2rayHosts Hosts { get; set; } = hosts is null ? new V2rayHosts([]) : hosts; - - public string ToJsonStringV4(bool pretty = true) - { - var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }); - jsonOptions.Converters.Add(new JsonInterfaceConverter()); - jsonOptions.Converters.Add(new JsonInterfaceConverter()); - - var templateValues = new Dictionary - { - [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))), - [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(), jsonOptions), - [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions), - }; - - var configString = Template.Generate(templateValues); - - if (pretty) - { - var jsonOptionsPretty = new JsonSerializerOptions(jsonOptions) - { - WriteIndented = true, - }; - return JsonSerializer.Serialize(JsonSerializer.Deserialize(configString, jsonOptionsPretty), jsonOptionsPretty); - } - else - { - return configString; - } - } - - public static V2rayConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string? hostsPath) - { - foreach (var path in new List([templatePath, vmessPath, proxyPath])) - { - if (!File.Exists(path)) - { - throw new FileNotFoundException($"Required config file not found: {path}."); - } - } - - ProxyFile proxyFile = new(proxyPath); - string templateString, vmessString; - string? hostsString; - - string file = ""; - try - { - file = templatePath; - templateString = File.ReadAllText(templatePath); - file = vmessPath; - vmessString = File.ReadAllText(vmessPath); - hostsString = hostsPath is not null ? File.ReadAllText(hostsPath) : null; - } - catch (Exception e) - { - throw new Exception($"Error reading config file {file}.", e); - } - - try - { - file = templatePath; - var template = new Template(templateString); - file = vmessPath; - var vmess = V2rayVmessProxy.CreateFromConfigString(vmessString, "proxy"); - file = proxyPath; - var routing = proxyFile.ToV2rayRouting("proxy", UseCnGeoSite); - file = hostsPath ?? ""; - var hosts = hostsString is not null ? V2rayHosts.CreateFromHostMatcherConfigString(hostsString) : null; - return new V2rayConfig(template, [vmess], routing, hosts); - } - catch (Exception e) - { - throw new Exception($"Error parsing config file {file}.", e); - } - } - - public static V2rayConfig FromDirectory(string directory) - { - return FromFiles( - Path.Join(directory, ConfigTemplateFileName), - Path.Join(directory, VmessConfigFileName), - Path.Join(directory, ProxyConfigFileName), - Path.Join(directory, HostsConfigFileName) - ); - } - - public static void FromDirectoryAndWriteToFile(string directory, string outputPath) - { - var config = FromDirectory(directory); - File.WriteAllText(outputPath, config.ToJsonStringV4()); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs deleted file mode 100644 index 4656216..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Diagnostics; - -namespace Crupest.V2ray; - -public class V2rayController(string executablePath, string configPath, string? assetPath) -{ - public const string V2rayAssetEnvironmentVariableName = "v2ray.location.asset"; - - public static string? FindExecutable(string contentDir, out bool isLocal, string? executableName = null) - { - isLocal = false; - executableName ??= "v2ray"; - - if (OperatingSystem.IsWindows()) - { - executableName += ".exe"; - } - - var localV2rayPath = Path.Combine(contentDir, executableName); - if (File.Exists(localV2rayPath)) - { - isLocal = true; - return localV2rayPath; - } - - var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator); - if (paths is not null) - { - foreach (var p in paths) - { - var v2rayPath = Path.Combine(p, executableName); - if (File.Exists(v2rayPath)) - { - return v2rayPath; - } - } - } - - return null; - } - - public string ExecutablePath { get; } = executablePath; - public string ConfigPath { get; } = configPath; - public string? AssetPath { get; } = assetPath; - public Process? CurrentProcess { get; private set; } - - private Process CreateProcess() - { - var process = new Process(); - - var startInfo = new ProcessStartInfo - { - FileName = ExecutablePath, - }; - startInfo.ArgumentList.Add("run"); - startInfo.ArgumentList.Add("-c"); - startInfo.ArgumentList.Add(ConfigPath); - if (AssetPath is not null) - { - startInfo.EnvironmentVariables[V2rayAssetEnvironmentVariableName] = AssetPath; - } - - process.StartInfo = startInfo; - process.OutputDataReceived += (_, args) => - { - Console.Out.Write(args.Data); - }; - process.ErrorDataReceived += (_, args) => - { - Console.Error.WriteLine(args.Data); - }; - - return process; - } - - public void Stop() - { - if (CurrentProcess is not null) - { - CurrentProcess.Kill(); - CurrentProcess.Dispose(); - CurrentProcess = null; - Console.WriteLine("V2ray stopped."); - } - } - - public void Start(bool stopOld = false) - { - if (stopOld) Stop(); - - if (CurrentProcess is null) - { - CurrentProcess = CreateProcess(); - CurrentProcess.EnableRaisingEvents = true; - CurrentProcess.Exited += (_, _) => - { - if (CurrentProcess.ExitCode != 0) - { - const string message = "V2ray exited with error."; - Console.Error.WriteLine(message); - throw new Exception(message); - } - }; - CurrentProcess.Start(); - Console.WriteLine("V2ray started."); - } - } - - public void Restart() - { - Start(true); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs deleted file mode 100644 index 36ae44b..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Crupest.V2ray; - -public enum V2rayHostMatcherKind -{ - DomainFull, - DomainSuffix, - DomainKeyword, - DomainRegex, - Ip, - GeoSite, - GeoIp, -} - -public record V2rayHostMatcherItem(V2rayHostMatcherKind Kind, string Matcher, List Values); - -public class V2rayHostMatcherConfig(string configString, List allowedMatchers, int minComponentCount = -1, int maxComponentCount = -1) -{ - static bool IsDomainMatcher(V2rayHostMatcherKind kind) => kind switch - { - V2rayHostMatcherKind.DomainFull => true, - V2rayHostMatcherKind.DomainSuffix => true, - V2rayHostMatcherKind.DomainKeyword => true, - V2rayHostMatcherKind.DomainRegex => true, - _ => false, - }; - - private static List Parse(string configString, List allowedMatchers, int minComponentCount = -1, int maxComponentCount = -1) - { - var items = new ListConfig(configString).Config; - var result = new List(); - - foreach (var item in items) - { - var lineNumber = item.LineNumber; - var line = item.Value; - var hasExplicitMatcher = false; - var segments = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); - - foreach (var matcher in Enum.GetValues()) - { - var matcherName = Enum.GetName(matcher) ?? throw new Exception("No such matcher."); - hasExplicitMatcher = true; - if (segments[0] == matcherName) - { - if (segments.Count < 2) - { - throw new FormatException($"Explicit matcher needs a value in line {lineNumber}."); - } - if (allowedMatchers.Contains(matcher)) - { - if (IsDomainMatcher(matcher) && matcher == V2rayHostMatcherKind.DomainRegex && Uri.CheckHostName(matcherName) != UriHostNameType.Dns) - { - throw new FormatException($"Invalid domain format in line {lineNumber}."); - } - - var components = segments[2..].ToList(); - if (minComponentCount > 0 && components.Count < minComponentCount) - { - throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required."); - } - if (maxComponentCount >= 0 && components.Count > maxComponentCount) - { - throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed."); - } - result.Add(new V2rayHostMatcherItem(matcher, segments[1], components)); - } - else - { - throw new FormatException($"Matcher {matcherName} is not allowed at line {lineNumber}."); - } - } - } - - if (!hasExplicitMatcher) - { - if (minComponentCount > 0 && segments.Count - 1 < minComponentCount) - { - throw new FormatException($"Too few components in line {lineNumber}, at least {minComponentCount} required."); - } - if (maxComponentCount >= 0 && segments.Count - 1 > maxComponentCount) - { - throw new FormatException($"Too many components in line {lineNumber}, only {maxComponentCount} allowed."); - } - result.Add(new V2rayHostMatcherItem(V2rayHostMatcherKind.DomainSuffix, segments[0], segments.Count == 1 ? [] : segments[1..])); - } - } - return result; - } - - public string ConfigString { get; } = configString; - public List AllowedMatchers { get; } = allowedMatchers; - public int MinComponentCount { get; } = minComponentCount; - public int MaxComponentCount { get; } = maxComponentCount; - public List Items { get; } = Parse(configString, allowedMatchers, minComponentCount, maxComponentCount); -} - -public class HostMatcherConfigFile -{ - public HostMatcherConfigFile(string path, List allowedMatchers, int minComponentCount = -1, int maxComponentCount = -1) - { - Path = path; - FileContent = File.ReadAllText(path); - MatcherConfig = new V2rayHostMatcherConfig(FileContent, allowedMatchers, minComponentCount, maxComponentCount); ; - } - - public string Path { get; } - public string FileContent { get; } - public V2rayHostMatcherConfig MatcherConfig { get; } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs deleted file mode 100644 index e9bf8cf..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace Crupest.V2ray; - -public record V2rayHostRule(V2rayHostMatcherKind MatcherKind, string MatcherString, List ResolveResult) -{ - public string AddressString() - { - return MatcherKind switch - { - V2rayHostMatcherKind.DomainFull => MatcherString, - V2rayHostMatcherKind.DomainSuffix => $"domain:{MatcherString}", - V2rayHostMatcherKind.DomainKeyword => $"keyword:{MatcherString}", - V2rayHostMatcherKind.DomainRegex => $"regexp:{MatcherString}", - _ => throw new ArgumentOutOfRangeException($"Matcher {MatcherKind} is not allowed in host rule."), - }; - } - - public object ResolveResultToJsonObject() - { - return ResolveResult.Count == 1 ? ResolveResult[0] : ResolveResult; - } -} - -public class V2rayHosts(List rules) : IV2rayV4ConfigObject -{ - public List Rules { get; } = rules; - - public Dictionary ToJsonObjectV4() => - Rules.ToDictionary(rule => rule.AddressString(), rule => rule.ResolveResultToJsonObject()); - - object IV2rayV4ConfigObject.ToJsonObjectV4() - { - return ToJsonObjectV4(); - } - - public static V2rayHosts CreateFromHostMatcherConfigString(string configString) - { - var matcherConfig = new V2rayHostMatcherConfig(configString, - [V2rayHostMatcherKind.DomainFull, V2rayHostMatcherKind.DomainKeyword, V2rayHostMatcherKind.DomainRegex, V2rayHostMatcherKind.DomainSuffix], minComponentCount: 1); - - return new V2rayHosts(matcherConfig.Items.Select(i => new V2rayHostRule(i.Kind, i.Matcher, [.. i.Values])).ToList()); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs deleted file mode 100644 index bcb2b51..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Crupest.V2ray; - -public abstract class V2rayProxy(string tag) : IV2rayV4ConfigObject -{ - public string Tag { get; set; } = tag; - - public abstract V2rayV4ConfigJsonObjects.Outbound ToJsonObjectV4(); - - object IV2rayV4ConfigObject.ToJsonObjectV4() - { - return ToJsonObjectV4(); - } -} - -public class V2rayHttpProxy(string host, int port, string tag) : V2rayProxy(tag) -{ - public string Host { get; set; } = host; - public int Port { get; set; } = port; - - public override V2rayV4ConfigJsonObjects.Outbound ToJsonObjectV4() - { - return new V2rayV4ConfigJsonObjects.Outbound(Tag, "http", - new V2rayV4ConfigJsonObjects.HttpOutboundSettings([new V2rayV4ConfigJsonObjects.HttpOutboundServer(Host, Port, [])]), - null - ); - } -} - - -public class V2rayVmessProxy(string host, int port, string userId, string path, string tag) : V2rayProxy(tag) -{ - public string Host { get; set; } = host; - public int Port { get; set; } = port; - public string Path { get; set; } = path; - public string UserId { get; set; } = userId; - - public override V2rayV4ConfigJsonObjects.Outbound ToJsonObjectV4() - { - return new V2rayV4ConfigJsonObjects.Outbound(Tag, "vmess", - new V2rayV4ConfigJsonObjects.VmessOutboundSettings( - [new V2rayV4ConfigJsonObjects.VnextServer(Host, Port, [new V2rayV4ConfigJsonObjects.VnextServerUser(UserId, 0, "auto", 0)])]), - new V2rayV4ConfigJsonObjects.WsStreamSettings("ws", "tls", new V2rayV4ConfigJsonObjects.WsSettings(Path, new() { ["Host"] = Host })) - ); - } - - public static V2rayVmessProxy CreateFromConfigString(string configString, string tag) - { - var config = new DictionaryConfig(configString, ["host", "port", "userid", "path"]); - var portString = config.GetItemCaseInsensitive("port").Value; - if (!int.TryParse(portString, out var port) || port <= 0) - { - throw new FormatException($"Invalid port number: {portString}: not an integer or is a invalid number."); - } - return new V2rayVmessProxy(config.GetItemCaseInsensitive("host").Value, port, - config.GetItemCaseInsensitive("userid").Value, config.GetItemCaseInsensitive("path").Value, tag - ); - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs deleted file mode 100644 index f385233..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace Crupest.V2ray; - -public record V2rayRoutingRule(V2rayHostMatcherKind MatcherKind, string MatcherString, string OutboundTag) : IV2rayV4ConfigObject -{ - public string ComposedMatcherString => MatcherKind switch - { - V2rayHostMatcherKind.DomainFull => $"full:{MatcherString}", - V2rayHostMatcherKind.DomainSuffix => $"domain:{MatcherString}", - V2rayHostMatcherKind.DomainKeyword => MatcherString, - V2rayHostMatcherKind.DomainRegex => $"regexp:{MatcherString}", - V2rayHostMatcherKind.Ip => MatcherString, - V2rayHostMatcherKind.GeoSite => $"geosite:{MatcherString}", - V2rayHostMatcherKind.GeoIp => $"geoip:{MatcherString}", - _ => throw new ArgumentException("Invalid matcher kind.") - }; - - public static Dictionary> GroupByOutboundTag(List rules) - => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary(); - - public static Dictionary> GroupByMatcherKind(List rules) - => rules.GroupBy(r => r.MatcherKind).Select(g => (g.Key, g.ToList())).ToDictionary(); - - public static List> GroupByOutboundTagAndMatcherKind(List rules) - => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatcherKind(groupByTag).Values).ToList(); - - public static V2rayV4ConfigJsonObjects.RoutingRule ListToJsonObject(List rules) - { - if (rules.Count == 0) - { - throw new ArgumentException("Rule list is empty."); - } - - var matcherKind = rules[0].MatcherKind; - var outboundTag = rules[0].OutboundTag; - - if (rules.Any(r => r.OutboundTag != outboundTag) || rules.Any(r => r.MatcherKind != matcherKind)) - { - throw new ArgumentException("Rules must have the same matcher kind and outbound tag."); - } - - List composedMatcherStringList = rules.Select(r => r.ComposedMatcherString).ToList(); - - return new V2rayV4ConfigJsonObjects.RoutingRule(OutboundTag: outboundTag, - Ip: (matcherKind is V2rayHostMatcherKind.Ip or V2rayHostMatcherKind.GeoIp) ? composedMatcherStringList : null, - Domains: (matcherKind is V2rayHostMatcherKind.DomainFull or V2rayHostMatcherKind.DomainSuffix or V2rayHostMatcherKind.DomainKeyword or V2rayHostMatcherKind.DomainRegex or V2rayHostMatcherKind.GeoSite) ? composedMatcherStringList : null - ); - } - - public V2rayRoutingRule CloneGeositeWithCnAttribute(string outboundTag) - { - if (MatcherKind is not V2rayHostMatcherKind.GeoSite) - { - throw new ArgumentException("Matcher kind must be GeoSite."); - } - - return new V2rayRoutingRule(V2rayHostMatcherKind.GeoSite, $"{MatcherString}@cn", outboundTag); - } - - public V2rayV4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]); - - object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); -} - -public record V2rayRouting(List Rules, bool DirectGeositeCn = true, string DomainStrategy = "IpOnDemand") : IV2rayV4ConfigObject -{ - public List CreateGeositeCnDirectRules() - { - return Rules.Where(r => r.MatcherKind is V2rayHostMatcherKind.GeoSite) - .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList(); - } - - public V2rayV4ConfigJsonObjects.Routing ToJsonObjectV4(bool directGeositeCn = true) - { - List ruleJsonObjects = []; - - if (directGeositeCn) - { - ruleJsonObjects.Add(V2rayRoutingRule.ListToJsonObject(CreateGeositeCnDirectRules())); - } - - ruleJsonObjects.AddRange(V2rayRoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(V2rayRoutingRule.ListToJsonObject)); - - return new V2rayV4ConfigJsonObjects.Routing(ruleJsonObjects); - } - - object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs deleted file mode 100644 index 672af71..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Crupest.V2ray; - -public static class V2rayV4ConfigJsonObjects -{ - public interface IObject; - public interface IOutboundSettings : IObject; - public interface IOutboundStreamSettings : IObject; - - public record WsSettings(string Path, Dictionary Headers) : IObject; - public record WsStreamSettings(string Network, string Security, WsSettings WsSettings) : IOutboundStreamSettings; - public record VnextServerUser(string Id, int AlterId, string Security, int Level) : IObject; - public record VnextServer(string Address, int Port, List Users) : IObject; - public record VmessOutboundSettings(List Vnext) : IOutboundSettings; - public record HttpOutboundUser(string User, string Pass) : IObject; - public record HttpOutboundServer(string Address, int Port, List Users) : IObject; - public record HttpOutboundSettings(List Servers) : IOutboundSettings; - public record Outbound(string Tag, string Protocol, IOutboundSettings Settings, - IOutboundStreamSettings? StreamSettings) : IObject; - - public record RoutingRule(string DomainMatcher = "mph", string Type = "field", List? Domains = null, List? Ip = null, - string? Port = null, string? SourcePort = null, string? Network = null, List? Source = null, - List? User = null, List? InboundTag = null, List? Protocol = null, string? Attrs = null, - string? OutboundTag = null, string? BalancerTag = null) : IObject; - public record Routing(List Rules, string DomainStrategy = "IpOnDemand", string DomainMatcher = "mph") : IObject; -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs deleted file mode 100644 index 56d64ca..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Crupest.V2ray; - -public static class V2rayV5ConfigJsonObjects -{ - public record OutboundObject(string Protocol, object Settings, string Tag, object? StreamSettings) - { - public static OutboundObject VmessViaWs(string tag, string address, int port, string uuid, string path) - { - return new OutboundObject("vmess", new VmessSettings(address, port, uuid), tag, StreamSettingsObject.Ws(path)); - } - - public static OutboundObject Http(string tag, string address, int port) - { - return new OutboundObject("http", new HttpSettingsObject(address, port), tag, null); - } - } - - public record WsSettingsObject(string Path, Dictionary Headers); - - public record StreamSettingsObject(string Transport, object TransportSettings, string Security, object SecuritySettings) - { - public static StreamSettingsObject Ws(string path) - { - return new StreamSettingsObject("ws", new WsSettingsObject(path, new()), "tls", new()); - } - } - - public record VmessSettings(string Address, int Port, string Uuid); - - public record HttpSettingsObject(string Address, int Port); -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs deleted file mode 100644 index cdead3c..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System.Net; - -namespace Crupest.V2ray; - -public interface IV2rayStaticHostResolveResult -{ - IDictionary GetJsonProperties(); -} - -public class V2rayStaticHostDomainResolveResult : IV2rayStaticHostResolveResult -{ - public V2rayStaticHostDomainResolveResult(string domain) - { - Domain = domain; - } - - public string Domain { get; } - - public IDictionary GetJsonProperties() - { - return new Dictionary - { - - ["proxiedDomain"] = Domain - }; - } -} - -public class V2rayStaticHostIpResolveResult : IV2rayStaticHostResolveResult -{ - public V2rayStaticHostIpResolveResult(IEnumerable ips) - { - Ips = ips.ToList(); - } - - public IReadOnlyList Ips { get; } - - public IDictionary GetJsonProperties() - { - return new Dictionary - { - ["ip"] = Ips - }; - } -} - - -public class V2rayV5StaticHostRule(V2rayV5StaticHostRule.MatcherKind matcher, string domain, IV2rayStaticHostResolveResult resolveResult) -{ - public enum MatcherKind - { - Full, - Subdomain, - Keyword, - Regex - } - - public MatcherKind Matcher { get; } = matcher; - public string Domain { get; } = domain; - public IV2rayStaticHostResolveResult ResolveResult { get; } = resolveResult; - - public Dictionary ToJsonObject() - { - var result = new Dictionary - { - ["type"] = Enum.GetName(Matcher)!, - ["domain"] = Domain - }; - - foreach (var (key, value) in ResolveResult.GetJsonProperties()) - { - result.Add(key, value); - } - - return result; - } - - public static V2rayV5StaticHostRule IpRule(MatcherKind matcher, string domain, IEnumerable ips) - { - return new V2rayV5StaticHostRule(matcher, domain, new V2rayStaticHostIpResolveResult(ips)); - } - - public static V2rayV5StaticHostRule DomainRule(MatcherKind matcher, string domain, string resolvedDomain) - { - return new V2rayV5StaticHostRule(matcher, domain, new V2rayStaticHostDomainResolveResult(resolvedDomain)); - } - - public static V2rayV5StaticHostRule Parse(string str) - { - var components = str.Trim().Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); - - if (components.Count <= 1) - { - throw new FormatException("The str only has one or no component."); - } - - var matcher = MatcherKind.Subdomain; - - if (Enum.TryParse(components[0], out var m)) - { - matcher = m; - components.RemoveAt(0); - } - - if (components.Count <= 1) - { - throw new FormatException("The str only has one component after remove matcher."); - } - - var domain = components[0]; - components.RemoveAt(0); - - if (components.Count > 1 || IPAddress.TryParse(components[0], out var _)) - { - return new V2rayV5StaticHostRule(matcher, domain, new V2rayStaticHostIpResolveResult(components)); - } - else - { - return new V2rayV5StaticHostRule(matcher, domain, new V2rayStaticHostDomainResolveResult(domain)); - } - } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template deleted file mode 100644 index 686006c..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template +++ /dev/null @@ -1,63 +0,0 @@ -{ - "log": { - "loglevel": "warning" - }, - "inbounds": [ - { - "port": 3081, - "listen": "127.0.0.1", - "tag": "socks-inbound", - "protocol": "socks", - "settings": { - "auth": "noauth" - } - }, - { - "port": 3080, - "listen": "127.0.0.1", - "tag": "http-inbound", - "protocol": "http", - "settings": { - "auth": "noauth" - } - } - ], - "outbounds": [ - { - "protocol": "freedom", - "settings": {}, - "tag": "direct" - }, - { - "protocol": "blackhole", - "settings": {}, - "tag": "blocked" - }, - ${PROXY_ANCHOR} - ], - "routing": ${ROUTING_ANCHOR}, - "dns": { - "hosts": ${HOSTS_ANCHOR}, - "servers": [ - "https://doh.pub/dns-query", - "1.1.1.1", - "8.8.8.8", - "localhost" - ] - }, - "policy": { - "levels": { - "0": { - "uplinkOnly": 0, - "downlinkOnly": 0 - } - }, - "system": { - "statsInboundUplink": false, - "statsInboundDownlink": false, - "statsOutboundUplink": false, - "statsOutboundDownlink": false - } - }, - "other": {} -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template deleted file mode 100644 index 01ccf7a..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template +++ /dev/null @@ -1,55 +0,0 @@ -{ - "log": { - "access": { - "type": "Console", - "level": "Info" - } - }, - "dns": { - "nameServer": [{ - "address": "https://doh.pub/dns-query" - }, { - "address": "1.1.1.1" - }, { - "address": "8.8.8.8" - }, { - "address": "localhost" - }], - "staticHosts": ${HOSTS_ANCHOR} - }, - "inbounds": [{ - { - "protocol": "socks", - "port": 2081, - "listen": "127.0.0.1", - "tag": "socks-inbound", - "settings": { - "auth": "noauth" - } - }, - { - "protocol": "http", - "port": 2080, - "listen": "127.0.0.1", - "tag": "http-inbound", - "settings": { - "auth": "noauth" - } - } - }], - "outbounds": [ - { - "protocol": "freedom", - "settings": {}, - "tag": "direct" - }, - { - "protocol": "blackhole", - "settings": {}, - "tag": "blocked" - }, - ${PROXY_ANCHOR} - ], - "router": ${ROUTER_ANCHOR} -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/hosts.txt b/tools/Crupest.V2ray/Crupest.V2ray/hosts.txt deleted file mode 100644 index 88d5015..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/hosts.txt +++ /dev/null @@ -1,2 +0,0 @@ -cdn.jsdelivr.net cdn.jsdelivr.net.cdn.cloudflare.net - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt b/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt deleted file mode 100644 index 6273e35..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt +++ /dev/null @@ -1,45 +0,0 @@ -GeoSite github -GeoSite google -GeoSite youtube -GeoSite twitter -GeoSite facebook -GeoSite discord -GeoSite reddit -GeoSite twitch -GeoSite onedrive -GeoSite quora -GeoSite telegram -GeoSite imgur -GeoSite stackexchange - -GeoSite duckduckgo -GeoSite wikimedia -GeoSite gitbook -GeoSite gitlab -GeoSite sourceforge -GeoSite creativecommons -GeoSite archive -GeoSite matrix -GeoSite tor - -GeoSite python -GeoSite ruby -GeoSite rust -GeoSite nodejs -GeoSite npmjs -GeoSite qt -GeoSite docker -GeoSite v2ray -GeoSite homebrew - -GeoSite azure -GeoSite jsdelivr -GeoSite fastly -GeoSite heroku -GeoSite bootstrap -GeoSite vercel - -GeoSite ieee -GeoSite sci-hub -GeoSite libgen -GeoSite z-library diff --git a/tools/Crupest.V2ray/CrupestV2ray.sln b/tools/Crupest.V2ray/CrupestV2ray.sln deleted file mode 100644 index 3045b4e..0000000 --- a/tools/Crupest.V2ray/CrupestV2ray.sln +++ /dev/null @@ -1,33 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.7.34024.191 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4C2CE80-CDF8-4B08-8912-D1F0F14196AD}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Crupest.V2ray", "Crupest.V2ray\Crupest.V2ray.csproj", "{154D49F2-242E-4384-8D34-73774231AA75}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {154D49F2-242E-4384-8D34-73774231AA75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {154D49F2-242E-4384-8D34-73774231AA75}.Debug|Any CPU.Build.0 = Debug|Any CPU - {154D49F2-242E-4384-8D34-73774231AA75}.Release|Any CPU.ActiveCfg = Release|Any CPU - {154D49F2-242E-4384-8D34-73774231AA75}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {154D49F2-242E-4384-8D34-73774231AA75} = {F4C2CE80-CDF8-4B08-8912-D1F0F14196AD} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B1E8FD9C-9157-4F4E-8265-4B37F30EEC5E} - EndGlobalSection -EndGlobal diff --git a/tools/Crupest.V2ray/build-secret.bash b/tools/Crupest.V2ray/build-secret.bash deleted file mode 100755 index bc5c7ee..0000000 --- a/tools/Crupest.V2ray/build-secret.bash +++ /dev/null @@ -1,41 +0,0 @@ -#! /usr/bin/env bash - -set -e - -function print_argument_error_message_and_exit() { - argument_error_message="You must specify exactly one argument, the build target (win-x64 | linux-x64 | osx-x64)." - echo "$argument_error_message" - exit 1 -} - - - -if [[ $# != 1 ]]; then - print_argument_error_message_and_exit -fi - -case "$1" in - win-x64 | linux-x64 | osx-x64) - echo "Build target: $1" - ;; - *) - print_argument_error_message_and_exit - ;; -esac - -secret_dir=$(realpath "$(dirname "$0")") - -echo "Secret dir: ${secret_dir}" - -echo "Check dotnet..." -dotnet --version - -echo "Enter \"secret\" dir..." -pushd "$secret_dir" - -echo "Begin to build..." -dotnet publish Crupest.V2ray -c Release -o "$secret_dir/publish" --sc -r "$1" - -popd - -echo "Finish!" diff --git a/tools/Crupest.V2ray/tools/cru-proxy-edit b/tools/Crupest.V2ray/tools/cru-proxy-edit deleted file mode 100755 index 9ba6cbc..0000000 --- a/tools/Crupest.V2ray/tools/cru-proxy-edit +++ /dev/null @@ -1,12 +0,0 @@ -#! /usr/bin/env bash - -set -e - -p="$HOME/codes/crupest/tools/Crupest.V2ray/publish/proxy.txt" - -if [[ ! -f "$p" ]]; then - echo "File $p does not exist!" >&2 - exit 1 -fi - -exec vim "$p" diff --git a/tools/Crupest.V2ray/tools/cru-proxy-log b/tools/Crupest.V2ray/tools/cru-proxy-log deleted file mode 100755 index 0ac800c..0000000 --- a/tools/Crupest.V2ray/tools/cru-proxy-log +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env bash - -set -e - -if [[ -e /proc ]]; then - # I don't believe your system is Linux but there is no /proc. - exec journalctl --user -u crupest-v2ray "$@" -elif [[ "$(uname)" == "Darwin" ]]; then - exec less "$HOME/.local/state/Crupest.V2ray/log" -else - echo "Not supported on systems other than macOS and Linux now." >&2 - exit 1 -fi diff --git a/tools/Crupest.V2ray/tools/crupest-v2ray.service b/tools/Crupest.V2ray/tools/crupest-v2ray.service deleted file mode 100644 index afe840f..0000000 --- a/tools/Crupest.V2ray/tools/crupest-v2ray.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -Description=crupest v2ray service - -[Service] -ExecStart=%h/.local/bin/Crupest.V2ray - -[Install] -WantedBy=default.target diff --git a/tools/Crupest.V2ray/tools/life.crupest.v2ray.plist b/tools/Crupest.V2ray/tools/life.crupest.v2ray.plist deleted file mode 100644 index 4569ae2..0000000 --- a/tools/Crupest.V2ray/tools/life.crupest.v2ray.plist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Label - life.crupest.v2ray - ProgramArguments - - /Users/crupest/.local/bin/Crupest.V2ray - - KeepAlive - - StandardOutPath - /Users/crupest/.local/state/Crupest.V2ray/log - StandardErrorPath - /Users/crupest/.local/state/Crupest.V2ray/error - - -- cgit v1.2.3