diff options
author | crupest <crupest@outlook.com> | 2024-09-17 23:59:44 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2024-09-18 21:47:50 +0800 |
commit | 57514635a10bad1feceda4c6f576baf07607af33 (patch) | |
tree | d08be10b64947caa033bd1b67b1417fe99ae462a | |
parent | cbe3f1de55795809ddda910122bc51c395dae48c (diff) | |
download | crupest-57514635a10bad1feceda4c6f576baf07607af33.tar.gz crupest-57514635a10bad1feceda4c6f576baf07607af33.tar.bz2 crupest-57514635a10bad1feceda4c6f576baf07607af33.zip |
refactor(secret): refactor codes and make config work.
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} } |