fix(hysteria2): default the port to 443 when the share URI omits it (#10026)

The Hysteria2 URI scheme makes the port optional: "The hostname and
optional port of the server. If the port is omitted, it defaults to 443."
`Hysteria2Fmt.Resolve` assigned `url.Port` straight through, and
`System.Uri` answers -1 for an unregistered scheme that carries no port,
so `hysteria2://password@hy2.example/` imported as a profile with
`Port = -1`. `ProfileItem.IsValid` rejects any port outside 1..65535, so
such a link produced a profile that could never be used, and nothing said
why.

-1 is the only value that means "the port was omitted"; a ':' with no
digits after it maps to -1 as well. An explicit ":0" parses as 0 and
keeps the fate it has today - rejected by `IsValid` - rather than being
redirected to a server the link never named.

`ResolveRealm` takes its port from `HyRealm.RendezvousPort` instead of
the URI, so it is unaffected.

The added tests cover both spellings of the scheme, with and without a
trailing slash, a bare ':', and the resulting profile's validity. Two of
them are controls: an explicit port is still preserved, and an explicit
":0" still does not turn into 443.
This commit is contained in:
Miheichev Aleksandr Sergeevich
2026-08-28 06:33:05 +00:00
committed by GitHub
parent 4b412e246f
commit f332caf507
2 changed files with 59 additions and 1 deletions

View File

@@ -0,0 +1,54 @@
namespace ServiceLib.Tests.Fmt;
public class Hysteria2FmtTests
{
// "The hostname and optional port of the server. If the port is omitted, it defaults to 443."
// -- https://v2.hysteria.network/docs/developers/URI-Scheme/
// A ':' with no digits after it is an omitted port too, per RFC 3986 'port = *DIGIT'.
[Test]
[Arguments("hysteria2://password@hy2.example/")]
[Arguments("hysteria2://password@hy2.example")]
[Arguments("hysteria2://password@hy2.example:/")]
[Arguments("hy2://password@hy2.example/?sni=real.example")]
public async Task ResolveConfig_WithoutPort_ShouldDefaultTo443(string shareUri)
{
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Hysteria2);
await resolved.Address.Should().BeEqualTo("hy2.example");
await resolved.Port.Should().BeEqualTo(443);
}
[Test]
public async Task ResolveConfig_WithoutPort_ShouldProduceAValidProfile()
{
// Uri.Port is -1 for an unregistered scheme with no port, and ProfileItem.IsValid rejects
// any port outside 1..65535 - so the default is what keeps such a link usable at all.
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example/", out _);
await resolved.Should().NotBeNull();
await resolved!.IsValid().Should().BeTrue();
}
[Test]
public async Task ResolveConfig_WithExplicitPort_ShouldKeepIt()
{
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:8443/", out _);
await resolved.Should().NotBeNull();
await resolved!.Port.Should().BeEqualTo(8443);
}
[Test]
public async Task ResolveConfig_WithExplicitZeroPort_ShouldNotApplyTheDefault()
{
// Uri.Port is 0 here, not -1: the port is present, it is just not a usable one. Treating
// it as "omitted" would silently move the endpoint to :443, so it stays invalid instead.
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:0/", out _);
await resolved.Should().NotBeNull();
await resolved!.Port.Should().BeEqualTo(0);
await resolved.IsValid().Should().BeFalse();
}
}

View File

@@ -17,7 +17,11 @@ public class Hysteria2Fmt : BaseFmt
}
item.Address = url.IdnHost;
item.Port = url.Port;
// The URI scheme makes the port optional and defaults it to 443. Uri.Port answers -1 for
// an unregistered scheme carrying no port, which ProfileItem.IsValid then rejects.
// Only -1 means "omitted": an explicit ":0" has to stay 0 and be rejected the way it
// always was, instead of being quietly redirected to a server the link never named.
item.Port = url.Port == -1 ? 443 : url.Port;
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
item.Password = Utils.UrlDecode(url.UserInfo);