diff options
12 files changed, 334 insertions, 45 deletions
| 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 @@      <None Update="hosts.txt">        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>      </None> +    <None Update="sing-config.json.template"> +      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> +    </None>    </ItemGroup>  </Project> 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<string> Attributes, string ContainingSite) : IGeoSiteEntry  {      public bool IsInclude => false; + +    public RoutingRuleMatcher GetRoutingRuleMatcher() => new(Kind, Value);  }  public record GeoSite(string Name, List<IGeoSiteEntry> 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<HostMatchKind> DomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword, HostMatchKind.DomainRegex];      public static List<HostMatchKind> NonRegexDomainMatchKinds { get; } = [HostMatchKind.DomainFull, HostMatchKind.DomainSuffix, HostMatchKind.DomainKeyword]; + +    public static List<HostMatchKind> SupportedInSingRouteMatchKinds { get; } = [..DomainMatchKinds, HostMatchKind.Ip]; + +    public static bool IsSupportedInSingRoute(this HostMatchKind kind) => SupportedInSingRouteMatchKinds.Contains(kind);  }  public record HostMatchConfigItem(HostMatchKind Kind, string MatchString, List<string> 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<HostMatchKind>()], 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<HostMatchKind>()], maxComponentCount: 0)
 +    {
 +        RoutingRuleMatchers = Config.Items.Select(i => new RoutingRuleMatcher(i.Kind, i.MatchString)).ToList();
 +    }
 +
 +    public List<RoutingRuleMatcher> RoutingRuleMatchers { get; }
 +
 +    public List<RoutingRuleMatcher> 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<RoutingRuleMatcher> 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<string, List<RoutingRule>> GroupByOutboundTag(List<RoutingRule> 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<List<RoutingRule>> GroupByOutboundTagAndMatcherKind(List<RoutingRule> rules)          => GroupByOutboundTag(rules).Values.SelectMany((groupByTag) => GroupByMatchKind(groupByTag).Values).ToList(); +    public static SingConfigJsonObjects.RouteRule ListToJsonObjectSing(List<RoutingRule> 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<RoutingRule> 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<RoutingRule> Rules, bool DirectGeositeCn = true, string DomainStrategy = "IpOnDemand") : IV4ConfigObject +public record Routing(List<RoutingRule> Rules) : IV4ConfigObject, ISingConfigObject  {      public List<RoutingRule> CreateGeositeCnDirectRules()      { @@ -69,7 +106,14 @@ public record Routing(List<RoutingRule> Rules, bool DirectGeositeCn = true, stri              .Select(r => r.CloneGeositeWithCnAttribute("direct")).ToList();      } -    public V4ConfigJsonObjects.Routing ToJsonObjectV4(bool directGeositeCn = true) +    public SingConfigJsonObjects.Route ToJsonObjectSing() +    { +        List<SingConfigJsonObjects.RouteRule> 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<V4ConfigJsonObjects.RoutingRule> ruleJsonObjects = []; @@ -80,18 +124,32 @@ public record Routing(List<RoutingRule> 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<RoutingRule> 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<string, string>? 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<string>? Domain = null, List<string>? DomainSuffix = null, List<string>? DomainKeyword = null,
 +        List<string>? DomainRegex = null, List<string>? IpCidr = null, List<string>? SourceIpCidr = null,
 +         List<int>? Port = null, List<int>? SourcePort = null, List<string>? PortRange = null, List<string>? SourcePortRange = null,
 +         string? Network = null, List<string>? Inbound = null, string? Outbound = null) : IObject;
 +
 +    public record Route(List<RouteRule> 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<HostMatchKind> DomainMatcherKinds { get; } = [ -        HostMatchKind.DomainFull, HostMatchKind.DomainKeyword, -        HostMatchKind.DomainRegex, HostMatchKind.DomainSuffix, -    ]; +    public static string GenerateSurgeRuleSetString(List<RoutingRuleMatcher> 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<Proxy> proxies, Routing router, StaticHosts? hosts)  {      private class JsonInterfaceConverter<Interface> : JsonConverter<Interface> @@ -35,11 +40,14 @@ public class ToolConfig(Template template, List<Proxy> 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<string> RequiredConfigFileNames { get; } = [ConfigTemplateFileName, VmessConfigFileName, ProxyConfigFileName];      public static List<string> 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<Proxy> 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<SingConfigJsonObjects.OutboundBase>()); +        jsonOptions.Converters.Add(new JsonInterfaceConverter<SingConfigJsonObjects.V2rayTransportBase>()); + +        var templateValues = new Dictionary<string, string> +        { +            [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<object>(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<Proxy> proxies, Routing router,          var templateValues = new Dictionary<string, string>          {              [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<Proxy> 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<Proxy> proxies, Routing router,          }      } +    public static ToolConfig FromFilesForSing(string templatePath, string vmessPath, string proxyPath, bool clean, bool silent) +    { +        foreach (var path in new List<string>([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<Proxy> 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}
 +}
 | 
