diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs index 75d70920..db149f31 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs @@ -817,4 +817,29 @@ public class CoreConfigSingboxServiceTests await proxyOutbound.method.Should().BeEqualTo("aes-128-gcm"); await proxyOutbound.password.Should().BeEqualTo("custom_password"); } + + [Test] + [Arguments("/ws?ed=2048", "/ws")] + [Arguments("/ws?ed=2048&key=value", "/ws?key=value")] + [Arguments("/ws?key=value&ed=2048", "/ws?key=value")] + [Arguments("/ws?a=1&ed=2048&b=2", "/ws?a=1&b=2")] + public async Task GenerateClientConfigContent_WsEarlyDataInPath_ShouldKeepRemainingQuery(string path, string expectedPath) + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + CoreConfigTestFactory.BindAppManagerConfig(config); + var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box); + node.Network = nameof(ETransport.ws); + node.SetTransportExtra(node.GetTransportExtra() with { Path = path }); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box); + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var transport = cfg.outbounds.First(o => o.tag == Global.ProxyTag).transport; + await transport.Should().NotBeNull(); + await transport!.max_early_data.Should().BeEqualTo(2048); + await transport.early_data_header_name.Should().BeEqualTo("Sec-WebSocket-Protocol"); + await transport.path.Should().BeEqualTo(expectedPath); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 2ef891e8..7cbea305 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -542,7 +542,9 @@ public partial class CoreConfigSingboxService // Parse eh and ed parameters from path using regex if (!wsPath.IsNullOrEmpty()) { - var edRegex = new Regex(@"[?&]ed=(\d+)"); + // Keep the '?' or '&' in front of "ed" out of the match, so removing it + // leaves "/ws?ed=2048&key=value" as "/ws?key=value", not "/ws&key=value". + var edRegex = new Regex(@"(?<=[?&])ed=(\d+)&?"); var edMatch = edRegex.Match(wsPath); if (edMatch.Success && int.TryParse(edMatch.Groups[1].Value, out var edValue)) { @@ -550,11 +552,7 @@ public partial class CoreConfigSingboxService transport.early_data_header_name = "Sec-WebSocket-Protocol"; wsPath = edRegex.Replace(wsPath, ""); - wsPath = wsPath.Replace("?&", "?"); - if (wsPath.EndsWith('?')) - { - wsPath = wsPath.TrimEnd('?'); - } + wsPath = wsPath.TrimEnd('&').TrimEnd('?'); } var ehRegex = new Regex(@"[?&]eh=([^&]+)");