diff options
28 files changed, 847 insertions, 677 deletions
diff --git a/crupest-words.txt b/crupest-words.txt index af85fa2..62906cb 100644 --- a/crupest-words.txt +++ b/crupest-words.txt @@ -1,9 +1,13 @@  crupest  # secret +vnext  vmess  confdir  geodata +geosite +geoip +userid  # university  ustc diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Config.cs b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs new file mode 100644 index 0000000..fc71007 --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/Config.cs @@ -0,0 +1,87 @@ +namespace Crupest.V2ray; + +public record ConfigItem(string Value, int LineNumber); + +public class DictionaryConfig(string configString, List<string>? requiredKeys = null) +{ +    private static Dictionary<string, ConfigItem> Parse(string configString, List<string>? requiredKeys = null) +    { +        var config = new Dictionary<string, ConfigItem>(); +        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<string>? RequiredKeys { get; } = requiredKeys; +    public Dictionary<string, ConfigItem> 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<ConfigItem> Parse(string configString) +    { +        var config = new List<ConfigItem>(); +        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; +            } +            config.Add(new ConfigItem(trimmedLine, lineNumber)); +            lineNumber++; +        } + +        return config; +    } + +    public string ConfigString { get; } = configString; +    public List<ConfigItem> Config { get; } = Parse(configString); +} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs deleted file mode 100644 index 32be5b0..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/ConfigGenerationWatcher.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Crupest.V2ray; - -public class ConfigGenerationWatcher -{ -    public ConfigGenerationWatcher() : this(Program.ExeDir, Program.ConfigTemplateFileName, Program.VmessConfigFileName, Program.ProxyConfigFileName, Program.HostsConfigFileName, Path.Combine(Program.ExeDir, Program.ConfigOutputFileName), new List<string>()) -    { - -    } - -    public ConfigGenerationWatcher(string directory, string configTemplateFileName, string vmessConfigFileName, string proxyConfigFileName, string hostsConfigFileName, string configOutputPath, List<string> otherWatchFiles) -    { -        Directory = directory; -        ConfigTemplateFileName = configTemplateFileName; -        VmessConfigFileName = vmessConfigFileName; -        ProxyConfigFileName = proxyConfigFileName; -        HostsConfigFileName = hostsConfigFileName; -        ConfigOutputPath = configOutputPath; -        OtherWatchFiles = otherWatchFiles; -    } - -    public string Directory { get; set; } -    public string ConfigTemplateFileName { get; set; } -    public string VmessConfigFileName { get; set; } -    public string ProxyConfigFileName { get; set; } -    public string HostsConfigFileName { get; set; } -    public List<string> OtherWatchFiles { get; set; } -    public string ConfigOutputPath { get; set; } - -    public string ConfigTemplateFilePath => Path.Combine(Directory, ConfigTemplateFileName); -    public string VmessConfigFilePath => Path.Combine(Directory, VmessConfigFileName); -    public string ProxyConfigFilePath => Path.Combine(Directory, ProxyConfigFileName); -    public string HostsConfigFilePath => Path.Combine(Directory, HostsConfigFileName); - -    public delegate void OnConfigChangedHandler(); - -    public void Generate() -    { -        var config = V2rayConfig.FromFiles(ConfigTemplateFilePath, VmessConfigFilePath, ProxyConfigFilePath, HostsConfigFilePath); - -        File.WriteAllText(ConfigOutputPath, config.ToJson()); -    } - -    public void Run(OnConfigChangedHandler onChanged) -    { -        var sourceWatcher = new FileSystemWatcher(Directory); -        sourceWatcher.Filters.Add(ConfigTemplateFileName); -        sourceWatcher.Filters.Add(VmessConfigFileName); -        sourceWatcher.Filters.Add(ProxyConfigFileName); -        OtherWatchFiles.ForEach((f) => sourceWatcher.Filters.Add(f)); -        sourceWatcher.NotifyFilter = NotifyFilters.LastWrite; - -        while (true) -        { -            var result = sourceWatcher.WaitForChanged(WatcherChangeTypes.Changed); -            onChanged(); -        } -    } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj b/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj index 3962fe4..0812e4c 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj +++ b/tools/Crupest.V2ray/Crupest.V2ray/Crupest.V2ray.csproj @@ -2,7 +2,7 @@    <PropertyGroup>      <OutputType>Exe</OutputType> -    <TargetFramework>net7.0</TargetFramework> +    <TargetFramework>net8.0</TargetFramework>      <ImplicitUsings>enable</ImplicitUsings>      <Nullable>enable</Nullable>    </PropertyGroup> diff --git a/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs b/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs deleted file mode 100644 index 08de673..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/FileUtility.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Text.Json; -using System.Text.RegularExpressions; - -namespace Crupest.V2ray; - -public static partial class FileUtility -{ -    public static List<string> ReadList(string str) -    { -        return str.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); -    } - -    public static Dictionary<string, string> ReadDictionary(string str, bool keyToLower = true) -    { -        var lines = str.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); -        var result = new Dictionary<string, string>(); -        for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++) -        { -            var line = lines[lineNumber]; -            if (!line.Contains('=')) -            { -                throw new FormatException($"Line {lineNumber + 1} does not contain a '='."); -            } -            var equalIndex = line.IndexOf('='); -            var key = line[..equalIndex].Trim(); -            if (keyToLower) key = key.ToLower(); -            var value = line[(equalIndex + 1)..].Trim(); -            result[key] = value; -        } -        return result; -    } - -    public static List<string> ReadListFile(string path, bool required = true) -    { -        if (File.Exists(path)) -        { -            return ReadList(File.ReadAllText(path)); -        } -        else -        { -            if (required) -            { -                throw new FileNotFoundException($"File {path} is required but it does not exist."); -            } -            return new(); -        } -    } - -    public static Dictionary<string, string> ReadDictionaryFile(string path, bool required = true, bool keyToLower = true) -    { -        if (File.Exists(path)) -        { -            return ReadDictionary(File.ReadAllText(path), keyToLower); -        } -        else -        { -            if (required) -            { -                throw new FileNotFoundException($"File {path} is required but it does not exist."); -            } -            return new(); -        } -    } - -    private static Regex TemplateValuePattern { get; } = CreateTemplateValuePattern(); - -    [GeneratedRegex(@"\$\{\s*([_a-zA-Z][_a-zA-Z0-9]*)\s*\}")] -    private static partial Regex CreateTemplateValuePattern(); - -    public static string TextFromTemplate(string template, Dictionary<string, string> dict) -    { -        return TemplateValuePattern.Replace(template, (match) => -        { -            var key = match.Groups[1].Value; -            if (dict.ContainsKey(key)) -            { -                return dict[key]; -            } -            return match.Value; -        }); -    } - -    public static string JsonFormat(string json) -    { -        var options = new JsonSerializerOptions -        { -            WriteIndented = true, -            AllowTrailingCommas = true, -            ReadCommentHandling = JsonCommentHandling.Skip -        }; - -        return JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonDocument>(json, options), options); -    } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs new file mode 100644 index 0000000..547adeb --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/FileWatcher.cs @@ -0,0 +1,26 @@ +namespace Crupest.V2ray; + +public class FileWatcher(string directory, List<string> fileNames) +{ +    public string Directory { get; set; } = directory; +    public List<string> 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/IV2rayProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/IV2rayProxy.cs deleted file mode 100644 index 7643a01..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayProxy.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Crupest.V2ray; - -public interface IV2rayProxy -{ -    object ToOutboundJsonObject(); -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs b/tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs deleted file mode 100644 index ae5fe1c..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/IV2rayStaticHostResolveResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Crupest.V2ray; - -public interface IV2rayStaticHostResolveResult -{ -    IDictionary<string, object> GetJsonProperties(); -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs index e623a88..e06a92d 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/Program.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/Program.cs @@ -4,14 +4,32 @@ namespace Crupest.V2ray;  public static class Program  { -    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 const string ConfigOutputFileName = "config.json"; +    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.")); -    public static string ExeDir { get; } = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new Exception("Can't get the path of exe.")); +    private const string ConfigOutputFileName = "config.json"; +    public static void RunV2rayAndWatchConfigChange() +    { +        var v2rayPath = V2rayController.FindExecutable(CrupestV2rayDirectory) ?? +            throw new Exception("Can't find v2ray executable either in Crupest.V2ray directory or in PATH."); + +        var v2rayController = new V2rayController(v2rayPath, Path.Combine(CrupestV2rayDirectory, ConfigOutputFileName), CrupestV2rayDirectory); +        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)      { @@ -25,22 +43,18 @@ public static class Program              if (verb == "download-geodata" || verb == "dg")              {                  var geoDataDownloader = new GeoDataDownloader(); -                geoDataDownloader.Download(ExeDir); +                geoDataDownloader.Download(CrupestV2rayDirectory); +                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.");          } -        var v2rayController = new V2rayController(); -        var configGenerationWatcher = new ConfigGenerationWatcher(); - -        configGenerationWatcher.Generate(); -        v2rayController.Start(); - -        configGenerationWatcher.Run(() => -        { -            configGenerationWatcher.Generate(); -            v2rayController.Restart(); -        }); +        RunV2rayAndWatchConfigChange();      }  } diff --git a/tools/Crupest.V2ray/Crupest.V2ray/Template.cs b/tools/Crupest.V2ray/Crupest.V2ray/Template.cs new file mode 100644 index 0000000..9c137b0 --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/Template.cs @@ -0,0 +1,231 @@ +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<string, string> values); +    } + +    private class TextNode(string text) : ITemplateNode +    { + +        public string Text { get; } = text; + +        public string Render(Dictionary<string, string> values) +        { +            return Text; +        } +    } + +    private class VariableNode(string variableName) : ITemplateNode +    { +        public string VariableName { get; } = variableName; + +        public string Render(Dictionary<string, string> values) +        { +            return values.GetValueOrDefault(VariableName) ?? ""; +        } +    } + +    public Template(string templateString) +    { +        TemplateString = templateString; +        Nodes = Parse(templateString); +        VariableNames = Nodes.OfType<VariableNode>().Select(node => node.VariableName).ToList(); +    } + +    private static List<ITemplateNode> Parse(string templateString) +    { +        int lineNumber = 1; +        int columnNumber = 0; +        List<ITemplateNode> 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<ITemplateNode> Nodes { get; set; } +    public List<string> VariableNames { get; } + +    public string Generate(Dictionary<string, string> 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 index 5623a20..c246bb5 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayConfig.cs @@ -1,57 +1,147 @@  using System.Text.Json; +using System.Text.Json.Serialization;  namespace Crupest.V2ray; -public class V2rayConfig +public interface IV2rayV4ConfigObject  { -    private const string ProxyAnchor = "PROXY_ANCHOR"; -    private const string VmessAnchor = "VMESS_PROXY_ANCHOR"; -    private const string RoutingAnchor = "ROUTING_ANCHOR"; -    private const string HostsAnchor = "HOSTS_ANCHOR"; +    object ToJsonObjectV4(); +} -    public V2rayConfig(string template, IV2rayProxy proxy, V2rayRouting router, V2rayHosts hosts) +public class V2rayConfig(Template template, List<V2rayProxy> proxies, V2rayRouting router, V2rayHosts? hosts) +{ +    private class JsonInterfaceConverter<Interface> : JsonConverter<Interface>      { -        Template = template; -        Proxy = proxy; -        Routing = router; -        Hosts = hosts; +        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 string Template { get; set; } -    public IV2rayProxy Proxy { get; set; } -    public V2rayRouting Routing { get; set; } -    public V2rayHosts Hosts { get; set; } -    public string ToJson(bool pretty = true) +    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<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 HostsAnchor = "HOSTS_ANCHOR"; + +    public Template Template { get; set; } = template; +    public List<V2rayProxy> 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<V2rayV4ConfigJsonObjects.IOutboundSettings>()); +        jsonOptions.Converters.Add(new JsonInterfaceConverter<V2rayV4ConfigJsonObjects.IOutboundStreamSettings>());          var templateValues = new Dictionary<string, string>          { -            [VmessAnchor] = JsonSerializer.Serialize(Proxy.ToOutboundJsonObject(), jsonOptions), -            [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObject(), jsonOptions), -            [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObject(), jsonOptions), +            [ProxyAnchor] = string.Join(',', Proxies.Select(p => JsonSerializer.Serialize(p.ToJsonObjectV4(), jsonOptions))), +            [RoutingAnchor] = JsonSerializer.Serialize(Routing.ToJsonObjectV4(), jsonOptions), +            [HostsAnchor] = JsonSerializer.Serialize(Hosts.ToJsonObjectV4(), jsonOptions),          }; -        return FileUtility.JsonFormat(FileUtility.TextFromTemplate(Template, templateValues)); +        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 static V2rayConfig FromFiles(string templatePath, string vmessPath, string routingPath, string hostsPath) +    public static V2rayConfig FromFiles(string templatePath, string vmessPath, string proxyPath, string? hostsPath)      { -        var template = File.ReadAllText(templatePath); +        foreach (var path in new List<string>([templatePath, vmessPath, proxyPath])) +        { +            if (!File.Exists(path)) +            { +                throw new FileNotFoundException($"Required config file not found: {path}."); +            } +        } + +        string templateString, vmessString, routingString; +        string? hostsString; + +        string file = ""; +        try +        { +            file = templatePath; +            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) +        { +            throw new Exception($"Error reading config file {file}.", e); +        } -        var vmessDict = FileUtility.ReadDictionaryFile(vmessPath); -        var proxyRoutingList = FileUtility.ReadListFile(routingPath); -        var hostsList = FileUtility.ReadListFile(hostsPath); +        try +        { +            file = templatePath; +            var template = new Template(templateString); +            file = vmessPath; +            var vmess = V2rayVmessProxy.CreateFromConfigString(vmessString, "proxy"); +            file = proxyPath; +            var routing = V2rayRouting.CreateFromConfigString(routingString, "proxy"); +            file = hostsPath ?? ""; +            var hosts = hostsString is not null ? V2rayHosts.CreateFromConfigString(hostsString) : null; +            return new V2rayConfig(template, [vmess], routing, hosts); +        } +        catch (Exception e) +        { +            throw new Exception($"Error parsing config file {file}.", e); +        } +    } -        var vmess = V2rayVmessProxy.FromDictionary(vmessDict); -        var routing = V2rayRouting.FromStringList(proxyRoutingList); -        var hosts = V2rayHosts.FromStringList(hostsList); +    public static V2rayConfig FromDirectory(string directory) +    { +        return FromFiles( +            Path.Join(directory, ConfigTemplateFileName), +            Path.Join(directory, VmessConfigFileName), +            Path.Join(directory, ProxyConfigFileName), +            Path.Join(directory, HostsConfigFileName) +        ); +    } -        return new V2rayConfig(template, vmess, routing, hosts); +    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 index 201dcf4..ab1614a 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayController.cs @@ -2,69 +2,58 @@ using System.Diagnostics;  namespace Crupest.V2ray; -public class V2rayController +public class V2rayController(string executablePath, string configPath, string assetPath)  { -    public static string V2rayExecutableName { get; } = OperatingSystem.IsWindows() ? "v2ray.exe" : "v2ray"; -    public const string V2rayExecutableLocationEnvironmentVariableName = "V2RAY_LOCATION_EXE"; -    public const string V2rayAssetLocationEnvironmentVariableName = "V2RAY_LOCATION_ASSET"; -    public const string V2rayConfigLocationEnvironmentVariableName = "V2RAY_LOCATION_CONFIG"; -    public const string V2rayV5ConfdirEnvironmentVariableName = "v2ray.location.confdir"; +    public const string V2rayAssetEnvironmentVariableName = "v2ray.location.asset"; -    public V2rayController() : this(V2rayExecutableName, Program.ExeDir, Program.ExeDir) +    public static string? FindExecutable(string contentDir, string? executableName = null)      { -        var localV2ray = Path.Combine(Program.ExeDir, V2rayExecutableName); -        if (Path.Exists(localV2ray)) +        executableName ??= "v2ray"; + +        if (OperatingSystem.IsWindows())          { -            V2rayExePath = localV2ray; +            executableName += ".exe";          } -    } - -    public V2rayController(string v2rayExePath, string configDirPath, string assetDirPath) -    { -        V2rayExePath = v2rayExePath; -        ConfigDirPath = configDirPath; -        AssetDirPath = assetDirPath; -    } - -    public string V2rayExePath { get; } -    public string ConfigDirPath { get; } -    public string AssetDirPath { get; } -    public Process? CurrentProcess { get; private set; } -    private Process CreateProcess() -    { -        var process = new Process(); - -        var startInfo = new ProcessStartInfo +        var localV2rayPath = Path.Combine(contentDir, executableName); +        if (File.Exists(localV2rayPath))          { -            FileName = V2rayExePath, -        }; -        startInfo.EnvironmentVariables[V2rayConfigLocationEnvironmentVariableName] = ConfigDirPath; -        startInfo.EnvironmentVariables[V2rayAssetLocationEnvironmentVariableName] = AssetDirPath; +            return localV2rayPath; +        } -        process.StartInfo = startInfo; -        process.OutputDataReceived += (_, args) => -        { -            Console.Out.Write(args.Data); -        }; -        process.ErrorDataReceived += (_, args) => +        var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator); +        if (paths is not null)          { -            Console.Error.WriteLine(args.Data); -        }; +            foreach (var p in paths) +            { +                var v2rayPath = Path.Combine(p, executableName); +                if (File.Exists(v2rayPath)) +                { +                    return v2rayPath; +                } +            } +        } -        return process; +        return null;      } -    private Process V5CreateProcess() +    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 = V2rayExePath, +            FileName = ExecutablePath,          };          startInfo.ArgumentList.Add("run"); -        startInfo.EnvironmentVariables[V2rayV5ConfdirEnvironmentVariableName] = ConfigDirPath; +        startInfo.ArgumentList.Add("-c"); +        startInfo.ArgumentList.Add(ConfigPath); +        startInfo.EnvironmentVariables[V2rayAssetEnvironmentVariableName] = AssetPath;          process.StartInfo = startInfo;          process.OutputDataReceived += (_, args) => @@ -96,13 +85,13 @@ public class V2rayController          if (CurrentProcess is null)          { -            CurrentProcess = V5CreateProcess(); +            CurrentProcess = CreateProcess();              CurrentProcess.EnableRaisingEvents = true;              CurrentProcess.Exited += (_, _) =>              {                  if (CurrentProcess.ExitCode != 0)                  { -                    const string message = "V2ray exits with error."; +                    const string message = "V2ray exited with error.";                      Console.Error.WriteLine(message);                      throw new Exception(message);                  } diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs new file mode 100644 index 0000000..f444b5d --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostMacherConfig.cs @@ -0,0 +1,95 @@ +namespace Crupest.V2ray; + +public enum V2rayHostMatcherKind +{ +    DomainFull, +    DomainSuffix, +    DomainKeyword, +    DomainRegex, +    Ip, +    GeoSite, +    GeoIp, +} + +public record V2rayHostMatcherItem(V2rayHostMatcherKind Kind, string Matcher, List<string> Values); + +public class V2rayHostMatcherConfig(string configString, List<V2rayHostMatcherKind> 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<V2rayHostMatcherItem> Parse(string configString, List<V2rayHostMatcherKind> allowedMatchers, int minComponentCount = -1, int maxComponentCount = -1) +    { +        var items = new ListConfig(configString).Config; +        var result = new List<V2rayHostMatcherItem>(); + +        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<V2rayHostMatcherKind>()) +            { +                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) && 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<V2rayHostMatcherKind> AllowedMatchers { get; } = allowedMatchers; +    public int MinComponentCount { get; } = minComponentCount; +    public int MaxComponentCount { get; } = maxComponentCount; +    public List<V2rayHostMatcherItem> Items { get; } = Parse(configString, allowedMatchers, minComponentCount, maxComponentCount); +} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs deleted file mode 100644 index 31feaaf..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHostRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Crupest.V2ray; - -public record V2rayHostRule(string Origin, List<string> Resolved) -{ -    public static V2rayHostRule Parse(string str) -    { -        var segments = str.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); -        if (segments.Length == 1) -        { -            throw new Exception("Host rule only contains 1 segment."); -        } - -        var resolved = new List<string>(); -        resolved.AddRange(segments[1..]); - -        return new V2rayHostRule(segments[0], resolved); -    } -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs index a1c8d61..1d1c6d8 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHosts.cs @@ -1,26 +1,42 @@  namespace Crupest.V2ray; -public record V2rayHosts(List<V2rayHostRule> Rules) +public record V2rayHostRule(V2rayHostMatcherKind MatcherKind, string MatcherString, List<string> ResolveResult)  { -    public V2rayHosts() : this(new List<V2rayHostRule>()) { } - -    public Dictionary<string, List<string>> ToJsonObject() +    public string AddressString()      { -        var result = new Dictionary<string, List<string>>(); -        foreach (var rule in Rules) +        return MatcherKind switch          { -            result.Add(rule.Origin, rule.Resolved); -        } -        return result; +            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 static V2rayHosts FromStringList(List<string> list) +    public object ResolveResultToJsonObject()      { -        var hosts = new V2rayHosts(); -        foreach (var str in list) -        { -            hosts.Rules.Add(V2rayHostRule.Parse(str)); -        } -        return hosts; +        return ResolveResult.Count == 1 ? ResolveResult[0] : ResolveResult; +    } +} + +public class V2rayHosts(List<V2rayHostRule> rules) : IV2rayV4ConfigObject +{ +    public List<V2rayHostRule> Rules { get; } = rules; + +    public Dictionary<string, object> ToJsonObjectV4() => +        Rules.ToDictionary(rule => rule.AddressString(), rule => rule.ResolveResultToJsonObject()); + +    object IV2rayV4ConfigObject.ToJsonObjectV4() +    { +        return ToJsonObjectV4(); +    } + +    public static V2rayHosts CreateFromConfigString(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/V2rayHttpProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayHttpProxy.cs deleted file mode 100644 index c641b4b..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayHttpProxy.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Crupest.V2ray; - -public class V2rayHttpProxy : IV2rayProxy -{ -    public record HttpOutboundJsonObject(string Protocol, SettingsJsonObject Settings, string Tag) -    { -        public static HttpOutboundJsonObject Create(string address, int port, string tag) -        { -            return new HttpOutboundJsonObject("http", new SettingsJsonObject( -                new List<ServerJsonObject> { new ServerJsonObject(address, port) } -            ), tag); -        } -    } - -    public record ServerJsonObject(string Address, int Port); -    public record SettingsJsonObject(List<ServerJsonObject> Servers); - -    public string Host { get; set; } -    public int Port { get; set; } - -    public V2rayHttpProxy(string host, int port) -    { -        Host = host; -        Port = port; -    } - -    public HttpOutboundJsonObject ToOutboundJsonObject(string tag = "proxy") -    { -        return HttpOutboundJsonObject.Create(Host, Port, tag); -    } - -    object IV2rayProxy.ToOutboundJsonObject() -    { -        return ToOutboundJsonObject(); -    } - -    public static V2rayHttpProxy FromDictionary(Dictionary<string, string> dict) -    { -        return new V2rayHttpProxy(dict["host"], int.Parse(dict["port"])); -    } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs new file mode 100644 index 0000000..bcb2b51 --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayProxy.cs @@ -0,0 +1,58 @@ +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 index d5ad0da..528b49e 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRouting.cs @@ -1,65 +1,72 @@  namespace Crupest.V2ray; -public record V2rayRouting(List<V2rayRoutingRule> Rules, string DomainStrategy = "IpOnDemand") +public record V2rayRoutingRule(V2rayHostMatcherKind MatcherKind, string MatcherString, string OutboundTag) : IV2rayV4ConfigObject  { -    public record DomainRuleJsonObject(List<string> Domains, string OutboundTag, string Type = "field"); - -    public record IpRuleJsonObject(List<string> Ip, string OutboundTag, string Type = "field"); - -    public record V5DomainRuleJsonObject(List<V2rayRoutingRuleMatcher.V5DomainObject> Domain, string OutboundTag); +    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<string, List<V2rayRoutingRule>> GroupByOutboundTag(List<V2rayRoutingRule> rules) +        => rules.GroupBy(r => r.OutboundTag).Select(g => (g.Key, g.ToList())).ToDictionary(); + +    public static Dictionary<V2rayHostMatcherKind, List<V2rayRoutingRule>> GroupByMatcherByKind(List<V2rayRoutingRule> rules) +        => rules.GroupBy(r => r.MatcherKind).Select(g => (g.Key, g.ToList())).ToDictionary(); + +    public static V2rayV4ConfigJsonObjects.RoutingRule ListToJsonObject(List<V2rayRoutingRule> rules) +    { +        if (rules.Count == 0) +        { +            throw new ArgumentException("Rule list is empty."); +        } -    public record V5GeoDomainRuleJsonObject(List<V2rayRoutingRuleMatcher.V5GeoDomainObject> GeoDomain, string OutboundTag); +        var matcherKind = rules[0].MatcherKind; +        var outboundTag = rules[0].OutboundTag; -    public record V5GeoIpRuleJsonObject(List<V2rayRoutingRuleMatcher.V5GeoIpObject> Geoip, string 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."); +        } -    public record RoutingJsonObject(string DomainStrategy, List<object> Rules); +        List<string> composedMatcherStringList = rules.Select(r => r.ComposedMatcherString).ToList(); -    public record V5RouterJsonObject(string DomainStrategy, List<object> Rule); +        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 V2rayRouting() : this(new List<V2rayRoutingRule>()) -    { +    public V2rayV4ConfigJsonObjects.RoutingRule ToJsonObjectV4() => ListToJsonObject([this]); -    } +    object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); +} -    public RoutingJsonObject ToJsonObject() +public record V2rayRouting(List<V2rayRoutingRule> Rules, string DomainStrategy = "IpOnDemand") : IV2rayV4ConfigObject +{ +    public V2rayV4ConfigJsonObjects.Routing ToJsonObjectV4()      {          var ruleJsonObjects = new List<object>(); -        foreach (var (outBoundTag, proxyRules) in V2rayRoutingRule.GroupByOutboundTag(Rules)) -        { -            foreach (var (matchByKind, rules) in V2rayRoutingRule.GroupByMatchByKind(proxyRules)) -            { -                ruleJsonObjects.Add( -                    matchByKind switch -                    { -                        V2rayRoutingRuleMatcher.MatchByKind.Ip => new IpRuleJsonObject(rules.Select(r => r.Matcher.ToString()).ToList(), outBoundTag), -                        V2rayRoutingRuleMatcher.MatchByKind.Domain => new DomainRuleJsonObject(rules.Select(r => r.Matcher.ToString()).ToList(), outBoundTag), -                        _ => throw new Exception("Unknown match by kind."), -                    } -                ); -            } -        } +        var rules = V2rayRoutingRule.GroupByOutboundTag(Rules).ToList().SelectMany((groupByTag) => +            V2rayRoutingRule.GroupByMatcherByKind(groupByTag.Value).ToList().Select((groupByMatcher) => +                V2rayRoutingRule.ListToJsonObject(groupByMatcher.Value)) +        ).ToList(); -        return new RoutingJsonObject(DomainStrategy, ruleJsonObjects); +        return new V2rayV4ConfigJsonObjects.Routing(rules);      } -    public V5RouterJsonObject V5ToJsonObject() -    { -        throw new NotImplementedException(); -    } +    object IV2rayV4ConfigObject.ToJsonObjectV4() => ToJsonObjectV4(); -    public static V2rayRouting FromStringList(List<string> list, string outboundTag = "proxy") +    public static V2rayRouting CreateFromConfigString(string configString, string outboundTag)      { -        var router = new V2rayRouting(); - -        foreach (var line in list) -        { -            var matcher = V2rayRoutingRuleMatcher.Parse(line); -            if (matcher != null) -                router.Rules.Add(new V2rayRoutingRule(matcher, outboundTag)); -        } - -        return router; +        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());      }  } - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs deleted file mode 100644 index 1928de0..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRule.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Crupest.V2ray; - -public record V2rayRoutingRule(V2rayRoutingRuleMatcher Matcher, string OutboundTag) -{ -    public static Dictionary<string, List<V2rayRoutingRule>> GroupByOutboundTag(List<V2rayRoutingRule> rules) -    { -        var result = new Dictionary<string, List<V2rayRoutingRule>>(); -        foreach (var group in rules.GroupBy(r => r.OutboundTag)) -        { -            result[group.Key] = group.ToList(); -        } -        return result; -    } - -    public static Dictionary<V2rayRoutingRuleMatcher.MatchByKind, List<V2rayRoutingRule>> GroupByMatchByKind(List<V2rayRoutingRule> rules) -    { -        var result = new Dictionary<V2rayRoutingRuleMatcher.MatchByKind, List<V2rayRoutingRule>>(); -        foreach (var group in rules.GroupBy(r => r.Matcher.MatchBy)) -        { -            result[group.Key] = group.ToList(); -        } -        return result; -    } - -    public static Dictionary<V2rayRoutingRuleMatcher.V5MatchByKind, List<V2rayRoutingRule>> V5GroupByMatchByKind(List<V2rayRoutingRule> rules) -    { -        var result = new Dictionary<V2rayRoutingRuleMatcher.V5MatchByKind, List<V2rayRoutingRule>>(); -        foreach (var group in rules.GroupBy(r => r.Matcher.V5MatchBy)) -        { -            result[group.Key] = group.ToList(); -        } -        return result; -    } -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs deleted file mode 100644 index 7c853c4..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayRoutingRuleMatcher.cs +++ /dev/null @@ -1,142 +0,0 @@ -namespace Crupest.V2ray; - -public record V2rayRoutingRuleMatcher(V2rayRoutingRuleMatcher.MatchKind Kind, string Value) -{ -    public enum MatchByKind -    { -        Domain, -        Ip -    } - -    public enum V5MatchByKind -    { -        Domain, -        // Ip, -        GeoIp, -        GeoSite, -    } - -    public enum MatchKind -    { -        GeoIp, -        GeoSite, -        DomainPlain, -        DomainSuffix, -        DomainRegex, -        DomainFull, -    } - -    public MatchByKind MatchBy -    { -        get -        { -            return Kind switch -            { -                MatchKind.GeoIp => MatchByKind.Ip, -                _ => MatchByKind.Domain -            }; -        } -    } - -    public V5MatchByKind V5MatchBy -    { -        get -        { -            return Kind switch -            { -                MatchKind.GeoIp => V5MatchByKind.GeoIp, -                MatchKind.GeoSite => V5MatchByKind.GeoSite, -                _ => V5MatchByKind.Domain, -            }; -        } -    } - -    public static V2rayRoutingRuleMatcher? Parse(string line) -    { -        if (line.IndexOf('#') != -1) -        { -            line = line[..line.IndexOf('#')]; -        } - -        line = line.Trim(); - -        if (line.Length == 0) { return null; } - -        var kind = MatchKind.DomainSuffix; - -        foreach (var name in Enum.GetNames<MatchKind>()) -        { -            if (line.StartsWith(name)) -            { -                kind = Enum.Parse<MatchKind>(name); -                line = line[name.Length..]; -                line = line.Trim(); -                break; -            } -        } - -        return new V2rayRoutingRuleMatcher(kind, line); -    } - - -    public override string ToString() -    { -        return Kind switch -        { -            MatchKind.GeoSite => $"geosite:{Value}", -            MatchKind.GeoIp => $"geoip:{Value}", -            MatchKind.DomainPlain => Value, -            MatchKind.DomainSuffix => $"domain:{Value}", -            MatchKind.DomainFull => $"full:{Value}", -            MatchKind.DomainRegex => $"regexp:{Value}", -            _ => throw new Exception("Unknown matcher kind."), -        }; -    } - -    public enum V5DomainObjectType -    { -        Plain, -        Regex, -        RootDomain, -        Full, -    } - -    public record V5DomainObject(V5DomainObjectType Type, string Value); - -    public V5DomainObject ToDomainObject() -    { -        return new V5DomainObject(Kind switch -        { -            MatchKind.DomainFull => V5DomainObjectType.Full, -            MatchKind.DomainPlain => V5DomainObjectType.Plain, -            MatchKind.DomainRegex => V5DomainObjectType.Regex, -            MatchKind.DomainSuffix => V5DomainObjectType.RootDomain, -            _ => throw new Exception("Not a domain matcher."), -        }, Value); -    } - -    public record V5GeoDomainObject(string Code); - -    public V5GeoDomainObject ToGeoDomainObject() -    { -        if (Kind != MatchKind.GeoSite) -        { -            throw new Exception("Not a geo-domain matcher."); -        } - -        return new V5GeoDomainObject(Value); -    } - -    public record V5GeoIpObject(string Code); - -    public V5GeoIpObject ToGeoIpObject() -    { -        if (Kind != MatchKind.GeoIp) -        { -            throw new Exception("Not a geo-ip matcher."); -        } - -        return new V5GeoIpObject(Value); -    } -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs deleted file mode 100644 index 88ea5ba..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostDomainResolveResult.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Crupest.V2ray; - -public class V2rayStaticHostDomainResolveResult : IV2rayStaticHostResolveResult -{ -    public V2rayStaticHostDomainResolveResult(string domain) -    { -        Domain = domain; -    } - -    public string Domain { get; } - -    public IDictionary<string, object> GetJsonProperties() -    { -        return new Dictionary<string, object> -        { - -            ["proxiedDomain"] = Domain -        }; -    } -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs deleted file mode 100644 index 2829152..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayStaticHostIpResolveResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Crupest.V2ray; - -public class V2rayStaticHostIpResolveResult : IV2rayStaticHostResolveResult -{ -    public V2rayStaticHostIpResolveResult(IEnumerable<string> ips) -    { -        Ips = ips.ToList(); -    } - -    public IReadOnlyList<string> Ips { get; } - -    public IDictionary<string, object> GetJsonProperties() -    { -        return new Dictionary<string, object> -        { -            ["ip"] = Ips -        }; -    } -} - diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs new file mode 100644 index 0000000..672af71 --- /dev/null +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV4ConfigJsonObjects.cs @@ -0,0 +1,25 @@ +namespace Crupest.V2ray; + +public static class V2rayV4ConfigJsonObjects +{ +    public interface IObject; +    public interface IOutboundSettings : IObject; +    public interface IOutboundStreamSettings : IObject; + +    public record WsSettings(string Path, Dictionary<string, string> 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<VnextServerUser> Users) : IObject; +    public record VmessOutboundSettings(List<VnextServer> Vnext) : IOutboundSettings; +    public record HttpOutboundUser(string User, string Pass) : IObject; +    public record HttpOutboundServer(string Address, int Port, List<HttpOutboundUser> Users) : IObject; +    public record HttpOutboundSettings(List<HttpOutboundServer> Servers) : IOutboundSettings; +    public record Outbound(string Tag, string Protocol, IOutboundSettings Settings, +        IOutboundStreamSettings? StreamSettings) : IObject; + +    public record RoutingRule(string DomainMatcher = "mph", string Type = "field", List<string>? Domains = null, List<string>? Ip = null, +        string? Port = null, string? SourcePort = null, string? Network = null, List<string>? Source = null, +        List<string>? User = null, List<string>? InboundTag = null, List<string>? Protocol = null, string? Attrs = null, +        string? OutboundTag = null, string? BalancerTag = null) : IObject; +    public record Routing(List<RoutingRule> Rules, string DomainStrategy = "IpOnDemand", string DomainMatcher = "mph") : IObject; +} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigObjects.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs index f4001c1..56d64ca 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigObjects.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5ConfigJsonObjects.cs @@ -1,6 +1,6 @@  namespace Crupest.V2ray; -public static class V2rayV5ConfigObjects +public static class V2rayV5ConfigJsonObjects  {      public record OutboundObject(string Protocol, object Settings, string Tag, object? StreamSettings)      { diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs index 4a89134..cdead3c 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs +++ b/tools/Crupest.V2ray/Crupest.V2ray/V2rayV5StaticHostRule.cs @@ -2,7 +2,50 @@ using System.Net;  namespace Crupest.V2ray; -public class V2rayV5StaticHostRule +public interface IV2rayStaticHostResolveResult +{ +    IDictionary<string, object> GetJsonProperties(); +} + +public class V2rayStaticHostDomainResolveResult : IV2rayStaticHostResolveResult +{ +    public V2rayStaticHostDomainResolveResult(string domain) +    { +        Domain = domain; +    } + +    public string Domain { get; } + +    public IDictionary<string, object> GetJsonProperties() +    { +        return new Dictionary<string, object> +        { + +            ["proxiedDomain"] = Domain +        }; +    } +} + +public class V2rayStaticHostIpResolveResult : IV2rayStaticHostResolveResult +{ +    public V2rayStaticHostIpResolveResult(IEnumerable<string> ips) +    { +        Ips = ips.ToList(); +    } + +    public IReadOnlyList<string> Ips { get; } + +    public IDictionary<string, object> GetJsonProperties() +    { +        return new Dictionary<string, object> +        { +            ["ip"] = Ips +        }; +    } +} + + +public class V2rayV5StaticHostRule(V2rayV5StaticHostRule.MatcherKind matcher, string domain, IV2rayStaticHostResolveResult resolveResult)  {      public enum MatcherKind      { @@ -12,16 +55,9 @@ public class V2rayV5StaticHostRule          Regex      } -    public V2rayV5StaticHostRule(MatcherKind matcher, string domain, IV2rayStaticHostResolveResult resolveResult) -    { -        Matcher = matcher; -        Domain = domain; -        ResolveResult = resolveResult; -    } - -    public MatcherKind Matcher { get; } -    public string Domain { get; } -    public IV2rayStaticHostResolveResult ResolveResult { get; } +    public MatcherKind Matcher { get; } = matcher; +    public string Domain { get; } = domain; +    public IV2rayStaticHostResolveResult ResolveResult { get; } = resolveResult;      public Dictionary<string, object> ToJsonObject()      { diff --git a/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs b/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs deleted file mode 100644 index 495961c..0000000 --- a/tools/Crupest.V2ray/Crupest.V2ray/V2rayVmessProxy.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Crupest.V2ray; - -public class V2rayVmessProxy : IV2rayProxy -{ -    public record VmessOutboundJsonObject(string Protocol, SettingsJsonObject Settings, string Tag, StreamSettingsJsonObject StreamSettings) -    { -        public static VmessOutboundJsonObject ByWs(string address, int port, string uuid, string tag, string path) -        { -            return new VmessOutboundJsonObject("vmess", new SettingsJsonObject( -                new List<VnextJsonObject> { new VnextJsonObject(address, port, new List<VnextUserJsonObject> { new VnextUserJsonObject(uuid) }) } -            ), tag, StreamSettingsJsonObject.Ws(path)); -        } -    } - -    public record SettingsJsonObject(List<VnextJsonObject> Vnext); - -    public record VnextJsonObject(string Address, int Port, List<VnextUserJsonObject> Users); - -    public record VnextUserJsonObject(string Id, int AlterId = 0, string Security = "auto", int Level = 0); - -    public record WsSettingsJsonObject(string Path, Dictionary<string, string> Headers); - -    public record StreamSettingsJsonObject(string Network, string Security, WsSettingsJsonObject WsSettings) -    { -        public static StreamSettingsJsonObject Ws(string path) -        { -            return new StreamSettingsJsonObject("ws", "tls", new WsSettingsJsonObject(path, new())); -        } -    } - -    public string Host { get; set; } -    public int Port { get; set; } -    public string Path { get; set; } -    public string UserId { get; set; } - - -    public V2rayVmessProxy(string host, int port, string userId, string path) -    { -        Host = host; -        Port = port; -        UserId = userId; -        Path = path; -    } - -    public VmessOutboundJsonObject ToOutboundJsonObject(string tag = "proxy") -    { -        return VmessOutboundJsonObject.ByWs(Host, Port, UserId, tag, Path); -    } - -    public V2rayV5ConfigObjects.OutboundObject ToOutboundJsonObjectV5(string tag = "proxy") -    { -        return V2rayV5ConfigObjects.OutboundObject.VmessViaWs(tag, Host, Port, UserId, Path); -    } - -    object IV2rayProxy.ToOutboundJsonObject() -    { -        return ToOutboundJsonObject(); -    } - -    public static V2rayVmessProxy FromDictionary(Dictionary<string, string> dict) -    { -        return new V2rayVmessProxy(dict["host"], int.Parse(dict["port"]), dict["userid"], dict["path"]); -    } -} diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template index 53aee76..424e996 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/config.json.template +++ b/tools/Crupest.V2ray/Crupest.V2ray/config.json.template @@ -33,7 +33,7 @@        "settings": {},        "tag": "blocked"      }, -    ${VMESS_PROXY_ANCHOR} +    ${PROXY_ANCHOR}    ],    "routing": ${ROUTING_ANCHOR},    "dns": { diff --git a/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template b/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template index 49d97c2..01ccf7a 100644 --- a/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template +++ b/tools/Crupest.V2ray/Crupest.V2ray/config.v5.json.template @@ -15,7 +15,7 @@          }, {              "address": "localhost"          }], -        "staticHosts": ${STATIC_HOSTS_ANCHOR} +        "staticHosts": ${HOSTS_ANCHOR}      },      "inbounds": [{          { @@ -48,7 +48,7 @@            "settings": {},            "tag": "blocked"          }, -        ${PROXY_OUTBOUND_ANCHOR} +        ${PROXY_ANCHOR}      ],      "router": ${ROUTER_ANCHOR}  }  | 
