diff options
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/Config.cs | 14 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs | 138 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/Program.cs | 12 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs | 14 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/SurgeConfigGenerator.cs | 60 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs | 10 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs | 16 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs | 2 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs | 6 | ||||
-rw-r--r-- | tools/Crupest.V2ray/Crupest.V2ray/proxy.txt | 1 |
10 files changed, 234 insertions, 39 deletions
diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Config.cs b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs index fc71007..2ef18f0 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/Config.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs @@ -69,13 +69,17 @@ public class ListConfig(string configString) foreach (var line in lines) { - var trimmedLine = line.Trim(); - if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith('#')) + var l = line; + var beginOfComment = l.IndexOf('#'); + if (beginOfComment >= 0) { - lineNumber++; - continue; + l = line[..beginOfComment]; + } + l = l.Trim(); + if (!string.IsNullOrEmpty(l)) + { + config.Add(new ConfigItem(l, lineNumber)); } - config.Add(new ConfigItem(trimmedLine, lineNumber)); lineNumber++; } diff --git a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs index 75f071b..3dce3f6 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/GeoDataManager.cs @@ -33,7 +33,7 @@ public record GeoSite(string Name, List<IGeoSiteEntry> Entries) var include = value["include:".Length..].Trim(); if (include.Length == 0 || include.Contains(' ')) { - throw new FormatException($"Invalid geo site rule in line {line}. Invalid include value."); + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid include value."); } entries.Add(new GeoSiteIncludeEntry(include, name)); continue; @@ -42,7 +42,7 @@ public record GeoSite(string Name, List<IGeoSiteEntry> Entries) var segments = value.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (segments.Length > 2) { - throw new FormatException($"Invalid geo site rule in line {line}. More than one ':'."); + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. More than one ':'."); } V2rayHostMatcherKind kind; @@ -54,7 +54,7 @@ public record GeoSite(string Name, List<IGeoSiteEntry> Entries) "full" => kind = V2rayHostMatcherKind.DomainFull, "keyword" => kind = V2rayHostMatcherKind.DomainKeyword, "regexp" => kind = V2rayHostMatcherKind.DomainRegex, - _ => throw new FormatException($"Invalid geo site rule in line {line}. Unknown matcher.") + _ => throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Unknown matcher.") }; } else @@ -64,17 +64,17 @@ public record GeoSite(string Name, List<IGeoSiteEntry> Entries) var domainSegments = segments[^1].Split('@', StringSplitOptions.TrimEntries); var domain = domainSegments[0]; - if (Uri.CheckHostName(domain) != UriHostNameType.Dns) + if (kind != V2rayHostMatcherKind.DomainRegex && Uri.CheckHostName(domain) != UriHostNameType.Dns) { - throw new FormatException($"Invalid geo site rule in line {line}. Invalid domain."); + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Invalid domain."); } List<string> attributes = []; - foreach (var s in domainSegments) + foreach (var s in domainSegments[1..]) { if (s.Length == 0) { - throw new FormatException($"Invalid geo site rule in line {line}. Empty attribute value."); + throw new FormatException($"Invalid geo site rule '{name}' in line {line}. Empty attribute value."); } attributes.Add(s); } @@ -85,16 +85,15 @@ public record GeoSite(string Name, List<IGeoSiteEntry> Entries) } } -public class GeoSiteDataParser(string directory) +public class GeoSiteData(string directory) { private static List<GeoSite> Parse(string directory) { var sites = new List<GeoSite>(); - foreach (var file in Directory.GetFileSystemEntries(directory)) + foreach (var path in Directory.GetFileSystemEntries(directory)) { - var path = Path.Combine(directory, file); var content = File.ReadAllText(path); - sites.Add(GeoSite.Parse(file, content)); + sites.Add(GeoSite.Parse(Path.GetFileName(path), content)); } return sites; } @@ -102,6 +101,62 @@ public class GeoSiteDataParser(string directory) public string DataDirectory { get; } = directory; public List<GeoSite> Sites { get; } = Parse(directory); + + public GeoSite? GetSite(string name) + { + return Sites.Where(s => s.Name == name).FirstOrDefault(); + } + + public List<GeoSiteRuleEntry> GetEntriesRecursive(List<string> sites, + List<V2rayHostMatcherKind>? onlyMatcherKinds = null, List<string>? onlyAttributes = null) + { + List<GeoSiteRuleEntry> entries = []; + HashSet<string> visited = []; + HashSet<V2rayHostMatcherKind>? 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 @@ -132,6 +187,15 @@ public class GeoDataManager public List<GeoDataAsset> 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}"; @@ -181,11 +245,14 @@ public class GeoDataManager return $"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"; } - private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath) + private static void GithubDownloadRepository(HttpClient httpClient, string user, string repo, string outputPath, bool silent) { - using var responseStream = httpClient.GetStreamAsync(GetGithubRepositoryArchiveUrl(user, repo)).Result; + 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) @@ -194,16 +261,47 @@ public class GeoDataManager zip.ExtractToDirectory(outputPath); } - private string DownloadAndExtractGeoSiteRepository(bool silent) + 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); - var archivePath = Path.Combine(tempDirectory.FullName, zipFileName); - var extractPath = Path.Combine(tempDirectory.FullName, "repo"); - GithubDownloadRepository(httpClient, V2rayGithubOrganization, V2rayGeoSiteGithubRepository, archivePath); - Directory.CreateDirectory(extractPath); - Unzip(archivePath, extractPath); - return Path.Join(extractPath, "domain-list-community-master"); + 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 index a40aa21..0e98861 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs @@ -12,6 +12,8 @@ public static class Program 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() { @@ -68,6 +70,16 @@ public static class Program 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); diff --git a/tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs b/tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs new file mode 100644 index 0000000..ca5ca56 --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/ProxyFile.cs @@ -0,0 +1,14 @@ +namespace Crupest.V2ray; + +public class ProxyFile(string path) : + HostMatcherConfigFile(path, [.. Enum.GetValues<V2rayHostMatcherKind>()], 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 new file mode 100644 index 0000000..bd52234 --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/SurgeConfigGenerator.cs @@ -0,0 +1,60 @@ +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<V2rayHostMatcherKind> 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/V2rayConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs index 3758f47..e81a6cb 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs @@ -99,7 +99,8 @@ public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouti } } - string templateString, vmessString, routingString; + ProxyFile proxyFile = new(proxyPath); + string templateString, vmessString; string? hostsString; string file = ""; @@ -109,9 +110,6 @@ public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouti templateString = File.ReadAllText(templatePath); file = vmessPath; vmessString = File.ReadAllText(vmessPath); - file = proxyPath; - routingString = File.ReadAllText(proxyPath); - file = proxyPath; hostsString = hostsPath is not null ? File.ReadAllText(hostsPath) : null; } catch (Exception e) @@ -126,9 +124,9 @@ public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouti file = vmessPath; var vmess = V2rayVmessProxy.CreateFromConfigString(vmessString, "proxy"); file = proxyPath; - var routing = V2rayRouting.CreateFromConfigString(routingString, "proxy", UseCnGeoSite); + var routing = proxyFile.ToV2rayRouting("proxy", UseCnGeoSite); file = hostsPath ?? ""; - var hosts = hostsString is not null ? V2rayHosts.CreateFromConfigString(hostsString) : null; + var hosts = hostsString is not null ? V2rayHosts.CreateFromHostMatcherConfigString(hostsString) : null; return new V2rayConfig(template, [vmess], routing, hosts); } catch (Exception e) diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs index f444b5d..36ae44b 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs @@ -48,7 +48,7 @@ public class V2rayHostMatcherConfig(string configString, List<V2rayHostMatcherKi } if (allowedMatchers.Contains(matcher)) { - if (IsDomainMatcher(matcher) && Uri.CheckHostName(matcherName) != UriHostNameType.Dns) + if (IsDomainMatcher(matcher) && matcher == V2rayHostMatcherKind.DomainRegex && Uri.CheckHostName(matcherName) != UriHostNameType.Dns) { throw new FormatException($"Invalid domain format in line {lineNumber}."); } @@ -93,3 +93,17 @@ public class V2rayHostMatcherConfig(string configString, List<V2rayHostMatcherKi public int MaxComponentCount { get; } = maxComponentCount; public List<V2rayHostMatcherItem> Items { get; } = Parse(configString, allowedMatchers, minComponentCount, maxComponentCount); } + +public class HostMatcherConfigFile +{ + public HostMatcherConfigFile(string path, List<V2rayHostMatcherKind> 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 index 1d1c6d8..e9bf8cf 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs @@ -32,7 +32,7 @@ public class V2rayHosts(List<V2rayHostRule> rules) : IV2rayV4ConfigObject return ToJsonObjectV4(); } - public static V2rayHosts CreateFromConfigString(string configString) + public static V2rayHosts CreateFromHostMatcherConfigString(string configString) { var matcherConfig = new V2rayHostMatcherConfig(configString, [V2rayHostMatcherKind.DomainFull, V2rayHostMatcherKind.DomainKeyword, V2rayHostMatcherKind.DomainRegex, V2rayHostMatcherKind.DomainSuffix], minComponentCount: 1); diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs index 080a7b4..f385233 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs @@ -84,10 +84,4 @@ public record V2rayRouting(List<V2rayRoutingRule> Rules, bool DirectGeositeCn = } object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); - - public static V2rayRouting CreateFromConfigString(string configString, string outboundTag, bool directGeositeCn = true) - { - var matcherConfig = new V2rayHostMatcherConfig(configString, [.. Enum.GetValues<V2rayHostMatcherKind>()], maxComponentCount: 0); - return new V2rayRouting(matcherConfig.Items.Select(i => new V2rayRoutingRule(i.Kind, i.Matcher, outboundTag)).ToList(), directGeositeCn); - } } diff --git a/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt b/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt index 33d3913..6273e35 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt +++ b/tools/Crupest.V2ray/Crupest.V2ray/proxy.txt @@ -32,6 +32,7 @@ GeoSite docker GeoSite v2ray GeoSite homebrew +GeoSite azure GeoSite jsdelivr GeoSite fastly GeoSite heroku |