diff --git a/Netch/Models/Setting.cs b/Netch/Models/Setting.cs index db53e02a..a7ab9d4f 100644 --- a/Netch/Models/Setting.cs +++ b/Netch/Models/Setting.cs @@ -61,13 +61,14 @@ namespace Netch.Models public class V2rayConfig { - public bool XrayCone = false; - public bool AllowInsecure = true; public KcpConfig KcpConfig = new(); public bool UseMux = false; + + public bool V2rayNShareLink = true; + public bool XrayCone = false; } public class AioDNSConfig diff --git a/Netch/Servers/VLESS/V2rayUtils.cs b/Netch/Servers/VLESS/V2rayUtils.cs new file mode 100644 index 00000000..364e41f1 --- /dev/null +++ b/Netch/Servers/VLESS/V2rayUtils.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Web; +using Netch.Models; +using Netch.Utils; + +namespace Netch.Servers.V2ray +{ + internal static class V2rayUtils + { + public static IEnumerable ParseVUri(string text) + { + var scheme = ShareLink.GetUriScheme(text); + try + { + var server = new VMess.VMess(); + if (text.Contains("#")) + { + server.Remark = Uri.UnescapeDataString(text.Split('#')[1]); + text = text.Split('#')[0]; + } + + if (text.Contains("?")) + { + var parameter = HttpUtility.ParseQueryString(text.Split('?')[1]); + text = text.Substring(0, text.IndexOf("?", StringComparison.Ordinal)); + server.TransferProtocol = parameter.Get("type") ?? "tcp"; + server.EncryptMethod = parameter.Get("encryption") ?? scheme switch {"vless" => "none", _ => "auto"}; + switch (server.TransferProtocol) + { + case "tcp": + break; + case "kcp": + server.FakeType = parameter.Get("headerType") ?? "none"; + server.Path = Uri.UnescapeDataString(parameter.Get("seed") ?? ""); + break; + case "ws": + server.Path = Uri.UnescapeDataString(parameter.Get("path") ?? "/"); + server.Host = parameter.Get("host") ?? ""; + break; + case "h2": + server.Path = Uri.UnescapeDataString(parameter.Get("path") ?? "/"); + server.Host = Uri.UnescapeDataString(parameter.Get("host") ?? ""); + break; + case "quic": + server.QUICSecure = parameter.Get("quicSecurity") ?? "none"; + server.QUICSecret = parameter.Get("key") ?? ""; + server.FakeType = parameter.Get("headerType") ?? "none"; + break; + } + server.TLSSecureType = parameter.Get("security") ?? "none"; + if (server.TLSSecureType != "none") + { + server.Host = parameter.Get("sni") ?? ""; + if (server.TLSSecureType == "xtls") + ((VLESS.VLESS) server).Flow = parameter.Get("flow") ?? ""; + } + } + var finder = new Regex(@$"^{scheme}://(?.+?)@(?.+):(?\d+)"); + var match = finder.Match(text.Split('?')[0]); + if (!match.Success) + throw new FormatException(); + + server.UserID = match.Groups["guid"].Value; + server.Hostname = match.Groups["server"].Value; + server.Port = ushort.Parse(match.Groups["port"].Value); + + return new[] + { + server + }; + } + catch (FormatException e) + { + Logging.Error(e.ToString()); + return null; + } + } + public static string GetVShareLink(Server s, string scheme = "vmess") + { + var server = (VMess.VMess) s; + var parameter = new Dictionary(); + // protocol-specific fields + parameter.Add("type", server.TransferProtocol); + if (server.EncryptMethod == "none") + // VLESS outbounds[].settings.encryption,当前可选值只有 none + parameter.Add("encryption", server.EncryptMethod); + // transport-specific fields + switch (server.TransferProtocol) + { + case "tcp": + break; + case "kcp": + if (server.FakeType != "none") + parameter.Add("headerType", server.FakeType); + if (!server.Path.IsNullOrWhiteSpace()) + parameter.Add("seed", Uri.EscapeDataString(server.Path)); + break; + case "ws": + parameter.Add("path", Uri.EscapeDataString(server.Path.IsNullOrWhiteSpace() ? "/" : server.Path)); + if (!server.Host.IsNullOrWhiteSpace()) + parameter.Add("host", server.Host); + break; + case "h2": + parameter.Add("path", Uri.EscapeDataString(server.Path.IsNullOrWhiteSpace() ? "/" : server.Path)); + if (!server.Host.IsNullOrWhiteSpace()) + parameter.Add("host", Uri.EscapeDataString(server.Host)); + break; + case "quic": + if (server.QUICSecure != "none") + { + parameter.Add("quicSecurity", server.QUICSecure); + parameter.Add("key", server.QUICSecret); + } + + if (server.FakeType != "none") + parameter.Add("headerType", server.FakeType); + break; + } + + if (server.TLSSecureType != "none") + { + parameter.Add("security", server.TLSSecureType); + + if (!server.Host.IsNullOrWhiteSpace()) + parameter.Add("sni", server.Host); + + if (server.TLSSecureType == "xtls") + { + var flow = ((VLESS.VLESS) server).Flow.Replace("-udp443", ""); + if (!flow.IsNullOrWhiteSpace()) + parameter.Add("flow", flow); + } + } + + return $"{scheme}://{server.UserID}@{server.Hostname}:{server.Port}?{string.Join("&", parameter.Select(p => $"{p.Key}={p.Value}"))}{(server.Remark.IsNullOrWhiteSpace() ? "" : $"#{Uri.EscapeDataString(server.Remark)}")}"; + } + } +} \ No newline at end of file diff --git a/Netch/Servers/VLESS/VLESS.cs b/Netch/Servers/VLESS/VLESS.cs index ea4bef0b..e3908e7c 100644 --- a/Netch/Servers/VLESS/VLESS.cs +++ b/Netch/Servers/VLESS/VLESS.cs @@ -32,18 +32,15 @@ namespace Netch.Servers.VLESS public class VLESSGlobal { - public static readonly List FakeTypes = new() - { - "none", - "http" - }; - public static readonly List TLSSecure = new() { "none", "tls", "xtls" }; + public static List FakeTypes => VMessGlobal.FakeTypes; public static List TransferProtocols => VMessGlobal.TransferProtocols; + + public static List QUIC => VMessGlobal.QUIC; } } \ No newline at end of file diff --git a/Netch/Servers/VLESS/VLESSUtil.cs b/Netch/Servers/VLESS/VLESSUtil.cs index 055eab46..d51cbf0f 100644 --- a/Netch/Servers/VLESS/VLESSUtil.cs +++ b/Netch/Servers/VLESS/VLESSUtil.cs @@ -12,7 +12,7 @@ namespace Netch.Servers.VLESS public string TypeName { get; } = "VLESS"; public string FullName { get; } = "VLESS"; public string ShortName { get; } = "VL"; - public string[] UriScheme { get; } = { }; + public string[] UriScheme { get; } = {"vless"}; public Server ParseJObject(in JObject j) { @@ -29,10 +29,9 @@ namespace Netch.Servers.VLESS new VLESSForm.VLESSForm().ShowDialog(); } - public string GetShareLink(Server server) + public string GetShareLink(Server s) { - // TODO - return ""; + return V2rayUtils.GetVShareLink(s, "vless"); } public IServerController GetController() @@ -42,7 +41,7 @@ namespace Netch.Servers.VLESS public IEnumerable ParseUri(string text) { - throw new System.NotImplementedException(); + return V2rayUtils.ParseVUri(text); } public bool CheckServer(Server s) diff --git a/Netch/Servers/VMess/VMessUtil.cs b/Netch/Servers/VMess/VMessUtil.cs index e365dc32..f26cecdc 100644 --- a/Netch/Servers/VMess/VMessUtil.cs +++ b/Netch/Servers/VMess/VMessUtil.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Netch.Controllers; using Netch.Models; @@ -36,23 +35,28 @@ namespace Netch.Servers.VMess public string GetShareLink(Server s) { - var server = (VMess) s; - - var vmessJson = JsonConvert.SerializeObject(new + if (Global.Settings.V2RayConfig.V2rayNShareLink) { - v = "2", - ps = server.Remark, - add = server.Hostname, - port = server.Port, - id = server.UserID, - aid = server.AlterID, - net = server.TransferProtocol, - type = server.FakeType, - host = server.Host, - path = server.Path, - tls = server.TLSSecureType - }); - return "vmess://" + ShareLink.URLSafeBase64Encode(vmessJson); + var server = (VMess) s; + + var vmessJson = JsonConvert.SerializeObject(new + { + v = "2", + ps = server.Remark, + add = server.Hostname, + port = server.Port, + id = server.UserID, + aid = server.AlterID, + net = server.TransferProtocol, + type = server.FakeType, + host = server.Host, + path = server.Path, + tls = server.TLSSecureType + }); + return "vmess://" + ShareLink.URLSafeBase64Encode(vmessJson); + } + + return V2rayUtils.GetVShareLink(s); } public IServerController GetController() @@ -64,16 +68,14 @@ namespace Netch.Servers.VMess { var data = new VMess(); - text = text.Substring(8); V2rayNSharing vmess; try { - vmess = JsonConvert.DeserializeObject(ShareLink.URLSafeBase64Decode(text)); + vmess = JsonConvert.DeserializeObject(ShareLink.URLSafeBase64Decode(text.Substring(8))); } - catch (Exception e) + catch { - Logging.Warning(e.ToString()); - return null; + return V2rayUtils.ParseVUri(text); } data.Remark = vmess.ps; @@ -120,13 +122,11 @@ namespace Netch.Servers.VMess } if (server.TransferProtocol == "quic") - { if (!VMessGlobal.QUIC.Contains(server.QUICSecure)) { Logging.Error($"不支持的 VMess QUIC 加密方式:{server.QUICSecure}"); return false; } - } return true; } diff --git a/Netch/Utils/ShareLink.cs b/Netch/Utils/ShareLink.cs index 829a565b..8417974d 100644 --- a/Netch/Utils/ShareLink.cs +++ b/Netch/Utils/ShareLink.cs @@ -3,106 +3,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Netch.Models; using Netch.Servers.Shadowsocks; using Netch.Servers.Shadowsocks.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Server = Netch.Models.Server; namespace Netch.Utils { public static class ShareLink { - #region Utils - - /// - /// URL 传输安全的 Base64 解码 - /// - /// 需要解码的字符串 - /// 解码后的字符串 - public static string URLSafeBase64Decode(string text) - { - return Encoding.UTF8.GetString(Convert.FromBase64String(text.Replace("-", "+").Replace("_", "/").PadRight(text.Length + (4 - text.Length % 4) % 4, '='))); - } - - /// - /// URL 传输安全的 Base64 加密 - /// - /// 需要加密的字符串 - /// 加密后的字符串 - public static string URLSafeBase64Encode(string text) - { - return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).Replace("+", "-").Replace("/", "_").Replace("=", ""); - } - - private static string RemoveEmoji(string text) - { - byte[] emojiBytes = {240, 159}; - var remark = Encoding.UTF8.GetBytes(text); - var startIndex = 0; - while (remark.Length > startIndex + 1 && remark[startIndex] == emojiBytes[0] && remark[startIndex + 1] == emojiBytes[1]) - startIndex += 4; - return Encoding.UTF8.GetString(remark.Skip(startIndex).ToArray()).Trim(); - } - - - public static string UnBase64String(string value) - { - if (string.IsNullOrEmpty(value)) - { - return ""; - } - - var bytes = Convert.FromBase64String(value); - return Encoding.UTF8.GetString(bytes); - } - - public static string ToBase64String(string value) - { - if (string.IsNullOrEmpty(value)) - { - return ""; - } - - var bytes = Encoding.UTF8.GetBytes(value); - return Convert.ToBase64String(bytes); - } - - public static Dictionary ParseParam(string paramStr) - { - var paramsDict = new Dictionary(); - var obfsParams = paramStr.Split('&'); - foreach (var p in obfsParams) - { - if (p.IndexOf('=') > 0) - { - var index = p.IndexOf('='); - var key = p.Substring(0, index); - var val = p.Substring(index + 1); - paramsDict[key] = val; - } - } - - return paramsDict; - } - - public static IEnumerable GetLines(this string str, bool removeEmptyLines = true) - { - using var sr = new StringReader(str); - string line; - while ((line = sr.ReadLine()) != null) - { - if (removeEmptyLines && string.IsNullOrWhiteSpace(line)) - { - continue; - } - - yield return line; - } - } - - #endregion - public static string GetShareLink(Server server) { return ServerHelper.GetUtilByTypeName(server.Type).GetShareLink(server); @@ -137,9 +47,7 @@ namespace Netch.Utils catch (JsonReaderException) { foreach (var line in text.GetLines()) - { list.AddRange(ParseUri(line)); - } } catch (Exception e) { @@ -168,13 +76,9 @@ namespace Netch.Utils var scheme = GetUriScheme(text); var util = ServerHelper.GetUtilByUriScheme(scheme); if (util != null) - { list.AddRange(util.ParseUri(text)); - } else - { Logging.Warning($"无法处理 {scheme} 协议订阅链接"); - } } } catch (Exception e) @@ -183,14 +87,13 @@ namespace Netch.Utils } foreach (var node in list) - { - node.Remark = RemoveEmoji(node.Remark); - } + if (!node.Remark.IsNullOrWhiteSpace()) + node.Remark = RemoveEmoji(node.Remark); return list.Where(s => s != null); } - private static string GetUriScheme(string text) + public static string GetUriScheme(string text) { var endIndex = text.IndexOf("://", StringComparison.Ordinal); if (endIndex == -1) @@ -206,19 +109,13 @@ namespace Netch.Utils var NetchLink = (JObject) JsonConvert.DeserializeObject(URLSafeBase64Decode(text)); if (NetchLink == null) - { return null; - } if (string.IsNullOrEmpty((string) NetchLink["Hostname"])) - { return null; - } if (!ushort.TryParse((string) NetchLink["Port"], out _)) - { return null; - } var type = (string) NetchLink["Type"]; var s = ServerHelper.GetUtilByTypeName(type).ParseJObject(NetchLink); @@ -236,5 +133,86 @@ namespace Netch.Utils var server = (Server) s.Clone(); return "Netch://" + URLSafeBase64Encode(JsonConvert.SerializeObject(server, new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore})); } + + #region Utils + + /// + /// URL 传输安全的 Base64 解码 + /// + /// 需要解码的字符串 + /// 解码后的字符串 + public static string URLSafeBase64Decode(string text) + { + return Encoding.UTF8.GetString(Convert.FromBase64String(text.Replace("-", "+").Replace("_", "/").PadRight(text.Length + (4 - text.Length % 4) % 4, '='))); + } + + /// + /// URL 传输安全的 Base64 加密 + /// + /// 需要加密的字符串 + /// 加密后的字符串 + public static string URLSafeBase64Encode(string text) + { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).Replace("+", "-").Replace("/", "_").Replace("=", ""); + } + + private static string RemoveEmoji(string text) + { + byte[] emojiBytes = {240, 159}; + var remark = Encoding.UTF8.GetBytes(text); + var startIndex = 0; + while (remark.Length > startIndex + 1 && remark[startIndex] == emojiBytes[0] && remark[startIndex + 1] == emojiBytes[1]) + startIndex += 4; + return Encoding.UTF8.GetString(remark.Skip(startIndex).ToArray()).Trim(); + } + + public static string UnBase64String(string value) + { + if (string.IsNullOrEmpty(value)) + return ""; + + var bytes = Convert.FromBase64String(value); + return Encoding.UTF8.GetString(bytes); + } + + public static string ToBase64String(string value) + { + if (string.IsNullOrEmpty(value)) + return ""; + + var bytes = Encoding.UTF8.GetBytes(value); + return Convert.ToBase64String(bytes); + } + + public static Dictionary ParseParam(string paramStr) + { + var paramsDict = new Dictionary(); + var obfsParams = paramStr.Split('&'); + foreach (var p in obfsParams) + if (p.IndexOf('=') > 0) + { + var index = p.IndexOf('='); + var key = p.Substring(0, index); + var val = p.Substring(index + 1); + paramsDict[key] = val; + } + + return paramsDict; + } + + public static IEnumerable GetLines(this string str, bool removeEmptyLines = true) + { + using var sr = new StringReader(str); + string line; + while ((line = sr.ReadLine()) != null) + { + if (removeEmptyLines && string.IsNullOrWhiteSpace(line)) + continue; + + yield return line; + } + } + + #endregion } } \ No newline at end of file diff --git a/Test/Tests.cs b/Test/Tests.cs index 4ca7b16f..b4f2fb29 100644 --- a/Test/Tests.cs +++ b/Test/Tests.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Windows.Forms; using Netch.Servers.ShadowsocksR; +using Netch.Servers.VLESS; using Netch.Servers.VMess; using Netch.Servers.VMess.Form; using Netch.Utils; @@ -47,5 +48,11 @@ namespace Test */ return (VMess) new VMessUtil().ParseUri(@"vmess://eyAidiI6ICIyIiwgInBzIjogIuWkh+azqOWIq+WQjSIsICJhZGQiOiAiMTExLjExMS4xMTEuMTExIiwgInBvcnQiOiAiMzIwMDAiLCAiaWQiOiAiMTM4NmY4NWUtNjU3Yi00ZDZlLTlkNTYtNzhiYWRiNzVlMWZkIiwgImFpZCI6ICIxMDAiLCAibmV0IjogInRjcCIsICJ0eXBlIjogIm5vbmUiLCAiaG9zdCI6ICJ3d3cuYmJiLmNvbSIsICJwYXRoIjogIi8iLCAidGxzIjogInRscyIgfQ==").First(); } + + [Test] + public void ParseVLESSUri() + { + var server = new VLESSUtil().ParseUri(@"vless://399ce595-894d-4d40-add1-7d87f1a3bd10@qv2ray.net:41971?type=kcp&headerType=wireguard&seed=69f04be3-d64e-45a3-8550-af3172c63055#VLESSmKCPSeedWG").First(); + } } } \ No newline at end of file