mirror of
https://github.com/netchx/netch.git
synced 2026-05-07 22:44:03 +08:00
Feat #517 new VMess/VLESS ShareLink format get and parse
This commit is contained in:
@@ -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
|
||||
|
||||
141
Netch/Servers/VLESS/V2rayUtils.cs
Normal file
141
Netch/Servers/VLESS/V2rayUtils.cs
Normal file
@@ -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<Server> 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}://(?<guid>.+?)@(?<server>.+):(?<port>\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<string, string>();
|
||||
// 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)}")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,18 +32,15 @@ namespace Netch.Servers.VLESS
|
||||
|
||||
public class VLESSGlobal
|
||||
{
|
||||
public static readonly List<string> FakeTypes = new()
|
||||
{
|
||||
"none",
|
||||
"http"
|
||||
};
|
||||
|
||||
public static readonly List<string> TLSSecure = new()
|
||||
{
|
||||
"none",
|
||||
"tls",
|
||||
"xtls"
|
||||
};
|
||||
public static List<string> FakeTypes => VMessGlobal.FakeTypes;
|
||||
public static List<string> TransferProtocols => VMessGlobal.TransferProtocols;
|
||||
|
||||
public static List<string> QUIC => VMessGlobal.QUIC;
|
||||
}
|
||||
}
|
||||
@@ -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<Server> ParseUri(string text)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
return V2rayUtils.ParseVUri(text);
|
||||
}
|
||||
|
||||
public bool CheckServer(Server s)
|
||||
|
||||
@@ -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<V2rayNSharing>(ShareLink.URLSafeBase64Decode(text));
|
||||
vmess = JsonConvert.DeserializeObject<V2rayNSharing>(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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// URL 传输安全的 Base64 解码
|
||||
/// </summary>
|
||||
/// <param name="text">需要解码的字符串</param>
|
||||
/// <returns>解码后的字符串</returns>
|
||||
public static string URLSafeBase64Decode(string text)
|
||||
{
|
||||
return Encoding.UTF8.GetString(Convert.FromBase64String(text.Replace("-", "+").Replace("_", "/").PadRight(text.Length + (4 - text.Length % 4) % 4, '=')));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// URL 传输安全的 Base64 加密
|
||||
/// </summary>
|
||||
/// <param name="text">需要加密的字符串</param>
|
||||
/// <returns>加密后的字符串</returns>
|
||||
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<string, string> ParseParam(string paramStr)
|
||||
{
|
||||
var paramsDict = new Dictionary<string, string>();
|
||||
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<string> 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
|
||||
|
||||
/// <summary>
|
||||
/// URL 传输安全的 Base64 解码
|
||||
/// </summary>
|
||||
/// <param name="text">需要解码的字符串</param>
|
||||
/// <returns>解码后的字符串</returns>
|
||||
public static string URLSafeBase64Decode(string text)
|
||||
{
|
||||
return Encoding.UTF8.GetString(Convert.FromBase64String(text.Replace("-", "+").Replace("_", "/").PadRight(text.Length + (4 - text.Length % 4) % 4, '=')));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// URL 传输安全的 Base64 加密
|
||||
/// </summary>
|
||||
/// <param name="text">需要加密的字符串</param>
|
||||
/// <returns>加密后的字符串</returns>
|
||||
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<string, string> ParseParam(string paramStr)
|
||||
{
|
||||
var paramsDict = new Dictionary<string, string>();
|
||||
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<string> 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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user