mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-18 23:54:49 +03:00
4562d4cf00
Introduces EchConfigList and EchForceQuery fields to ProfileItem and V2rayConfig models, updates related handlers and services to process these fields, and extends the AddServerWindow UI to allow user input for ECH configuration. Also adds localization entries for the new fields and updates extension methods for string handling.
328 lines
12 KiB
C#
328 lines
12 KiB
C#
using System.Collections.Specialized;
|
|
|
|
namespace ServiceLib.Handler.Fmt;
|
|
|
|
public class BaseFmt
|
|
{
|
|
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
|
|
|
|
protected static string GetIpv6(string address)
|
|
{
|
|
if (Utils.IsIpv6(address))
|
|
{
|
|
// Check if the address is already surrounded by square brackets, if not, add square brackets
|
|
return address.StartsWith('[') && address.EndsWith(']') ? address : $"[{address}]";
|
|
}
|
|
else
|
|
{
|
|
return address;
|
|
}
|
|
}
|
|
|
|
protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery)
|
|
{
|
|
if (item.Flow.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("flow", item.Flow);
|
|
}
|
|
|
|
if (item.StreamSecurity.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("security", item.StreamSecurity);
|
|
}
|
|
else
|
|
{
|
|
if (securityDef != null)
|
|
{
|
|
dicQuery.Add("security", securityDef);
|
|
}
|
|
}
|
|
if (item.Sni.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
|
|
}
|
|
if (item.Fingerprint.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("fp", Utils.UrlEncode(item.Fingerprint));
|
|
}
|
|
if (item.PublicKey.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("pbk", Utils.UrlEncode(item.PublicKey));
|
|
}
|
|
if (item.ShortId.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("sid", Utils.UrlEncode(item.ShortId));
|
|
}
|
|
if (item.SpiderX.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("spx", Utils.UrlEncode(item.SpiderX));
|
|
}
|
|
if (item.Mldsa65Verify.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify));
|
|
}
|
|
|
|
if (item.StreamSecurity.Equals(Global.StreamSecurity))
|
|
{
|
|
if (item.Alpn.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
|
}
|
|
ToUriQueryAllowInsecure(item, ref dicQuery);
|
|
}
|
|
if (item.EchConfigList.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
|
}
|
|
|
|
dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
|
|
|
|
switch (item.Network)
|
|
{
|
|
case nameof(ETransport.tcp):
|
|
dicQuery.Add("headerType", item.HeaderType.IsNotEmpty() ? item.HeaderType : Global.None);
|
|
if (item.RequestHost.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("host", Utils.UrlEncode(item.RequestHost));
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.kcp):
|
|
dicQuery.Add("headerType", item.HeaderType.IsNotEmpty() ? item.HeaderType : Global.None);
|
|
if (item.Path.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("seed", Utils.UrlEncode(item.Path));
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.ws):
|
|
case nameof(ETransport.httpupgrade):
|
|
if (item.RequestHost.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("host", Utils.UrlEncode(item.RequestHost));
|
|
}
|
|
if (item.Path.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("path", Utils.UrlEncode(item.Path));
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.xhttp):
|
|
if (item.RequestHost.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("host", Utils.UrlEncode(item.RequestHost));
|
|
}
|
|
if (item.Path.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("path", Utils.UrlEncode(item.Path));
|
|
}
|
|
if (item.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(item.HeaderType))
|
|
{
|
|
dicQuery.Add("mode", Utils.UrlEncode(item.HeaderType));
|
|
}
|
|
if (item.Extra.IsNotEmpty())
|
|
{
|
|
var node = JsonUtils.ParseJson(item.Extra);
|
|
var extra = node != null
|
|
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = false,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
})
|
|
: item.Extra;
|
|
dicQuery.Add("extra", Utils.UrlEncode(extra));
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.http):
|
|
case nameof(ETransport.h2):
|
|
dicQuery["type"] = nameof(ETransport.http);
|
|
if (item.RequestHost.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("host", Utils.UrlEncode(item.RequestHost));
|
|
}
|
|
if (item.Path.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("path", Utils.UrlEncode(item.Path));
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.quic):
|
|
dicQuery.Add("headerType", item.HeaderType.IsNotEmpty() ? item.HeaderType : Global.None);
|
|
dicQuery.Add("quicSecurity", Utils.UrlEncode(item.RequestHost));
|
|
dicQuery.Add("key", Utils.UrlEncode(item.Path));
|
|
break;
|
|
|
|
case nameof(ETransport.grpc):
|
|
if (item.Path.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("authority", Utils.UrlEncode(item.RequestHost));
|
|
dicQuery.Add("serviceName", Utils.UrlEncode(item.Path));
|
|
if (item.HeaderType is Global.GrpcGunMode or Global.GrpcMultiMode)
|
|
{
|
|
dicQuery.Add("mode", Utils.UrlEncode(item.HeaderType));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
protected static int ToUriQueryLite(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
|
{
|
|
if (item.Sni.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
|
|
}
|
|
if (item.Alpn.IsNotEmpty())
|
|
{
|
|
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
|
}
|
|
|
|
ToUriQueryAllowInsecure(item, ref dicQuery);
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
|
{
|
|
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
|
|
{
|
|
// Add two for compatibility
|
|
dicQuery.Add("insecure", "1");
|
|
dicQuery.Add("allowInsecure", "1");
|
|
}
|
|
else
|
|
{
|
|
dicQuery.Add("insecure", "0");
|
|
dicQuery.Add("allowInsecure", "0");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item)
|
|
{
|
|
item.Flow = GetQueryValue(query, "flow");
|
|
item.StreamSecurity = GetQueryValue(query, "security");
|
|
item.Sni = GetQueryValue(query, "sni");
|
|
item.Alpn = GetQueryDecoded(query, "alpn");
|
|
item.Fingerprint = GetQueryDecoded(query, "fp");
|
|
item.PublicKey = GetQueryDecoded(query, "pbk");
|
|
item.ShortId = GetQueryDecoded(query, "sid");
|
|
item.SpiderX = GetQueryDecoded(query, "spx");
|
|
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
|
|
item.EchConfigList = GetQueryDecoded(query, "ech");
|
|
|
|
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
|
|
{
|
|
item.AllowInsecure = Global.AllowInsecure.First();
|
|
}
|
|
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
|
|
{
|
|
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
|
|
}
|
|
else
|
|
{
|
|
item.AllowInsecure = string.Empty;
|
|
}
|
|
|
|
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
|
|
switch (item.Network)
|
|
{
|
|
case nameof(ETransport.tcp):
|
|
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
|
|
item.RequestHost = GetQueryDecoded(query, "host");
|
|
break;
|
|
|
|
case nameof(ETransport.kcp):
|
|
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
|
|
item.Path = GetQueryDecoded(query, "seed");
|
|
break;
|
|
|
|
case nameof(ETransport.ws):
|
|
case nameof(ETransport.httpupgrade):
|
|
item.RequestHost = GetQueryDecoded(query, "host");
|
|
item.Path = GetQueryDecoded(query, "path", "/");
|
|
break;
|
|
|
|
case nameof(ETransport.xhttp):
|
|
item.RequestHost = GetQueryDecoded(query, "host");
|
|
item.Path = GetQueryDecoded(query, "path", "/");
|
|
item.HeaderType = GetQueryDecoded(query, "mode");
|
|
var extraDecoded = GetQueryDecoded(query, "extra");
|
|
if (extraDecoded.IsNotEmpty())
|
|
{
|
|
var node = JsonUtils.ParseJson(extraDecoded);
|
|
if (node != null)
|
|
{
|
|
extraDecoded = JsonUtils.Serialize(node, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
});
|
|
}
|
|
}
|
|
item.Extra = extraDecoded;
|
|
break;
|
|
|
|
case nameof(ETransport.http):
|
|
case nameof(ETransport.h2):
|
|
item.Network = nameof(ETransport.h2);
|
|
item.RequestHost = GetQueryDecoded(query, "host");
|
|
item.Path = GetQueryDecoded(query, "path", "/");
|
|
break;
|
|
|
|
case nameof(ETransport.quic):
|
|
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
|
|
item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None);
|
|
item.Path = GetQueryDecoded(query, "key");
|
|
break;
|
|
|
|
case nameof(ETransport.grpc):
|
|
item.RequestHost = GetQueryDecoded(query, "authority");
|
|
item.Path = GetQueryDecoded(query, "serviceName");
|
|
item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
protected static bool Contains(string str, params string[] s)
|
|
{
|
|
return s.All(item => str.Contains(item, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
protected static string WriteAllText(string strData, string ext = "json")
|
|
{
|
|
var fileName = Utils.GetTempPath($"{Utils.GetGuid(false)}.{ext}");
|
|
File.WriteAllText(fileName, strData);
|
|
return fileName;
|
|
}
|
|
|
|
protected static string ToUri(EConfigType eConfigType, string address, object port, string userInfo, Dictionary<string, string>? dicQuery, string? remark)
|
|
{
|
|
var query = dicQuery != null
|
|
? ("?" + string.Join("&", dicQuery.Select(x => x.Key + "=" + x.Value).ToArray()))
|
|
: string.Empty;
|
|
|
|
var url = $"{Utils.UrlEncode(userInfo)}@{GetIpv6(address)}:{port}";
|
|
return $"{Global.ProtocolShares[eConfigType]}{url}{query}{remark}";
|
|
}
|
|
|
|
protected static string GetQueryValue(NameValueCollection query, string key, string defaultValue = "")
|
|
{
|
|
return query[key] ?? defaultValue;
|
|
}
|
|
|
|
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
|
|
{
|
|
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
|
|
}
|
|
}
|