Files
v2rayN/v2rayN/ServiceLib/Handler/Fmt/NaiveFmt.cs
T
Wei-Lun Li 2010477f38 fix(fmt): keep a ':' in a Naive password through export and import (#10526)
NaiveFmt.ToUri percent-encodes the username and password, so a
password-only profile with the password a:b is exported as
naive+https://a%3Ab@host. Resolve decoded the user info before
looking for the ':' separator and imported it as username "a",
password "b". Split the raw user info first and decode each part.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:13:47 +08:00

96 lines
3.2 KiB
C#

namespace ServiceLib.Handler.Fmt;
public class NaiveFmt : BaseFmt
{
public static ProfileItem? Resolve(string str, out string msg)
{
msg = ResUI.ConfigurationFormatIncorrect;
var parsedUrl = Utils.TryUri(str);
if (parsedUrl == null)
{
return null;
}
ProfileItem item = new()
{
ConfigType = EConfigType.Naive,
Remarks = parsedUrl.GetComponents(UriComponents.Fragment, UriFormat.Unescaped),
Address = parsedUrl.IdnHost,
// naive+https:// and naive+quic:// links leave the port out when it is 443
// (https://gist.github.com/DuckSoft/ca03913b0a26fc77a1da4d01cc6ab2f1). Uri.Port answers
// -1 for an unregistered scheme carrying no port; an explicit ":0" stays 0.
Port = parsedUrl.Port == -1 ? 443 : parsedUrl.Port,
};
var protocolExtra = item.GetProtocolExtra();
if (parsedUrl.Scheme.Contains("quic"))
{
protocolExtra = protocolExtra with
{
NaiveQuic = true,
};
}
// Split before decoding: ToUri percent-encodes a ':' inside the username or password.
var rawUserInfo = parsedUrl.UserInfo;
if (rawUserInfo.Contains(':'))
{
var split = rawUserInfo.Split(':', 2);
item.Username = Utils.UrlDecode(split[0]);
item.Password = Utils.UrlDecode(split[1]);
}
else
{
item.Password = Utils.UrlDecode(rawUserInfo);
}
var query = Utils.ParseQueryString(parsedUrl.Query);
ResolveUriQuery(query, ref item);
var insecureConcurrency = int.TryParse(GetQueryValue(query, "insecure-concurrency"), out var ic) ? ic : 0;
if (insecureConcurrency > 0)
{
protocolExtra = protocolExtra with
{
InsecureConcurrency = insecureConcurrency,
};
}
item.SetProtocolExtra(protocolExtra);
return item;
}
public static string? ToUri(ProfileItem? item)
{
if (item == null)
{
return null;
}
var remark = string.Empty;
if (item.Remarks.IsNotEmpty())
{
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var userInfo = item.Username.IsNotEmpty() ? $"{Utils.UrlEncode(item.Username)}:{Utils.UrlEncode(item.Password)}" : Utils.UrlEncode(item.Password);
var dicQuery = new Dictionary<string, string>();
ToUriQuery(item, Global.None, ref dicQuery);
var protocolExtra = item.GetProtocolExtra();
if (protocolExtra.InsecureConcurrency > 0)
{
dicQuery.Add("insecure-concurrency", protocolExtra?.InsecureConcurrency.ToString());
}
var query = dicQuery.Count > 0
? ("?" + string.Join("&", dicQuery.Select(x => x.Key + "=" + x.Value).ToArray()))
: string.Empty;
var url = $"{userInfo}@{GetIpv6(item.Address)}:{item.Port}";
if (protocolExtra.NaiveQuic == true)
{
return $"{Global.NaiveQuicProtocolShare}{url}{query}{remark}";
}
else
{
return $"{Global.NaiveHttpsProtocolShare}{url}{query}{remark}";
}
}
}