From 13c2cb7e1693e49e9a76aeb5ce65ff1591a48fc9 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 23 Oct 2024 23:32:32 +0800 Subject: fix(secret): add sing config generation. --- .../Crupest.SecretTool/Crupest.SecretTool.csproj | 3 + .../Crupest.SecretTool/GeoDataManager.cs | 2 + .../Crupest.SecretTool/HostMatchConfig.cs | 9 +- .../Crupest.SecretTool/Program.cs | 12 ++- .../Crupest.SecretTool/Crupest.SecretTool/Proxy.cs | 20 +++- .../Crupest.SecretTool/ProxyFile.cs | 31 +++++++ .../Crupest.SecretTool/Routing.cs | 72 +++++++++++++-- .../Crupest.SecretTool/SingConfigJsonObjects.cs | 20 ++++ .../Crupest.SecretTool/SurgeConfigGenerator.cs | 42 ++++----- .../Crupest.SecretTool/ToolConfig.cs | 102 ++++++++++++++++++++- .../Crupest.SecretTool/proxy.txt | 11 ++- .../Crupest.SecretTool/sing-config.json.template | 55 +++++++++++ 12 files changed, 334 insertions(+), 45 deletions(-) create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs create mode 100644 tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template (limited to 'tools') diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj b/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj index 0812e4c..1e011b1 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Crupest.SecretTool.csproj @@ -20,6 +20,9 @@ PreserveNewest + + PreserveNewest + diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs index 0b61cac..8f4c171 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/GeoDataManager.cs @@ -16,6 +16,8 @@ public record GeoSiteIncludeEntry(string Value, string ContainingSite) : IGeoSit public record GeoSiteRuleEntry(HostMatchKind Kind, string Value, List Attributes, string ContainingSite) : IGeoSiteEntry { public bool IsInclude => false; + + public RoutingRuleMatcher GetRoutingRuleMatcher() => new(Kind, Value); } public record GeoSite(string Name, List Entries) diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs index 5d2c504..858333d 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/HostMatchConfig.cs @@ -27,6 +27,10 @@ public static class HostMatchKindExtensions 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 static List SupportedInSingRouteMatchKinds { get; } = [..DomainMatchKinds, HostMatchKind.Ip]; + + public static bool IsSupportedInSingRoute(this HostMatchKind kind) => SupportedInSingRouteMatchKinds.Contains(kind); } public record HostMatchConfigItem(HostMatchKind Kind, string MatchString, List Values); @@ -117,8 +121,3 @@ public class HostMatchConfigFile 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 index 9898af1..310143d 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Program.cs @@ -74,16 +74,22 @@ public static class Program GeoDataManager.Instance.Download(CrupestSecretToolDirectory, false); return; } - else if (verb == "generate-surge-rule-set" || verb == "gs") + else if (verb == "generate-surge-rule-set" || verb == "gsr") { SurgeConfigGenerator.GenerateTo( - Path.Join(CrupestSecretToolDirectory, "proxy.txt"), + CrupestSecretToolDirectory, Path.Join(CrupestSecretToolDirectory, SurgeRuleSetChinaOutputFileName), Path.Join(CrupestSecretToolDirectory, SurgeRuleSetGlobalOutputFileName), - true, false + true, true ); return; } + else if (verb == "generate-sing-config" || verb == "gs") + { + var config = ToolConfig.FromDirectoryForSing(CrupestSecretToolDirectory, true, true); + Console.Out.WriteLine(config.ToSingConfigString()); + return; + } else if (verb == "generate" || verb == "g") { var config = ToolConfig.FromDirectory(CrupestSecretToolDirectory); diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs index 638edb6..ddbbde8 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Proxy.cs @@ -1,15 +1,21 @@ namespace Crupest.SecretTool; -public abstract class Proxy(string tag) : IV4ConfigObject +public abstract class Proxy(string tag) : IV4ConfigObject, ISingConfigObject { public string Tag { get; set; } = tag; public abstract V4ConfigJsonObjects.Outbound ToJsonObjectV4(); + public abstract SingConfigJsonObjects.OutboundBase ToJsonObjectSing(); object IV4ConfigObject.ToJsonObjectV4() { return ToJsonObjectV4(); } + + object ISingConfigObject.ToJsonObjectSing() + { + return ToJsonObjectSing(); + } } public class HttpProxy(string host, int port, string tag) : Proxy(tag) @@ -17,6 +23,11 @@ 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 SingConfigJsonObjects.OutboundBase ToJsonObjectSing() + { + throw new NotImplementedException("Http proxy is not supported in sing now."); + } + public override V4ConfigJsonObjects.Outbound ToJsonObjectV4() { return new V4ConfigJsonObjects.Outbound(Tag, "http", @@ -34,6 +45,13 @@ public class VmessProxy(string host, int port, string userId, string path, strin public string Path { get; set; } = path; public string UserId { get; set; } = userId; + public override SingConfigJsonObjects.OutboundBase ToJsonObjectSing() + { + return new SingConfigJsonObjects.VmessOutbound(Tag, Host, Port, UserId, + Transport: new SingConfigJsonObjects.V2rayWebsocketTransport(Path), + Tls: new SingConfigJsonObjects.OutboundTls(true)); + } + public override V4ConfigJsonObjects.Outbound ToJsonObjectV4() { return new V4ConfigJsonObjects.Outbound(Tag, "vmess", diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs new file mode 100644 index 0000000..81698a3 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/ProxyFile.cs @@ -0,0 +1,31 @@ +namespace Crupest.SecretTool; + +public class ProxyFile : HostMatchConfigFile +{ + public ProxyFile(string path) : base(path, [.. Enum.GetValues()], maxComponentCount: 0) + { + RoutingRuleMatchers = Config.Items.Select(i => new RoutingRuleMatcher(i.Kind, i.MatchString)).ToList(); + } + + public List RoutingRuleMatchers { get; } + + public List GetChinaRulesByGeoSite(GeoSiteData geoSiteData) + { + var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList(); + return geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds, ["cn"]).Select(e => e.GetRoutingRuleMatcher()).ToList(); + } + + public List GetRulesFlattenGeoSite(GeoSiteData geoSiteData, bool noCn = false) + { + var geoSites = RoutingRuleMatchers.Where(m => m.MatchKind == HostMatchKind.GeoSite).Select(i => i.MatchString).ToList(); + var flattenGeoSiteRules = geoSiteData.GetEntriesRecursive(geoSites, HostMatchKindExtensions.DomainMatchKinds) + .Where(e => !noCn || !e.Attributes.Contains("cn")) + .Select(e => e.GetRoutingRuleMatcher()) + .ToList(); + var otherRules = RoutingRuleMatchers.Where(m => m.MatchKind != HostMatchKind.GeoSite).ToList(); + return [ + ..flattenGeoSiteRules, + ..otherRules + ]; + } +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs index dbced0e..9c247a2 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/Routing.cs @@ -1,5 +1,10 @@ namespace Crupest.SecretTool; +public record RoutingRuleMatcher(HostMatchKind MatchKind, string MatchString) +{ + public RoutingRule ToRoutingRule(string OutboundTag) => new(MatchKind, MatchString, OutboundTag); +} + public record RoutingRule(HostMatchKind MatchKind, string MatchString, string OutboundTag) : IV4ConfigObject { public string ToolConfigString => MatchKind switch @@ -14,6 +19,8 @@ public record RoutingRule(HostMatchKind MatchKind, string MatchString, string Ou _ => throw new ArgumentException("Invalid matcher kind.") }; + public string ToolConfigStringSing => MatchKind.IsSupportedInSingRoute() ? MatchString : throw new ArgumentException("Unsupported matcher kind for sing."); + public static Dictionary> GroupByOutboundTag(List rules) => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary(); @@ -23,6 +30,34 @@ public record RoutingRule(HostMatchKind MatchKind, string MatchString, string Ou public static List> GroupByOutboundTagAndMatcherKind(List rules) => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatchKind(groupByTag).Values).ToList(); + public static SingConfigJsonObjects.RouteRule ListToJsonObjectSing(List rules) + { + if (rules.Count == 0) + { + throw new ArgumentException("Rule list is empty."); + } + + var outboundTag = rules[0].OutboundTag; + + if (rules.Any(r => !r.MatchKind.IsSupportedInSingRoute())) + { + throw new ArgumentException("Rules must have matcher kinds supported in sing."); + } + + if (rules.Any(r => r.OutboundTag != outboundTag)) + { + throw new ArgumentException("Rules must have the same outbound tag."); + } + + return new SingConfigJsonObjects.RouteRule(Outbound: outboundTag, + Domain: rules.Where(r => r.MatchKind == HostMatchKind.DomainFull).Select(r => r.ToolConfigStringSing).ToList(), + DomainSuffix: rules.Where(r => r.MatchKind == HostMatchKind.DomainSuffix).Select(r => r.ToolConfigStringSing).ToList(), + DomainKeyword: rules.Where(r => r.MatchKind == HostMatchKind.DomainKeyword).Select(r => r.ToolConfigStringSing).ToList(), + DomainRegex: rules.Where(r => r.MatchKind == HostMatchKind.DomainRegex).Select(r => r.ToolConfigStringSing).ToList(), + IpCidr: rules.Where(r => r.MatchKind == HostMatchKind.Ip).Select(r => r.ToolConfigStringSing).ToList() + ); + } + public static V4ConfigJsonObjects.RoutingRule ListToJsonObject(List rules) { if (rules.Count == 0) @@ -56,12 +91,14 @@ public record RoutingRule(HostMatchKind MatchKind, string MatchString, string Ou return new RoutingRule(HostMatchKind.GeoSite, $"{MatchString}@cn", outboundTag); } + public RoutingRuleMatcher GetMatcher() => new(MatchKind, MatchString); + public V4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]); object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); } -public record Routing(List Rules, bool DirectGeositeCn = true, string DomainStrategy = "IpOnDemand") : IV4ConfigObject +public record Routing(List Rules) : IV4ConfigObject, ISingConfigObject { public List CreateGeositeCnDirectRules() { @@ -69,7 +106,14 @@ public record Routing(List Rules, bool DirectGeositeCn = true, stri .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList(); } - public V4ConfigJsonObjects.Routing ToJsonObjectV4(bool directGeositeCn = true) + public SingConfigJsonObjects.Route ToJsonObjectSing() + { + List ruleJsonObjects = []; + ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTag(Rules).Values.Select(RoutingRule.ListToJsonObjectSing)); + return new SingConfigJsonObjects.Route(ruleJsonObjects); + } + + public V4ConfigJsonObjects.Routing ToJsonObjectV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true) { List ruleJsonObjects = []; @@ -80,18 +124,32 @@ public record Routing(List Rules, bool DirectGeositeCn = true, stri ruleJsonObjects.AddRange(RoutingRule.GroupByOutboundTagAndMatcherKind(Rules).Select(RoutingRule.ListToJsonObject)); - return new V4ConfigJsonObjects.Routing(ruleJsonObjects); + return new V4ConfigJsonObjects.Routing(ruleJsonObjects, domainStrategy); } object IV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); - public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag, bool directGeositeCn) + object ISingConfigObject.ToJsonObjectSing() => ToJsonObjectSing(); + + public static Routing FromProxyFile(ProxyFile proxyFile, string outboundTag) { + return new Routing( + proxyFile.RoutingRuleMatchers.Select(m => m.ToRoutingRule(outboundTag)).ToList()); + } + + public static Routing FromProxyFileForSing(ProxyFile proxyFile, GeoSiteData geoSiteData, string outboundTag, string? directCnOutboundTag = null) + { + List rules = []; + + if (directCnOutboundTag is not null) + { + rules.AddRange(proxyFile.GetChinaRulesByGeoSite(geoSiteData).Select(m => m.ToRoutingRule(directCnOutboundTag)).ToList()); + } + + rules.AddRange(proxyFile.GetRulesFlattenGeoSite(geoSiteData).Where(m => m.MatchKind.IsSupportedInSingRoute()).Select(m => m.ToRoutingRule(outboundTag)).ToList()); return new Routing( - proxyFile.Config.Items.Select( - i => new RoutingRule(i.Kind, i.MatchString, outboundTag)).ToList(), - directGeositeCn + rules ); } } diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs new file mode 100644 index 0000000..6af0cd1 --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/SingConfigJsonObjects.cs @@ -0,0 +1,20 @@ +namespace Crupest.SecretTool; + +public static class SingConfigJsonObjects +{ + public interface IObject; + + public record OutboundTls(bool Enabled); + public record V2rayTransportBase(string Type); + public record V2rayWebsocketTransport(string Path, Dictionary? Headers = null) : V2rayTransportBase("ws"); + public record OutboundBase(string Tag, string Type) : IObject; + public record VmessOutbound(string Tag, string Server, int ServerPort, string Uuid, string Security = "auto", + V2rayTransportBase? Transport = null, OutboundTls? Tls = null): OutboundBase(Tag, "vmess"); + + public record RouteRule(List? Domain = null, List? DomainSuffix = null, List? DomainKeyword = null, + List? DomainRegex = null, List? IpCidr = null, List? SourceIpCidr = null, + List? Port = null, List? SourcePort = null, List? PortRange = null, List? SourcePortRange = null, + string? Network = null, List? Inbound = null, string? Outbound = null) : IObject; + + public record Route(List Rules) : IObject; +} diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs index 451db3e..8a57c9f 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/SurgeConfigGenerator.cs @@ -1,9 +1,9 @@ namespace Crupest.SecretTool; -public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoSiteData) +public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoData) { - public ProxyFile ProxyFile { get; } = proxyFile; - public GeoSiteData GeoSiteData { get; } = geoSiteData; + public ProxyFile ProxyFile => proxyFile; + public GeoSiteData GeoData => geoData; private static string ToSurgeRuleString(HostMatchKind kind, string value) { @@ -19,39 +19,35 @@ public class SurgeConfigGenerator(ProxyFile proxyFile, GeoSiteData geoSiteData) return $"{ruleType},{value}"; } - private static List DomainMatcherKinds { get; } = [ - HostMatchKind.DomainFull, HostMatchKind.DomainKeyword, - HostMatchKind.DomainRegex, HostMatchKind.DomainSuffix, - ]; + public static string GenerateSurgeRuleSetString(List rules) + { + return string.Join('\n', rules.Select(r => ToSurgeRuleString(r.MatchKind, r.MatchString))); + } 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))); + return GenerateSurgeRuleSetString(proxyFile.GetChinaRulesByGeoSite(GeoData)); } 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)) - ]); + return GenerateSurgeRuleSetString(proxyFile.GetRulesFlattenGeoSite(geoData, true)); } - public static SurgeConfigGenerator Create(string proxyFilePath, bool clean, bool silent) + public static void GenerateTo(ProxyFile proxyFile, GeoSiteData geoSiteData, string cnPath, string globalPath, bool silent) { - var proxyFile = new ProxyFile(proxyFilePath); - var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); - return new SurgeConfigGenerator(proxyFile, geoSiteData); + var generator = new SurgeConfigGenerator(proxyFile, geoSiteData); + 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}."); } - public static void GenerateTo(string proxyFilePath, string cnPath, string globalPath, bool clean, bool silent) + public static void GenerateTo(string directory, string cnPath, string globalPath, bool clean, bool silent) { - var generator = Create(proxyFilePath, clean, silent); + var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); + var proxyFile = new ProxyFile(Path.Combine(directory, ToolConfig.ProxyConfigFileName)); + var generator = new SurgeConfigGenerator(proxyFile, geoSiteData); File.WriteAllText(cnPath, generator.GenerateChinaRuleSet()); if (!silent) Console.WriteLine($"China rule set written to {cnPath}."); File.WriteAllText(globalPath, generator.GenerateGlobalRuleSet()); diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs b/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs index 4fe9a40..534bd65 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/ToolConfig.cs @@ -8,6 +8,11 @@ public interface IV4ConfigObject object ToJsonObjectV4(); } +public interface ISingConfigObject +{ + object ToJsonObjectSing(); +} + public class ToolConfig(Template template, List proxies, Routing router, StaticHosts? hosts) { private class JsonInterfaceConverter : JsonConverter @@ -35,11 +40,14 @@ public class ToolConfig(Template template, List proxies, Routing router, public const string ProxyConfigFileName = "proxy.txt"; public const string HostsConfigFileName = "hosts.txt"; + public const string SingConfigTemplateFileName = "sing-config.json.template"; + 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 SingRouteAnchor = "ROUTE_ANCHOR"; private const string HostsAnchor = "HOSTS_ANCHOR"; public const string AddCnAttributeToGeositeEnvironmentVariable = "CRUPEST_V2RAY_GEOSITE_USE_CN"; @@ -55,7 +63,41 @@ public class ToolConfig(Template template, List proxies, Routing router, public Routing Routing { get; set; } = router; public StaticHosts Hosts { get; set; } = hosts is null ? new StaticHosts([]) : hosts; - public string ToJsonStringV4(bool pretty = true) + public string ToSingConfigString(bool pretty = true) + { + var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, + 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.ToJsonObjectSing(), jsonOptions))), + [SingRouteAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectSing(), 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 string ToJsonStringV4(string domainStrategy = "IpOnDemand", bool directGeositeCn = true, bool pretty = true) { var jsonOptions = new JsonSerializerOptions(new JsonSerializerOptions { @@ -70,7 +112,7 @@ public class ToolConfig(Template template, List proxies, Routing router, var templateValues = new Dictionary { [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))), - [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(), jsonOptions), + [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(domainStrategy, directGeositeCn), jsonOptions), [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions), }; @@ -125,7 +167,7 @@ public class ToolConfig(Template template, List proxies, Routing router, file = vmessPath; var vmess = VmessProxy.CreateFromConfigString(vmessString, "proxy"); file = proxyPath; - var routing = Routing.FromProxyFile(proxyFile, "proxy", UseCnGeoSite); + var routing = Routing.FromProxyFile(proxyFile, "proxy"); file = hostsPath ?? ""; var hosts = hostsString is not null ? StaticHosts.CreateFromHostMatchConfigString(hostsString) : null; return new ToolConfig(template, [vmess], routing, hosts); @@ -136,6 +178,50 @@ public class ToolConfig(Template template, List proxies, Routing router, } } + public static ToolConfig FromFilesForSing(string templatePath, string vmessPath, string proxyPath, bool clean, bool silent) + { + foreach (var path in new List([templatePath, vmessPath, proxyPath])) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Required config file not found: {path}."); + } + } + + var geoSiteData = GeoDataManager.Instance.GetOrCreateGeoSiteData(clean, silent); + + ProxyFile proxyFile = new(proxyPath); + string templateString, vmessString; + + string file = ""; + try + { + file = templatePath; + templateString = File.ReadAllText(templatePath); + file = vmessPath; + vmessString = File.ReadAllText(vmessPath); + } + 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.FromProxyFileForSing(proxyFile, geoSiteData, "proxy", "direct"); + return new ToolConfig(template, [vmess], routing, null); + } + catch (Exception e) + { + throw new Exception($"Error parsing config file {file}.", e); + } + } + public static ToolConfig FromDirectory(string directory) { return FromFiles( @@ -146,6 +232,16 @@ public class ToolConfig(Template template, List proxies, Routing router, ); } + public static ToolConfig FromDirectoryForSing(string directory, bool clean, bool silent) + { + return FromFilesForSing( + Path.Join(directory, SingConfigTemplateFileName), + Path.Join(directory, VmessConfigFileName), + Path.Join(directory, ProxyConfigFileName), + clean, silent + ); + } + public static void FromDirectoryAndWriteToFile(string directory, string outputPath) { var config = FromDirectory(directory); diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt b/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt index 6273e35..39800f9 100644 --- a/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/proxy.txt @@ -1,20 +1,21 @@ -GeoSite github +GeoSite microsoft GeoSite google GeoSite youtube -GeoSite twitter +GeoSite x GeoSite facebook GeoSite discord GeoSite reddit GeoSite twitch -GeoSite onedrive GeoSite quora GeoSite telegram GeoSite imgur GeoSite stackexchange +GeoSite medium GeoSite duckduckgo GeoSite wikimedia GeoSite gitbook +GeoSite github GeoSite gitlab GeoSite sourceforge GeoSite creativecommons @@ -33,6 +34,8 @@ GeoSite v2ray GeoSite homebrew GeoSite azure +GeoSite akamai +GeoSite aws GeoSite jsdelivr GeoSite fastly GeoSite heroku @@ -43,3 +46,5 @@ GeoSite ieee GeoSite sci-hub GeoSite libgen GeoSite z-library + +sagernet.org diff --git a/tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template b/tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template new file mode 100644 index 0000000..429bd1d --- /dev/null +++ b/tools/Crupest.SecretTool/Crupest.SecretTool/sing-config.json.template @@ -0,0 +1,55 @@ +{ + "log": { + "disabled": false, + "level": "info", + "timestamp": true + }, + "dns": { + "servers": [ + { + "tag": "ali-doh", + "address": "https://dns.alidns.com/dns-query", + "address_resolver": "ali" + }, + { + "tag": "ali", + "address": "223.5.5.5" + }, + { + "tag": "cloudflare", + "address": "1.1.1.1" + }, + { + "tag": "google", + "address": "8.8.8.8" + } + + ] + }, + "inbounds": [ + { + "tag": "http-in", + "type": "http", + "listen": "127.0.0.1", + "listen_port": 3080 + }, + { + "tag": "socks-in", + "type": "socks", + "listen": "127.0.0.1", + "listen_port": 3081 + } + ], + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + }, + ${PROXY_ANCHOR} + ], + "route": ${ROUTE_ANCHOR} +} -- cgit v1.2.3