Feat #517 new VMess/VLESS ShareLink format get and parse

This commit is contained in:
ChsBuffer
2021-02-16 20:02:07 +08:00
parent eac456b3a8
commit 8f8fa159c7
7 changed files with 267 additions and 144 deletions

View File

@@ -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

View 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)}")}";
}
}
}

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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();
}
}
}